restic-compose-backup/restic_volume_backup/containers.py

231 lines
7.0 KiB
Python
Raw Normal View History

2019-04-13 17:04:54 +00:00
import os
import docker
import json
2019-04-15 14:16:06 +00:00
import pprint
2019-04-13 17:04:54 +00:00
from restic_volume_backup import utils
2019-04-13 17:04:54 +00:00
VOLUME_TYPE_BIND = "bind"
VOLUME_TYPE_VOLUME = "volume"
2019-04-13 21:19:34 +00:00
2019-04-13 17:04:54 +00:00
class Container:
"""Represents a docker container"""
2019-04-13 17:04:54 +00:00
def __init__(self, data):
self._data = data
self.id = data['Id']
self.name = data['Name']
2019-04-13 17:04:54 +00:00
self._state = data.get('State')
self._config = data.get('Config')
self._mounts = [Mount(mnt, container=self) for mnt in data.get('Mounts')]
2019-04-16 22:09:36 +00:00
if not self._state:
raise ValueError('Container meta missing State')
if self._config is None:
raise ValueError('Container meta missing Config')
2019-04-16 22:09:36 +00:00
self._labels = self._config.get('Labels')
if self._labels is None:
raise ValueError('Container mtea missing Config->Labels')
2019-04-16 22:09:36 +00:00
self._include = self._parse_pattern(self.get_label('restic-volume-backup.include'))
self._exclude = self._parse_pattern(self.get_label('restic-volume-backup.exclude'))
2019-04-16 22:09:36 +00:00
@property
def image(self):
"""Image name"""
return self.get_config('Image')
2019-04-13 23:34:39 +00:00
@property
def environment(self):
"""All configured env vars for the container"""
return self.get_config('Env', default=[])
@property
def volumes(self):
"""
Return volumes for the container in the following format:
{'/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'},}
"""
volumes = {}
for mount in self._mounts:
volumes[mount.source] = {'bind': mount.destination, 'mode': 'ro'}
return volumes
2019-04-13 21:19:34 +00:00
@property
def backup_enabled(self) -> bool:
"""Is backup enabled for this container?"""
return self.get_label('restic-volume-backup.enabled') == 'True'
2019-04-13 21:19:34 +00:00
@property
def is_backup_process_container(self) -> bool:
"""Is this container the running backup process?"""
return self.get_label('restic-volume-backup.backup_process') == 'True'
2019-04-13 21:19:34 +00:00
@property
def is_running(self) -> bool:
"""Is the container running?"""
return self._state.get('Running', False)
2019-04-13 21:19:34 +00:00
@property
def service_name(self) ->str:
"""Name of the container/service"""
return self.get_label('com.docker.compose.service', default='')
2019-04-13 21:19:34 +00:00
@property
def project_name(self) -> str:
"""Name of the compose setup"""
return self.get_label('com.docker.compose.project', default='')
2019-04-13 21:19:34 +00:00
@property
def is_oneoff(self) -> bool:
"""Was this container started with run command?"""
return self.get_label('com.docker.compose.oneoff', default='False') == 'True'
def get_config(self, name, default=None):
"""Get value from config dict"""
return self._config.get(name, default)
def get_label(self, name, default=None):
"""Get a label by name"""
return self._labels.get(name, None)
2019-04-13 21:19:34 +00:00
def filter_mounts(self):
"""Get all mounts for this container matching include/exclude filters"""
2019-04-16 22:09:36 +00:00
filtered = []
if self._include:
for mount in self._mounts:
for pattern in self._include:
if pattern in mount.source:
2019-04-16 22:09:36 +00:00
break
else:
continue
filtered.append(mount)
elif self._exclude:
for mount in self._mounts:
for pattern in self._exclude:
2019-04-17 00:44:48 +00:00
if pattern in mount.source:
break
else:
filtered.append(mount)
2019-04-16 22:09:36 +00:00
else:
return self._mounts
2019-04-16 22:09:36 +00:00
return filtered
def _parse_pattern(self, value):
if not value:
return None
if type(value) is not str:
return None
value = value.strip()
if len(value) == 0:
return None
return value.split(',')
2019-04-13 17:04:54 +00:00
class Mount:
"""Represents a volume mount (volume or bind)"""
2019-04-13 17:04:54 +00:00
def __init__(self, data, container=None):
2019-04-14 18:55:35 +00:00
self._data = data
2019-04-13 17:04:54 +00:00
self._container = container
@property
2019-04-17 01:01:12 +00:00
def container(self) -> Container:
"""The container this mount belongs to"""
2019-04-13 17:04:54 +00:00
return self._container
@property
2019-04-17 01:01:12 +00:00
def type(self) -> str:
"""bind/volume"""
2019-04-14 18:55:35 +00:00
return self._data.get('Type')
2019-04-13 17:04:54 +00:00
@property
2019-04-17 01:01:12 +00:00
def name(self) -> str:
"""Name of the mount"""
2019-04-14 18:55:35 +00:00
return self._data.get('Name')
2019-04-13 17:04:54 +00:00
@property
2019-04-17 01:01:12 +00:00
def source(self) -> str:
"""Source of the mount. Volume name or path"""
2019-04-14 18:55:35 +00:00
return self._data.get('Source')
2019-04-13 17:04:54 +00:00
@property
2019-04-17 01:01:12 +00:00
def destination(self) -> str:
"""Destionatin path for the volume mount in the container"""
2019-04-14 18:55:35 +00:00
return self._data.get('Destination')
2019-04-13 17:04:54 +00:00
2019-04-17 01:01:12 +00:00
def mount_string(self) -> str:
2019-04-13 17:04:54 +00:00
if self.type == VOLUME_TYPE_VOLUME:
return "- {}:{}:ro".format(self.name.split('_')[-1], self.destination)
elif self.type == VOLUME_TYPE_BIND:
return "- {}:{}:ro".format(self.source, self.destination)
else:
raise ValueError("Uknown volume type: {}".format(self.type))
2019-04-17 01:01:12 +00:00
def __repr__(self) -> str:
2019-04-13 17:04:54 +00:00
return str(self)
2019-04-17 01:01:12 +00:00
def __str__(self) -> str:
2019-04-14 18:55:35 +00:00
return str(self._data)
2019-04-13 17:04:54 +00:00
def __hash__(self):
"""Uniquness for a volume"""
if self.type == VOLUME_TYPE_VOLUME:
return hash(self.name)
elif self.type == VOLUME_TYPE_BIND:
return hash(self.source)
else:
raise ValueError("Uknown volume type: {}".format(self.type))
class RunningContainers:
def __init__(self):
all_containers = utils.list_containers()
2019-04-13 17:04:54 +00:00
self.containers = []
2019-04-15 14:16:06 +00:00
self.this_container = None
2019-04-17 01:01:12 +00:00
self.backup_process_container = None
2019-04-13 17:04:54 +00:00
2019-04-15 14:16:06 +00:00
# Find the container we are running in.
# If we don't have this information we cannot continue
for container_data in all_containers:
if container_data.get('Id').startswith(os.environ['HOSTNAME']):
self.this_container = Container(container_data)
2019-04-13 21:19:34 +00:00
2019-04-15 14:16:06 +00:00
if not self.this_container:
2019-04-13 17:04:54 +00:00
raise ValueError("Cannot find metadata for backup container")
2019-04-15 14:16:06 +00:00
# Gather all containers in the current compose setup
for container_data in all_containers:
container = Container(container_data)
# Detect running backup process container
if container.is_backup_process_container:
self.backup_process_container = container
# Detect containers beloging to the current compose setup
2019-04-17 17:15:58 +00:00
if container.project_name == self.this_container.project_name and not container.is_oneoff:
2019-04-15 14:16:06 +00:00
if container.id != self.this_container.id:
self.containers.append(container)
@property
2019-04-17 01:01:12 +00:00
def backup_process_running(self) -> bool:
"""Is the backup process container running?"""
2019-04-17 01:01:12 +00:00
return self.backup_process_container is not None
def get_service(self, name) -> Container:
2019-04-16 18:55:22 +00:00
for container in self.containers:
if container.service_name == name:
return container
return None