appliance info: update apltest for zstd based archives
[pve-manager.git] / www / manager6 / storage / PBSEdit.js
1 /*global QRCode*/
2 Ext.define('PVE.Storage.PBSKeyShow', {
3 extend: 'Ext.window.Window',
4 xtype: 'pvePBSKeyShow',
5 mixins: ['Proxmox.Mixin.CBind'],
6
7 width: 600,
8 modal: true,
9 resizable: false,
10 title: gettext('Important: Save your Encryption Key'),
11
12 // avoid that esc closes this by mistake, force user to more manual action
13 onEsc: Ext.emptyFn,
14 closable: false,
15
16 items: [
17 {
18 xtype: 'form',
19 layout: {
20 type: 'vbox',
21 align: 'stretch',
22 },
23 bodyPadding: 10,
24 border: false,
25 defaults: {
26 anchor: '100%',
27 border: false,
28 padding: '10 0 0 0',
29 },
30 items: [
31 {
32 xtype: 'textfield',
33 fieldLabel: gettext('Key'),
34 labelWidth: 80,
35 inputId: 'encryption-key-value',
36 cbind: {
37 value: '{key}',
38 },
39 editable: false,
40 },
41 {
42 xtype: 'component',
43 html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.')
44 + '<br>' + gettext('We recommend the following safe-keeping strategy:'),
45 },
46 {
47 xtyp: 'container',
48 layout: 'hbox',
49 items: [
50 {
51 xtype: 'component',
52 html: '1. ' + gettext('Save the key in your password manager.'),
53 flex: 1,
54 },
55 {
56 xtype: 'button',
57 text: gettext('Copy Key'),
58 iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small',
59 cls: 'x-btn-default-toolbar-small proxmox-inline-button',
60 width: 110,
61 handler: function(b) {
62 document.getElementById('encryption-key-value').select();
63 document.execCommand("copy");
64 },
65 },
66 ],
67 },
68 {
69 xtype: 'container',
70 layout: 'hbox',
71 items: [
72 {
73 xtype: 'component',
74 html: '2. ' + gettext('Download the key to a USB (pen) drive, placed in secure vault.'),
75 flex: 1,
76 },
77 {
78 xtype: 'button',
79 text: gettext('Download'),
80 iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small',
81 cls: 'x-btn-default-toolbar-small proxmox-inline-button',
82 width: 110,
83 handler: function(b) {
84 let win = this.up('window');
85
86 let pveID = PVE.ClusterName || window.location.hostname;
87 let name = `pve-${pveID}-storage-${win.sid}.enc`;
88
89 let hiddenElement = document.createElement('a');
90 hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key);
91 hiddenElement.target = '_blank';
92 hiddenElement.download = name;
93 hiddenElement.click();
94 },
95 },
96 ],
97 },
98 {
99 xtype: 'container',
100 layout: 'hbox',
101 items: [
102 {
103 xtype: 'component',
104 html: '3. ' + gettext('Print as paperkey, laminated and placed in secure vault.'),
105 flex: 1,
106 },
107 {
108 xtype: 'button',
109 text: gettext('Print Key'),
110 iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small',
111 cls: 'x-btn-default-toolbar-small proxmox-inline-button',
112 width: 110,
113 handler: function(b) {
114 let win = this.up('window');
115 win.paperkey(win.key);
116 },
117 },
118 ],
119 },
120 ],
121 },
122 {
123 xtype: 'component',
124 border: false,
125 padding: '10 10 10 10',
126 userCls: 'pmx-hint',
127 html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'),
128 },
129 ],
130 buttons: [
131 {
132 text: gettext('Close'),
133 handler: function(b) {
134 let win = this.up('window');
135 win.close();
136 },
137 },
138 ],
139 paperkey: function(keyString) {
140 let me = this;
141
142 const key = JSON.parse(keyString);
143
144 const qrwidth = 500;
145 let qrdiv = document.createElement('div');
146 let qrcode = new QRCode(qrdiv, {
147 width: qrwidth,
148 height: qrwidth,
149 correctLevel: QRCode.CorrectLevel.H,
150 });
151 qrcode.makeCode(keyString);
152
153 let shortKeyFP = '';
154 if (key.fingerprint) {
155 shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint);
156 }
157
158 let printFrame = document.createElement("iframe");
159 Object.assign(printFrame.style, {
160 position: "fixed",
161 right: "0",
162 bottom: "0",
163 width: "0",
164 height: "0",
165 border: "0",
166 });
167 const prettifiedKey = JSON.stringify(key, null, 2);
168 const keyQrBase64 = qrdiv.children[0].toDataURL("image/png");
169 const html = `<html><head><script>
170 window.addEventListener('DOMContentLoaded', (ev) => window.print());
171 </script><style>@media print and (max-height: 150mm) {
172 h4, p { margin: 0; font-size: 1em; }
173 }</style></head><body style="padding: 5px;">
174 <h4>Encryption Key - Storage '${me.sid}' (${shortKeyFP})</h4>
175 <p style="font-size:1.2em;font-family:monospace;white-space:pre-wrap;overflow-wrap:break-word;">
176 -----BEGIN PROXMOX BACKUP KEY-----
177 ${prettifiedKey}
178 -----END PROXMOX BACKUP KEY-----</p>
179 <center><img style="width: 100%; max-width: ${qrwidth}px;" src="${keyQrBase64}"></center>
180 </body></html>`;
181
182 printFrame.src = "data:text/html;base64," + btoa(html);
183 document.body.appendChild(printFrame);
184 },
185 });
186
187 Ext.define('PVE.panel.PBSEncryptionKeyTab', {
188 extend: 'Proxmox.panel.InputPanel',
189 xtype: 'pvePBSEncryptionKeyTab',
190 mixins: ['Proxmox.Mixin.CBind'],
191
192 onlineHelp: 'storage_pbs_encryption',
193
194 onGetValues: function(form) {
195 let values = {};
196 if (form.cryptMode === 'upload') {
197 values['encryption-key'] = form['crypt-key-upload'];
198 } else if (form.cryptMode === 'autogenerate') {
199 values['encryption-key'] = 'autogen';
200 } else if (form.cryptMode === 'none') {
201 if (!this.isCreate) {
202 values.delete = ['encryption-key'];
203 }
204 }
205 return values;
206 },
207
208 setValues: function(values) {
209 let me = this;
210 let vm = me.getViewModel();
211
212 let cryptKeyInfo = values['encryption-key'];
213 if (cryptKeyInfo) {
214 let icon = '<span class="fa fa-lock good"></span> ';
215 if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint
216 let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo);
217 values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
218 } else {
219 // old key without FP
220 values['crypt-key-fp'] = icon + gettext('Active');
221 }
222 } else {
223 values['crypt-key-fp'] = gettext('None');
224 let cryptModeNone = me.down('radiofield[inputValue=none]');
225 cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
226 cryptModeNone.setValue(true);
227 }
228 vm.set('keepCryptVisible', !!cryptKeyInfo);
229 vm.set('allowEdit', !cryptKeyInfo);
230
231 me.callParent([values]);
232 },
233
234 viewModel: {
235 data: {
236 allowEdit: true,
237 keepCryptVisible: false,
238 },
239 formulas: {
240 showDangerousHint: get => {
241 let allowEdit = get('allowEdit');
242 return get('keepCryptVisible') && allowEdit;
243 },
244 },
245 },
246
247 items: [
248 {
249 xtype: 'displayfield',
250 name: 'crypt-key-fp',
251 fieldLabel: gettext('Encryption Key'),
252 padding: '2 0',
253 },
254 {
255 xtype: 'checkbox',
256 name: 'crypt-allow-edit',
257 boxLabel: gettext('Edit existing encryption key (dangerous!)'),
258 hidden: true,
259 submitValue: false,
260 isDirty: () => false,
261 bind: {
262 hidden: '{!keepCryptVisible}',
263 value: '{allowEdit}',
264 },
265 },
266 {
267 xtype: 'radiofield',
268 name: 'cryptMode',
269 inputValue: 'keep',
270 boxLabel: gettext('Keep encryption key'),
271 padding: '0 0 0 25',
272 cbind: {
273 hidden: '{isCreate}',
274 checked: '{!isCreate}',
275 },
276 bind: {
277 hidden: '{!keepCryptVisible}',
278 disabled: '{!allowEdit}',
279 },
280 },
281 {
282 xtype: 'radiofield',
283 name: 'cryptMode',
284 inputValue: 'none',
285 checked: true,
286 padding: '0 0 0 25',
287 cbind: {
288 disabled: '{!isCreate}',
289 checked: '{isCreate}',
290 boxLabel: get => get('isCreate')
291 ? gettext('Do not encrypt backups')
292 : gettext('Delete existing encryption key'),
293 },
294 bind: {
295 disabled: '{!allowEdit}',
296 },
297 },
298 {
299 xtype: 'radiofield',
300 name: 'cryptMode',
301 inputValue: 'autogenerate',
302 boxLabel: gettext('Auto-generate a client encryption key'),
303 padding: '0 0 0 25',
304 cbind: {
305 disabled: '{!isCreate}',
306 },
307 bind: {
308 disabled: '{!allowEdit}',
309 },
310 },
311 {
312 xtype: 'radiofield',
313 name: 'cryptMode',
314 inputValue: 'upload',
315 boxLabel: gettext('Upload an existing client encryption key'),
316 padding: '0 0 0 25',
317 cbind: {
318 disabled: '{!isCreate}',
319 },
320 bind: {
321 disabled: '{!allowEdit}',
322 },
323 listeners: {
324 change: function(f, value) {
325 let panel = this.up('inputpanel');
326 if (!panel.rendered) {
327 return;
328 }
329 let uploadKeyField = panel.down('field[name=crypt-key-upload]');
330 uploadKeyField.setDisabled(!value);
331 uploadKeyField.setHidden(!value);
332
333 let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
334 uploadKeyButton.setDisabled(!value);
335 uploadKeyButton.setHidden(!value);
336
337 if (value) {
338 uploadKeyField.validate();
339 } else {
340 uploadKeyField.reset();
341 }
342 },
343 },
344 },
345 {
346 xtype: 'fieldcontainer',
347 layout: 'hbox',
348 items: [
349 {
350 xtype: 'proxmoxtextfield',
351 name: 'crypt-key-upload',
352 fieldLabel: gettext('Key'),
353 value: '',
354 disabled: true,
355 hidden: true,
356 allowBlank: false,
357 labelAlign: 'right',
358 flex: 1,
359 emptyText: gettext('You can drag-and-drop a key file here.'),
360 validator: function(value) {
361 if (value.length) {
362 let key;
363 try {
364 key = JSON.parse(value);
365 } catch (e) {
366 return "Failed to parse key - " + e;
367 }
368 if (key.data === undefined) {
369 return "Does not seems like a valid Proxmox Backup key!";
370 }
371 }
372 return true;
373 },
374 afterRender: function() {
375 if (!window.FileReader) {
376 // No FileReader support in this browser
377 return;
378 }
379 let cancel = function(ev) {
380 ev = ev.event;
381 if (ev.preventDefault) {
382 ev.preventDefault();
383 }
384 };
385 this.inputEl.on('dragover', cancel);
386 this.inputEl.on('dragenter', cancel);
387 this.inputEl.on('drop', ev => {
388 cancel(ev);
389 let files = ev.event.dataTransfer.files;
390 PVE.Utils.loadTextFromFile(files[0], v => this.setValue(v));
391 });
392 },
393 },
394 {
395 xtype: 'filebutton',
396 name: 'crypt-upload-button',
397 iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
398 cls: 'x-btn-default-toolbar-small proxmox-inline-button',
399 margin: '0 0 0 4',
400 disabled: true,
401 hidden: true,
402 listeners: {
403 change: function(btn, e, value) {
404 let ev = e.event;
405 let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
406 PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v));
407 btn.reset();
408 },
409 },
410 },
411 ],
412 },
413 {
414 xtype: 'component',
415 border: false,
416 padding: '5 2',
417 userCls: 'pmx-hint',
418 html: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` +
419 `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` +
420 gettext('Deleting or replacing the encryption key will break restoring backups created with it!'),
421 hidden: true,
422 bind: {
423 hidden: '{!showDangerousHint}',
424 },
425 },
426 ],
427 });
428
429 Ext.define('PVE.storage.PBSInputPanel', {
430 extend: 'PVE.panel.StorageBase',
431
432 onlineHelp: 'storage_pbs',
433
434 apiCallDone: function(success, response, options) {
435 let res = response.result.data;
436 if (!(res && res.config && res.config['encryption-key'])) {
437 return;
438 }
439 let key = res.config['encryption-key'];
440 Ext.create('PVE.Storage.PBSKeyShow', {
441 autoShow: true,
442 sid: res.storage,
443 key: key,
444 });
445 },
446
447 isPBS: true, // HACK
448
449 extraTabs: [
450 {
451 xtype: 'pvePBSEncryptionKeyTab',
452 title: gettext('Encryption'),
453 },
454 ],
455
456 setValues: function(values) {
457 let me = this;
458
459 let server = values.server;
460 if (values.port !== undefined) {
461 if (Proxmox.Utils.IP6_match.test(server)) {
462 server = `[${server}]`;
463 }
464 server += `:${values.port}`;
465 }
466 values.hostport = server;
467
468 return me.callParent([values]);
469 },
470
471 initComponent: function() {
472 var me = this;
473
474 me.column1 = [
475 {
476 xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
477 fieldLabel: gettext('Server'),
478 allowBlank: false,
479 name: 'hostport',
480 submitValue: false,
481 vtype: 'HostPort',
482 listeners: {
483 change: function(field, newvalue) {
484 let server = newvalue;
485 let port;
486
487 let match = Proxmox.Utils.HostPort_match.exec(newvalue);
488 if (match === null) {
489 match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue);
490 if (match === null) {
491 match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue);
492 }
493 }
494
495 if (match !== null) {
496 server = match[1];
497 if (match[2] !== undefined) {
498 port = match[2];
499 }
500 }
501
502 field.up('inputpanel').down('field[name=server]').setValue(server);
503 field.up('inputpanel').down('field[name=port]').setValue(port);
504 },
505 },
506 },
507 {
508 xtype: 'proxmoxtextfield',
509 hidden: true,
510 name: 'server',
511 submitValue: me.isCreate, // it is fixed
512 },
513 {
514 xtype: 'proxmoxtextfield',
515 hidden: true,
516 deleteEmpty: !me.isCreate,
517 name: 'port',
518 },
519 {
520 xtype: me.isCreate ? 'textfield' : 'displayfield',
521 name: 'username',
522 value: '',
523 emptyText: gettext('Example') + ': admin@pbs',
524 fieldLabel: gettext('Username'),
525 regex: /\S+@\w+/,
526 regexText: gettext('Example') + ': admin@pbs',
527 allowBlank: false,
528 },
529 {
530 xtype: me.isCreate ? 'textfield' : 'displayfield',
531 inputType: 'password',
532 name: 'password',
533 value: me.isCreate ? '' : '********',
534 emptyText: me.isCreate ? gettext('None') : '',
535 fieldLabel: gettext('Password'),
536 allowBlank: false,
537 },
538 ];
539
540 me.column2 = [
541 {
542 xtype: 'displayfield',
543 name: 'content',
544 value: 'backup',
545 submitValue: true,
546 fieldLabel: gettext('Content'),
547 },
548 {
549 xtype: me.isCreate ? 'textfield' : 'displayfield',
550 name: 'datastore',
551 value: '',
552 fieldLabel: 'Datastore',
553 allowBlank: false,
554 },
555 {
556 xtype: me.isCreate ? 'textfield' : 'displayfield',
557 name: 'namespace',
558 value: '',
559 emptyText: gettext('Root'),
560 fieldLabel: gettext('Namespace'),
561 allowBlank: true,
562 },
563 ];
564
565 me.columnB = [
566 {
567 xtype: 'proxmoxtextfield',
568 name: 'fingerprint',
569 value: me.isCreate ? null : undefined,
570 fieldLabel: gettext('Fingerprint'),
571 emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
572 regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
573 regexText: gettext('Example') + ': AB:CD:EF:...',
574 deleteEmpty: !me.isCreate,
575 allowBlank: true,
576 },
577 ];
578
579 me.callParent();
580 },
581 });