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