]> git.proxmox.com Git - pve-common.git/blame - src/PVE/PBSClient.pm
add PBSClient module
[pve-common.git] / src / PVE / PBSClient.pm
CommitLineData
0904f388
SI
1package PVE::PBSClient;
2
3# utility functions for interaction with Proxmox Backup Server
4
5use strict;
6use warnings;
7use Fcntl qw(F_GETFD F_SETFD FD_CLOEXEC);
8use IO::File;
9use JSON;
10use POSIX qw(strftime ENOENT);
11
12use PVE::Tools qw(run_command file_set_contents file_get_contents file_read_firstline);
13use PVE::JSONSchema qw(get_standard_option);
14
15sub new {
16 my ($class, $scfg, $storeid, $sdir) = @_;
17
18 die "no section config provided\n" if ref($scfg) eq '';
19 die "undefined store id\n" if !defined($storeid);
20
21 my $secret_dir = $sdir // '/etc/pve/priv/storage';
22
23 my $self = bless { scfg => $scfg, storeid => $storeid, secret_dir => $secret_dir }, $class;
24}
25
26my sub password_file_name {
27 my ($self) = @_;
28
29 return "$self->{secret_dir}/$self->{storeid}.pw";
30}
31
32sub set_password {
33 my ($self, $password) = @_;
34
35 my $pwfile = password_file_name($self);
36 mkdir $self->{secret_dir};
37
38 PVE::Tools::file_set_contents($pwfile, "$password\n", 0600);
39};
40
41sub delete_password {
42 my ($self) = @_;
43
44 my $pwfile = password_file_name($self);
45
46 unlink $pwfile;
47};
48
49sub get_password {
50 my ($self) = @_;
51
52 my $pwfile = password_file_name($self);
53
54 return PVE::Tools::file_read_firstline($pwfile);
55}
56
57sub encryption_key_file_name {
58 my ($self) = @_;
59
60 return "$self->{secret_dir}/$self->{storeid}.enc";
61};
62
63sub set_encryption_key {
64 my ($self, $key) = @_;
65
66 my $encfile = encryption_key_file_name($self);
67 mkdir $self->{secret_dir};
68
69 PVE::Tools::file_set_contents($encfile, "$key\n", 0600);
70};
71
72sub delete_encryption_key {
73 my ($self) = @_;
74
75 my $encfile = encryption_key_file_name($self);
76
77 if (!unlink $encfile) {
78 return if $! == ENOENT;
79 die "failed to delete encryption key! $!\n";
80 }
81};
82
83# Returns a file handle if there is an encryption key, or `undef` if there is not. Dies on error.
84my sub open_encryption_key {
85 my ($self) = @_;
86
87 my $encryption_key_file = encryption_key_file_name($self);
88
89 my $keyfd;
90 if (!open($keyfd, '<', $encryption_key_file)) {
91 return undef if $! == ENOENT;
92 die "failed to open encryption key: $encryption_key_file: $!\n";
93 }
94
95 return $keyfd;
96}
97
98my $USE_CRYPT_PARAMS = {
99 backup => 1,
100 restore => 1,
101 'upload-log' => 1,
102};
103
104my sub do_raw_client_cmd {
105 my ($self, $client_cmd, $param, %opts) = @_;
106
107 my $use_crypto = $USE_CRYPT_PARAMS->{$client_cmd};
108
109 my $client_exe = '/usr/bin/proxmox-backup-client';
110 die "executable not found '$client_exe'! Proxmox backup client not installed?\n"
111 if ! -x $client_exe;
112
113 my $scfg = $self->{scfg};
114 my $server = $scfg->{server};
115 my $datastore = $scfg->{datastore};
116 my $username = $scfg->{username} // 'root@pam';
117
118 my $userns_cmd = delete $opts{userns_cmd};
119
120 my $cmd = [];
121
122 push @$cmd, @$userns_cmd if defined($userns_cmd);
123
124 push @$cmd, $client_exe, $client_cmd;
125
126 # This must live in the top scope to not get closed before the `run_command`
127 my $keyfd;
128 if ($use_crypto) {
129 if (defined($keyfd = open_encryption_key($self))) {
130 my $flags = fcntl($keyfd, F_GETFD, 0)
131 // die "failed to get file descriptor flags: $!\n";
132 fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
133 or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
134 push @$cmd, '--crypt-mode=encrypt', '--keyfd='.fileno($keyfd);
135 } else {
136 push @$cmd, '--crypt-mode=none';
137 }
138 }
139
140 push @$cmd, @$param if defined($param);
141
142 push @$cmd, "--repository", "$username\@$server:$datastore";
143
144 local $ENV{PBS_PASSWORD} = get_password($self);
145
146 local $ENV{PBS_FINGERPRINT} = $scfg->{fingerprint};
147
148 # no ascii-art on task logs
149 local $ENV{PROXMOX_OUTPUT_NO_BORDER} = 1;
150 local $ENV{PROXMOX_OUTPUT_NO_HEADER} = 1;
151
152 if (my $logfunc = $opts{logfunc}) {
153 $logfunc->("run: " . join(' ', @$cmd));
154 }
155
156 run_command($cmd, %opts);
157}
158
159my sub run_raw_client_cmd {
160 my ($self, $client_cmd, $param, %opts) = @_;
161 return do_raw_client_cmd($self, $client_cmd, $param, %opts);
162}
163
164my sub run_client_cmd {
165 my ($self, $client_cmd, $param, $no_output) = @_;
166
167 my $json_str = '';
168 my $outfunc = sub { $json_str .= "$_[0]\n" };
169
170 $param = [] if !defined($param);
171 $param = [ $param ] if !ref($param);
172
173 $param = [@$param, '--output-format=json'] if !$no_output;
174
175 do_raw_client_cmd($self, $client_cmd, $param,
176 outfunc => $outfunc, errmsg => 'proxmox-backup-client failed');
177
178 return undef if $no_output;
179
180 my $res = decode_json($json_str);
181
182 return $res;
183}
184
185sub autogen_encryption_key {
186 my ($self) = @_;
187 my $encfile = encryption_key_file_name($self);
188 run_command(['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile]);
189};
190
191sub get_snapshots {
192 my ($self, $opts) = @_;
193
194 my $param = [];
195 if (defined($opts->{group})) {
196 push @$param, $opts->{group};
197 }
198
199 return run_client_cmd($self, "snapshots", $param);
200};
201
202sub backup_tree {
203 my ($self, $opts) = @_;
204
205 my $type = delete $opts->{type};
206 die "backup-type not provided\n" if !defined($type);
207 my $id = delete $opts->{id};
208 die "backup-id not provided\n" if !defined($id);
209 my $root = delete $opts->{root};
210 die "root dir not provided\n" if !defined($root);
211 my $pxarname = delete $opts->{pxarname};
212 die "archive name not provided\n" if !defined($pxarname);
213 my $time = delete $opts->{time};
214
215 my $param = [];
216
217 push @$param, "$pxarname.pxar:$root";
218 push @$param, '--backup-type', $type;
219 push @$param, '--backup-id', $id;
220 push @$param, '--backup-time', $time if defined($time);
221
222 return run_raw_client_cmd($self, 'backup', $param, %$opts);
223};
224
225sub restore_pxar {
226 my ($self, $opts) = @_;
227
228 my $snapshot = delete $opts->{snapshot};
229 die "snapshot not provided\n" if !defined($snapshot);
230 my $pxarname = delete $opts->{pxarname};
231 die "archive name not provided\n" if !defined($pxarname);
232 my $target = delete $opts->{target};
233 die "restore-target not provided\n" if !defined($target);
234 #my $time = delete $opts->{time};
235
236 my $param = [];
237
238 push @$param, "$snapshot";
239 push @$param, "$pxarname.pxar";
240 push @$param, "$target";
241 push @$param, "--allow-existing-dirs", 0;
242
243 return run_raw_client_cmd($self, 'restore', $param, %$opts);
244};
245
246sub forget_snapshot {
247 my ($self, $snapshot) = @_;
248
249 die "snapshot not provided\n" if !defined($snapshot);
250
251 my $param = [];
252
253 push @$param, "$snapshot";
254
255 return run_raw_client_cmd($self, 'forget', $param);
256};
257
258sub prune_group {
259 my ($self, $opts, $prune_opts, $group) = @_;
260
261 die "group not provided\n" if !defined($group);
262
263 # do nothing if no keep options specified for remote
264 return [] if scalar(keys %$prune_opts) == 0;
265
266 my $param = [];
267
268 push @$param, "--quiet";
269
270 if (defined($opts->{'dry-run'}) && $opts->{'dry-run'}) {
271 push @$param, "--dry-run", $opts->{'dry-run'};
272 }
273
274 foreach my $keep_opt (keys %$prune_opts) {
275 push @$param, "--$keep_opt", $prune_opts->{$keep_opt};
276 }
277 push @$param, "$group";
278
279 return run_client_cmd($self, 'prune', $param);
280};
281
282sub status {
283 my ($self) = @_;
284
285 my $total = 0;
286 my $free = 0;
287 my $used = 0;
288 my $active = 0;
289
290 eval {
291 my $res = run_client_cmd($self, "status");
292
293 $active = 1;
294 $total = $res->{total};
295 $used = $res->{used};
296 $free = $res->{avail};
297 };
298 if (my $err = $@) {
299 warn $err;
300 }
301
302 return ($total, $free, $used, $active);
303};
304
3051;