def filter_mounts(self): """Get all mounts for this container matching include/exclude filters""" filtered = [] # If exclude_bind_mounts is true, only volume mounts are kept in the list of mounts exclude_bind_mounts = utils.is_true(config.exclude_bind_mounts) mounts = list( filter(lambda m: not exclude_bind_mounts or m.type == "volume", self._mounts)) if not self.volume_backup_enabled: return filtered if self._include: for mount in mounts: for pattern in self._include: if pattern in mount.source: break else: continue filtered.append(mount) elif self._exclude: for mount in mounts: for pattern in self._exclude: if pattern in mount.source: break else: filtered.append(mount) else: return mounts return filtered
def backup_destination_path(self) -> str: destination = Path("/databases") if utils.is_true(config.include_project_name): project_name = self.project_name if project_name != "": destination /= project_name destination /= self.service_name return destination
def backup_destination_path(self) -> str: destination = Path("/databases") if utils.is_true(config.include_project_name): project_name = self.project_name if project_name != "": destination /= project_name destination /= self.service_name destination /= f"{self.get_credentials()['database']}.sql" return destination
def get_volume_backup_destination(self, mount, source_prefix) -> str: """Get the destination path for backups of the given mount""" destination = Path(source_prefix) if utils.is_true(config.include_project_name): project_name = self.project_name if project_name != '': destination /= project_name destination /= self.service_name destination /= Path(utils.strip_root(mount.destination)) return str(destination)
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("Include project name in backup path?: %s", utils.is_true(config.include_project_name)) logger.debug("Exclude bind mounts from backups?: %s", utils.is_true(config.exclude_bind_mounts)) logger.info("Checking docker availability") utils.list_containers() if containers.stale_backup_process_containers: utils.remove_containers(containers.stale_backup_process_containers) # Check if repository is initialized with restic snapshots if not restic.is_initialized(config.repository): logger.info( "Could not get repository info. Attempting to initialize it.") result = restic.init_repo(config.repository) if result == 0: logger.info("Successfully initialized repository: %s", config.repository) else: logger.error("Failed to initialize repository") logger.info("%s Detected Config %s", "-" * 25, "-" * 25) # Start making snapshots backup_containers = containers.containers_for_backup() for container in backup_containers: logger.info('service: %s', container.service_name) if container.volume_backup_enabled: for mount in container.filter_mounts(): logger.info( ' - volume: %s -> %s', mount.source, container.get_volume_backup_destination(mount, '/volumes'), ) if container.database_backup_enabled: instance = container.instance ping = instance.ping() logger.info( ' - %s (is_ready=%s) -> %s', instance.container_type, ping == 0, instance.backup_destination_path(), ) if ping != 0: 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.*' label") logger.info("-" * 67)
def start_backup_process(config, containers): """The actual backup process running inside the spawned container""" if not utils.is_true(os.environ.get('BACKUP_PROCESS_CONTAINER')): logger.error( "Cannot run backup process in this container. Use backup command instead. " "This will spawn a new container with the necessary mounts.") alerts.send( subject="Cannot run backup process in this container", body= ("Cannot run backup process in this container. Use backup command instead. " "This will spawn a new container with the necessary mounts.")) exit(1) status(config, containers) errors = False # Did we actually get any volumes mounted? try: has_volumes = os.stat('/volumes') is not None except FileNotFoundError: logger.warning("Found no volumes to back up") has_volumes = False # Warn if there is nothing to do if len(containers.containers_for_backup()) == 0 and not has_volumes: logger.error("No containers for backup found") exit(1) if has_volumes: try: logger.info('Backing up volumes') vol_result = restic.backup_files(config.repository, source='/volumes') logger.debug('Volume backup exit code: %s', vol_result) if vol_result != 0: logger.error('Volume backup exited with non-zero code: %s', vol_result) errors = True except Exception as ex: logger.error('Exception raised during volume backup') logger.exception(ex) errors = True # back up databases logger.info('Backing up databases') for container in containers.containers_for_backup(): if container.database_backup_enabled: try: instance = container.instance logger.info('Backing up %s in service %s', instance.container_type, instance.service_name) result = instance.backup() logger.debug('Exit code: %s', result) if result != 0: logger.error( 'Backup command exited with non-zero code: %s', result) errors = True except Exception as ex: logger.exception(ex) errors = True if errors: logger.error('Exit code: %s', errors) exit(1) # Only run cleanup if backup was successful result = cleanup(config, container) logger.debug('cleanup exit code: %s', result) if result != 0: logger.error('cleanup exit code: %s', result) exit(1) # Test the repository for errors logger.info("Checking the repository for errors") result = restic.check(config.repository) if result != 0: logger.error('Check exit code: %s', result) exit(1) logger.info('Backup completed')
def postgresql_backup_enabled(self) -> bool: """bool: If the ``restic-compose-backup.postgres`` label is set""" return utils.is_true(self.get_label(enums.LABEL_POSTGRES_ENABLED))
def mariadb_backup_enabled(self) -> bool: """bool: If the ``restic-compose-backup.mariadb`` label is set""" return utils.is_true(self.get_label(enums.LABEL_MARIADB_ENABLED))
def mysql_backup_enabled(self) -> bool: """bool: If the ``restic-compose-backup.mysql`` label is set""" return utils.is_true(self.get_label(enums.LABEL_MYSQL_ENABLED))
def volume_backup_enabled(self) -> bool: """bool: If the ``restic-compose-backup.volumes`` label is set""" return utils.is_true(self.get_label(enums.LABEL_VOLUMES_ENABLED))
def minecraft_backup_enabled(self) -> bool: """bool: If the ``restic-compose-backup.minecraft``` label is set""" return utils.is_true(self.get_label(enums.LABEL_MINECRAFT_ENABLED))