From: Dietmar Maurer Date: Wed, 19 Feb 2020 12:55:27 +0000 (+0100) Subject: PVE/Storage/PBSPlugin.pm: start new proxmox backup server plugin X-Git-Url: https://git.proxmox.com/?p=pve-storage.git;a=commitdiff_plain;h=271fe394601b7f1733e866c73f92b009c7277b28 PVE/Storage/PBSPlugin.pm: start new proxmox backup server plugin --- diff --git a/PVE/API2/Storage/Config.pm b/PVE/API2/Storage/Config.pm index e00dc95..d202784 100755 --- a/PVE/API2/Storage/Config.pm +++ b/PVE/API2/Storage/Config.pm @@ -136,7 +136,7 @@ __PACKAGE__->register_method ({ my $password; # always extract pw, else it gets written to the www-data readable scfg if (my $tmp_pw = extract_param($param, 'password')) { - if ($type eq 'cifs' && $param->{username}) { + if (($type eq 'pbs') || ($type eq 'cifs' && $param->{username})) { $password = $tmp_pw; } else { warn "ignore password parameter\n"; diff --git a/PVE/Storage.pm b/PVE/Storage.pm index 62d72de..17196a8 100755 --- a/PVE/Storage.pm +++ b/PVE/Storage.pm @@ -36,6 +36,7 @@ use PVE::Storage::GlusterfsPlugin; use PVE::Storage::ZFSPoolPlugin; use PVE::Storage::ZFSPlugin; use PVE::Storage::DRBDPlugin; +use PVE::Storage::PBSPlugin; # Storage API version. Icrement it on changes in storage API interface. use constant APIVER => 3; @@ -58,6 +59,7 @@ PVE::Storage::GlusterfsPlugin->register(); PVE::Storage::ZFSPoolPlugin->register(); PVE::Storage::ZFSPlugin->register(); PVE::Storage::DRBDPlugin->register(); +PVE::Storage::PBSPlugin->register(); # load third-party plugins if ( -d '/usr/share/perl5/PVE/Storage/Custom' ) { diff --git a/PVE/Storage/Makefile b/PVE/Storage/Makefile index 53b2ca7..5b8f6e8 100644 --- a/PVE/Storage/Makefile +++ b/PVE/Storage/Makefile @@ -12,6 +12,7 @@ SOURCES= \ ZFSPoolPlugin.pm \ ZFSPlugin.pm \ DRBDPlugin.pm \ + PBSPlugin.pm \ LvmThinPlugin.pm .PHONY: install diff --git a/PVE/Storage/PBSPlugin.pm b/PVE/Storage/PBSPlugin.pm new file mode 100644 index 0000000..7142474 --- /dev/null +++ b/PVE/Storage/PBSPlugin.pm @@ -0,0 +1,358 @@ +package PVE::Storage::PBSPlugin; + +# Plugin to access Proxmox Backup Server + +use strict; +use warnings; +use POSIX qw(strftime); +use IO::File; +use HTTP::Request; +use LWP::UserAgent; +use JSON; +use Data::Dumper; # fixme: remove + +use PVE::Tools qw(run_command file_read_firstline trim dir_glob_regex dir_glob_foreach); +use PVE::Storage::Plugin; +use PVE::JSONSchema qw(get_standard_option); + +use base qw(PVE::Storage::Plugin); + +# Configuration + +sub type { + return 'pbs'; +} + +sub plugindata { + return { + content => [ {backup => 1, none => 1}, { backup => 1 }], + }; +} + +sub properties { + return { + datastore => { + description => "Proxmox backup server datastore name.", + type => 'string', + }, + # openssl s_client -connect :8007 2>&1 |openssl x509 -fingerprint -sha256 + fingerprint => get_standard_option('fingerprint-sha256'), + }; +} + +sub options { + return { + server => { fixed => 1 }, + datastore => { fixed => 1 }, + nodes => { optional => 1}, + disable => { optional => 1}, + content => { optional => 1}, + username => { optional => 1 }, + password => { optional => 1}, + maxfiles => { optional => 1 }, + fingerprint => { optional => 1 }, + }; +} + +# Helpers + +sub pbs_password_file_name { + my ($scfg, $storeid) = @_; + + return "/etc/pve/priv/${storeid}.pw"; +} + +sub pbs_set_password { + my ($scfg, $storeid, $password) = @_; + + my $pwfile = pbs_password_file_name($scfg, $storeid); + + PVE::Tools::file_set_contents($pwfile, "$password\n"); +} + +sub pbs_delete_password { + my ($scfg, $storeid) = @_; + + my $pwfile = pbs_password_file_name($scfg, $storeid); + + unlink $pwfile; +} + +sub pbs_get_password { + my ($scfg, $storeid) = @_; + + my $pwfile = pbs_password_file_name($scfg, $storeid); + + return PVE::Tools::file_read_firstline($pwfile); +} + + +sub run_raw_client_cmd { + my ($scfg, $storeid, $client_cmd, $param, %opts) = @_; + + my $server = $scfg->{server}; + my $datastore = $scfg->{datastore}; + my $username = $scfg->{username} // 'root@pam'; + + my $userns_cmd = delete $opts{userns_cmd}; + + my $cmd = []; + + push @$cmd, @$userns_cmd if defined($userns_cmd); + + push @$cmd, "/usr/bin/proxmox-backup-client", $client_cmd; + + push @$cmd, @$param if defined($param); + + push @$cmd, "--repository", "$username\@$server:$datastore"; + + local $ENV{PBS_PASSWORD} = pbs_get_password($scfg, $storeid); + + local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint}; + + if (my $logfunc = $opts{logfunc}) { + $logfunc->("run bps command: " . join(' ', @$cmd)); + } + + run_command($cmd, %opts); +} + +sub run_client_cmd { + my ($scfg, $storeid, $client_cmd, $param, $no_output) = @_; + + my $json_str = ''; + + my $outfunc = sub { + my $line = shift; + $json_str .= "$line\n"; + }; + + $param = [] if !defined($param); + $param = [ $param ] if !ref($param); + + $param = [@$param, '--output-format=json'] if !$no_output; + + run_raw_client_cmd($scfg, $storeid, $client_cmd, $param, + outfunc => $outfunc, errmsg => 'proxmox-backup-client failed'); + + return undef if $no_output; + + my $res = decode_json($json_str); + + return $res; +} + +# Storage implementation + +sub on_add_hook { + my ($class, $storeid, $scfg, %param) = @_; + + if (my $password = $param{password}) { + pbs_set_password($scfg, $storeid, $password); + } +} + +sub on_delete_hook { + my ($class, $storeid, $scfg) = @_; + + pbs_delete_password($scfg, $storeid); +} + +sub parse_volname { + my ($class, $volname) = @_; + + if ($volname =~ m!^backup/([^\s_]+)/([^\s_]+)/([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z)$!) { + my $btype = $1; + my $bid = $2; + my $btime = $3; + my $format = "pbs-$btype"; + + my $name = "$btype/$bid/$btime"; + + if ($bid =~ m/^\d+$/) { + return ('backup', $name, $bid, undef, undef, undef, $format); + } else { + return ('backup', $name, undef, undef, undef, undef, $format); + } + } + + die "unable to parse PBS volume name '$volname'\n"; +} + +sub path { + my ($class, $scfg, $volname, $storeid, $snapname) = @_; + + die "volume snapshot is not possible on pbs storage" + if defined($snapname); + + my ($vtype, $name, $vmid) = $class->parse_volname($volname); + + my $server = $scfg->{server}; + my $datastore = $scfg->{datastore}; + my $username = $scfg->{username} // 'root@pam'; + + # artifical url - we currently do not use that anywhere + my $path = "pbs://$username\@$server:$datastore/$name"; + + return ($path, $vmid, $vtype); +} + +sub create_base { + my ($class, $storeid, $scfg, $volname) = @_; + + die "can't create base images in pbs storage\n"; +} + +sub clone_image { + my ($class, $scfg, $storeid, $volname, $vmid, $snap) = @_; + + die "can't clone images in pbs storage\n"; +} + +sub alloc_image { + my ($class, $storeid, $scfg, $vmid, $fmt, $name, $size) = @_; + + die "can't allocate space in pbs storage\n"; +} + +sub free_image { + my ($class, $storeid, $scfg, $volname, $isBase) = @_; + + my ($vtype, $name, $vmid) = $class->parse_volname($volname); + + run_client_cmd($scfg, $storeid, "forget", [ $name ], 1); +} + + +sub list_images { + my ($class, $storeid, $scfg, $vmid, $vollist, $cache) = @_; + + my $res = []; + + return $res; +} + +sub list_volumes { + my ($class, $storeid, $scfg, $vmid, $content_types) = @_; + + my $res = []; + + return $res if !grep { $_ eq 'backup' } @$content_types; + + my $data = run_client_cmd($scfg, $storeid, "snapshots"); + + foreach my $item (@$data) { + my $btype = $item->{"backup-type"}; + my $bid = $item->{"backup-id"}; + my $btime = $item->{"backup-time"}; + my $size = $item->{size} // 1; + + next if !($btype eq 'vm' || $btype eq 'ct'); + next if $bid !~ m/^\d+$/; + + $btime = strftime("%FT%TZ", gmtime($btime)); + my $volname = "backup/${btype}/${bid}/${btime}"; + + my $volid = "$storeid:$volname"; + + my $info = { volid => $volid , format => "pbs-$btype", size => $size, content => 'backup', vmid => int($bid) }; + + push @$res, $info; + } + + return $res; +} + +sub status { + my ($class, $storeid, $scfg, $cache) = @_; + + my $total = 0; + my $free = 0; + my $used = 0; + my $active = 0; + + eval { + my $res = run_client_cmd($scfg, $storeid, "status"); + + $active = 1; + $total = $res->{total}; + $used = $res->{used}; + $free = $res->{avail}; + }; + if (my $err = $@) { + warn $err; + } + + return ($total, $free, $used, $active); +} + +sub activate_storage { + my ($class, $storeid, $scfg, $cache) = @_; + return 1; +} + +sub deactivate_storage { + my ($class, $storeid, $scfg, $cache) = @_; + return 1; +} + +sub activate_volume { + my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; + + die "volume snapshot is not possible on pbs device" if $snapname; + + return 1; +} + +sub deactivate_volume { + my ($class, $storeid, $scfg, $volname, $snapname, $cache) = @_; + + die "volume snapshot is not possible on pbs device" if $snapname; + + return 1; +} + +sub volume_size_info { + my ($class, $scfg, $storeid, $volname, $timeout) = @_; + + my ($vtype, $name, undef, undef, undef, undef, $format) = $class->parse_volname($volname); + + my $data = run_client_cmd($scfg, $storeid, "files", [ $name ]); + + my $size = 0; + foreach my $info (@$data) { + $size += $info->{size} if $info->{size}; + } + + my $used = $size; + + return wantarray ? ($size, $format, $used, undef) : $size; +} + +sub volume_resize { + my ($class, $scfg, $storeid, $volname, $size, $running) = @_; + die "volume resize is not possible on pbs device"; +} + +sub volume_snapshot { + my ($class, $scfg, $storeid, $volname, $snap) = @_; + die "volume snapshot is not possible on pbs device"; +} + +sub volume_snapshot_rollback { + my ($class, $scfg, $storeid, $volname, $snap) = @_; + die "volume snapshot rollback is not possible on pbs device"; +} + +sub volume_snapshot_delete { + my ($class, $scfg, $storeid, $volname, $snap) = @_; + die "volume snapshot delete is not possible on pbs device"; +} + +sub volume_has_feature { + my ($class, $scfg, $feature, $storeid, $volname, $snapname, $running) = @_; + + return undef; +} + +1;