Skip to content

Commit

Permalink
Upgraded to quartz for cronjobs
Browse files Browse the repository at this point in the history
  • Loading branch information
lyze237 committed May 6, 2024
1 parent b05e5ec commit d0f0013
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 137 deletions.
2 changes: 2 additions & 0 deletions DockerProxmoxBackup.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
<ItemGroup>
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Quartz" Version="3.8.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.8.1" />
</ItemGroup>
</Project>
268 changes: 135 additions & 133 deletions Worker.cs → Jobs/BackupJob.cs
Original file line number Diff line number Diff line change
@@ -1,133 +1,135 @@
using System.Diagnostics;
using System.Net;
using Docker.DotNet;
using Docker.DotNet.Models;
using DockerProxmoxBackup.Options;
using Microsoft.Extensions.Options;

namespace DockerProxmoxBackup;

public class Worker(IOptions<ProxmoxOptions> options, ILogger<Worker> logger) : BackgroundService
{
private readonly ProxmoxOptions options = options.Value;
private readonly DockerClient client = new DockerClientConfiguration().CreateClient();

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Running worker");

var containers = await client.Containers.ListContainersAsync(new ContainersListParameters(), stoppingToken);

var directory = new DirectoryInfo($"/tmp/{DateTime.Now.ToString("O")}");

foreach (var container in containers)
{
logger.LogDebug("Checking {Container}", container.ID);
if (!container.Image.Contains("postgres"))
continue;

logger.LogInformation("Backing up {Container} {Hostnames}", container.ID, string.Join(", ", container.Names));
var createDumpResult = await DumpToFile(container, stoppingToken);
logger.LogInformation("Backed up to {File} inside container with exit code {ExitCode}", createDumpResult.fileName, createDumpResult.exitCode);

logger.LogDebug(createDumpResult.stdout.Trim());
logger.LogError(createDumpResult.stderr.Trim());

if (createDumpResult.exitCode != 0)
{
logger.LogError("Failed to backup {Container} due to exit code {ExitCode} != 0, terminating", container.ID, createDumpResult.exitCode);

continue;
}

logger.LogInformation("Extracting dump from container");
var getDumpFileResult = await CpDumpFromContainer(container, createDumpResult.fileName, stoppingToken);

var backupFile = await WriteDumpToFile(directory, container, getDumpFileResult, stoppingToken);

logger.LogInformation("Added dump {file} to proxmox backup with {Size} <size units>", backupFile.FullName, getDumpFileResult.Stat.Size);
}

await UploadToProxmox(directory);
directory.Delete(true);
}

private async Task<FileInfo> WriteDumpToFile(DirectoryInfo directory, ContainerListResponse container, GetArchiveFromContainerResponse dumpFile, CancellationToken stoppingToken)
{
if (!directory.Exists)
directory.Create();

var tmpFile = new FileInfo(Path.Combine(directory.FullName, $"{GetContainerName(container)}.dump"));
await using var tmpStream = File.Create(tmpFile.FullName);
await dumpFile.Stream.CopyToAsync(tmpStream);

return tmpFile;
}

private async Task UploadToProxmox(DirectoryInfo directory)
{
var process = new Process
{
StartInfo = new ProcessStartInfo("proxmox-backup-client")
{

ArgumentList = { "backup", $"dockerProxmoxBackup.pxar:{directory.FullName}", "--repository", options.Repository, "--ns", options.Namespace },
Environment = { { "PBS_PASSWORD_FILE", options.PasswordFile} },
RedirectStandardError = true,
RedirectStandardOutput = true
}
};

logger.LogInformation("Running proxmox-backup-client {Args} with Env Args {Env}", string.Join(" ", process.StartInfo.ArgumentList), string.Join(", ", process.StartInfo.Environment.Select(kvp => $"{kvp.Key}={kvp.Value}")));

process.OutputDataReceived += (_, args) => logger.LogInformation($"[Exec] {args.Data}");
process.ErrorDataReceived += (_, args) => logger.LogInformation($"[Exec] {args.Data}");
process.Start();
await process.WaitForExitAsync();

logger.LogInformation("Proxmox backup exited with {ExitCode}", process.ExitCode);

if (process.ExitCode != 0)
logger.LogError("Proxmox exited in a bad state.");
}

private async Task<GetArchiveFromContainerResponse> CpDumpFromContainer(ContainerListResponse container, string fileName, CancellationToken stoppingToken)
{
var getArchiveFromContainerParameters = new GetArchiveFromContainerParameters
{
Path = fileName
};

var getDumpCommand = await client.Containers.GetArchiveFromContainerAsync(container.ID, getArchiveFromContainerParameters, false, stoppingToken);
return getDumpCommand;
}

