Commit c9a52d4a authored by Tim Bleimehl's avatar Tim Bleimehl 🤸🏼
Browse files

wip

parent 33aca929
......@@ -4,7 +4,7 @@ from Configs import getConfig
from pathlib import PurePath, Path
from config import DEFAULT
from executer import Executer
from backup_manager import BackupManager, RetentionType
from backup_manager import Backup, BackupManager, RetentionType
from container_helper import Container
from label import ValidLabels, Label
......@@ -141,28 +141,26 @@ class BaseBackupper:
def get_backup_command(self, database_name, target_filepath):
raise NotImplementedError
def _find_backup_by_name(self, database: str, backup_name: str) -> Dict:
backup: Dict = None
for retention_type, backup_list in self.manager.list_backups(database).items():
for backup_ in backup_list:
if backup_["backup_name"] == backup_name:
backup = backup_
break
def _find_backup_by_name(self, database_name: str, backup_name: str) -> Dict:
backup: Backup = None
for retention_type, backups in self.manager.list_backups(
database_name=database_name
).items():
backup = next((b for b in backups if b.name == backup_name), None)
if backup:
break
if backup is None:
raise ValueError(
f"No backup with name '{backup_name}' for database '{database}' found."
f"No backup with name '{backup_name}' for database '{database_name}' found."
)
return backup
def get_restore_command(self, database_name, source_filepath):
raise NotImplementedError
def restore(self, database: str, backup_name: str):
def restore(self, database: str, backup_name: str, dry_run: bool = False):
log.debug(
f"Restore orders for database '{database}'. backup_name: '{backup_name}'"
f"Restore order for database '{database}'. backup_name: '{backup_name}'"
)
backup: Dict = self._find_backup_by_name(database, backup_name)
log.debug(f"Found backup to restore: {backup}")
......@@ -170,6 +168,7 @@ class BaseBackupper:
self.executer.container_exec(
command=com,
prefix=f"{'zcat' if self.compress_backup else 'cat'} <{backup['path'].absolute()} |",
dry_run=dry_run,
)
......
......@@ -2,6 +2,7 @@
# from email.policy import default
# from pydoc import describe
from statistics import mode
import click
import os
import sys
......@@ -18,7 +19,7 @@ if __name__ == "__main__":
sys.path.append(os.path.normpath(SCRIPT_DIR))
from backup_manager import RetentionType
from backup_manager import RetentionType, Backup
from executer import Executer
from backupper import (
BaseBackupper,
......@@ -27,10 +28,7 @@ from backupper import (
Neo4jOfflineBackupper,
Neo4jOnlineBackupper,
)
from cli_helper import (
backup_list_to_human,
backup_list_to_machine_readable,
)
from cli_helper import list_backups, format_n_output_backup_list
from log import log
from label import ValidLabels, Label
......@@ -163,7 +161,7 @@ def backup_now(
container = ContainerHelper.kubernetes_get_pods_by_workload(workload)
if len(container) != 1:
raise ValueError(
f"Workload '{workload['metadata']['namespace']}/{workload['metadata']['name']}' containers multiple pods. Dont know what to do..."
f"Workload '{workload['metadata']['namespace']}/{workload['metadata']['name']}' contains multiple pods. Dont know what to do..."
)
backup_name = workload_name
container_name = container[0].name
......@@ -272,6 +270,7 @@ def backup_docker(target_dir):
databases=container.coda_labels[ValidLabels.database_names].val,
retention_type=RetentionType.DAILY,
)
bu.manager.rotate_existing_backups()
......@@ -290,9 +289,9 @@ def restore_cli(debug):
@restore_cli.command(name="now")
@click.option(
"--mode",
default="Docker",
default="docker",
prompt="How is your DB running?",
help="Environment the database is running in: 'Docker', 'kubernetes', 'native'",
help="Environment the database is running in: 'docker', 'kubernetes', 'native'",
type=click.Choice(["docker", "kubernetes", "native"], case_sensitive=False),
)
@click.option(
......@@ -359,25 +358,47 @@ def restore_now(
help="Define the namespace we should search for database pods to be backed up.",
)
@click.option(
"--all-namespaces",
"-all-namespaces",
is_flag=True,
default=False,
help="If we should search for pods, to be backed up, in all namespaces. When used `--namespace` will be ignored.",
)
@click.option(
"--pod-name",
hide_input=False,
"--workload-name",
required=True,
type=str,
help="Name of the pod/container the to be restored database runs in?",
)
@click.option(
"--backup-name",
hide_input=False,
required=True,
type=str,
help="Name of the backup (find via coda-restore kubernetes list)?",
)
def restore_kubernetes(ctx, namespace, all_namespaces, pod_name, backup_name):
@click.option(
"--database-name",
type=str,
help="name of the database instance to be restored",
required=True,
)
def restore_kubernetes(ctx, namespace, workload_name, backup_name, database_name):
if ctx.invoked_subcommand is None:
click.echo("DO restore")
# you are here
workload = next(
(
wl
for wl in ContainerHelper.kubernetes_get_workloads(namespace=namespace)
if wl["metadata"]["name"] == workload_name
),
None,
)
if not workload:
raise ValueError(
f"Could not find workload with name '{workload}' in namespace '{namespace}'"
)
pods = ContainerHelper.kubernetes_get_pods_by_workload(workload)
if len(pods) > 1:
raise NotImplementedError("Only single pod workloads are supported atm")
pod = pods[0]
BackupperClass = database_type_backupper_mapping[
pod.coda_labels[ValidLabels.database_type].val
]
bu = BackupperClass(db_container=pod)
bu.restore(database=database_name, backup_name=backup_name, dry_run=True)
@restore_kubernetes.command(name="list")
......@@ -424,82 +445,74 @@ def restore_kubernetes(ctx, namespace, all_namespaces, pod_name, backup_name):
def restore_kubernetes_list(
workload_name, databases, namespace, all_namespaces, source_dir, output_format
):
"""Display available backup files"""
# ToDo: This func is messy :) we sure can improve this.
backup_list: List = {}
data = []
for pod in ContainerHelper.kubernetes_get_pods_to_be_backed_up(
namespace=namespace, all_namespaces=all_namespaces
):
if databases:
databases = databases.split(",")
if workload_name and pod.parent["metadata"]["name"] != workload_name:
continue
pod_workload_name = pod.parent["metadata"]["name"]
if pod.coda_labels[ValidLabels.enabled].val:
BackupperClass = database_type_backupper_mapping[
pod.coda_labels[ValidLabels.database_type].val
]
bu = BackupperClass(pod)
for database_name in bu.manager.list_backuped_databases():
if databases and not database_name in databases:
continue
namespace = None
for ns in data:
if (
"namespace" in ns
and ns["namespace"] == pod.kubernetes_namespace
):
namespace = ns
if namespace is None:
namespace = {
"namespace": pod.kubernetes_namespace,
"mode": "kubernetes",
"database_containers": [],
}
data.append(namespace)
pod_entry = next(
(
p
for p in namespace["database_containers"]
if p["name"] == pod_workload_name
),
None,
)
if not pod_entry:
pod_entry = {
"name": pod_workload_name,
"basedir": bu.manager.base_path,
"databases": [],
}
namespace["database_containers"].append(pod_entry)
pod_entry["databases"].append(
{
"name": database_name,
"basedir": Path(PurePath(bu.manager.base_path, database_name)),
"backups": bu.manager.list_backups(database_name),
}
)
if output_format in ["json", "yaml"]:
output = backup_list_to_machine_readable(data, output_format)
print(output)
return output
elif output_format == "human":
output = backup_list_to_human(data)
print(output)
return output
elif output_format == "dict":
return data
"""Display available backup files for existing workloads"""
data = list_backups(
database_type_backupper_mapping=database_type_backupper_mapping,
mode="kubernetes",
container_name=workload_name,
databases=databases,
namespace=namespace,
all_namespaces=all_namespaces,
source_dir=source_dir,
)
return format_n_output_backup_list(data, output_format)
def restore_docker():
pass
@restore_cli.group(invoke_without_command=True, name="docker")
@click.pass_context
@click.option(
"--container-name",
hide_input=False,
help="Name of the pod/container the to be restored database runs in?",
)
@click.option(
"--backup-name",
hide_input=False,
help="Name of the backup (find via coda-restore kubernetes list)?",
)
def restore_docker(ctx, container_name, backup_name):
if ctx.invoked_subcommand is None:
click.echo("DO docker restore")
def restore_docker_list():
pass
@restore_docker.command(name="list")
@click.option(
"--container-name",
"-c",
default=None,
help="User to access the database?",
)
@click.option(
"--databases",
"-d",
default=None,
help="List only these databases (seperated by comma)",
)
@click.option(
"--source-dir",
"-s",
type=str,
default=None,
help=f"Where to look for the backups. Will default to '{ValidLabels.backup_dir.val}'",
)
@click.option(
"--output-format",
"-o",
default="human",
help="Define the output format: 'human' an easy to read table, 'json' or 'yaml' for inter-process readability, dict if used in a python script",
type=click.Choice(["human", "json", "yaml", "dict"], case_sensitive=False),
)
def restore_docker_list(container_name, databases, source_dir, output_format):
"""Display available backup files for existing containers"""
data = list_backups(
database_type_backupper_mapping=database_type_backupper_mapping,
mode="docker",
container_name=container_name,
databases=databases,
source_dir=source_dir,
)
return format_n_output_backup_list(data, output_format)
if __name__ == "__main__":
......
from ast import Call
from typing import Dict, List, Union, Type, Callable
from datetime import datetime, timedelta
import time
import humanize
from numpy import sort
from pathlib import Path
from pathlib import Path, PurePath
import tabulate
tabulate.PRESERVE_WHITESPACE = True
from backup_manager import Backup, RetentionType
from container_helper import Container, ContainerHelper
from label import ValidLabels
import json
import yaml
def list_backups(
database_type_backupper_mapping: Dict,
mode: str,
container_name: str,
databases: List[str],
namespace: str = None,
all_namespaces: bool = None,
source_dir: str = None,
):
# todo: can we improve this func? it is a little bit messy :)
if isinstance(databases, str):
databases = databases.split(",")
containers: List[Dict[str, Container]] = []
output_data: List = []
# Lets find all the container with CoDa enabled and already create the namespace entries for the output data
if mode == "kubernetes":
for container in ContainerHelper.kubernetes_get_pods_to_be_backed_up(
namespace=namespace, all_namespaces=all_namespaces
):
if databases:
databases = databases.split(",")
# as this is container framework agnostic funtion we actually search for the workload and not the container/pod by name.
# but we will extract the pod name in the workload for later use
if (
container_name
and container.parent["metadata"]["name"] != container_name
):
continue
containers.append({container.backup_name: container})
# create an entry for the namespace if not exists
if container.kubernetes_namespace not in [
ns["namespace"] for ns in output_data
]:
namespace = {
"namespace": container.kubernetes_namespace,
"mode": "kubernetes",
"database_containers": [],
}
output_data.append(namespace)
else:
namespace = next(
ns
for ns in output_data
if ns["namespace"] == container.kubernetes_namespace
)
elif mode == "docker":
# in docker we dont have namespaces we use only one placeholder "default" namespace to keep the format constistent
namespace = "default"
namespace = {
"namespace": "default",
"mode": "docker",
"database_containers": [],
}
output_data.append(namespace)
# collect all containers to be backed up
for container in ContainerHelper.docker_get_container_to_be_backed_up(
describe=True
):
if container_name and container.name != container_name:
continue
containers.append({container.backup_name: container})
for c in containers:
container_output_name = list(c.keys())[0]
container = list(c.values())[0]
BackupperClass = database_type_backupper_mapping[
container.coda_labels[ValidLabels.database_type].val
]
bu = BackupperClass(container, target_base_dir=source_dir)
for database_name in bu.manager.list_backuped_databases():
if databases and not database_name in databases:
continue
namespace = None
if mode == "docker":
# in docker we dont have namespaces we use the placeholder "default" namespace
namespace = output_data[0]
elif mode == "kubernetes":
namespace = next(
ns
for ns in output_data
if ns["namespace"] == container.kubernetes_namespace
)
# lets look up if there is already a namespace entry for this container
container_entry = next(
(
c
for c in namespace["database_containers"]
if c["name"] == container_output_name
),
None,
)
if not container_entry:
# this database is in a new container
# lets create new container entry in this namespace
container_entry = {
"name": container_output_name,
"basedir": bu.manager.base_path,
"databases": [],
}
namespace["database_containers"].append(container_entry)
container_entry["databases"].append(
{
"name": database_name,
"basedir": Path(PurePath(bu.manager.base_path, database_name)),
"backups": bu.manager.list_backups(database_name),
}
)
return output_data
def backup_list_to_human(backup_list: Dict):
"""[summary]
......@@ -159,3 +273,16 @@ def cast_type_in_nested_dict_or_list(
return new_data
elif isinstance(data, target_type):
return cast_func(data)
def format_n_output_backup_list(data, output_format):
if output_format in ["json", "yaml"]:
output = backup_list_to_machine_readable(data, output_format)
print(output)
return output
elif output_format == "human":
output = backup_list_to_human(data)
print(output)
return output
elif output_format == "dict":
return data
......@@ -39,9 +39,18 @@ class Executer:
)
def container_exec(
self, command: str, prefix: str = None, interactive: bool = False
self,
command: str,
prefix: str = None,
interactive: bool = False,
dry_run: bool = False,
):
exec_command = f"{prefix + ' ' if prefix is not None else ''} {self._get_container_exec_command_base()} {'-it' if interactive else ''} {self.container_name}{' --' if self.mode.lower() == 'kubernetes' else ''} {command}"
if dry_run:
log.info(
f"##Dry run!##\nFollowing command will be executed:\n{exec_command}"
)
return exec_command
return self.exec(exec_command)
@classmethod
......
......@@ -4,7 +4,7 @@ A container native database backup and restore solution
Maintainer: tim.bleimehl@dzd-ev.de
Status: Alpha (WIP - not executable atm)
Status: Alpha (WIP - not basic-feature completed yet)
# What is this (short)
......@@ -37,7 +37,7 @@ services:
- "backup.dzd-ev.de/type=mysql"
- "backup.dzd-ev.de/username=root"
- "backup.dzd-ev.de/password=mysuperpw"
```
```
Note the `labels`; these will direct our CoDaBackup instance.
......@@ -84,28 +84,27 @@ Thats it. We now have a directory `./backups/` in front of us, with all database
* (WIP) Restore Wizard
* (WIP) Neo4j support
* (Planned) Database auth via Docker/kubernetes Secrets
* (Planned) Suppord pod with more than one container https://kubernetes.io/docs/tasks/debug-application-cluster/get-shell-running-container/#opening-a-shell-when-a-pod-has-more-than-one-container
* (Planned) Create database if not existent via label conf (checked/executed when pod starts via https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/)
* (Planned) Email notification
* (Planned) Support of docker and kubernetes secrets for providing database auth
* (Planned) Docker Event listener / Kubectl hooks to react to container started/stopped
* (Planned) Improve human readable output by printing a tree structure (https://www.baeldung.com/java-print-binary-tree-diagram)
* (Planned) Compression optional
* (Idea) Database tools (Create non existing databases, based on labels)
* (Idea) Switch to https://github.com/kubernetes-client/python and https://docker-py.readthedocs.io/en/stable/ (or create alternative modules)
* (Idea) restore by label (checked/executed when pod starts via https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/)
* (Idea) Matrix notifications
* (Idea) [Podman](https://podman.io/) support. Your help is greatly appreciated. Should be easy or maybe no work at all
* (Idea) [LCX](https://linuxcontainers.org/) support. Help needed!
# ToDo
* (Idea) Suppord pod with more than one container https://kubernetes.io/docs/tasks/debug-application-cluster/get-shell-running-container/#opening-a-shell-when-a-pod-has-more-than-one-container
# Current ToDo
* implement RETENTION_ON_COLLISION_KEEP_NEWEST_BACKUP
* Restore CLI API
* write docs
# Placeholder volume for docker.sock
VOLUME ["/var/run/docker.sock"]
# Placeholder volume for kubectl config
VOLUME ["/.kube/config"]
# Docker Volumes Pathes
`/var/run/docker.sock`
`/.kube/config`
# Dev Notes
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment