Skip to content

Commit d694ab5

Browse files
authored
Merge pull request #44 from cybertec-postgresql/CON-878
CON-878
2 parents 97cab68 + 489fbf3 commit d694ab5

6 files changed

Lines changed: 134 additions & 9 deletions

File tree

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ pgbackrest-build:
7171
pgbackrest: pgbackrest-build;
7272

7373
postgres-build:
74+
7475
docker build $(ROOTPATH) \
7576
--file $(ROOTPATH)/docker/postgres/Dockerfile \
7677
--tag cybertec-pg-container/postgres:$(IMAGE_TAG) \
@@ -86,7 +87,8 @@ postgres-build:
8687
--build-arg OLD_PG_VERSIONS="$(OLD_PG_VERSIONS)" \
8788
--build-arg PGVERSION=$(PGVERSION) \
8889
--build-arg ETCD_VERSION=$(ETCD_VERSION) \
89-
--build-arg ARCH=$(ARCH)
90+
--build-arg PGVERSION=$(PGVERSION) \
91+
--build-arg ARCH=$(ARCH)
9092

9193
postgres: postgres-build
9294

bootstrap/clone_with_pgbackrest.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import argparse
2+
import logging
3+
import os
4+
import subprocess
5+
import sys
6+
7+
from collections import namedtuple
8+
from dateutil.parser import parse
9+
10+
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.INFO)
11+
logger = logging.getLogger(__name__)
12+
13+
def read_configuration():
14+
parser = argparse.ArgumentParser(description="Script to clone using pgbackrest. ")
15+
parser.add_argument('--scope', required=True, help='target cluster name')
16+
parser.add_argument('--datadir', required=True, help='target cluster postgres data directory')
17+
parser.add_argument('--recovery-target-time',
18+
help='the timestamp up to which recovery will proceed (including time zone)',
19+
dest='recovery_target_time_string')
20+
parser.add_argument('--dry-run', action='store_true', help='find a matching backup and build the wal-e '
21+
'command to fetch that backup without running it')
22+
args = parser.parse_args()
23+
24+
options = namedtuple('Options', 'name datadir recovery_target_time dry_run')
25+
if args.recovery_target_time_string:
26+
recovery_target_time = parse(args.recovery_target_time_string)
27+
if recovery_target_time.tzinfo is None:
28+
raise Exception("recovery target time must contain a timezone")
29+
else:
30+
recovery_target_time = None
31+
32+
return options(args.scope, args.datadir, recovery_target_time, args.dry_run)
33+
34+
def run_clone_from_pgbackrest(options):
35+
env = os.environ.copy()
36+
37+
pg_path_argument = "--pg1-path={0}".format(options.datadir)
38+
39+
if options.recovery_target_time:
40+
target_time_argument = "--target={0}".format(options.recovery_target_time)
41+
pgbackrest_command = ['/usr/bin/pgbackrest', '--stanza=db', '--type=time', target_time_argument, 'restore', pg_path_argument]
42+
else:
43+
pgbackrest_command = ['/usr/bin/pgbackrest', '--stanza=db', 'restore', pg_path_argument]
44+
45+
logger.info("cloning cluster %s using %s", options.name, ' '.join(pgbackrest_command))
46+
47+
if not options.dry_run:
48+
ret = subprocess.call(pgbackrest_command, env=env)
49+
if ret != 0:
50+
raise Exception("pgbackrest restore exited with exit code {0}".format(ret))
51+
52+
return 0
53+
54+
def main():
55+
options = read_configuration()
56+
try:
57+
run_clone_from_pgbackrest(options)
58+
except Exception:
59+
logger.exception("Clone with pgbackrest failed")
60+
return 1
61+
62+
if __name__ == '__main__':
63+
sys.exit(main())

cron_unprivileged.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#include <sys/types.h>
2+
3+
int setuid(uid_t euid)
4+
{
5+
return 0;
6+
}
7+
8+
int seteuid(uid_t euid)
9+
{
10+
return 0;
11+
}
12+
13+
int initgroups(const char *user, gid_t group)
14+
{
15+
return 0;
16+
}

docker/postgres/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ RUN ${PACKAGER} -y update && ${PACKAGER} -y install --nodocs --noplugins --setop
5656
dumb-init \
5757
libicu \
5858
pgbackrest-${PGBACKREST_VERSION} \
59+
cronie \
5960
&& ${PACKAGER} -y clean all;
6061

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

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

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

119122
RUN cd /package && tar -xvzf runit-2.1.2.tar.gz && rm runit-2.1.2.tar.gz \
120123
&& cd admin/runit-2.1.2 && package/install \

runit/cron/run

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
if [ "$(id -u)" -ne 0 ]; then
44
LD_PRELOAD=/usr/local/lib/cron_unprivileged.so
55
fi
6+
CROND_PATH=/usr/sbin/crond
67

78
exec 2>&1
8-
exec env -i LD_PRELOAD=$LD_PRELOAD /usr/sbin/cron -f
9+
# Check if the crond binary exists
10+
if [ -f "$CROND_PATH" ]; then
11+
# Execute the command if the file exists
12+
exec env -i LD_PRELOAD=$LD_PRELOAD $CROND_PATH -n
13+
else
14+
# Print a message or handle the case where the file does not exist
15+
echo "Error: $CROND_PATH does not exist (is cron enabled durring build time?). Command not executed."
16+
sv -w 86400 stop /etc/service/cron
17+
exit 1
18+
fi

