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
|
|
|
|
2019-04-15 17:06:48 +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:
|
2019-04-17 01:09:33 +00:00
|
|
|
"""Represents a docker container"""
|
2019-04-13 17:04:54 +00:00
|
|
|
|
|
|
|
def __init__(self, data):
|
|
|
|
self.id = data.get('Id')
|
2019-04-13 21:19:34 +00:00
|
|
|
self.state = data.get('State')
|
2019-04-13 23:34:39 +00:00
|
|
|
self.labels = data.get('Labels', {})
|
|
|
|
self.names = data.get('Names', [])
|
2019-04-13 17:04:54 +00:00
|
|
|
self.mounts = [Mount(mnt, container=self) for mnt in data.get('Mounts')]
|
|
|
|
|
2019-04-16 22:09:36 +00:00
|
|
|
self.include = self._parse_pattern(self.labels.get('restic-volume-backup.include'))
|
|
|
|
self.exclude = self._parse_pattern(self.labels.get('restic-volume-backup.exclude'))
|
|
|
|
|
|
|
|
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 23:34:39 +00:00
|
|
|
|
2019-04-13 21:19:34 +00:00
|
|
|
@property
|
|
|
|
def backup_enabled(self):
|
2019-04-17 01:09:33 +00:00
|
|
|
"""Is backup enabled for this container?"""
|
2019-04-13 21:19:34 +00:00
|
|
|
return self.labels.get('restic-volume-backup.enabled') == 'True'
|
|
|
|
|
2019-04-17 01:09:33 +00:00
|
|
|
@property
|
|
|
|
def is_backup_process_container(self):
|
|
|
|
"""Is this container the running backup process?"""
|
2019-04-17 01:28:31 +00:00
|
|
|
return self.labels.get('restic-volume-backup.backup_process') == 'True'
|
2019-04-17 01:09:33 +00:00
|
|
|
|
2019-04-13 21:19:34 +00:00
|
|
|
@property
|
|
|
|
def is_running(self):
|
2019-04-17 01:09:33 +00:00
|
|
|
"""Is the container running?"""
|
2019-04-13 21:19:34 +00:00
|
|
|
return self.state == 'running'
|
|
|
|
|
|
|
|
@property
|
|
|
|
def service_name(self):
|
2019-04-17 01:09:33 +00:00
|
|
|
"""Name of the container/service"""
|
2019-04-13 21:19:34 +00:00
|
|
|
return self.labels['com.docker.compose.service']
|
|
|
|
|
|
|
|
@property
|
|
|
|
def project_name(self):
|
2019-04-17 01:09:33 +00:00
|
|
|
"""Name of the compose setup"""
|
2019-04-13 21:19:34 +00:00
|
|
|
return self.labels['com.docker.compose.project']
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_oneoff(self):
|
2019-04-17 01:09:33 +00:00
|
|
|
"""Was this container started with run command?"""
|
2019-04-13 21:19:34 +00:00
|
|
|
return self.labels['com.docker.compose.oneoff'] == 'True'
|
|
|
|
|
2019-04-14 19:22:11 +00:00
|
|
|
def filter_mounts(self):
|
2019-04-17 01:09:33 +00:00
|
|
|
"""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:
|
2019-04-14 19:22:11 +00:00
|
|
|
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:
|
2019-04-17 00:44:48 +00:00
|
|
|
for mount in self.mounts:
|
|
|
|
for pattern in self.exclude:
|
|
|
|
if pattern in mount.source:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
filtered.append(mount)
|
2019-04-16 22:09:36 +00:00
|
|
|
else:
|
|
|
|
return self.mounts
|
|
|
|
|
|
|
|
return filtered
|
2019-04-14 19:22:11 +00:00
|
|
|
|
2019-04-13 17:04:54 +00:00
|
|
|
def to_dict(self):
|
|
|
|
return {
|
2019-04-13 21:19:34 +00:00
|
|
|
'Id': self.id,
|
|
|
|
'Names': self.names,
|
|
|
|
'State': self.state,
|
|
|
|
'Labels': self.labels,
|
2019-04-14 19:22:11 +00:00
|
|
|
'Mounts': [mnt.data for mnt in self.mounts],
|
|
|
|
'include': self.include,
|
|
|
|
'exclude': self.exlude,
|
2019-04-13 17:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class Mount:
|
2019-04-17 01:09:33 +00:00
|
|
|
"""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:
|
2019-04-17 01:09:33 +00:00
|
|
|
"""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:
|
2019-04-17 01:09:33 +00:00
|
|
|
"""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:
|
2019-04-17 01:09:33 +00:00
|
|
|
"""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:
|
2019-04-17 01:09:33 +00:00
|
|
|
"""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):
|
2019-04-15 17:06:48 +00:00
|
|
|
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)
|
2019-04-17 01:17:50 +00:00
|
|
|
|
|
|
|
# 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-15 14:16:06 +00:00
|
|
|
if container.project_name == self.this_container.project_name:
|
|
|
|
if container.id != self.this_container.id:
|
|
|
|
self.containers.append(container)
|
|
|
|
|
|
|
|
# for container in self.all_containers:
|
|
|
|
# # Weed out containers not beloging to this project.
|
|
|
|
# if container.project_name != self.backup_container.project_name:
|
|
|
|
# continue
|
|
|
|
|
|
|
|
# # Keep only containers with backup enabled
|
|
|
|
# if not container.backup_enabled:
|
|
|
|
# continue
|
|
|
|
|
|
|
|
# # and not oneoffs (started manually with run or similar)
|
|
|
|
# if container.is_oneoff:
|
|
|
|
# continue
|
|
|
|
|
|
|
|
# self.containers.append(container)
|
|
|
|
|
|
|
|
# def gen_volumes(self, volume_type):
|
|
|
|
# """Generator yielding volumes of a specific type"""
|
|
|
|
# for cont in self.containers:
|
|
|
|
# for mnt in cont.mounts:
|
|
|
|
# if mnt.type == volume_type:
|
|
|
|
# yield mnt
|
|
|
|
|
|
|
|
# def volume_mounts(self):
|
|
|
|
# """Docker volumes"""
|
|
|
|
# return set(mnt for mnt in self.gen_volumes(VOLUME_TYPE_VOLUME))
|
|
|
|
|
|
|
|
# def bind_mounts(self):
|
|
|
|
# """Host mapped volumes"""
|
|
|
|
# return set(mnt for mnt in self.gen_volumes(VOLUME_TYPE_BIND))
|
|
|
|
|
2019-04-17 01:17:50 +00:00
|
|
|
@property
|
2019-04-17 01:01:12 +00:00
|
|
|
def backup_process_running(self) -> bool:
|
2019-04-17 01:09:33 +00:00
|
|
|
"""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
|