]> git.proxmox.com Git - pmg-api.git/commitdiff
Add API2 module for per-node backups to PBS
authorStoiko Ivanov <s.ivanov@proxmox.com>
Mon, 16 Nov 2020 11:01:12 +0000 (12:01 +0100)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Tue, 17 Nov 2020 10:30:36 +0000 (11:30 +0100)
The module adds API2 methods for:

* creating/restoring/listing/forgetting backups on a configured PBS remote

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
src/Makefile
src/PMG/API2/Nodes.pm
src/PMG/API2/PBS/Job.pm [new file with mode: 0644]

index 5add6affa958492e6847e5f18ced0457c9da4964..fb42f21b0c27a04216105112e882f673c9251ed4 100644 (file)
@@ -137,6 +137,7 @@ LIBSOURCES =                                \
        PMG/API2/Statistics.pm          \
        PMG/API2/MailTracker.pm         \
        PMG/API2/Backup.pm              \
+       PMG/API2/PBS/Job.pm             \
        PMG/API2/PBS/Remote.pm          \
        PMG/API2/Nodes.pm               \
        PMG/API2/Postfix.pm             \
index 96aa146ea59b8d9e6bf79e95b2f0dd9a11379ded..259f8f3992572b653546fc9c14220450706001d8 100644 (file)
@@ -26,6 +26,7 @@ use PMG::API2::SpamAssassin;
 use PMG::API2::Postfix;
 use PMG::API2::MailTracker;
 use PMG::API2::Backup;
+use PMG::API2::PBS::Job;
 
 use base qw(PVE::RESTHandler);
 
@@ -79,6 +80,11 @@ __PACKAGE__->register_method ({
     path => 'backup',
 });
 
