21 Commits
0.3.0 ... 0.3.1

Author SHA1 Message Date
Einar Forselv
fab988a05e status command should display repository 2019-12-05 12:57:15 +01:00
Einar Forselv
164834d3a9 bug: cleanup should return integer exit code 2019-12-05 12:55:13 +01:00
Einar Forselv
a0dfb04aa7 Run repo init in status command 2019-12-05 12:54:34 +01:00
Einar Forselv
7f588c57ab bug: forget and prune was never executed 2019-12-05 12:42:21 +01:00
Einar Forselv
e01f7c6cff Update README.md 2019-12-05 12:33:24 +01:00
Einar Forselv
102073cb70 Bug: Do not refer to old tag 2019-12-05 12:30:50 +01:00
Einar Forselv
e060c28c93 Update README.md 2019-12-05 11:26:11 +01:00
Einar Forselv
14903f3bbd Fully working tox run 2019-12-05 11:23:33 +01:00
Einar Forselv
96bd419a24 Move tests into src 2019-12-05 11:23:14 +01:00
Einar Forselv
75ab549370 re-add pytest.ini 2019-12-05 11:15:14 +01:00
Einar Forselv
6f06d25db5 pep8 2019-12-05 11:09:36 +01:00
Einar Forselv
0a9e5edfe4 Add basic tox setup 2019-12-05 11:08:40 +01:00
Einar Forselv
130be30268 Move package to src/ to properly separate what goes into docker image 2019-12-05 10:26:46 +01:00
Einar Forselv
0af9f2e8ee Catch exceptions in backup_runner 2019-12-05 10:16:34 +01:00
Einar Forselv
c59f022a55 Properly log exceptions 2019-12-05 10:15:49 +01:00
Einar Forselv
98fe448348 Delete mail.py 2019-12-05 10:15:29 +01:00
Einar Forselv
3708bb9100 re-add provate alert creds 2019-12-05 10:15:09 +01:00
Einar Forselv
d7039cccf4 bump docker-py version 2019-12-05 09:59:52 +01:00
Einar Forselv
864c026402 backup_runner: stop container 2019-12-05 09:59:41 +01:00
Einar Forselv
fcd18ba1cb Update release.md 2019-12-05 09:59:24 +01:00
Einar Forselv
915695043c bump version 2019-12-05 02:48:24 +01:00
30 changed files with 130 additions and 92 deletions

3
.gitignore vendored
View File

@@ -24,3 +24,6 @@ alerts.env
# docs # docs
build/ build/
docs/_build docs/_build
# tests
.tox

View File

@@ -38,8 +38,6 @@ Automatically detects and backs up volumes, mysql, mariadb and postgres database
docker pull zettaio/restic-compose-backup docker pull zettaio/restic-compose-backup
``` ```
.. or clone this repo and build it.
## Configuration ## Configuration
Required env variables for restic: Required env variables for restic:
@@ -81,7 +79,7 @@ version: '3'
services: services:
# The backup service # The backup service
backup: backup:
build: restic-compose-backup image: zettaio/restic-compose-backup
environment: environment:
- RESTIC_REPOSITORY=<whatever restic supports> - RESTIC_REPOSITORY=<whatever restic supports>
- RESTIC_PASSWORD=hopefullyasecturepw - RESTIC_PASSWORD=hopefullyasecturepw
@@ -178,20 +176,19 @@ path `/databases/<service_name>/dump.sql` or similar.
restic-compose-backup.postgres: true restic-compose-backup.postgres: true
``` ```
## Running Tests ## Running Tests
```bash ```bash
python setup.py develop pip install -e src/
pip install -r tests/requirements.txt pip install -r src/tests/requirements.txt
pytest tests tox
``` ```
## Building Docs ## Building Docs
```bash ```bash
pip install -r docs/requirements.txt pip install -r docs/requirements.txt
python setup.py build_sphinx python src/setup.py build_sphinx
``` ```
## Contributing ## Contributing

View File

@@ -1,9 +1,10 @@
version: '3' version: '3'
services: services:
backup: backup:
build: . build: ./src
env_file: env_file:
- restic_compose_backup.env - restic_compose_backup.env
- alerts.env
volumes: volumes:
# Map in docker socket # Map in docker socket
- /var/run/docker.sock:/tmp/docker.sock:ro - /var/run/docker.sock:/tmp/docker.sock:ro
@@ -11,7 +12,7 @@ services:
- ./restic_data:/restic_data - ./restic_data:/restic_data
- ./restic_cache:/cache - ./restic_cache:/cache
# Map in project source in dev # Map in project source in dev
- .:/restic-compose-backup - ./src:/restic-compose-backup
web: web:
image: nginx image: nginx
labels: labels:

