Ext.ns('PBS');
console.log("Starting Backup Server GUI");
Ext.define('PBS.Utils', {
singleton: true,
missingText: gettext('missing'),
updateLoginData: function(data) {
Proxmox.Utils.setAuthData(data);
},
dataStorePrefix: 'DataStore-',
cryptmap: [
'none',
'mixed',
'sign-only',
'encrypt',
],
cryptText: [
Proxmox.Utils.noText,
gettext('Mixed'),
gettext('Signed'),
gettext('Encrypted'),
],
cryptIconCls: [
'',
'',
'lock faded',
'lock good',
],
calculateCryptMode: function(data) {
let mixed = data.mixed;
let encrypted = data.encrypt;
let signed = data['sign-only'];
let files = data.count;
if (mixed > 0) {
return PBS.Utils.cryptmap.indexOf('mixed');
} else if (files === encrypted && encrypted > 0) {
return PBS.Utils.cryptmap.indexOf('encrypt');
} else if (files === signed && signed > 0) {
return PBS.Utils.cryptmap.indexOf('sign-only');
} else if ((signed+encrypted) === 0) {
return PBS.Utils.cryptmap.indexOf('none');
} else {
return PBS.Utils.cryptmap.indexOf('mixed');
}
},
noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.',
getDataStoreFromPath: function(path) {
return path.slice(PBS.Utils.dataStorePrefix.length);
},
isDataStorePath: function(path) {
return path.indexOf(PBS.Utils.dataStorePrefix) === 0;
},
parsePropertyString: function(value, defaultKey) {
var res = {},
error;
if (typeof value !== 'string' || value === '') {
return res;
}
Ext.Array.each(value.split(','), function(p) {
var kv = p.split('=', 2);
if (Ext.isDefined(kv[1])) {
res[kv[0]] = kv[1];
} else if (Ext.isDefined(defaultKey)) {
if (Ext.isDefined(res[defaultKey])) {
error = 'defaultKey may be only defined once in propertyString';
return false; // break
}
res[defaultKey] = kv[0];
} else {
error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
return false; // break
}
return true;
});
if (error !== undefined) {
console.error(error);
return null;
}
return res;
},
printPropertyString: function(data, defaultKey) {
var stringparts = [],
gotDefaultKeyVal = false,
defaultKeyVal;
Ext.Object.each(data, function(key, value) {
if (defaultKey !== undefined && key === defaultKey) {
gotDefaultKeyVal = true;
defaultKeyVal = value;
} else if (value !== '' && value !== undefined) {
stringparts.push(key + '=' + value);
}
});
stringparts = stringparts.sort();
if (gotDefaultKeyVal) {
stringparts.unshift(defaultKeyVal);
}
return stringparts.join(',');
},
// helper for deleting field which are set to there default values
delete_if_default: function(values, fieldname, default_val, create) {
if (values[fieldname] === '' || values[fieldname] === default_val) {
if (!create) {
if (values.delete) {
if (Ext.isArray(values.delete)) {
values.delete.push(fieldname);
} else {
values.delete += ',' + fieldname;
}
} else {
values.delete = [fieldname];
}
}
delete values[fieldname];
}
},
render_datetime_utc: function(datetime) {
let pad = (number) => number < 10 ? '0' + number : number;
return datetime.getUTCFullYear() +
'-' + pad(datetime.getUTCMonth() + 1) +
'-' + pad(datetime.getUTCDate()) +
'T' + pad(datetime.getUTCHours()) +
':' + pad(datetime.getUTCMinutes()) +
':' + pad(datetime.getUTCSeconds()) +
'Z';
},
render_datastore_worker_id: function(id, what) {
const res = id.match(/^(\S+?):(\S+?)\/(\S+?)(\/(.+))?$/);
if (res) {
let datastore = res[1], backupGroup = `${res[2]}/${res[3]}`;
if (res[4] !== undefined) {
let datetime = Ext.Date.parse(parseInt(res[5], 16), 'U');
let utctime = PBS.Utils.render_datetime_utc(datetime);
return `Datastore ${datastore} ${what} ${backupGroup}/${utctime}`;
} else {
return `Datastore ${datastore} ${what} ${backupGroup}`;
}
}
return `Datastore ${what} ${id}`;
},
render_tape_backup_id: function(id, what) {
const res = id.match(/^(\S+?):(\S+?):(\S+?)(:(.+))?$/);
if (res) {
let datastore = res[1];
let pool = res[2];
let drive = res[3];
return `${what} ${datastore} (pool ${pool}, drive ${drive})`;
}
return `${what} ${id}`;
},
render_drive_load_media_id: function(id, what) {
const res = id.match(/^(\S+?):(\S+?)$/);
if (res) {
let drive = res[1];
let label = res[2];
return gettext('Drive') + ` ${drive} - ${what} '${label}'`;
}
return `${what} ${id}`;
},
// mimics Display trait in backend
renderKeyID: function(fingerprint) {
return fingerprint.substring(0, 23);
},
render_task_status: function(value, metadata, record) {
if (!record.data['last-run-upid']) {
return '-';
}
if (!record.data['last-run-endtime']) {
metadata.tdCls = 'x-grid-row-loading';
return '';
}
let parsed = Proxmox.Utils.parse_task_status(value);
let text = value;
let icon = '';
switch (parsed) {
case 'unknown':
icon = 'question faded';
text = Proxmox.Utils.unknownText;
break;
case 'error':
icon = 'times critical';
text = Proxmox.Utils.errorText + ': ' + value;
break;
case 'warning':
icon = 'exclamation warning';
break;
case 'ok':
icon = 'check good';
text = gettext("OK");
}
return ` ${text}`;
},
render_next_task_run: function(value, metadat, record) {
if (!value) return '-';
let now = new Date();
let next = new Date(value*1000);
if (next < now) {
return gettext('pending');
}
return Proxmox.Utils.render_timestamp(value);
},
render_optional_timestamp: function(value, metadata, record) {
if (!value) return '-';
return Proxmox.Utils.render_timestamp(value);
},
parse_datastore_worker_id: function(type, id) {
let result;
let res;
if (type.startsWith('verif')) {
res = PBS.Utils.VERIFICATION_JOB_ID_RE.exec(id);
if (res) {
result = res[1];
}
} else if (type.startsWith('sync')) {
res = PBS.Utils.SYNC_JOB_ID_RE.exec(id);
if (res) {
result = res[3];
}
} else if (type === 'backup') {
res = PBS.Utils.BACKUP_JOB_ID_RE.exec(id);
if (res) {
result = res[1];
}
} else if (type === 'garbage_collection') {
return id;
} else if (type === 'prune') {
return id;
}
return result;
},
extractTokenUser: function(tokenid) {
return tokenid.match(/^(.+)!([^!]+)$/)[1];
},
extractTokenName: function(tokenid) {
return tokenid.match(/^(.+)!([^!]+)$/)[2];
},
render_estimate: function(value) {
if (value === undefined) {
return gettext('Not enough data');
}
let now = new Date();
let estimate = new Date(value*1000);
let timespan = (estimate - now)/1000;
if (Number(estimate) <= Number(now) || isNaN(timespan)) {
return gettext('Never');
}
let duration = Proxmox.Utils.format_duration_human(timespan);
return Ext.String.format(gettext("in {0}"), duration);
},
render_size_usage: function(val, max) {
if (max === 0) {
return gettext('N/A');
}
return (val*100/max).toFixed(2) + '% (' +
Ext.String.format(gettext('{0} of {1}'),
Proxmox.Utils.format_size(val), Proxmox.Utils.format_size(max)) + ')';
},
get_help_tool: function(blockid) {
let info = Proxmox.Utils.get_help_info(blockid);
if (info === undefined) {
info = Proxmox.Utils.get_help_info('pbs_documentation_index');
}
if (info === undefined) {
throw "get_help_info failed"; // should not happen
}
let docsURI = window.location.origin + info.link;
let title = info.title;
if (info.subtitle) {
title += ' - ' + info.subtitle;
}
return {
type: 'help',
tooltip: title,
handler: function() {
window.open(docsURI);
},
};
},
calculate_dedup_factor: function(gcstatus) {
let dedup = 1.0;
if (gcstatus['disk-bytes'] > 0) {
dedup = (gcstatus['index-data-bytes'] || 0)/gcstatus['disk-bytes'];
}
return dedup;
},
parse_snapshot_id: function(snapshot) {
if (!snapshot) {
return [undefined, undefined, undefined];
}
let nsRegex = /(?:^|\/)(ns\/([^/]+))/g;
let namespaces = [];
let nsPaths = [];
snapshot = snapshot.replace(nsRegex, (_, nsPath, ns) => { nsPaths.push(nsPath); namespaces.push(ns); return ""; });
let [_match, type, group, id] = /^\/?([^/]+)\/([^/]+)\/(.+)$/.exec(snapshot);
return [type, group, id, namespaces.join('/'), nsPaths.join('/')];
},
get_type_icon_cls: function(btype) {
var cls = '';
if (btype.startsWith('vm')) {
cls = 'fa-desktop';
} else if (btype.startsWith('ct')) {
cls = 'fa-cube';
} else if (btype.startsWith('host')) {
cls = 'fa-building';
}
return cls;
},
constructor: function() {
var me = this;
let PROXMOX_SAFE_ID_REGEX = "([A-Za-z0-9_][A-Za-z0-9._-]*)";
me.SAFE_ID_RE = new RegExp(`^${PROXMOX_SAFE_ID_REGEX}$`);
// only anchored at beginning, only parses datastore for now
me.VERIFICATION_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':?');
me.SYNC_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':' +
PROXMOX_SAFE_ID_REGEX + ':' + PROXMOX_SAFE_ID_REGEX + ':');
me.BACKUP_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':');
// do whatever you want here
Proxmox.Utils.override_task_descriptions({
'acme-deactivate': (type, id) =>
Ext.String.format(gettext("Deactivate {0} Account"), 'ACME') + ` '${id || 'default'}'`,
'acme-register': (type, id) =>
Ext.String.format(gettext("Register {0} Account"), 'ACME') + ` '${id || 'default'}'`,
'acme-update': (type, id) =>
Ext.String.format(gettext("Update {0} Account"), 'ACME') + ` '${id || 'default'}'`,
'acme-new-cert': ['', gettext('Order Certificate')],
'acme-renew-cert': ['', gettext('Renew Certificate')],
'acme-revoke-cert': ['', gettext('Revoke Certificate')],
backup: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Backup')),
'barcode-label-media': [gettext('Drive'), gettext('Barcode-Label Media')],
'catalog-media': [gettext('Drive'), gettext('Catalog Media')],
'delete-datastore': [gettext('Datastore'), gettext('Remove Datastore')],
dircreate: [gettext('Directory Storage'), gettext('Create')],
dirremove: [gettext('Directory'), gettext('Remove')],
'eject-media': [gettext('Drive'), gettext('Eject Media')],
"format-media": [gettext('Drive'), gettext('Format media')],
"forget-group": [gettext('Group'), gettext('Remove Group')],
garbage_collection: ['Datastore', gettext('Garbage Collect')],
'inventory-update': [gettext('Drive'), gettext('Inventory Update')],
'label-media': [gettext('Drive'), gettext('Label Media')],
'load-media': (type, id) => PBS.Utils.render_drive_load_media_id(id, gettext('Load Media')),
logrotate: [null, gettext('Log Rotation')],
prune: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Prune')),
reader: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Read Objects')),
'rewind-media': [gettext('Drive'), gettext('Rewind Media')],
sync: ['Datastore', gettext('Remote Sync')],
syncjob: [gettext('Sync Job'), gettext('Remote Sync')],
'tape-backup': (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup')),
'tape-backup-job': (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup Job')),
'tape-restore': ['Datastore', gettext('Tape Restore')],
'unload-media': [gettext('Drive'), gettext('Unload Media')],
verificationjob: [gettext('Verify Job'), gettext('Scheduled Verification')],
verify: ['Datastore', gettext('Verification')],
verify_group: ['Group', gettext('Verification')],
verify_snapshot: ['Snapshot', gettext('Verification')],
zfscreate: [gettext('ZFS Storage'), gettext('Create')],
});
Proxmox.Schema.overrideAuthDomains({
pbs: {
name: 'Proxmox Backup authentication server',
add: false,
edit: false,
pwchange: true,
},
});
},
// Convert an ArrayBuffer to a base64url encoded string.
// A `null` value will be preserved for convenience.
bytes_to_base64url: function(bytes) {
if (bytes === null) {
return null;
}
return btoa(Array
.from(new Uint8Array(bytes))
.map(val => String.fromCharCode(val))
.join(''),
)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/[=]/g, '');
},
// Convert an a base64url string to an ArrayBuffer.
// A `null` value will be preserved for convenience.
base64url_to_bytes: function(b64u) {
if (b64u === null) {
return null;
}
return new Uint8Array(
atob(b64u
.replace(/-/g, '+')
.replace(/_/g, '/'),
)
.split('')
.map(val => val.charCodeAt(0)),
);
},
driveCommand: function(driveid, command, reqOpts) {
let params = Ext.apply(reqOpts, {
url: `/api2/extjs/tape/drive/${driveid}/${command}`,
timeout: 5*60*1000,
failure: function(response) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
Proxmox.Utils.API2Request(params);
},
showMediaLabelWindow: function(response) {
let list = [];
for (let [key, val] of Object.entries(response.result.data)) {
if (key === 'ctime' || key === 'media-set-ctime') {
val = Proxmox.Utils.render_timestamp(val);
}
list.push({ key: key, value: val });
}
Ext.create('Ext.window.Window', {
title: gettext('Label Information'),
modal: true,
width: 600,
height: 450,
layout: 'fit',
scrollable: true,
items: [
{
xtype: 'grid',
store: {
data: list,
},
columns: [
{
text: gettext('Property'),
dataIndex: 'key',
width: 120,
},
{
text: gettext('Value'),
dataIndex: 'value',
flex: 1,
},
],
},
],
}).show();
},
showCartridgeMemoryWindow: function(response) {
Ext.create('Ext.window.Window', {
title: gettext('Cartridge Memory'),
modal: true,
width: 600,
height: 450,
layout: 'fit',
scrollable: true,
items: [
{
xtype: 'grid',
store: {
data: response.result.data,
},
columns: [
{
text: gettext('ID'),
hidden: true,
dataIndex: 'id',
width: 60,
},
{
text: gettext('Name'),
dataIndex: 'name',
flex: 2,
},
{
text: gettext('Value'),
dataIndex: 'value',
flex: 1,
},
],
},
],
}).show();
},
showVolumeStatisticsWindow: function(response) {
let list = [];
for (let [key, val] of Object.entries(response.result.data)) {
if (key === 'total-native-capacity' ||
key === 'total-used-native-capacity' ||
key === 'lifetime-bytes-read' ||
key === 'lifetime-bytes-written' ||
key === 'last-mount-bytes-read' ||
key === 'last-mount-bytes-written') {
val = Proxmox.Utils.format_size(val);
}
list.push({ key: key, value: val });
}
Ext.create('Ext.window.Window', {
title: gettext('Volume Statistics'),
modal: true,
width: 600,
height: 450,
layout: 'fit',
scrollable: true,
items: [
{
xtype: 'grid',
store: {
data: list,
},
columns: [
{
text: gettext('Property'),
dataIndex: 'key',
flex: 1,
},
{
text: gettext('Value'),
dataIndex: 'value',
flex: 1,
},
],
},
],
}).show();
},
showDriveStatusWindow: function(response) {
let list = [];
for (let [key, val] of Object.entries(response.result.data)) {
if (key === 'manufactured') {
val = Proxmox.Utils.render_timestamp(val);
}
if (key === 'bytes-read' || key === 'bytes-written') {
val = Proxmox.Utils.format_size(val);
}
list.push({ key: key, value: val });
}
Ext.create('Ext.window.Window', {
title: gettext('Status'),
modal: true,
width: 600,
height: 450,
layout: 'fit',
scrollable: true,
items: [
{
xtype: 'grid',
store: {
data: list,
},
columns: [
{
text: gettext('Property'),
dataIndex: 'key',
width: 120,
},
{
text: gettext('Value'),
dataIndex: 'value',
flex: 1,
},
],
},
],
}).show();
},
renderDriveState: function(value, md) {
if (!value) {
return gettext('Idle');
}
let icon = '';
if (value.startsWith("UPID")) {
let upid = Proxmox.Utils.parse_task_upid(value);
md.tdCls = "pointer";
return `${icon} ${upid.desc}`;
}
return `${icon} ${value}`;
},
// FIXME: this "parser" is brittle and relies on the order the arguments will appear in
parseMaintenanceMode: function(mode) {
let [type, message] = mode.split(/,(.+)/);
type = type.split("=").pop();
message = message ? message.split("=")[1]
.replace(/^"(.*)"$/, '$1')
.replaceAll('\\"', '"') : null;
return [type, message];
},
renderMaintenance: function(mode, activeTasks) {
if (!mode) {
return gettext('None');
}
let [type, message] = PBS.Utils.parseMaintenanceMode(mode);
const conflictingTasks = activeTasks.write + (type === 'offline' ? activeTasks.read : 0);
let extra = '';
if (conflictingTasks > 0) {
extra += '| ';
extra += Ext.String.format(gettext('{0} conflicting tasks still active.'), conflictingTasks);
} else {
extra += '';
}
if (message) {
extra += ` ("${message}")`;
}
let modeText = Proxmox.Utils.unknownText;
switch (type) {
case 'read-only': modeText = gettext("Read-only");
break;
case 'offline': modeText = gettext("Offline");
break;
}
return `${modeText} ${extra}`;
},
render_optional_namespace: function(value, metadata, record) {
if (!value) return `- (${gettext('Root')})`;
return Ext.String.htmlEncode(value);
},
});