+__PACKAGE__->register_method ({
+    subclass => "PMG::API2::PBS::Job",
+    path => 'pbs',
+});
+
 __PACKAGE__->register_method ({
     name => 'index',
     path => '',
@@ -105,6 +111,7 @@ __PACKAGE__->register_method ({
        my $result = [
            { name => 'apt' },
            { name => 'backup' },
+           { name => 'pbs' },
            { name => 'clamav' },
            { name => 'spamassassin' },
            { name => 'postfix' },
diff --git a/src/PMG/API2/PBS/Job.pm b/src/PMG/API2/PBS/Job.pm
new file mode 100644 (file)
index 0000000..dee1754
--- /dev/null
@@ -0,0 +1,371 @@
+package PMG::API2::PBS::Job;
+
+use strict;
+use warnings;
+
+use POSIX qw(strftime);
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::SafeSyslog;
+use PVE::Tools qw(extract_param);
+use PVE::PBSClient;
+
+use PMG::RESTEnvironment;
+use PMG::Backup;
+use PMG::PBSConfig;
+
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method ({
+    name => 'list',
+    path => '',
+    method => 'GET',
+    description => "List all configured Proxmox Backup Server jobs.",
+    permissions => { check => [ 'admin', 'audit' ] },
+    proxyto => 'node',
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+       },
+    },
+    returns => {
+        type => "array",
+        items => PMG::PBSConfig->createSchema(1),
+        links => [ { rel => 'child', href => "{remote}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $res = [];
+
+       my $conf = PMG::PBSConfig->new();
+       if (defined($conf)) {
+           foreach my $remote (keys %{$conf->{ids}}) {
+               my $d = $conf->{ids}->{$remote};
+               my $entry = {
+                   remote => $remote,
+                   server => $d->{server},
+                   datastore => $d->{datastore},
+               };
+               push @$res, $entry;
+           }
+       }
+
+       return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'remote_index',
+    path => '{remote}',
+    method => 'GET',
+    description => "Backup Job index.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           remote => {
+               description => "Proxmox Backup Server ID.",
+               type => 'string', format => 'pve-configid',
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => { section => { type => 'string'} },
+       },
+       links => [ { rel => 'child', href => "{section}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $result = [
+           { section => 'snapshots' },
+           { section => 'backup' },
+           { section => 'restore' },
+           { section => 'timer' },
+       ];
+       return $result;
+}});
+
+__PACKAGE__->register_method ({
+    name => 'get_snapshots',
+    path => '{remote}/snapshots',
+    method => 'GET',
+    description => "Get snapshots stored on remote.",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           remote => {
+               description => "Proxmox Backup Server ID.",
+               type => 'string', format => 'pve-configid',
+           },
+       },
+    },
+    returns => {
+       type => 'array',
+       items => {
+           type => "object",
+           properties => {
+               time => { type => 'string'},
+               ctime => { type => 'string'},
+               size => { type => 'integer'},
+           },
+       },
+       links => [ { rel => 'child', href => "{time}" } ],
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $remote = $param->{remote};
+       my $node = $param->{node};
+
+       my $conf = PMG::PBSConfig->new();
+
+       my $remote_config = $conf->{ids}->{$remote};
+       die "PBS remote '$remote' does not exist\n" if !$remote_config;
+
+       return [] if $remote_config->{disable};
+
+       my $snap_param = {
+           group => "host/$node",
+       };
+
+       my $pbs = PVE::PBSClient->new($remote_config, $remote, $conf->{secret_dir});
+       my $snapshots = $pbs->get_snapshots($snap_param);
+       my $res = [];
+       foreach my $item (@$snapshots) {
+           my $btype = $item->{"backup-type"};
+           my $bid = $item->{"backup-id"};
+           my $epoch = $item->{"backup-time"};
+           my $size = $item->{size} // 1;
+
+           my @pxar = grep { $_->{filename} eq 'pmgbackup.pxar.didx' } @{$item->{files}};
+           die "unexpected number of pmgbackup archives in snapshot\n" if (scalar(@pxar) != 1);
+
+
+           next if !($btype eq 'host');
+           next if !($bid eq $node);
+
+           my $time = strftime("%FT%TZ", gmtime($epoch));
+
+           my $info = {
+               time => $time,
+               ctime => $epoch,
+               size => $size,
+           };
+
+           push @$res, $info;
+       }
+
+       return $res;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'forget_snapshot',
+    path => '{remote}/snapshots/{time}',
+    method => 'DELETE',
+    description => "Forget a snapshot",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           remote => {
+               description => "Proxmox Backup Server ID.",
+               type => 'string', format => 'pve-configid',
+           },
+           time => {
+               description => "Backup time in RFC 3399 format",
+               type => 'string',
+           },
+       },
+    },
+    returns => {type => 'null' },
+    code => sub {
+       my ($param) = @_;
+
+       my $remote = $param->{remote};
+       my $node = $param->{node};
+       my $time = $param->{time};
+
+       my $snapshot = "host/$node/$time";
+
+       my $conf = PMG::PBSConfig->new();
+
+       my $remote_config = $conf->{ids}->{$remote};
+       die "PBS remote '$remote' does not exist\n" if !$remote_config;
+       die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+
+       my $pbs = PVE::PBSClient->new($remote_config, $remote, $conf->{secret_dir});
+
+       eval {
+           $pbs->forget_snapshot($snapshot);
+       };
+       die "Forgetting backup failed: $@" if $@;
+
+       return;
+
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'run_backup',
+    path => '{remote}/backup',
+    method => 'POST',
+    description => "run backup and prune the backupgroup afterwards.",
+    proxyto => 'node',
+    protected => 1,
+    permissions => { check => [ 'admin', 'audit' ] },
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           remote => {
+               description => "Proxmox Backup Server ID.",
+               type => 'string', format => 'pve-configid',
+           },
+       },
+    },
+    returns => { type => "string" },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PMG::RESTEnvironment->get();
+       my $authuser = $rpcenv->get_user();
+
+       my $remote = $param->{remote};
+       my $node = $param->{node};
+
+       my $conf = PMG::PBSConfig->new();
+
+       my $remote_config = $conf->{ids}->{$remote};
+       die "PBS remote '$remote' does not exist\n" if !$remote_config;
+       die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+
+       my $pbs = PVE::PBSClient->new($remote_config, $remote, $conf->{secret_dir});
+       my $backup_dir = "/var/lib/pmg/backup/current";
+
+       my $worker = sub {
+           my $upid = shift;
+
+           print "starting update of current backup state\n";
+
+           -d $backup_dir || mkdir $backup_dir;
+           PMG::Backup::pmg_backup($backup_dir, $param->{statistic});
+           my $pbs_opts = {
+               type => 'host',
+               id => $node,
+               pxarname => 'pmgbackup',
+               root => $backup_dir,
+           };
+
+           $pbs->backup_tree($pbs_opts);
+
+           print "backup finished\n";
+
+           my $group = "host/$node";
+           print "starting prune of $group\n";
+           my $prune_opts = $conf->prune_options($remote);
+           my $res = $pbs->prune_group(undef, $prune_opts, $group);
+
+           foreach my $pruned (@$res){
+               my $time = strftime("%FT%TZ", gmtime($pruned->{'backup-time'}));
+               my $snap = $pruned->{'backup-type'} . '/' . $pruned->{'backup-id'} . '/' .  $time;
+               print "pruned snapshot: $snap\n";
+           }
+
+           print "prune finished\n";
+
+           return;
+       };
+
+       return $rpcenv->fork_worker('pbs_backup', undef, $authuser, $worker);
+
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'restore',
+    path => '{remote}/restore',
+    method => 'POST',
+    description => "Restore the system configuration.",
+    permissions => { check => [ 'admin' ] },
+    proxyto => 'node',
+    protected => 1,
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           PMG::Backup::get_restore_options(),
+           remote => {
+               description => "Proxmox Backup Server ID.",
+               type => 'string', format => 'pve-configid',
+           },
+           'backup-time' => {description=> "backup-time to restore",
+               optional => 1, type => 'string'
+           },
+           'backup-id' => {description => "backup-id (hostname) of backup snapshot",
+               optional => 1, type => 'string'
+           },
+       },
+    },
+    returns => { type => "string" },
+    code => sub {
+       my ($param) = @_;
+
+       my $rpcenv = PMG::RESTEnvironment->get();
+       my $authuser = $rpcenv->get_user();
+
+       my $remote = $param->{remote};
+       my $backup_id = $param->{'backup-id'} // $param->{node};
+       my $snapshot = "host/$backup_id";
+       $snapshot .= "/$param->{'backup-time'}" if defined($param->{'backup-time'});
+
+       my $conf = PMG::PBSConfig->new();
+
+       my $remote_config = $conf->{ids}->{$remote};
+       die "PBS remote '$remote' does not exist\n" if !$remote_config;
+       die "PBS remote '$remote' is disabled\n" if $remote_config->{disable};
+
+       my $pbs = PVE::PBSClient->new($remote_config, $remote, $conf->{secret_dir});
+
+       my $time = time;
+       my $dirname = "/tmp/proxrestore_$$.$time";
+
+       $param->{database} //= 1;
+
+       die "nothing selected - please select what you want to restore (config or database?)\n"
+           if !($param->{database} || $param->{config});
+
+       my $pbs_opts = {
+           pxarname => 'pmgbackup',
+           target => $dirname,
+           snapshot => $snapshot,
+       };
+
+       my $worker = sub {
+           my $upid = shift;
+
+           print "starting restore of $snapshot from $remote\n";
+
+           $pbs->restore_pxar($pbs_opts);
+           print "starting restore of PMG config\n";
+           PMG::Backup::pmg_restore($dirname, $param->{database},
+                $param->{config}, $param->{statistic});
+           print "restore finished\n";
+
+           return;
+       };
+
+       return $rpcenv->fork_worker('pbs_restore', undef, $authuser, $worker);
+    }});
+
+1;