View File

@@ -22,7 +22,7 @@ copyright = '2019, Zetta.IO Technology AS'
author = 'Zetta.IO Technology AS' author = 'Zetta.IO Technology AS'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = '0.2.0' release = '0.3.0'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------

View File

@@ -1,13 +1,20 @@
# Making a release # Making a release
- Update version in setup.py - Update version in `setup.py`
- Update version in `docs/conf.py`
- Build and tag image - Build and tag image
- push: `docker push zettaio/restic-compose-backup:<version>` - push: `docker push zettaio/restic-compose-backup:<version>`
- Ensure RTD has new docs published - Ensure RTD has new docs published
## Example ## Example
When releasing a bugfix version we need to update the
main image as well.
```bash ```bash
docker build . --tag zettaio/restic-compose-backup:0.2.0 docker build . --tag zettaio/restic-compose-backup:0.3
docker push zettaio/restic-compose-backup:0.2.0 docker build . --tag zettaio/restic-compose-backup:0.3.1
docker push zettaio/restic-compose-backup:0.3
docker push zettaio/restic-compose-backup:0.3.1
``` ```

View File

@@ -1,4 +1,4 @@
[pytest] [pytest]
testpaths = tests testpaths = src/tests
python_files=test*.py python_files=test*.py
addopts = -v --verbose addopts = -v --verbose

View File

@@ -1,37 +0,0 @@
"""
"""
import smtplib
from email.mime.text import MIMEText
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 465
EMAIL_HOST_USER = ""
EMAIL_HOST_PASSWORD = ""
EMAIL_SEND_TO = ['']
def main():
send_mail("Hello world!")
def send_mail(text):
msg = MIMEText(text)
msg['Subject'] = "Message from restic-compose-backup"
msg['From'] = EMAIL_HOST_USER
msg['To'] = ', '.join(EMAIL_SEND_TO)
try:
print("Connecting to {} port {}".format(EMAIL_HOST, EMAIL_PORT))
server = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT)
server.ehlo()
server.login(EMAIL_HOST_USER, EMAIL_HOST_PASSWORD)
server.sendmail(EMAIL_HOST_USER, EMAIL_SEND_TO, msg.as_string())
print('Email Sent')
except Exception as e:
print(e)
finally:
server.close()
if __name__ == '__main__':
main()

View File

@@ -2,7 +2,6 @@ import logging
from restic_compose_backup.alerts.smtp import SMTPAlert from restic_compose_backup.alerts.smtp import SMTPAlert
from restic_compose_backup.alerts.discord import DiscordWebhookAlert from restic_compose_backup.alerts.discord import DiscordWebhookAlert
from restic_compose_backup.config import Config
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -24,6 +23,7 @@ def send(subject: str = None, body: str = None, alert_type: str = 'INFO'):
) )
except Exception as ex: except Exception as ex:
logger.error("Exception raised when sending alert [%s]: %s", instance.name, ex) logger.error("Exception raised when sending alert [%s]: %s", instance.name, ex)
logger.exception(ex)
if len(alert_classes) == 0: if len(alert_classes) == 0:
logger.info("No alerts configured") logger.info("No alerts configured")
@@ -36,7 +36,7 @@ def configured_alert_types():
for cls in BACKENDS: for cls in BACKENDS:
instance = cls.create_from_env() instance = cls.create_from_env()
logger.debug("Alert backend '%s' configured: %s", cls.name, instance != None) logger.debug("Alert backend '%s' configured: %s", cls.name, instance is not None)
if instance: if instance:
entires.append(instance) entires.append(instance)

View File

@@ -1,6 +1,5 @@
import os import os
import logging import logging
from urllib.parse import urlparse
import requests import requests
from restic_compose_backup.alerts.base import BaseAlert from restic_compose_backup.alerts.base import BaseAlert
@@ -41,6 +40,6 @@ class DiscordWebhookAlert(BaseAlert):
} }
response = requests.post(self.url, params={'wait': True}, json=data) response = requests.post(self.url, params={'wait': True}, json=data)
if response.status_code not in self.success_codes: if response.status_code not in self.success_codes:
log.error("Discord webhook failed: %s: %s", response.status_code, response.content) logger.error("Discord webhook failed: %s: %s", response.status_code, response.content)
else: else:
logger.info('Discord webhook successful') logger.info('Discord webhook successful')

View File

@@ -51,6 +51,6 @@ class SMTPAlert(BaseAlert):
server.sendmail(self.user, self.to, msg.as_string()) server.sendmail(self.user, self.to, msg.as_string())
logger.info('Email sent') logger.info('Email sent')
except Exception as ex: except Exception as ex:
logger.error(ex) logger.exception(ex)
finally: finally:
server.close() server.close()

