788588cb86
We had done so already in PBS and PMG since a bit without seeing anything broken due to it, which makes sense as this was enabled for *during* the step-by-step upgrade from ExtJS 4 to 5 (and then directly 6). Re-enabling could help with some accessibility issues we have some tangential reports[0]. [0]: https://github.com/nvaccess/nvda/issues/11530 Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
1857 lines
46 KiB
JavaScript
1857 lines
46 KiB
JavaScript
Ext.ns('PVE');
|
|
|
|
console.log("Starting Proxmox VE Manager");
|
|
|
|
Ext.Ajax.defaultHeaders = {
|
|
'Accept': 'application/json',
|
|
};
|
|
|
|
Ext.define('PVE.Utils', {
|
|
utilities: {
|
|
|
|
// this singleton contains miscellaneous utilities
|
|
|
|
toolkit: undefined, // (extjs|touch), set inside Toolkit.js
|
|
|
|
bus_match: /^(ide|sata|virtio|scsi)\d+$/,
|
|
|
|
log_severity_hash: {
|
|
0: "panic",
|
|
1: "alert",
|
|
2: "critical",
|
|
3: "error",
|
|
4: "warning",
|
|
5: "notice",
|
|
6: "info",
|
|
7: "debug",
|
|
},
|
|
|
|
support_level_hash: {
|
|
'c': gettext('Community'),
|
|
'b': gettext('Basic'),
|
|
's': gettext('Standard'),
|
|
'p': gettext('Premium'),
|
|
},
|
|
|
|
noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit '
|
|
+'<a target="_blank" href="https://www.proxmox.com/products/proxmox-ve/subscription-service-plans">'
|
|
+'www.proxmox.com</a> to get a list of available options.',
|
|
|
|
kvm_ostypes: {
|
|
'Linux': [
|
|
{ desc: '5.x - 2.6 Kernel', val: 'l26' },
|
|
{ desc: '2.4 Kernel', val: 'l24' },
|
|
],
|
|
'Microsoft Windows': [
|
|
{ desc: '10/2016/2019', val: 'win10' },
|
|
{ desc: '8.x/2012/2012r2', val: 'win8' },
|
|
{ desc: '7/2008r2', val: 'win7' },
|
|
{ desc: 'Vista/2008', val: 'w2k8' },
|
|
{ desc: 'XP/2003', val: 'wxp' },
|
|
{ desc: '2000', val: 'w2k' },
|
|
],
|
|
'Solaris Kernel': [
|
|
{ desc: '-', val: 'solaris' },
|
|
],
|
|
'Other': [
|
|
{ desc: '-', val: 'other' },
|
|
],
|
|
},
|
|
|
|
is_windows: function(ostype) {
|
|
for (let entry of PVE.Utils.kvm_ostypes['Microsoft Windows']) {
|
|
if (entry.val === ostype) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
get_health_icon: function(state, circle) {
|
|
if (circle === undefined) {
|
|
circle = false;
|
|
}
|
|
|
|
if (state === undefined) {
|
|
state = 'uknown';
|
|
}
|
|
|
|
var icon = 'faded fa-question';
|
|
switch (state) {
|
|
case 'good':
|
|
icon = 'good fa-check';
|
|
break;
|
|
case 'upgrade':
|
|
icon = 'warning fa-upload';
|
|
break;
|
|
case 'old':
|
|
icon = 'warning fa-refresh';
|
|
break;
|
|
case 'warning':
|
|
icon = 'warning fa-exclamation';
|
|
break;
|
|
case 'critical':
|
|
icon = 'critical fa-times';
|
|
break;
|
|
default: break;
|
|
}
|
|
|
|
if (circle) {
|
|
icon += '-circle';
|
|
}
|
|
|
|
return icon;
|
|
},
|
|
|
|
parse_ceph_version: function(service) {
|
|
if (service.ceph_version_short) {
|
|
return service.ceph_version_short;
|
|
}
|
|
|
|
if (service.ceph_version) {
|
|
var match = service.ceph_version.match(/version (\d+(\.\d+)*)/);
|
|
if (match) {
|
|
return match[1];
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
|
|
compare_ceph_versions: function(a, b) {
|
|
let avers = [];
|
|
let bvers = [];
|
|
|
|
if (a === b) {
|
|
return 0;
|
|
}
|
|
|
|
if (Ext.isArray(a)) {
|
|
avers = a.slice(); // copy array
|
|
} else {
|
|
avers = a.toString().split('.');
|
|
}
|
|
|
|
if (Ext.isArray(b)) {
|
|
bvers = b.slice(); // copy array
|
|
} else {
|
|
bvers = b.toString().split('.');
|
|
}
|
|
|
|
for (;;) {
|
|
let av = avers.shift();
|
|
let bv = bvers.shift();
|
|
|
|
if (av === undefined && bv === undefined) {
|
|
return 0;
|
|
} else if (av === undefined) {
|
|
return -1;
|
|
} else if (bv === undefined) {
|
|
return 1;
|
|
} else {
|
|
let diff = parseInt(av, 10) - parseInt(bv, 10);
|
|
if (diff !== 0) return diff;
|
|
// else we need to look at the next parts
|
|
}
|
|
}
|
|
},
|
|
|
|
get_ceph_icon_html: function(health, fw) {
|
|
var state = PVE.Utils.map_ceph_health[health];
|
|
var cls = PVE.Utils.get_health_icon(state);
|
|
if (fw) {
|
|
cls += ' fa-fw';
|
|
}
|
|
return "<i class='fa " + cls + "'></i> ";
|
|
},
|
|
|
|
map_ceph_health: {
|
|
'HEALTH_OK': 'good',
|
|
'HEALTH_UPGRADE': 'upgrade',
|
|
'HEALTH_OLD': 'old',
|
|
'HEALTH_WARN': 'warning',
|
|
'HEALTH_ERR': 'critical',
|
|
},
|
|
|
|
render_sdn_pending: function(rec, value, key, index) {
|
|
if (rec.data.state === undefined || rec.data.state === null) {
|
|
return value;
|
|
}
|
|
|
|
if (rec.data.state === 'deleted') {
|
|
if (value === undefined) {
|
|
return ' ';
|
|
} else {
|
|
return '<div style="text-decoration: line-through;">'+ value +'</div>';
|
|
}
|
|
} else if (rec.data.pending[key] !== undefined && rec.data.pending[key] !== null) {
|
|
if (rec.data.pending[key] === 'deleted') {
|
|
return ' ';
|
|
} else {
|
|
return rec.data.pending[key];
|
|
}
|
|
}
|
|
return value;
|
|
},
|
|
|
|
render_sdn_pending_state: function(rec, value) {
|
|
if (value === undefined || value === null) {
|
|
return ' ';
|
|
}
|
|
|
|
let icon = `<i class="fa fa-fw fa-refresh warning"></i>`;
|
|
|
|
if (value === 'deleted') {
|
|
return '<span>' + icon + value + '</span>';
|
|
}
|
|
|
|
let tip = gettext('Pending Changes') + ': <br>';
|
|
|
|
for (const [key, keyvalue] of Object.entries(rec.data.pending)) {
|
|
if ((rec.data[key] !== undefined && rec.data.pending[key] !== rec.data[key]) ||
|
|
rec.data[key] === undefined
|
|
) {
|
|
tip += `${key}: ${keyvalue} <br>`;
|
|
}
|
|
}
|
|
return '<span data-qtip="' + tip + '">'+ icon + value + '</span>';
|
|
},
|
|
|
|
render_ceph_health: function(healthObj) {
|
|
var state = {
|
|
iconCls: PVE.Utils.get_health_icon(),
|
|
text: '',
|
|
};
|
|
|
|
if (!healthObj || !healthObj.status) {
|
|
return state;
|
|
}
|
|
|
|
var health = PVE.Utils.map_ceph_health[healthObj.status];
|
|
|
|
state.iconCls = PVE.Utils.get_health_icon(health, true);
|
|
state.text = healthObj.status;
|
|
|
|
return state;
|
|
},
|
|
|
|
render_zfs_health: function(value) {
|
|
if (typeof value === 'undefined') {
|
|
return "";
|
|
}
|
|
var iconCls = 'question-circle';
|
|
switch (value) {
|
|
case 'AVAIL':
|
|
case 'ONLINE':
|
|
iconCls = 'check-circle good';
|
|
break;
|
|
case 'REMOVED':
|
|
case 'DEGRADED':
|
|
iconCls = 'exclamation-circle warning';
|
|
break;
|
|
case 'UNAVAIL':
|
|
case 'FAULTED':
|
|
case 'OFFLINE':
|
|
iconCls = 'times-circle critical';
|
|
break;
|
|
default: //unknown
|
|
}
|
|
|
|
return '<i class="fa fa-' + iconCls + '"></i> ' + value;
|
|
},
|
|
|
|
render_pbs_fingerprint: fp => fp.substring(0, 23),
|
|
|
|
render_backup_encryption: function(v, meta, record) {
|
|
if (!v) {
|
|
return gettext('No');
|
|
}
|
|
|
|
let tip = '';
|
|
if (v.match(/^[a-fA-F0-9]{2}:/)) { // fingerprint
|
|
tip = `Key fingerprint ${PVE.Utils.render_pbs_fingerprint(v)}`;
|
|
}
|
|
let icon = `<i class="fa fa-fw fa-lock good"></i>`;
|
|
return `<span data-qtip="${tip}">${icon} ${gettext('Encrypted')}</span>`;
|
|
},
|
|
|
|
render_backup_verification: function(v, meta, record) {
|
|
let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;
|
|
if (v === undefined || v === null) {
|
|
return i('question-circle-o warning', gettext('None'));
|
|
}
|
|
let tip = "";
|
|
let txt = gettext('Failed');
|
|
let iconCls = 'times critical';
|
|
if (v.state === 'ok') {
|
|
txt = gettext('OK');
|
|
iconCls = 'check good';
|
|
let now = Date.now() / 1000;
|
|
let task = Proxmox.Utils.parse_task_upid(v.upid);
|
|
let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
|
|
tip = `Last verify task started on ${verify_time}`;
|
|
if (now - v.starttime > 30 * 24 * 60 * 60) {
|
|
tip = `Last verify task over 30 days ago: ${verify_time}`;
|
|
iconCls = 'check warning';
|
|
}
|
|
}
|
|
return `<span data-qtip="${tip}"> ${i(iconCls, txt)} </span>`;
|
|
},
|
|
|
|
render_backup_status: function(value, meta, record) {
|
|
if (typeof value === 'undefined') {
|
|
return "";
|
|
}
|
|
|
|
let iconCls = 'check-circle good';
|
|
let text = gettext('Yes');
|
|
|
|
if (!PVE.Parser.parseBoolean(value.toString())) {
|
|
iconCls = 'times-circle critical';
|
|
|
|
text = gettext('No');
|
|
|
|
let reason = record.get('reason');
|
|
if (typeof reason !== 'undefined') {
|
|
if (reason in PVE.Utils.backup_reasons_table) {
|
|
reason = PVE.Utils.backup_reasons_table[record.get('reason')];
|
|
}
|
|
text = `${text} - ${reason}`;
|
|
}
|
|
}
|
|
|
|
return `<i class="fa fa-${iconCls}"></i> ${text}`;
|
|
},
|
|
|
|
render_backup_days_of_week: function(val) {
|
|
var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
var selected = [];
|
|
var cur = -1;
|
|
val.split(',').forEach(function(day) {
|
|
cur++;
|
|
var dow = (dows.indexOf(day)+6)%7;
|
|
if (cur === dow) {
|
|
if (selected.length === 0 || selected[selected.length-1] === 0) {
|
|
selected.push(1);
|
|
} else {
|
|
selected[selected.length-1]++;
|
|
}
|
|
} else {
|
|
while (cur < dow) {
|
|
cur++;
|
|
selected.push(0);
|
|
}
|
|
selected.push(1);
|
|
}
|
|
});
|
|
|
|
cur = -1;
|
|
var days = [];
|
|
selected.forEach(function(item) {
|
|
cur++;
|
|
if (item > 2) {
|
|
days.push(Ext.Date.dayNames[cur+1] + '-' + Ext.Date.dayNames[(cur+item)%7]);
|
|
cur += item-1;
|
|
} else if (item === 2) {
|
|
days.push(Ext.Date.dayNames[cur+1]);
|
|
days.push(Ext.Date.dayNames[(cur+2)%7]);
|
|
cur++;
|
|
} else if (item === 1) {
|
|
days.push(Ext.Date.dayNames[(cur+1)%7]);
|
|
}
|
|
});
|
|
return days.join(', ');
|
|
},
|
|
|
|
render_backup_selection: function(value, metaData, record) {
|
|
let allExceptText = gettext('All except {0}');
|
|
let allText = '-- ' + gettext('All') + ' --';
|
|
if (record.data.all) {
|
|
if (record.data.exclude) {
|
|
return Ext.String.format(allExceptText, record.data.exclude);
|
|
}
|
|
return allText;
|
|
}
|
|
if (record.data.vmid) {
|
|
return record.data.vmid;
|
|
}
|
|
|
|
if (record.data.pool) {
|
|
return "Pool '"+ record.data.pool + "'";
|
|
}
|
|
|
|
return "-";
|
|
},
|
|
|
|
backup_reasons_table: {
|
|
'backup=yes': gettext('Enabled'),
|
|
'backup=no': gettext('Disabled'),
|
|
'enabled': gettext('Enabled'),
|
|
'disabled': gettext('Disabled'),
|
|
'not a volume': gettext('Not a volume'),
|
|
'efidisk but no OMVF BIOS': gettext('EFI Disk without OMVF BIOS'),
|
|
},
|
|
|
|
get_kvm_osinfo: function(value) {
|
|
var info = { base: 'Other' }; // default
|
|
if (value) {
|
|
Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) {
|
|
Ext.each(PVE.Utils.kvm_ostypes[k], function(e) {
|
|
if (e.val === value) {
|
|
info = { desc: e.desc, base: k };
|
|
}
|
|
});
|
|
});
|
|
}
|
|
return info;
|
|
},
|
|
|
|
render_kvm_ostype: function(value) {
|
|
var osinfo = PVE.Utils.get_kvm_osinfo(value);
|
|
if (osinfo.desc && osinfo.desc !== '-') {
|
|
return osinfo.base + ' ' + osinfo.desc;
|
|
} else {
|
|
return osinfo.base;
|
|
}
|
|
},
|
|
|
|
render_hotplug_features: function(value) {
|
|
var fa = [];
|
|
|
|
if (!value || value === '0') {
|
|
return gettext('Disabled');
|
|
}
|
|
|
|
if (value === '1') {
|
|
value = 'disk,network,usb';
|
|
}
|
|
|
|
Ext.each(value.split(','), function(el) {
|
|
if (el === 'disk') {
|
|
fa.push(gettext('Disk'));
|
|
} else if (el === 'network') {
|
|
fa.push(gettext('Network'));
|
|
} else if (el === 'usb') {
|
|
fa.push('USB');
|
|
} else if (el === 'memory') {
|
|
fa.push(gettext('Memory'));
|
|
} else if (el === 'cpu') {
|
|
fa.push(gettext('CPU'));
|
|
} else {
|
|
fa.push(el);
|
|
}
|
|
});
|
|
|
|
return fa.join(', ');
|
|
},
|
|
|
|
render_localtime: function(value) {
|
|
if (value === '__default__') {
|
|
return Proxmox.Utils.defaultText + ' (' + gettext('Enabled for Windows') + ')';
|
|
}
|
|
return Proxmox.Utils.format_boolean(value);
|
|
},
|
|
|
|
render_qga_features: function(config) {
|
|
if (!config) {
|
|
return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')';
|
|
}
|
|
let qga = PVE.Parser.parsePropertyString(config, 'enabled');
|
|
if (!PVE.Parser.parseBoolean(qga.enabled)) {
|
|
return Proxmox.Utils.disabledText;
|
|
}
|
|
delete qga.enabled;
|
|
|
|
let agentstring = Proxmox.Utils.enabledText;
|
|
|
|
for (const [key, value] of Object.entries(qga)) {
|
|
let displayText = Proxmox.Utils.disabledText;
|
|
if (key === 'type') {
|
|
let map = {
|
|
isa: "ISA",
|
|
virtio: "VirtIO",
|
|
};
|
|
displayText = map[value] || Proxmox.Utils.unknownText;
|
|
} else if (PVE.Parser.parseBoolean(value)) {
|
|
displayText = Proxmox.Utils.enabledText;
|
|
}
|
|
agentstring += `, ${key}: ${displayText}`;
|
|
}
|
|
|
|
return agentstring;
|
|
},
|
|
|
|
render_qemu_machine: function(value) {
|
|
return value || Proxmox.Utils.defaultText + ' (i440fx)';
|
|
},
|
|
|
|
render_qemu_bios: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText + ' (SeaBIOS)';
|
|
} else if (value === 'seabios') {
|
|
return "SeaBIOS";
|
|
} else if (value === 'ovmf') {
|
|
return "OVMF (UEFI)";
|
|
} else {
|
|
return value;
|
|
}
|
|
},
|
|
|
|
render_dc_ha_opts: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText;
|
|
} else {
|
|
return PVE.Parser.printPropertyString(value);
|
|
}
|
|
},
|
|
render_as_property_string: function(value) {
|
|
return !value ? Proxmox.Utils.defaultText
|
|
: PVE.Parser.printPropertyString(value);
|
|
},
|
|
|
|
render_scsihw: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText + ' (LSI 53C895A)';
|
|
} else if (value === 'lsi') {
|
|
return 'LSI 53C895A';
|
|
} else if (value === 'lsi53c810') {
|
|
return 'LSI 53C810';
|
|
} else if (value === 'megasas') {
|
|
return 'MegaRAID SAS 8708EM2';
|
|
} else if (value === 'virtio-scsi-pci') {
|
|
return 'VirtIO SCSI';
|
|
} else if (value === 'virtio-scsi-single') {
|
|
return 'VirtIO SCSI single';
|
|
} else if (value === 'pvscsi') {
|
|
return 'VMware PVSCSI';
|
|
} else {
|
|
return value;
|
|
}
|
|
},
|
|
|
|
render_spice_enhancements: function(values) {
|
|
let props = PVE.Parser.parsePropertyString(values);
|
|
if (Ext.Object.isEmpty(props)) {
|
|
return Proxmox.Utils.noneText;
|
|
}
|
|
|
|
let output = [];
|
|
if (PVE.Parser.parseBoolean(props.foldersharing)) {
|
|
output.push('Folder Sharing: ' + gettext('Enabled'));
|
|
}
|
|
if (props.videostreaming === 'all' || props.videostreaming === 'filter') {
|
|
output.push('Video Streaming: ' + props.videostreaming);
|
|
}
|
|
return output.join(', ');
|
|
},
|
|
|
|
// fixme: auto-generate this
|
|
// for now, please keep in sync with PVE::Tools::kvmkeymaps
|
|
kvm_keymaps: {
|
|
//ar: 'Arabic',
|
|
da: 'Danish',
|
|
de: 'German',
|
|
'de-ch': 'German (Swiss)',
|
|
'en-gb': 'English (UK)',
|
|
'en-us': 'English (USA)',
|
|
es: 'Spanish',
|
|
//et: 'Estonia',
|
|
fi: 'Finnish',
|
|
//fo: 'Faroe Islands',
|
|
fr: 'French',
|
|
'fr-be': 'French (Belgium)',
|
|
'fr-ca': 'French (Canada)',
|
|
'fr-ch': 'French (Swiss)',
|
|
//hr: 'Croatia',
|
|
hu: 'Hungarian',
|
|
is: 'Icelandic',
|
|
it: 'Italian',
|
|
ja: 'Japanese',
|
|
lt: 'Lithuanian',
|
|
//lv: 'Latvian',
|
|
mk: 'Macedonian',
|
|
nl: 'Dutch',
|
|
//'nl-be': 'Dutch (Belgium)',
|
|
no: 'Norwegian',
|
|
pl: 'Polish',
|
|
pt: 'Portuguese',
|
|
'pt-br': 'Portuguese (Brazil)',
|
|
//ru: 'Russian',
|
|
sl: 'Slovenian',
|
|
sv: 'Swedish',
|
|
//th: 'Thai',
|
|
tr: 'Turkish',
|
|
},
|
|
|
|
kvm_vga_drivers: {
|
|
std: gettext('Standard VGA'),
|
|
vmware: gettext('VMware compatible'),
|
|
qxl: 'SPICE',
|
|
qxl2: 'SPICE dual monitor',
|
|
qxl3: 'SPICE three monitors',
|
|
qxl4: 'SPICE four monitors',
|
|
serial0: gettext('Serial terminal') + ' 0',
|
|
serial1: gettext('Serial terminal') + ' 1',
|
|
serial2: gettext('Serial terminal') + ' 2',
|
|
serial3: gettext('Serial terminal') + ' 3',
|
|
virtio: 'VirtIO-GPU',
|
|
none: Proxmox.Utils.noneText,
|
|
},
|
|
|
|
render_kvm_language: function(value) {
|
|
if (!value || value === '__default__') {
|
|
return Proxmox.Utils.defaultText;
|
|
}
|
|
var text = PVE.Utils.kvm_keymaps[value];
|
|
if (text) {
|
|
return text + ' (' + value + ')';
|
|
}
|
|
return value;
|
|
},
|
|
|
|
kvm_keymap_array: function() {
|
|
var data = [['__default__', PVE.Utils.render_kvm_language('')]];
|
|
Ext.Object.each(PVE.Utils.kvm_keymaps, function(key, value) {
|
|
data.push([key, PVE.Utils.render_kvm_language(value)]);
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
console_map: {
|
|
'__default__': Proxmox.Utils.defaultText + ' (xterm.js)',
|
|
'vv': 'SPICE (remote-viewer)',
|
|
'html5': 'HTML5 (noVNC)',
|
|
'xtermjs': 'xterm.js',
|
|
},
|
|
|
|
render_console_viewer: function(value) {
|
|
value = value || '__default__';
|
|
if (PVE.Utils.console_map[value]) {
|
|
return PVE.Utils.console_map[value];
|
|
}
|
|
return value;
|
|
},
|
|
|
|
console_viewer_array: function() {
|
|
return Ext.Array.map(Object.keys(PVE.Utils.console_map), function(v) {
|
|
return [v, PVE.Utils.render_console_viewer(v)];
|
|
});
|
|
},
|
|
|
|
render_kvm_vga_driver: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText;
|
|
}
|
|
var vga = PVE.Parser.parsePropertyString(value, 'type');
|
|
var text = PVE.Utils.kvm_vga_drivers[vga.type];
|
|
if (!vga.type) {
|
|
text = Proxmox.Utils.defaultText;
|
|
}
|
|
if (text) {
|
|
return text + ' (' + value + ')';
|
|
}
|
|
return value;
|
|
},
|
|
|
|
kvm_vga_driver_array: function() {
|
|
var data = [['__default__', PVE.Utils.render_kvm_vga_driver('')]];
|
|
Ext.Object.each(PVE.Utils.kvm_vga_drivers, function(key, value) {
|
|
data.push([key, PVE.Utils.render_kvm_vga_driver(value)]);
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
render_kvm_startup: function(value) {
|
|
var startup = PVE.Parser.parseStartup(value);
|
|
|
|
var res = 'order=';
|
|
if (startup.order === undefined) {
|
|
res += 'any';
|
|
} else {
|
|
res += startup.order;
|
|
}
|
|
if (startup.up !== undefined) {
|
|
res += ',up=' + startup.up;
|
|
}
|
|
if (startup.down !== undefined) {
|
|
res += ',down=' + startup.down;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
extractFormActionError: function(action) {
|
|
var msg;
|
|
switch (action.failureType) {
|
|
case Ext.form.action.Action.CLIENT_INVALID:
|
|
msg = gettext('Form fields may not be submitted with invalid values');
|
|
break;
|
|
case Ext.form.action.Action.CONNECT_FAILURE:
|
|
msg = gettext('Connection error');
|
|
var resp = action.response;
|
|
if (resp.status && resp.statusText) {
|
|
msg += " " + resp.status + ": " + resp.statusText;
|
|
}
|
|
break;
|
|
case Ext.form.action.Action.LOAD_FAILURE:
|
|
case Ext.form.action.Action.SERVER_INVALID:
|
|
msg = Proxmox.Utils.extractRequestError(action.result, true);
|
|
break;
|
|
}
|
|
return msg;
|
|
},
|
|
|
|
contentTypes: {
|
|
'images': gettext('Disk image'),
|
|
'backup': gettext('VZDump backup file'),
|
|
'vztmpl': gettext('Container template'),
|
|
'iso': gettext('ISO image'),
|
|
'rootdir': gettext('Container'),
|
|
'snippets': gettext('Snippets'),
|
|
},
|
|
|
|
volume_is_qemu_backup: function(volid, format) {
|
|
return format === 'pbs-vm' || volid.match(':backup/vzdump-qemu-');
|
|
},
|
|
|
|
volume_is_lxc_backup: function(volid, format) {
|
|
return format === 'pbs-ct' || volid.match(':backup/vzdump-(lxc|openvz)-');
|
|
},
|
|
|
|
authSchema: {
|
|
ad: {
|
|
name: gettext('Active Directory Server'),
|
|
ipanel: 'pveAuthADPanel',
|
|
syncipanel: 'pveAuthLDAPSyncPanel',
|
|
add: true,
|
|
tfa: true,
|
|
pwchange: true,
|
|
},
|
|
ldap: {
|
|
name: gettext('LDAP Server'),
|
|
ipanel: 'pveAuthLDAPPanel',
|
|
syncipanel: 'pveAuthLDAPSyncPanel',
|
|
add: true,
|
|
tfa: true,
|
|
pwchange: true,
|
|
},
|
|
openid: {
|
|
name: gettext('OpenID Connect Server'),
|
|
ipanel: 'pveAuthOpenIDPanel',
|
|
add: true,
|
|
tfa: false,
|
|
pwchange: false,
|
|
iconCls: 'pmx-itype-icon-openid-logo',
|
|
},
|
|
pam: {
|
|
name: 'Linux PAM',
|
|
ipanel: 'pveAuthBasePanel',
|
|
add: false,
|
|
tfa: true,
|
|
pwchange: true,
|
|
},
|
|
pve: {
|
|
name: 'Proxmox VE authentication server',
|
|
ipanel: 'pveAuthBasePanel',
|
|
add: false,
|
|
pwchange: true,
|
|
},
|
|
},
|
|
|
|
storageSchema: {
|
|
dir: {
|
|
name: Proxmox.Utils.directoryText,
|
|
ipanel: 'DirInputPanel',
|
|
faIcon: 'folder',
|
|
backups: true,
|
|
},
|
|
lvm: {
|
|
name: 'LVM',
|
|
ipanel: 'LVMInputPanel',
|
|
faIcon: 'folder',
|
|
backups: false,
|
|
},
|
|
lvmthin: {
|
|
name: 'LVM-Thin',
|
|
ipanel: 'LvmThinInputPanel',
|
|
faIcon: 'folder',
|
|
backups: false,
|
|
},
|
|
btrfs: {
|
|
name: 'BTRFS',
|
|
ipanel: 'BTRFSInputPanel',
|
|
faIcon: 'folder',
|
|
backups: true,
|
|
},
|
|
nfs: {
|
|
name: 'NFS',
|
|
ipanel: 'NFSInputPanel',
|
|
faIcon: 'building',
|
|
backups: true,
|
|
},
|
|
cifs: {
|
|
name: 'CIFS',
|
|
ipanel: 'CIFSInputPanel',
|
|
faIcon: 'building',
|
|
backups: true,
|
|
},
|
|
glusterfs: {
|
|
name: 'GlusterFS',
|
|
ipanel: 'GlusterFsInputPanel',
|
|
faIcon: 'building',
|
|
backups: true,
|
|
},
|
|
iscsi: {
|
|
name: 'iSCSI',
|
|
ipanel: 'IScsiInputPanel',
|
|
faIcon: 'building',
|
|
backups: false,
|
|
},
|
|
cephfs: {
|
|
name: 'CephFS',
|
|
ipanel: 'CephFSInputPanel',
|
|
faIcon: 'building',
|
|
backups: true,
|
|
},
|
|
pvecephfs: {
|
|
name: 'CephFS (PVE)',
|
|
ipanel: 'CephFSInputPanel',
|
|
hideAdd: true,
|
|
faIcon: 'building',
|
|
backups: true,
|
|
},
|
|
rbd: {
|
|
name: 'RBD',
|
|
ipanel: 'RBDInputPanel',
|
|
faIcon: 'building',
|
|
backups: false,
|
|
},
|
|
pveceph: {
|
|
name: 'RBD (PVE)',
|
|
ipanel: 'RBDInputPanel',
|
|
hideAdd: true,
|
|
faIcon: 'building',
|
|
backups: false,
|
|
},
|
|
zfs: {
|
|
name: 'ZFS over iSCSI',
|
|
ipanel: 'ZFSInputPanel',
|
|
faIcon: 'building',
|
|
backups: false,
|
|
},
|
|
zfspool: {
|
|
name: 'ZFS',
|
|
ipanel: 'ZFSPoolInputPanel',
|
|
faIcon: 'folder',
|
|
backups: false,
|
|
},
|
|
pbs: {
|
|
name: 'Proxmox Backup Server',
|
|
ipanel: 'PBSInputPanel',
|
|
faIcon: 'floppy-o',
|
|
backups: true,
|
|
},
|
|
drbd: {
|
|
name: 'DRBD',
|
|
hideAdd: true,
|
|
backups: false,
|
|
},
|
|
},
|
|
|
|
sdnvnetSchema: {
|
|
vnet: {
|
|
name: 'vnet',
|
|
faIcon: 'folder',
|
|
},
|
|
},
|
|
|
|
sdnzoneSchema: {
|
|
zone: {
|
|
name: 'zone',
|
|
hideAdd: true,
|
|
},
|
|
simple: {
|
|
name: 'Simple',
|
|
ipanel: 'SimpleInputPanel',
|
|
faIcon: 'th',
|
|
},
|
|
vlan: {
|
|
name: 'VLAN',
|
|
ipanel: 'VlanInputPanel',
|
|
faIcon: 'th',
|
|
},
|
|
qinq: {
|
|
name: 'QinQ',
|
|
ipanel: 'QinQInputPanel',
|
|
faIcon: 'th',
|
|
},
|
|
vxlan: {
|
|
name: 'VXLAN',
|
|
ipanel: 'VxlanInputPanel',
|
|
faIcon: 'th',
|
|
},
|
|
evpn: {
|
|
name: 'EVPN',
|
|
ipanel: 'EvpnInputPanel',
|
|
faIcon: 'th',
|
|
},
|
|
},
|
|
|
|
sdncontrollerSchema: {
|
|
controller: {
|
|
name: 'controller',
|
|
hideAdd: true,
|
|
},
|
|
evpn: {
|
|
name: 'evpn',
|
|
ipanel: 'EvpnInputPanel',
|
|
faIcon: 'crosshairs',
|
|
},
|
|
bgp: {
|
|
name: 'bgp',
|
|
ipanel: 'BgpInputPanel',
|
|
faIcon: 'crosshairs',
|
|
},
|
|
},
|
|
|
|
sdnipamSchema: {
|
|
ipam: {
|
|
name: 'ipam',
|
|
hideAdd: true,
|
|
},
|
|
pve: {
|
|
name: 'PVE',
|
|
ipanel: 'PVEIpamInputPanel',
|
|
faIcon: 'th',
|
|
hideAdd: true,
|
|
},
|
|
netbox: {
|
|
name: 'Netbox',
|
|
ipanel: 'NetboxInputPanel',
|
|
faIcon: 'th',
|
|
},
|
|
phpipam: {
|
|
name: 'PhpIpam',
|
|
ipanel: 'PhpIpamInputPanel',
|
|
faIcon: 'th',
|
|
},
|
|
},
|
|
|
|
sdndnsSchema: {
|
|
dns: {
|
|
name: 'dns',
|
|
hideAdd: true,
|
|
},
|
|
powerdns: {
|
|
name: 'powerdns',
|
|
ipanel: 'PowerdnsInputPanel',
|
|
faIcon: 'th',
|
|
},
|
|
},
|
|
|
|
format_sdnvnet_type: function(value, md, record) {
|
|
var schema = PVE.Utils.sdnvnetSchema[value];
|
|
if (schema) {
|
|
return schema.name;
|
|
}
|
|
return Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
format_sdnzone_type: function(value, md, record) {
|
|
var schema = PVE.Utils.sdnzoneSchema[value];
|
|
if (schema) {
|
|
return schema.name;
|
|
}
|
|
return Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
format_sdncontroller_type: function(value, md, record) {
|
|
var schema = PVE.Utils.sdncontrollerSchema[value];
|
|
if (schema) {
|
|
return schema.name;
|
|
}
|
|
return Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
format_sdnipam_type: function(value, md, record) {
|
|
var schema = PVE.Utils.sdnipamSchema[value];
|
|
if (schema) {
|
|
return schema.name;
|
|
}
|
|
return Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
format_sdndns_type: function(value, md, record) {
|
|
var schema = PVE.Utils.sdndnsSchema[value];
|
|
if (schema) {
|
|
return schema.name;
|
|
}
|
|
return Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
format_storage_type: function(value, md, record) {
|
|
if (value === 'rbd') {
|
|
value = !record || record.get('monhost') ? 'rbd' : 'pveceph';
|
|
} else if (value === 'cephfs') {
|
|
value = !record || record.get('monhost') ? 'cephfs' : 'pvecephfs';
|
|
}
|
|
|
|
var schema = PVE.Utils.storageSchema[value];
|
|
if (schema) {
|
|
return schema.name;
|
|
}
|
|
return Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
format_ha: function(value) {
|
|
var text = Proxmox.Utils.noneText;
|
|
|
|
if (value.managed) {
|
|
text = value.state || Proxmox.Utils.noneText;
|
|
|
|
text += ', ' + Proxmox.Utils.groupText + ': ';
|
|
text += value.group || Proxmox.Utils.noneText;
|
|
}
|
|
|
|
return text;
|
|
},
|
|
|
|
format_content_types: function(value) {
|
|
return value.split(',').sort().map(function(ct) {
|
|
return PVE.Utils.contentTypes[ct] || ct;
|
|
}).join(', ');
|
|
},
|
|
|
|
render_storage_content: function(value, metaData, record) {
|
|
var data = record.data;
|
|
if (Ext.isNumber(data.channel) &&
|
|
Ext.isNumber(data.id) &&
|
|
Ext.isNumber(data.lun)) {
|
|
return "CH " +
|
|
Ext.String.leftPad(data.channel, 2, '0') +
|
|
" ID " + data.id + " LUN " + data.lun;
|
|
}
|
|
return data.volid.replace(/^.*?:(.*?\/)?/, '');
|
|
},
|
|
|
|
render_serverity: function(value) {
|
|
return PVE.Utils.log_severity_hash[value] || value;
|
|
},
|
|
|
|
calculate_hostcpu: function(data) {
|
|
if (!(data.uptime && Ext.isNumeric(data.cpu))) {
|
|
return -1;
|
|
}
|
|
|
|
if (data.type !== 'qemu' && data.type !== 'lxc') {
|
|
return -1;
|
|
}
|
|
|
|
var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node);
|
|
var node = PVE.data.ResourceStore.getAt(index);
|
|
if (!Ext.isDefined(node) || node === null) {
|
|
return -1;
|
|
}
|
|
var maxcpu = node.data.maxcpu || 1;
|
|
|
|
if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
|
|
return -1;
|
|
}
|
|
|
|
return (data.cpu/maxcpu) * data.maxcpu;
|
|
},
|
|
|
|
render_hostcpu: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
if (!(record.data.uptime && Ext.isNumeric(record.data.cpu))) {
|
|
return '';
|
|
}
|
|
|
|
if (record.data.type !== 'qemu' && record.data.type !== 'lxc') {
|
|
return '';
|
|
}
|
|
|
|
var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node);
|
|
var node = PVE.data.ResourceStore.getAt(index);
|
|
if (!Ext.isDefined(node) || node === null) {
|
|
return '';
|
|
}
|
|
var maxcpu = node.data.maxcpu || 1;
|
|
|
|
if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
|
|
return '';
|
|
}
|
|
|
|
var per = (record.data.cpu/maxcpu) * record.data.maxcpu * 100;
|
|
|
|
return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU');
|
|
},
|
|
|
|
render_bandwidth: function(value) {
|
|
if (!Ext.isNumeric(value)) {
|
|
return '';
|
|
}
|
|
|
|
return Proxmox.Utils.format_size(value) + '/s';
|
|
},
|
|
|
|
render_timestamp_human_readable: function(value) {
|
|
return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s');
|
|
},
|
|
|
|
calculate_mem_usage: function(data) {
|
|
if (!Ext.isNumeric(data.mem) ||
|
|
data.maxmem === 0 ||
|
|
data.uptime < 1) {
|
|
return -1;
|
|
}
|
|
|
|
return data.mem / data.maxmem;
|
|
},
|
|
|
|
calculate_hostmem_usage: function(data) {
|
|
if (data.type !== 'qemu' && data.type !== 'lxc') {
|
|
return -1;
|
|
}
|
|
|
|
var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node);
|
|
var node = PVE.data.ResourceStore.getAt(index);
|
|
|
|
if (!Ext.isDefined(node) || node === null) {
|
|
return -1;
|
|
}
|
|
var maxmem = node.data.maxmem || 0;
|
|
|
|
if (!Ext.isNumeric(data.mem) ||
|
|
maxmem === 0 ||
|
|
data.uptime < 1) {
|
|
return -1;
|
|
}
|
|
|
|
return data.mem / maxmem;
|
|
},
|
|
|
|
render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
if (!Ext.isNumeric(value) || value === -1) {
|
|
return '';
|
|
}
|
|
if (value > 1) {
|
|
// we got no percentage but bytes
|
|
var mem = value;
|
|
var maxmem = record.data.maxmem;
|
|
if (!record.data.uptime ||
|
|
maxmem === 0 ||
|
|
!Ext.isNumeric(mem)) {
|
|
return '';
|
|
}
|
|
|
|
return (mem*100/maxmem).toFixed(1) + " %";
|
|
}
|
|
return (value*100).toFixed(1) + " %";
|
|
},
|
|
|
|
render_hostmem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
if (!Ext.isNumeric(record.data.mem) || value === -1) {
|
|
return '';
|
|
}
|
|
|
|
if (record.data.type !== 'qemu' && record.data.type !== 'lxc') {
|
|
return '';
|
|
}
|
|
|
|
var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node);
|
|
var node = PVE.data.ResourceStore.getAt(index);
|
|
var maxmem = node.data.maxmem || 0;
|
|
|
|
if (record.data.mem > 1) {
|
|
// we got no percentage but bytes
|
|
var mem = record.data.mem;
|
|
if (!record.data.uptime ||
|
|
maxmem === 0 ||
|
|
!Ext.isNumeric(mem)) {
|
|
return '';
|
|
}
|
|
|
|
return ((mem*100)/maxmem).toFixed(1) + " %";
|
|
}
|
|
return (value*100).toFixed(1) + " %";
|
|
},
|
|
|
|
render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
var mem = value;
|
|
var maxmem = record.data.maxmem;
|
|
|
|
if (!record.data.uptime) {
|
|
return '';
|
|
}
|
|
|
|
if (!(Ext.isNumeric(mem) && maxmem)) {
|
|
return '';
|
|
}
|
|
|
|
return Proxmox.Utils.render_size(value);
|
|
},
|
|
|
|
calculate_disk_usage: function(data) {
|
|
if (!Ext.isNumeric(data.disk) ||
|
|
((data.type === 'qemu' || data.type === 'lxc') && data.uptime === 0) ||
|
|
data.maxdisk === 0
|
|
) {
|
|
return -1;
|
|
}
|
|
|
|
return data.disk / data.maxdisk;
|
|
},
|
|
|
|
render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
if (!Ext.isNumeric(value) || value === -1) {
|
|
return '';
|
|
}
|
|
|
|
return (value * 100).toFixed(1) + " %";
|
|
},
|
|
|
|
render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
var disk = value;
|
|
var maxdisk = record.data.maxdisk;
|
|
var type = record.data.type;
|
|
|
|
if (!Ext.isNumeric(disk) ||
|
|
maxdisk === 0 ||
|
|
((type === 'qemu' || type === 'lxc') && record.data.uptime === 0)
|
|
) {
|
|
return '';
|
|
}
|
|
|
|
return Proxmox.Utils.render_size(value);
|
|
},
|
|
|
|
get_object_icon_class: function(type, record) {
|
|
var status = '';
|
|
var objType = type;
|
|
|
|
if (type === 'type') {
|
|
// for folder view
|
|
objType = record.groupbyid;
|
|
} else if (record.template) {
|
|
// templates
|
|
objType = 'template';
|
|
status = type;
|
|
} else {
|
|
// everything else
|
|
status = record.status + ' ha-' + record.hastate;
|
|
}
|
|
|
|
if (record.lock) {
|
|
status += ' locked lock-' + record.lock;
|
|
}
|
|
|
|
var defaults = PVE.tree.ResourceTree.typeDefaults[objType];
|
|
if (defaults && defaults.iconCls) {
|
|
var retVal = defaults.iconCls + ' ' + status;
|
|
return retVal;
|
|
}
|
|
|
|
return '';
|
|
},
|
|
|
|
render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
var cls = PVE.Utils.get_object_icon_class(value, record.data);
|
|
|
|
var fa = '<i class="fa-fw x-grid-icon-custom ' + cls + '"></i> ';
|
|
return fa + value;
|
|
},
|
|
|
|
render_support_level: function(value, metaData, record) {
|
|
return PVE.Utils.support_level_hash[value] || '-';
|
|
},
|
|
|
|
render_upid: function(value, metaData, record) {
|
|
var type = record.data.type;
|
|
var id = record.data.id;
|
|
|
|
return Proxmox.Utils.format_task_description(type, id);
|
|
},
|
|
|
|
render_optional_url: function(value) {
|
|
if (value && value.match(/^https?:\/\//)) {
|
|
return '<a target="_blank" href="' + value + '">' + value + '</a>';
|
|
}
|
|
return value;
|
|
},
|
|
|
|
render_san: function(value) {
|
|
var names = [];
|
|
if (Ext.isArray(value)) {
|
|
value.forEach(function(val) {
|
|
if (!Ext.isNumber(val)) {
|
|
names.push(val);
|
|
}
|
|
});
|
|
return names.join('<br>');
|
|
}
|
|
return value;
|
|
},
|
|
|
|
render_full_name: function(firstname, metaData, record) {
|
|
var first = firstname || '';
|
|
var last = record.data.lastname || '';
|
|
return Ext.htmlEncode(first + " " + last);
|
|
},
|
|
|
|
render_u2f_error: function(error) {
|
|
var ErrorNames = {
|
|
'1': gettext('Other Error'),
|
|
'2': gettext('Bad Request'),
|
|
'3': gettext('Configuration Unsupported'),
|
|
'4': gettext('Device Ineligible'),
|
|
'5': gettext('Timeout'),
|
|
};
|
|
return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
windowHostname: function() {
|
|
return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match,
|
|
function(m, addr, offset, original) { return addr; });
|
|
},
|
|
|
|
openDefaultConsoleWindow: function(consoles, consoleType, vmid, nodename, vmname, cmd) {
|
|
var dv = PVE.Utils.defaultViewer(consoles, consoleType);
|
|
PVE.Utils.openConsoleWindow(dv, consoleType, vmid, nodename, vmname, cmd);
|
|
},
|
|
|
|
openConsoleWindow: function(viewer, consoleType, vmid, nodename, vmname, cmd) {
|
|
if (vmid === undefined && (consoleType === 'kvm' || consoleType === 'lxc')) {
|
|
throw "missing vmid";
|
|
}
|
|
if (!nodename) {
|
|
throw "no nodename specified";
|
|
}
|
|
|
|
if (viewer === 'html5') {
|
|
PVE.Utils.openVNCViewer(consoleType, vmid, nodename, vmname, cmd);
|
|
} else if (viewer === 'xtermjs') {
|
|
Proxmox.Utils.openXtermJsViewer(consoleType, vmid, nodename, vmname, cmd);
|
|
} else if (viewer === 'vv') {
|
|
let url = '/nodes/' + nodename + '/spiceshell';
|
|
let params = {
|
|
proxy: PVE.Utils.windowHostname(),
|
|
};
|
|
if (consoleType === 'kvm') {
|
|
url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy';
|
|
} else if (consoleType === 'lxc') {
|
|
url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy';
|
|
} else if (consoleType === 'upgrade') {
|
|
params.cmd = 'upgrade';
|
|
} else if (consoleType === 'cmd') {
|
|
params.cmd = cmd;
|
|
} else if (consoleType !== 'shell') {
|
|
throw `unknown spice viewer type '${consoleType}'`;
|
|
}
|
|
PVE.Utils.openSpiceViewer(url, params);
|
|
} else {
|
|
throw `unknown viewer type '${viewer}'`;
|
|
}
|
|
},
|
|
|
|
defaultViewer: function(consoles, type) {
|
|
var allowSpice, allowXtermjs;
|
|
|
|
if (consoles === true) {
|
|
allowSpice = true;
|
|
allowXtermjs = true;
|
|
} else if (typeof consoles === 'object') {
|
|
allowSpice = consoles.spice;
|
|
allowXtermjs = !!consoles.xtermjs;
|
|
}
|
|
let dv = PVE.VersionInfo.console || (type === 'kvm' ? 'vv' : 'xtermjs');
|
|
if (dv === 'vv' && !allowSpice) {
|
|
dv = allowXtermjs ? 'xtermjs' : 'html5';
|
|
} else if (dv === 'xtermjs' && !allowXtermjs) {
|
|
dv = allowSpice ? 'vv' : 'html5';
|
|
}
|
|
|
|
return dv;
|
|
},
|
|
|
|
openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) {
|
|
let scaling = 'off';
|
|
if (Proxmox.Utils.toolkit !== 'touch') {
|
|
var sp = Ext.state.Manager.getProvider();
|
|
scaling = sp.get('novnc-scaling', 'off');
|
|
}
|
|
var url = Ext.Object.toQueryString({
|
|
console: vmtype, // kvm, lxc, upgrade or shell
|
|
novnc: 1,
|
|
vmid: vmid,
|
|
vmname: vmname,
|
|
node: nodename,
|
|
resize: scaling,
|
|
cmd: cmd,
|
|
});
|
|
var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427");
|
|
if (nw) {
|
|
nw.focus();
|
|
}
|
|
},
|
|
|
|
openSpiceViewer: function(url, params) {
|
|
var downloadWithName = function(uri, name) {
|
|
var link = Ext.DomHelper.append(document.body, {
|
|
tag: 'a',
|
|
href: uri,
|
|
css: 'display:none;visibility:hidden;height:0px;',
|
|
});
|
|
|
|
// Note: we need to tell Android and Chrome the correct file name extension
|
|
// but we do not set 'download' tag for other environments, because
|
|
// It can have strange side effects (additional user prompt on firefox)
|
|
if (navigator.userAgent.match(/Android|Chrome/i)) {
|
|
link.download = name;
|
|
}
|
|
|
|
if (link.fireEvent) {
|
|
link.fireEvent('onclick');
|
|
} else {
|
|
let evt = document.createEvent("MouseEvents");
|
|
evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
|
|
link.dispatchEvent(evt);
|
|
}
|
|
};
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: url,
|
|
params: params,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
let cfg = response.result.data;
|
|
let raw = Object.entries(cfg).reduce((acc, [k, v]) => acc + `${k}=${v}\n`, "[virt-viewer]\n");
|
|
let spiceDownload = 'data:application/x-virt-viewer;charset=UTF-8,' + encodeURIComponent(raw);
|
|
downloadWithName(spiceDownload, "pve-spice.vv");
|
|
},
|
|
});
|
|
},
|
|
|
|
openTreeConsole: function(tree, record, item, index, e) {
|
|
e.stopEvent();
|
|
let nodename = record.data.node;
|
|
let vmid = record.data.vmid;
|
|
let vmname = record.data.name;
|
|
if (record.data.type === 'qemu' && !record.data.template) {
|
|
Proxmox.Utils.API2Request({
|
|
url: `/nodes/${nodename}/qemu/${vmid}/status/current`,
|
|
failure: response => Ext.Msg.alert('Error', response.htmlStatus),
|
|
success: function(response, opts) {
|
|
let conf = response.result.data;
|
|
let consoles = {
|
|
spice: !!conf.spice,
|
|
xtermjs: !!conf.serial,
|
|
};
|
|
PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
|
|
},
|
|
});
|
|
} else if (record.data.type === 'lxc' && !record.data.template) {
|
|
PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
|
|
}
|
|
},
|
|
|
|
// test automation helper
|
|
call_menu_handler: function(menu, text) {
|
|
let item = menu.query('menuitem').find(el => el.text === text);
|
|
if (item && item.handler) {
|
|
item.handler();
|
|
}
|
|
},
|
|
|
|
createCmdMenu: function(v, record, item, index, event) {
|
|
event.stopEvent();
|
|
if (!(v instanceof Ext.tree.View)) {
|
|
v.select(record);
|
|
}
|
|
let menu;
|
|
let type = record.data.type;
|
|
|
|
if (record.data.template) {
|
|
if (type === 'qemu' || type === 'lxc') {
|
|
menu = Ext.create('PVE.menu.TemplateMenu', {
|
|
pveSelNode: record,
|
|
});
|
|
}
|
|
} else if (type === 'qemu' || type === 'lxc' || type === 'node') {
|
|
menu = Ext.create('PVE.' + type + '.CmdMenu', {
|
|
pveSelNode: record,
|
|
nodename: record.data.node,
|
|
});
|
|
} else {
|
|
return undefined;
|
|
}
|
|
|
|
menu.showAt(event.getXY());
|
|
return menu;
|
|
},
|
|
|
|
// 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];
|
|
}
|
|
},
|
|
|
|
loadSSHKeyFromFile: function(file, callback) {
|
|
// ssh-keygen produces ~ 740 bytes for a 4096 bit RSA key, current max is 16 kbit, so assume:
|
|
// 740 * 8 for max. 32kbit (5920 bytes), round upwards to 8192 bytes, leaves lots of comment space
|
|
PVE.Utils.loadFile(file, callback, 8192);
|
|
},
|
|
|
|
loadFile: function(file, callback, maxSize) {
|
|
maxSize = maxSize || 32 * 1024;
|
|
if (file.size > maxSize) {
|
|
Ext.Msg.alert(gettext('Error'), `${gettext("Invalid file size")}: ${file.size} > ${maxSize}`);
|
|
return;
|
|
}
|
|
let reader = new FileReader();
|
|
reader.onload = evt => callback(evt.target.result);
|
|
reader.readAsText(file);
|
|
},
|
|
|
|
loadTextFromFile: function(file, callback, maxBytes) {
|
|
let maxSize = maxBytes || 8192;
|
|
if (file.size > maxSize) {
|
|
Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
|
|
return;
|
|
}
|
|
let reader = new FileReader();
|
|
reader.onload = evt => callback(evt.target.result);
|
|
reader.readAsText(file);
|
|
},
|
|
|
|
diskControllerMaxIDs: {
|
|
ide: 4,
|
|
sata: 6,
|
|
scsi: 31,
|
|
virtio: 16,
|
|
},
|
|
|
|
// types is either undefined (all busses), an array of busses, or a single bus
|
|
forEachBus: function(types, func) {
|
|
let busses = Object.keys(PVE.Utils.diskControllerMaxIDs);
|
|
|
|
if (Ext.isArray(types)) {
|
|
busses = types;
|
|
} else if (Ext.isDefined(types)) {
|
|
busses = [types];
|
|
}
|
|
|
|
// check if we only have valid busses
|
|
for (let i = 0; i < busses.length; i++) {
|
|
if (!PVE.Utils.diskControllerMaxIDs[busses[i]]) {
|
|
throw "invalid bus: '" + busses[i] + "'";
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < busses.length; i++) {
|
|
let count = PVE.Utils.diskControllerMaxIDs[busses[i]];
|
|
for (let j = 0; j < count; j++) {
|
|
let cont = func(busses[i], j);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
mp_counts: {
|
|
mps: 256,
|
|
unused: 256,
|
|
},
|
|
|
|
forEachMP: function(func, includeUnused) {
|
|
for (let i = 0; i < PVE.Utils.mp_counts.mps; i++) {
|
|
let cont = func('mp', i);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!includeUnused) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < PVE.Utils.mp_counts.unused; i++) {
|
|
let cont = func('unused', i);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
hardware_counts: { net: 32, usb: 5, hostpci: 16, audio: 1, efidisk: 1, serial: 4, rng: 1 },
|
|
|
|
cleanEmptyObjectKeys: function(obj) {
|
|
for (const propName of Object.keys(obj)) {
|
|
if (obj[propName] === null || obj[propName] === undefined) {
|
|
delete obj[propName];
|
|
}
|
|
}
|
|
},
|
|
|
|
acmedomain_count: 5,
|
|
|
|
add_domain_to_acme: function(acme, domain) {
|
|
if (acme.domains === undefined) {
|
|
acme.domains = [domain];
|
|
} else {
|
|
acme.domains.push(domain);
|
|
acme.domains = acme.domains.filter((value, index, self) => self.indexOf(value) === index);
|
|
}
|
|
return acme;
|
|
},
|
|
|
|
remove_domain_from_acme: function(acme, domain) {
|
|
if (acme.domains !== undefined) {
|
|
acme.domains = acme
|
|
.domains
|
|
.filter((value, index, self) => self.indexOf(value) === index && value !== domain);
|
|
}
|
|
return acme;
|
|
},
|
|
|
|
handleStoreErrorOrMask: function(view, store, regex, callback) {
|
|
view.mon(store, 'load', function(proxy, response, success, operation) {
|
|
if (success) {
|
|
Proxmox.Utils.setErrorMask(view, false);
|
|
return;
|
|
}
|
|
let msg;
|
|
if (operation.error.statusText) {
|
|
if (operation.error.statusText.match(regex)) {
|
|
callback(view, operation.error);
|
|
return;
|
|
} else {
|
|
msg = operation.error.statusText + ' (' + operation.error.status + ')';
|
|
}
|
|
} else {
|
|
msg = gettext('Connection error');
|
|
}
|
|
Proxmox.Utils.setErrorMask(view, msg);
|
|
});
|
|
},
|
|
|
|
showCephInstallOrMask: function(container, msg, nodename, callback) {
|
|
if (msg.match(/not (installed|initialized)/i)) {
|
|
if (Proxmox.UserName === 'root@pam') {
|
|
container.el.mask();
|
|
if (!container.down('pveCephInstallWindow')) {
|
|
var isInstalled = !!msg.match(/not initialized/i);
|
|
var win = Ext.create('PVE.ceph.Install', {
|
|
nodename: nodename,
|
|
});
|
|
win.getViewModel().set('isInstalled', isInstalled);
|
|
container.add(win);
|
|
win.show();
|
|
callback(win);
|
|
}
|
|
} else {
|
|
container.mask(Ext.String.format(gettext('{0} not installed.') +
|
|
' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']);
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
monitor_ceph_installed: function(view, rstore, nodename, maskOwnerCt) {
|
|
PVE.Utils.handleStoreErrorOrMask(
|
|
view,
|
|
rstore,
|
|
/not (installed|initialized)/i,
|
|
(_, error) => {
|
|
nodename = nodename || 'localhost';
|
|
let maskTarget = maskOwnerCt ? view.ownerCt : view;
|
|
rstore.stopUpdate();
|
|
PVE.Utils.showCephInstallOrMask(maskTarget, error.statusText, nodename, win => {
|
|
view.mon(win, 'cephInstallWindowClosed', () => rstore.startUpdate());
|
|
});
|
|
},
|
|
);
|
|
},
|
|
|
|
|
|
propertyStringSet: function(target, source, name, value) {
|
|
if (source) {
|
|
if (value === undefined) {
|
|
target[name] = source;
|
|
} else {
|
|
target[name] = value;
|
|
}
|
|
} else {
|
|
delete target[name];
|
|
}
|
|
},
|
|
|
|
forEachCorosyncLink: function(nodeinfo, cb) {
|
|
let re = /(?:ring|link)(\d+)_addr/;
|
|
Ext.iterate(nodeinfo, (prop, val) => {
|
|
let match = re.exec(prop);
|
|
if (match) {
|
|
cb(Number(match[1]), val);
|
|
}
|
|
});
|
|
},
|
|
|
|
cpu_vendor_map: {
|
|
'default': 'QEMU',
|
|
'AuthenticAMD': 'AMD',
|
|
'GenuineIntel': 'Intel',
|
|
},
|
|
|
|
cpu_vendor_order: {
|
|
"AMD": 1,
|
|
"Intel": 2,
|
|
"QEMU": 3,
|
|
"Host": 4,
|
|
"_default_": 5, // includes custom models
|
|
},
|
|
|
|
verify_ip64_address_list: function(value, with_suffix) {
|
|
for (let addr of value.split(/[ ,;]+/)) {
|
|
if (addr === '') {
|
|
continue;
|
|
}
|
|
|
|
if (with_suffix) {
|
|
let parts = addr.split('%');
|
|
addr = parts[0];
|
|
|
|
if (parts.length > 2) {
|
|
return false;
|
|
}
|
|
|
|
if (parts.length > 1 && !addr.startsWith('fe80:')) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!Proxmox.Utils.IP64_match.test(addr)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
},
|
|
|
|
singleton: true,
|
|
constructor: function() {
|
|
var me = this;
|
|
Ext.apply(me, me.utilities);
|
|
|
|
Proxmox.Utils.override_task_descriptions({
|
|
acmedeactivate: ['ACME Account', gettext('Deactivate')],
|
|
acmenewcert: ['SRV', gettext('Order Certificate')],
|
|
acmerefresh: ['ACME Account', gettext('Refresh')],
|
|
acmeregister: ['ACME Account', gettext('Register')],
|
|
acmerenew: ['SRV', gettext('Renew Certificate')],
|
|
acmerevoke: ['SRV', gettext('Revoke Certificate')],
|
|
acmeupdate: ['ACME Account', gettext('Update')],
|
|
'auth-realm-sync': [gettext('Realm'), gettext('Sync')],
|
|
'auth-realm-sync-test': [gettext('Realm'), gettext('Sync Preview')],
|
|
cephcreatemds: ['Ceph Metadata Server', gettext('Create')],
|
|
cephcreatemgr: ['Ceph Manager', gettext('Create')],
|
|
cephcreatemon: ['Ceph Monitor', gettext('Create')],
|
|
cephcreateosd: ['Ceph OSD', gettext('Create')],
|
|
cephcreatepool: ['Ceph Pool', gettext('Create')],
|
|
cephdestroymds: ['Ceph Metadata Server', gettext('Destroy')],
|
|
cephdestroymgr: ['Ceph Manager', gettext('Destroy')],
|
|
cephdestroymon: ['Ceph Monitor', gettext('Destroy')],
|
|
cephdestroyosd: ['Ceph OSD', gettext('Destroy')],
|
|
cephdestroypool: ['Ceph Pool', gettext('Destroy')],
|
|
cephfscreate: ['CephFS', gettext('Create')],
|
|
cephsetpool: ['Ceph Pool', gettext('Edit')],
|
|
cephsetflags: ['', gettext('Change global Ceph flags')],
|
|
clustercreate: ['', gettext('Create Cluster')],
|
|
clusterjoin: ['', gettext('Join Cluster')],
|
|
dircreate: [gettext('Directory Storage'), gettext('Create')],
|
|
dirremove: [gettext('Directory'), gettext('Remove')],
|
|
download: [gettext('File'), gettext('Download')],
|
|
hamigrate: ['HA', gettext('Migrate')],
|
|
hashutdown: ['HA', gettext('Shutdown')],
|
|
hastart: ['HA', gettext('Start')],
|
|
hastop: ['HA', gettext('Stop')],
|
|
imgcopy: ['', gettext('Copy data')],
|
|
imgdel: ['', gettext('Erase data')],
|
|
lvmcreate: [gettext('LVM Storage'), gettext('Create')],
|
|
lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
|
|
migrateall: ['', gettext('Migrate all VMs and Containers')],
|
|
'move_volume': ['CT', gettext('Move Volume')],
|
|
'pbs-download': ['VM/CT', gettext('File Restore Download')],
|
|
pull_file: ['CT', gettext('Pull file')],
|
|
push_file: ['CT', gettext('Push file')],
|
|
qmclone: ['VM', gettext('Clone')],
|
|
qmconfig: ['VM', gettext('Configure')],
|
|
qmcreate: ['VM', gettext('Create')],
|
|
qmdelsnapshot: ['VM', gettext('Delete Snapshot')],
|
|
qmdestroy: ['VM', gettext('Destroy')],
|
|
qmigrate: ['VM', gettext('Migrate')],
|
|
qmmove: ['VM', gettext('Move disk')],
|
|
qmpause: ['VM', gettext('Pause')],
|
|
qmreboot: ['VM', gettext('Reboot')],
|
|
qmreset: ['VM', gettext('Reset')],
|
|
qmrestore: ['VM', gettext('Restore')],
|
|
qmresume: ['VM', gettext('Resume')],
|
|
qmrollback: ['VM', gettext('Rollback')],
|
|
qmshutdown: ['VM', gettext('Shutdown')],
|
|
qmsnapshot: ['VM', gettext('Snapshot')],
|
|
qmstart: ['VM', gettext('Start')],
|
|
qmstop: ['VM', gettext('Stop')],
|
|
qmsuspend: ['VM', gettext('Hibernate')],
|
|
qmtemplate: ['VM', gettext('Convert to template')],
|
|
spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'],
|
|
spiceshell: ['', gettext('Shell') + ' (Spice)'],
|
|
startall: ['', gettext('Start all VMs and Containers')],
|
|
stopall: ['', gettext('Stop all VMs and Containers')],
|
|
unknownimgdel: ['', gettext('Destroy image from unknown guest')],
|
|
wipedisk: ['Device', gettext('Wipe Disk')],
|
|
vncproxy: ['VM/CT', gettext('Console')],
|
|
vncshell: ['', gettext('Shell')],
|
|
vzclone: ['CT', gettext('Clone')],
|
|
vzcreate: ['CT', gettext('Create')],
|
|
vzdelsnapshot: ['CT', gettext('Delete Snapshot')],
|
|
vzdestroy: ['CT', gettext('Destroy')],
|
|
vzdump: (type, id) => id ? `VM/CT ${id} - ${gettext('Backup')}` : gettext('Backup Job'),
|
|
vzmigrate: ['CT', gettext('Migrate')],
|
|
vzmount: ['CT', gettext('Mount')],
|
|
vzreboot: ['CT', gettext('Reboot')],
|
|
vzrestore: ['CT', gettext('Restore')],
|
|
vzresume: ['CT', gettext('Resume')],
|
|
vzrollback: ['CT', gettext('Rollback')],
|
|
vzshutdown: ['CT', gettext('Shutdown')],
|
|
vzsnapshot: ['CT', gettext('Snapshot')],
|
|
vzstart: ['CT', gettext('Start')],
|
|
vzstop: ['CT', gettext('Stop')],
|
|
vzsuspend: ['CT', gettext('Suspend')],
|
|
vztemplate: ['CT', gettext('Convert to template')],
|
|
vzumount: ['CT', gettext('Unmount')],
|
|
zfscreate: [gettext('ZFS Storage'), gettext('Create')],
|
|
});
|
|
},
|
|
|
|
});
|