private async Task<(string stdout, string stderr, long exitCode, string fileName)> DumpToFile(ContainerListResponse container, CancellationToken stoppingToken)
{
var fileName = "/postgres.dump";

var cmd = await client.Exec.ExecCreateContainerAsync(container.ID, new ContainerExecCreateParameters
{
AttachStderr = true,
AttachStdout = true,
Cmd = ["pg_dumpall", "--clean", "-U", "postgres", "-f", fileName]
}, stoppingToken);

using var stream = await client.Exec.StartAndAttachContainerExecAsync(cmd.ID, true, stoppingToken);
var (stdout, stderr) = await stream.ReadOutputToEndAsync(stoppingToken);
var execInspectResponse = await client.Exec.InspectContainerExecAsync(cmd.ID, stoppingToken);

return (stdout, stderr, execInspectResponse.ExitCode, fileName);
}

private string GetContainerName(ContainerListResponse container)
{
if (container.Labels.TryGetValue("backup.name", out var labelName))
return labelName;

if (container.Labels.TryGetValue("com.docker.swarm.service.name", out var sarmName))
return sarmName;

return container.Names.FirstOrDefault()?.Replace("/", "") ?? container.ID;
}
}
using System.Diagnostics;
using System.Net;
using Docker.DotNet;
using Docker.DotNet.Models;
using DockerProxmoxBackup.Options;
using Microsoft.Extensions.Options;
using Quartz;

namespace DockerProxmoxBackup.Jobs;

public class BackupJob(IOptions<ProxmoxOptions> options, ILogger<BackupJob> logger) : IJob
{
private readonly ProxmoxOptions options = options.Value;
private readonly DockerClient client = new DockerClientConfiguration().CreateClient();

public async Task Execute(IJobExecutionContext context)
{
logger.LogInformation("Running worker");

var containers = await client.Containers.ListContainersAsync(new ContainersListParameters(), context.CancellationToken);

var directory = new DirectoryInfo($"/tmp/{DateTime.Now.ToString("O")}");

foreach (var container in containers)
{
logger.LogDebug("Checking {Container}", container.ID);
if (!container.Image.Contains("postgres"))
continue;

logger.LogInformation("Backing up {Container} {Hostnames}", container.ID, string.Join(", ", container.Names));
var createDumpResult = await DumpToFile(container, context.CancellationToken);
logger.LogInformation("Backed up to {File} inside container with exit code {ExitCode}", createDumpResult.fileName, createDumpResult.exitCode);

logger.LogDebug(createDumpResult.stdout.Trim());
logger.LogError(createDumpResult.stderr.Trim());

if (createDumpResult.exitCode != 0)
{
logger.LogError("Failed to backup {Container} due to exit code {ExitCode} != 0, terminating", container.ID, createDumpResult.exitCode);

continue;
}

logger.LogInformation("Extracting dump from container");
var getDumpFileResult = await CpDumpFromContainer(container, createDumpResult.fileName, context.CancellationToken);

var backupFile = await WriteDumpToFile(directory, container, getDumpFileResult, context.CancellationToken);

logger.LogInformation("Added dump {file} to proxmox backup with {Size} <size units>", backupFile.FullName, getDumpFileResult.Stat.Size);
}

await UploadToProxmox(directory);
directory.Delete(true);
}


private async Task<FileInfo> WriteDumpToFile(DirectoryInfo directory, ContainerListResponse container, GetArchiveFromContainerResponse dumpFile, CancellationToken stoppingToken)
{
if (!directory.Exists)
directory.Create();

var tmpFile = new FileInfo(Path.Combine(directory.FullName, $"{GetContainerName(container)}.dump"));
await using var tmpStream = File.Create(tmpFile.FullName);
await dumpFile.Stream.CopyToAsync(tmpStream, stoppingToken);

return tmpFile;
}

private async Task UploadToProxmox(DirectoryInfo directory)
{
var process = new Process
{
StartInfo = new ProcessStartInfo("proxmox-backup-client")
{

ArgumentList = { "backup", $"dockerProxmoxBackup.pxar:{directory.FullName}", "--repository", options.Repository, "--ns", options.Namespace },
Environment = { { "PBS_PASSWORD_FILE", options.PasswordFile} },
RedirectStandardError = true,
RedirectStandardOutput = true
}
};

logger.LogInformation("Running proxmox-backup-client {Args} with Env Args {Env}", string.Join(" ", process.StartInfo.ArgumentList), string.Join(", ", process.StartInfo.Environment.Select(kvp => $"{kvp.Key}={kvp.Value}")));

process.OutputDataReceived += (_, args) => logger.LogInformation($"[Exec] {args.Data}");
process.ErrorDataReceived += (_, args) => logger.LogInformation($"[Exec] {args.Data}");
process.Start();
await process.WaitForExitAsync();

logger.LogInformation("Proxmox backup exited with {ExitCode}", process.ExitCode);

if (process.ExitCode != 0)
logger.LogError("Proxmox exited in a bad state.");
}

private async Task<GetArchiveFromContainerResponse> CpDumpFromContainer(ContainerListResponse container, string fileName, CancellationToken stoppingToken)
{
var getArchiveFromContainerParameters = new GetArchiveFromContainerParameters
{
Path = fileName
};

var getDumpCommand = await client.Containers.GetArchiveFromContainerAsync(container.ID, getArchiveFromContainerParameters, false, stoppingToken);
return getDumpCommand;
}

private async Task<(string stdout, string stderr, long exitCode, string fileName)> DumpToFile(ContainerListResponse container, CancellationToken stoppingToken)
{
var fileName = "/postgres.dump";

var cmd = await client.Exec.ExecCreateContainerAsync(container.ID, new ContainerExecCreateParameters
{
AttachStderr = true,
AttachStdout = true,
Cmd = ["pg_dumpall", "--clean", "-U", "postgres", "-f", fileName]
}, stoppingToken);

using var stream = await client.Exec.StartAndAttachContainerExecAsync(cmd.ID, true, stoppingToken);
var (stdout, stderr) = await stream.ReadOutputToEndAsync(stoppingToken);
var execInspectResponse = await client.Exec.InspectContainerExecAsync(cmd.ID, stoppingToken);

return (stdout, stderr, execInspectResponse.ExitCode, fileName);
}

private string GetContainerName(ContainerListResponse container)
{
if (container.Labels.TryGetValue("backup.name", out var labelName))
return labelName;

if (container.Labels.TryGetValue("com.docker.swarm.service.name", out var sarmName))
return sarmName;

return container.Names.FirstOrDefault()?.Replace("/", "") ?? container.ID;
}
}
1 change: 1 addition & 0 deletions Options/ProxmoxOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public class ProxmoxOptions
public string PasswordFile { get; set; }
public string Repository { get; set; }
public string Namespace { get; set; }
public string Cronjob { get; set; }
}
23 changes: 21 additions & 2 deletions Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
using DockerProxmoxBackup;
using DockerProxmoxBackup.Jobs;
using DockerProxmoxBackup.Options;
using Quartz;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.Configure<ProxmoxOptions>(builder.Configuration.GetSection("Proxmox"));
builder.Services.AddHostedService<Worker>();

