]> git.proxmox.com Git - proxmox-backup.git/blob - www/Utils.js
fix #5117: ui: node info: avoid invalid array access for certain foreign kernels
[proxmox-backup.git] / www / Utils.js
1 Ext.ns('PBS');
2
3 console.log("Starting Backup Server GUI");
4
5 Ext.define('PBS.Utils', {
6 singleton: true,
7
8 missingText: gettext('missing'),
9
10 updateLoginData: function(data) {
11 Proxmox.Utils.setAuthData(data);
12 },
13
14 dataStorePrefix: 'DataStore-',
15
16 cryptmap: [
17 'none',
18 'mixed',
19 'sign-only',
20 'encrypt',
21 ],
22
23 cryptText: [
24 Proxmox.Utils.noText,
25 gettext('Mixed'),
26 gettext('Signed'),
27 gettext('Encrypted'),
28 ],
29
30 cryptIconCls: [
31 '',
32 '',
33 'lock faded',
34 'lock good',
35 ],
36
37 calculateCryptMode: function(data) {
38 let mixed = data.mixed;
39 let encrypted = data.encrypt;
40 let signed = data['sign-only'];
41 let files = data.count;
42 if (mixed > 0) {
43 return PBS.Utils.cryptmap.indexOf('mixed');
44 } else if (files === encrypted && encrypted > 0) {
45 return PBS.Utils.cryptmap.indexOf('encrypt');
46 } else if (files === signed && signed > 0) {
47 return PBS.Utils.cryptmap.indexOf('sign-only');
48 } else if ((signed+encrypted) === 0) {
49 return PBS.Utils.cryptmap.indexOf('none');
50 } else {
51 return PBS.Utils.cryptmap.indexOf('mixed');
52 }
53 },
54
55 noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit <a target="_blank" href="https://www.proxmox.com/proxmox-backup-server/pricing">www.proxmox.com</a> to get a list of available options.',
56
57 getDataStoreFromPath: function(path) {
58 return path.slice(PBS.Utils.dataStorePrefix.length);
59 },
60
61 isDataStorePath: function(path) {
62 return path.indexOf(PBS.Utils.dataStorePrefix) === 0;
63 },
64
65 parsePropertyString: function(value, defaultKey) {
66 var res = {},
67 error;
68
69 if (typeof value !== 'string' || value === '') {
70 return res;
71 }
72
73 Ext.Array.each(value.split(','), function(p) {
74 var kv = p.split('=', 2);
75 if (Ext.isDefined(kv[1])) {
76 res[kv[0]] = kv[1];
77 } else if (Ext.isDefined(defaultKey)) {
78 if (Ext.isDefined(res[defaultKey])) {
79 error = 'defaultKey may be only defined once in propertyString';
80 return false; // break
81 }
82 res[defaultKey] = kv[0];
83 } else {
84 error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
85 return false; // break
86 }
87 return true;
88 });
89
90 if (error !== undefined) {
91 console.error(error);
92 return null;
93 }
94
95 return res;
96 },
97
98 printPropertyString: function(data, defaultKey) {
99 var stringparts = [],
100 gotDefaultKeyVal = false,
101 defaultKeyVal;
102
103 Ext.Object.each(data, function(key, value) {
104 if (defaultKey !== undefined && key === defaultKey) {
105 gotDefaultKeyVal = true;
106 defaultKeyVal = value;
107 } else if (value !== '' && value !== undefined) {
108 stringparts.push(key + '=' + value);
109 }
110 });
111
112 stringparts = stringparts.sort();
113 if (gotDefaultKeyVal) {
114 stringparts.unshift(defaultKeyVal);
115 }
116
117 return stringparts.join(',');
118 },
119
120 // helper for deleting field which are set to there default values
121 delete_if_default: function(values, fieldname, default_val, create) {
122 if (values[fieldname] === '' || values[fieldname] === default_val) {
123 if (!create) {
124 if (values.delete) {
125 if (Ext.isArray(values.delete)) {
126 values.delete.push(fieldname);
127 } else {
128 values.delete += ',' + fieldname;
129 }
130 } else {
131 values.delete = [fieldname];
132 }
133 }
134
135 delete values[fieldname];
136 }
137 },
138
139
140 render_datetime_utc: function(datetime) {
141 let pad = (number) => number < 10 ? '0' + number : number;
142 return datetime.getUTCFullYear() +
143 '-' + pad(datetime.getUTCMonth() + 1) +
144 '-' + pad(datetime.getUTCDate()) +
145 'T' + pad(datetime.getUTCHours()) +
146 ':' + pad(datetime.getUTCMinutes()) +
147 ':' + pad(datetime.getUTCSeconds()) +
148 'Z';
149 },
150
151 render_datastore_worker_id: function(id, what) {
152 const res = id.match(/^(\S+?):(\S+?)\/(\S+?)(\/(.+))?$/);
153 if (res) {
154 let datastore = res[1], backupGroup = `${res[2]}/${res[3]}`;
155 if (res[4] !== undefined) {
156 let datetime = Ext.Date.parse(parseInt(res[5], 16), 'U');
157 let utctime = PBS.Utils.render_datetime_utc(datetime);
158 return `Datastore ${datastore} ${what} ${backupGroup}/${utctime}`;
159 } else {
160 return `Datastore ${datastore} ${what} ${backupGroup}`;
161 }
162 }
163 return `Datastore ${what} ${id}`;
164 },
165
166 render_prune_job_worker_id: function(id, what) {
167 const res = id.match(/^(\S+?):(\S+)$/);
168 if (!res) {
169 return `${what} on Datastore ${id}`;
170 }
171 let datastore = res[1], namespace = res[2];
172 return `${what} on Datastore ${datastore} Namespace ${namespace}`;
173 },
174
175 render_tape_backup_id: function(id, what) {
176 const res = id.match(/^(\S+?):(\S+?):(\S+?)(:(.+))?$/);
177 if (res) {
178 let datastore = res[1];
179 let pool = res[2];
180 let drive = res[3];
181 return `${what} ${datastore} (pool ${pool}, drive ${drive})`;
182 }
183 return `${what} ${id}`;
184 },
185
186 render_drive_load_media_id: function(id, what) {
187 const res = id.match(/^(\S+?):(\S+?)$/);
188 if (res) {
189 let drive = res[1];
190 let label = res[2];
191 return gettext('Drive') + ` ${drive} - ${what} '${label}'`;
192 }
193
194 return `${what} ${id}`;
195 },
196
197 // mimics Display trait in backend
198 renderKeyID: function(fingerprint) {
199 return fingerprint.substring(0, 23);
200 },
201
202 render_task_status: function(value, metadata, record) {
203 if (!record.data['last-run-upid']) {
204 return '-';
205 }
206
207 if (!record.data['last-run-endtime']) {
208 metadata.tdCls = 'x-grid-row-loading';
209 return '';
210 }
211
212 let parsed = Proxmox.Utils.parse_task_status(value);
213 let text = value;
214 let icon = '';
215 switch (parsed) {
216 case 'unknown':
217 icon = 'question faded';
218 text = Proxmox.Utils.unknownText;
219 break;
220 case 'error':
221 icon = 'times critical';
222 text = Proxmox.Utils.errorText + ': ' + value;
223 break;
224 case 'warning':
225 icon = 'exclamation warning';
226 break;
227 case 'ok':
228 icon = 'check good';
229 text = gettext("OK");
230 }
231
232 return `<i class="fa fa-${icon}"></i> ${text}`;
233 },
234
235 render_next_task_run: function(value, metadat, record) {
236 if (!value) return '-';
237
238 let now = new Date();
239 let next = new Date(value*1000);
240
241 if (next < now) {
242 return gettext('pending');
243 }
244 return Proxmox.Utils.render_timestamp(value);
245 },
246
247 render_optional_timestamp: function(value, metadata, record) {
248 if (!value) return '-';
249 return Proxmox.Utils.render_timestamp(value);
250 },
251
252 parse_datastore_worker_id: function(type, id) {
253 let result;
254 let res;
255 if (type.startsWith('verif')) {
256 res = PBS.Utils.VERIFICATION_JOB_ID_RE.exec(id);
257 if (res) {
258 result = res[1];
259 }
260 } else if (type.startsWith('sync')) {
261 res = PBS.Utils.SYNC_JOB_ID_RE.exec(id);
262 if (res) {
263 result = res[3];
264 }
265 } else if (type === 'backup') {
266 res = PBS.Utils.BACKUP_JOB_ID_RE.exec(id);
267 if (res) {
268 result = res[1];
269 }
270 } else if (type === 'garbage_collection') {
271 return id;
272 } else if (type === 'prune') {
273 return id;
274 }
275
276
277 return result;
278 },
279
280 extractTokenUser: function(tokenid) {
281 return tokenid.match(/^(.+)!([^!]+)$/)[1];
282 },
283
284 extractTokenName: function(tokenid) {
285 return tokenid.match(/^(.+)!([^!]+)$/)[2];
286 },
287
288 render_estimate: function(value, metaData, record) {
289 if (record.data.avail === 0) {
290 return gettext("Full");
291 }
292
293 if (value === undefined) {
294 return gettext('Not enough data');
295 }
296
297 let now = new Date();
298 let estimate = new Date(value*1000);
299
300 let timespan = (estimate - now)/1000;
301
302 if (Number(estimate) <= Number(now) || isNaN(timespan)) {
303 return gettext('Never');
304 }
305
306 let duration = Proxmox.Utils.format_duration_human(timespan);
307 return Ext.String.format(gettext("in {0}"), duration);
308 },
309
310 // FIXME: deprecated by Proxmox.Utils.render_size_usage ?!
311 render_size_usage: function(val, max) {
312 if (max === 0) {
313 return gettext('N/A');
314 }
315 return (val*100/max).toFixed(2) + '% (' +
316 Ext.String.format(gettext('{0} of {1}'),
317 Proxmox.Utils.format_size(val), Proxmox.Utils.format_size(max)) + ')';
318 },
319
320 get_help_tool: function(blockid) {
321 let info = Proxmox.Utils.get_help_info(blockid);
322 if (info === undefined) {
323 info = Proxmox.Utils.get_help_info('pbs_documentation_index');
324 }
325 if (info === undefined) {
326 throw "get_help_info failed"; // should not happen
327 }
328
329 let docsURI = window.location.origin + info.link;
330 let title = info.title;
331 if (info.subtitle) {
332 title += ' - ' + info.subtitle;
333 }
334 return {
335 type: 'help',
336 tooltip: title,
337 handler: function() {
338 window.open(docsURI);
339 },
340 };
341 },
342
343 calculate_dedup_factor: function(gcstatus) {
344 let dedup = 1.0;
345 if (gcstatus['disk-bytes'] > 0) {
346 dedup = (gcstatus['index-data-bytes'] || 0)/gcstatus['disk-bytes'];
347 }
348 return dedup;
349 },
350
351 parse_snapshot_id: function(snapshot) {
352 if (!snapshot) {
353 return [undefined, undefined, undefined];
354 }
355 let nsRegex = /(?:^|\/)(ns\/([^/]+))/g;
356 let namespaces = [];
357 let nsPaths = [];
358 snapshot = snapshot.replace(nsRegex, (_, nsPath, ns) => { nsPaths.push(nsPath); namespaces.push(ns); return ""; });
359 let [_match, type, group, id] = /^\/?([^/]+)\/([^/]+)\/(.+)$/.exec(snapshot);
360
361 return [type, group, id, namespaces.join('/'), nsPaths.join('/')];
362 },
363
364 get_type_icon_cls: function(btype) {
365 var cls = '';
366 if (btype.startsWith('vm')) {
367 cls = 'fa-desktop';
368 } else if (btype.startsWith('ct')) {
369 cls = 'fa-cube';
370 } else if (btype.startsWith('host')) {
371 cls = 'fa-building';
372 }
373 return cls;
374 },
375
376 constructor: function() {
377 var me = this;
378
379 let PROXMOX_SAFE_ID_REGEX = "([A-Za-z0-9_][A-Za-z0-9._-]*)";
380 me.SAFE_ID_RE = new RegExp(`^${PROXMOX_SAFE_ID_REGEX}$`);
381 // only anchored at beginning, only parses datastore for now
382 me.VERIFICATION_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':?');
383 me.SYNC_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':' +
384 PROXMOX_SAFE_ID_REGEX + ':' + PROXMOX_SAFE_ID_REGEX + ':');
385 me.BACKUP_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':');
386
387 // do whatever you want here
388 Proxmox.Utils.override_task_descriptions({
389 'acme-deactivate': (type, id) =>
390 Ext.String.format(gettext("Deactivate {0} Account"), 'ACME') + ` '${id || 'default'}'`,
391 'acme-register': (type, id) =>
392 Ext.String.format(gettext("Register {0} Account"), 'ACME') + ` '${id || 'default'}'`,
393 'acme-update': (type, id) =>
394 Ext.String.format(gettext("Update {0} Account"), 'ACME') + ` '${id || 'default'}'`,
395 'acme-new-cert': ['', gettext('Order Certificate')],
396 'acme-renew-cert': ['', gettext('Renew Certificate')],
397 'acme-revoke-cert': ['', gettext('Revoke Certificate')],
398 backup: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Backup')),
399 'barcode-label-media': [gettext('Drive'), gettext('Barcode-Label Media')],
400 'catalog-media': [gettext('Drive'), gettext('Catalog Media')],
401 'delete-datastore': [gettext('Datastore'), gettext('Remove Datastore')],
402 'delete-namespace': [gettext('Namespace'), gettext('Remove Namespace')],
403 dircreate: [gettext('Directory Storage'), gettext('Create')],
404 dirremove: [gettext('Directory'), gettext('Remove')],
405 'eject-media': [gettext('Drive'), gettext('Eject Media')],
406 "format-media": [gettext('Drive'), gettext('Format media')],
407 "forget-group": [gettext('Group'), gettext('Remove Group')],
408 garbage_collection: ['Datastore', gettext('Garbage Collect')],
409 'realm-sync': ['Realm', gettext('User Sync')],
410 'inventory-update': [gettext('Drive'), gettext('Inventory Update')],
411 'label-media': [gettext('Drive'), gettext('Label Media')],
412 'load-media': (type, id) => PBS.Utils.render_drive_load_media_id(id, gettext('Load Media')),
413 logrotate: [null, gettext('Log Rotation')],
414 prune: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Prune')),
415 prunejob: (type, id) => PBS.Utils.render_prune_job_worker_id(id, gettext('Prune Job')),
416 reader: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Read Objects')),
417 'rewind-media': [gettext('Drive'), gettext('Rewind Media')],
418 sync: ['Datastore', gettext('Remote Sync')],
419 syncjob: [gettext('Sync Job'), gettext('Remote Sync')],
420 'tape-backup': (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup')),
421 'tape-backup-job': (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup Job')),
422 'tape-restore': ['Datastore', gettext('Tape Restore')],
423 'unload-media': [gettext('Drive'), gettext('Unload Media')],
424 verificationjob: [gettext('Verify Job'), gettext('Scheduled Verification')],
425 verify: ['Datastore', gettext('Verification')],
426 verify_group: ['Group', gettext('Verification')],
427 verify_snapshot: ['Snapshot', gettext('Verification')],
428 wipedisk: ['Device', gettext('Wipe Disk')],
429 zfscreate: [gettext('ZFS Storage'), gettext('Create')],
430 });
431
432 Proxmox.Schema.overrideAuthDomains({
433 pbs: {
434 name: 'Proxmox Backup authentication server',
435 add: false,
436 edit: false,
437 pwchange: true,
438 sync: false,
439 },
440 });
441 },
442
443 // Convert an ArrayBuffer to a base64url encoded string.
444 // A `null` value will be preserved for convenience.
445 bytes_to_base64url: function(bytes) {
446 if (bytes === null) {
447 return null;
448 }
449
450 return btoa(Array
451 .from(new Uint8Array(bytes))
452 .map(val => String.fromCharCode(val))
453 .join(''),
454 )
455 .replace(/\+/g, '-')
456 .replace(/\//g, '_')
457 .replace(/[=]/g, '');
458 },
459
460 // Convert an a base64url string to an ArrayBuffer.
461 // A `null` value will be preserved for convenience.
462 base64url_to_bytes: function(b64u) {
463 if (b64u === null) {
464 return null;
465 }
466
467 return new Uint8Array(
468 atob(b64u
469 .replace(/-/g, '+')
470 .replace(/_/g, '/'),
471 )
472 .split('')
473 .map(val => val.charCodeAt(0)),
474 );
475 },
476
477 driveCommand: function(driveid, command, reqOpts) {
478 let params = Ext.apply(reqOpts, {
479 url: `/api2/extjs/tape/drive/${driveid}/${command}`,
480 timeout: 5*60*1000,
481 failure: function(response) {
482 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
483 },
484 });
485
486 Proxmox.Utils.API2Request(params);
487 },
488
489 showMediaLabelWindow: function(response) {
490 let list = [];
491 for (let [key, val] of Object.entries(response.result.data)) {
492 if (key === 'ctime' || key === 'media-set-ctime') {
493 val = Proxmox.Utils.render_timestamp(val);
494 }
495 list.push({ key: key, value: val });
496 }
497
498 Ext.create('Ext.window.Window', {
499 title: gettext('Label Information'),
500 modal: true,
501 width: 600,
502 height: 450,
503 layout: 'fit',
504 scrollable: true,
505 items: [
506 {
507 xtype: 'grid',
508 store: {
509 data: list,
510 },
511 columns: [
512 {
513 text: gettext('Property'),
514 dataIndex: 'key',
515 width: 120,
516 },
517 {
518 text: gettext('Value'),
519 dataIndex: 'value',
520 flex: 1,
521 },
522 ],
523 },
524 ],
525 }).show();
526 },
527
528 showCartridgeMemoryWindow: function(response) {
529 Ext.create('Ext.window.Window', {
530 title: gettext('Cartridge Memory'),
531 modal: true,
532 width: 600,
533 height: 450,
534 layout: 'fit',
535 scrollable: true,
536 items: [
537 {
538 xtype: 'grid',
539 store: {
540 data: response.result.data,
541 },
542 columns: [
543 {
544 text: gettext('ID'),
545 hidden: true,
546 dataIndex: 'id',
547 width: 60,
548 },
549 {
550 text: gettext('Name'),
551 dataIndex: 'name',
552 flex: 2,
553 },
554 {
555 text: gettext('Value'),
556 dataIndex: 'value',
557 flex: 1,
558 },
559 ],
560 },
561 ],
562 }).show();
563 },
564
565 showVolumeStatisticsWindow: function(response) {
566 let list = [];
567 for (let [key, val] of Object.entries(response.result.data)) {
568 if (key === 'total-native-capacity' ||
569 key === 'total-used-native-capacity' ||
570 key === 'lifetime-bytes-read' ||
571 key === 'lifetime-bytes-written' ||
572 key === 'last-mount-bytes-read' ||
573 key === 'last-mount-bytes-written') {
574 val = Proxmox.Utils.format_size(val);
575 }
576 list.push({ key: key, value: val });
577 }
578 Ext.create('Ext.window.Window', {
579 title: gettext('Volume Statistics'),
580 modal: true,
581 width: 600,
582 height: 450,
583 layout: 'fit',
584 scrollable: true,
585 items: [
586 {
587 xtype: 'grid',
588 store: {
589 data: list,
590 },
591 columns: [
592 {
593 text: gettext('Property'),
594 dataIndex: 'key',
595 flex: 1,
596 },
597 {
598 text: gettext('Value'),
599 dataIndex: 'value',
600 flex: 1,
601 },
602 ],
603 },
604 ],
605 }).show();
606 },
607
608 showDriveStatusWindow: function(response) {
609 let list = [];
610 for (let [key, val] of Object.entries(response.result.data)) {
611 if (key === 'manufactured') {
612 val = Proxmox.Utils.render_timestamp(val);
613 }
614 if (key === 'bytes-read' || key === 'bytes-written') {
615 val = Proxmox.Utils.format_size(val);
616 }
617 list.push({ key: key, value: val });
618 }
619
620 Ext.create('Ext.window.Window', {
621 title: gettext('Status'),
622 modal: true,
623 width: 600,
624 height: 450,
625 layout: 'fit',
626 scrollable: true,
627 items: [
628 {
629 xtype: 'grid',
630 store: {
631 data: list,
632 },
633 columns: [
634 {
635 text: gettext('Property'),
636 dataIndex: 'key',
637 width: 120,
638 },
639 {
640 text: gettext('Value'),
641 dataIndex: 'value',
642 flex: 1,
643 },
644 ],
645 },
646 ],
647 }).show();
648 },
649
650 renderDriveState: function(value, md) {
651 if (!value) {
652 return gettext('Idle');
653 }
654
655 let icon = '<i class="fa fa-spinner fa-pulse fa-fw"></i>';
656
657 if (value.startsWith("UPID")) {
658 let upid = Proxmox.Utils.parse_task_upid(value);
659 md.tdCls = "pointer";
660 return `${icon} ${upid.desc}`;
661 }
662
663 return `${icon} ${value}`;
664 },
665
666 // FIXME: this "parser" is brittle and relies on the order the arguments will appear in
667 parseMaintenanceMode: function(mode) {
668 let [type, message] = mode.split(/,(.+)/);
669 type = type.split("=").pop();
670 message = message ? message.split("=")[1]
671 .replace(/^"(.*)"$/, '$1')
672 .replaceAll('\\"', '"') : null;
673 return [type, message];
674 },
675
676 renderMaintenance: function(mode, activeTasks) {
677 if (!mode) {
678 return gettext('None');
679 }
680
681 let [type, message] = PBS.Utils.parseMaintenanceMode(mode);
682
683 let extra = '';
684
685 if (activeTasks !== undefined) {
686 const conflictingTasks = activeTasks.write + (type === 'offline' ? activeTasks.read : 0);
687
688 if (conflictingTasks > 0) {
689 extra += '| <i class="fa fa-spinner fa-pulse fa-fw"></i> ';
690 extra += Ext.String.format(gettext('{0} conflicting tasks still active.'), conflictingTasks);
691 } else {
692 extra += '<i class="fa fa-check"></i>';
693 }
694 }
695
696 if (message) {
697 extra += ` ("${message}")`;
698 }
699
700 let modeText = Proxmox.Utils.unknownText;
701 switch (type) {
702 case 'read-only': modeText = gettext("Read-only");
703 break;
704 case 'offline': modeText = gettext("Offline");
705 break;
706 }
707 return `${modeText} ${extra}`;
708 },
709
710 render_optional_namespace: function(value, metadata, record) {
711 if (!value) return `- (${gettext('Root')})`;
712 return Ext.String.htmlEncode(value);
713 },
714
715 render_optional_remote: function(value, metadata, record) {
716 if (!value) {
717 return `- (${gettext('Local')})`;
718 }
719 return Ext.String.htmlEncode(value);
720 },
721
722 tuningOptions: {
723 'chunk-order': {
724 '__default__': Proxmox.Utils.defaultText + ` (${gettext('Inode')})`,
725 none: gettext('None'),
726 inode: gettext('Inode'),
727 },
728 'sync-level': {
729 '__default__': Proxmox.Utils.defaultText + ` (${gettext('Filesystem')})`,
730 none: gettext('None'),
731 file: gettext('File'),
732 filesystem: gettext('Filesystem'),
733 },
734 },
735
736 render_tuning_options: function(tuning) {
737 let options = [];
738 let order = tuning['chunk-order'];
739 delete tuning['chunk-order'];
740 order = PBS.Utils.tuningOptions['chunk-order'][order ?? '__default__'];
741 options.push(`${gettext('Chunk Order')}: ${order}`);
742
743 let sync = tuning['sync-level'];
744 delete tuning['sync-level'];
745 sync = PBS.Utils.tuningOptions['sync-level'][sync ?? '__default__'];
746 options.push(`${gettext('Sync Level')}: ${sync}`);
747
748 for (const [k, v] of Object.entries(tuning)) {
749 options.push(`${k}: ${v}`);
750 }
751
752 return options.join(', ');
753 },
754 });