render_mem_usage_percent() treated any value above 1 as a byte count and recalculated the percentage from 'mem' and 'maxmem'. But the memory usage ratio of a guest can legitimately exceed 1, for example when the reported usage is the host-side memory of the QEMU process, which includes its own overhead. In the reported case, 'mem' was 2222472192 with a 'maxmem' of 2147483648, so the ratio of about 1.035 got taken as bytes and the search and resource grids showed 0.0 % instead of 103.5 %. All users of the renderer now pass the usage as a ratio, so drop the bytes fallback and always render the value as percentage. Signed-off-by: Michal Fox <me@dualfroz.com>
2267 lines
77 KiB
JavaScript
2267 lines
77 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/en/proxmox-virtual-environment/pricing">' +
|
|
'www.proxmox.com</a> to get a list of available options.',
|
|
|
|
getClusterSubscriptionLevel: async function () {
|
|
let { result } = await Proxmox.Async.api2({ url: '/cluster/status' });
|
|
let levelMap = Object.fromEntries(
|
|
result.data.filter((v) => v.type === 'node').map((v) => [v.name, v.level]),
|
|
);
|
|
return levelMap;
|
|
},
|
|
|
|
kvm_ostypes: {
|
|
Linux: [
|
|
{ desc: '7.x - 2.6 Kernel', val: 'l26' },
|
|
{ desc: '2.4 Kernel', val: 'l24' },
|
|
],
|
|
'Microsoft Windows': [
|
|
{ desc: '11/2022/2025', val: 'win11' },
|
|
{ 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) {
|
|
// See PVE/Ceph/Tools.pm - get_local_version
|
|
const match = service.ceph_version.match(/^ceph.*\sv?(\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 Ext.htmlEncode(value);
|
|
}
|
|
|
|
if (rec.data.state === 'deleted') {
|
|
if (value === undefined) {
|
|
return ' ';
|
|
} else {
|
|
return `<span style="text-decoration: line-through;">${Ext.htmlEncode(value)}</span>`;
|
|
}
|
|
} else if (
|
|
rec.data?.pending?.[key] !== undefined &&
|
|
rec.data?.pending?.[key] !== null
|
|
) {
|
|
if (rec.data.pending[key] === 'deleted') {
|
|
return ' ';
|
|
} else {
|
|
return Ext.htmlEncode(rec.data.pending[key]);
|
|
}
|
|
}
|
|
|
|
return Ext.htmlEncode(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}${Ext.htmlEncode(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 += `${Ext.htmlEncode(key)}: ${Ext.htmlEncode(keyvalue)} <br>`;
|
|
}
|
|
}
|
|
return `<span data-qtip="${Ext.htmlEncode(tip)}">${icon}${Ext.htmlEncode(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;
|
|
},
|
|
|
|
validateZfsBlocksize: function (value) {
|
|
if (!value) {
|
|
return true;
|
|
}
|
|
|
|
let match = value.match(/^([1-9][0-9]*)([km])?$/i);
|
|
if (!match) {
|
|
return gettext(
|
|
'Invalid format. Use numbers with optional k or m suffix (e.g., 16k).',
|
|
);
|
|
}
|
|
|
|
let bytes = parseInt(match[1], 10);
|
|
let suffix = match[2]?.toLowerCase();
|
|
|
|
if (suffix === 'k') {
|
|
bytes *= 1024;
|
|
} else if (suffix === 'm') {
|
|
bytes *= 1024 * 1024;
|
|
}
|
|
|
|
if (bytes < 512 || (bytes & (bytes - 1)) !== 0 || bytes > 16 * 1024 * 1024) {
|
|
return gettext('Value must be a power of 2 between 512 and 16m');
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
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 OVMF BIOS': gettext('EFI Disk without OVMF BIOS'),
|
|
},
|
|
|
|
renderNotFound: (what) => Ext.String.format(gettext('No {0} found'), what),
|
|
|
|
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 (key === 'freeze-fs-on-backup' && PVE.Parser.parseBoolean(value)) {
|
|
continue;
|
|
} else if (key === 'freeze-fs' && PVE.Parser.parseBoolean(value)) {
|
|
continue;
|
|
} else if (PVE.Parser.parseBoolean(value)) {
|
|
displayText = Proxmox.Utils.enabledText;
|
|
}
|
|
agentstring += `, ${key}: ${displayText}`;
|
|
}
|
|
|
|
return agentstring;
|
|
},
|
|
|
|
render_qemu_machine: function (value, arch = 'x86_64') {
|
|
let machineTextMap = {
|
|
pc: 'i440fx',
|
|
};
|
|
if (!value) {
|
|
let machine = PVE.qemu.Architecture.defaultMachines[arch];
|
|
let machineText = machineTextMap[machine] ?? machine;
|
|
return `${Proxmox.Utils.defaultText} (${machineText})`;
|
|
}
|
|
return value;
|
|
},
|
|
|
|
render_qemu_bios: function (value, arch = 'x86_64') {
|
|
if (!value) {
|
|
let defaultBios = arch === 'aarch64' ? 'OVMF (UEFI)' : 'SeaBIOS';
|
|
return `${Proxmox.Utils.defaultText} (${defaultBios})`;
|
|
} 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: (v) =>
|
|
!v ? Proxmox.Utils.defaultText : PVE.Parser.printPropertyString(v),
|
|
|
|
render_scsihw: function (value) {
|
|
if (!value || value === '__default__') {
|
|
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: {
|
|
__default__: Proxmox.Utils.defaultText,
|
|
//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: {
|
|
__default__: Proxmox.Utils.defaultText,
|
|
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',
|
|
'virtio-gl': 'VirGL GPU',
|
|
none: Proxmox.Utils.noneText,
|
|
},
|
|
|
|
render_kvm_language: function (value) {
|
|
if (!value || value === '__default__') {
|
|
return Proxmox.Utils.defaultText;
|
|
}
|
|
let text = PVE.Utils.kvm_keymaps[value];
|
|
return text ? `${text} (${value})` : value;
|
|
},
|
|
|
|
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__';
|
|
return PVE.Utils.console_map[value] || value;
|
|
},
|
|
|
|
render_kvm_vga_driver: function (value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText;
|
|
}
|
|
let vga = PVE.Parser.parsePropertyString(value, 'type');
|
|
let text = PVE.Utils.kvm_vga_drivers[vga.type];
|
|
if (!vga.type) {
|
|
text = Proxmox.Utils.defaultText;
|
|
}
|
|
return text ? `${text} (${value})` : value;
|
|
},
|
|
|
|
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');
|
|
let 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('Backup'),
|
|
vztmpl: gettext('Container template'),
|
|
iso: gettext('ISO image'),
|
|
rootdir: gettext('Container'),
|
|
snippets: gettext('Snippets'),
|
|
import: gettext('Import'),
|
|
},
|
|
|
|
// volume can be a full volume info object, in which case the format parameter is ignored, or
|
|
// you can pass the volume ID and format as separate string parameters.
|
|
volume_is_qemu_backup: function (volume, format) {
|
|
let volid, subtype;
|
|
if (typeof volume === 'string') {
|
|
volid = volume;
|
|
} else if (typeof volume === 'object') {
|
|
({ volid, format, subtype } = volume);
|
|
} else {
|
|
console.error('internal error - unexpected type', volume);
|
|
}
|
|
return format === 'pbs-vm' || volid.match(':backup/vzdump-qemu-') || subtype === 'qemu';
|
|
},
|
|
|
|
volume_is_lxc_backup: function (volume) {
|
|
return (
|
|
volume.format === 'pbs-ct' ||
|
|
volume.volid.match(':backup/vzdump-(lxc|openvz)-') ||
|
|
volume.subtype === 'lxc'
|
|
);
|
|
},
|
|
|
|
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,
|
|
tfa: true,
|
|
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: 'SMB/CIFS',
|
|
ipanel: 'CIFSInputPanel',
|
|
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,
|
|
},
|
|
esxi: {
|
|
name: 'ESXi',
|
|
ipanel: 'ESXIInputPanel',
|
|
faIcon: 'cloud-download',
|
|
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',
|
|
},
|
|
isis: {
|
|
name: 'isis',
|
|
ipanel: 'IsisInputPanel',
|
|
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';
|
|
}
|
|
|
|
let schema = PVE.Utils.storageSchema[value];
|
|
return schema?.name ?? value;
|
|
},
|
|
|
|
format_ha: function (value) {
|
|
var text = Proxmox.Utils.noneText;
|
|
|
|
if (value.managed) {
|
|
text = value.state || 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) {
|
|
let data = record.data;
|
|
let result;
|
|
if (Ext.isNumber(data.channel) && Ext.isNumber(data.id) && Ext.isNumber(data.lun)) {
|
|
result =
|
|
'CH ' +
|
|
Ext.String.leftPad(data.channel, 2, '0') +
|
|
' ID ' +
|
|
data.id +
|
|
' LUN ' +
|
|
data.lun;
|
|
} else if (data.content === 'import') {
|
|
if (data.volid.match(/^.*?:import\//)) {
|
|
// dir-based storages
|
|
result = data.volid.replace(/^.*?:import\//, '');
|
|
} else {
|
|
// esxi storage
|
|
result = data.volid.replace(/^.*?:/, '');
|
|
}
|
|
} else {
|
|
result = data.volid.replace(/^.*?:(.*?\/)?/, '');
|
|
}
|
|
return Ext.String.htmlEncode(result);
|
|
},
|
|
|
|
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;
|
|
}
|
|
|
|
let node = PVE.data.ResourceStore.getNodeById(data.node);
|
|
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 '';
|
|
}
|
|
|
|
let node = PVE.data.ResourceStore.getNodeById(record.data.node);
|
|
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;
|
|
const cpu_label = maxcpu > 1 ? 'CPUs' : 'CPU';
|
|
|
|
return `${per.toFixed(1)}% of ${maxcpu} ${cpu_label}`;
|
|
},
|
|
|
|
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');
|
|
},
|
|
|
|
// render a timestamp or pending
|
|
render_next_event: function (value) {
|
|
if (!value) {
|
|
return '-';
|
|
}
|
|
let now = new Date(),
|
|
next = new Date(value * 1000);
|
|
if (next < now) {
|
|
return gettext('pending');
|
|
}
|
|
return Proxmox.Utils.render_timestamp(value);
|
|
},
|
|
|
|
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;
|
|
}
|
|
|
|
let node = PVE.data.ResourceStore.getNodeById(data.node);
|
|
|
|
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;
|
|
}
|
|
|
|
if (data.type === 'qemu' && Ext.isNumeric(data.memhost)) {
|
|
return data.memhost / maxmem;
|
|
}
|
|
return data.mem / maxmem;
|
|
},
|
|
|
|
render_mem_usage_percent: function (value, metaData, record, rowIndex, colIndex, store) {
|
|
if (!Ext.isNumeric(value) || value === -1) {
|
|
return '';
|
|
}
|
|
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 '';
|
|
}
|
|
|
|
let node = PVE.data.ResourceStore.getNodeById(record.data.node);
|
|
var maxmem = node.data.maxmem || 0;
|
|
|
|
if (record.data.mem > 1) {
|
|
// we got no percentage but bytes
|
|
let mem = record.data.mem;
|
|
if (record.data.type === 'qemu' && Ext.isNumeric(record.data.memhost)) {
|
|
mem = record.data.memhost;
|
|
}
|
|
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 if (type === 'network') {
|
|
const networkTypeMapping = {
|
|
fabric: 'fa fa-road',
|
|
zone: 'fa fa-th',
|
|
};
|
|
|
|
return networkTypeMapping[record['network-type']] ?? '';
|
|
} else if (type === 'storage' && record.content === 'import') {
|
|
return 'fa fa-cloud-download';
|
|
} 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) {
|
|
return defaults.iconCls + ' ' + status;
|
|
}
|
|
|
|
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> ';
|
|
|
|
if (value === 'network') {
|
|
return fa + record.data['network-type'];
|
|
}
|
|
|
|
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 Ext.htmlEncode(Proxmox.Utils.format_task_description(type, id));
|
|
},
|
|
|
|
render_san: function (value) {
|
|
var names = [];
|
|
if (Ext.isArray(value)) {
|
|
value.forEach(function (val) {
|
|
if (!Ext.isNumber(val)) {
|
|
names.push(Ext.htmlEncode(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);
|
|
},
|
|
|
|
// expecting the following format:
|
|
// [v2:10.10.10.1:6802/2008,v1:10.10.10.1:6803/2008]
|
|
render_ceph_osd_addr: function (value) {
|
|
value = value.trim();
|
|
if (value.startsWith('[') && value.endsWith(']')) {
|
|
value = value.slice(1, -1); // remove []
|
|
}
|
|
value = value.replaceAll(',', '\n'); // split IPs in lines
|
|
let retVal = '';
|
|
for (const i of value.matchAll(/^(v[0-9]):(.*):([0-9]*)\/([0-9]*)$/gm)) {
|
|
retVal += `${i[1]}: ${i[2]}:${i[3]}<br>`;
|
|
}
|
|
return retVal.length < 1 ? value : retVal;
|
|
},
|
|
|
|
render_tfa: function (v, _mD, record) {
|
|
let tfa_type = PVE.Parser.parseTfaType(v);
|
|
if (tfa_type === undefined) {
|
|
return Proxmox.Utils.noText;
|
|
}
|
|
|
|
if (tfa_type !== 1) {
|
|
return tfa_type;
|
|
}
|
|
|
|
let locked_until = record.data['tfa-locked-until'];
|
|
if (locked_until !== undefined) {
|
|
let now = new Date().getTime() / 1000;
|
|
if (locked_until > now) {
|
|
return gettext('Locked');
|
|
}
|
|
}
|
|
|
|
if (record.data['totp-locked']) {
|
|
return gettext('TOTP Locked');
|
|
}
|
|
|
|
return Proxmox.Utils.yesText;
|
|
},
|
|
|
|
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.UIOptions.options.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') {
|
|
let 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, AppleWebKit 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|AppleWebKit|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 if (type === 'tag') {
|
|
menu = Ext.create('PVE.dc.TagCmdMenu', {
|
|
tag: record.data.tag,
|
|
});
|
|
} else if (record?.isRoot()) {
|
|
menu = Ext.create('PVE.dc.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,
|
|
unused: 256,
|
|
},
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
lxc_mp_counts: {
|
|
mp: 256,
|
|
unused: 256,
|
|
},
|
|
|
|
forEachLxcMP: function (func, includeUnused) {
|
|
for (let i = 0; i < PVE.Utils.lxc_mp_counts.mp; i++) {
|
|
let cont = func('mp', i, `mp${i}`);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!includeUnused) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < PVE.Utils.lxc_mp_counts.unused; i++) {
|
|
let cont = func('unused', i, `unused${i}`);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
lxc_dev_count: 256,
|
|
|
|
forEachLxcDev: function (func) {
|
|
for (let i = 0; i < PVE.Utils.lxc_dev_count; i++) {
|
|
let cont = func(i, `dev${i}`);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
hardware_counts: {
|
|
net: 32,
|
|
usb: 14,
|
|
usb_old: 5,
|
|
hostpci: 16,
|
|
audio: 1,
|
|
efidisk: 1,
|
|
serial: 4,
|
|
rng: 1,
|
|
tpmstate: 1,
|
|
virtiofs: 10,
|
|
},
|
|
|
|
// we can have usb6 and up only for specific machine/ostypes
|
|
get_max_usb_count: function (ostype, machine) {
|
|
if (!ostype) {
|
|
return PVE.Utils.hardware_counts.usb_old;
|
|
}
|
|
|
|
let match = /-(\d+).(\d+)/.exec(machine ?? '');
|
|
if (!match || PVE.Utils.qemu_min_version([match[1], match[2]], [7, 1])) {
|
|
if (ostype === 'l26') {
|
|
return PVE.Utils.hardware_counts.usb;
|
|
}
|
|
let os_match = /^win(\d+)$/.exec(ostype);
|
|
if (os_match && os_match[1] > 7) {
|
|
return PVE.Utils.hardware_counts.usb;
|
|
}
|
|
}
|
|
|
|
return PVE.Utils.hardware_counts.usb_old;
|
|
},
|
|
|
|
// parameters are expected to be arrays, e.g. [7,1], [4,0,1]
|
|
// returns true if toCheck is equal or greater than minVersion
|
|
qemu_min_version: function (toCheck, minVersion) {
|
|
let i;
|
|
for (i = 0; i < toCheck.length && i < minVersion.length; i++) {
|
|
if (toCheck[i] > minVersion[i]) {
|
|
return true;
|
|
}
|
|
if (toCheck[i] < minVersion[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (minVersion.length > toCheck.length) {
|
|
for (; i < minVersion.length; i++) {
|
|
if (minVersion[i] !== 0) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
qemu_implicit_machine_version: function (machineType, creationQemu, arch) {
|
|
let baseVersion = '5.1';
|
|
let m = creationQemu?.match(/^(\d+)\.(\d+)/);
|
|
if (m) {
|
|
let major = parseInt(m[1], 10);
|
|
let minor = parseInt(m[2], 10);
|
|
if (major > 9 || (major === 9 && minor >= 1)) {
|
|
baseVersion = `${major}.${minor}`;
|
|
}
|
|
}
|
|
|
|
let base;
|
|
if (machineType === 'q35') {
|
|
base = 'pc-q35';
|
|
} else {
|
|
let defaultMachine = PVE.qemu.Architecture.defaultMachines[arch];
|
|
base = defaultMachine === 'virt' ? 'virt' : 'pc-i440fx';
|
|
}
|
|
|
|
return `${base}-${baseVersion}`;
|
|
},
|
|
|
|
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, Ext.htmlEncode(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')) {
|
|
let isInstalled = !!msg.match(/not initialized/i);
|
|
let win = Ext.create('PVE.ceph.Install', {
|
|
nodename: nodename,
|
|
});
|
|
win.getViewModel().set('isInstalled', isInstalled);
|
|
container.add(win);
|
|
win.on('close', () => {
|
|
container.el.unmask();
|
|
});
|
|
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 || Proxmox.NodeName;
|
|
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;
|
|
},
|
|
|
|
sortByPreviousUsage: function (vmconfig, nodename) {
|
|
let controllerList = ['ide', 'virtio', 'scsi', 'sata'];
|
|
let usedControllers = {};
|
|
for (const type of Object.keys(PVE.Utils.diskControllerMaxIDs)) {
|
|
usedControllers[type] = 0;
|
|
}
|
|
|
|
for (const property of Object.keys(vmconfig)) {
|
|
if (
|
|
property.match(PVE.Utils.bus_match) &&
|
|
!vmconfig[property].match(/media=cdrom/)
|
|
) {
|
|
const foundController = property.match(PVE.Utils.bus_match)[1];
|
|
usedControllers[foundController]++;
|
|
}
|
|
}
|
|
|
|
let arch = PVE.qemu.Architecture.getGuestArchitecture(vmconfig.arch, nodename);
|
|
let sortPriority = PVE.qemu.OSDefaults.getDefaults(vmconfig.ostype, arch).busPriority;
|
|
|
|
let sortedList = Ext.clone(controllerList);
|
|
sortedList.sort(function (a, b) {
|
|
if (usedControllers[b] === usedControllers[a]) {
|
|
return sortPriority[b] - sortPriority[a];
|
|
}
|
|
return usedControllers[b] - usedControllers[a];
|
|
});
|
|
|
|
return sortedList;
|
|
},
|
|
|
|
nextFreeDisk: function (controllers, config) {
|
|
for (const controller of controllers) {
|
|
for (let i = 0; i < PVE.Utils.diskControllerMaxIDs[controller]; i++) {
|
|
let confid = controller + i.toString();
|
|
if (!Ext.isDefined(config[confid])) {
|
|
return {
|
|
controller,
|
|
id: i,
|
|
confid,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
|
|
nextFreeLxcMP: function (type, config) {
|
|
for (let i = 0; i < PVE.Utils.lxc_mp_counts[type]; i++) {
|
|
let confid = `${type}${i}`;
|
|
if (!Ext.isDefined(config[confid])) {
|
|
return {
|
|
type,
|
|
id: i,
|
|
confid,
|
|
};
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
},
|
|
|
|
escapeNotesTemplate: function (value) {
|
|
let replace = {
|
|
'\\': '\\\\',
|
|
'\n': '\\n',
|
|
};
|
|
return value.replace(/(\\|[\n])/g, (match) => replace[match]);
|
|
},
|
|
|
|
unEscapeNotesTemplate: function (value) {
|
|
let replace = {
|
|
'\\\\': '\\',
|
|
'\\n': '\n',
|
|
};
|
|
return value.replace(/(\\\\|\\n)/g, (match) => replace[match]);
|
|
},
|
|
|
|
notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],
|
|
|
|
renderTags: function (tagstext, overrides) {
|
|
let text = '';
|
|
if (tagstext) {
|
|
let tags = (tagstext.split(/[,; ]/) || []).filter((t) => !!t);
|
|
if (PVE.UIOptions.shouldSortTags()) {
|
|
tags = tags.sort((a, b) => {
|
|
let alc = a.toLowerCase();
|
|
let blc = b.toLowerCase();
|
|
return alc < blc ? -1 : blc < alc ? 1 : a.localeCompare(b);
|
|
});
|
|
}
|
|
text += ' ';
|
|
tags.forEach((tag) => {
|
|
text += Proxmox.Utils.getTagElement(tag, overrides);
|
|
});
|
|
}
|
|
return text;
|
|
},
|
|
|
|
tagCharRegex: /^[a-z0-9+_.-]+$/i,
|
|
|
|
verificationStateOrder: {
|
|
failed: 0,
|
|
none: 1,
|
|
ok: 2,
|
|
__default__: 3,
|
|
},
|
|
|
|
isStandaloneNode: function () {
|
|
return PVE.data.ResourceStore.getNodes().length < 2;
|
|
},
|
|
|
|
// main use case of this helper is the login window
|
|
getUiLanguage: function () {
|
|
let languageCookie = Ext.util.Cookies.get('PVELangCookie');
|
|
if (languageCookie === 'kr') {
|
|
// fix-up 'kr' being used for Korean by mistake FIXME: remove with PVE 9
|
|
let dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
|
|
languageCookie = 'ko';
|
|
Ext.util.Cookies.set('PVELangCookie', languageCookie, dt);
|
|
}
|
|
return languageCookie || Proxmox.defaultLang || 'en';
|
|
},
|
|
|
|
getFormattedGuestIdentifier: function (vmid, guestName) {
|
|
if (PVE.UIOptions.getTreeSortingValue('sort-field') === 'vmid') {
|
|
return guestName ? `${vmid} (${guestName})` : vmid;
|
|
} else {
|
|
return guestName ? `${guestName} (${vmid})` : vmid;
|
|
}
|
|
},
|
|
|
|
formatGuestTaskConfirmation: function (taskType, vmid, guestName) {
|
|
let description = Proxmox.Utils.format_task_description(
|
|
taskType,
|
|
this.getFormattedGuestIdentifier(vmid, guestName),
|
|
);
|
|
return Ext.htmlEncode(description);
|
|
},
|
|
},
|
|
|
|
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')],
|
|
'bulk-migrate': ['', gettext('Bulk migrate VMs and Containers')],
|
|
'bulk-start': ['', gettext('Bulk start VMs and Containers')],
|
|
'bulk-shutdown': ['', gettext('Bulk shutdown VMs and Containers')],
|
|
'bulk-suspend': ['', gettext('Bulk shutdown VMs and Containers')],
|
|
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')],
|
|
cephdestroyfs: ['CephFS', 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')],
|
|
lvmremove: ['Volume Group', gettext('Remove')],
|
|
lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
|
|
lvmthinremove: ['Thinpool', gettext('Remove')],
|
|
migrateall: ['', gettext('Bulk migrate 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')],
|
|
resize: ['VM/CT', gettext('Resize')],
|
|
reloadnetworkall: ['', gettext('Reload network configuration on all nodes')],
|
|
spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'],
|
|
spiceshell: ['', gettext('Shell') + ' (Spice)'],
|
|
startall: ['', gettext('Bulk start VMs and Containers')],
|
|
stopall: ['', gettext('Bulk shutdown VMs and Containers')],
|
|
suspendall: ['', gettext('Suspend all VMs')],
|
|
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')],
|
|
zfsremove: ['ZFS Pool', gettext('Remove')],
|
|
});
|
|
|
|
Proxmox.Utils.overrideNotificationFieldName({
|
|
'job-id': gettext('Job ID'),
|
|
});
|
|
|
|
Proxmox.Utils.overrideNotificationFieldValue({
|
|
'package-updates': gettext('Package updates are available'),
|
|
vzdump: gettext('Backup notifications'),
|
|
replication: gettext('Replication job notifications'),
|
|
fencing: gettext('Node fencing notifications'),
|
|
});
|
|
},
|
|
});
|