builder.Services.AddQuartz(q =>
{
var job = new JobKey("backup");
q.AddJob<BackupJob>(opts => opts.WithIdentity(job));
q.AddTrigger(opts =>
{
var cronjob = builder.Configuration.GetSection("Proxmox")["Cronjob"] ?? throw new ArgumentNullException("Proxmox__Cronjob environment variable not set");
Console.WriteLine($"Running backups with cronjob: {cronjob}");
opts.ForJob(job)
.WithIdentity("backup-trigger")
.WithCronSchedule(cronjob)
.StartNow();
});
});

builder.Services.AddQuartzHostedService(opts => { opts.WaitForJobsToComplete = true; } );

var host = builder.Build();
host.Run();
69 changes: 68 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,69 @@
# DockerProxmoxBackup
A simple tool to backup docker containers to a proxmox backup server
A simple tool to backup docker containers[1] to a proxmox backup server

[1] Right now only postgres backup

---

Add the backup container to either a docker compose or docker stack (for docker swarm) file:

```yml
services:
DockerProxmoxBackup:
# swap this if you're using swarm
# hostname: "{{.Node.Hostname}}"
hostname: devtools
image: ghcr.io/lyze237/dockerproxmoxbackup:main
secrets:
- proxmox_password_secret
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
Proxmox__PasswordFile: "/run/secrets/proxmox_password_secret"
Proxmox__Repository: "bdd@pbs@backups.chirps.cafe:backups"
Proxmox__Namespace: "files"
Proxmox__Cronjob: "0 0 3 * * ?" # https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontrigger.html
```
The tool uses the following order for the backup file name:
* Label: `backup.name`
* Label: `com.docker.swarm.service.name`
* Hostname
* ID

```yml
OtherPostgresExample:
image: postgres
labels:
backup.name: other
environment:
POSTGRES_PASSWORD: password
```

Try and keep the name to the same across backups, so that proxmox can deduplicate it.

Here's a full swarm example which deploys the container across all nodes:

```yml
version: '3.8'
services:
dockerProxmoxBackup:
image: ghcr.io/lyze237/dockerproxmoxbackup:main
hostname: "{{.Node.Hostname}}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
secrets:
- proxmox_password_secret
environment:
Proxmox__PasswordFile: "/run/secrets/proxmox_password_secret"
Proxmox__Repository: "bdd@pbs@backups.chirps.cafe:backups"
Proxmox__Namespace: "files"
Proxmox__Cronjob: "0 0 3 * * ?" # https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontrigger.html
deploy:
mode: global
secrets:
proxmox_password_secret:
file: proxmox_password.secret
```
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ services:
environment:
Proxmox__PasswordFile: "/run/secrets/proxmox_password_secret"
Proxmox__Repository: "bdd@pbs@backups.chirps.cafe:backups"
ProxmoX__Namespace: "files"
Proxmox__Namespace: "files"
Proxmox__Cronjob: "0 0 3 * * ?" # https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontrigger.html

PostgresExample:
image: postgres
Expand Down

0 comments on commit d0f0013

Please sign in to comment.