]> git.proxmox.com Git - pve-manager.git/blobdiff - www/manager6/storage/PBSEdit.js
bump version to 8.2.7
[pve-manager.git] / www / manager6 / storage / PBSEdit.js
index c49c0f306224489a5bc4cff87ecbe0bedf9ce47d..70dc42f5786f663fa7f2a29bcc14c56532ee565e 100644 (file)
@@ -1,70 +1,7 @@
 /*global QRCode*/
-Ext.define('Proxmox.form.PBSEncryptionCheckbox', {
-    extend: 'Ext.form.field.Checkbox',
-    xtype: 'pbsEncryptionCheckbox',
-
-    inputValue: true,
-
-    viewModel: {
-       data: {
-           value: null,
-           originalValue: null,
-       },
-       formulas: {
-           blabel: (get) => {
-               let v = get('value');
-               let original = get('originalValue');
-               if (!get('isCreate') && original) {
-                   if (!v) {
-                       return gettext('Warning: Existing encryption key will be deleted!');
-                   }
-                   return gettext('Active');
-               } else {
-                   return gettext('Auto-generate a client encryption key, saved privately on cluster filesystem');
-               }
-           },
-       },
-    },
-
-    bind: {
-       value: '{value}',
-       boxLabel: '{blabel}',
-    },
-    resetOriginalValue: function() {
-       let me = this;
-       let vm = me.getViewModel();
-       vm.set('originalValue', me.value);
-
-       me.callParent(arguments);
-    },
-
-    getSubmitData: function() {
-       let me = this;
-       let val = me.getSubmitValue();
-       if (!me.isCreate) {
-           if (val === null) {
-              return { 'delete': 'encryption-key' };
-           } else if (val && !!val !== !!me.originalValue) {
-              return { 'encryption-key': 'autogen' };
-           }
-       } else if (val) {
-          return { 'encryption-key': 'autogen' };
-       }
-       return null;
-    },
-
-    initComponent: function() {
-       let me = this;
-       me.callParent();
-
-       let vm = me.getViewModel();
-       vm.set('isCreate', me.isCreate);
-    },
-});
-
 Ext.define('PVE.Storage.PBSKeyShow', {
     extend: 'Ext.window.Window',
-    alias: ['widget.pveKeyShow'],
+    xtype: 'pvePBSKeyShow',
     mixins: ['Proxmox.Mixin.CBind'],
 
     width: 600,
@@ -94,7 +31,7 @@ Ext.define('PVE.Storage.PBSKeyShow', {
                {
                    xtype: 'textfield',
                    fieldLabel: gettext('Key'),
-                   labelWidth: 30,
+                   labelWidth: 80,
                    inputId: 'encryption-key-value',
                    cbind: {
                        value: '{key}',
@@ -103,7 +40,7 @@ Ext.define('PVE.Storage.PBSKeyShow', {
                },
                {
                    xtype: 'component',
-                   html: gettext('Keep your master key safe, but easily accessible for disaster recovery.')
+                   html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.')
                        + '<br>' + gettext('We recommend the following safe-keeping strategy:'),
                },
                {
@@ -187,7 +124,7 @@ Ext.define('PVE.Storage.PBSKeyShow', {
            border: false,
            padding: '10 10 10 10',
            userCls: 'pmx-hint',
-           html: gettext('Please save the encryption key - loosing it will render any backup created with it unuseable'),
+           html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'),
        },
     ],
     buttons: [
@@ -199,9 +136,11 @@ Ext.define('PVE.Storage.PBSKeyShow', {
            },
        },
     ],
-    paperkey: function(key) {
+    paperkey: function(keyString) {
        let me = this;
 
+       const key = JSON.parse(keyString);
+
        const qrwidth = 500;
        let qrdiv = document.createElement('div');
        let qrcode = new QRCode(qrdiv, {
@@ -209,7 +148,12 @@ Ext.define('PVE.Storage.PBSKeyShow', {
            height: qrwidth,
            correctLevel: QRCode.CorrectLevel.H,
        });
-       qrcode.makeCode(key);
+       qrcode.makeCode(keyString);
+
+       let shortKeyFP = '';
+       if (key.fingerprint) {
+           shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint);
+       }
 
        let printFrame = document.createElement("iframe");
        Object.assign(printFrame.style, {
@@ -220,15 +164,15 @@ Ext.define('PVE.Storage.PBSKeyShow', {
            height: "0",
            border: "0",
        });
-       const prettifiedKey = JSON.stringify(JSON.parse(key), null, 2);
+       const prettifiedKey = JSON.stringify(key, null, 2);
        const keyQrBase64 = qrdiv.children[0].toDataURL("image/png");
        const html = `<html><head><script>
            window.addEventListener('DOMContentLoaded', (ev) => window.print());
        </script><style>@media print and (max-height: 150mm) {
          h4, p { margin: 0; font-size: 1em; }
        }</style></head><body style="padding: 5px;">
-       <h4>Encryption Key - Storage '${me.sid}'</h4>
-<p style="font-size: 1.2em; font-family: monospace; white-space: pre-wrap;">
+       <h4>Encryption Key - Storage '${me.sid}' (${shortKeyFP})</h4>
+<p style="font-size:1.2em;font-family:monospace;white-space:pre-wrap;overflow-wrap:break-word;">
 -----BEGIN PROXMOX BACKUP KEY-----
 ${prettifiedKey}
 -----END PROXMOX BACKUP KEY-----</p>
@@ -237,13 +181,256 @@ ${prettifiedKey}
 
        printFrame.src = "data:text/html;base64," + btoa(html);
        document.body.appendChild(printFrame);
+       me.on('destroy', () => document.body.removeChild(printFrame));
     },
 });
 
+Ext.define('PVE.panel.PBSEncryptionKeyTab', {
+    extend: 'Proxmox.panel.InputPanel',
+    xtype: 'pvePBSEncryptionKeyTab',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'storage_pbs_encryption',
+
+    onGetValues: function(form) {
+       let values = {};
+       if (form.cryptMode === 'upload') {
+           values['encryption-key'] = form['crypt-key-upload'];
+       } else if (form.cryptMode === 'autogenerate') {
+           values['encryption-key'] = 'autogen';
+       } else if (form.cryptMode === 'none') {
+           if (!this.isCreate) {
+               values.delete = ['encryption-key'];
+           }
+       }
+       return values;
+    },
+
+    setValues: function(values) {
+       let me = this;
+       let vm = me.getViewModel();
+
+       let cryptKeyInfo = values['encryption-key'];
+       if (cryptKeyInfo) {
+           let icon = '<span class="fa fa-lock good"></span> ';
+           if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint
+               let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo);
+               values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
+           } else {
+               // old key without FP
+               values['crypt-key-fp'] = icon + gettext('Active');
+           }
+       } else {
+           values['crypt-key-fp'] = gettext('None');
+           let cryptModeNone = me.down('radiofield[inputValue=none]');
+           cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
+           cryptModeNone.setValue(true);
+       }
+       vm.set('keepCryptVisible', !!cryptKeyInfo);
+       vm.set('allowEdit', !cryptKeyInfo);
+
+       me.callParent([values]);
+    },
+
+    viewModel: {
+       data: {
+           allowEdit: true,
+           keepCryptVisible: false,
+       },
+       formulas: {
+           showDangerousHint: get => {
+               let allowEdit = get('allowEdit');
+               return get('keepCryptVisible') && allowEdit;
+           },
+       },
+    },
+
+    items: [
+       {
+           xtype: 'displayfield',
+           name: 'crypt-key-fp',
+           fieldLabel: gettext('Encryption Key'),
+           padding: '2 0',
+       },
+       {
+           xtype: 'checkbox',
+           name: 'crypt-allow-edit',
+           boxLabel: gettext('Edit existing encryption key (dangerous!)'),
+           hidden: true,
+           submitValue: false,
+           isDirty: () => false,
+           bind: {
+               hidden: '{!keepCryptVisible}',
+               value: '{allowEdit}',
+           },
+       },
+       {
+           xtype: 'radiofield',
+           name: 'cryptMode',
+           inputValue: 'keep',
+           boxLabel: gettext('Keep encryption key'),
+           padding: '0 0 0 25',
+           cbind: {
+               hidden: '{isCreate}',
+               checked: '{!isCreate}',
+           },
+           bind: {
+               hidden: '{!keepCryptVisible}',
+               disabled: '{!allowEdit}',
+           },
+       },
+       {
+           xtype: 'radiofield',
+           name: 'cryptMode',
+           inputValue: 'none',
+           checked: true,
+           padding: '0 0 0 25',
+           cbind: {
+               disabled: '{!isCreate}',
+               checked: '{isCreate}',
+               boxLabel: get => get('isCreate')
+                   ? gettext('Do not encrypt backups')
+                   : gettext('Delete existing encryption key'),
+           },
+           bind: {
+               disabled: '{!allowEdit}',
+           },
+       },
+       {
+           xtype: 'radiofield',
+           name: 'cryptMode',
+           inputValue: 'autogenerate',
+           boxLabel: gettext('Auto-generate a client encryption key'),
+           padding: '0 0 0 25',
+           cbind: {
+               disabled: '{!isCreate}',
+           },
+           bind: {
+               disabled: '{!allowEdit}',
+           },
+       },
+       {
+           xtype: 'radiofield',
+           name: 'cryptMode',
+           inputValue: 'upload',
+           boxLabel: gettext('Upload an existing client encryption key'),
+           padding: '0 0 0 25',
+           cbind: {
+               disabled: '{!isCreate}',
+           },
+           bind: {
+               disabled: '{!allowEdit}',
+           },
+           listeners: {
+               change: function(f, value) {
+                   let panel = this.up('inputpanel');
+                   if (!panel.rendered) {
+                       return;
+                   }
+                   let uploadKeyField = panel.down('field[name=crypt-key-upload]');
+                   uploadKeyField.setDisabled(!value);
+                   uploadKeyField.setHidden(!value);
+
+                   let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
+                   uploadKeyButton.setDisabled(!value);
+                   uploadKeyButton.setHidden(!value);
+
+                   if (value) {
+                       uploadKeyField.validate();
+                   } else {
+                       uploadKeyField.reset();
+                   }
+               },
+           },
+       },
+       {
+           xtype: 'fieldcontainer',
+           layout: 'hbox',
+           items: [
+               {
+                   xtype: 'proxmoxtextfield',
+                   name: 'crypt-key-upload',
+                   fieldLabel: gettext('Key'),
+                   value: '',
+                   disabled: true,
+                   hidden: true,
+                   allowBlank: false,
+                   labelAlign: 'right',
+                   flex: 1,
+                   emptyText: gettext('You can drag-and-drop a key file here.'),
+                   validator: function(value) {
+                       if (value.length) {
+                           let key;
+                           try {
+                               key = JSON.parse(value);
+                           } catch (e) {
+                               return "Failed to parse key - " + e;
+                           }
+                           if (key.data === undefined) {
+                               return "Does not seems like a valid Proxmox Backup key!";
+                           }
+                       }
+                       return true;
+                   },
+                   afterRender: function() {
+                       if (!window.FileReader) {
+                           // No FileReader support in this browser
+                           return;
+                       }
+                       let cancel = function(ev) {
+                           ev = ev.event;
+                           if (ev.preventDefault) {
+                               ev.preventDefault();
+                           }
+                       };
+                       this.inputEl.on('dragover', cancel);
+                       this.inputEl.on('dragenter', cancel);
+                       this.inputEl.on('drop', ev => {
+                           cancel(ev);
+                           let files = ev.event.dataTransfer.files;
+                           PVE.Utils.loadTextFromFile(files[0], v => this.setValue(v));
+                       });
+                   },
+               },
+               {
+                   xtype: 'filebutton',
+                   name: 'crypt-upload-button',
+                   iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
+                   cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                   margin: '0 0 0 4',
+                   disabled: true,
+                   hidden: true,
+                   listeners: {
+                       change: function(btn, e, value) {
+                           let ev = e.event;
+                           let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
+                           PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v));
+                           btn.reset();
+                       },
+                   },
+               },
+           ],
+       },
+       {
+           xtype: 'component',
+           border: false,
+           padding: '5 2',
+           userCls: 'pmx-hint',
+           html: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` +
+             `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` +
+             gettext('Deleting or replacing the encryption key will break restoring backups created with it!'),
+           hidden: true,
+           bind: {
+               hidden: '{!showDangerousHint}',
+           },
+       },
+    ],
+});
+
 Ext.define('PVE.storage.PBSInputPanel', {
     extend: 'PVE.panel.StorageBase',
 
-    //onlineHelp: 'storage_pbs',
+    onlineHelp: 'storage_pbs',
 
     apiCallDone: function(success, response, options) {
        let res = response.result.data;
@@ -258,17 +445,77 @@ Ext.define('PVE.storage.PBSInputPanel', {
        });
     },
 
+    isPBS: true, // HACK
+
+    extraTabs: [
+       {
+           xtype: 'pvePBSEncryptionKeyTab',
+           title: gettext('Encryption'),
+       },
+    ],
+
+    setValues: function(values) {
+       let me = this;
+
+       let server = values.server;
+       if (values.port !== undefined) {
+           if (Proxmox.Utils.IP6_match.test(server)) {
+               server = `[${server}]`;
+           }
+           server += `:${values.port}`;
+       }
+       values.hostport = server;
+
+       return me.callParent([values]);
+    },
+
     initComponent: function() {
        var me = this;
 
        me.column1 = [
            {
-               xtype: me.isCreate ? 'textfield' : 'displayfield',
-               name: 'server',
-               value: '',
-               vtype: 'DnsOrIp',
+               xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
                fieldLabel: gettext('Server'),
                allowBlank: false,
+               name: 'hostport',
+               submitValue: false,
+               vtype: 'HostPort',
+               listeners: {
+                   change: function(field, newvalue) {
+                       let server = newvalue;
+                       let port;
+
+                       let match = Proxmox.Utils.HostPort_match.exec(newvalue);
+                       if (match === null) {
+                           match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue);
+                           if (match === null) {
+                               match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue);
+                           }
+                       }
+
+                       if (match !== null) {
+                           server = match[1];
+                           if (match[2] !== undefined) {
+                               port = match[2];
+                           }
+                       }
+
+                       field.up('inputpanel').down('field[name=server]').setValue(server);
+                       field.up('inputpanel').down('field[name=port]').setValue(port);
+                   },
+               },
+           },
+           {
+               xtype: 'proxmoxtextfield',
+               hidden: true,
+               name: 'server',
+               submitValue: me.isCreate, // it is fixed
+           },
+           {
+               xtype: 'proxmoxtextfield',
+               hidden: true,
+               deleteEmpty: !me.isCreate,
+               name: 'port',
            },
            {
                xtype: me.isCreate ? 'textfield' : 'displayfield',
@@ -289,13 +536,6 @@ Ext.define('PVE.storage.PBSInputPanel', {
                fieldLabel: gettext('Password'),
                allowBlank: false,
            },
-           {
-               xtype: me.isCreate ? 'textfield' : 'displayfield',
-               name: 'datastore',
-               value: '',
-               fieldLabel: 'Datastore',
-               allowBlank: false,
-           },
        ];
 
        me.column2 = [
@@ -306,6 +546,21 @@ Ext.define('PVE.storage.PBSInputPanel', {
                submitValue: true,
                fieldLabel: gettext('Content'),
            },
+           {
+               xtype: me.isCreate ? 'textfield' : 'displayfield',
+               name: 'datastore',
+               value: '',
+               fieldLabel: 'Datastore',
+               allowBlank: false,
+           },
+           {
+               xtype: me.isCreate ? 'textfield' : 'displayfield',
+               name: 'namespace',
+               value: '',
+               emptyText: gettext('Root'),
+               fieldLabel: gettext('Namespace'),
+               allowBlank: true,
+           },
        ];
 
        me.columnB = [
@@ -317,15 +572,9 @@ Ext.define('PVE.storage.PBSInputPanel', {
                emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
                regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
                regexText: gettext('Example') + ': AB:CD:EF:...',
+               deleteEmpty: !me.isCreate,
                allowBlank: true,
            },
-           {
-               // FIXME: allow uploading their own, maybe export for root@pam?
-               xtype: 'pbsEncryptionCheckbox',
-               name: 'encryption-key',
-               isCreate: me.isCreate,
-               fieldLabel: gettext('Encryption Key'),
-           },
        ];
 
        me.callParent();