View File

@@ -51,9 +51,9 @@ def run(image: str = None, command: str = None, volumes: dict = None,
fd.write('\n') fd.write('\n')
logger.info(line) logger.info(line)
container.reload() container.reload()
logger.debug("Container ExitCode %s", container.attrs['State']['ExitCode']) logger.debug("Container ExitCode %s", container.attrs['State']['ExitCode'])
container.stop()
container.remove() container.remove()
return container.attrs['State']['ExitCode'] return container.attrs['State']['ExitCode']

View File

@@ -1,5 +1,4 @@
import argparse import argparse
import pprint
import logging import logging
from restic_compose_backup import ( from restic_compose_backup import (
@@ -47,9 +46,13 @@ def main():
def status(config, containers): def status(config, containers):
"""Outputs the backup config for the compose setup""" """Outputs the backup config for the compose setup"""
logger.info("Status for compose project '%s'", containers.project_name) logger.info("Status for compose project '%s'", containers.project_name)
logger.info("Repository: '%s'", config.repository)
logger.info("Backup currently running?: %s", containers.backup_process_running) logger.info("Backup currently running?: %s", containers.backup_process_running)
logger.info("%s Detected Config %s", "-" * 25, "-" * 25) logger.info("%s Detected Config %s", "-" * 25, "-" * 25)
logger.info("Initializing repository (may fail if already initalized)")
restic.init_repo(config.repository)
backup_containers = containers.containers_for_backup() backup_containers = containers.containers_for_backup()
for container in backup_containers: for container in backup_containers:
logger.info('service: %s', container.service_name) logger.info('service: %s', container.service_name)
@@ -63,10 +66,11 @@ def status(config, containers):
ping = instance.ping() ping = instance.ping()
logger.info(' - %s (is_ready=%s)', instance.container_type, ping == 0) logger.info(' - %s (is_ready=%s)', instance.container_type, ping == 0)
if ping != 0: if ping != 0:
logger.error("Database '%s' in service %s cannot be reached", instance.container_type, container.service_name) logger.error("Database '%s' in service %s cannot be reached",
instance.container_type, container.service_name)
if len(backup_containers) == 0: if len(backup_containers) == 0:
logger.info("No containers in the project has 'restic-compose-backup.enabled' label") logger.info("No containers in the project has 'restic-compose-backup.*' label")
logger.info("-" * 67) logger.info("-" * 67)
@@ -77,11 +81,6 @@ def backup(config, containers):
if containers.backup_process_running: if containers.backup_process_running:
raise ValueError("Backup process already running") raise ValueError("Backup process already running")
logger.info("Initializing repository (may fail if already initalized)")
# TODO: Errors when repo already exists
restic.init_repo(config.repository)
# Map all volumes from the backup container into the backup process container # Map all volumes from the backup container into the backup process container
volumes = containers.this_container.volumes volumes = containers.this_container.volumes
@@ -89,6 +88,7 @@ def backup(config, containers):
mounts = containers.generate_backup_mounts('/volumes') mounts = containers.generate_backup_mounts('/volumes')
volumes.update(mounts) volumes.update(mounts)
try:
result = backup_runner.run( result = backup_runner.run(
image=containers.this_container.image, image=containers.this_container.image,
command='restic-compose-backup start-backup-process', command='restic-compose-backup start-backup-process',
@@ -100,6 +100,15 @@ def backup(config, containers):
"com.docker.compose.project": containers.project_name, "com.docker.compose.project": containers.project_name,
}, },
) )
except Exception as ex:
logger.exception(ex)
alerts.send(
subject="Exception during backup",
body=str(ex),
alert_type='ERROR',
)
return
logger.info('Backup container exit code: %s', result) logger.info('Backup container exit code: %s', result)
# Alert the user if something went wrong # Alert the user if something went wrong
@@ -133,7 +142,7 @@ def start_backup_process(config, containers):
logger.error('Backup command exited with non-zero code: %s', vol_result) logger.error('Backup command exited with non-zero code: %s', vol_result)
errors = True errors = True
except Exception as ex: except Exception as ex:
logger.error(ex) logger.exception(ex)
errors = True errors = True
# back up databases # back up databases
@@ -148,7 +157,7 @@ def start_backup_process(config, containers):
logger.error('Backup command exited with non-zero code: %s', result) logger.error('Backup command exited with non-zero code: %s', result)
errors = True errors = True
except Exception as ex: except Exception as ex:
logger.error(ex) logger.exception(ex)
errors = True errors = True
if errors: if errors:
@@ -173,7 +182,8 @@ def cleanup(config, containers):
) )
logger.info('Prune stale data freeing storage space') logger.info('Prune stale data freeing storage space')
prune_result = restic.prune(config.repository) prune_result = restic.prune(config.repository)
return forget_result == 0 and prune_result == 0 return forget_result and prune_result
def snapshots(config, containers): def snapshots(config, containers):
"""Display restic snapshots""" """Display restic snapshots"""