scripts/configure_spilo.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,14 @@ def deep_update(a, b):
248248
recovery_target_inclusive: false
249249
{{/CLONE_TARGET_INCLUSIVE}}
250250
{{/CLONE_WITH_WALE}}
251+
{{#CLONE_WITH_PGBACKREST}}
252+
method: clone_with_pgbackrest
253+
clone_with_pgbackrest:
254+
command: python3 /scripts/clone_with_pgbackrest.py
255+
--recovery-target-time="{{CLONE_TARGET_TIME}}"
256+
recovery_conf:
257+
restore_command: pgbackrest --stanza=db archive-get %f "%p"
258+
{{/CLONE_WITH_PGBACKREST}}
251259
{{#CLONE_WITH_BASEBACKUP}}
252260
method: clone_with_basebackup
253261
clone_with_basebackup:
@@ -560,7 +568,8 @@ def get_placeholders(provider):
560568

561569
placeholders.setdefault('PGHOME', os.path.expanduser('~'))
562570
placeholders.setdefault('APIPORT', '8008')
563-
placeholders.setdefault('BACKUP_SCHEDULE', '0 1 * * *')
571+
placeholders.setdefault('BACKUP_SCHEDULE', '0 1 * * SAT')
572+
placeholders.setdefault('BACKUP_SCHEDULE_INCREMENTAL', '0 1 * * *')
564573
placeholders.setdefault('BACKUP_NUM_TO_RETAIN', '5')
565574
placeholders.setdefault('CRONTAB', '[]')
566575
placeholders.setdefault('PGROOT', os.path.join(placeholders['PGHOME'], 'pgroot'))
@@ -620,6 +629,7 @@ def get_placeholders(provider):
620629
placeholders.setdefault('USE_PAUSE_AT_RECOVERY_TARGET', False)
621630
placeholders.setdefault('CLONE_METHOD', '')
622631
placeholders.setdefault('CLONE_WITH_WALE', '')
632+
placeholders.setdefault('CLONE_WITH_PGBACKREST', '')
623633
placeholders.setdefault('CLONE_WITH_BASEBACKUP', '')
624634
placeholders.setdefault('CLONE_TARGET_TIME', '')
625635
placeholders.setdefault('CLONE_TARGET_INCLUSIVE', True)
@@ -1008,16 +1018,28 @@ def write_clone_pgpass(placeholders, overwrite):
10081018

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

10161029

10171030
def setup_crontab(user, lines):
10181031
lines += [''] # EOF requires empty line for cron
1019-
c = subprocess.Popen(['crontab', '-u', user, '-'], stdin=subprocess.PIPE)
1020-
c.communicate(input='\n'.join(lines).encode())
1032+
c = subprocess.Popen(['crontab', '-u', user, '-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1033+
stdout, stderr = c.communicate(input='\n'.join(lines).encode())
1034+
if stderr:
1035+
return logging.error('Error while adding a crontab: %s', stderr)
1036+
1037+
def setup_crontab_postgres(lines):
1038+
lines += [''] # EOF requires empty line for cron
1039+
c = subprocess.Popen(['crontab','-'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1040+
stdout, stderr = c.communicate(input='\n'.join(lines).encode())
1041+
if stderr:
1042+
return logging.error('Error while adding a crontab for user postgres: %s', stderr)
10211043

10221044

10231045
def setup_runit_cron(placeholders):
@@ -1063,6 +1085,12 @@ def write_crontab(placeholders, overwrite):
10631085
hash_dir = os.path.join(placeholders['RW_DIR'], 'tmp')
10641086
lines += ['*/5 * * * * {0} /scripts/test_reload_ssl.sh {1}'.format(env, hash_dir)]
10651087

1088+
1089+
if bool(placeholders.get('USE_PGBACKREST')) and not USE_KUBERNETES:
1090+
lines += [('{BACKUP_SCHEDULE} /usr/bin/pgbackrest --stanza=db --type=full backup').format(**placeholders)]
1091+
lines += [('{BACKUP_SCHEDULE_INCREMENTAL} /usr/bin/pgbackrest --stanza=db --type=incr backup').format(**placeholders)]
1092+
1093+
10661094
if bool(placeholders.get('USE_WALE')):
10671095
lines += [('{BACKUP_SCHEDULE} envdir "{WALE_ENV_DIR}" /scripts/postgres_backup.sh' +
10681096
' "{PGDATA}"').format(**placeholders)]
@@ -1077,7 +1105,10 @@ def write_crontab(placeholders, overwrite):
10771105
setup_runit_cron(placeholders)
10781106

10791107
if len(lines) > 1 and (overwrite or check_crontab('postgres')):
1080-
setup_crontab('postgres', lines)
1108+
try:
1109+
setup_crontab_postgres(lines)
1110+
except:
1111+
logging.error('Unable to add crontab, is cron as service enabled during build? ')
10811112

10821113
if root_lines and (overwrite or check_crontab('root')):
10831114
setup_crontab('root', root_lines)

0 commit comments

Comments
 (0)