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
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
```
.. or clone this repo and build it.
## Configuration
Required env variables for restic:
@@ -81,7 +79,7 @@ version: '3'
services:
# The backup service
backup:
build: restic-compose-backup
image: zettaio/restic-compose-backup
environment:
- RESTIC_REPOSITORY=<whatever restic supports>
- RESTIC_PASSWORD=hopefullyasecturepw
@@ -178,22 +176,21 @@ path `/databases/<service_name>/dump.sql` or similar.
restic-compose-backup.postgres: true
```
## Running Tests
```bash
python setup.py develop
pip install -r tests/requirements.txt
pytest tests
pip install -e src/
pip install -r src/tests/requirements.txt
tox
```
## Building Docs
```bash
pip install -r docs/requirements.txt
python setup.py build_sphinx
python src/setup.py build_sphinx
```
## Contributing
Contributions are welcome regardless of experience level. Don't hesitate submitting issues, opening partial or completed pull requests.
Contributions are welcome regardless of experience level. Don't hesitate submitting issues, opening partial or completed pull requests.

View File

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

View File

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

View File

@@ -1,13 +1,20 @@
# Making a release
- Update version in setup.py
- Update version in `setup.py`
- Update version in `docs/conf.py`
- Build and tag image
- push: `docker push zettaio/restic-compose-backup:<version>`
- Ensure RTD has new docs published
## Example
When releasing a bugfix version we need to update the
main image as well.
```bash
docker build . --tag zettaio/restic-compose-backup:0.2.0
docker push zettaio/restic-compose-backup:0.2.0
docker build . --tag zettaio/restic-compose-backup:0.3
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]
testpaths = tests
testpaths = src/tests
python_files=test*.py
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.discord import DiscordWebhookAlert
from restic_compose_backup.config import Config
logger = logging.getLogger(__name__)
@@ -24,6 +23,7 @@ def send(subject: str = None, body: str = None, alert_type: str = 'INFO'):
)
except Exception as ex:
logger.error("Exception raised when sending alert [%s]: %s", instance.name, ex)
logger.exception(ex)
if len(alert_classes) == 0:
logger.info("No alerts configured")
@@ -36,7 +36,7 @@ def configured_alert_types():
for cls in BACKENDS:
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:
entires.append(instance)

View File

@@ -1,6 +1,5 @@
import os
import logging
from urllib.parse import urlparse
import requests
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)
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:
logger.info('Discord webhook successful')

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import argparse
import pprint
import logging
from restic_compose_backup import (
@@ -47,9 +46,13 @@ def main():
def status(config, containers):
"""Outputs the backup config for the compose setup"""
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("%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()
for container in backup_containers:
logger.info('service: %s', container.service_name)
@@ -63,10 +66,11 @@ def status(config, containers):
ping = instance.ping()
logger.info(' - %s (is_ready=%s)', instance.container_type, 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:
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)
@@ -77,11 +81,6 @@ def backup(config, containers):
if containers.backup_process_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
volumes = containers.this_container.volumes
@@ -89,17 +88,27 @@ def backup(config, containers):
mounts = containers.generate_backup_mounts('/volumes')
volumes.update(mounts)
result = backup_runner.run(
image=containers.this_container.image,
command='restic-compose-backup start-backup-process',
volumes=volumes,
environment=containers.this_container.environment,
source_container_id=containers.this_container.id,
labels={
"restic-compose-backup.backup_process": 'True',
"com.docker.compose.project": containers.project_name,
},
)
try:
result = backup_runner.run(
image=containers.this_container.image,
command='restic-compose-backup start-backup-process',
volumes=volumes,
environment=containers.this_container.environment,
source_container_id=containers.this_container.id,
labels={
"restic-compose-backup.backup_process": 'True',
"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)
# 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)
errors = True
except Exception as ex:
logger.error(ex)
logger.exception(ex)
errors = True
# back up databases
@@ -148,7 +157,7 @@ def start_backup_process(config, containers):
logger.error('Backup command exited with non-zero code: %s', result)
errors = True
except Exception as ex:
logger.error(ex)
logger.exception(ex)
errors = True
if errors:
@@ -173,7 +182,8 @@ def cleanup(config, containers):
)
logger.info('Prune stale data freeing storage space')
prune_result = restic.prune(config.repository)
return forget_result == 0 and prune_result == 0
return forget_result and prune_result
def snapshots(config, containers):
"""Display restic snapshots"""

View File

@@ -6,7 +6,7 @@ logger = logging.getLogger(__name__)
def test():
return run_command(['ls', '/volumes'])
return run(['ls', '/volumes'])
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"""
return run([
'mysqladmin',
@@ -43,7 +43,7 @@ def ping_postgres(host, port, username, password) -> int:
"pg_isready",
f"--host={host}",
f"--port={port}",
f"--username={username}",
f"--username={username}",
])

View File

@@ -68,7 +68,7 @@ class Container:
def get_config_env(self, name) -> str:
"""Get a config environment variable by name"""
# convert to dict and fetch env var by name
data = {i[0:i.find('=')]: i[i.find('=')+1:] for i in self.environment}
data = {i[0:i.find('=')]: i[i.find('=') + 1:] for i in self.environment}
return data.get(name)
def set_config_env(self, name, value):
@@ -169,7 +169,7 @@ class Container:
return self._labels.get(name, None)
def filter_mounts(self):
"""Get all mounts for this container matching include/exclude filters"""
"""Get all mounts for this container matching include/exclude filters"""
filtered = []
if not self.volume_backup_enabled:

View File

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

View File

@@ -63,12 +63,12 @@ def backup_from_stdin(repository: str, filename: str, source_command: List[str])
def snapshots(repository: str, last=True) -> Tuple[str, str]:
args = ["snapshots"]
if last:
args.append('--last')
args.append('--last')
return commands.run_capture_std(restic(repository, args))
def forget(repository: str, daily: str, weekly: str, monthly: str, yearly: str):
return restic(repository, [
return commands.run(restic(repository, [
'forget',
'--keep-daily',
daily,
@@ -78,13 +78,13 @@ def forget(repository: str, daily: str, weekly: str, monthly: str, yearly: str):
monthly,
'--keep-yearly',
yearly,
])
]))
def prune(repository: str):
return restic(repository, [
return commands.run(restic(repository, [
'prune',
])
]))
def check(repository: str):

View File

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

View File

@@ -1 +1,2 @@
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