View File

@@ -6,7 +6,7 @@ logger = logging.getLogger(__name__)
def test(): def test():
return run_command(['ls', '/volumes']) return run(['ls', '/volumes'])
def ping_mysql(host, port, username) -> int: def ping_mysql(host, port, username) -> int:
@@ -23,7 +23,7 @@ def ping_mysql(host, port, username) -> int:
]) ])
def ping_mariadb(host, port, username): #, password) -> int: def ping_mariadb(host, port, username) -> int:
"""Check if the mariadb is up and can be reached""" """Check if the mariadb is up and can be reached"""
return run([ return run([
'mysqladmin', 'mysqladmin',

View File

@@ -13,6 +13,7 @@ LOG_LEVELS = {
'error': logging.ERROR, 'error': logging.ERROR,
} }
def setup(level: str = 'warning'): def setup(level: str = 'warning'):
"""Set up logging""" """Set up logging"""
level = level or "" level = level or ""

View File

@@ -68,7 +68,7 @@ def snapshots(repository: str, last=True) -> Tuple[str, str]:
def forget(repository: str, daily: str, weekly: str, monthly: str, yearly: str): def forget(repository: str, daily: str, weekly: str, monthly: str, yearly: str):
return restic(repository, [ return commands.run(restic(repository, [
'forget', 'forget',
'--keep-daily', '--keep-daily',
daily, daily,
@@ -78,13 +78,13 @@ def forget(repository: str, daily: str, weekly: str, monthly: str, yearly: str):
monthly, monthly,
'--keep-yearly', '--keep-yearly',
yearly, yearly,
]) ]))
def prune(repository: str): def prune(repository: str):
return restic(repository, [ return commands.run(restic(repository, [
'prune', 'prune',
]) ]))
def check(repository: str): def check(repository: str):

View File

@@ -8,7 +8,7 @@ setup(
author_email="eforselv@gmail.com", author_email="eforselv@gmail.com",
packages=find_namespace_packages(include=['restic_compose_backup']), packages=find_namespace_packages(include=['restic_compose_backup']),
install_requires=[ install_requires=[
'docker==3.7.2', 'docker==4.1.*',
], ],
entry_points={'console_scripts': [ entry_points={'console_scripts': [
'restic-compose-backup = restic_compose_backup.cli:main', 'restic-compose-backup = restic_compose_backup.cli:main',

View File

@@ -1 +1,2 @@
pytest==4.3.1 pytest==4.3.1
tox

56
tox.ini Normal file
View File

@@ -0,0 +1,56 @@
# Ensure that this file do not contain non-ascii characters
# as flake8 can fail to parse the file on OS X and Windows
[tox]
skipsdist = True
setupdir={toxinidir}/src
envlist =
py37
pep8
[testenv]
usedevelop = True
basepython =
py37: python3.7
deps =
-r{toxinidir}/tests/requirements.txt
commands =
; coverage run --source=restic_compose_backup -m pytest tests/
; coverage report
pytest
[testenv:pep8]
usedevelop = false
deps = flake8
basepython = python3.7
commands = flake8
[pytest]
norecursedirs = tests/* .venv/* .tox/* build/ docs/
[flake8]
# H405: multi line docstring summary not separated with an empty line
# D100: Missing docstring in public module
# D101: Missing docstring in public class
# D102: Missing docstring in public method
# D103: Missing docstring in public function
# D104: Missing docstring in public package
# D105: Missing docstring in magic method
# D200: One-line docstring should fit on one line with quotes
# D202: No blank lines allowed after function docstring
# D203: 1 blank required before class docstring.
# D204: 1 blank required after class docstring
# D205: Blank line required between one-line summary and description.
# D207: Docstring is under-indented
# D208: Docstring is over-indented
# D211: No blank lines allowed before class docstring
# D301: Use r""" if any backslashes in a docstring
# D400: First line should end with a period.
# D401: First line should be in imperative mood.
# *** E302 expected 2 blank lines, found 1
# *** W503 line break before binary operator
ignore = H405,D100,D101,D102,D103,D104,D105,D200,D202,D203,D204,D205,D211,D301,D400,D401,W503
show-source = True
max-line-length = 120
exclude = .tox,.venv*,tests,build,conf.py