Backups of guests from different teams or users often end up on the
same storage, and the resource pool of the guest is a common way to
tell them apart. But the pool is not part of the backup, and the VMID
alone is not enough to find out later where a backup came from, as
VMIDs can be reused.
Add a {{pool}} variable to the notes template, which is replaced with
the resource pool the guest is a member of at the time of the backup,
or with an empty string if it is not in any pool, and list it in the
hint of the backup dialogs.
Signed-off-by: Michal Fox <me@dualfroz.com>
2277 lines
77 KiB
JavaScript
2277 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 '';
|
|
}
|
|
if (value > 1) {
|
|
// we got no percentage but bytes
|
|
let mem = value;
|
|
let 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 '';
|
|
}
|
|
|
|
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', 'pool', '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'),
|
|
});
|
|
},
|
|
});
|