commit 8cb3c7079cfb64b678b3ae26e2d8af787966a49b Author: upstream Date: Fri Sep 25 07:15:08 2026 +0200 Upstream proxmox-git/pve-manager state of the touched files before 87f04ef76a diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js new file mode 100644 index 0000000..8b99371 --- /dev/null +++ b/www/manager6/Utils.js @@ -0,0 +1,2276 @@ +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 ' + + '' + + 'www.proxmox.com 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 " "; + }, + + 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 `${Ext.htmlEncode(value)}`; + } + } 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 = ``; + + if (value === 'deleted') { + return `${icon}${Ext.htmlEncode(value)}`; + } + + let tip = gettext('Pending Changes') + ':
'; + + 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)}
`; + } + } + return `${icon}${Ext.htmlEncode(value)}`; + }, + + 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 ' ' + 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 = ``; + return `${icon} ${gettext('Encrypted')}`; + }, + + render_backup_verification: function (v, meta, record) { + let i = (cls, txt) => ` ${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 ` ${i(iconCls, txt)} `; + }, + + 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 ` ${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 = ' '; + + 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('
'); + } + 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]}
`; + } + 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'), + }); + }, +});