diff --git a/PveSensorInfoMod.pm b/PveMod_SensorInfo.pm
similarity index 99%
rename from PveSensorInfoMod.pm
rename to PveMod_SensorInfo.pm
index 7cfdb44..6ff9e82 100644
--- a/PveSensorInfoMod.pm
+++ b/PveMod_SensorInfo.pm
@@ -9,7 +9,7 @@ use Fcntl qw(:flock O_CREAT O_EXCL O_WRONLY);
use File::Path qw(remove_tree);
# debug configuration - set to 0 to disable all _debug output
-my $debug_ENABLED = 1;
+my $DEBUG_ENABLED = 1;
my $VERSION = '1.0.0';
# ============================================================================
@@ -72,7 +72,7 @@ my %collectors = (); # key: device/card name, value: PID
# debug function showing line number and call chain
# Usage: _debug(__LINE__, "message")
sub _debug {
- return unless $debug_ENABLED;
+ return unless $DEBUG_ENABLED;
my ($line, $message) = @_;
diff --git a/PveMod_pvemanagerlib.js b/PveMod_pvemanagerlib.js
new file mode 100644
index 0000000..dd6c077
--- /dev/null
+++ b/PveMod_pvemanagerlib.js
@@ -0,0 +1,958 @@
+Ext.define('PVE.node.StatusView', {
+ extend: 'Proxmox.panel.StatusView',
+ alias: 'widget.pveNodeStatus',
+
+ minHeight: 360,
+ flex: 1,
+ collapsible: true,
+ titleCollapse: true,
+ bodyPadding: '20 15 20 15',
+
+ layout: {
+ type: 'table',
+ columns: 2,
+ trAttrs: { valign: 'top' },
+ tableAttrs: {
+ style: {
+ width: '100%',
+ },
+ },
+ },
+
+ defaults: {
+ xtype: 'pmxInfoWidget',
+ padding: '0 10 2 10',
+ },
+
+ items: [
+ // ========== PRIMARY KPIs (Tier 1) ==========
+ {
+ xtype: 'box',
+ colspan: 2,
+ padding: '0',
+ html: '
Primary Metrics
',
+ },
+ {
+ itemId: 'cpu',
+ iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
+ title: gettext('CPU Usage'),
+ valueField: 'cpu',
+ maxField: 'cpuinfo',
+ renderer: function(value, record) {
+ let result = Proxmox.Utils.render_node_cpu_usage(value, record);
+ // Append CPU model if available
+ if (record && record.cpuinfo && record.cpuinfo.model) {
+ result += ` (${record.cpuinfo.model})`;
+ }
+ return result;
+ },
+ },
+ {
+ iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
+ itemId: 'memory',
+ title: gettext('Memory Usage'),
+ valueField: 'memory',
+ maxField: 'memory',
+ warningThreshold: 0.9,
+ criticalThreshold: 0.975,
+ renderer: Proxmox.Utils.render_node_size_usage,
+ },
+ {
+ itemId: 'ksm',
+ iconCls: 'fa fa-fw fa-clone',
+ printBar: false,
+ title: gettext('KSM sharing'),
+ textField: 'ksm',
+ renderer: function (record) {
+ return Proxmox.Utils.render_size(record.shared);
+ },
+ },
+ {
+ itemId: 'gpu',
+ iconCls: 'fa fa-fw fa-desktop',
+ title: gettext('GPU Usage'),
+ printBar: false,
+ textField: 'gpuStats',
+ renderer: function(gpuStats) {
+ if (!gpuStats || !gpuStats.Graphics) {
+ return '';
+ }
+
+ let hasActiveGPU = false;
+ let gpuName = '';
+
+ // Check Intel GPUs
+ if (gpuStats.Graphics.Intel) {
+ const keys = Object.keys(gpuStats.Graphics.Intel).sort();
+ if (keys.length > 0) {
+ const gpuData = gpuStats.Graphics.Intel[keys[0]];
+ hasActiveGPU = true;
+ gpuName = gpuData.name;
+ }
+ }
+
+ // Check NVIDIA GPUs
+ if (gpuStats.Graphics.NVIDIA) {
+ const keys = Object.keys(gpuStats.Graphics.NVIDIA).sort();
+ if (keys.length > 0) {
+ const stats = gpuStats.Graphics.NVIDIA[keys[0]].stats;
+ hasActiveGPU = true;
+ gpuName = stats.name;
+ }
+ }
+
+ return hasActiveGPU ? gpuName : '';
+ },
+ },
+ {
+ itemId: 'gpu_usage',
+ iconCls: 'fa fa-fw fa-desktop',
+ title: gettext('GPU 0'),
+ valueField: 'gpuStats',
+ printBar: false,
+ textField: 'gpuStats',
+ renderer: function(gpuStats) {
+ if (!gpuStats || !gpuStats.Graphics) {
+ return '';
+ }
+
+ // Check Intel GPUs
+ if (gpuStats.Graphics.Intel) {
+ const keys = Object.keys(gpuStats.Graphics.Intel).sort();
+ if (keys.length > 0) {
+ const gpuData = gpuStats.Graphics.Intel[keys[0]];
+ if (gpuData.stats.engines && gpuData.stats.engines['Render/3D']) {
+ const usage = gpuData.stats.engines['Render/3D'].busy;
+ return `${usage}%`;
+ }
+ }
+ }
+
+ // Check NVIDIA GPUs
+ if (gpuStats.Graphics.NVIDIA) {
+ const keys = Object.keys(gpuStats.Graphics.NVIDIA).sort();
+ if (keys.length > 0) {
+ const stats = gpuStats.Graphics.NVIDIA[keys[0]].stats;
+ if (stats.utilization) {
+ return `${stats.utilization.gpu}%`;
+ }
+ }
+ }
+
+ return '';
+ },
+ },
+ {
+ iconCls: 'fa fa-fw fa-hdd-o',
+ itemId: 'rootfs',
+ title: gettext('Disk (/) Usage'),
+ valueField: 'rootfs',
+ maxField: 'rootfs',
+ renderer: Proxmox.Utils.render_node_size_usage,
+ },
+ {
+ iconCls: 'fa fa-fw fa-refresh',
+ itemId: 'swap',
+ title: gettext('SWAP Usage'),
+ valueField: 'swap',
+ maxField: 'swap',
+ warningThreshold: 0.4,
+ criticalThreshold: 0.8,
+ renderer: Proxmox.Utils.render_node_size_usage,
+ },
+ // Fill the remaining cell so the next colspan:2 section header starts on a new row.
+ {
+ xtype: 'box',
+ html: '',
+ padding: 0,
+ },
+
+ // ========== SECONDARY DETAILS (Tier 2) ==========
+ {
+ xtype: 'box',
+ colspan: 2,
+ padding: '15 0 5 0',
+ html: 'Secondary Details
',
+ },
+ {
+ itemId: 'load',
+ iconCls: 'fa fa-fw fa-tasks',
+ title: gettext('CPU Load Average'),
+ printBar: false,
+ textField: 'loadavg',
+ },
+ {
+ itemId: 'wait',
+ iconCls: 'fa fa-fw fa-clock-o',
+ title: gettext('CPU I/O Delay'),
+ valueField: 'wait',
+ },
+ {
+ itemId: 'thermalCpu',
+ colspan: 2,
+ printBar: false,
+ title: gettext('CPU Thermal State'),
+ iconCls: 'fa fa-fw fa-thermometer-half',
+ textField: 'sensorsOutput',
+ renderer: function(value){
+ // sensors configuration
+ const cpuTempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS});
+ // display configuration
+ const itemsPerRow = 0;
+ // ---
+ let objValue;
+ try {
+ objValue = JSON.parse(value) || {};
+ objValue = objValue[Object.keys(objValue)[0]] || {};
+ } catch(e) {
+ objValue = {};
+ }
+
+ const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort();
+ const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort();
+ const bINTEL = cpuKeysI.length > 0 ? true : false;
+ const INTELPackagePrefix = 'Core' == 'Core' ? 'Core ' : 'Package id';
+ const INTELPackageCaption = 'Core' == 'Core' ? 'Core' : 'Package';
+ let AMDPackagePrefix = 'Tccd';
+ let AMDPackageCaption = 'CCD';
+
+ if (cpuKeysA.length > 0) {
+ let bTccd = false;
+ let bTctl = false;
+ let bTdie = false;
+ let bCpuCoreTemp = false;
+ cpuKeysA.forEach((cpuKey, cpuIndex) => {
+ let items = objValue[cpuKey];
+ bTccd = Object.keys(items).findIndex(item => { return String(item).startsWith('Tccd'); }) >= 0;
+ bTctl = Object.keys(items).findIndex(item => { return String(item).startsWith('Tctl'); }) >= 0;
+ bTdie = Object.keys(items).findIndex(item => { return String(item).startsWith('Tdie'); }) >= 0;
+ bCpuCoreTemp = Object.keys(items).findIndex(item => { return String(item) === 'CPU Core Temp'; }) >= 0;
+ });
+ if (bTccd && 'Core' == 'Core') {
+ AMDPackagePrefix = 'Tccd';
+ AMDPackageCaption = 'ccd';
+ } else if (bCpuCoreTemp && 'Core' == 'Package') {
+ AMDPackagePrefix = 'CPU Core Temp';
+ AMDPackageCaption = 'CPU Core Temp';
+ } else if (bTdie) {
+ AMDPackagePrefix = 'Tdie';
+ AMDPackageCaption = 'die';
+ } else if (bTctl) {
+ AMDPackagePrefix = 'Tctl';
+ AMDPackageCaption = 'ctl';
+ } else {
+ AMDPackagePrefix = 'temp';
+ AMDPackageCaption = 'Temp';
+ }
+ }
+
+ const cpuKeys = bINTEL ? cpuKeysI : cpuKeysA;
+ const cpuItemPrefix = bINTEL ? INTELPackagePrefix : AMDPackagePrefix;
+ const cpuTempCaption = bINTEL ? INTELPackageCaption : AMDPackageCaption;
+ const formatTemp = bINTEL ? '0' : '0.0';
+ const cpuCount = cpuKeys.length;
+ let temps = [];
+
+ cpuKeys.forEach((cpuKey, cpuIndex) => {
+ let cpuTemps = [];
+ const items = objValue[cpuKey];
+ const cpuModel = items.cpu_model || '';
+
+ const itemKeys = Object.keys(items).filter(item => {
+ if ('Core' == 'Core') {
+ // In Core mode: only show individual cores/CCDs, exclude overall CPU temp
+ return String(item).includes(cpuItemPrefix) || String(item).startsWith('Tccd');
+ } else {
+ // In Package mode: show overall CPU temp and package-level readings
+ return String(item).includes(cpuItemPrefix) || String(item) === 'CPU Core Temp';
+ }
+ }).sort((a, b) => {
+ // Sort cores numerically
+ let numA = parseInt(a.match(/\d+/)?.[0] || '0', 10);
+ let numB = parseInt(b.match(/\d+/)?.[0] || '0', 10);
+ return numA - numB;
+ });
+
+ itemKeys.forEach((coreKey) => {
+ try {
+ let tempVal = NaN, tempMax = NaN, tempCrit = NaN;
+ Object.keys(items[coreKey]).forEach((secondLevelKey) => {
+ if (secondLevelKey.endsWith('_input')) {
+ tempVal = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_max')) {
+ tempMax = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_crit')) {
+ tempCrit = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
+ }
+ });
+
+ if (!isNaN(tempVal)) {
+ let tempStyle = '';
+ if (!isNaN(tempMax) && tempVal >= tempMax) {
+ tempStyle = 'color: #FFC300; font-weight: bold;';
+ }
+ if (!isNaN(tempCrit) && tempVal >= tempCrit) {
+ tempStyle = 'color: red; font-weight: bold;';
+ }
+
+ let tempStr = '';
+
+ // Enhanced parsing for AMD temperatures
+ if (coreKey.startsWith('Tccd')) {
+ let tempIndex = coreKey.match(/Tccd(\d+)/);
+ if (tempIndex !== null && tempIndex.length > 1) {
+ tempIndex = tempIndex[1];
+ tempStr = `${cpuTempCaption} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
+ } else {
+ tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
+ }
+ }
+ // Handle CPU Core Temp (single overall temperature)
+ else if (coreKey === 'CPU Core Temp') {
+ tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
+ }
+ // Enhanced parsing for Intel cores (P-Core, E-Core, regular Core)
+ else {
+ let tempIndex = coreKey.match(/(?:P\s+Core|E\s+Core|Core)\s*(\d+)/);
+ if (tempIndex !== null && tempIndex.length > 1) {
+ tempIndex = tempIndex[1];
+ let coreType = coreKey.startsWith('P Core') ? 'P Core' :
+ coreKey.startsWith('E Core') ? 'E Core' :
+ cpuTempCaption;
+ tempStr = `${coreType} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
+ } else {
+ // fallback for CPUs which do not have a core index
+ let coreType = coreKey.startsWith('P Core') ? 'P Core' :
+ coreKey.startsWith('E Core') ? 'E Core' :
+ cpuTempCaption;
+ tempStr = `${coreType}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
+ }
+ }
+
+ cpuTemps.push(tempStr);
+ }
+ } catch (e) { /*_*/ }
+ });
+
+ if(cpuTemps.length > 0) {
+ temps.push({ model: cpuModel, temps: cpuTemps });
+ }
+ });
+
+ let html = '';
+ temps.forEach((cpuData, cpuIndex) => {
+ const strCoreTemps = cpuData.temps.map((strTemp, index, arr) => {
+ return strTemp + (index + 1 < arr.length ? (itemsPerRow > 0 && (index + 1) % itemsPerRow === 0 ? ' ' : ' | ') : '');
+ });
+ if(strCoreTemps.length > 0) {
+ let cpuLabel = cpuCount > 1 ? `Socket ${cpuIndex + 1}` : 'Socket 1';
+ let cpuModelStr = cpuData.model || 'Unknown CPU';
+
+ html += '';
+ html += `${cpuModelStr} `;
+ html += `${strCoreTemps.join('')} `;
+ html += ' ';
+ }
+ });
+ html += '
';
+
+ return html.indexOf('') > 0
+ ? '' + html + '
'
+ : 'N/A';
+ }
+ },
+ {
+ itemId: 'gpu_details',
+ colspan: 2,
+ iconCls: 'fa fa-fw fa-desktop',
+ title: gettext('GPU Details'),
+ printBar: false,
+ textField: 'gpuStats',
+ renderer: function(gpuStats) {
+ if (!gpuStats || !gpuStats.Graphics) {
+ return '';
+ }
+
+ let html = '';
+
+ // Intel GPUs - Secondary details
+ if (gpuStats.Graphics.Intel) {
+ Object.keys(gpuStats.Graphics.Intel).sort().forEach(key => {
+ const gpuData = gpuStats.Graphics.Intel[key];
+
+ let details = [];
+
+ // All engine details
+ if (gpuData.stats.engines) {
+ if (gpuData.stats.engines['Render/3D']) {
+ details.push(`Render/3D: ${gpuData.stats.engines['Render/3D'].busy}%`);
+ }
+ if (gpuData.stats.engines['Video']) {
+ details.push(`Video: ${gpuData.stats.engines['Video'].busy}%`);
+ }
+ if (gpuData.stats.engines['Blitter']) {
+ details.push(`Blitter: ${gpuData.stats.engines['Blitter'].busy}%`);
+ }
+ if (gpuData.stats.engines['VideoEnhance']) {
+ details.push(`VideoEnhance: ${gpuData.stats.engines['VideoEnhance'].busy}%`);
+ }
+ }
+
+ // Power
+ if (gpuData.stats.power) {
+ details.push(`Power: ${gpuData.stats.power?.GPU ?? 'N/A'} / ${gpuData.stats.power?.Package ?? 'N/A'} ${gpuData.stats.power?.unit || 'W'}`);
+ }
+
+ // Frequency
+ if (gpuData.stats.frequency) {
+ details.push(`Freq: ${gpuData.stats.frequency?.actual ?? 'N/A'}/${gpuData.stats.frequency?.requested ?? 'N/A'} ${gpuData.stats.frequency?.unit || 'MHz'}`);
+ }
+
+ html += '';
+ html += `${gpuData.name} `;
+ html += `${details.join(' | ')} `;
+ html += ' ';
+ });
+ }
+
+ // NVIDIA GPUs - Secondary details
+ if (gpuStats.Graphics.NVIDIA) {
+ Object.keys(gpuStats.Graphics.NVIDIA).sort().forEach(key => {
+ const gpuData = gpuStats.Graphics.NVIDIA[key];
+ const stats = gpuData.stats;
+
+ let details = [];
+
+ // Memory Utilization
+ if (stats.utilization && stats.utilization.memory) {
+ const memUsage = parseInt(stats.utilization.memory);
+ let memStyle = '';
+ if (memUsage >= 90) memStyle = 'color: #d9534f; font-weight: bold;';
+ else if (memUsage >= 70) memStyle = 'color: #f0ad4e; font-weight: bold;';
+ details.push(`MEM: ${stats.utilization.memory}% `);
+ }
+
+ // VRAM Usage
+ if (stats.memory) {
+ const vramUsedGB = parseInt(stats.memory.used);
+ const vramTotalGB = parseInt(stats.memory.total);
+ const vramPercent = (vramUsedGB / vramTotalGB) * 100;
+ let vramStyle = '';
+ if (vramPercent >= 90) vramStyle = 'color: #d9534f; font-weight: bold;';
+ else if (vramPercent >= 70) vramStyle = 'color: #f0ad4e; font-weight: bold;';
+ details.push(`VRAM: ${stats.memory.used}/${stats.memory.total} ${stats.memory.unit} `);
+ }
+
+ // Temperature
+ if (stats.temperature) {
+ let tempStyle = '';
+ if (stats.temperature.gpu >= 80) {
+ tempStyle = 'color: red; font-weight: bold;';
+ } else if (stats.temperature.gpu >= 70) {
+ tempStyle = 'color: #FFC300; font-weight: bold;';
+ }
+ details.push(`Temp: ${stats.temperature.gpu}${stats.temperature.unit} `);
+ }
+
+ // Power
+ if (stats.power) {
+ details.push(`Power: ${stats.power.draw}/${stats.power.limit} ${stats.power.unit}`);
+ }
+
+ html += '';
+ html += `${stats.name} `;
+ html += `${details.join(' | ')} `;
+ html += ' ';
+ });
+ }
+
+ html += '
';
+ return html.indexOf(' ') > 0
+ ? '' + html + '
'
+ : '';
+ },
+ },
+ {
+ itemId: 'thermalNvme',
+ colspan: 2,
+ printBar: false,
+ title: gettext('NVMe Temperatures'),
+ iconCls: 'fa fa-fw fa-thermometer-half',
+ textField: 'sensorsOutput',
+ renderer: function(value) {
+ // sensors configuration
+ const addressPrefix = "nvme-pci-";
+ const sensorName = "Composite";
+ const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS});
+ // display configuration
+ const itemsPerRow = 0;
+ // ---
+ let objValue;
+ try {
+ objValue = JSON.parse(value) || {};
+ objValue = objValue[Object.keys(objValue)[0]] || {};
+ } catch(e) {
+ objValue = {};
+ }
+ const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();
+ let nvmeData = [];
+ nvmeKeys.forEach((nvmeKey, index) => {
+ try {
+ let tempVal = NaN, tempMax = NaN, tempCrit = NaN, model = '', serial = '';
+ Object.keys(objValue[nvmeKey][sensorName]).forEach((secondLevelKey) => {
+ if (secondLevelKey.endsWith('_input')) {
+ tempVal = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_max')) {
+ tempMax = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
+ } else if (secondLevelKey.endsWith('_crit')) {
+ tempCrit = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
+ }
+ });
+ model = objValue[nvmeKey]['model'] || 'Unknown';
+ serial = objValue[nvmeKey]['serial'] || '';
+
+ if (!isNaN(tempVal)) {
+ let tempStyle = '';
+ if (!isNaN(tempMax) && tempVal >= tempMax) {
+ tempStyle = 'color: #FFC300; font-weight: bold;';
+ }
+ if (!isNaN(tempCrit) && tempVal >= tempCrit) {
+ tempStyle = 'color: red; font-weight: bold;';
+ }
+ nvmeData.push({
+ model: model,
+ serial: serial,
+ temp: tempVal,
+ tempStyle: tempStyle,
+ unit: tempHelper.getUnit()
+ });
+ }
+ } catch(e) { /*_*/ }
+ });
+
+ if (nvmeData.length === 0) {
+ return 'N/A';
+ }
+
+ let html = '';
+ nvmeData.forEach((data) => {
+ let deviceName = data.model;
+ if (data.serial) {
+ deviceName += ` (${data.serial})`;
+ }
+ html += '';
+ html += `${deviceName} `;
+ html += `${Ext.util.Format.number(data.temp, '0.0')}${data.unit} `;
+ html += ' ';
+ });
+ html += '
';
+ return '' + html + '
';
+ }
+ },
+
+ // ========== TERTIARY DIAGNOSTICS (Tier 3) ==========
+ {
+ xtype: 'box',
+ colspan: 2,
+ padding: '15 0 5 0',
+ html: 'Diagnostics
',
+ },
+ {
+ itemId: 'speedFan',
+ colspan: 2,
+ printBar: false,
+ title: gettext('System Fans'),
+ iconCls: 'fa fa-fw fa-snowflake-o',
+ textField: 'sensorsOutput',
+ renderer: function(value) {
+ // ---
+ let objValue;
+ try {
+ objValue = JSON.parse(value) || {};
+ objValue = objValue[Object.keys(objValue)[0]] || {};
+ } catch(e) {
+ objValue = {};
+ }
+
+ // Recursive function to find fan keys and values
+ function findFanKeys(obj, fanKeys, parentKey = null) {
+ Object.keys(obj).forEach(key => {
+ const value = obj[key];
+ if (typeof value === 'object' && value !== null) {
+ // If the value is an object, recursively call the function
+ findFanKeys(value, fanKeys, key);
+ } else if (/^fan[0-9]+(_input)?$/.test(key)) {
+ if (true != true && value === 0) {
+ // Skip this fan if DISPLAY_ZERO_SPEED_FANS is false and value is 0
+ return;
+ }
+ // If the key matches the pattern, add the parent key and value to the fanKeys array
+ fanKeys.push({ key: parentKey, value: value });
+ }
+ });
+ }
+
+ let speeds = [];
+ // Loop through the parent keys
+ Object.keys(objValue).forEach(parentKey => {
+ const parentObj = objValue[parentKey];
+ // Array to store fan keys and values
+ const fanKeys = [];
+ // Call the recursive function to find fan keys and values
+ findFanKeys(parentObj, fanKeys);
+ // Sort the fan keys
+ fanKeys.sort((a, b) => {
+ if (a.key < b.key) return -1;
+ if (a.key > b.key) return 1;
+ return 0;
+ });
+ // Process each fan key and value
+ fanKeys.forEach(({ key: fanKey, value: fanSpeed }) => {
+ try {
+ const fan = fanKey.charAt(0).toUpperCase() + fanKey.slice(1); // Capitalize the first letter of fanKey
+ speeds.push(`${fan}: ${fanSpeed} RPM`);
+ } catch(e) {
+ console.error(`Error retrieving fan speed for ${fanKey} in ${parentKey}:`, e); // Debug: Log specific error
+ }
+ });
+ });
+ return '' + (speeds.length > 0 ? speeds.join(' | ') : 'N/A') + '
';
+ }
+ },
+ {
+ itemId: 'gpuFans',
+ colspan: 2,
+ printBar: false,
+ title: gettext('GPU Fans'),
+ iconCls: 'fa fa-fw fa-snowflake-o',
+ textField: 'gpuStats',
+ renderer: function(gpuStats) {
+ if (!gpuStats || !gpuStats.Graphics || !gpuStats.Graphics.NVIDIA) {
+ return 'N/A';
+ }
+
+ let rows = [];
+
+ Object.keys(gpuStats.Graphics.NVIDIA).sort().forEach(key => {
+ const gpuData = gpuStats.Graphics.NVIDIA[key];
+ const stats = gpuData?.stats;
+ const fan = stats?.fan;
+
+ if (!fan || fan.speed === undefined || fan.speed === null) {
+ return;
+ }
+
+ const gpuName = stats?.name || key;
+ const unit = fan.unit || '%';
+ rows.push(
+ ' ' +
+ `${gpuName} ` +
+ `Fan: ${fan.speed}${unit} ` +
+ ' ',
+ );
+ });
+
+ if (rows.length === 0) {
+ return 'N/A';
+ }
+
+ return '';
+ },
+ },
+ {
+ itemId: 'upsc',
+ colspan: 2,
+ printBar: false,
+ title: gettext('UPS Status'),
+ iconCls: 'fa fa-fw fa-battery-three-quarters',
+ textField: 'upsStats',
+ renderer: function(value) {
+ let objValue = {};
+ try {
+ // Parse the UPS data
+ if (typeof value === 'string') {
+ objValue = JSON.parse(value) || {};
+ } else if (typeof value === 'object') {
+ objValue = value || {};
+ }
+ } catch(e) {
+ objValue = {};
+ }
+
+ // If objValue is null or empty, return N/A
+ if (!objValue || Object.keys(objValue).length === 0) {
+ return 'N/A';
+ }
+
+ // Helper function to get status color
+ function getStatusColor(status) {
+ if (!status) return '#999';
+ const statusUpper = status.toUpperCase();
+ if (statusUpper.includes('OL')) return null;
+ if (statusUpper.includes('OB')) return '#d9534f';
+ if (statusUpper.includes('LB')) return '#d9534f';
+ return '#f0ad4e';
+ }
+
+ // Helper function to get load/charge color
+ function getPercentageColor(value, isLoad = false) {
+ if (!value || isNaN(value)) return '#999';
+ const num = parseFloat(value);
+ if (isLoad) {
+ if (num >= 80) return '#d9534f';
+ if (num >= 60) return '#f0ad4e';
+ return null;
+ } else {
+ if (num <= 20) return '#d9534f';
+ if (num <= 50) return '#f0ad4e';
+ return null;
+ }
+ }
+
+ // Helper function to format runtime
+ function formatRuntime(seconds) {
+ if (!seconds || isNaN(seconds)) return 'N/A';
+ const mins = Math.floor(seconds / 60);
+ const secs = seconds % 60;
+ return `${mins}m ${secs}s`;
+ }
+
+ // Process each UPS in the data
+ let allDisplayItems = [];
+
+ Object.keys(objValue).forEach(upsKey => {
+ const upsData = objValue[upsKey];
+
+ // Extract key UPS information
+ const batteryCharge = upsData['battery.charge'];
+ const batteryRuntime = upsData['battery.runtime'];
+ const inputVoltage = upsData['input.voltage'];
+ const upsLoad = upsData['ups.load'];
+ const upsStatus = upsData['ups.status'];
+ const upsModel = upsData['ups.model'] || upsData['device.model'];
+ const testResult = upsData['ups.test.result'];
+ const batteryChargeLow = upsData['battery.charge.low'];
+ const batteryRuntimeLow = upsData['battery.runtime.low'];
+ const upsRealPowerNominal = upsData['ups.realpower.nominal'];
+ const batteryMfrDate = upsData['battery.mfr.date'];
+
+ // Main status line with all metrics
+ let statusLine = '';
+
+ // Status
+ if (upsStatus) {
+ const statusUpper = upsStatus.toUpperCase();
+ let statusText = 'Unknown';
+ let statusColor = '#f0ad4e';
+
+ if (statusUpper.includes('OL')) {
+ statusText = 'Online';
+ statusColor = null;
+ } else if (statusUpper.includes('OB')) {
+ statusText = 'On Battery';
+ statusColor = '#d9534f';
+ } else if (statusUpper.includes('LB')) {
+ statusText = 'Low Battery';
+ statusColor = '#d9534f';
+ } else {
+ statusText = upsStatus;
+ statusColor = '#f0ad4e';
+ }
+
+ let statusStyle = statusColor ? ('color: ' + statusColor + ';') : '';
+ statusLine += 'Status: ' + statusText + ' ';
+ } else {
+ statusLine += 'Status: N/A ';
+ }
+
+ // Battery charge
+ if (statusLine) statusLine += ' | ';
+ if (batteryCharge) {
+ const chargeColor = getPercentageColor(batteryCharge, false);
+ let chargeStyle = chargeColor ? ('color: ' + chargeColor + ';') : '';
+ statusLine += 'Battery: ' + batteryCharge + '% ';
+ } else {
+ statusLine += 'Battery: N/A ';
+ }
+
+ // Load percentage
+ if (statusLine) statusLine += ' | ';
+ if (upsLoad) {
+ const loadColor = getPercentageColor(upsLoad, true);
+ let loadStyle = loadColor ? ('color: ' + loadColor + ';') : '';
+ statusLine += 'Load: ' + upsLoad + '% ';
+ } else {
+ statusLine += 'Load: N/A ';
+ }
+
+ // Runtime
+ if (statusLine) statusLine += ' | ';
+ if (batteryRuntime) {
+ const runtime = parseInt(batteryRuntime);
+ const runtimeLowThreshold = batteryRuntimeLow ? parseInt(batteryRuntimeLow) : 600;
+ let runtimeColor = null;
+ if (runtime <= runtimeLowThreshold / 2) runtimeColor = '#d9534f';
+ else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e';
+ let runtimeStyle = runtimeColor ? ('color: ' + runtimeColor + ';') : '';
+ statusLine += 'Runtime: ' + formatRuntime(runtime) + ' ';
+ } else {
+ statusLine += 'Runtime: N/A ';
+ }
+
+ // Input voltage
+ if (statusLine) statusLine += ' | ';
+ if (inputVoltage) {
+ statusLine += 'Input: ' + parseFloat(inputVoltage).toFixed(0) + 'V ';
+ } else {
+ statusLine += 'Input: N/A ';
+ }
+
+ // Calculate actual watt usage
+ if (statusLine) statusLine += ' | ';
+ let actualWattage = null;
+ if (upsLoad && upsRealPowerNominal) {
+ const load = parseFloat(upsLoad);
+ const nominal = parseFloat(upsRealPowerNominal);
+ if (!isNaN(load) && !isNaN(nominal)) {
+ actualWattage = Math.round((load / 100) * nominal);
+ }
+ }
+
+ // Real power (calculated watt usage)
+ if (actualWattage !== null) {
+ statusLine += 'Output: ' + actualWattage + 'W ';
+ } else {
+ statusLine += 'Output: N/A ';
+ }
+
+ // Append battery MFD + last test to the same line (single-line UPS summary)
+ statusLine += ' | Battery MFD: ' + (batteryMfrDate || 'N/A');
+ if (testResult && !testResult.toLowerCase().includes('no test')) {
+ const testColor = testResult.toLowerCase().includes('passed') ? null : '#d9534f';
+ let testStyle = testColor ? ('color: ' + testColor + ';') : '';
+ statusLine += ' | Test: ' + testResult + ' ';
+ } else {
+ statusLine += ' | Test: N/A';
+ }
+
+ // Build UPS display with model on left, details on right
+ let upsHtml = '';
+ upsHtml += '' + (upsModel || upsKey) + ' ';
+ upsHtml += '' + statusLine + ' ';
+ upsHtml += ' ';
+
+ allDisplayItems.push(upsHtml);
+ });
+
+ // Format the final output for all UPS devices
+ return '' + allDisplayItems.join('') + '
';
+ }
+ },
+ {
+ xtype: 'box',
+ colspan: 2,
+ padding: '15 0 5 0',
+ html: 'System
',
+ },
+ {
+ colspan: 2,
+ title: gettext('Kernel Version'),
+ printBar: false,
+ // TODO: remove with next major and only use newish current-kernel textfield
+ multiField: true,
+ //textField: 'current-kernel',
+ renderer: ({ data }) => {
+ if (!data['current-kernel']) {
+ return data.kversion;
+ }
+ let kernel = data['current-kernel'];
+ let buildDate = kernel.version.match(/\((.+)\)\s*$/)?.[1] ?? 'unknown';
+ return `${kernel.sysname} ${kernel.release} (${buildDate})`;
+ },
+ value: '',
+ },
+ {
+ colspan: 2,
+ title: gettext('Boot Mode'),
+ printBar: false,
+ textField: 'boot-info',
+ renderer: (boot) => {
+ if (boot.mode === 'legacy-bios') {
+ return 'Legacy BIOS';
+ } else if (boot.mode === 'efi') {
+ return `EFI${boot.secureboot ? ' (Secure Boot)' : ''}`;
+ }
+ return Proxmox.Utils.unknownText;
+ },
+ value: '',
+ },
+ {
+ itemId: 'version',
+ colspan: 2,
+ printBar: false,
+ title: gettext('Manager Version'),
+ textField: 'pveversion',
+ value: '',
+ },
+ {
+ itemId: 'pve_mod_version',
+ colspan: 2,
+ printBar: false,
+ title: gettext('Sensor Mod Version'),
+ textField: 'pveModVersion',
+ value: '',
+ },
+ ],
+
+ updateTitle: function () {
+ var me = this;
+ var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime'));
+ me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')');
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ let stateProvider = Ext.state.Manager.getProvider();
+ let repoLink = stateProvider.encodeHToken({
+ view: 'server',
+ rid: `node/${me.pveSelNode.data.node}`,
+ ltab: 'tasks',
+ nodetab: 'aptrepositories',
+ });
+
+ me.items.push({
+ xtype: 'pmxNodeInfoRepoStatus',
+ itemId: 'repositoryStatus',
+ product: 'Proxmox VE',
+ repoLink: `#${repoLink}`,
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.node.SubscriptionKeyEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ title: gettext('Upload Subscription Key'),
+ width: 350,
+
+ items: {
+ xtype: 'textfield',
+ name: 'key',
+ value: '',
+ fieldLabel: gettext('Subscription Key'),
+ labelWidth: 120,
+ getSubmitValue: function () {
+ return this.processRawValue(this.getRawValue())?.trim();
+ },
+ },
+
+ initComponent: function () {
+ var me = this;
+
+ me.callParent();
+
+ me.load();
+ },
+});
\ No newline at end of file
diff --git a/pve-mod-gui-sensors.sh b/pve-mod-gui-sensors.sh
index 04ce4a7..c28345e 100644
--- a/pve-mod-gui-sensors.sh
+++ b/pve-mod-gui-sensors.sh
@@ -39,7 +39,7 @@ JSON_EXPORT_FILENAME="sensorsdata.json"
# File paths
PVE_MANAGER_LIB_JS_FILE="/usr/share/pve-manager/js/pvemanagerlib.js"
NODES_PM_FILE="/usr/share/perl5/PVE/API2/Nodes.pm"
-PVE_SENSOR_INFO_MOD_FILE="/usr/share/perl5/PVE/API2/PveSensorInfoMod.pm"
+PVE_SENSOR_INFO_MOD_FILE="/usr/share/perl5/PVE/API2/PveMod_SensorInfo.pm"
#region message tools
# Section header (bold)
@@ -480,6 +480,8 @@ function install_mod {
install_sensor_monitor_module
insert_sensor_monitor_into_pve
+ exit
+
#### Temperature helper parameters ####
msgb "\n=== Creating temperature conversion helper ==="
HELPERCTORPARAMS=$([[ "$TEMP_UNIT" = "F" ]] && \
diff --git a/pvemanagerlib.js b/pvemanagerlib.js
deleted file mode 100644
index 2efa46c..0000000
--- a/pvemanagerlib.js
+++ /dev/null
@@ -1,68907 +0,0 @@
-const pveOnlineHelpInfo = {
- "ceph_rados_block_devices" : {
- "link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices",
- "title" : "Ceph RADOS Block Devices (RBD)"
- },
- "chapter_ha_manager" : {
- "link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager",
- "title" : "High Availability"
- },
- "chapter_lvm" : {
- "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm",
- "title" : "Logical Volume Manager (LVM)"
- },
- "chapter_notifications" : {
- "link" : "/pve-docs/chapter-notifications.html#chapter_notifications",
- "title" : "Notifications"
- },
- "chapter_pct" : {
- "link" : "/pve-docs/chapter-pct.html#chapter_pct",
- "title" : "Proxmox Container Toolkit"
- },
- "chapter_pve_firewall" : {
- "link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall",
- "title" : "Proxmox VE Firewall"
- },
- "chapter_pveceph" : {
- "link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph",
- "title" : "Deploy Hyper-Converged Ceph Cluster"
- },
- "chapter_pvecm" : {
- "link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm",
- "title" : "Cluster Manager"
- },
- "chapter_pvesdn" : {
- "link" : "/pve-docs/chapter-pvesdn.html#chapter_pvesdn",
- "title" : "Software-Defined Network"
- },
- "chapter_pvesr" : {
- "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr",
- "title" : "Storage Replication"
- },
- "chapter_storage" : {
- "link" : "/pve-docs/chapter-pvesm.html#chapter_storage",
- "title" : "Proxmox VE Storage"
- },
- "chapter_system_administration" : {
- "link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration",
- "title" : "Host System Administration"
- },
- "chapter_user_management" : {
- "link" : "/pve-docs/chapter-pveum.html#chapter_user_management",
- "title" : "User Management"
- },
- "chapter_virtual_machines" : {
- "link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "chapter_vzdump" : {
- "link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump",
- "title" : "Backup and Restore"
- },
- "chapter_zfs" : {
- "link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs",
- "title" : "ZFS on Linux"
- },
- "datacenter_configuration_file" : {
- "link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file",
- "title" : "Datacenter Configuration"
- },
- "external_metric_server" : {
- "link" : "/pve-docs/chapter-sysadmin.html#external_metric_server",
- "title" : "External Metric Server"
- },
- "getting_help" : {
- "link" : "/pve-docs/chapter-pve-intro.html#getting_help",
- "title" : "Getting Help"
- },
- "gui_consent_banner" : {
- "link" : "/pve-docs/chapter-pve-gui.html#gui_consent_banner",
- "subtitle" : "Consent Banner",
- "title" : "Graphical User Interface"
- },
- "gui_my_settings" : {
- "link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings",
- "subtitle" : "My Settings",
- "title" : "Graphical User Interface"
- },
- "gui_tags" : {
- "link" : "/pve-docs/chapter-pve-gui.html#gui_tags",
- "subtitle" : "Tags",
- "title" : "Graphical User Interface"
- },
- "ha_manager_crs" : {
- "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_crs",
- "subtitle" : "Cluster Resource Scheduling",
- "title" : "High Availability"
- },
- "ha_manager_fencing" : {
- "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing",
- "subtitle" : "Fencing",
- "title" : "High Availability"
- },
- "ha_manager_resource_config" : {
- "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resource_config",
- "subtitle" : "Resources",
- "title" : "High Availability"
- },
- "ha_manager_resources" : {
- "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resources",
- "subtitle" : "Resources",
- "title" : "High Availability"
- },
- "ha_manager_rules" : {
- "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_rules",
- "subtitle" : "Rules",
- "title" : "High Availability"
- },
- "ha_manager_shutdown_policy" : {
- "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_shutdown_policy",
- "subtitle" : "Shutdown Policy",
- "title" : "High Availability"
- },
- "markdown_basics" : {
- "link" : "/pve-docs/pve-admin-guide.html#markdown_basics",
- "title" : "Markdown Primer"
- },
- "metric_server_graphite" : {
- "link" : "/pve-docs/chapter-sysadmin.html#metric_server_graphite",
- "subtitle" : "Graphite server configuration",
- "title" : "External Metric Server"
- },
- "metric_server_influxdb" : {
- "link" : "/pve-docs/chapter-sysadmin.html#metric_server_influxdb",
- "subtitle" : "Influxdb plugin configuration",
- "title" : "External Metric Server"
- },
- "notification_matchers" : {
- "link" : "/pve-docs/chapter-notifications.html#notification_matchers",
- "subtitle" : "Notification Matchers",
- "title" : "Notifications"
- },
- "notification_targets_gotify" : {
- "link" : "/pve-docs/chapter-notifications.html#notification_targets_gotify",
- "subtitle" : "Gotify",
- "title" : "Notifications"
- },
- "notification_targets_sendmail" : {
- "link" : "/pve-docs/chapter-notifications.html#notification_targets_sendmail",
- "subtitle" : "Sendmail",
- "title" : "Notifications"
- },
- "notification_targets_smtp" : {
- "link" : "/pve-docs/chapter-notifications.html#notification_targets_smtp",
- "subtitle" : "SMTP",
- "title" : "Notifications"
- },
- "notification_targets_webhook" : {
- "link" : "/pve-docs/chapter-notifications.html#notification_targets_webhook",
- "subtitle" : "Webhook",
- "title" : "Notifications"
- },
- "pct_configuration" : {
- "link" : "/pve-docs/chapter-pct.html#pct_configuration",
- "subtitle" : "Configuration",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_container_images" : {
- "link" : "/pve-docs/chapter-pct.html#pct_container_images",
- "subtitle" : "Container Images",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_container_network" : {
- "link" : "/pve-docs/chapter-pct.html#pct_container_network",
- "subtitle" : "Network",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_container_storage" : {
- "link" : "/pve-docs/chapter-pct.html#pct_container_storage",
- "subtitle" : "Container Storage",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_cpu" : {
- "link" : "/pve-docs/chapter-pct.html#pct_cpu",
- "subtitle" : "CPU",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_general" : {
- "link" : "/pve-docs/chapter-pct.html#pct_general",
- "subtitle" : "General Settings",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_memory" : {
- "link" : "/pve-docs/chapter-pct.html#pct_memory",
- "subtitle" : "Memory",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_migration" : {
- "link" : "/pve-docs/chapter-pct.html#pct_migration",
- "subtitle" : "Migration",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_options" : {
- "link" : "/pve-docs/chapter-pct.html#pct_options",
- "subtitle" : "Options",
- "title" : "Proxmox Container Toolkit"
- },
- "pct_startup_and_shutdown" : {
- "link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown",
- "subtitle" : "Automatic Start and Shutdown of Containers",
- "title" : "Proxmox Container Toolkit"
- },
- "proxmox_node_management" : {
- "link" : "/pve-docs/chapter-sysadmin.html#proxmox_node_management",
- "title" : "Proxmox Node Management"
- },
- "pve_admin_guide" : {
- "link" : "/pve-docs/pve-admin-guide.html",
- "title" : "Proxmox VE Administration Guide"
- },
- "pve_ceph_install" : {
- "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install",
- "subtitle" : "CLI Installation of Ceph Packages",
- "title" : "Deploy Hyper-Converged Ceph Cluster"
- },
- "pve_ceph_osds" : {
- "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds",
- "subtitle" : "Ceph OSDs",
- "title" : "Deploy Hyper-Converged Ceph Cluster"
- },
- "pve_ceph_pools" : {
- "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools",
- "subtitle" : "Ceph Pools",
- "title" : "Deploy Hyper-Converged Ceph Cluster"
- },
- "pve_documentation_index" : {
- "link" : "/pve-docs/index.html",
- "title" : "Proxmox VE Documentation Index"
- },
- "pve_firewall_cluster_wide_setup" : {
- "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup",
- "subtitle" : "Cluster Wide Setup",
- "title" : "Proxmox VE Firewall"
- },
- "pve_firewall_host_specific_configuration" : {
- "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration",
- "subtitle" : "Host Specific Configuration",
- "title" : "Proxmox VE Firewall"
- },
- "pve_firewall_ip_aliases" : {
- "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases",
- "subtitle" : "IP Aliases",
- "title" : "Proxmox VE Firewall"
- },
- "pve_firewall_ip_sets" : {
- "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets",
- "subtitle" : "IP Sets",
- "title" : "Proxmox VE Firewall"
- },
- "pve_firewall_security_groups" : {
- "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_security_groups",
- "subtitle" : "Security Groups",
- "title" : "Proxmox VE Firewall"
- },
- "pve_firewall_vm_container_configuration" : {
- "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration",
- "subtitle" : "VM/Container Configuration",
- "title" : "Proxmox VE Firewall"
- },
- "pve_service_daemons" : {
- "link" : "/pve-docs/index.html#_service_daemons",
- "title" : "Service Daemons"
- },
- "pveceph_fs" : {
- "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs",
- "subtitle" : "CephFS",
- "title" : "Deploy Hyper-Converged Ceph Cluster"
- },
- "pveceph_fs_create" : {
- "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create",
- "subtitle" : "Create CephFS",
- "title" : "Deploy Hyper-Converged Ceph Cluster"
- },
- "pvecm_create_cluster" : {
- "link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster",
- "subtitle" : "Create a Cluster",
- "title" : "Cluster Manager"
- },
- "pvecm_join_node_to_cluster" : {
- "link" : "/pve-docs/chapter-pvecm.html#pvecm_join_node_to_cluster",
- "subtitle" : "Adding Nodes to the Cluster",
- "title" : "Cluster Manager"
- },
- "pvesdn_config_controllers" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_controllers",
- "subtitle" : "Controllers",
- "title" : "Software-Defined Network"
- },
- "pvesdn_config_fabrics" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_fabrics",
- "subtitle" : "Fabrics",
- "title" : "Software-Defined Network"
- },
- "pvesdn_config_vnet" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_vnet",
- "subtitle" : "VNets",
- "title" : "Software-Defined Network"
- },
- "pvesdn_config_zone" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_zone",
- "subtitle" : "Zones",
- "title" : "Software-Defined Network"
- },
- "pvesdn_controller_plugin_evpn" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_controller_plugin_evpn",
- "subtitle" : "EVPN Controller",
- "title" : "Software-Defined Network"
- },
- "pvesdn_dns_plugin_powerdns" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_dns_plugin_powerdns",
- "subtitle" : "PowerDNS Plugin",
- "title" : "Software-Defined Network"
- },
- "pvesdn_firewall_integration" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_firewall_integration",
- "subtitle" : "Firewall Integration",
- "title" : "Software-Defined Network"
- },
- "pvesdn_ipam_plugin_netbox" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_netbox",
- "subtitle" : "NetBox IPAM Plugin",
- "title" : "Software-Defined Network"
- },
- "pvesdn_ipam_plugin_phpipam" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_phpipam",
- "subtitle" : "phpIPAM Plugin",
- "title" : "Software-Defined Network"
- },
- "pvesdn_ipam_plugin_pveipam" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_pveipam",
- "subtitle" : "PVE IPAM Plugin",
- "title" : "Software-Defined Network"
- },
- "pvesdn_openfabric_fabric" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_openfabric_fabric",
- "subtitle" : "On the Fabric",
- "title" : "Software-Defined Network"
- },
- "pvesdn_ospf_fabric" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ospf_fabric",
- "subtitle" : "On the Fabric",
- "title" : "Software-Defined Network"
- },
- "pvesdn_zone_plugin_evpn" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_evpn",
- "subtitle" : "EVPN Zones",
- "title" : "Software-Defined Network"
- },
- "pvesdn_zone_plugin_qinq" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_qinq",
- "subtitle" : "QinQ Zones",
- "title" : "Software-Defined Network"
- },
- "pvesdn_zone_plugin_simple" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_simple",
- "subtitle" : "Simple Zones",
- "title" : "Software-Defined Network"
- },
- "pvesdn_zone_plugin_vlan" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vlan",
- "subtitle" : "VLAN Zones",
- "title" : "Software-Defined Network"
- },
- "pvesdn_zone_plugin_vxlan" : {
- "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vxlan",
- "subtitle" : "VXLAN Zones",
- "title" : "Software-Defined Network"
- },
- "pvesr_schedule_time_format" : {
- "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format",
- "subtitle" : "Schedule Format",
- "title" : "Storage Replication"
- },
- "pveum_authentication_realms" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms",
- "subtitle" : "Authentication Realms",
- "title" : "User Management"
- },
- "pveum_configure_u2f" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_configure_u2f",
- "subtitle" : "Server Side U2F Configuration",
- "title" : "User Management"
- },
- "pveum_configure_webauthn" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_configure_webauthn",
- "subtitle" : "Server Side Webauthn Configuration",
- "title" : "User Management"
- },
- "pveum_groups" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_groups",
- "subtitle" : "Groups",
- "title" : "User Management"
- },
- "pveum_ldap_sync" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_ldap_sync",
- "subtitle" : "Syncing LDAP-Based Realms",
- "title" : "User Management"
- },
- "pveum_permission_management" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_permission_management",
- "subtitle" : "Permission Management",
- "title" : "User Management"
- },
- "pveum_pools" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_pools",
- "subtitle" : "Pools",
- "title" : "User Management"
- },
- "pveum_roles" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_roles",
- "subtitle" : "Roles",
- "title" : "User Management"
- },
- "pveum_tokens" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_tokens",
- "subtitle" : "API Tokens",
- "title" : "User Management"
- },
- "pveum_users" : {
- "link" : "/pve-docs/chapter-pveum.html#pveum_users",
- "subtitle" : "Users",
- "title" : "User Management"
- },
- "qm_audio_device" : {
- "link" : "/pve-docs/chapter-qm.html#qm_audio_device",
- "subtitle" : "Audio Device",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_bios_and_uefi" : {
- "link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi",
- "subtitle" : "BIOS and UEFI",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_bootorder" : {
- "link" : "/pve-docs/chapter-qm.html#qm_bootorder",
- "subtitle" : "Device Boot Order",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_cloud_init" : {
- "link" : "/pve-docs/chapter-qm.html#qm_cloud_init",
- "title" : "Cloud-Init Support"
- },
- "qm_copy_and_clone" : {
- "link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone",
- "subtitle" : "Copies and Clones",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_cpu" : {
- "link" : "/pve-docs/chapter-qm.html#qm_cpu",
- "subtitle" : "CPU",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_display" : {
- "link" : "/pve-docs/chapter-qm.html#qm_display",
- "subtitle" : "Display",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_general_settings" : {
- "link" : "/pve-docs/chapter-qm.html#qm_general_settings",
- "subtitle" : "General Settings",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_hard_disk" : {
- "link" : "/pve-docs/chapter-qm.html#qm_hard_disk",
- "subtitle" : "Hard Disk",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_import_virtual_machines" : {
- "link" : "/pve-docs/chapter-qm.html#qm_import_virtual_machines",
- "subtitle" : "Importing Virtual Machines",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_machine_type" : {
- "link" : "/pve-docs/chapter-qm.html#qm_machine_type",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_memory" : {
- "link" : "/pve-docs/chapter-qm.html#qm_memory",
- "subtitle" : "Memory",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_migration" : {
- "link" : "/pve-docs/chapter-qm.html#qm_migration",
- "subtitle" : "Migration",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_network_device" : {
- "link" : "/pve-docs/chapter-qm.html#qm_network_device",
- "subtitle" : "Network Device",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_options" : {
- "link" : "/pve-docs/chapter-qm.html#qm_options",
- "subtitle" : "Options",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_os_settings" : {
- "link" : "/pve-docs/chapter-qm.html#qm_os_settings",
- "subtitle" : "OS Settings",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_pci_passthrough_vm_config" : {
- "link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough_vm_config",
- "subtitle" : "VM Configuration",
- "title" : "PCI(e) Passthrough"
- },
- "qm_qemu_agent" : {
- "link" : "/pve-docs/chapter-qm.html#qm_qemu_agent",
- "subtitle" : "QEMU Guest Agent",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_spice_enhancements" : {
- "link" : "/pve-docs/chapter-qm.html#qm_spice_enhancements",
- "subtitle" : "SPICE Enhancements",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_startup_and_shutdown" : {
- "link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown",
- "subtitle" : "Automatic Start and Shutdown of Virtual Machines",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_system_settings" : {
- "link" : "/pve-docs/chapter-qm.html#qm_system_settings",
- "subtitle" : "System Settings",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_tpm" : {
- "link" : "/pve-docs/chapter-qm.html#qm_tpm",
- "subtitle" : "Trusted Platform Module (TPM)",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_usb_passthrough" : {
- "link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough",
- "subtitle" : "USB Passthrough",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_virtio_rng" : {
- "link" : "/pve-docs/chapter-qm.html#qm_virtio_rng",
- "subtitle" : "VirtIO RNG",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_virtiofs" : {
- "link" : "/pve-docs/chapter-qm.html#qm_virtiofs",
- "subtitle" : "Virtiofs",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_virtual_machines_settings" : {
- "link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings",
- "subtitle" : "Virtual Machines Settings",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "qm_vmstatestorage" : {
- "link" : "/pve-docs/chapter-qm.html#qm_vmstatestorage",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "resource_mapping" : {
- "link" : "/pve-docs/chapter-qm.html#resource_mapping",
- "subtitle" : "Resource Mapping",
- "title" : "QEMU/KVM Virtual Machines"
- },
- "storage_btrfs" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_btrfs",
- "title" : "BTRFS Backend"
- },
- "storage_cephfs" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_cephfs",
- "title" : "Ceph Filesystem (CephFS)"
- },
- "storage_cifs" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_cifs",
- "title" : "CIFS Backend"
- },
- "storage_directory" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_directory",
- "title" : "Directory Backend"
- },
- "storage_lvm" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_lvm",
- "title" : "LVM Backend"
- },
- "storage_lvmthin" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_lvmthin",
- "title" : "LVM thin Backend"
- },
- "storage_nfs" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_nfs",
- "title" : "NFS Backend"
- },
- "storage_open_iscsi" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_open_iscsi",
- "title" : "Open-iSCSI initiator"
- },
- "storage_pbs" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_pbs",
- "title" : "Proxmox Backup Server"
- },
- "storage_pbs_encryption" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_pbs_encryption",
- "subtitle" : "Encryption",
- "title" : "Proxmox Backup Server"
- },
- "storage_zfspool" : {
- "link" : "/pve-docs/chapter-pvesm.html#storage_zfspool",
- "title" : "Local ZFS Pool Backend"
- },
- "sysadmin_certificate_management" : {
- "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management",
- "title" : "Certificate Management"
- },
- "sysadmin_certs_acme_account" : {
- "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certs_acme_account",
- "subtitle" : "ACME Account",
- "title" : "Certificate Management"
- },
- "sysadmin_network_configuration" : {
- "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration",
- "title" : "Network Configuration"
- },
- "sysadmin_package_repositories" : {
- "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_package_repositories",
- "title" : "Package Repositories"
- },
- "user-realms-ad" : {
- "link" : "/pve-docs/chapter-pveum.html#user-realms-ad",
- "subtitle" : "Microsoft Active Directory (AD)",
- "title" : "User Management"
- },
- "user-realms-ldap" : {
- "link" : "/pve-docs/chapter-pveum.html#user-realms-ldap",
- "subtitle" : "LDAP",
- "title" : "User Management"
- },
- "user-realms-pam" : {
- "link" : "/pve-docs/chapter-pveum.html#user-realms-pam",
- "subtitle" : "Linux PAM Standard Authentication",
- "title" : "User Management"
- },
- "user_mgmt" : {
- "link" : "/pve-docs/chapter-pveum.html#user_mgmt",
- "title" : "User Management"
- },
- "vzdump_retention" : {
- "link" : "/pve-docs/chapter-vzdump.html#vzdump_retention",
- "subtitle" : "Backup Retention",
- "title" : "Backup and Restore"
- }
-};
-// Some configuration values are complex strings - so we need parsers/generators for them.
-Ext.define('PVE.Parser', {
- statics: {
- // this class only contains static functions
-
- printACME: function (value) {
- if (Ext.isArray(value.domains)) {
- value.domains = value.domains.join(';');
- }
- return PVE.Parser.printPropertyString(value);
- },
-
- parseACME: function (value) {
- if (!value) {
- return {};
- }
-
- let res = {};
- try {
- value.split(',').forEach((property) => {
- let [k, v] = property.split('=', 2);
- if (Ext.isDefined(v)) {
- res[k] = v;
- } else {
- throw `Failed to parse key-value pair: ${property}`;
- }
- });
- } catch (err) {
- console.warn(err);
- return undefined;
- }
-
- if (res.domains !== undefined) {
- res.domains = res.domains.split(/;/);
- }
-
- return res;
- },
-
- parseBoolean: function (value, default_value) {
- if (!Ext.isDefined(value)) {
- return default_value;
- }
- value = value.toLowerCase();
- return value === '1' || value === 'on' || value === 'yes' || value === 'true';
- },
-
- parsePropertyString: function (value, defaultKey) {
- let res = {};
-
- if (typeof value !== 'string' || value === '') {
- return res;
- }
-
- try {
- value.split(',').forEach((property) => {
- let [k, v] = property.split('=', 2);
- if (Ext.isDefined(v)) {
- res[k] = v;
- } else if (Ext.isDefined(defaultKey)) {
- if (Ext.isDefined(res[defaultKey])) {
- throw 'defaultKey may be only defined once in propertyString';
- }
- res[defaultKey] = k; // k is the value in this case
- } else {
- throw `Failed to parse key-value pair: ${property}`;
- }
- });
- } catch (err) {
- console.warn(err);
- return undefined;
- }
-
- return res;
- },
-
- printPropertyString: function (data, defaultKey) {
- var stringparts = [],
- gotDefaultKeyVal = false,
- defaultKeyVal;
-
- Ext.Object.each(data, function (key, value) {
- if (defaultKey !== undefined && key === defaultKey) {
- gotDefaultKeyVal = true;
- defaultKeyVal = value;
- } else if (value !== '') {
- stringparts.push(key + '=' + value);
- }
- });
-
- stringparts = stringparts.sort();
- if (gotDefaultKeyVal) {
- stringparts.unshift(defaultKeyVal);
- }
-
- return stringparts.join(',');
- },
-
- parseQemuNetwork: function (key, value) {
- if (!(key && value)) {
- return undefined;
- }
-
- let res = {},
- errors = false;
- Ext.Array.each(value.split(','), function (p) {
- if (!p || p.match(/^\s*$/)) {
- return undefined; // continue
- }
-
- let match_res;
-
- if (
- (match_res = p.match(
- /^(ne2k_pci|e1000e?|e1000-82540em|e1000-82544gc|e1000-82545em|vmxnet3|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i,
- )) !== null
- ) {
- res.model = match_res[1].toLowerCase();
- if (match_res[3]) {
- res.macaddr = match_res[3];
- }
- } else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) {
- res.bridge = match_res[1];
- } else if ((match_res = p.match(/^rate=(\d+(\.\d+)?|\.\d+)$/)) !== null) {
- res.rate = match_res[1];
- } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) {
- res.tag = match_res[1];
- } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
- res.firewall = match_res[1];
- } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
- res.disconnect = match_res[1];
- } else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) {
- res.queues = match_res[1];
- } else if (
- (match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null
- ) {
- res.trunks = match_res[1];
- } else if ((match_res = p.match(/^mtu=(\d+)$/)) !== null) {
- res.mtu = match_res[1];
- } else {
- errors = true;
- return false; // break
- }
- return undefined; // continue
- });
-
- if (errors || !res.model) {
- return undefined;
- }
-
- return res;
- },
-
- printQemuNetwork: function (net) {
- var netstr = net.model;
- if (net.macaddr) {
- netstr += '=' + net.macaddr;
- }
- if (net.bridge) {
- netstr += ',bridge=' + net.bridge;
- if (net.tag) {
- netstr += ',tag=' + net.tag;
- }
- if (net.firewall) {
- netstr += ',firewall=' + net.firewall;
- }
- }
- if (net.rate) {
- netstr += ',rate=' + net.rate;
- }
- if (net.queues) {
- netstr += ',queues=' + net.queues;
- }
- if (net.disconnect) {
- netstr += ',link_down=' + net.disconnect;
- }
- if (net.trunks) {
- netstr += ',trunks=' + net.trunks;
- }
- if (net.mtu) {
- netstr += ',mtu=' + net.mtu;
- }
- return netstr;
- },
-
- parseQemuDrive: function (key, value) {
- if (!(key && value)) {
- return undefined;
- }
-
- const [, bus, index] = key.match(/^([a-z]+)(\d+)$/);
- if (!bus) {
- return undefined;
- }
- let res = {
- interface: bus,
- index,
- };
-
- var errors = false;
- Ext.Array.each(value.split(','), function (p) {
- if (!p || p.match(/^\s*$/)) {
- return undefined; // continue
- }
- let match = p.match(/^([a-z_]+)=(\S+)$/);
- if (!match) {
- if (!p.match(/[=]/)) {
- res.file = p;
- return undefined; // continue
- }
- errors = true;
- return false; // break
- }
- let [, k, v] = match;
- if (k === 'volume') {
- k = 'file';
- }
-
- if (Ext.isDefined(res[k])) {
- errors = true;
- return false; // break
- }
-
- if (k === 'cache' && v === 'off') {
- v = 'none';
- }
-
- res[k] = v;
-
- return undefined; // continue
- });
-
- if (errors || !res.file) {
- return undefined;
- }
-
- return res;
- },
-
- printQemuDrive: function (drive) {
- var drivestr = drive.file;
-
- Ext.Object.each(drive, function (key, value) {
- if (
- !Ext.isDefined(value) ||
- key === 'file' ||
- key === 'index' ||
- key === 'interface'
- ) {
- return; // continue
- }
- drivestr += ',' + key + '=' + value;
- });
-
- return drivestr;
- },
-
- parseIPConfig: function (key, value) {
- if (!(key && value)) {
- return undefined; // continue
- }
-
- let res = {};
- try {
- value.split(',').forEach((p) => {
- if (!p || p.match(/^\s*$/)) {
- return; // continue
- }
-
- const match = p.match(/^(ip|gw|ip6|gw6)=(\S+)$/);
- if (!match) {
- throw `could not parse as IP config: ${p}`;
- }
- let [, k, v] = match;
- res[k] = v;
- });
- } catch (err) {
- console.warn(err);
- return undefined; // continue
- }
-
- return res;
- },
-
- printIPConfig: function (cfg) {
- return Object.entries(cfg)
- .filter(([k, v]) => v && k.match(/^(ip|gw|ip6|gw6)$/))
- .map(([k, v]) => `${k}=${v}`)
- .join(',');
- },
-
- parseLxcNetwork: function (value) {
- if (!value) {
- return undefined;
- }
-
- let data = {};
- value.split(',').forEach((p) => {
- if (!p || p.match(/^\s*$/)) {
- return; // continue
- }
- let match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/);
- if (match_res) {
- data[match_res[1]] = match_res[2];
- } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
- data.firewall = PVE.Parser.parseBoolean(match_res[1]);
- } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
- data.link_down = PVE.Parser.parseBoolean(match_res[1]);
- } else if ((match_res = p.match(/^host-managed=(\d+)$/)) !== null) {
- data['host-managed'] = PVE.Parser.parseBoolean(match_res[1]);
- } else if (!p.match(/^type=\S+$/)) {
- console.warn(`could not parse LXC network string ${p}`);
- }
- });
-
- return data;
- },
-
- printLxcNetwork: function (config) {
- let knownKeys = {
- bridge: 1,
- firewall: 1,
- gw6: 1,
- gw: 1,
- hwaddr: 1,
- ip6: 1,
- ip: 1,
- mtu: 1,
- name: 1,
- rate: 1,
- tag: 1,
- link_down: 1,
- 'host-managed': 1,
- };
- return Object.entries(config)
- .filter(([k, v]) => v !== undefined && v !== '' && knownKeys[k])
- .map(([k, v]) => `${k}=${v}`)
- .join(',');
- },
-
- parseLxcMountPoint: function (value) {
- if (!value) {
- return undefined;
- }
-
- let res = {};
- let errors = false;
- Ext.Array.each(value.split(','), function (p) {
- if (!p || p.match(/^\s*$/)) {
- return undefined; // continue
- }
- let match = p.match(/^([a-z_]+)=(.+)$/);
- if (!match) {
- if (!p.match(/[=]/)) {
- res.file = p;
- return undefined; // continue
- }
- errors = true;
- return false; // break
- }
- let [, k, v] = match;
- if (k === 'volume') {
- k = 'file';
- }
-
- if (Ext.isDefined(res[k])) {
- errors = true;
- return false; // break
- }
-
- res[k] = v;
-
- return undefined;
- });
-
- if (errors || !res.file) {
- return undefined;
- }
-
- const match = res.file.match(/^([a-z][a-z0-9\-_.]*[a-z0-9]):/i);
- if (match) {
- res.storage = match[1];
- res.type = 'volume';
- } else if (res.file.match(/^\/dev\//)) {
- res.type = 'device';
- } else {
- res.type = 'bind';
- }
-
- return res;
- },
-
- printLxcMountPoint: function (mp) {
- let drivestr = mp.file;
- for (const [key, value] of Object.entries(mp)) {
- if (
- !Ext.isDefined(value) ||
- key === 'file' ||
- key === 'type' ||
- key === 'storage'
- ) {
- continue;
- }
- drivestr += `,${key}=${value}`;
- }
- return drivestr;
- },
-
- parseStartup: function (value) {
- if (value === undefined) {
- return undefined;
- }
-
- let res = {};
- try {
- value.split(',').forEach((p) => {
- if (!p || p.match(/^\s*$/)) {
- return; // continue
- }
-
- let match_res;
- if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) {
- res.order = match_res[2];
- } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) {
- res.up = match_res[1];
- } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) {
- res.down = match_res[1];
- } else {
- throw `could not parse startup config ${p}`;
- }
- });
- } catch (err) {
- console.warn(err);
- return undefined;
- }
-
- return res;
- },
-
- printStartup: function (startup) {
- let arr = [];
- if (startup.order !== undefined && startup.order !== '') {
- arr.push('order=' + startup.order);
- }
- if (startup.up !== undefined && startup.up !== '') {
- arr.push('up=' + startup.up);
- }
- if (startup.down !== undefined && startup.down !== '') {
- arr.push('down=' + startup.down);
- }
-
- return arr.join(',');
- },
-
- parseQemuSmbios1: function (value) {
- let res = value.split(',').reduce((acc, currentValue) => {
- const [k, v] = currentValue.split(/[=](.+)/);
- acc[k] = v;
- return acc;
- }, {});
-
- if (PVE.Parser.parseBoolean(res.base64, false)) {
- for (const [k, v] of Object.entries(res)) {
- if (k !== 'uuid') {
- res[k] = Ext.util.Base64.decode(v);
- }
- }
- }
-
- return res;
- },
-
- printQemuSmbios1: function (data) {
- let base64 = false;
- let datastr = Object.entries(data)
- .map(([key, value]) => {
- if (value === '') {
- return undefined;
- }
- if (key !== 'uuid') {
- base64 = true; // smbios values can be arbitrary, so encode and mark config as such
- value = Ext.util.Base64.encode(value);
- }
- return `${key}=${value}`;
- })
- .filter((v) => v !== undefined)
- .join(',');
-
- if (base64) {
- datastr += ',base64=1';
- }
- return datastr;
- },
-
- parseTfaConfig: function (value) {
- let res = {};
- value.split(',').forEach((p) => {
- const [k, v] = p.split('=', 2);
- res[k] = v;
- });
-
- return res;
- },
-
- parseTfaType: function (value) {
- let match;
- if (!value || !value.length) {
- return undefined;
- } else if (value === 'x!oath') {
- return 'totp';
- } else if ((match = value.match(/^x!(.+)$/)) !== null) {
- return match[1];
- } else {
- return 1;
- }
- },
-
- parseQemuCpu: function (value) {
- if (!value) {
- return {};
- }
-
- let res = {};
- let errors = false;
- Ext.Array.each(value.split(','), function (p) {
- if (!p || p.match(/^\s*$/)) {
- return undefined; // continue
- }
-
- if (!p.match(/[=]/)) {
- if (Ext.isDefined(res.cpu)) {
- errors = true;
- return false; // break
- }
- res.cputype = p;
- return undefined; // continue
- }
-
- let match = p.match(/^([a-z_-]+)=(\S+)$/);
- if (!match || Ext.isDefined(res[match[1]])) {
- errors = true;
- return false; // break
- }
-
- let [, k, v] = match;
- res[k] = v;
-
- return undefined;
- });
-
- if (errors || !res.cputype) {
- return undefined;
- }
-
- return res;
- },
-
- printQemuCpu: function (cpu) {
- let cpustr = cpu.cputype;
- let optstr = '';
-
- Ext.Object.each(cpu, function (key, value) {
- if (!Ext.isDefined(value) || key === 'cputype') {
- return; // continue
- }
- optstr += ',' + key + '=' + value;
- });
-
- if (!cpustr) {
- if (optstr) {
- return 'kvm64' + optstr;
- } else {
- return undefined;
- }
- }
-
- return cpustr + optstr;
- },
-
- parseSSHKey: function (key) {
- // |--- options can have quotes--| type key comment
- let keyre = /^(?:((?:[^\s"]|"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
- let typere =
- /^(?:(?:sk-)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)(?:@(?:[a-z0-9_-]+\.)+[a-z]{2,})?)$/;
-
- let m = key.match(keyre);
- if (!m || m.length < 3 || !m[2]) {
- // [2] is always either type or key
- return null;
- }
- if (m[1] && m[1].match(typere)) {
- return {
- type: m[1],
- key: m[2],
- comment: m[3],
- };
- }
- if (m[2].match(typere)) {
- return {
- options: m[1],
- type: m[2],
- key: m[3],
- comment: m[4],
- };
- }
- return null;
- },
-
- parseACMEPluginData: function (data) {
- let res = {};
- let extradata = [];
- data.split('\n').forEach((line) => {
- // capture everything after the first = as value
- let [key, value] = line.split(/[=](.+)/);
- if (value !== undefined) {
- res[key] = value;
- } else {
- extradata.push(line);
- }
- });
- return [res, extradata];
- },
-
- filterPropertyStringList: function (list, filterFn, defaultKey) {
- return list.filter((entry) =>
- filterFn(PVE.Parser.parsePropertyString(entry, defaultKey)),
- );
- },
- },
-});
-/* This state provider keeps part of the state inside the browser history.
- *
- * We compress (shorten) url using dictionary based compression, i.e., we use
- * column separated list instead of url encoded hash:
- * #v\d* version/format
- * := indicates string values
- * :\d+ lookup value in dictionary hash
- * #v1:=value1:5:=value2:=value3:...
- */
-
-Ext.define('PVE.StateProvider', {
- extend: 'Ext.state.LocalStorageProvider',
-
- // private
- setHV: function (name, newvalue, fireEvents) {
- let me = this;
-
- let changes = false;
- let oldtext = Ext.encode(me.UIState[name]);
- let newtext = Ext.encode(newvalue);
- if (newtext !== oldtext) {
- changes = true;
- me.UIState[name] = newvalue;
- if (fireEvents) {
- me.fireEvent('statechange', me, name, { value: newvalue });
- }
- }
- return changes;
- },
-
- // private
- hslist: [
- // order is important for notifications
- // [ name, default ]
- ['view', 'server'],
- ['rid', 'root'],
- ['ltab', 'tasks'],
- ['nodetab', ''],
- ['storagetab', ''],
- ['sdntab', ''],
- ['pooltab', ''],
- ['kvmtab', ''],
- ['lxctab', ''],
- ['dctab', ''],
- ],
-
- hprefix: 'v1',
-
- compDict: {
- tfa: 54,
- sdn: 53,
- cloudinit: 52,
- replication: 51,
- system: 50,
- monitor: 49,
- 'ha-fencing': 48,
- 'ha-rules': 47,
- 'ha-resources': 46,
- 'ceph-log': 45,
- 'ceph-crushmap': 44,
- 'ceph-pools': 43,
- 'ceph-osdtree': 42,
- 'ceph-disklist': 41,
- 'ceph-monlist': 40,
- 'ceph-config': 39,
- ceph: 38,
- 'firewall-fwlog': 37,
- 'firewall-options': 36,
- 'firewall-ipset': 35,
- 'firewall-aliases': 34,
- 'firewall-sg': 33,
- firewall: 32,
- apt: 31,
- members: 30,
- snapshot: 29,
- ha: 28,
- support: 27,
- pools: 26,
- syslog: 25,
- ubc: 24,
- initlog: 23,
- openvz: 22,
- backup: 21,
- resources: 20,
- content: 19,
- root: 18,
- domains: 17,
- roles: 16,
- groups: 15,
- users: 14,
- time: 13,
- dns: 12,
- network: 11,
- services: 10,
- options: 9,
- console: 8,
- hardware: 7,
- permissions: 6,
- summary: 5,
- tasks: 4,
- clog: 3,
- storage: 2,
- folder: 1,
- server: 0,
- },
-
- decodeHToken: function (token) {
- let me = this;
-
- let state = {};
- if (!token) {
- me.hslist.forEach(([k, v]) => {
- state[k] = v;
- });
- return state;
- }
-
- let [prefix, ...items] = token.split(':');
-
- if (prefix !== me.hprefix) {
- return me.decodeHToken();
- }
-
- Ext.Array.each(me.hslist, function (rec) {
- let value = items.shift();
- if (value) {
- if (value[0] === '=') {
- value = decodeURIComponent(value.slice(1));
- }
- for (const [key, hash] of Object.entries(me.compDict)) {
- if (String(value) === String(hash)) {
- value = key;
- break;
- }
- }
- }
- state[rec[0]] = value;
- });
-
- return state;
- },
-
- encodeHToken: function (state) {
- let me = this;
-
- let ctoken = me.hprefix;
- Ext.Array.each(me.hslist, function (rec) {
- let value = state[rec[0]];
- if (!Ext.isDefined(value)) {
- value = rec[1];
- }
- value = encodeURIComponent(value);
- if (!value) {
- ctoken += ':';
- } else if (Ext.isDefined(me.compDict[value])) {
- ctoken += ':' + me.compDict[value];
- } else {
- ctoken += ':=' + value;
- }
- });
-
- return ctoken;
- },
-
- constructor: function (config) {
- let me = this;
-
- me.callParent([config]);
-
- me.UIState = me.decodeHToken(); // set default
-
- let history_change_cb = function (token) {
- if (!token) {
- Ext.History.back();
- return;
- }
-
- let newstate = me.decodeHToken(token);
- Ext.Array.each(me.hslist, function (rec) {
- if (typeof newstate[rec[0]] === 'undefined') {
- return;
- }
- me.setHV(rec[0], newstate[rec[0]], true);
- });
- };
-
- let start_token = Ext.History.getToken();
- if (start_token) {
- history_change_cb(start_token);
- } else {
- let htext = me.encodeHToken(me.UIState);
- Ext.History.add(htext);
- }
-
- Ext.History.on('change', history_change_cb);
- },
-
- get: function (name, defaultValue) {
- let me = this;
-
- let data;
- if (typeof me.UIState[name] !== 'undefined') {
- data = { value: me.UIState[name] };
- } else {
- data = me.callParent(arguments);
- if (!data && name === 'GuiCap') {
- data = {
- vms: {},
- storage: {},
- access: {},
- nodes: {},
- dc: {},
- sdn: {},
- };
- }
- }
- return data;
- },
-
- clear: function (name) {
- let me = this;
-
- if (typeof me.UIState[name] !== 'undefined') {
- me.UIState[name] = null;
- }
- me.callParent(arguments);
- },
-
- set: function (name, value, fireevent) {
- let me = this;
-
- if (typeof me.UIState[name] !== 'undefined') {
- let newvalue = value ? value.value : null;
- if (me.setHV(name, newvalue, fireevent)) {
- let htext = me.encodeHToken(me.UIState);
- Ext.History.add(htext);
- }
- } else {
- me.callParent(arguments);
- }
- },
-});
-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: '6.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;
- },
-
- 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 OMVF BIOS': gettext('EFI Disk without OMVF 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 (PVE.Parser.parseBoolean(value)) {
- displayText = Proxmox.Utils.enabledText;
- }
- agentstring += `, ${key}: ${displayText}`;
- }
-
- return agentstring;
- },
-
- render_qemu_machine: function (value) {
- return value || Proxmox.Utils.defaultText + ' (i440fx)';
- },
-
- render_qemu_bios: function (value) {
- if (!value) {
- return Proxmox.Utils.defaultText + ' (SeaBIOS)';
- } else if (value === 'seabios') {
- return 'SeaBIOS';
- } else if (value === 'ovmf') {
- return 'OVMF (UEFI)';
- } else {
- return value;
- }
- },
-
- render_dc_ha_opts: function (value) {
- if (!value) {
- return Proxmox.Utils.defaultText;
- } else {
- return PVE.Parser.printPropertyString(value);
- }
- },
- render_as_property_string: (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_optional_url: function (value) {
- if (value && value.match(/^https?:\/\//)) {
- return '' + value + ' ';
- }
- return value;
- },
-
- render_san: function (value) {
- var names = [];
- if (Ext.isArray(value)) {
- value.forEach(function (val) {
- if (!Ext.isNumber(val)) {
- names.push(val);
- }
- });
- return names.join(' ');
- }
- 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;
- },
-
- 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 false;
- }
- }
-
- if (minVersion.length > toCheck.length) {
- for (; i < minVersion.length; i++) {
- if (minVersion[i] !== 0) {
- return false;
- }
- }
- }
-
- return true;
- },
-
- 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, controllerList) {
- if (!controllerList) {
- 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 sortPriority = PVE.qemu.OSDefaults.getDefaults(vmconfig.ostype).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'),
- });
- },
-});
-Ext.define('PVE.UIOptions', {
- singleton: true,
-
- options: {
- 'allowed-tags': [],
- },
-
- update: function () {
- Proxmox.Utils.API2Request({
- url: '/cluster/options',
- method: 'GET',
- success: function (response) {
- for (const option of ['allowed-tags', 'console', 'tag-style']) {
- PVE.UIOptions.options[option] = response?.result?.data?.[option];
- }
-
- PVE.UIOptions.updateTagList(PVE.UIOptions.options['allowed-tags']);
- PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']);
- PVE.UIOptions.fireUIConfigChanged();
- },
- });
- },
-
- tagList: [],
-
- updateTagList: function (tags) {
- PVE.UIOptions.tagList = [...new Set([...tags])].sort();
- },
-
- parseTagOverrides: function (overrides) {
- let colors = {};
- (overrides || '').split(';').forEach((color) => {
- if (!color) {
- return;
- }
- let [tag, color_hex, font_hex] = color.split(':');
- let r = parseInt(color_hex.slice(0, 2), 16);
- let g = parseInt(color_hex.slice(2, 4), 16);
- let b = parseInt(color_hex.slice(4, 6), 16);
- colors[tag] = [r, g, b];
- if (font_hex) {
- colors[tag].push(parseInt(font_hex.slice(0, 2), 16));
- colors[tag].push(parseInt(font_hex.slice(2, 4), 16));
- colors[tag].push(parseInt(font_hex.slice(4, 6), 16));
- }
- });
- return colors;
- },
-
- tagOverrides: {},
-
- updateTagOverrides: function (colors) {
- let sp = Ext.state.Manager.getProvider();
- let color_state = sp.get('colors', '');
- let browser_colors = PVE.UIOptions.parseTagOverrides(color_state);
- PVE.UIOptions.tagOverrides = Ext.apply({}, browser_colors, colors);
- },
-
- updateTagSettings: function (style) {
- let overrides = style?.['color-map'];
- PVE.UIOptions.updateTagOverrides(PVE.UIOptions.parseTagOverrides(overrides ?? ''));
-
- let shape = style?.shape ?? 'circle';
- if (shape === '__default__') {
- style = 'circle';
- }
-
- Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${shape}`);
- },
-
- tagTreeStyles: {
- __default__: `${Proxmox.Utils.defaultText} (${gettext('Circle')})`,
- full: gettext('Full'),
- circle: gettext('Circle'),
- dense: gettext('Dense'),
- none: Proxmox.Utils.NoneText,
- },
-
- tagOrderOptions: {
- __default__: `${Proxmox.Utils.defaultText} (${gettext('Alphabetical')})`,
- config: gettext('Configuration'),
- alphabetical: gettext('Alphabetical'),
- },
-
- shouldSortTags: function () {
- return !(PVE.UIOptions.options['tag-style']?.ordering === 'config');
- },
-
- getTreeSortingValue: function (key) {
- let localStorage = Ext.state.Manager.getProvider();
- let browserValues = localStorage.get('pve-tree-sorting');
- let defaults = {
- 'sort-field': 'vmid',
- 'group-templates': true,
- 'group-guest-types': true,
- };
-
- return browserValues?.[key] ?? defaults[key];
- },
-
- fireUIConfigChanged: function () {
- PVE.data.ResourceStore.refresh();
- Ext.GlobalEvents.fireEvent('loadedUiOptions');
- },
-});
-// ExtJS related things
-
-Proxmox.Utils.toolkit = 'extjs';
-
-// custom PVE specific VTypes
-Ext.apply(Ext.form.field.VTypes, {
- QemuStartDate: function (v) {
- return /^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/.test(v);
- },
- QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"',
- IP64AddressList: (v) => PVE.Utils.verify_ip64_address_list(v, false),
- IP64AddressWithSuffixList: (v) => PVE.Utils.verify_ip64_address_list(v, true),
- IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2',
- IP64AddressListMask: /[A-Fa-f0-9,:.; ]/,
- PciIdText: gettext('Example') + ': 0x8086',
- PciId: (v) => /^0x[0-9a-fA-F]{4}$/.test(v),
-});
-
-Ext.define('PVE.form.field.Display', {
- override: 'Ext.form.field.Display',
-
- setSubmitValue: function (value) {
- // do nothing, this is only to allow generalized bindings for the:
- // `me.isCreate ? 'textfield' : 'displayfield'` cases we have.
- },
-});
-Ext.define('PVE.noVncConsole', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveNoVncConsole',
-
- nodename: undefined,
- vmid: undefined,
- cmd: undefined,
-
- consoleType: undefined, // lxc, kvm, shell, cmd
- xtermjs: false,
-
- layout: 'fit',
- border: false,
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.consoleType) {
- throw 'no console type specified';
- }
-
- if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') {
- throw 'no VM ID specified';
- }
-
- // always use same iframe, to avoid running several noVnc clients
- // at same time (to avoid performance problems)
- var box = Ext.create('Ext.ux.IFrame', { itemid: 'vncconsole', flex: 1 });
-
- let warning = Ext.create('Ext.Component', {
- userCls: 'pmx-hint',
- padding: 5,
- hidden: true,
- style: {
- 'text-align': 'center',
- },
- html: gettext(
- 'Application container detected - console might not be fully functional.',
- ),
- });
-
- var type = me.xtermjs ? 'xtermjs' : 'novnc';
- Ext.apply(me, {
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- items: [warning, box],
- listeners: {
- activate: function () {
- let sp = Ext.state.Manager.getProvider();
- if (Ext.isFunction(me.beforeLoad)) {
- me.beforeLoad();
- }
- let queryDict = {
- console: me.consoleType, // kvm, lxc, upgrade or shell
- vmid: me.vmid,
- node: me.nodename,
- cmd: me.cmd,
- 'cmd-opts': me.cmdOpts,
- resize: sp.get('novnc-scaling', 'scale'),
- };
- queryDict[type] = 1;
- PVE.Utils.cleanEmptyObjectKeys(queryDict);
- var url = '/?' + Ext.Object.toQueryString(queryDict);
- box.load(url);
- },
- },
- });
-
- me.callParent();
-
- // check for app container
- if (me.consoleType === 'lxc') {
- Proxmox.Utils.API2Request({
- url: `/nodes/${me.nodename}/lxc/${me.vmid}/config`,
- success: function (response) {
- let consoleMode = response?.result?.data?.cmode;
- let entryPoint = response?.result?.data?.entrypoint;
- let customEntryPoint = entryPoint !== undefined && entryPoint !== '/sbin/init';
-
- if (customEntryPoint && consoleMode === 'console') {
- warning.setVisible(true);
- setTimeout(() => {
- warning.setVisible(false);
- }, 8_000);
- }
- },
- });
- }
-
- me.on('afterrender', function () {
- box.focus();
- });
- },
-
- reload: function () {
- // reload IFrame content to forcibly reconnect VNC/xterm.js to VM
- var box = this.down('[itemid=vncconsole]');
- box.getWin().location.reload();
- },
-});
-Ext.define('PVE.button.ConsoleButton', {
- extend: 'Ext.button.Split',
- alias: 'widget.pveConsoleButton',
-
- consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd'
-
- cmd: undefined,
-
- consoleName: undefined,
-
- iconCls: 'fa fa-terminal',
-
- enableSpice: true,
- enableXtermjs: true,
-
- nodename: undefined,
-
- vmid: 0,
-
- text: gettext('Console'),
-
- setEnableSpice: function (enable) {
- var me = this;
-
- me.enableSpice = enable;
- me.down('#spicemenu').setDisabled(!enable);
- },
-
- setEnableXtermJS: function (enable) {
- var me = this;
-
- me.enableXtermjs = enable;
- me.down('#xtermjs').setDisabled(!enable);
- },
-
- handler: function () {
- // main, general, handler
- let me = this;
- PVE.Utils.openDefaultConsoleWindow(
- {
- spice: me.enableSpice,
- xtermjs: me.enableXtermjs,
- },
- me.consoleType,
- me.vmid,
- me.nodename,
- me.consoleName,
- me.cmd,
- );
- },
-
- openConsole: function (types) {
- // used by split-menu buttons
- let me = this;
- PVE.Utils.openConsoleWindow(
- types,
- me.consoleType,
- me.vmid,
- me.nodename,
- me.consoleName,
- me.cmd,
- );
- },
-
- menu: [
- {
- xtype: 'menuitem',
- text: 'noVNC',
- iconCls: 'pve-itype-icon-novnc',
- type: 'html5',
- handler: function (button) {
- let view = this.up('button');
- view.openConsole(button.type);
- },
- },
- {
- xterm: 'menuitem',
- itemId: 'spicemenu',
- text: 'SPICE',
- type: 'vv',
- iconCls: 'pve-itype-icon-virt-viewer',
- handler: function (button) {
- let view = this.up('button');
- view.openConsole(button.type);
- },
- },
- {
- text: 'xterm.js',
- itemId: 'xtermjs',
- iconCls: 'pve-itype-icon-xtermjs',
- type: 'xtermjs',
- handler: function (button) {
- let view = this.up('button');
- view.openConsole(button.type);
- },
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.button.PendingRevert', {
- extend: 'Proxmox.button.Button',
- alias: 'widget.pvePendingRevertButton',
-
- text: gettext('Revert'),
- disabled: true,
- config: {
- pendingGrid: null,
- apiurl: undefined,
- },
-
- handler: function () {
- if (!this.pendingGrid) {
- this.pendingGrid = this.up('proxmoxPendingObjectGrid');
- if (!this.pendingGrid) {
- throw 'revert button requires a pendingGrid';
- }
- }
- let view = this.pendingGrid;
-
- let rec = view.getSelectionModel().getSelection()[0];
- if (!rec) {
- return;
- }
-
- let rowdef = view.rows[rec.data.key] || {};
- let keys = rowdef.multiKey || [rec.data.key];
-
- Proxmox.Utils.API2Request({
- url: this.apiurl || view.editorConfig.url,
- waitMsgTarget: view,
- selModel: view.getSelectionModel(),
- method: 'PUT',
- params: {
- revert: keys.join(','),
- },
- callback: () => view.reload(),
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- });
- },
-});
-/* Button features:
- * - observe selection changes to enable/disable the button using enableFn()
- * - pop up confirmation dialog using confirmMsg()
- *
- * does this for the button and every menu item
- */
-Ext.define('PVE.button.Split', {
- extend: 'Ext.button.Split',
- alias: 'widget.pveSplitButton',
-
- // the selection model to observe
- selModel: undefined,
-
- // if 'false' handler will not be called (button disabled)
- enableFn: function (record) {
- // do nothing
- },
-
- // function(record) or text
- confirmMsg: false,
-
- // take special care in confirm box (select no as default).
- dangerous: false,
-
- handlerWrapper: function (button, event) {
- var me = this;
- var rec, msg;
- if (me.selModel) {
- rec = me.selModel.getSelection()[0];
- if (!rec || me.enableFn(rec) === false) {
- return;
- }
- }
-
- if (me.confirmMsg) {
- msg = me.confirmMsg;
- // confirMsg can be boolean or function
- if (Ext.isFunction(me.confirmMsg)) {
- msg = me.confirmMsg(rec);
- }
- Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
- Ext.Msg.show({
- title: gettext('Confirm'),
- icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
- msg: msg,
- buttons: Ext.Msg.YESNO,
- callback: function (btn) {
- if (btn !== 'yes') {
- return;
- }
- me.realHandler(button, event, rec);
- },
- });
- } else {
- me.realHandler(button, event, rec);
- }
- },
-
- initComponent: function () {
- var me = this;
-
- if (me.handler) {
- me.realHandler = me.handler;
- me.handler = me.handlerWrapper;
- }
-
- if (me.menu && me.menu.items) {
- me.menu.items.forEach(function (item) {
- if (item.handler) {
- item.realHandler = item.handler;
- item.handler = me.handlerWrapper;
- }
-
- if (item.selModel) {
- me.mon(item.selModel, 'selectionchange', function () {
- var rec = item.selModel.getSelection()[0];
- if (!rec || item.enableFn(rec) === false) {
- item.setDisabled(true);
- } else {
- item.setDisabled(false);
- }
- });
- }
- });
- }
-
- me.callParent();
-
- if (me.selModel) {
- me.mon(me.selModel, 'selectionchange', function () {
- var rec = me.selModel.getSelection()[0];
- if (!rec || me.enableFn(rec) === false) {
- me.setDisabled(true);
- } else {
- me.setDisabled(false);
- }
- });
- }
- },
-});
-Ext.define('PVE.controller.StorageEdit', {
- extend: 'Ext.app.ViewController',
- alias: 'controller.storageEdit',
- control: {
- 'field[name=content]': {
- change: function (field, value) {
- const hasImages = Ext.Array.contains(value, 'images');
- const prealloc = field.up('form').getForm().findField('preallocation');
- if (prealloc) {
- prealloc.setDisabled(!hasImages);
- }
- },
- },
- },
-});
-Ext.define('PVE.data.PermPathStore', {
- extend: 'Ext.data.Store',
- alias: 'store.pvePermPath',
- fields: ['value'],
- autoLoad: false,
- data: [
- { value: '/' },
- { value: '/access' },
- { value: '/access/groups' },
- { value: '/access/realm' },
- { value: '/mapping' },
- { value: '/mapping/hwrng' },
- { value: '/mapping/notifications' },
- { value: '/mapping/pci' },
- { value: '/mapping/usb' },
- { value: '/nodes' },
- { value: '/pool' },
- { value: '/sdn/fabrics' },
- { value: '/sdn/zones' },
- { value: '/storage' },
- { value: '/vms' },
- ],
-
- constructor: function (config) {
- var me = this;
-
- config = config || {};
-
- me.callParent([config]);
-
- let donePaths = {};
- me.suspendEvents();
- PVE.data.ResourceStore.each(function (record) {
- let path;
- switch (record.get('type')) {
- case 'node':
- path = '/nodes/' + record.get('text');
- break;
- case 'qemu':
- path = '/vms/' + record.get('vmid');
- break;
- case 'lxc':
- path = '/vms/' + record.get('vmid');
- break;
- case 'sdn':
- path = '/sdn/zones/' + record.get('sdn');
- break;
- case 'storage':
- path = '/storage/' + record.get('storage');
- break;
- case 'pool':
- path = '/pool/' + record.get('pool');
- break;
- }
- if (path !== undefined && !donePaths[path]) {
- me.add({ value: path });
- donePaths[path] = 1;
- }
- });
- me.resumeEvents();
-
- me.fireEvent('refresh', me);
- me.fireEvent('datachanged', me);
-
- me.sort({
- property: 'value',
- direction: 'ASC',
- });
- },
-});
-Ext.define('PVE.data.ResourceStore', {
- extend: 'Proxmox.data.UpdateStore',
- singleton: true,
-
- nodeCache: {},
-
- findVMID: function (vmid) {
- let me = this;
- return me.findExact('vmid', parseInt(vmid, 10)) >= 0;
- },
-
- // returns the cached data from all nodes
- getNodes: function () {
- let me = this;
-
- let nodes = [];
- me.each(function (record) {
- if (record.get('type') === 'node') {
- nodes.push(record.getData());
- }
- });
-
- return nodes;
- },
-
- getNodeById: function (id) {
- let me = this;
-
- if (!me.nodeCache[id]) {
- let idx = me.findExact('id', `node/${id}`);
- me.nodeCache[id] = me.getAt(idx);
- }
-
- return me.nodeCache[id];
- },
-
- clearCache: function () {
- let me = this;
- me.nodeCache = {};
- },
-
- storageIsShared: function (storage_path) {
- let me = this;
-
- let index = me.findExact('id', storage_path);
- if (index >= 0) {
- return me.getAt(index).data.shared;
- } else {
- return undefined;
- }
- },
-
- guestNode: function (vmid) {
- let me = this;
-
- let index = me.findExact('vmid', parseInt(vmid, 10));
-
- return me.getAt(index).data.node;
- },
-
- guestName: function (vmid) {
- let me = this;
- let index = me.findExact('vmid', parseInt(vmid, 10));
- if (index < 0) {
- return '-';
- }
- let rec = me.getAt(index).data;
- if ('name' in rec) {
- return rec.name;
- }
- return '';
- },
-
- refresh: function () {
- let me = this;
- // can only refresh if we're loaded at least once and are not currently loading
- if (!me.isLoading() && me.isLoaded()) {
- let records = (me.getData().getSource() || me.getData()).getRange();
- me.fireEvent('load', me, records);
- }
- },
-
- constructor: function (config) {
- let me = this;
-
- config = config || {};
-
- let field_defaults = {
- type: {
- header: gettext('Type'),
- type: 'string',
- renderer: PVE.Utils.render_resource_type,
- sortable: true,
- hideable: false,
- width: 100,
- },
- id: {
- header: 'ID',
- type: 'string',
- hidden: true,
- sortable: true,
- width: 80,
- },
- running: {
- header: gettext('Online'),
- type: 'boolean',
- renderer: Proxmox.Utils.format_boolean,
- hidden: true,
- convert: function (value, record) {
- var info = record.data;
- return Ext.isNumeric(info.uptime) && info.uptime > 0;
- },
- },
- text: {
- header: gettext('Description'),
- type: 'string',
- sortable: true,
- width: 200,
- convert: function (value, record) {
- if (value) {
- return value;
- }
-
- let info = record.data,
- text;
- if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
- text = String(info.vmid);
- if (info.name) {
- text += ' (' + info.name + ')';
- }
- } else {
- // node, pool, storage
- text = info[info.type] || info.id;
- if (info.node && info.type !== 'node') {
- text += ' (' + info.node + ')';
- }
- }
-
- return text;
- },
- },
- vmid: {
- header: 'VMID',
- type: 'integer',
- hidden: true,
- sortable: true,
- width: 80,
- },
- name: {
- header: gettext('Name'),
- hidden: true,
- sortable: true,
- type: 'string',
- },
- disk: {
- header: gettext('Disk usage'),
- type: 'integer',
- renderer: PVE.Utils.render_disk_usage,
- sortable: true,
- width: 100,
- hidden: true,
- },
- diskuse: {
- header: gettext('Disk usage') + ' %',
- type: 'number',
- sortable: true,
- renderer: PVE.Utils.render_disk_usage_percent,
- width: 100,
- calculate: PVE.Utils.calculate_disk_usage,
- sortType: 'asFloat',
- },
- maxdisk: {
- header: gettext('Disk size'),
- type: 'integer',
- renderer: Proxmox.Utils.render_size,
- sortable: true,
- hidden: true,
- width: 100,
- },
- mem: {
- header: gettext('Memory usage'),
- type: 'integer',
- renderer: PVE.Utils.render_mem_usage,
- sortable: true,
- hidden: true,
- width: 100,
- },
- memhost: {
- header: gettext('Host Memory usage'),
- type: 'integer',
- renderer: PVE.Utils.render_mem_usage,
- sortable: true,
- hidden: true,
- width: 100,
- },
- memuse: {
- header: gettext('Memory usage') + ' %',
- type: 'number',
- renderer: PVE.Utils.render_mem_usage_percent,
- calculate: PVE.Utils.calculate_mem_usage,
- sortType: 'asFloat',
- sortable: true,
- width: 100,
- },
- maxmem: {
- header: gettext('Memory size'),
- type: 'integer',
- renderer: Proxmox.Utils.render_size,
- hidden: true,
- sortable: true,
- width: 100,
- },
- cpu: {
- header: gettext('CPU usage'),
- type: 'float',
- renderer: Proxmox.Utils.render_cpu,
- sortable: true,
- width: 100,
- },
- maxcpu: {
- header: gettext('maxcpu'),
- type: 'integer',
- hidden: true,
- sortable: true,
- width: 60,
- },
- diskread: {
- header: gettext('Total Disk Read'),
- type: 'integer',
- hidden: true,
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- width: 100,
- },
- diskwrite: {
- header: gettext('Total Disk Write'),
- type: 'integer',
- hidden: true,
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- width: 100,
- },
- netin: {
- header: gettext('Total NetIn'),
- type: 'integer',
- hidden: true,
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- width: 100,
- },
- netout: {
- header: gettext('Total NetOut'),
- type: 'integer',
- hidden: true,
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- width: 100,
- },
- template: {
- header: gettext('Template'),
- type: 'integer',
- hidden: true,
- sortable: true,
- width: 60,
- },
- uptime: {
- header: gettext('Uptime'),
- type: 'integer',
- renderer: Proxmox.Utils.render_uptime,
- sortable: true,
- width: 110,
- },
- node: {
- header: gettext('Node'),
- type: 'string',
- hidden: true,
- sortable: true,
- width: 110,
- },
- storage: {
- header: gettext('Storage'),
- type: 'string',
- hidden: true,
- sortable: true,
- width: 110,
- },
- pool: {
- header: gettext('Pool'),
- type: 'string',
- hidden: true,
- sortable: true,
- width: 110,
- },
- hastate: {
- header: gettext('HA State'),
- type: 'string',
- defaultValue: 'unmanaged',
- hidden: true,
- sortable: true,
- },
- status: {
- header: gettext('Status'),
- type: 'string',
- hidden: true,
- sortable: true,
- width: 110,
- },
- lock: {
- header: gettext('Lock'),
- type: 'string',
- hidden: true,
- sortable: true,
- width: 110,
- },
- hostcpu: {
- header: gettext('Host CPU usage'),
- type: 'float',
- renderer: PVE.Utils.render_hostcpu,
- calculate: PVE.Utils.calculate_hostcpu,
- sortType: 'asFloat',
- sortable: true,
- width: 100,
- },
- hostmemuse: {
- header: gettext('Host Memory usage') + ' %',
- type: 'number',
- renderer: PVE.Utils.render_hostmem_usage_percent,
- calculate: PVE.Utils.calculate_hostmem_usage,
- sortType: 'asFloat',
- sortable: true,
- width: 100,
- },
- tags: {
- header: gettext('Tags'),
- renderer: (value) => PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
- type: 'string',
- sortable: true,
- flex: 1,
- },
- // note: flex only last column to keep info closer together
- };
-
- let fields = [];
- let fieldNames = [];
- Ext.Object.each(field_defaults, function (key, value) {
- var field = { name: key, type: value.type };
- if (Ext.isDefined(value.convert)) {
- field.convert = value.convert;
- }
-
- if (Ext.isDefined(value.calculate)) {
- field.calculate = value.calculate;
- }
-
- if (Ext.isDefined(value.defaultValue)) {
- field.defaultValue = value.defaultValue;
- }
-
- fields.push(field);
- fieldNames.push(key);
- });
-
- Ext.define('PVEResources', {
- extend: 'Ext.data.Model',
- fields: fields,
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/resources',
- },
- });
-
- Ext.define('PVETree', {
- extend: 'Ext.data.Model',
- fields: fields,
- proxy: { type: 'memory' },
- });
-
- Ext.apply(config, {
- storeid: 'PVEResources',
- model: 'PVEResources',
- defaultColumns: function () {
- let res = [];
- Ext.Object.each(field_defaults, function (field, info) {
- let fieldInfo = Ext.apply({ dataIndex: field }, info);
- res.push(fieldInfo);
- });
- return res;
- },
- fieldNames: fieldNames,
- });
-
- me.callParent([config]);
-
- me.on('beforeload', me.clearCache, me);
- },
-});
-Ext.define('pve-rrd-node', {
- extend: 'Ext.data.Model',
- fields: [
- {
- name: 'cpu',
- // percentage
- convert: function (value) {
- return value * 100;
- },
- },
- {
- name: 'iowait',
- // percentage
- convert: function (value) {
- return value * 100;
- },
- },
- 'loadavg',
- 'maxcpu',
- 'memtotal',
- 'memused',
- 'netin',
- 'netout',
- 'roottotal',
- 'rootused',
- 'swaptotal',
- 'swapused',
- 'memavailable',
- 'arcsize',
- 'pressurecpusome',
- 'pressureiosome',
- 'pressureiofull',
- 'pressurememorysome',
- 'pressurememoryfull',
- { type: 'date', dateFormat: 'timestamp', name: 'time' },
- ],
-});
-
-Ext.define('pve-rrd-guest', {
- extend: 'Ext.data.Model',
- fields: [
- {
- name: 'cpu',
- // percentage
- convert: function (value) {
- return value * 100;
- },
- },
- 'maxcpu',
- 'netin',
- 'netout',
- { name: 'mem', defaultValue: null },
- 'maxmem',
- 'disk',
- 'maxdisk',
- 'diskread',
- 'diskwrite',
- 'memhost',
- 'pressurecpusome',
- 'pressurecpufull',
- 'pressureiosome',
- 'pressurecpufull',
- 'pressurememorysome',
- 'pressurememoryfull',
- { type: 'date', dateFormat: 'timestamp', name: 'time' },
- ],
-});
-
-Ext.define('pve-rrd-storage', {
- extend: 'Ext.data.Model',
- fields: ['used', 'total', { type: 'date', dateFormat: 'timestamp', name: 'time' }],
-});
-// This is a container intended to show a field on the first column and one on the second column.
-// One can set a ratio for the field sizes.
-//
-// Works around a limitation of our input panel column1/2 handling that entries are not vertically
-// aligned when one of them has wrapping text (like it happens sometimes with such longer
-// descriptions)
-Ext.define('PVE.container.TwoColumnContainer', {
- extend: 'Ext.container.Container',
- alias: 'widget.pveTwoColumnContainer',
-
- layout: {
- type: 'hbox',
- align: 'begin',
- },
-
- // The default ratio of the start widget. It an be an integer or a floating point number
- startFlex: 1,
-
- // The default ratio of the end widget. It an be an integer or a floating point number
- endFlex: 1,
-
- // the padding between the two columns
- columnPadding: 20,
-
- // the config of the first widget
- startColumn: undefined,
-
- // the config of the second widget
- endColumn: undefined,
-
- // same as fields in a panel
- padding: '0 0 5 0',
-
- initComponent: function () {
- let me = this;
-
- if (!me.startColumn) {
- throw 'no start widget configured';
- }
- if (!me.endColumn) {
- throw 'no end widget configured';
- }
-
- Ext.apply(me, {
- items: [
- Ext.applyIf({ flex: me.startFlex }, me.startColumn),
- {
- xtype: 'box',
- width: me.columnPadding,
- },
- Ext.applyIf({ flex: me.endFlex }, me.endColumn),
- ],
- });
-
- me.callParent();
- },
-});
-Ext.define('pve-acme-challenges', {
- extend: 'Ext.data.Model',
- fields: ['id', 'type', 'schema'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/acme/challenge-schema',
- },
- idProperty: 'id',
-});
-
-Ext.define('PVE.form.ACMEApiSelector', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveACMEApiSelector',
-
- fieldLabel: gettext('DNS API'),
- displayField: 'name',
- valueField: 'id',
-
- store: {
- model: 'pve-acme-challenges',
- autoLoad: true,
- },
-
- triggerAction: 'all',
- queryMode: 'local',
- allowBlank: false,
- editable: true,
- forceSelection: true,
- anyMatch: true,
- selectOnFocus: true,
-
- getSchema: function () {
- let me = this;
- let val = me.getValue();
- if (val) {
- let record = me.getStore().findRecord('id', val, 0, false, true, true);
- if (record) {
- return record.data.schema;
- }
- }
- return {};
- },
-});
-Ext.define('PVE.form.ACMEAccountSelector', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveACMEAccountSelector',
-
- displayField: 'name',
- valueField: 'name',
-
- store: {
- model: 'pve-acme-accounts',
- autoLoad: true,
- },
-
- triggerAction: 'all',
- queryMode: 'local',
- allowBlank: false,
- editable: false,
- forceSelection: true,
-
- isEmpty: function () {
- return this.getStore().getData().length === 0;
- },
-});
-Ext.define('PVE.form.ACMEPluginSelector', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveACMEPluginSelector',
-
- fieldLabel: gettext('Plugin'),
- displayField: 'plugin',
- valueField: 'plugin',
-
- store: {
- model: 'pve-acme-plugins',
- autoLoad: true,
- filters: (item) => item.data.type === 'dns',
- },
-
- triggerAction: 'all',
- queryMode: 'local',
- allowBlank: false,
- editable: false,
-});
-Ext.define('PVE.form.AgentFeatureSelector', {
- extend: 'Proxmox.panel.InputPanel',
- alias: ['widget.pveAgentFeatureSelector'],
-
- viewModel: {},
-
- items: [
- {
- xtype: 'proxmoxcheckbox',
- boxLabel: Ext.String.format(gettext('Use {0}'), 'QEMU Guest Agent'),
- name: 'enabled',
- reference: 'enabled',
- uncheckedValue: 0,
- },
- {
- xtype: 'proxmoxcheckbox',
- boxLabel: gettext('Run guest-trim after a disk move or VM migration'),
- name: 'fstrim_cloned_disks',
- bind: {
- disabled: '{!enabled.checked}',
- },
- disabled: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- boxLabel: gettext('Freeze/thaw guest filesystems on backup for consistency'),
- name: 'freeze-fs-on-backup',
- reference: 'freeze_fs_on_backup',
- bind: {
- disabled: '{!enabled.checked}',
- },
- disabled: true,
- uncheckedValue: '0',
- defaultValue: '1',
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- 'Freeze/thaw for guest filesystems disabled. This can lead to inconsistent disk backups.',
- ),
- bind: {
- hidden: '{freeze_fs_on_backup.checked}',
- },
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext('Make sure the QEMU Guest Agent is installed in the VM'),
- bind: {
- hidden: '{!enabled.checked}',
- },
- },
- ],
-
- advancedItems: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'type',
- value: '__default__',
- deleteEmpty: false,
- fieldLabel: 'Type',
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (VirtIO)'],
- ['virtio', 'VirtIO'],
- ['isa', 'ISA'],
- ],
- },
- ],
-
- onGetValues: function (values) {
- if (PVE.Parser.parseBoolean(values['freeze-fs-on-backup'])) {
- delete values['freeze-fs-on-backup'];
- }
-
- const agentstr = PVE.Parser.printPropertyString(values, 'enabled');
- return { agent: agentstr };
- },
-
- setValues: function (values) {
- let res = PVE.Parser.parsePropertyString(values.agent, 'enabled');
- if (!Ext.isDefined(res['freeze-fs-on-backup'])) {
- res['freeze-fs-on-backup'] = 1;
- }
-
- this.callParent([res]);
- },
-});
-Ext.define('PVE.form.BackupCompressionSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveBackupCompressionSelector'],
- comboItems: [
- ['0', Proxmox.Utils.noneText],
- ['lzo', 'LZO (' + gettext('fast') + ')'],
- ['gzip', 'GZIP (' + gettext('good') + ')'],
- ['zstd', 'ZSTD (' + gettext('fast and good') + ')'],
- ],
-});
-Ext.define('PVE.form.BackupModeSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveBackupModeSelector'],
- comboItems: [
- ['snapshot', gettext('Snapshot')],
- ['suspend', gettext('Suspend')],
- ['stop', gettext('Stop')],
- ],
-});
-Ext.define('PVE.form.SizeField', {
- extend: 'Ext.form.FieldContainer',
- alias: 'widget.pveSizeField',
-
- mixins: ['Proxmox.Mixin.CBind'],
-
- viewModel: {
- data: {
- unit: 'MiB',
- unitPostfix: '',
- },
- formulas: {
- unitlabel: (get) => get('unit') + get('unitPostfix'),
- },
- },
-
- emptyText: '',
-
- layout: 'hbox',
- defaults: {
- hideLabel: true,
- },
-
- units: {
- B: 1,
- KiB: 1024,
- MiB: 1024 * 1024,
- GiB: 1024 * 1024 * 1024,
- TiB: 1024 * 1024 * 1024 * 1024,
- KB: 1000,
- MB: 1000 * 1000,
- GB: 1000 * 1000 * 1000,
- TB: 1000 * 1000 * 1000 * 1000,
- },
-
- // display unit (TODO: make (optionally) selectable)
- unit: 'MiB',
- unitPostfix: '',
-
- // use this if the backend saves values in a unit other than bytes, e.g.,
- // for KiB set it to 'KiB'
- backendUnit: undefined,
-
- // allow setting 0 and using it as a submit value
- allowZero: false,
-
- emptyValue: null,
-
- items: [
- {
- xtype: 'numberfield',
- cbind: {
- name: '{name}',
- emptyText: '{emptyText}',
- allowZero: '{allowZero}',
- emptyValue: '{emptyValue}',
- },
- minValue: 0,
- step: 1,
- submitLocaleSeparator: false,
- fieldStyle: 'text-align: right',
- flex: 1,
- enableKeyEvents: true,
- setValue: function (v) {
- if (!this._transformed && v !== null) {
- let fieldContainer = this.up('fieldcontainer');
- let vm = fieldContainer.getViewModel();
- let unit = vm.get('unit');
-
- v /= fieldContainer.units[unit];
- v *= fieldContainer.backendFactor;
-
- this._transformed = true;
- }
-
- if (Number(v) === 0 && !this.allowZero) {
- v = undefined;
- }
-
- return Ext.form.field.Text.prototype.setValue.call(this, v);
- },
- getSubmitValue: function () {
- let v = this.processRawValue(this.getRawValue());
- v = v.replace(this.decimalSeparator, '.');
-
- if (v === undefined || v === '') {
- return this.emptyValue;
- }
-
- if (Number(v) === 0) {
- return this.allowZero ? 0 : null;
- }
-
- let fieldContainer = this.up('fieldcontainer');
- let vm = fieldContainer.getViewModel();
- let unit = vm.get('unit');
-
- v = parseFloat(v) * fieldContainer.units[unit];
- v /= fieldContainer.backendFactor;
-
- return String(Math.floor(v));
- },
- listeners: {
- // our setValue gets only called if we have a value, avoid
- // transformation of the first user-entered value
- keydown: function () {
- this._transformed = true;
- },
- },
- },
- {
- xtype: 'displayfield',
- name: 'unit',
- submitValue: false,
- padding: '0 0 0 10',
- bind: {
- value: '{unitlabel}',
- },
- listeners: {
- change: (f, v) => {
- f.originalValue = v;
- },
- },
- width: 40,
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- me.unit = me.unit || 'MiB';
- if (!(me.unit in me.units)) {
- throw 'unknown unit: ' + me.unit;
- }
-
- me.backendFactor = 1;
- if (me.backendUnit !== undefined) {
- if (!(me.unit in me.units)) {
- throw 'unknown backend unit: ' + me.backendUnit;
- }
- me.backendFactor = me.units[me.backendUnit];
- }
-
- me.callParent(arguments);
-
- me.getViewModel().set('unit', me.unit);
- me.getViewModel().set('unitPostfix', me.unitPostfix);
- },
-});
-
-Ext.define('PVE.form.BandwidthField', {
- extend: 'PVE.form.SizeField',
- alias: 'widget.pveBandwidthField',
-
- unitPostfix: '/s',
-});
-Ext.define('PVE.form.BridgeSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.PVE.form.BridgeSelector'],
-
- bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge
-
- store: {
- fields: ['iface', 'active', 'type'],
- filterOnLoad: true,
- sorters: [
- {
- property: 'iface',
- direction: 'ASC',
- },
- ],
- },
- valueField: 'iface',
- displayField: 'iface',
- listConfig: {
- columns: [
- {
- header: gettext('Bridge'),
- dataIndex: 'iface',
- hideable: false,
- width: 100,
- },
- {
- header: gettext('Active'),
- width: 60,
- dataIndex: 'active',
- renderer: Proxmox.Utils.format_boolean,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comments',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- ],
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- me.store.setProxy({
- type: 'proxmox',
- url: '/api2/json/nodes/' + me.nodename + '/network?type=' + me.bridgeType,
- });
-
- me.store.load();
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.nodename;
- me.nodename = undefined;
-
- me.callParent();
-
- me.setNodename(nodename);
- },
-});
-Ext.define('PVE.form.BusTypeSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: 'widget.pveBusSelector',
-
- withVirtIO: true,
- withUnused: false,
-
- initComponent: function () {
- var me = this;
-
- me.comboItems = [
- ['ide', 'IDE'],
- ['sata', 'SATA'],
- ];
-
- if (me.withVirtIO) {
- me.comboItems.push(['virtio', 'VirtIO Block']);
- }
-
- me.comboItems.push(['scsi', 'SCSI']);
-
- if (me.withUnused) {
- me.comboItems.push(['unused', 'Unused']);
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.data.CPUModel', {
- extend: 'Ext.data.Model',
- fields: [{ name: 'name' }, { name: 'vendor' }, { name: 'custom' }, { name: 'displayname' }],
-});
-
-Ext.define('PVE.form.CPUModelSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.CPUModelSelector'],
-
- valueField: 'name',
- displayField: 'displayname',
-
- emptyText: Proxmox.Utils.defaultText + ' (kvm64)',
- allowBlank: true,
-
- editable: true,
- anyMatch: true,
- forceSelection: true,
- autoSelect: false,
-
- deleteEmpty: true,
-
- listConfig: {
- columns: [
- {
- header: gettext('Model'),
- dataIndex: 'displayname',
- hideable: false,
- sortable: true,
- flex: 3,
- },
- {
- header: gettext('Vendor'),
- dataIndex: 'vendor',
- hideable: false,
- sortable: true,
- flex: 2,
- },
- ],
- width: 360,
- },
-
- store: {
- autoLoad: true,
- model: 'PVE.data.CPUModel',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/localhost/capabilities/qemu/cpu',
- },
- sorters: [
- {
- sorterFn: function (recordA, recordB) {
- let a = recordA.data;
- let b = recordB.data;
-
- let vendorOrder = PVE.Utils.cpu_vendor_order;
- let orderA = vendorOrder[a.vendor] || vendorOrder._default_;
- let orderB = vendorOrder[b.vendor] || vendorOrder._default_;
-
- if (orderA > orderB) {
- return 1;
- } else if (orderA < orderB) {
- return -1;
- }
-
- // Within same vendor, sort alphabetically
- return a.name.localeCompare(b.name);
- },
- direction: 'ASC',
- },
- ],
- listeners: {
- load: function (store, records, success) {
- if (success) {
- records.forEach((rec) => {
- rec.data.displayname = rec.data.name.replace(/^custom-/, '');
-
- let vendor = rec.data.vendor;
-
- if (rec.data.name === 'host') {
- vendor = 'Host';
- }
-
- // We receive vendor names as given to QEMU as CPUID
- vendor = PVE.Utils.cpu_vendor_map[vendor] || vendor;
-
- if (rec.data.custom) {
- vendor = gettext('Custom') + ` (${vendor})`;
- }
-
- rec.data.vendor = vendor;
- });
-
- store.sort();
- }
- },
- },
- },
-});
-Ext.define('PVE.form.CacheTypeSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.CacheTypeSelector'],
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (' + gettext('No cache') + ')'],
- ['directsync', 'Direct sync'],
- ['writethrough', 'Write through'],
- ['writeback', 'Write back'],
- ['unsafe', 'Write back (' + gettext('unsafe') + ')'],
- ['none', gettext('No cache')],
- ],
-});
-Ext.define('PVE.form.CalendarEvent', {
- extend: 'Ext.form.field.ComboBox',
- xtype: 'pveCalendarEvent',
-
- editable: true,
- emptyText: gettext('Editable'), // FIXME: better way to convey that to confused users?
-
- valueField: 'value',
- queryMode: 'local',
-
- matchFieldWidth: false,
- listConfig: {
- maxWidth: 450,
- },
-
- store: {
- field: ['value', 'text'],
- data: [
- { value: '*/30', text: Ext.String.format(gettext('Every {0} minutes'), 30) },
- { value: '*/2:00', text: gettext('Every two hours') },
- { value: '21:00', text: gettext('Every day') + ' 21:00' },
- { value: '2,22:30', text: gettext('Every day') + ' 02:30, 22:30' },
- { value: 'mon..fri 00:00', text: gettext('Monday to Friday') + ' 00:00' },
- {
- value: 'mon..fri */1:00',
- text: gettext('Monday to Friday') + ': ' + gettext('hourly'),
- },
- {
- value: 'mon..fri 7..18:00/15',
- text:
- gettext('Monday to Friday') +
- ', ' +
- Ext.String.format(gettext('{0} to {1}'), '07:00', '18:45') +
- ': ' +
- Ext.String.format(gettext('Every {0} minutes'), 15),
- },
- { value: 'sun 01:00', text: gettext('Sunday') + ' 01:00' },
- { value: 'monthly', text: gettext('Every first day of the Month') + ' 00:00' },
- { value: 'sat *-1..7 15:00', text: gettext('First Saturday each month') + ' 15:00' },
- { value: 'yearly', text: gettext('First day of the year') + ' 00:00' },
- ],
- },
-
- tpl: [
- '',
- ],
-
- displayTpl: ['', '{value}', ' '],
-});
-Ext.define('PVE.form.CephPoolSelector', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveCephPoolSelector',
-
- allowBlank: false,
- valueField: 'pool_name',
- displayField: 'pool_name',
- listConfig: {
- itemTpl: '{pool_name:htmlEncode}',
- },
- editable: false,
- queryMode: 'local',
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- let onlyRBDPools = ({ data }) =>
- !data?.application_metadata || !!data?.application_metadata?.rbd;
-
- var store = Ext.create('Ext.data.Store', {
- fields: ['name'],
- sorters: 'name',
- filters: [onlyRBDPools],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/' + me.nodename + '/ceph/pool',
- },
- });
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
-
- store.load({
- callback: function (rec, op, success) {
- let filteredRec = rec.filter(onlyRBDPools);
-
- if (success && filteredRec.length > 0) {
- me.select(filteredRec[0]);
- }
- },
- });
- },
-});
-Ext.define('PVE.form.CephFSSelector', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveCephFSSelector',
-
- allowBlank: false,
- valueField: 'name',
- displayField: 'name',
- editable: false,
- queryMode: 'local',
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- var store = Ext.create('Ext.data.Store', {
- fields: ['name'],
- sorters: 'name',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/' + me.nodename + '/ceph/fs',
- },
- });
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
-
- store.load({
- callback: function (rec, op, success) {
- if (success && rec.length > 0) {
- me.select(rec[0]);
- }
- },
- });
- },
-});
-Ext.define('PVE.form.ComboBoxSetStoreNode', {
- extend: 'Proxmox.form.ComboGrid',
- config: {
- apiBaseUrl: '/api2/json/nodes/',
- apiSuffix: '',
- },
-
- showNodeSelector: false,
-
- setNodeName: function (value) {
- let me = this;
- value ||= Proxmox.NodeName;
-
- me.getStore().getProxy().setUrl(`${me.apiBaseUrl}${value}${me.apiSuffix}`);
- me.clearValue();
- },
-
- nodeChange: function (_field, value) {
- let me = this;
- // disable autoSelect if there is already a selection or we have the picker open
- if (me.getValue() || me.isExpanded) {
- let autoSelect = me.autoSelect;
- me.autoSelect = false;
- me.store.on(
- 'afterload',
- function () {
- me.autoSelect = autoSelect;
- },
- { single: true },
- );
- }
- me.setNodeName(value);
- me.fireEvent('nodechanged', value);
- },
-
- tbarMouseDown: function () {
- this.topBarMousePress = true;
- },
-
- tbarMouseUp: function () {
- let me = this;
- delete this.topBarMousePress;
- if (me.focusLeft) {
- me.focus();
- delete me.focusLeft;
- }
- },
-
- // conditionally prevent the focusLeave handler to continue, preventing collapsing of the picker
- onFocusLeave: function () {
- let me = this;
- me.focusLeft = true;
- if (!me.topBarMousePress) {
- me.callParent(arguments);
- }
-
- return undefined;
- },
-
- initComponent: function () {
- let me = this;
-
- if (me.showNodeSelector && !PVE.Utils.isStandaloneNode()) {
- me.errorHeight = 140;
- Ext.apply(me.listConfig ?? {}, {
- tbar: {
- xtype: 'toolbar',
- minHeight: 40,
- listeners: {
- mousedown: me.tbarMouseDown,
- mouseup: me.tbarMouseUp,
- element: 'el',
- scope: me,
- },
- items: [
- {
- xtype: 'pveStorageScanNodeSelector',
- autoSelect: false,
- fieldLabel: gettext('Node to scan'),
- listeners: {
- change: (field, value) => me.nodeChange(field, value),
- },
- },
- ],
- },
- emptyText: me.listConfig?.emptyText ?? gettext('Nothing found'),
- });
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.form.ContentTypeSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveContentTypeSelector'],
-
- cts: undefined,
-
- initComponent: function () {
- var me = this;
-
- me.comboItems = [];
-
- if (me.cts === undefined) {
- me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets', 'import'];
- }
-
- Ext.Array.each(me.cts, function (ct) {
- me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]);
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.form.ControllerSelector', {
- extend: 'Ext.form.FieldContainer',
- alias: 'widget.pveControllerSelector',
-
- withVirtIO: true,
- withUnused: false,
-
- vmconfig: {}, // used to check for existing devices
-
- setToFree: function (controllers, busField, deviceIDField) {
- let me = this;
- let freeId = PVE.Utils.nextFreeDisk(controllers, me.vmconfig);
-
- if (freeId !== undefined) {
- busField?.setValue(freeId.controller);
- deviceIDField.setValue(freeId.id);
- }
- },
-
- updateVMConfig: function (vmconfig) {
- let me = this;
- me.vmconfig = Ext.apply({}, vmconfig);
-
- me.down('field[name=deviceid]').validate();
- },
-
- setVMConfig: function (vmconfig, autoSelect) {
- let me = this;
-
- me.vmconfig = Ext.apply({}, vmconfig);
-
- let bussel = me.down('field[name=controller]');
- let deviceid = me.down('field[name=deviceid]');
-
- let clist;
- if (autoSelect === 'cdrom') {
- if (!Ext.isDefined(me.vmconfig.ide2)) {
- bussel.setValue('ide');
- deviceid.setValue(2);
- return;
- }
- clist = ['ide', 'scsi', 'sata'];
- } else {
- // in most cases we want to add a disk to the same controller we previously used
- clist = PVE.Utils.sortByPreviousUsage(me.vmconfig);
- }
-
- me.setToFree(clist, bussel, deviceid);
-
- deviceid.validate();
- },
-
- getConfId: function () {
- let me = this;
- let controller = me.getComponent('controller').getValue() || 'ide';
- let id = me.getComponent('deviceid').getValue() || 0;
-
- return `${controller}${id}`;
- },
-
- initComponent: function () {
- let me = this;
-
- Ext.apply(me, {
- fieldLabel: gettext('Bus/Device'),
- layout: 'hbox',
- defaults: {
- hideLabel: true,
- },
- items: [
- {
- xtype: 'pveBusSelector',
- name: 'controller',
- itemId: 'controller',
- value: PVE.qemu.OSDefaults.generic.busType,
- withVirtIO: me.withVirtIO,
- withUnused: me.withUnused,
- allowBlank: false,
- flex: 2,
- listeners: {
- change: function (t, value) {
- if (!value) {
- return;
- }
- let field = me.down('field[name=deviceid]');
- me.setToFree([value], undefined, field);
- field.setMaxValue(PVE.Utils.diskControllerMaxIDs[value] - 1);
- field.validate();
- },
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'deviceid',
- itemId: 'deviceid',
- minValue: 0,
- maxValue: PVE.Utils.diskControllerMaxIDs.ide - 1,
- value: '0',
- flex: 1,
- allowBlank: false,
- validator: function (value) {
- if (!me.rendered) {
- return undefined;
- }
- let controller = me.down('field[name=controller]').getValue();
- let confid = controller + value;
- if (Ext.isDefined(me.vmconfig[confid])) {
- return 'This device is already in use.';
- }
- return true;
- },
- },
- ],
- });
-
- me.callParent();
-
- if (me.selectFree) {
- me.setVMConfig(me.vmconfig);
- }
- },
-});
-Ext.define('PVE.form.DayOfWeekSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveDayOfWeekSelector'],
- comboItems: [],
- initComponent: function () {
- var me = this;
- me.comboItems = [
- ['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])],
- ['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])],
- ['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])],
- ['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])],
- ['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])],
- ['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])],
- ['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])],
- ];
- this.callParent();
- },
-});
-Ext.define('PVE.form.DirMapSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: 'widget.pveDirMapSelector',
-
- store: {
- fields: ['name', 'path'],
- filterOnLoad: true,
- sorters: [
- {
- property: 'id',
- direction: 'ASC',
- },
- ],
- },
-
- allowBlank: false,
- autoSelect: false,
- displayField: 'id',
- valueField: 'id',
-
- listConfig: {
- columns: [
- {
- header: gettext('Directory ID'),
- dataIndex: 'id',
- flex: 1,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'description',
- flex: 1,
- },
- ],
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- me.store.setProxy({
- type: 'proxmox',
- url: `/api2/json/cluster/mapping/dir?check-node=${nodename}`,
- });
-
- me.store.load();
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.nodename;
- me.nodename = undefined;
-
- me.callParent();
-
- me.setNodename(nodename);
- },
-});
-Ext.define('PVE.form.DiskFormatSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: 'widget.pveDiskFormatSelector',
- comboItems: [
- ['raw', gettext('Raw disk image') + ' (raw)'],
- ['qcow2', gettext('QEMU image format') + ' (qcow2)'],
- ['vmdk', gettext('VMware image format') + ' (vmdk)'],
- ],
-});
-Ext.define('PVE.form.DiskStorageSelector', {
- extend: 'Ext.container.Container',
- alias: 'widget.pveDiskStorageSelector',
-
- layout: 'fit',
- defaults: {
- margin: '0 0 5 0',
- },
-
- // the fieldLabel for the storageselector
- storageLabel: gettext('Storage'),
-
- // the content to show (e.g., images or rootdir)
- storageContent: undefined,
-
- // if true, selects the first available storage
- autoSelect: false,
-
- allowBlank: false,
- emptyText: '',
-
- // hides the selection field
- // this is always hidden on creation,
- // and only shown when the storage needs a selection and
- // hideSelection is not true
- hideSelection: undefined,
-
- // hides the size field (e.g, for the efi disk dialog)
- hideSize: false,
-
- // hides the format field
- hideFormat: false,
-
- // sets the initial size value
- // string because else we get a type confusion
- defaultSize: '32',
-
- changeStorage: function (f, value) {
- var me = this;
- var formatsel = me.getComponent('diskformat');
- var hdfilesel = me.getComponent('hdimage');
- var hdsizesel = me.getComponent('disksize');
-
- // initial store load, and reset/deletion of the storage
- if (!value) {
- hdfilesel.setDisabled(true);
- hdfilesel.setVisible(false);
-
- formatsel.setDisabled(true);
- return;
- }
-
- var rec = f.store.getById(value);
- // if the storage is not defined, or valid,
- // we cannot know what to enable/disable
- if (!rec) {
- return;
- }
-
- let validFormats = {};
- let defaultFormat = 'raw';
- let selectFormat = defaultFormat;
- if (rec.data.formats) {
- for (const format of rec.data.formats.supported) {
- validFormats[format] = true;
- }
- defaultFormat = rec.data.formats.default;
- } else if (rec.data.format) {
- // legacy api, just for compatibility
- // 0 is the formats, 1 the default in the backend
- validFormats = rec.data.format[0];
- defaultFormat = rec.data.format[1];
- }
-
- if (Object.keys(validFormats).length > 0) {
- delete validFormats.subvol; // we never need subvol in the gui
- if (validFormats.qcow2) {
- selectFormat = 'qcow2';
- } else if (validFormats.raw) {
- selectFormat = 'raw';
- } else {
- selectFormat = defaultFormat;
- }
- }
-
- var select = !!rec.data.select_existing && !me.hideSelection;
-
- let numberOfValidFormats = Ext.Object.getValues(validFormats).filter(
- (valid) => !!valid,
- ).length;
- formatsel.setDisabled(me.hideFormat || numberOfValidFormats <= 1);
- formatsel.setValue(selectFormat);
-
- hdfilesel.setDisabled(!select);
- hdfilesel.setVisible(select);
- if (select) {
- hdfilesel.setStorage(value);
- }
-
- hdsizesel.setDisabled(select || me.hideSize);
- hdsizesel.setVisible(!select && !me.hideSize);
- },
-
- setNodename: function (nodename) {
- var me = this;
- var hdstorage = me.getComponent('hdstorage');
- var hdfilesel = me.getComponent('hdimage');
-
- hdstorage.setNodename(nodename);
- hdfilesel.setNodename(nodename);
- },
-
- setDisabled: function (value) {
- var me = this;
- var hdstorage = me.getComponent('hdstorage');
-
- // reset on disable
- if (value) {
- hdstorage.setValue();
- }
- hdstorage.setDisabled(value);
-
- // disabling does not always fire this event and we do not need
- // the value of the validity
- hdstorage.fireEvent('validitychange');
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: 'pveStorageSelector',
- itemId: 'hdstorage',
- name: 'hdstorage',
- fieldLabel: me.storageLabel,
- nodename: me.nodename,
- storageContent: me.storageContent,
- disabled: me.disabled,
- autoSelect: me.autoSelect,
- allowBlank: me.allowBlank,
- emptyText: me.emptyText,
- listeners: {
- change: {
- fn: me.changeStorage,
- scope: me,
- },
- },
- },
- {
- xtype: 'pveFileSelector',
- name: 'hdimage',
- itemId: 'hdimage',
- fieldLabel: gettext('Disk image'),
- nodename: me.nodename,
- disabled: true,
- hidden: true,
- },
- {
- xtype: 'numberfield',
- itemId: 'disksize',
- name: 'disksize',
- fieldLabel: `${gettext('Disk size')} (${gettext('GiB')})`,
- hidden: me.hideSize,
- disabled: me.hideSize,
- minValue: 0.001,
- maxValue: 128 * 1024,
- decimalPrecision: 3,
- value: me.defaultSize,
- allowBlank: false,
- },
- {
- xtype: 'pveDiskFormatSelector',
- itemId: 'diskformat',
- name: 'diskformat',
- fieldLabel: gettext('Format'),
- nodename: me.nodename,
- disabled: true,
- hidden: me.hideFormat || me.storageContent === 'rootdir',
- value: 'qcow2',
- allowBlank: false,
- },
- ];
-
- // use it to disable the children but not ourself
- me.disabled = false;
-
- me.callParent();
- },
-});
-Ext.define('PVE.form.FileSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: 'widget.pveFileSelector',
-
- editable: true,
- anyMatch: true,
- forceSelection: true,
-
- listeners: {
- afterrender: function () {
- var me = this;
- if (!me.disabled) {
- me.setStorage(me.storage, me.nodename);
- }
- },
- },
-
- setStorage: function (storage, nodename) {
- var me = this;
-
- var change = false;
- if (storage && me.storage !== storage) {
- me.storage = storage;
- change = true;
- }
-
- if (nodename && me.nodename !== nodename) {
- me.nodename = nodename;
- change = true;
- }
-
- if (!(me.storage && me.nodename && change)) {
- return;
- }
-
- var url = '/api2/json/nodes/' + me.nodename + '/storage/' + me.storage + '/content';
- if (me.storageContent) {
- url += '?content=' + me.storageContent;
- }
-
- me.store.setProxy({
- type: 'proxmox',
- url: url,
- });
-
- if (Ext.isFunction(me.filter)) {
- me.store.clearFilter();
- me.store.addFilter([me.filter]);
- } else {
- me.store.clearFilter();
- }
-
- me.store.removeAll();
- me.store.load();
- },
-
- setNodename: function (nodename) {
- this.setStorage(undefined, nodename);
- },
-
- store: {
- model: 'pve-storage-content',
- },
-
- allowBlank: false,
- autoSelect: false,
- valueField: 'volid',
- displayField: 'text',
-
- // An optional filter function
- filter: undefined,
-
- listConfig: {
- width: 600,
- columns: [
- {
- header: gettext('Name'),
- dataIndex: 'text',
- hideable: false,
- flex: 1,
- },
- {
- header: gettext('Format'),
- width: 60,
- dataIndex: 'format',
- },
- {
- header: gettext('Size'),
- width: 100,
- dataIndex: 'size',
- renderer: Proxmox.Utils.format_size,
- },
- ],
- },
-});
-Ext.define('PVE.form.FirewallPolicySelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveFirewallPolicySelector'],
- comboItems: [
- ['ACCEPT', 'ACCEPT'],
- ['REJECT', 'REJECT'],
- ['DROP', 'DROP'],
- ],
-});
-/*
- * This is a global search field it loads the /cluster/resources on focus and displays the
- * result in a floating grid. Filtering and sorting is done in the customFilter function
- *
- * Accepts key up/down and enter for input, and it opens to CTRL+SHIFT+F and CTRL+SPACE
- */
-Ext.define('PVE.form.GlobalSearchField', {
- extend: 'Ext.form.field.Text',
- alias: 'widget.pveGlobalSearchField',
-
- emptyText: gettext('Search'),
- enableKeyEvents: true,
- selectOnFocus: true,
- padding: '0 5 0 5',
-
- grid: {
- xtype: 'gridpanel',
- userCls: 'proxmox-tags-full',
- focusOnToFront: false,
- floating: true,
- emptyText: Proxmox.Utils.noneText,
- width: 600,
- height: 400,
- scrollable: {
- xtype: 'scroller',
- y: true,
- x: true,
- },
- store: {
- model: 'PVEResources',
- proxy: {
- type: 'proxmox',
- url: '/api2/extjs/cluster/resources',
- },
- },
- plugins: {
- ptype: 'bufferedrenderer',
- trailingBufferZone: 20,
- leadingBufferZone: 20,
- },
-
- hideMe: function () {
- var me = this;
- if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
- return;
- }
- me.hasFocus = false;
- if (!me.textfield.hasFocus) {
- me.hide();
- }
- },
-
- setFocus: function () {
- var me = this;
- me.hasFocus = true;
- },
-
- listeners: {
- rowclick: function (grid, record) {
- var me = this;
- me.textfield.selectAndHide(record.id);
- },
- itemcontextmenu: function (v, record, item, index, event) {
- var me = this;
- me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
- },
- focusleave: 'hideMe',
- focusenter: 'setFocus',
- },
-
- columns: [
- {
- text: gettext('Type'),
- dataIndex: 'type',
- width: 100,
- renderer: PVE.Utils.render_resource_type,
- },
- {
- text: gettext('Description'),
- flex: 1,
- dataIndex: 'text',
- renderer: function (value, mD, rec) {
- let overrides = PVE.UIOptions.tagOverrides;
- let tags = PVE.Utils.renderTags(rec.data.tags, overrides);
- return `${value}${tags}`;
- },
- },
- {
- text: gettext('Node'),
- dataIndex: 'node',
- },
- {
- text: gettext('Pool'),
- dataIndex: 'pool',
- },
- ],
- },
-
- customFilter: function (item) {
- let me = this;
-
- if (me.filterVal === '') {
- item.data.relevance = 0;
- return true;
- }
- // different types have different fields to search, e.g., a node will never have a pool
- const fieldMap = {
- pool: ['type', 'pool', 'text'],
- node: ['type', 'node', 'text'],
- storage: ['type', 'pool', 'node', 'storage'],
- default: ['name', 'type', 'node', 'pool', 'vmid'],
- };
- let fields = fieldMap[item.data.type] || fieldMap.default;
- let fieldArr = fields.map((field) => item.data[field]?.toString().toLowerCase());
- if (item.data.tags) {
- let tags = item.data.tags.split(/[;, ]/);
- fieldArr.push(...tags);
- }
-
- let filterWords = me.filterVal.split(/\s+/);
-
- // all text is case insensitive and each split-out word is searched for separately.
- // a row gets 1 point for every partial match, and and additional point for every exact match
- let match = 0;
- for (let fieldValue of fieldArr) {
- if (fieldValue === undefined || fieldValue === '') {
- continue;
- }
- for (let filterWord of filterWords) {
- if (fieldValue.indexOf(filterWord) !== -1) {
- match++; // partial match
- if (fieldValue === filterWord) {
- match++; // exact match is worth more
- }
- }
- }
- }
- item.data.relevance = match; // set the row's virtual 'relevance' value for ordering
- return match > 0;
- },
-
- updateFilter: function (field, newValue, oldValue) {
- let me = this;
- // parse input and filter store, show grid
- me.grid.store.filterVal = newValue.toLowerCase().trim();
- me.grid.store.clearFilter(true);
- me.grid.store.filterBy(me.customFilter);
- me.grid.getSelectionModel().select(0);
- },
-
- selectAndHide: function (id) {
- var me = this;
- me.tree.selectById(id);
- me.grid.hide();
- me.setValue('');
- me.blur();
- },
-
- onKey: function (field, e) {
- var me = this;
- var key = e.getKey();
-
- switch (key) {
- case Ext.event.Event.ENTER:
- // go to first entry if there is one
- if (me.grid.store.getCount() > 0) {
- me.selectAndHide(me.grid.getSelection()[0].data.id);
- }
- break;
- case Ext.event.Event.UP:
- me.grid.getSelectionModel().selectPrevious();
- break;
- case Ext.event.Event.DOWN:
- me.grid.getSelectionModel().selectNext();
- break;
- case Ext.event.Event.ESC:
- me.grid.hide();
- me.blur();
- break;
- }
- },
-
- loadValues: function (field) {
- let me = this;
- me.hasFocus = true;
- me.grid.textfield = me;
- me.grid.store.load();
- me.grid.showBy(me, 'tl-bl');
- },
-
- hideGrid: function () {
- let me = this;
- me.hasFocus = false;
- if (!me.grid.hasFocus) {
- me.grid.hide();
- }
- },
-
- listeners: {
- change: {
- fn: 'updateFilter',
- buffer: 250,
- },
- specialkey: 'onKey',
- focusenter: 'loadValues',
- focusleave: {
- fn: 'hideGrid',
- delay: 100,
- },
- },
-
- toggleFocus: function () {
- let me = this;
- if (!me.hasFocus) {
- me.focus();
- } else {
- me.blur();
- }
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.tree) {
- throw 'no tree given';
- }
-
- me.grid = Ext.create(me.grid);
-
- me.callParent();
-
- // bind CTRL + SHIFT + F and CTRL + SPACE to open/close the search
- me.keymap = new Ext.KeyMap({
- target: Ext.get(document),
- binding: [
- {
- key: 'F',
- ctrl: true,
- shift: true,
- fn: me.toggleFocus,
- scope: me,
- },
- {
- key: ' ',
- ctrl: true,
- fn: me.toggleFocus,
- scope: me,
- },
- ],
- });
-
- // always select first item and sort by relevance after load
- me.mon(me.grid.store, 'load', function () {
- me.grid.getSelectionModel().select(0);
- me.grid.store.sort({
- property: 'relevance',
- direction: 'DESC',
- });
- });
- },
-});
-Ext.define('pve-groups', {
- extend: 'Ext.data.Model',
- fields: ['groupid', 'comment', 'users'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/access/groups',
- },
- idProperty: 'groupid',
-});
-
-Ext.define('PVE.form.GroupSelector', {
- extend: 'Proxmox.form.ComboGrid',
- xtype: 'pveGroupSelector',
-
- editable: true,
- anyMatch: true,
- forceSelection: true,
-
- allowBlank: false,
- autoSelect: false,
- valueField: 'groupid',
- displayField: 'groupid',
- listConfig: {
- columns: [
- {
- header: gettext('Group'),
- sortable: true,
- dataIndex: 'groupid',
- flex: 1,
- },
- {
- header: gettext('Comment'),
- sortable: false,
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- {
- header: gettext('Users'),
- sortable: false,
- dataIndex: 'users',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- ],
- },
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-groups',
- sorters: [
- {
- property: 'groupid',
- },
- ],
- });
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
-
- store.load();
- },
-});
-Ext.define('PVE.form.GuestIDSelector', {
- extend: 'Ext.form.field.Number',
- alias: 'widget.pveGuestIDSelector',
-
- allowBlank: false,
-
- minValue: 100,
-
- maxValue: 999999999,
-
- validateExists: undefined,
-
- loadNextFreeID: false,
-
- guestType: undefined,
-
- validator: function (value) {
- var me = this;
-
- if (!Ext.isNumeric(value) || value < me.minValue || value > me.maxValue) {
- // check is done by ExtJS
- return true;
- }
-
- if (me.validateExists === true && !me.exists) {
- return me.unknownID;
- }
-
- if (me.validateExists === false && me.exists) {
- return me.inUseID;
- }
-
- return true;
- },
-
- initComponent: function () {
- var me = this;
- var label = '{0} ID';
- var unknownID = gettext('This {0} ID does not exist');
- var inUseID = gettext('This {0} ID is already in use');
- var type = 'CT/VM';
-
- if (me.guestType === 'lxc') {
- type = 'CT';
- } else if (me.guestType === 'qemu') {
- type = 'VM';
- }
-
- me.label = Ext.String.format(label, type);
- me.unknownID = Ext.String.format(unknownID, type);
- me.inUseID = Ext.String.format(inUseID, type);
-
- Ext.apply(me, {
- fieldLabel: me.label,
- listeners: {
- change: function (field, newValue, oldValue) {
- if (!Ext.isDefined(me.validateExists)) {
- return;
- }
- Proxmox.Utils.API2Request({
- params: { vmid: newValue },
- url: '/cluster/nextid',
- method: 'GET',
- success: function (response, opts) {
- me.exists = false;
- me.validate();
- },
- failure: function (response, opts) {
- me.exists = true;
- me.validate();
- },
- });
- },
- },
- });
-
- me.callParent();
-
- if (me.loadNextFreeID) {
- Proxmox.Utils.API2Request({
- url: '/cluster/nextid',
- method: 'GET',
- success: function (response, opts) {
- me.setRawValue(response.result.data);
- },
- });
- }
- },
-});
-Ext.define('PVE.form.hashAlgorithmSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveHashAlgorithmSelector'],
- config: {
- deleteEmpty: false,
- },
- comboItems: [
- ['__default__', 'None'],
- ['md5', 'MD5'],
- ['sha1', 'SHA-1'],
- ['sha224', 'SHA-224'],
- ['sha256', 'SHA-256'],
- ['sha384', 'SHA-384'],
- ['sha512', 'SHA-512'],
- ],
-});
-Ext.define('PVE.form.HotplugFeatureSelector', {
- extend: 'Ext.form.CheckboxGroup',
- alias: 'widget.pveHotplugFeatureSelector',
-
- columns: 1,
- vertical: true,
-
- defaults: {
- name: 'hotplugCbGroup',
- submitValue: false,
- },
- items: [
- {
- boxLabel: gettext('Disk'),
- inputValue: 'disk',
- checked: true,
- },
- {
- boxLabel: gettext('Network'),
- inputValue: 'network',
- checked: true,
- },
- {
- boxLabel: 'USB',
- inputValue: 'usb',
- checked: true,
- },
- {
- boxLabel: gettext('Memory'),
- inputValue: 'memory',
- },
- {
- boxLabel: gettext('CPU'),
- inputValue: 'cpu',
- },
- ],
-
- setValue: function (value) {
- var me = this;
- var newVal = [];
- if (value === '1') {
- newVal = ['disk', 'network', 'usb'];
- } else if (value !== '0') {
- newVal = value.split(',');
- }
- me.callParent([{ hotplugCbGroup: newVal }]);
- },
-
- // override framework function to
- // assemble the hotplug value
- getSubmitData: function () {
- var me = this,
- boxes = me.getBoxes(),
- data = [];
- Ext.Array.forEach(boxes, function (box) {
- if (box.getValue()) {
- data.push(box.inputValue);
- }
- });
-
- /* because above is hotplug an array */
- if (data.length === 0) {
- return { hotplug: '0' };
- } else {
- return { hotplug: data.join(',') };
- }
- },
-});
-Ext.define('PVE.form.IPProtocolSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveIPProtocolSelector'],
- valueField: 'p',
- displayField: 'p',
- listConfig: {
- columns: [
- {
- header: gettext('Protocol'),
- dataIndex: 'p',
- hideable: false,
- sortable: false,
- width: 100,
- },
- {
- header: gettext('Number'),
- dataIndex: 'n',
- hideable: false,
- sortable: false,
- width: 50,
- },
- {
- header: gettext('Description'),
- dataIndex: 'd',
- hideable: false,
- sortable: false,
- flex: 1,
- },
- ],
- },
- store: {
- fields: ['p', 'd', 'n'],
- data: [
- { p: 'tcp', n: 6, d: 'Transmission Control Protocol' },
- { p: 'udp', n: 17, d: 'User Datagram Protocol' },
- { p: 'icmp', n: 1, d: 'Internet Control Message Protocol' },
- { p: 'igmp', n: 2, d: 'Internet Group Management' },
- { p: 'ggp', n: 3, d: 'gateway-gateway protocol' },
- { p: 'ipencap', n: 4, d: 'IP encapsulated in IP' },
- { p: 'st', n: 5, d: 'ST datagram mode' },
- { p: 'egp', n: 8, d: 'exterior gateway protocol' },
- { p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' },
- { p: 'pup', n: 12, d: 'PARC universal packet protocol' },
- { p: 'hmp', n: 20, d: 'host monitoring protocol' },
- { p: 'xns-idp', n: 22, d: 'Xerox NS IDP' },
- { p: 'rdp', n: 27, d: '"reliable datagram" protocol' },
- { p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' },
- { p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' },
- { p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' },
- { p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' },
- { p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' },
- { p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' },
- { p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' },
- { p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' },
- { p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' },
- { p: 'rsvp', n: 46, d: 'Reservation Protocol' },
- { p: 'gre', n: 47, d: 'General Routing Encapsulation' },
- { p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' },
- { p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' },
- { p: 'skip', n: 57, d: 'SKIP' },
- { p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' },
- { p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' },
- { p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' },
- { p: 'vmtp', n: 81, d: 'Versatile Message Transport' },
- { p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' },
- { p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' },
- { p: 'ax.25', n: 93, d: 'AX.25 frames' },
- { p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' },
- { p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' },
- { p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' },
- { p: 'pim', n: 103, d: 'Protocol Independent Multicast' },
- { p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' },
- { p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' },
- { p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' },
- { p: 'isis', n: 124, d: 'IS-IS over IPv4' },
- { p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' },
- { p: 'fc', n: 133, d: 'Fibre Channel' },
- { p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' },
- { p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' },
- { p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' },
- { p: 'hip', n: 139, d: 'Host Identity Protocol' },
- { p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' },
- { p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' },
- { p: 'rohc', n: 142, d: 'Robust Header Compression' },
- ],
- },
-});
-Ext.define('PVE.form.IPRefSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveIPRefSelector'],
-
- base_url: undefined,
-
- preferredValue: '', // hack: else Form sets dirty flag?
-
- ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias']
-
- valueField: 'scopedref',
- displayField: 'ref',
- notFoundIsValid: true,
-
- initComponent: function () {
- var me = this;
-
- if (!me.base_url) {
- throw 'no base_url specified';
- }
-
- var url = '/api2/json' + me.base_url;
- if (me.ref_type) {
- url += '?type=' + me.ref_type;
- }
-
- var store = Ext.create('Ext.data.Store', {
- autoLoad: true,
- fields: [
- 'type',
- 'name',
- 'ref',
- 'comment',
- 'scope',
- {
- name: 'scopedref',
- calculate: function (v) {
- if (v.type === 'alias') {
- return `${v.scope}/${v.name}`;
- } else if (v.type === 'ipset') {
- return `+${v.scope}/${v.name}`;
- } else {
- return v.ref;
- }
- },
- },
- ],
- idProperty: 'ref',
- proxy: {
- type: 'proxmox',
- url: url,
- },
- sorters: {
- property: 'ref',
- direction: 'ASC',
- },
- });
-
- var columns = [];
-
- if (!me.ref_type) {
- columns.push({
- header: gettext('Type'),
- dataIndex: 'type',
- hideable: false,
- width: 60,
- });
- }
-
- let scopes = {
- dc: gettext('Datacenter'),
- guest: gettext('Guest'),
- sdn: gettext('SDN'),
- };
-
- columns.push(
- {
- header: gettext('Name'),
- dataIndex: 'ref',
- hideable: false,
- width: 140,
- },
- {
- header: gettext('Scope'),
- dataIndex: 'scope',
- hideable: false,
- width: 140,
- renderer: function (value) {
- return scopes[value] ?? 'unknown scope';
- },
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- minWidth: 60,
- flex: 1,
- },
- );
-
- Ext.apply(me, {
- store: store,
- listConfig: {
- columns: columns,
- width: 500,
- },
- });
-
- me.on('beforequery', function (queryPlan) {
- return !(queryPlan.query === null || queryPlan.query.match(/^\d/));
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.form.MDevSelector', {
- extend: 'Proxmox.form.ComboGrid',
- xtype: 'pveMDevSelector',
-
- store: {
- fields: ['type', 'available', 'description'],
- filterOnLoad: true,
- sorters: [
- {
- property: 'type',
- direction: 'ASC',
- },
- ],
- },
- autoSelect: false,
- valueField: 'type',
- displayField: 'type',
- listConfig: {
- width: 550,
- columns: [
- {
- header: gettext('Type'),
- dataIndex: 'type',
- renderer: function (value, md, rec) {
- if (rec.data.name !== undefined) {
- return `${rec.data.name} (${value})`;
- }
- return value;
- },
- flex: 1,
- },
- {
- header: gettext('Avail'),
- dataIndex: 'available',
- width: 60,
- },
- {
- header: gettext('Description'),
- dataIndex: 'description',
- flex: 1,
- cellWrap: true,
- renderer: function (value) {
- if (!value) {
- return '';
- }
-
- return value.split('\n').join(' ');
- },
- },
- ],
- },
-
- setPciIdOrMapping: function (pciIdOrMapping, force) {
- var me = this;
-
- if (!force && (!pciIdOrMapping || me.pciIdOrMapping === pciIdOrMapping)) {
- return;
- }
-
- me.pciIdOrMapping = pciIdOrMapping;
- me.updateProxy();
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
- me.updateProxy();
- },
-
- updateProxy: function () {
- var me = this;
- me.store.setProxy({
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/hardware/pci/${me.pciIdOrMapping}/mdev`,
- });
- me.store.load();
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- me.callParent();
-
- if (me.pciIdOrMapping) {
- me.setPciIdOrMapping(me.pciIdOrMapping, true);
- }
- },
-});
-Ext.define('PVE.form.MemoryField', {
- extend: 'Ext.form.field.Number',
- alias: 'widget.pveMemoryField',
-
- allowBlank: false,
-
- hotplug: false,
-
- minValue: 32,
-
- maxValue: 4178944,
-
- step: 32,
-
- value: '512', // qm backend default
-
- allowDecimals: false,
-
- allowExponential: false,
-
- computeUpDown: function (value) {
- var me = this;
-
- if (!me.hotplug) {
- return { up: value + me.step, down: value - me.step };
- }
-
- var dimm_size = 512;
- var prev_dimm_size = 0;
- var min_size = 1024;
- var current_size = min_size;
- var value_up = min_size;
- var value_down = min_size;
- var value_start = min_size;
-
- var i, j;
- for (j = 0; j < 9; j++) {
- for (i = 0; i < 32; i++) {
- if (value >= current_size && value < current_size + dimm_size) {
- value_start = current_size;
- value_up = current_size + dimm_size;
- value_down = current_size - (i === 0 ? prev_dimm_size : dimm_size);
- }
- current_size += dimm_size;
- }
- prev_dimm_size = dimm_size;
- dimm_size = dimm_size * 2;
- }
-
- return { up: value_up, down: value_down, start: value_start };
- },
-
- onSpinUp: function () {
- var me = this;
- if (!me.readOnly) {
- let res = me.computeUpDown(me.getValue());
- me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue));
- }
- },
-
- onSpinDown: function () {
- var me = this;
- if (!me.readOnly) {
- let res = me.computeUpDown(me.getValue());
- me.setValue(Ext.Number.constrain(res.down, me.minValue, me.maxValue));
- }
- },
-
- initComponent: function () {
- var me = this;
-
- if (me.hotplug) {
- me.minValue = 1024;
-
- me.on('blur', function (field) {
- var value = me.getValue();
- var res = me.computeUpDown(value);
- if (value === res.start || value === res.up || value === res.down) {
- return;
- }
- field.setValue(res.up);
- });
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.form.MultiPCISelector', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveMultiPCISelector',
-
- emptyText: gettext('No Devices found'),
-
- mixins: {
- field: 'Ext.form.field.Field',
- },
-
- // will be called after loading finished
- onLoadCallBack: Ext.emptyFn,
-
- getValue: function () {
- let me = this;
- return me.value ?? [];
- },
-
- getSubmitData: function () {
- let me = this;
- let res = {};
- res[me.name] = me.getValue();
- return res;
- },
-
- setValue: function (value) {
- let me = this;
-
- value ??= [];
-
- me.updateSelectedDevices(value);
-
- return me.mixins.field.setValue.call(me, value);
- },
-
- getErrors: function () {
- let me = this;
-
- let errorCls = ['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid'];
-
- if (me.getValue().length < 1) {
- let error = gettext('Must choose at least one device');
- me.addCls(errorCls);
- me.getActionEl()?.dom.setAttribute('data-errorqtip', error);
-
- return [error];
- }
-
- me.removeCls(errorCls);
- me.getActionEl()?.dom.setAttribute('data-errorqtip', '');
-
- return [];
- },
-
- viewConfig: {
- getRowClass: function (record) {
- if (record.data.disabled === true) {
- return 'x-item-disabled';
- }
- return '';
- },
- },
-
- updateSelectedDevices: function (value = []) {
- let me = this;
-
- let recs = [];
- let store = me.getStore();
-
- for (const map of value) {
- let parsed = PVE.Parser.parsePropertyString(map);
- if (parsed.node !== me.nodename) {
- continue;
- }
-
- let rec = store.getById(parsed.path);
- if (rec) {
- recs.push(rec);
- }
- }
-
- me.suspendEvent('change');
- me.setSelection();
- me.setSelection(recs);
- me.resumeEvent('change');
- },
-
- setNodename: function (nodename) {
- let me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- me.getStore().setProxy({
- type: 'proxmox',
- url: '/api2/json/nodes/' + me.nodename + '/hardware/pci?pci-class-blacklist=',
- });
-
- me.setSelection();
-
- me.getStore().load({
- callback: (recs, op, success) => me.addSlotRecords(recs, op, success),
- });
- },
-
- setMdev: function (mdev) {
- let me = this;
- if (mdev) {
- me.getStore().addFilter({
- id: 'mdev-filter',
- property: 'mdev',
- value: '1',
- operator: '=',
- });
- } else {
- me.getStore().removeFilter('mdev-filter');
- }
- me.setSelection();
- },
-
- // adds the virtual 'slot' records (e.g. '0000:01:00') to the store
- addSlotRecords: function (records, _op, success) {
- let me = this;
- if (!success) {
- return;
- }
-
- let slots = {};
- records.forEach((rec) => {
- let slotname = rec.data.id.slice(0, -2); // remove function
- if (slots[slotname] !== undefined) {
- slots[slotname].count++;
- rec.set('slot', slots[slotname]);
- return;
- }
- slots[slotname] = {
- count: 1,
- };
-
- rec.set('slot', slots[slotname]);
-
- if (rec.data.id.endsWith('.0')) {
- slots[slotname].device = rec.data;
- }
- });
-
- let store = me.getStore();
-
- for (const [slot, { count, device }] of Object.entries(slots)) {
- if (count === 1) {
- continue;
- }
- store.add(
- Ext.apply(
- {},
- {
- id: slot,
- mdev: undefined,
- device_name: gettext('Pass through all functions as one device'),
- },
- device,
- ),
- );
- }
-
- me.updateSelectedDevices(me.value);
- },
-
- selectionChange: function (_grid, selection) {
- let me = this;
-
- let ids = {};
- selection
- .filter((rec) => rec.data.id.indexOf('.') === -1)
- .forEach((rec) => {
- ids[rec.data.id] = true;
- });
-
- let to_disable = [];
-
- me.getStore().each((rec) => {
- let id = rec.data.id;
- rec.set('disabled', false);
- if (id.indexOf('.') === -1) {
- return;
- }
- let slot = id.slice(0, -2); // remove function
-
- if (ids[slot]) {
- to_disable.push(rec);
- rec.set('disabled', true);
- }
- });
-
- me.suspendEvent('selectionchange');
- me.getSelectionModel().deselect(to_disable);
- me.resumeEvent('selectionchange');
-
- me.value = me.getSelection().map((rec) => {
- let res = {
- path: rec.data.id,
- node: me.nodename,
- id: `${rec.data.vendor}:${rec.data.device}`.replace(/0x/g, ''),
- 'subsystem-id': `${rec.data.subsystem_vendor}:${rec.data.subsystem_device}`.replace(
- /0x/g,
- '',
- ),
- };
-
- if (rec.data.iommugroup !== -1) {
- res.iommugroup = rec.data.iommugroup;
- }
-
- return PVE.Parser.printPropertyString(res);
- });
- me.checkChange();
- },
-
- selModel: {
- type: 'checkboxmodel',
- mode: 'SIMPLE',
- },
-
- columns: [
- {
- header: 'ID',
- dataIndex: 'id',
- renderer: function (value, _md, rec) {
- if (value.match(/\.[0-9a-f]/i) && rec.data.slot?.count > 1) {
- return ` ${value}`;
- }
- return value;
- },
- width: 150,
- },
- {
- header: gettext('IOMMU Group'),
- dataIndex: 'iommugroup',
- renderer: (v, _md, rec) => (rec.data.slot === rec.data.id ? '' : v === -1 ? '-' : v),
- width: 50,
- },
- {
- header: gettext('Vendor'),
- dataIndex: 'vendor_name',
- flex: 3,
- },
- {
- header: gettext('Device'),
- dataIndex: 'device_name',
- flex: 6,
- },
- {
- header: gettext('Mediated Devices'),
- dataIndex: 'mdev',
- flex: 1,
- renderer: function (val) {
- return Proxmox.Utils.format_boolean(!!val);
- },
- },
- ],
-
- listeners: {
- selectionchange: function () {
- this.selectionChange(...arguments);
- },
- },
-
- store: {
- fields: [
- 'id',
- 'vendor_name',
- 'device_name',
- 'vendor',
- 'device',
- 'iommugroup',
- 'mdev',
- 'subsystem_vendor',
- 'subsystem_device',
- 'disabled',
- {
- name: 'subsystem-vendor',
- calculate: function (data) {
- return data.subsystem_vendor;
- },
- },
- {
- name: 'subsystem-device',
- calculate: function (data) {
- return data.subsystem_device;
- },
- },
- ],
- sorters: [
- {
- property: 'id',
- direction: 'ASC',
- },
- ],
- },
-
- initComponent: function () {
- let me = this;
-
- let nodename = me.nodename;
- me.nodename = undefined;
-
- me.callParent();
-
- me.mon(me.getStore(), 'load', me.onLoadCallBack);
-
- Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
-
- me.setNodename(nodename);
-
- me.initField();
- },
-});
-Ext.define('PVE.form.NetworkCardSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: 'widget.pveNetworkCardSelector',
- comboItems: [
- ['e1000', 'Intel E1000'],
- ['e1000e', 'Intel E1000E'],
- ['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'],
- ['rtl8139', 'Realtek RTL8139'],
- ['vmxnet3', 'VMware vmxnet3'],
- ],
-});
-Ext.define('PVE.form.NodeSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveNodeSelector'],
-
- // invalidate nodes which are offline
- onlineValidator: false,
-
- selectCurNode: false,
-
- // do not allow those nodes (array)
- disallowedNodes: undefined,
-
- // only allow those nodes (array)
- allowedNodes: undefined,
-
- valueField: 'node',
- displayField: 'node',
- store: {
- fields: ['node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes',
- },
- sorters: [
- {
- property: 'node',
- direction: 'ASC',
- },
- {
- property: 'mem',
- direction: 'DESC',
- },
- ],
- },
-
- listConfig: {
- columns: [
- {
- header: gettext('Node'),
- dataIndex: 'node',
- sortable: true,
- hideable: false,
- flex: 1,
- },
- {
- header: gettext('Memory usage') + ' %',
- renderer: PVE.Utils.render_mem_usage_percent,
- sortable: true,
- width: 100,
- dataIndex: 'mem',
- },
- {
- header: gettext('CPU usage'),
- renderer: Proxmox.Utils.render_cpu,
- sortable: true,
- width: 100,
- dataIndex: 'cpu',
- },
- ],
- },
-
- validator: function (value) {
- let me = this;
- if (!me.onlineValidator || (me.allowBlank && !value)) {
- return true;
- }
-
- let offline = [],
- notAllowed = [];
- Ext.Array.each(value.split(/\s*,\s*/), function (node) {
- let rec = me.store.findRecord(me.valueField, node, 0, false, true, true);
- if (!(rec && rec.data) || rec.data.status !== 'online') {
- offline.push(node);
- } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) {
- notAllowed.push(node);
- }
- });
-
- if (value && notAllowed.length !== 0) {
- return 'Node ' + notAllowed.join(', ') + ' is not allowed for this action!';
- }
- if (value && offline.length !== 0) {
- return 'Node ' + offline.join(', ') + ' seems to be offline!';
- }
- return true;
- },
-
- initComponent: function () {
- var me = this;
-
- if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) {
- me.preferredValue = PVE.curSelectedNode.data.node;
- }
-
- me.callParent();
- me.getStore().load();
-
- me.getStore().addFilter(
- new Ext.util.Filter({
- // filter out disallowed nodes
- filterFn: (item) =>
- !(me.disallowedNodes && me.disallowedNodes.includes(item.data.node)),
- }),
- );
-
- me.mon(me.getStore(), 'load', () => me.isValid());
- },
-});
-Ext.define('PVE.form.NotificationModeSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveNotificationModeSelector'],
- comboItems: [
- ['notification-target', gettext('Target')],
- ['mailto', gettext('E-Mail')],
- ],
-});
-Ext.define('PVE.form.NotificationTargetSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveNotificationTargetSelector'],
-
- // set default value to empty array, else it inits it with
- // null and after the store load it is an empty array,
- // triggering dirtychange
- value: [],
- valueField: 'name',
- displayField: 'name',
- deleteEmpty: true,
- skipEmptyText: true,
-
- store: {
- fields: ['name', 'type', 'comment'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/notifications/targets',
- },
- sorters: [
- {
- property: 'name',
- direction: 'ASC',
- },
- ],
- autoLoad: true,
- },
-
- listConfig: {
- columns: [
- {
- header: gettext('Target'),
- dataIndex: 'name',
- sortable: true,
- hideable: false,
- flex: 1,
- },
- {
- header: gettext('Type'),
- dataIndex: 'type',
- sortable: true,
- hideable: false,
- flex: 1,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- sortable: true,
- hideable: false,
- flex: 2,
- },
- ],
- },
-});
-Ext.define('PVE.form.EmailNotificationSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveEmailNotificationSelector'],
- comboItems: [
- ['always', gettext('Always')],
- ['failure', gettext('On failure only')],
- ],
-});
-Ext.define('PVE.form.PCISelector', {
- extend: 'Proxmox.form.ComboGrid',
- xtype: 'pvePCISelector',
-
- store: {
- fields: ['id', 'vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev'],
- filterOnLoad: true,
- sorters: [
- {
- property: 'id',
- direction: 'ASC',
- },
- ],
- },
-
- autoSelect: false,
- valueField: 'id',
- displayField: 'id',
-
- // can contain a load callback for the store
- // useful to determine the state of the IOMMU
- onLoadCallBack: undefined,
-
- listConfig: {
- minHeight: 80,
- width: 800,
- columns: [
- {
- header: 'ID',
- dataIndex: 'id',
- width: 100,
- },
- {
- header: gettext('IOMMU Group'),
- dataIndex: 'iommugroup',
- renderer: (v) => (v === -1 ? '-' : v),
- width: 75,
- },
- {
- header: gettext('Vendor'),
- dataIndex: 'vendor_name',
- flex: 2,
- },
- {
- header: gettext('Device'),
- dataIndex: 'device_name',
- flex: 6,
- },
- {
- header: gettext('Mediated Devices'),
- dataIndex: 'mdev',
- flex: 1,
- renderer: function (val) {
- return Proxmox.Utils.format_boolean(!!val);
- },
- },
- ],
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- me.store.setProxy({
- type: 'proxmox',
- url: '/api2/json/nodes/' + me.nodename + '/hardware/pci',
- });
-
- me.store.load();
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.nodename;
- me.nodename = undefined;
-
- me.callParent();
-
- if (me.onLoadCallBack !== undefined) {
- me.mon(me.getStore(), 'load', me.onLoadCallBack);
- }
-
- me.setNodename(nodename);
- },
-});
-Ext.define('pve-mapped-pci-model', {
- extend: 'Ext.data.Model',
-
- fields: ['id', 'path', 'vendor', 'device', 'iommugroup', 'mdev', 'description', 'map'],
- idProperty: 'id',
-});
-
-Ext.define('PVE.form.PCIMapSelector', {
- extend: 'Proxmox.form.ComboGrid',
- xtype: 'pvePCIMapSelector',
-
- store: {
- model: 'pve-mapped-pci-model',
- filterOnLoad: true,
- sorters: [
- {
- property: 'id',
- direction: 'ASC',
- },
- ],
- },
-
- autoSelect: false,
- valueField: 'id',
- displayField: 'id',
-
- // can contain a load callback for the store
- // useful to determine the state of the IOMMU
- onLoadCallBack: undefined,
-
- listConfig: {
- width: 800,
- columns: [
- {
- header: gettext('ID'),
- dataIndex: 'id',
- flex: 1,
- },
- {
- header: gettext('Description'),
- dataIndex: 'description',
- flex: 1,
- renderer: Ext.String.htmlEncode,
- },
- {
- header: gettext('Status'),
- dataIndex: 'checks',
- renderer: function (value) {
- let _me = this;
-
- if (!Ext.isArray(value) || !value?.length) {
- return ` ${gettext('Mapping matches host data')}`;
- }
-
- let checks = [];
-
- value.forEach((check) => {
- let iconCls;
- switch (check?.severity) {
- case 'warning':
- iconCls = 'fa-exclamation-circle warning';
- break;
- case 'error':
- iconCls = 'fa-times-circle critical';
- break;
- }
-
- let message = check?.message;
- let icon = ` `;
- if (iconCls !== undefined) {
- checks.push(`${icon} ${message}`);
- }
- });
-
- return checks.join(' ');
- },
- flex: 3,
- },
- ],
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- me.store.setProxy({
- type: 'proxmox',
- url: `/api2/json/cluster/mapping/pci?check-node=${nodename}`,
- });
-
- me.store.load();
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.nodename;
- me.nodename = undefined;
-
- me.callParent();
-
- if (me.onLoadCallBack !== undefined) {
- me.mon(me.getStore(), 'load', me.onLoadCallBack);
- }
-
- me.setNodename(nodename);
- },
-});
-Ext.define('PVE.form.PermPathSelector', {
- extend: 'Ext.form.field.ComboBox',
- xtype: 'pvePermPathSelector',
-
- valueField: 'value',
- displayField: 'value',
- typeAhead: true,
- queryMode: 'local',
- width: 380,
-
- store: {
- type: 'pvePermPath',
- },
-});
-Ext.define(
- 'PVE.form.PoolSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pvePoolSelector'],
-
- allowBlank: false,
- valueField: 'poolid',
- displayField: 'poolid',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-pools',
- sorters: 'poolid',
- });
-
- Ext.apply(me, {
- store: store,
- autoSelect: false,
- listConfig: {
- columns: [
- {
- header: gettext('Pool'),
- sortable: true,
- dataIndex: 'poolid',
- flex: 1,
- },
- {
- header: gettext('Comment'),
- sortable: false,
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- ],
- },
- });
-
- me.callParent();
-
- store.load();
- },
- },
- function () {
- Ext.define('pve-pools', {
- extend: 'Ext.data.Model',
- fields: ['poolid', 'comment'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/pools',
- },
- idProperty: 'poolid',
- });
- },
-);
-Ext.define('PVE.form.preallocationSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pvePreallocationSelector'],
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText],
- ['off', 'Off'],
- ['metadata', 'Metadata'],
- ['falloc', 'Full (posix_fallocate)'],
- ['full', 'Full'],
- ],
-});
-Ext.define('PVE.form.PrivilegesSelector', {
- extend: 'Proxmox.form.KVComboBox',
- xtype: 'pvePrivilegesSelector',
-
- multiSelect: true,
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- Proxmox.Utils.API2Request({
- url: '/access/roles/Administrator',
- method: 'GET',
- success: function (response, options) {
- let data = Object.keys(response.result.data).map((key) => [key, key]);
-
- me.store.setData(data);
-
- me.store.sort({
- property: 'key',
- direction: 'ASC',
- });
- },
- failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- },
-});
-Ext.define('PVE.form.QemuBiosSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveQemuBiosSelector'],
-
- initComponent: function () {
- var me = this;
-
- me.comboItems = [
- ['__default__', PVE.Utils.render_qemu_bios('')],
- ['seabios', PVE.Utils.render_qemu_bios('seabios')],
- ['ovmf', PVE.Utils.render_qemu_bios('ovmf')],
- ];
-
- me.callParent();
- },
-});
-Ext.define(
- 'PVE.form.SDNControllerSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveSDNControllerSelector'],
-
- allowBlank: false,
- valueField: 'controller',
- displayField: 'controller',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-sdn-controller',
- sorters: {
- property: 'controller',
- direction: 'ASC',
- },
- });
-
- Ext.apply(me, {
- store: store,
- autoSelect: false,
- listConfig: {
- columns: [
- {
- header: gettext('Controller'),
- sortable: true,
- dataIndex: 'controller',
- flex: 1,
- },
- ],
- },
- });
-
- me.callParent();
-
- store.load();
- },
- },
- function () {
- Ext.define('pve-sdn-controller', {
- extend: 'Ext.data.Model',
- fields: ['controller'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/controllers',
- },
- idProperty: 'controller',
- });
- },
-);
-Ext.define(
- 'PVE.form.SDNZoneSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveSDNZoneSelector'],
-
- allowBlank: false,
- valueField: 'zone',
- displayField: 'zone',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-sdn-zone',
- sorters: {
- property: 'zone',
- direction: 'ASC',
- },
- });
-
- Ext.apply(me, {
- store: store,
- autoSelect: false,
- listConfig: {
- columns: [
- {
- header: gettext('Zone'),
- sortable: true,
- dataIndex: 'zone',
- flex: 1,
- },
- ],
- },
- });
-
- me.callParent();
-
- store.load();
- },
- },
- function () {
- Ext.define('pve-sdn-zone', {
- extend: 'Ext.data.Model',
- fields: ['zone', 'type'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/zones',
- },
- idProperty: 'zone',
- });
- },
-);
-Ext.define(
- 'PVE.form.SDNVnetSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveSDNVnetSelector'],
-
- allowBlank: false,
- valueField: 'vnet',
- displayField: 'vnet',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-sdn-vnet',
- sorters: {
- property: 'vnet',
- direction: 'ASC',
- },
- });
-
- Ext.apply(me, {
- store: store,
- autoSelect: false,
- listConfig: {
- columns: [
- {
- header: gettext('VNet'),
- sortable: true,
- dataIndex: 'vnet',
- flex: 1,
- },
- {
- header: gettext('Alias'),
- flex: 1,
- dataIndex: 'alias',
- },
- {
- header: gettext('Tag'),
- flex: 1,
- dataIndex: 'tag',
- },
- ],
- },
- });
-
- me.callParent();
-
- store.load();
- },
- },
- function () {
- Ext.define('pve-sdn-vnet', {
- extend: 'Ext.data.Model',
- fields: ['alias', 'tag', 'type', 'vnet', 'zone'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/vnets',
- },
- idProperty: 'vnet',
- });
- },
-);
-Ext.define(
- 'PVE.form.SDNIpamSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveSDNIpamSelector'],
-
- allowBlank: false,
- valueField: 'ipam',
- displayField: 'ipam',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-sdn-ipam',
- sorters: {
- property: 'ipam',
- direction: 'ASC',
- },
- });
-
- Ext.apply(me, {
- store: store,
- autoSelect: false,
- listConfig: {
- columns: [
- {
- header: gettext('Ipam'),
- sortable: true,
- dataIndex: 'ipam',
- flex: 1,
- },
- ],
- },
- });
-
- me.callParent();
-
- store.load();
- },
- },
- function () {
- Ext.define('pve-sdn-ipam', {
- extend: 'Ext.data.Model',
- fields: ['ipam'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/ipams',
- },
- idProperty: 'ipam',
- });
- },
-);
-Ext.define(
- 'PVE.form.SDNDnsSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveSDNDnsSelector'],
-
- allowBlank: false,
- valueField: 'dns',
- displayField: 'dns',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-sdn-dns',
- sorters: {
- property: 'dns',
- direction: 'ASC',
- },
- });
-
- Ext.apply(me, {
- store: store,
- autoSelect: false,
- listConfig: {
- columns: [
- {
- header: gettext('dns'),
- sortable: true,
- dataIndex: 'dns',
- flex: 1,
- },
- ],
- },
- });
-
- me.callParent();
-
- store.load();
- },
- },
- function () {
- Ext.define('pve-sdn-dns', {
- extend: 'Ext.data.Model',
- fields: ['dns'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/dns',
- },
- idProperty: 'dns',
- });
- },
-);
-Ext.define('PVE.form.ScsiHwSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveScsiHwSelector'],
- comboItems: [
- ['__default__', PVE.Utils.render_scsihw('')],
- ['lsi', PVE.Utils.render_scsihw('lsi')],
- ['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')],
- ['megasas', PVE.Utils.render_scsihw('megasas')],
- ['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')],
- ['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')],
- ['pvscsi', PVE.Utils.render_scsihw('pvscsi')],
- ],
-});
-Ext.define('PVE.form.SecurityGroupsSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveSecurityGroupsSelector'],
-
- valueField: 'group',
- displayField: 'group',
- initComponent: function () {
- var me = this;
-
- var store = Ext.create('Ext.data.Store', {
- autoLoad: true,
- fields: ['group', 'comment'],
- idProperty: 'group',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/firewall/groups',
- },
- sorters: {
- property: 'group',
- direction: 'ASC',
- },
- });
-
- Ext.apply(me, {
- store: store,
- listConfig: {
- columns: [
- {
- header: gettext('Security Group'),
- dataIndex: 'group',
- hideable: false,
- width: 100,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- renderer: function (value, metaData) {
- let comment = Ext.String.htmlEncode(value) || '';
- if (comment.length * 12 > metaData.column.cellWidth) {
- let qtip = Ext.htmlEncode(comment);
- comment = `${comment} `;
- }
- return comment;
- },
- flex: 1,
- },
- ],
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.form.SnapshotSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.PVE.form.SnapshotSelector'],
-
- valueField: 'name',
- displayField: 'name',
-
- loadStore: function (nodename, vmid) {
- var me = this;
-
- if (!nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- if (!vmid) {
- return;
- }
-
- me.vmid = vmid;
-
- me.store.setProxy({
- type: 'proxmox',
- url:
- '/api2/json/nodes/' +
- me.nodename +
- '/' +
- me.guestType +
- '/' +
- me.vmid +
- '/snapshot',
- });
-
- me.store.load();
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.vmid) {
- throw 'no VM ID specified';
- }
-
- if (!me.guestType) {
- throw 'no guest type specified';
- }
-
- var store = Ext.create('Ext.data.Store', {
- fields: ['name'],
- filterOnLoad: true,
- });
-
- Ext.apply(me, {
- store: store,
- listConfig: {
- columns: [
- {
- header: gettext('Snapshot'),
- dataIndex: 'name',
- hideable: false,
- flex: 1,
- },
- ],
- },
- });
-
- me.callParent();
-
- me.loadStore(me.nodename, me.vmid);
- },
-});
-Ext.define('PVE.form.SpiceEnhancementSelector', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveSpiceEnhancementSelector',
-
- viewModel: {},
-
- items: [
- {
- xtype: 'proxmoxcheckbox',
- itemId: 'foldersharing',
- name: 'foldersharing',
- reference: 'foldersharing',
- fieldLabel: 'Folder Sharing',
- uncheckedValue: 0,
- },
- {
- xtype: 'proxmoxKVComboBox',
- itemId: 'videostreaming',
- name: 'videostreaming',
- value: 'off',
- fieldLabel: 'Video Streaming',
- comboItems: [
- ['off', 'off'],
- ['all', 'all'],
- ['filter', 'filter'],
- ],
- },
- {
- xtype: 'displayfield',
- itemId: 'spicehint',
- userCls: 'pmx-hint',
- value: gettext(
- 'To use these features set the display to SPICE in the hardware settings of the VM.',
- ),
- hidden: true,
- },
- {
- xtype: 'displayfield',
- itemId: 'spicefolderhint',
- userCls: 'pmx-hint',
- value: gettext('Make sure the SPICE WebDav daemon is installed in the VM.'),
- bind: {
- hidden: '{!foldersharing.checked}',
- },
- },
- ],
-
- onGetValues: function (values) {
- var ret = {};
-
- if (values.videostreaming !== 'off') {
- ret.videostreaming = values.videostreaming;
- }
- if (values.foldersharing) {
- ret.foldersharing = 1;
- }
- if (Ext.Object.isEmpty(ret)) {
- return { delete: 'spice_enhancements' };
- }
- var enhancements = PVE.Parser.printPropertyString(ret);
- return { spice_enhancements: enhancements };
- },
-
- setValues: function (values) {
- var vga = PVE.Parser.parsePropertyString(values.vga, 'type');
- if (!/^qxl\d?$/.test(vga.type)) {
- this.down('#spicehint').setVisible(true);
- }
- if (values.spice_enhancements) {
- let enhancements = PVE.Parser.parsePropertyString(values.spice_enhancements);
- enhancements.foldersharing = PVE.Parser.parseBoolean(enhancements.foldersharing, 0);
- this.callParent([enhancements]);
- }
- },
-});
-Ext.define('PVE.form.StorageScanNodeSelector', {
- extend: 'PVE.form.NodeSelector',
- xtype: 'pveStorageScanNodeSelector',
-
- name: 'storageScanNode',
- itemId: 'pveStorageScanNodeSelector',
- fieldLabel: gettext('Scan node'),
- allowBlank: true,
- disallowedNodes: undefined,
- autoSelect: false,
- submitValue: false,
- value: null,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Scan for available storages on the selected node'),
- },
- triggers: {
- clear: {
- handler: function () {
- let me = this;
- me.setValue(null);
- },
- },
- },
-
- emptyText: Proxmox.NodeName,
-
- setValue: function (value) {
- let me = this;
- me.callParent([value]);
- me.triggers.clear.setVisible(!!value);
- },
-});
-Ext.define(
- 'PVE.form.StorageSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: 'widget.pveStorageSelector',
- mixins: ['Proxmox.Mixin.CBind'],
-
- cbindData: {
- clusterView: false,
- },
-
- allowBlank: false,
- valueField: 'storage',
- displayField: 'storage',
- listConfig: {
- cbind: {
- clusterView: '{clusterView}',
- },
- width: 450,
- columns: [
- {
- header: gettext('Name'),
- dataIndex: 'storage',
- hideable: false,
- flex: 1,
- },
- {
- header: gettext('Type'),
- width: 75,
- dataIndex: 'type',
- },
- {
- header: gettext('Avail'),
- width: 90,
- dataIndex: 'avail',
- renderer: Proxmox.Utils.format_size,
- cbind: {
- hidden: '{clusterView}',
- },
- },
- {
- header: gettext('Capacity'),
- width: 90,
- dataIndex: 'total',
- renderer: Proxmox.Utils.format_size,
- cbind: {
- hidden: '{clusterView}',
- },
- },
- {
- header: gettext('Nodes'),
- width: 120,
- dataIndex: 'nodes',
- renderer: (value) => (value ? value : '-- ' + gettext('All') + ' --'),
- cbind: {
- hidden: '{!clusterView}',
- },
- },
- {
- header: gettext('Shared'),
- width: 70,
- dataIndex: 'shared',
- renderer: Proxmox.Utils.format_boolean,
- cbind: {
- hidden: '{!clusterView}',
- },
- },
- ],
- },
-
- reloadStorageList: function () {
- let me = this;
-
- if (me.clusterView) {
- me.getStore().setProxy({
- type: 'proxmox',
- url: `/api2/json/storage`,
- });
-
- // filter here, back-end does not support it currently
- let filters = [(storage) => !storage.data.disable];
-
- if (me.storageContent) {
- filters.push((storage) =>
- storage.data.content.split(',').includes(me.storageContent),
- );
- }
-
- if (me.nodename) {
- filters.push(
- (storage) =>
- !storage.data.nodes || storage.data.nodes.includes(me.nodename),
- );
- }
-
- me.getStore().clearFilter();
- me.getStore().setFilters(filters);
- } else {
- if (!me.nodename) {
- return;
- }
-
- let params = {
- format: 1,
- };
- if (me.storageContent) {
- params.content = me.storageContent;
- }
- if (me.targetNode) {
- params.target = me.targetNode;
- params.enabled = 1; // skip disabled storages
- }
- me.store.setProxy({
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/storage`,
- extraParams: params,
- });
- }
-
- me.store.load(() => me.validate());
- },
-
- setTargetNode: function (targetNode) {
- var me = this;
-
- if (!targetNode || me.targetNode === targetNode) {
- return;
- }
-
- if (me.clusterView) {
- throw 'setting targetNode with clusterView is not implemented';
- }
-
- me.targetNode = targetNode;
-
- me.reloadStorageList();
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- nodename = nodename || '';
-
- if (me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- me.reloadStorageList();
- },
-
- initComponent: function () {
- var me = this;
-
- let nodename = me.nodename;
- me.nodename = undefined;
-
- var store = Ext.create('Ext.data.Store', {
- model: 'pve-storage-status',
- sorters: {
- property: 'storage',
- direction: 'ASC',
- },
- });
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
-
- me.setNodename(nodename);
- },
- },
- function () {
- Ext.define('pve-storage-status', {
- extend: 'Ext.data.Model',
- fields: ['storage', 'active', 'type', 'avail', 'total', 'nodes', 'shared'],
- idProperty: 'storage',
- });
- },
-);
-Ext.define('PVE.form.TFASelector', {
- extend: 'Ext.container.Container',
- xtype: 'pveTFASelector',
- mixins: ['Proxmox.Mixin.CBind'],
-
- deleteEmpty: true,
-
- viewModel: {
- data: {
- type: '__default__',
- step: null,
- digits: null,
- id: null,
- key: null,
- url: null,
- },
-
- formulas: {
- isOath: (get) => get('type') === 'oath',
- isYubico: (get) => get('type') === 'yubico',
- tfavalue: {
- get: function (get) {
- let val = {
- type: get('type'),
- };
- if (get('isOath')) {
- let step = get('step');
- let digits = get('digits');
- if (step) {
- val.step = step;
- }
- if (digits) {
- val.digits = digits;
- }
- } else if (get('isYubico')) {
- let id = get('id');
- let key = get('key');
- let url = get('url');
- val.id = id;
- val.key = key;
- if (url) {
- val.url = url;
- }
- } else if (val.type === '__default__') {
- return '';
- }
-
- return PVE.Parser.printPropertyString(val);
- },
- set: function (value) {
- let val = PVE.Parser.parseTfaConfig(value);
- this.set(val);
- this.notify();
- // we need to reset the original values, so that
- // we can reliably track the state of the form
- let form = this.getView().up('form');
- if (form.trackResetOnLoad) {
- let fields = this.getView().query('field[name!="tfa"]');
- fields.forEach((field) => field.resetOriginalValue());
- }
- },
- },
- },
- },
-
- items: [
- {
- xtype: 'proxmoxtextfield',
- name: 'tfa',
- hidden: true,
- submitValue: true,
- cbind: {
- deleteEmpty: '{deleteEmpty}',
- },
- bind: {
- value: '{tfavalue}',
- },
- },
- {
- xtype: 'proxmoxKVComboBox',
- value: '__default__',
- deleteEmpty: false,
- submitValue: false,
- fieldLabel: gettext('Require TFA'),
- comboItems: [
- ['__default__', Proxmox.Utils.noneText],
- ['oath', 'OATH/TOTP'],
- ['yubico', 'Yubico'],
- ],
- bind: {
- value: '{type}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- hidden: true,
- minValue: 10,
- submitValue: false,
- emptyText: Proxmox.Utils.defaultText + ' (30)',
- fieldLabel: gettext('Time Step'),
- bind: {
- value: '{step}',
- hidden: '{!isOath}',
- disabled: '{!isOath}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- hidden: true,
- submitValue: false,
- fieldLabel: gettext('Secret Length'),
- minValue: 6,
- maxValue: 8,
- emptyText: Proxmox.Utils.defaultText + ' (6)',
- bind: {
- value: '{digits}',
- hidden: '{!isOath}',
- disabled: '{!isOath}',
- },
- },
- {
- xtype: 'textfield',
- hidden: true,
- submitValue: false,
- allowBlank: false,
- fieldLabel: 'Yubico API Id',
- bind: {
- value: '{id}',
- hidden: '{!isYubico}',
- disabled: '{!isYubico}',
- },
- },
- {
- xtype: 'textfield',
- hidden: true,
- submitValue: false,
- allowBlank: false,
- fieldLabel: 'Yubico API Key',
- bind: {
- value: '{key}',
- hidden: '{!isYubico}',
- disabled: '{!isYubico}',
- },
- },
- {
- xtype: 'textfield',
- hidden: true,
- submitValue: false,
- fieldLabel: 'Yubico URL',
- bind: {
- value: '{url}',
- hidden: '{!isYubico}',
- disabled: '{!isYubico}',
- },
- },
- ],
-});
-Ext.define(
- 'PVE.form.TokenSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveTokenSelector'],
-
- allowBlank: false,
- autoSelect: false,
- displayField: 'id',
-
- editable: true,
- anyMatch: true,
- forceSelection: true,
-
- store: {
- model: 'pve-tokens',
- autoLoad: true,
- proxy: {
- type: 'proxmox',
- url: 'api2/json/access/users',
- extraParams: { full: 1 },
- },
- sorters: 'id',
- listeners: {
- load: function (store, records, success) {
- let tokens = [];
- for (const { data: user } of records) {
- if (!user.tokens || user.tokens.length === 0) {
- continue;
- }
- for (const token of user.tokens) {
- tokens.push({
- id: `${user.userid}!${token.tokenid}`,
- comment: token.comment,
- });
- }
- }
- store.loadData(tokens);
- },
- },
- },
-
- listConfig: {
- columns: [
- {
- header: gettext('API Token'),
- sortable: true,
- dataIndex: 'id',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- {
- header: gettext('Comment'),
- sortable: false,
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- ],
- },
- },
- function () {
- Ext.define('pve-tokens', {
- extend: 'Ext.data.Model',
- fields: [
- 'id',
- 'userid',
- 'tokenid',
- 'comment',
- { type: 'boolean', name: 'privsep' },
- { type: 'date', dateFormat: 'timestamp', name: 'expire' },
- ],
- idProperty: 'id',
- });
- },
-);
-Ext.define(
- 'PVE.form.USBSelector',
- {
- extend: 'Proxmox.form.ComboGrid',
- alias: ['widget.pveUSBSelector'],
-
- allowBlank: false,
- autoSelect: false,
- anyMatch: true,
- displayField: 'product_and_id',
- valueField: 'usbid',
- editable: true,
-
- validator: function (value) {
- var me = this;
- if (!value) {
- return true; // handled later by allowEmpty in the getErrors call chain
- }
- value = me.getValue(); // as the valueField is not the displayfield
- if (me.type === 'device') {
- return /^[a-f0-9]{4}:[a-f0-9]{4}$/i.test(value);
- } else if (me.type === 'port') {
- return /^[0-9]+-[0-9]+(\.[0-9]+)*$/.test(value);
- }
- return gettext('Invalid Value');
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- me.store.setProxy({
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/hardware/usb`,
- });
-
- me.store.load();
- },
-
- initComponent: function () {
- var me = this;
-
- if (me.pveSelNode) {
- me.nodename = me.pveSelNode.data.node;
- }
-
- var nodename = me.nodename;
- me.nodename = undefined;
-
- if (me.type !== 'device' && me.type !== 'port') {
- throw 'no valid type specified';
- }
-
- let store = new Ext.data.Store({
- model: `pve-usb-${me.type}`,
- filters: [
- ({ data }) => !!data.usbpath && !!data.prodid && String(data.class) !== '9',
- ],
- });
- let emptyText = '';
- if (me.type === 'device') {
- emptyText = gettext('Passthrough a specific device');
- } else {
- emptyText = gettext('Passthrough a full port');
- }
-
- Ext.apply(me, {
- store: store,
- emptyText: emptyText,
- listConfig: {
- minHeight: 80,
- width: 520,
- columns: [
- {
- header: me.type === 'device' ? gettext('Device') : gettext('Port'),
- sortable: true,
- dataIndex: 'usbid',
- width: 80,
- },
- {
- header: gettext('Manufacturer'),
- sortable: true,
- dataIndex: 'manufacturer',
- width: 150,
- },
- {
- header: gettext('Product'),
- sortable: true,
- dataIndex: 'product',
- flex: 1,
- },
- {
- header: gettext('Speed'),
- width: 75,
- sortable: true,
- dataIndex: 'speed',
- renderer: function (value) {
- let speed2Class = {
- 10000: 'USB 3.1',
- 5000: 'USB 3.0',
- 480: 'USB 2.0',
- 12: 'USB 1.x',
- 1.5: 'USB 1.x',
- };
- return speed2Class[value] || value + ' Mbps';
- },
- },
- ],
- },
- });
-
- me.callParent();
-
- me.setNodename(nodename);
- },
- },
- function () {
- Ext.define('pve-usb-device', {
- extend: 'Ext.data.Model',
- fields: [
- {
- name: 'usbid',
- convert: function (val, data) {
- if (val) {
- return val;
- }
- return data.get('vendid') + ':' + data.get('prodid');
- },
- },
- 'speed',
- 'product',
- 'manufacturer',
- 'vendid',
- 'prodid',
- 'usbpath',
- { name: 'port', type: 'number' },
- { name: 'level', type: 'number' },
- { name: 'class', type: 'number' },
- { name: 'devnum', type: 'number' },
- { name: 'busnum', type: 'number' },
- {
- name: 'product_and_id',
- type: 'string',
- convert: (v, rec) => {
- let res = rec.data.product || gettext('Unknown');
- res += ' (' + rec.data.usbid + ')';
- return res;
- },
- },
- ],
- });
-
- Ext.define('pve-usb-port', {
- extend: 'Ext.data.Model',
- fields: [
- {
- name: 'usbid',
- convert: function (val, data) {
- if (val) {
- return val;
- }
- return data.get('busnum') + '-' + data.get('usbpath');
- },
- },
- 'speed',
- 'product',
- 'manufacturer',
- 'vendid',
- 'prodid',
- 'usbpath',
- { name: 'port', type: 'number' },
- { name: 'level', type: 'number' },
- { name: 'class', type: 'number' },
- { name: 'devnum', type: 'number' },
- { name: 'busnum', type: 'number' },
- {
- name: 'product_and_id',
- type: 'string',
- convert: (v, rec) => {
- let res = rec.data.product || gettext('Unplugged');
- res += ' (' + rec.data.usbid + ')';
- return res;
- },
- },
- ],
- });
- },
-);
-Ext.define('PVE.form.USBMapSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: 'widget.pveUSBMapSelector',
-
- store: {
- fields: ['name', 'vendor', 'device', 'path'],
- filterOnLoad: true,
- sorters: [
- {
- property: 'name',
- direction: 'ASC',
- },
- ],
- },
-
- allowBlank: false,
- autoSelect: false,
- displayField: 'id',
- valueField: 'id',
-
- listConfig: {
- width: 800,
- columns: [
- {
- header: gettext('Name'),
- dataIndex: 'id',
- flex: 1,
- },
- {
- header: gettext('Status'),
- dataIndex: 'errors',
- flex: 2,
- renderer: function (value) {
- let _me = this;
-
- if (!Ext.isArray(value) || !value?.length) {
- return ` ${gettext('Mapping matches host data')}`;
- }
-
- let errors = [];
-
- value.forEach((error) => {
- let iconCls;
- switch (error?.severity) {
- case 'warning':
- iconCls = 'fa-exclamation-circle warning';
- break;
- case 'error':
- iconCls = 'fa-times-circle critical';
- break;
- }
-
- let message = error?.message;
- let icon = ` `;
- if (iconCls !== undefined) {
- errors.push(`${icon} ${message}`);
- }
- });
-
- return errors.join(' ');
- },
- },
- {
- header: gettext('Comment'),
- dataIndex: 'description',
- flex: 1,
- renderer: Ext.String.htmlEncode,
- },
- ],
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
-
- me.nodename = nodename;
-
- me.store.setProxy({
- type: 'proxmox',
- url: `/api2/json/cluster/mapping/usb?check-node=${nodename}`,
- });
-
- me.store.load();
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.nodename;
- me.nodename = undefined;
-
- me.callParent();
-
- me.setNodename(nodename);
- },
-});
-Ext.define('pmx-users', {
- extend: 'Ext.data.Model',
- fields: [
- 'userid',
- 'firstname',
- 'lastname',
- 'email',
- 'comment',
- { type: 'boolean', name: 'enable' },
- { type: 'date', dateFormat: 'timestamp', name: 'expire' },
- ],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/access/users?full=1',
- },
- idProperty: 'userid',
-});
-Ext.define('PVE.form.VlanField', {
- extend: 'Ext.form.field.Number',
- alias: ['widget.pveVlanField'],
-
- deleteEmpty: false,
-
- emptyText: gettext('no VLAN'),
-
- fieldLabel: gettext('VLAN Tag'),
-
- allowBlank: true,
-
- getSubmitData: function () {
- var me = this,
- data = null,
- val;
- if (!me.disabled && me.submitValue) {
- val = me.getSubmitValue();
- if (val) {
- data = {};
- data[me.getName()] = val;
- } else if (me.deleteEmpty) {
- data = {};
- data.delete = me.getName();
- }
- }
- return data;
- },
-
- initComponent: function () {
- var me = this;
-
- Ext.apply(me, {
- minValue: 1,
- maxValue: 4094,
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.form.VMCPUFlagSelector', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.vmcpuflagselector',
-
- mixins: {
- field: 'Ext.form.field.Field',
- },
-
- disableSelection: true,
- columnLines: false,
- selectable: false,
- hideHeaders: true,
-
- scrollable: 'y',
- height: 200,
-
- unkownFlags: [],
-
- store: {
- type: 'store',
- fields: ['name', { name: 'state', defaultValue: '=' }, 'description'],
- autoLoad: true,
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/localhost/capabilities/qemu/cpu-flags',
- },
- listeners: {
- update: function () {
- this.commitChanges();
- },
- refresh: function (store, eOpts) {
- let me = this;
- let view = me.view;
-
- if (store.adjustedForValue !== view.value) {
- view.adjustStoreForValue();
- }
- },
- },
- adjustedForValue: undefined,
- },
-
- getValue: function () {
- let me = this;
- let store = me.getStore();
-
- if (!store.isLoaded()) {
- return me.value;
- }
-
- let flags = '';
-
- store.getData().each(function (rec) {
- let s = rec.get('state');
- if (s && s !== '=') {
- let f = rec.get('name');
- if (flags === '') {
- flags = s + f;
- } else {
- flags += ';' + s + f;
- }
- }
- });
-
- flags += me.unkownFlags.join(';');
-
- return flags;
- },
-
- // Adjusts the store for the current value and determines the unkown flags based on what the
- // store does not know.
- adjustStoreForValue: function () {
- let me = this;
- let store = me.getStore();
- let value = me.value;
-
- me.unkownFlags = [];
-
- store.getData().each((rec) => rec.set('state', '='));
-
- let flags = value ? value.split(';') : [];
- flags.forEach(function (flag) {
- let sign = flag.substr(0, 1);
- flag = flag.substr(1);
-
- let rec = store.findRecord('name', flag, 0, false, true, true);
- if (rec !== null) {
- rec.set('state', sign);
- } else {
- me.unkownFlags.push(flag);
- }
- });
-
- store.adjustedForValue = value;
- },
-
- setValue: function (value) {
- let me = this;
-
- me.value = value || '';
-
- if (me.getStore().isLoaded()) {
- me.adjustStoreForValue();
- } // if not yet loaded, the store will trigger the function
-
- let res = me.mixins.field.setValue.call(me, value);
-
- return res;
- },
- columns: [
- {
- dataIndex: 'state',
- renderer: function (v) {
- switch (v) {
- case '=':
- return 'Default';
- case '-':
- return 'Off';
- case '+':
- return 'On';
- default:
- return 'Unknown';
- }
- },
- width: 65,
- },
- {
- xtype: 'widgetcolumn',
- dataIndex: 'state',
- width: 95,
- onWidgetAttach: function (column, widget, record) {
- let val = record.get('state') || '=';
- widget.down('[inputValue=' + val + ']').setValue(true);
- // TODO: disable if selected CPU model and flag are incompatible
- },
- widget: {
- xtype: 'radiogroup',
- hideLabel: true,
- layout: 'hbox',
- validateOnChange: false,
- value: '=',
- listeners: {
- change: function (f, value) {
- let v = Object.values(value)[0];
- f.getWidgetRecord().set('state', v);
-
- let view = this.up('grid');
- view.dirty = view.getValue() !== view.originalValue;
- view.checkDirty();
- //view.checkChange();
- },
- },
- items: [
- {
- boxLabel: '-',
- boxLabelAlign: 'before',
- inputValue: '-',
- isFormField: false,
- },
- {
- checked: true,
- inputValue: '=',
- isFormField: false,
- },
- {
- boxLabel: '+',
- inputValue: '+',
- isFormField: false,
- },
- ],
- },
- },
- {
- dataIndex: 'name',
- width: 100,
- },
- {
- dataIndex: 'description',
- cellWrap: true,
- flex: 1,
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- me.value = me.originalValue = '';
- me.store.view = me;
-
- me.callParent(arguments);
- },
-});
-/* filter is a javascript builtin, but extjs calls it also filter */
-Ext.define('PVE.form.VMSelector', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.vmselector',
-
- mixins: {
- field: 'Ext.form.field.Field',
- },
-
- allowBlank: true,
- selectAll: false,
- isFormField: true,
-
- plugins: 'gridfilters',
-
- store: {
- model: 'PVEResources',
- sorters: 'vmid',
- },
-
- userCls: 'proxmox-tags-circle',
-
- columnsDeclaration: [
- {
- header: 'ID',
- dataIndex: 'vmid',
- width: 80,
- filter: {
- type: 'number',
- },
- },
- {
- header: gettext('Node'),
- dataIndex: 'node',
- },
- {
- header: gettext('Status'),
- dataIndex: 'status',
- filter: {
- type: 'list',
- },
- },
- {
- header: gettext('Name'),
- dataIndex: 'name',
- flex: 1,
- filter: {
- type: 'string',
- },
- },
- {
- header: gettext('Pool'),
- dataIndex: 'pool',
- filter: {
- type: 'list',
- },
- },
- {
- header: gettext('Type'),
- dataIndex: 'type',
- width: 120,
- renderer: function (value) {
- if (value === 'qemu') {
- return gettext('Virtual Machine');
- } else if (value === 'lxc') {
- return gettext('LXC Container');
- }
-
- return '';
- },
- filter: {
- type: 'list',
- store: {
- data: [
- { id: 'qemu', text: gettext('Virtual Machine') },
- { id: 'lxc', text: gettext('LXC Container') },
- ],
- un: function () {
- // Due to EXTJS-18711. we have to do a static list via a store but to avoid
- // creating an object, we have to have an empty pseudo un function
- },
- },
- },
- },
- {
- header: gettext('Tags'),
- dataIndex: 'tags',
- renderer: (tags) => PVE.Utils.renderTags(tags, PVE.UIOptions.tagOverrides),
- flex: 1,
- },
- {
- header: 'HA ' + gettext('Status'),
- dataIndex: 'hastate',
- flex: 1,
- filter: {
- type: 'list',
- },
- },
- ],
-
- // should be a list of 'dataIndex' values, if 'undefined' all declared columns will be included
- columnSelection: undefined,
-
- selModel: {
- selType: 'checkboxmodel',
- mode: 'SIMPLE',
- },
-
- checkChangeEvents: ['selectionchange', 'change'],
-
- listeners: {
- selectionchange: function () {
- // to trigger validity and error checks
- this.checkChange();
- },
- },
-
- getValue: function () {
- var me = this;
- if (me.savedValue !== undefined) {
- return me.savedValue;
- }
- var sm = me.getSelectionModel();
- var selection = sm.getSelection();
- var values = [];
- var store = me.getStore();
- selection.forEach(function (item) {
- // only add if not filtered
- if (store.findExact('vmid', item.data.vmid) !== -1) {
- values.push(item.data.vmid);
- }
- });
- return values;
- },
-
- setValueSelection: function (value) {
- let me = this;
-
- let store = me.getStore();
- let notFound = [];
- let selection = value
- .map((item) => {
- let found = store.findRecord('vmid', item, 0, false, true, true);
- if (!found) {
- if (Ext.isNumeric(item)) {
- notFound.push(item);
- } else {
- console.warn(`invalid item in vm selection: ${item}`);
- }
- }
- return found;
- })
- .filter((r) => r);
-
- for (const vmid of notFound) {
- let rec = store.add({
- vmid,
- node: 'unknown',
- });
- selection.push(rec[0]);
- }
-
- let sm = me.getSelectionModel();
- if (selection.length) {
- sm.select(selection);
- } else {
- sm.deselectAll();
- }
- // to correctly trigger invalid class
- me.getErrors();
- },
-
- setValue: function (value) {
- let me = this;
- value ??= [];
- if (!Ext.isArray(value)) {
- value = value.split(',').filter((v) => v !== '');
- }
-
- let store = me.getStore();
- if (!store.isLoaded()) {
- me.savedValue = value;
- store.on(
- 'load',
- function () {
- me.setValueSelection(value);
- delete me.savedValue;
- },
- { single: true },
- );
- } else {
- me.setValueSelection(value);
- }
- return me.mixins.field.setValue.call(me, value);
- },
-
- getErrors: function (value) {
- let me = this;
- if (!me.isDisabled() && me.allowBlank === false && me.getValue().length === 0) {
- me.addBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
- return [gettext('No VM selected')];
- }
-
- me.removeBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
- return [];
- },
-
- setDisabled: function (disabled) {
- let me = this;
- let res = me.callParent([disabled]);
- me.getErrors();
- return res;
- },
-
- initComponent: function () {
- let me = this;
-
- let columns = me.columnsDeclaration
- .filter((column) =>
- me.columnSelection ? me.columnSelection.indexOf(column.dataIndex) !== -1 : true,
- )
- .map((x) => x);
-
- me.columns = columns;
-
- me.callParent();
-
- me.getStore().load({ params: { type: 'vm' } });
-
- if (me.nodename) {
- me.getStore().addFilter({
- property: 'node',
- exactMatch: true,
- value: me.nodename,
- });
- }
-
- // only show the relevant guests by default
- if (me.action) {
- let statusfilter = '';
- switch (me.action) {
- case 'startall':
- statusfilter = 'stopped';
- break;
- case 'stopall':
- statusfilter = 'running';
- break;
- }
- if (statusfilter !== '') {
- me.getStore().addFilter([
- {
- property: 'template',
- value: 0,
- },
- {
- id: 'x-gridfilter-status',
- operator: 'in',
- property: 'status',
- value: [statusfilter],
- },
- ]);
- }
- }
-
- if (me.selectAll) {
- me.mon(me.getStore(), 'load', function () {
- me.getSelectionModel().selectAll(false);
- });
- }
- },
-});
-
-Ext.define('PVE.form.VMComboSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: 'widget.vmComboSelector',
-
- valueField: 'vmid',
- displayField: 'vmid',
-
- autoSelect: false,
- editable: true,
- anyMatch: true,
- forceSelection: true,
-
- store: {
- model: 'PVEResources',
- autoLoad: true,
- sorters: 'vmid',
- filters: [
- {
- property: 'type',
- value: /lxc|qemu/,
- },
- ],
- },
-
- listConfig: {
- width: 600,
- plugins: 'gridfilters',
- columns: [
- {
- header: 'ID',
- dataIndex: 'vmid',
- width: 80,
- filter: {
- type: 'number',
- },
- },
- {
- header: gettext('Name'),
- dataIndex: 'name',
- flex: 1,
- filter: {
- type: 'string',
- },
- },
- {
- header: gettext('Node'),
- dataIndex: 'node',
- },
- {
- header: gettext('Status'),
- dataIndex: 'status',
- filter: {
- type: 'list',
- },
- },
- {
- header: gettext('Pool'),
- dataIndex: 'pool',
- hidden: true,
- filter: {
- type: 'list',
- },
- },
- {
- header: gettext('Type'),
- dataIndex: 'type',
- width: 120,
- renderer: function (value) {
- if (value === 'qemu') {
- return gettext('Virtual Machine');
- } else if (value === 'lxc') {
- return gettext('LXC Container');
- }
-
- return '';
- },
- filter: {
- type: 'list',
- store: {
- data: [
- { id: 'qemu', text: gettext('Virtual Machine') },
- { id: 'lxc', text: gettext('LXC Container') },
- ],
- un: function () {
- /* due to EXTJS-18711 */
- },
- },
- },
- },
- {
- header: 'HA ' + gettext('Status'),
- dataIndex: 'hastate',
- hidden: true,
- flex: 1,
- filter: {
- type: 'list',
- },
- },
- ],
- },
-});
-Ext.define('PVE.form.VNCKeyboardSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.VNCKeyboardSelector'],
- comboItems: Object.entries(PVE.Utils.kvm_keymaps),
-});
-/*
- * Top left combobox, used to select a view of the underneath RessourceTree
- */
-Ext.define('PVE.form.ViewSelector', {
- extend: 'Ext.form.field.ComboBox',
- alias: ['widget.pveViewSelector'],
-
- editable: false,
- allowBlank: false,
- forceSelection: true,
- autoSelect: false,
- valueField: 'key',
- displayField: 'value',
- hideLabel: true,
- queryMode: 'local',
-
- initComponent: function () {
- let me = this;
-
- let default_views = {
- server: {
- text: gettext('Server View'),
- groups: ['node'],
- },
- folder: {
- text: gettext('Folder View'),
- groups: ['type'],
- },
- pool: {
- text: gettext('Pool View'),
- groups: ['pool'],
- // Pool View only lists VMs and Containers
- getFilterFn:
- () =>
- ({ data }) =>
- data.type === 'qemu' || data.type === 'lxc' || data.type === 'pool',
- },
- tags: {
- text: gettext('Tag View'),
- groups: ['tag'],
- getFilterFn:
- () =>
- ({ data }) =>
- ['qemu', 'lxc', 'node', 'storage'].indexOf(data.type) !== -1,
- groupRenderer: function (info) {
- let tag = PVE.Utils.renderTags(info.tag, PVE.UIOptions.tagOverrides);
- return `${tag} `;
- },
- itemMap: function (item) {
- let tags = (item.data.tags ?? '').split(/[;, ]/);
- if (tags.length === 1 && tags[0] === '') {
- return item;
- }
- let items = [];
- for (const tag of tags) {
- let id = `${item.data.id}-${tag}`;
- let info = Ext.apply({ leaf: true }, item.data);
- info.tag = tag;
- info.realId = info.id;
- info.id = id;
- items.push(Ext.create('Ext.data.TreeModel', info));
- }
- return items;
- },
- attrMoveChecks: {
- tag: (newitem, olditem) => newitem.data.tags !== olditem.data.tags,
- },
- },
- };
- let groupdef = Object.entries(default_views).map(([name, config]) => [name, config.text]);
-
- let store = Ext.create('Ext.data.Store', {
- model: 'KeyValue',
- proxy: {
- type: 'memory',
- reader: 'array',
- },
- data: groupdef,
- autoload: true,
- });
-
- Ext.apply(me, {
- store: store,
- value: groupdef[0][0],
- getViewFilter: function () {
- let view = me.getValue();
- return Ext.apply({ id: view }, default_views[view] || default_views.server);
- },
- getState: function () {
- return { value: me.getValue() };
- },
- applyState: function (state, doSelect) {
- let view = me.getValue();
- if (state && state.value && view !== state.value) {
- let record = store.findRecord('key', state.value, 0, false, true, true);
- if (record) {
- me.setValue(state.value, true);
- if (doSelect) {
- me.fireEvent('select', me, [record]);
- }
- }
- }
- },
- stateEvents: ['select'],
- stateful: true,
- stateId: 'pveview',
- id: 'view',
- });
-
- me.callParent();
-
- let statechange = function (sp, key, value) {
- if (key === me.id) {
- me.applyState(value, true);
- }
- };
- let sp = Ext.state.Manager.getProvider();
- me.mon(sp, 'statechange', statechange, me);
- },
-});
-Ext.define('PVE.form.iScsiProviderSelector', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveiScsiProviderSelector'],
- comboItems: [
- ['comstar', 'Comstar'],
- ['istgt', 'istgt'],
- ['iet', 'IET'],
- ['LIO', 'LIO'],
- ],
-});
-Ext.define('PVE.form.ColorPicker', {
- extend: 'Ext.form.FieldContainer',
- alias: 'widget.pveColorPicker',
-
- defaultBindProperty: 'value',
-
- config: {
- value: null,
- },
-
- height: 24,
-
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
-
- getValue: function () {
- return this.realvalue.slice(1);
- },
-
- setValue: function (value) {
- let me = this;
- me.setColor(value);
- if (value && value.length === 6) {
- me.picker.value = value[0] !== '#' ? `#${value}` : value;
- }
- },
-
- setColor: function (value) {
- let me = this;
- let oldValue = me.realvalue;
- me.realvalue = value;
- let color = value.length === 6 ? `#${value}` : undefined;
- me.down('#picker').setStyle('background-color', color);
- me.down('#text').setValue(value ?? '');
- me.fireEvent('change', me, me.realvalue, oldValue);
- },
-
- initComponent: function () {
- let me = this;
- me.picker = document.createElement('input');
- me.picker.type = 'color';
- me.picker.style = `opacity: 0; border: 0px; width: 100%; height: ${me.height}px`;
- me.picker.value = `${me.value}`;
-
- me.items = [
- {
- xtype: 'textfield',
- itemId: 'text',
- minLength: !me.allowBlank ? 6 : undefined,
- maxLength: 6,
- enforceMaxLength: true,
- allowBlank: me.allowBlank,
- emptyText: me.allowBlank ? gettext('Automatic') : undefined,
- maskRe: /[a-f0-9]/i,
- regex: /^[a-f0-9]{6}$/i,
- flex: 1,
- listeners: {
- change: function (field, value) {
- me.setValue(value);
- },
- },
- },
- {
- xtype: 'box',
- style: {
- 'margin-left': '1px',
- border: '1px solid #cfcfcf',
- },
- itemId: 'picker',
- width: 24,
- contentEl: me.picker,
- },
- ];
-
- me.callParent();
- me.picker.oninput = function () {
- me.setColor(me.picker.value.slice(1));
- };
- },
-});
-
-Ext.define('PVE.form.TagColorGrid', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveTagColorGrid',
-
- mixins: ['Ext.form.field.Field'],
-
- allowBlank: true,
- selectAll: false,
- isFormField: true,
- deleteEmpty: false,
- selModel: 'checkboxmodel',
-
- config: {
- deleteEmpty: false,
- },
-
- emptyText: gettext('No Overrides'),
- viewConfig: {
- deferEmptyText: false,
- },
-
- setValue: function (value) {
- let me = this;
- let colors;
- if (Ext.isObject(value)) {
- colors = value.colors;
- } else {
- colors = value;
- }
- if (!colors) {
- me.getStore().removeAll();
- me.checkChange();
- return me;
- }
- let entries = (colors.split(';') || []).map((entry) => {
- let [tag, bg, fg] = entry.split(':');
- fg = fg || '';
- return {
- tag,
- color: bg,
- text: fg,
- };
- });
- me.getStore().setData(entries);
- me.checkChange();
- return me;
- },
-
- getValue: function () {
- let me = this;
- let values = [];
- me.getStore().each((rec) => {
- if (rec.data.tag) {
- let val = `${rec.data.tag}:${rec.data.color}`;
- if (rec.data.text) {
- val += `:${rec.data.text}`;
- }
- values.push(val);
- }
- });
- return values.join(';');
- },
-
- getErrors: function (value) {
- let me = this;
- let emptyTag = false;
- let notValidColor = false;
- let colorRegex = new RegExp(/^[0-9a-f]{6}$/i);
- me.getStore().each((rec) => {
- if (!rec.data.tag) {
- emptyTag = true;
- }
- if (!rec.data.color?.match(colorRegex)) {
- notValidColor = true;
- }
- if (rec.data.text && !rec.data.text?.match(colorRegex)) {
- notValidColor = true;
- }
- });
- let errors = [];
- if (emptyTag) {
- errors.push(gettext('Tag must not be empty.'));
- }
- if (notValidColor) {
- errors.push(gettext('Not a valid color.'));
- }
- return errors;
- },
-
- // override framework function to implement deleteEmpty behaviour
- getSubmitData: function () {
- let me = this,
- data = null,
- val;
- if (!me.disabled && me.submitValue) {
- val = me.getValue();
- if (val !== null && val !== '') {
- data = {};
- data[me.getName()] = val;
- } else if (me.getDeleteEmpty()) {
- data = {};
- data.delete = me.getName();
- }
- }
- return data;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- addLine: function () {
- let me = this;
- me.getView().getStore().add({
- tag: '',
- color: '',
- text: '',
- });
- },
-
- removeSelection: function () {
- let me = this;
- let view = me.getView();
- let selection = view.getSelection();
- if (selection === undefined) {
- return;
- }
-
- selection.forEach((sel) => {
- view.getStore().remove(sel);
- });
- view.checkChange();
- },
-
- tagChange: function (field, newValue, oldValue) {
- let me = this;
- let rec = field.getWidgetRecord();
- if (!rec) {
- return;
- }
- if (newValue && newValue !== oldValue) {
- let newrgb = Proxmox.Utils.stringToRGB(newValue);
- let newvalue = Proxmox.Utils.rgbToHex(newrgb);
- if (!rec.get('color')) {
- rec.set('color', newvalue);
- } else if (oldValue) {
- let oldrgb = Proxmox.Utils.stringToRGB(oldValue);
- let oldvalue = Proxmox.Utils.rgbToHex(oldrgb);
- if (rec.get('color') === oldvalue) {
- rec.set('color', newvalue);
- }
- }
- }
- me.fieldChange(field, newValue, oldValue);
- },
-
- backgroundChange: function (field, newValue, oldValue) {
- let me = this;
- let rec = field.getWidgetRecord();
- if (!rec) {
- return;
- }
- if (newValue && newValue !== oldValue) {
- let newrgb = Proxmox.Utils.hexToRGB(newValue);
- let newcls = Proxmox.Utils.getTextContrastClass(newrgb);
- let hexvalue = newcls === 'dark' ? '000000' : 'FFFFFF';
- if (!rec.get('text')) {
- rec.set('text', hexvalue);
- } else if (oldValue) {
- let oldrgb = Proxmox.Utils.hexToRGB(oldValue);
- let oldcls = Proxmox.Utils.getTextContrastClass(oldrgb);
- let oldvalue = oldcls === 'dark' ? '000000' : 'FFFFFF';
- if (rec.get('text') === oldvalue) {
- rec.set('text', hexvalue);
- }
- }
- }
- me.fieldChange(field, newValue, oldValue);
- },
-
- fieldChange: function (field, newValue, oldValue) {
- let me = this;
- let view = me.getView();
- let rec = field.getWidgetRecord();
- if (!rec) {
- return;
- }
- let column = field.getWidgetColumn();
- rec.set(column.dataIndex, newValue);
- view.checkChange();
- },
- },
-
- tbar: [
- {
- text: gettext('Add'),
- handler: 'addLine',
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Remove'),
- handler: 'removeSelection',
- disabled: true,
- },
- ],
-
- columns: [
- {
- header: 'Tag',
- dataIndex: 'tag',
- xtype: 'widgetcolumn',
- onWidgetAttach: function (col, widget, rec) {
- widget.getStore().setData(PVE.UIOptions.tagList.map((v) => ({ tag: v })));
- },
- widget: {
- xtype: 'combobox',
- isFormField: false,
- maskRe: PVE.Utils.tagCharRegex,
- allowBlank: false,
- queryMode: 'local',
- displayField: 'tag',
- valueField: 'tag',
- store: {},
- listeners: {
- change: 'tagChange',
- },
- },
- flex: 1,
- },
- {
- header: gettext('Background'),
- xtype: 'widgetcolumn',
- flex: 1,
- dataIndex: 'color',
- widget: {
- xtype: 'pveColorPicker',
- isFormField: false,
- listeners: {
- change: 'backgroundChange',
- },
- },
- },
- {
- header: gettext('Text'),
- xtype: 'widgetcolumn',
- flex: 1,
- dataIndex: 'text',
- widget: {
- xtype: 'pveColorPicker',
- allowBlank: true,
- isFormField: false,
- listeners: {
- change: 'fieldChange',
- },
- },
- },
- ],
-
- store: {
- listeners: {
- update: function () {
- this.commitChanges();
- },
- },
- },
-
- initComponent: function () {
- let me = this;
- me.callParent();
- me.initField();
- },
-});
-Ext.define('PVE.form.ListField', {
- extend: 'Ext.container.Container',
- alias: 'widget.pveListField',
-
- mixins: ['Ext.form.field.Field'],
-
- // override for column header
- fieldTitle: gettext('Item'),
-
- // will be applied to the textfields
- maskRe: undefined,
-
- allowBlank: true,
- selectAll: false,
- isFormField: true,
- deleteEmpty: false,
- config: {
- deleteEmpty: false,
- },
-
- setValue: function (list) {
- let me = this;
- list = Ext.isArray(list) ? list : (list ?? '').split(';').filter((t) => t !== '');
-
- let store = me.lookup('grid').getStore();
- if (list.length > 0) {
- store.setData(list.map((item) => ({ item })));
- } else {
- store.removeAll();
- }
- me.checkChange();
- return me;
- },
-
- getValue: function () {
- let me = this;
- let values = [];
- me.lookup('grid')
- .getStore()
- .each((rec) => {
- if (rec.data.item) {
- values.push(rec.data.item);
- }
- });
- return values.join(';');
- },
-
- getErrors: function (value) {
- let me = this;
- let empty = false;
- me.lookup('grid')
- .getStore()
- .each((rec) => {
- if (!rec.data.item) {
- empty = true;
- }
- });
- if (empty) {
- return [gettext('Tag must not be empty.')];
- }
- return [];
- },
-
- // override framework function to implement deleteEmpty behaviour
- getSubmitData: function () {
- let me = this,
- data = null,
- val;
- if (!me.disabled && me.submitValue) {
- val = me.getValue();
- if (val !== null && val !== '') {
- data = {};
- data[me.getName()] = val;
- } else if (me.getDeleteEmpty()) {
- data = {};
- data.delete = me.getName();
- }
- }
- return data;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- addLine: function () {
- let me = this;
- me.lookup('grid').getStore().add({
- item: '',
- });
- },
-
- removeSelection: function (field) {
- let me = this;
- let view = me.getView();
- let grid = me.lookup('grid');
-
- let record = field.getWidgetRecord();
- if (record === undefined) {
- // this is sometimes called before a record/column is initialized
- return;
- }
-
- grid.getStore().remove(record);
- view.checkChange();
- view.validate();
- },
-
- itemChange: function (field, newValue) {
- let rec = field.getWidgetRecord();
- if (!rec) {
- return;
- }
- let column = field.getWidgetColumn();
- rec.set(column.dataIndex, newValue);
- let list = field.up('pveListField');
- list.checkChange();
- list.validate();
- },
-
- control: {
- 'grid button': {
- click: 'removeSelection',
- },
- },
- },
-
- items: [
- {
- xtype: 'grid',
- reference: 'grid',
-
- viewConfig: {
- deferEmptyText: false,
- },
-
- store: {
- listeners: {
- update: function () {
- this.commitChanges();
- },
- },
- },
- },
- {
- xtype: 'button',
- text: gettext('Add'),
- iconCls: 'fa fa-plus-circle',
- handler: 'addLine',
- margin: '5 0 0 0',
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- for (const [key, value] of Object.entries(me.gridConfig ?? {})) {
- me.items[0][key] = value;
- }
-
- me.items[0].columns = [
- {
- header: me.fieldTtitle,
- dataIndex: 'item',
- xtype: 'widgetcolumn',
- widget: {
- xtype: 'textfield',
- isFormField: false,
- maskRe: me.maskRe,
- allowBlank: false,
- queryMode: 'local',
- listeners: {
- change: 'itemChange',
- },
- },
- flex: 1,
- },
- {
- xtype: 'widgetcolumn',
- width: 40,
- widget: {
- xtype: 'button',
- iconCls: 'fa fa-trash-o',
- },
- },
- ];
-
- me.callParent();
- me.initField();
- },
-});
-Ext.define('Proxmox.form.Tag', {
- extend: 'Ext.Component',
- alias: 'widget.pveTag',
-
- mode: 'editable',
-
- tag: '',
- cls: 'pve-edit-tag',
-
- tpl: [
- ' ',
- '{tag} ',
- ' ',
- ],
-
- focusable: true,
- getFocusEl: function () {
- return Ext.get(this.tagEl());
- },
-
- onFocus: function () {
- this.selectText();
- },
-
- // contains tags not to show in the picker and not allowing to set
- filter: [],
-
- updateFilter: function (tags) {
- this.filter = tags;
- },
-
- onClick: function (event) {
- let me = this;
- if (event.target.tagName === 'I' && !event.target.classList.contains('handle')) {
- if (me.mode === 'editable') {
- me.destroy();
- return;
- }
- } else if (event.target.tagName !== 'SPAN' || me.mode !== 'editable') {
- return;
- }
- me.selectText();
- },
-
- selectText: function (collapseToEnd) {
- let me = this;
- let tagEl = me.tagEl();
- tagEl.contentEditable = true;
- let range = document.createRange();
- range.selectNodeContents(tagEl);
- if (collapseToEnd) {
- range.collapse(false);
- }
- let sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
-
- me.showPicker();
- },
-
- showPicker: function () {
- let me = this;
- if (!me.picker) {
- me.picker = Ext.widget({
- xtype: 'boundlist',
- minWidth: 70,
- scrollable: true,
- floating: true,
- hidden: true,
- userCls: 'proxmox-tags-full',
- displayField: 'tag',
- itemTpl: [
- '{[Proxmox.Utils.getTagElement(values.tag, PVE.UIOptions.tagOverrides)]}',
- ],
- store: [],
- listeners: {
- select: function (picker, rec) {
- me.tagEl().innerHTML = rec.data.tag;
- me.setTag(rec.data.tag, true);
- me.selectText(true);
- me.setColor(rec.data.tag);
- me.picker.hide();
- },
- },
- });
- }
- me.picker.getStore()?.clearFilter();
- let taglist = PVE.UIOptions.tagList
- .filter((v) => !me.filter.includes(v))
- .map((v) => ({ tag: v }));
- if (taglist.length < 1) {
- return;
- }
- me.picker.getStore().setData(taglist);
- me.picker.showBy(me, 'tl-bl');
- me.picker.setMaxHeight(200);
- },
-
- setMode: function (mode) {
- let me = this;
- let tagEl = me.tagEl();
- if (tagEl) {
- tagEl.contentEditable = mode === 'editable';
- }
- me.removeCls(me.mode);
- me.addCls(mode);
- me.mode = mode;
- if (me.mode !== 'editable') {
- me.picker?.hide();
- }
- },
-
- onKeyPress: function (event) {
- let me = this;
- let key = event.browserEvent.key;
- switch (key) {
- case 'Enter':
- case 'Escape':
- me.fireEvent('keypress', key);
- break;
- case 'ArrowLeft':
- case 'ArrowRight':
- case 'Backspace':
- case 'Delete':
- return;
- default:
- if (key.match(PVE.Utils.tagCharRegex)) {
- return;
- }
- me.setTag(me.tagEl().innerHTML);
- }
- event.browserEvent.preventDefault();
- event.browserEvent.stopPropagation();
- },
-
- // for pasting text
- beforeInput: function (event) {
- let me = this;
- me.updateLayout();
- let tag = event.event.data ?? event.event.dataTransfer?.getData('text/plain');
- if (!tag) {
- return;
- }
- if (tag.match(PVE.Utils.tagCharRegex) === null) {
- event.event.preventDefault();
- event.event.stopPropagation();
- }
- },
-
- onInput: function (event) {
- let me = this;
- me.picker.getStore().filter({
- property: 'tag',
- value: me.tagEl().innerHTML,
- anyMatch: true,
- });
- me.setTag(me.tagEl().innerHTML);
- },
-
- lostFocus: function (list, event) {
- let me = this;
- me.picker?.hide();
- window.getSelection().removeAllRanges();
- },
-
- setColor: function (tag) {
- let me = this;
- let rgb = PVE.UIOptions.tagOverrides[tag] ?? Proxmox.Utils.stringToRGB(tag);
-
- let cls = Proxmox.Utils.getTextContrastClass(rgb);
- let color = Proxmox.Utils.rgbToCss(rgb);
- me.setUserCls(`proxmox-tag-${cls}`);
- me.setStyle('background-color', color);
- if (rgb.length > 3) {
- let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]);
-
- me.setStyle('color', fgcolor);
- } else {
- me.setStyle('color');
- }
- },
-
- setTag: function (tag) {
- let me = this;
- let oldtag = me.tag;
- me.tag = tag;
-
- clearTimeout(me.colorTimeout);
- me.colorTimeout = setTimeout(() => me.setColor(tag), 200);
-
- me.updateLayout();
- if (oldtag !== tag) {
- me.fireEvent('change', me, tag, oldtag);
- }
- },
-
- tagEl: function () {
- return this.el?.dom?.getElementsByTagName('span')?.[0];
- },
-
- listeners: {
- click: 'onClick',
- focusleave: 'lostFocus',
- keydown: 'onKeyPress',
- beforeInput: 'beforeInput',
- input: 'onInput',
- element: 'el',
- scope: 'this',
- },
-
- initComponent: function () {
- let me = this;
-
- me.data = {
- tag: me.tag,
- };
-
- me.setTag(me.tag);
- me.setColor(me.tag);
- me.setMode(me.mode ?? 'normal');
- me.callParent();
- },
-
- destroy: function () {
- let me = this;
- if (me.picker) {
- Ext.destroy(me.picker);
- }
- clearTimeout(me.colorTimeout);
- me.callParent();
- },
-});
-Ext.define('PVE.panel.TagEditContainer', {
- extend: 'Ext.container.Container',
- alias: 'widget.pveTagEditContainer',
-
- layout: {
- type: 'hbox',
- align: 'middle',
- },
-
- // set to false to hide the 'no tags' field and the edit button
- canEdit: true,
- editOnly: false,
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- loadTags: function (tagstring = '', force = false) {
- let me = this;
- let view = me.getView();
-
- if (me.oldTags === tagstring && !force) {
- return;
- }
-
- view.suspendLayout = true;
- me.forEachTag((tag) => {
- view.remove(tag);
- });
- me.getViewModel().set('tagCount', 0);
- let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || [];
- newtags.forEach((tag) => {
- me.addTag(tag);
- });
- view.suspendLayout = false;
- view.updateLayout();
- if (!force) {
- me.oldTags = tagstring;
- }
- me.tagsChanged();
- },
-
- onRender: function (v) {
- let me = this;
- let view = me.getView();
- view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
-
- view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), {
- getDragData: function (e) {
- let source = e.getTarget('.handle');
- if (!source) {
- return undefined;
- }
- let sourceId = source.parentNode.id;
- let cmp = Ext.getCmp(sourceId);
- let ddel = document.createElement('div');
- ddel.classList.add('proxmox-tags-full');
- ddel.innerHTML = Proxmox.Utils.getTagElement(
- cmp.tag,
- PVE.UIOptions.tagOverrides,
- );
- let repairXY = Ext.fly(source).getXY();
- cmp.setDisabled(true);
- ddel.id = Ext.id();
- return {
- ddel,
- repairXY,
- sourceId,
- };
- },
- onMouseUp: function (target, e, id) {
- let cmp = Ext.getCmp(this.dragData.sourceId);
- if (cmp && !cmp.isDestroyed) {
- cmp.setDisabled(false);
- }
- },
- getRepairXY: function () {
- return this.dragData.repairXY;
- },
- beforeInvalidDrop: function (target, e, id) {
- let cmp = Ext.getCmp(this.dragData.sourceId);
- if (cmp && !cmp.isDestroyed) {
- cmp.setDisabled(false);
- }
- },
- });
- view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), {
- getTargetFromEvent: function (e) {
- return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light');
- },
- getIndicator: function () {
- if (!view.indicator) {
- view.indicator = Ext.create('Ext.Component', {
- floating: true,
- html: ' ',
- hidden: true,
- shadow: false,
- });
- }
- return view.indicator;
- },
- onContainerOver: function () {
- this.getIndicator().setVisible(false);
- },
- notifyOut: function () {
- this.getIndicator().setVisible(false);
- },
- onNodeOver: function (target, dd, e, data) {
- let indicator = this.getIndicator();
- indicator.setVisible(true);
- indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]);
- return this.dropAllowed;
- },
- onNodeDrop: function (target, dd, e, data) {
- this.getIndicator().setVisible(false);
- let sourceCmp = Ext.getCmp(data.sourceId);
- if (!sourceCmp) {
- return;
- }
- sourceCmp.setDisabled(false);
- let targetCmp = Ext.getCmp(target.id);
- view.remove(sourceCmp, { destroy: false });
- view.insert(view.items.indexOf(targetCmp), sourceCmp);
- me.tagsChanged();
- },
- });
- },
-
- forEachTag: function (func) {
- let me = this;
- let view = me.getView();
- view.items.each((field) => {
- if (field.getXType() === 'pveTag') {
- func(field);
- }
- return true;
- });
- },
-
- toggleEdit: function (cancel) {
- let me = this;
- let vm = me.getViewModel();
- let view = me.getView();
- let editMode = !vm.get('editMode');
- vm.set('editMode', editMode);
-
- // get a current tag list for editing
- if (editMode) {
- PVE.UIOptions.update();
- }
-
- me.forEachTag((tag) => {
- tag.setMode(editMode ? 'editable' : 'normal');
- });
-
- if (!vm.get('editMode')) {
- let tags = [];
- if (cancel) {
- me.loadTags(me.oldTags, true);
- } else {
- let toRemove = [];
- me.forEachTag((cmp) => {
- if (cmp.isVisible() && cmp.tag) {
- tags.push(cmp.tag);
- } else {
- toRemove.push(cmp);
- }
- });
- toRemove.forEach((cmp) => view.remove(cmp));
- tags = tags.join(',');
- if (me.oldTags !== tags) {
- me.oldTags = tags;
- me.loadTags(tags, true);
- me.getView().fireEvent('change', tags);
- }
- }
- }
- me.getView().updateLayout();
- },
-
- tagsChanged: function () {
- let me = this;
- let tags = [];
- me.forEachTag((cmp) => {
- if (cmp.tag) {
- tags.push(cmp.tag);
- }
- });
- me.getViewModel().set('isDirty', me.oldTags !== tags.join(','));
- me.forEachTag((cmp) => {
- cmp.updateFilter(tags);
- });
- },
-
- addTag: function (tag, isNew) {
- let me = this;
- let view = me.getView();
- let vm = me.getViewModel();
- let index = view.items.length - 5;
- if (PVE.UIOptions.shouldSortTags() && !isNew) {
- index = view.items.findIndexBy((tagField) => {
- if (tagField.reference === 'noTagsField') {
- return false;
- }
- if (tagField.xtype !== 'pveTag') {
- return true;
- }
- let a = tagField.tag.toLowerCase();
- let b = tag.toLowerCase();
- return a > b ? true : a < b ? false : tagField.tag.localeCompare(tag) > 0;
- }, 1);
- }
- let tagField = view.insert(index, {
- xtype: 'pveTag',
- tag,
- mode: vm.get('editMode') ? 'editable' : 'normal',
- listeners: {
- change: 'tagsChanged',
- destroy: function () {
- vm.set('tagCount', vm.get('tagCount') - 1);
- me.tagsChanged();
- },
- keypress: function (key) {
- if (vm.get('hideFinishButtons')) {
- return;
- }
- if (key === 'Enter') {
- me.editClick();
- } else if (key === 'Escape') {
- me.cancelClick();
- }
- },
- },
- });
-
- if (isNew) {
- me.tagsChanged();
- tagField.selectText();
- }
-
- vm.set('tagCount', vm.get('tagCount') + 1);
- },
-
- addTagClick: function (event) {
- let me = this;
- me.lookup('noTagsField').setVisible(false);
- me.addTag('', true);
- },
-
- cancelClick: function () {
- this.toggleEdit(true);
- },
-
- editClick: function () {
- this.toggleEdit(false);
- },
-
- init: function (view) {
- let me = this;
- if (view.tags) {
- me.loadTags(view.tags);
- }
- me.getViewModel().set('canEdit', view.canEdit);
- me.getViewModel().set('editOnly', view.editOnly);
-
- me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
- let vm = me.getViewModel();
- view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
- me.loadTags(me.oldTags, !vm.get('editMode')); // refresh tag colors and order
- });
-
- if (view.editOnly) {
- me.toggleEdit();
- }
- },
- },
-
- getTags: function () {
- let me = this;
- let controller = me.getController();
- let tags = [];
- controller.forEachTag((cmp) => {
- if (cmp.tag.length) {
- tags.push(cmp.tag);
- }
- });
-
- return tags;
- },
-
- viewModel: {
- data: {
- tagCount: 0,
- editMode: false,
- canEdit: true,
- isDirty: false,
- editOnly: true,
- },
-
- formulas: {
- hideNoTags: function (get) {
- return get('tagCount') !== 0 || !get('canEdit');
- },
- hideEditBtn: function (get) {
- return get('editMode') || !get('canEdit');
- },
- hideFinishButtons: function (get) {
- return !get('editMode') || get('editOnly');
- },
- },
- },
-
- loadTags: function () {
- return this.getController().loadTags(...arguments);
- },
-
- items: [
- {
- xtype: 'box',
- reference: 'noTagsField',
- bind: {
- hidden: '{hideNoTags}',
- },
- html: gettext('No Tags'),
- style: {
- opacity: 0.5,
- },
- },
- {
- xtype: 'button',
- iconCls: 'fa fa-plus',
- tooltip: gettext('Add Tag'),
- bind: {
- hidden: '{!editMode}',
- },
- hidden: true,
- margin: '0 8 0 5',
- ui: 'default-toolbar',
- handler: 'addTagClick',
- },
- {
- xtype: 'tbseparator',
- ui: 'horizontal',
- bind: {
- hidden: '{hideFinishButtons}',
- },
- hidden: true,
- },
- {
- xtype: 'button',
- iconCls: 'fa fa-times',
- tooltip: gettext('Cancel Edit'),
- bind: {
- hidden: '{hideFinishButtons}',
- },
- hidden: true,
- margin: '0 5 0 0',
- ui: 'default-toolbar',
- handler: 'cancelClick',
- },
- {
- xtype: 'button',
- iconCls: 'fa fa-check',
- tooltip: gettext('Finish Edit'),
- bind: {
- hidden: '{hideFinishButtons}',
- disabled: '{!isDirty}',
- },
- hidden: true,
- handler: 'editClick',
- },
- {
- xtype: 'box',
- cls: 'pve-tag-inline-button',
- html: ` `,
- bind: {
- hidden: '{hideEditBtn}',
- },
- listeners: {
- click: 'editClick',
- element: 'el',
- },
- },
- ],
-
- listeners: {
- render: 'onRender',
- },
-
- destroy: function () {
- let me = this;
- Ext.destroy(me.dragzone);
- Ext.destroy(me.dropzone);
- Ext.destroy(me.indicator);
- me.callParent();
- },
-});
-// mostly copied from ExtJS FileButton, but added 'multiple' at the relevant
-// places so we have a file picker where one can select multiple files
-// changes are marked with an 'pmx:' comment
-Ext.define('PVE.form.MultiFileButton', {
- extend: 'Ext.form.field.FileButton',
- alias: 'widget.pveMultiFileButton',
-
- afterTpl: [
- ' accept="{accept}"',
- 'tabindex="{tabIndex}" ',
- '>',
- ],
-
- createFileInput: function (isTemporary) {
- var me = this,
- fileInputEl,
- listeners;
-
- fileInputEl = me.fileInputEl = me.el.createChild(
- {
- name: me.inputName || me.id,
- multiple: true, // pmx: added multiple option
- id: !isTemporary ? me.id + '-fileInputEl' : undefined,
- cls: me.inputCls + (me.getInherited().rtl ? ' ' + Ext.baseCSSPrefix + 'rtl' : ''),
- tag: 'input',
- type: 'file',
- size: 1,
- unselectable: 'on',
- },
- me.afterInputGuard,
- ); // Nothing special happens outside of IE/Edge
-
- // This is our focusEl
- fileInputEl.dom.setAttribute('data-componentid', me.id);
-
- if (me.tabIndex !== null) {
- me.setTabIndex(me.tabIndex);
- }
-
- if (me.accept) {
- fileInputEl.dom.setAttribute('accept', me.accept);
- }
-
- // We place focus and blur listeners on fileInputEl to activate Button's
- // focus and blur style treatment
- listeners = {
- scope: me,
- change: me.fireChange,
- mousedown: me.handlePrompt,
- keydown: me.handlePrompt,
- focus: me.onFileFocus,
- blur: me.onFileBlur,
- };
-
- if (me.useTabGuards) {
- listeners.keydown = me.onFileInputKeydown;
- }
-
- fileInputEl.on(listeners);
- },
-});
-Ext.define('PVE.form.TagFieldSet', {
- extend: 'Ext.form.FieldSet',
- alias: 'widget.pveTagFieldSet',
- mixins: ['Ext.form.field.Field'],
-
- title: gettext('Tags'),
- padding: '0 5 5 5',
-
- getValue: function () {
- let me = this;
- let tags = me
- .down('pveTagEditContainer')
- .getTags()
- .filter((t) => t !== '');
- return tags.join(';');
- },
-
- setValue: function (value) {
- let me = this;
- value ??= [];
- if (!Ext.isArray(value)) {
- value = value.split(/[;, ]/).filter((t) => t !== '');
- }
- me.down('pveTagEditContainer').loadTags(value.join(';'));
- },
-
- getErrors: function (value) {
- value ??= [];
- if (!Ext.isArray(value)) {
- value = value.split(/[;, ]/).filter((t) => t !== '');
- }
- if (value.some((t) => !t.match(PVE.Utils.tagCharRegex))) {
- return [gettext('Tags contain invalid characters.')];
- }
- return [];
- },
-
- getSubmitData: function () {
- let me = this;
- let value = me.getValue();
- if (me.disabled || !me.submitValue || value === '') {
- return null;
- }
- let data = {};
- data[me.getName()] = value;
- return data;
- },
-
- layout: 'fit',
-
- items: [
- {
- xtype: 'pveTagEditContainer',
- userCls: 'proxmox-tags-full proxmox-tag-fieldset',
- editOnly: true,
- allowBlank: true,
- layout: 'column',
- scrollable: true,
- },
- ],
-
- initComponent: function () {
- let me = this;
- me.callParent();
- me.initField();
- },
-});
-Ext.define('PVE.form.IsoSelector', {
- extend: 'Ext.container.Container',
- alias: 'widget.pveIsoSelector',
- mixins: ['Ext.form.field.Field', 'Proxmox.Mixin.CBind'],
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- nodename: undefined,
- insideWizard: false,
- labelWidth: undefined,
- labelAlign: 'right',
-
- cbindData: function () {
- let me = this;
- return {
- nodename: me.nodename,
- insideWizard: me.insideWizard,
- };
- },
-
- getValue: function () {
- return this.lookup('file').getValue();
- },
-
- setValue: function (value) {
- let me = this;
- if (!value) {
- me.lookup('file').reset();
- return;
- }
- var match = value.match(/^([^:]+):/);
- if (match) {
- me.lookup('storage').setValue(match[1]);
- me.lookup('file').setValue(value);
- }
- },
-
- getErrors: function () {
- let me = this;
- me.lookup('storage').validate();
- let file = me.lookup('file');
- file.validate();
- let value = file.getValue();
- if (!value || !value.length) {
- return ['']; // for validation
- }
- return [];
- },
-
- setNodename: function (nodename) {
- let me = this;
- me.lookup('storage').setNodename(nodename);
- me.lookup('file').setStorage(undefined, nodename);
- },
-
- setDisabled: function (disabled) {
- let me = this;
- me.lookup('storage').setDisabled(disabled);
- me.lookup('file').setDisabled(disabled);
- return me.callParent([disabled]);
- },
-
- referenceHolder: true,
-
- items: [
- {
- xtype: 'pveStorageSelector',
- reference: 'storage',
- isFormField: false,
- fieldLabel: gettext('Storage'),
- storageContent: 'iso',
- allowBlank: false,
- cbind: {
- nodename: '{nodename}',
- autoSelect: '{insideWizard}',
- insideWizard: '{insideWizard}',
- disabled: '{disabled}',
- labelWidth: '{labelWidth}',
- labelAlign: '{labelAlign}',
- },
- listeners: {
- change: function (f, value) {
- let me = this;
- let selector = me.up('pveIsoSelector');
- selector.lookup('file').setStorage(value);
- selector.checkChange();
- },
- },
- },
- {
- xtype: 'pveFileSelector',
- reference: 'file',
- isFormField: false,
- storageContent: 'iso',
- fieldLabel: gettext('ISO image'),
- labelAlign: 'right',
- cbind: {
- nodename: '{nodename}',
- disabled: '{disabled}',
- labelWidth: '{labelWidth}',
- labelAlign: '{labelAlign}',
- },
- allowBlank: false,
- listeners: {
- change: function () {
- this.up('pveIsoSelector').checkChange();
- },
- },
- },
- ],
-});
-Ext.define('PVE.grid.BackupView', {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveBackupView'],
-
- onlineHelp: 'chapter_vzdump',
-
- stateful: true,
- stateId: 'grid-guest-backup',
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- var vmtype = me.pveSelNode.data.type;
- if (!vmtype) {
- throw 'no VM type specified';
- }
-
- var vmtypeFilter;
- if (vmtype === 'lxc' || vmtype === 'openvz') {
- vmtypeFilter = function (item) {
- return PVE.Utils.volume_is_lxc_backup(item.data);
- };
- } else if (vmtype === 'qemu') {
- vmtypeFilter = function (item) {
- return PVE.Utils.volume_is_qemu_backup(item.data);
- };
- } else {
- throw "unsupported VM type '" + vmtype + "'";
- }
-
- let vmname = me.pveSelNode.data.name;
-
- var searchFilter = {
- property: 'volid',
- value: '',
- anyMatch: true,
- caseSensitive: false,
- };
-
- var vmidFilter = {
- property: 'vmid',
- value: vmid,
- exactMatch: true,
- };
-
- me.store = Ext.create('Ext.data.Store', {
- model: 'pve-storage-content',
- sorters: [
- {
- property: 'vmid',
- direction: 'ASC',
- },
- {
- property: 'vdate',
- direction: 'DESC',
- },
- ],
- filters: [vmtypeFilter, searchFilter, vmidFilter],
- });
-
- let updateFilter = function () {
- me.store.filter([vmtypeFilter, searchFilter, vmidFilter]);
- };
-
- const reload = Ext.Function.createBuffered((options) => {
- if (me.store) {
- me.store.load(options);
- }
- }, 100);
-
- let isPBS = false;
- var setStorage = function (storage) {
- var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content';
- url += '?content=backup';
-
- me.store.setProxy({
- type: 'proxmox',
- url: url,
- });
-
- Proxmox.Utils.monStoreErrors(me.view, me.store, true);
-
- reload();
- };
-
- let file_restore_btn;
-
- var storagesel = Ext.create('PVE.form.StorageSelector', {
- nodename: nodename,
- fieldLabel: gettext('Storage'),
- labelAlign: 'right',
- storageContent: 'backup',
- allowBlank: false,
- listeners: {
- change: function (f, value) {
- let storage = f.getStore().findRecord('storage', value, 0, false, true, true);
- if (storage) {
- isPBS = storage.data.type === 'pbs';
- me.getColumns().forEach((column) => {
- let id = column.dataIndex;
- if (id === 'verification' || id === 'encrypted') {
- column.setHidden(!isPBS);
- }
- });
- } else {
- isPBS = false;
- }
- setStorage(value);
- if (file_restore_btn) {
- file_restore_btn.setHidden(!isPBS);
- }
- },
- },
- });
-
- var storagefilter = Ext.create('Ext.form.field.Text', {
- fieldLabel: gettext('Search'),
- labelWidth: 50,
- labelAlign: 'right',
- enableKeyEvents: true,
- value: searchFilter.value,
- listeners: {
- buffer: 500,
- keyup: function (field) {
- me.store.clearFilter(true);
- searchFilter.value = field.getValue();
- updateFilter();
- },
- },
- });
-
- var vmidfilterCB = Ext.create('Ext.form.field.Checkbox', {
- boxLabel: gettext('Filter VMID'),
- value: '1',
- listeners: {
- change: function (cb, value) {
- vmidFilter.value = value ? vmid : '';
- vmidFilter.exactMatch = !!value;
- updateFilter();
- },
- },
- });
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var backup_btn = Ext.create('Ext.button.Button', {
- text: gettext('Backup now'),
- handler: function () {
- var win = Ext.create('PVE.window.Backup', {
- nodename: nodename,
- vmid: vmid,
- vmtype: vmtype,
- vmname: vmname,
- storage: storagesel.getValue(),
- listeners: {
- close: function () {
- reload();
- },
- },
- });
- win.show();
- },
- });
-
- var restore_btn = Ext.create('Proxmox.button.Button', {
- text: gettext('Restore'),
- disabled: true,
- selModel: sm,
- enableFn: function (rec) {
- return !!rec;
- },
- handler: function (b, e, rec) {
- let win = Ext.create('PVE.window.Restore', {
- nodename: nodename,
- vmid: vmid,
- vmname: vmname,
- volid: rec.data.volid,
- volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
- vmtype: vmtype,
- isPBS: isPBS,
- });
- win.show();
- win.on('destroy', reload);
- },
- });
-
- let delete_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- dangerous: true,
- delay: 5,
- enableFn: (rec) => !rec?.data?.protected,
- confirmMsg: ({ data }) => {
- let msg = Ext.String.format(
- gettext('Are you sure you want to remove entry {0}'),
- `'${data.volid}'`,
- );
- return msg + ' ' + gettext('This will permanently erase all data.');
- },
- getUrl: ({ data }) =>
- `/nodes/${nodename}/storage/${storagesel.getValue()}/content/${data.volid}`,
- callback: () => reload(),
- });
-
- let config_btn = Ext.create('Proxmox.button.Button', {
- text: gettext('Show Configuration'),
- disabled: true,
- selModel: sm,
- enableFn: (rec) => !!rec,
- handler: function (b, e, rec) {
- let storage = storagesel.getValue();
- if (!storage) {
- return;
- }
- Ext.create('PVE.window.BackupConfig', {
- volume: rec.data.volid,
- pveSelNode: me.pveSelNode,
- autoShow: true,
- });
- },
- });
-
- // declared above so that the storage selector can change this buttons hidden state
- file_restore_btn = Ext.create('Proxmox.button.Button', {
- text: gettext('File Restore'),
- disabled: true,
- selModel: sm,
- enableFn: (rec) => !!rec && isPBS,
- hidden: !isPBS,
- handler: function (b, e, rec) {
- let storage = storagesel.getValue();
- let isVMArchive = PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format);
- Ext.create('Proxmox.window.FileBrowser', {
- title: gettext('File Restore') + ' - ' + rec.data.text,
- listURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/list`,
- downloadURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/download`,
- extraParams: {
- volume: rec.data.volid,
- },
- archive: isVMArchive ? 'all' : undefined,
- autoShow: true,
- });
- },
- });
-
- Ext.apply(me, {
- selModel: sm,
- tbar: {
- overflowHandler: 'scroller',
- items: [
- backup_btn,
- '-',
- restore_btn,
- file_restore_btn,
- config_btn,
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit Notes'),
- disabled: true,
- handler: function () {
- let volid = sm.getSelection()[0].data.volid;
- var storage = storagesel.getValue();
- Ext.create('Proxmox.window.Edit', {
- autoLoad: true,
- width: 600,
- height: 400,
- resizable: true,
- title: gettext('Notes'),
- url: `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`,
- layout: 'fit',
- items: [
- {
- xtype: 'textarea',
- layout: 'fit',
- name: 'notes',
- height: '100%',
- },
- ],
- listeners: {
- destroy: () => reload(),
- },
- }).show();
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Change Protection'),
- disabled: true,
- handler: function (button, event, record) {
- let volid = record.data.volid,
- storage = storagesel.getValue();
- let url = `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`;
- Proxmox.Utils.API2Request({
- url: url,
- method: 'PUT',
- waitMsgTarget: me,
- params: {
- protected: record.data.protected ? 0 : 1,
- },
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- success: () => {
- reload({
- callback: () =>
- sm.fireEvent('selectionchange', sm, [record]),
- });
- },
- });
- },
- },
- '-',
- delete_btn,
- '->',
- storagesel,
- '-',
- vmidfilterCB,
- storagefilter,
- ],
- },
- columns: [
- {
- header: gettext('Name'),
- flex: 2,
- sortable: true,
- renderer: PVE.Utils.render_storage_content,
- dataIndex: 'volid',
- },
- {
- header: gettext('Notes'),
- dataIndex: 'notes',
- flex: 1,
- renderer: Ext.htmlEncode,
- },
- {
- header: ` `,
- tooltip: gettext('Protected'),
- width: 30,
- renderer: (v) =>
- v ? ` ` : '',
- sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0),
- dataIndex: 'protected',
- },
- {
- header: gettext('Date'),
- width: 150,
- dataIndex: 'vdate',
- },
- {
- header: gettext('Format'),
- width: 100,
- dataIndex: 'format',
- },
- {
- header: gettext('Size'),
- width: 100,
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'size',
- },
- {
- header: 'VMID',
- dataIndex: 'vmid',
- hidden: true,
- },
- {
- header: gettext('Encrypted'),
- dataIndex: 'encrypted',
- renderer: PVE.Utils.render_backup_encryption,
- },
- {
- // TRANSLATORS: The state of the verification task
- header: gettext('Verify State'),
- dataIndex: 'verification',
- renderer: PVE.Utils.render_backup_verification,
- },
- ],
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.FirewallAliasEdit', {
- extend: 'Proxmox.window.Edit',
-
- base_url: undefined,
-
- alias_name: undefined,
-
- width: 400,
-
- initComponent: function () {
- let me = this;
-
- me.isCreate = me.alias_name === undefined;
-
- if (me.isCreate) {
- me.url = '/api2/extjs' + me.base_url;
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name;
- me.method = 'PUT';
- }
-
- let ipanel = Ext.create('Proxmox.panel.InputPanel', {
- isCreate: me.isCreate,
- items: [
- {
- xtype: 'textfield',
- name: me.isCreate ? 'name' : 'rename',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'cidr',
- fieldLabel: gettext('IP/CIDR'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'comment',
- fieldLabel: gettext('Comment'),
- },
- ],
- });
-
- Ext.apply(me, {
- subject: gettext('Alias'),
- isAdd: true,
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- let values = response.result.data;
- values.rename = values.name;
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-
-Ext.define('pve-fw-aliases', {
- extend: 'Ext.data.Model',
-
- fields: ['name', 'cidr', 'comment', 'digest'],
- idProperty: 'name',
-});
-
-Ext.define('PVE.FirewallAliases', {
- extend: 'Ext.grid.Panel',
- alias: ['widget.pveFirewallAliases'],
-
- onlineHelp: 'pve_firewall_ip_aliases',
-
- stateful: true,
- stateId: 'grid-firewall-aliases',
-
- base_url: undefined,
-
- title: gettext('Alias'),
-
- initComponent: function () {
- let me = this;
-
- if (!me.base_url) {
- throw 'missing base_url configuration';
- }
-
- let store = new Ext.data.Store({
- model: 'pve-fw-aliases',
- proxy: {
- type: 'proxmox',
- url: '/api2/json' + me.base_url,
- },
- sorters: {
- property: 'name',
- direction: 'ASC',
- },
- });
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let caps = Ext.state.Manager.get('GuiCap');
- let canEdit =
- !!caps.vms['VM.Config.Network'] ||
- !!caps.dc['Sys.Modify'] ||
- !!caps.nodes['Sys.Modify'];
-
- let reload = function () {
- let oldrec = sm.getSelection()[0];
- store.load(function (records, operation, success) {
- if (oldrec) {
- let rec = store.findRecord('name', oldrec.data.name, 0, false, true, true);
- if (rec) {
- sm.select(rec);
- }
- }
- });
- };
-
- let run_editor = function () {
- let rec = me.getSelectionModel().getSelection()[0];
- if (!rec || !canEdit) {
- return;
- }
- let win = Ext.create('PVE.FirewallAliasEdit', {
- base_url: me.base_url,
- alias_name: rec.data.name,
- });
- win.show();
- win.on('destroy', reload);
- };
-
- me.editBtn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- enableFn: (rec) => canEdit,
- handler: run_editor,
- });
-
- me.addBtn = Ext.create('Ext.Button', {
- text: gettext('Add'),
- disabled:
- !caps.vms['VM.Config.Network'] &&
- !caps.dc['Sys.Modify'] &&
- !caps.nodes['Sys.Modify'],
- handler: function () {
- var win = Ext.create('PVE.FirewallAliasEdit', {
- base_url: me.base_url,
- });
- win.on('destroy', reload);
- win.show();
- },
- });
-
- me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
- disabled: true,
- selModel: sm,
- enableFn: (rec) =>
- !!caps.vms['VM.Config.Network'] ||
- !!caps.dc['Sys.Modify'] ||
- !!caps.nodes['Sys.Modify'],
- baseurl: me.base_url + '/',
- callback: reload,
- });
-
- Ext.apply(me, {
- store: store,
- tbar: [me.addBtn, me.removeBtn, me.editBtn],
- selModel: sm,
- columns: [
- {
- header: gettext('Name'),
- dataIndex: 'name',
- flex: 1,
- },
- {
- header: gettext('IP/CIDR'),
- dataIndex: 'cidr',
- flex: 1,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- flex: 3,
- },
- ],
- listeners: {
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
- me.on('activate', reload);
- },
-});
-Ext.define('PVE.FirewallOptions', {
- extend: 'Proxmox.grid.ObjectGrid',
- alias: ['widget.pveFirewallOptions'],
-
- fwtype: undefined, // 'dc', 'node', 'vm' or 'vnet'
-
- base_url: undefined,
-
- initComponent: function () {
- var me = this;
-
- if (!['dc', 'node', 'vm', 'vnet'].includes(me.fwtype)) {
- throw 'unknown firewall option type';
- }
-
- if (me.fwtype === 'node') {
- me.cwidth1 = 250;
- }
-
- let caps = Ext.state.Manager.get('GuiCap');
- let canEdit =
- caps.vms['VM.Config.Network'] || caps.dc['Sys.Modify'] || caps.nodes['Sys.Modify'];
-
- me.rows = {};
-
- var add_boolean_row = function (name, text, defaultValue) {
- me.add_boolean_row(name, text, { defaultValue: defaultValue });
- };
- var add_integer_row = function (name, text, minValue, labelWidth) {
- me.add_integer_row(name, text, {
- minValue: minValue,
- deleteEmpty: true,
- labelWidth: labelWidth,
- renderer: function (value) {
- if (value === undefined) {
- return Proxmox.Utils.defaultText;
- }
-
- return value;
- },
- });
- };
-
- var add_log_row = function (name, labelWidth) {
- me.rows[name] = {
- header: name,
- required: true,
- defaultValue: 'nolog',
- editor: {
- xtype: 'proxmoxWindowEdit',
- subject: name,
- fieldDefaults: { labelWidth: labelWidth || 100 },
- items: {
- xtype: 'pveFirewallLogLevels',
- name: name,
- fieldLabel: name,
- },
- },
- };
- };
-
- if (me.fwtype === 'node') {
- me.rows.enable = {
- required: true,
- defaultValue: 1,
- header: gettext('Firewall'),
- renderer: Proxmox.Utils.format_boolean,
- editor: {
- xtype: 'pveFirewallEnableEdit',
- defaultValue: 1,
- },
- };
- add_boolean_row('nosmurfs', gettext('SMURFS filter'), 1);
- add_boolean_row('tcpflags', gettext('TCP flags filter'), 0);
- add_boolean_row('ndp', 'NDP', 1);
- add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 32768, 120);
- add_integer_row(
- 'nf_conntrack_tcp_timeout_established',
- 'nf_conntrack_tcp_timeout_established',
- 7875,
- 250,
- );
- add_log_row('log_level_in');
- add_log_row('log_level_out');
- add_log_row('log_level_forward');
- add_log_row('tcp_flags_log_level', 120);
- add_log_row('smurf_log_level');
- add_boolean_row('nftables', gettext('nftables (tech preview)'), 0);
- } else if (me.fwtype === 'vm') {
- me.rows.enable = {
- required: true,
- defaultValue: 0,
- header: gettext('Firewall'),
- renderer: Proxmox.Utils.format_boolean,
- editor: {
- xtype: 'pveFirewallEnableEdit',
- defaultValue: 0,
- },
- };
- add_boolean_row('dhcp', 'DHCP', 1);
- add_boolean_row('ndp', 'NDP', 1);
- add_boolean_row('radv', gettext('Router Advertisement'), 0);
- add_boolean_row('macfilter', gettext('MAC filter'), 1);
- add_boolean_row('ipfilter', gettext('IP filter'), 0);
- add_log_row('log_level_in');
- add_log_row('log_level_out');
- } else if (me.fwtype === 'dc') {
- add_boolean_row('enable', gettext('Firewall'), 0);
- add_boolean_row('ebtables', 'ebtables', 1);
- me.rows.log_ratelimit = {
- header: gettext('Log rate limit'),
- required: true,
- defaultValue: gettext('Default') + ' (enable=1,rate1/second,burst=5)',
- editor: {
- xtype: 'pveFirewallLograteEdit',
- defaultValue: 'enable=1',
- },
- };
- } else if (me.fwtype === 'vnet') {
- add_boolean_row('enable', gettext('Firewall'), 0);
- add_log_row('log_level_forward');
- }
-
- if (me.fwtype === 'dc' || me.fwtype === 'vm') {
- me.rows.policy_in = {
- header: gettext('Input Policy'),
- required: true,
- defaultValue: 'DROP',
- editor: {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Input Policy'),
- items: {
- xtype: 'pveFirewallPolicySelector',
- name: 'policy_in',
- value: 'DROP',
- fieldLabel: gettext('Input Policy'),
- },
- },
- };
-
- me.rows.policy_out = {
- header: gettext('Output Policy'),
- required: true,
- defaultValue: 'ACCEPT',
- editor: {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Output Policy'),
- items: {
- xtype: 'pveFirewallPolicySelector',
- name: 'policy_out',
- value: 'ACCEPT',
- fieldLabel: gettext('Output Policy'),
- },
- },
- };
- }
-
- if (me.fwtype === 'vnet' || me.fwtype === 'dc') {
- me.rows.policy_forward = {
- header: gettext('Forward Policy'),
- required: true,
- defaultValue: 'ACCEPT',
- editor: {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Forward Policy'),
- items: {
- xtype: 'pveFirewallPolicySelector',
- name: 'policy_forward',
- value: 'ACCEPT',
- fieldLabel: gettext('Forward Policy'),
- comboItems: [
- ['ACCEPT', 'ACCEPT'],
- ['DROP', 'DROP'],
- ],
- },
- },
- };
- }
-
- var edit_btn = new Ext.Button({
- text: gettext('Edit'),
- disabled: true,
- handler: function () {
- me.run_editor();
- },
- });
-
- var set_button_status = function () {
- var sm = me.getSelectionModel();
- var rec = sm.getSelection()[0];
-
- if (!rec) {
- edit_btn.disable();
- return;
- }
- var rowdef = me.rows[rec.data.key];
- if (canEdit) {
- edit_btn.setDisabled(!rowdef.editor);
- }
- };
-
- Ext.apply(me, {
- tbar: [edit_btn],
- listeners: {
- itemdblclick: () => {
- if (canEdit) {
- me.run_editor();
- }
- },
- selectionchange: set_button_status,
- },
- });
-
- if (me.base_url) {
- me.applyUrl(me.base_url);
- } else {
- me.rstore = Ext.create('Proxmox.data.ObjectStore', {
- interval: me.interval,
- extraParams: me.extraParams,
- rows: me.rows,
- });
- }
-
- me.callParent();
-
- me.on('activate', me.rstore.startUpdate);
- me.on('destroy', me.rstore.stopUpdate);
- me.on('deactivate', me.rstore.stopUpdate);
- },
- applyUrl: function (url) {
- let me = this;
-
- Ext.apply(me, {
- url: '/api2/json' + url,
- editorConfig: {
- url: '/api2/extjs/' + url,
- },
- });
- },
- setBaseUrl: function (url) {
- let me = this;
-
- me.base_url = url;
-
- me.applyUrl(url);
-
- me.rstore.getProxy().setConfig('url', `/api2/extjs/${url}`);
- me.rstore.reload();
- },
-});
-
-Ext.define('PVE.FirewallLogLevels', {
- extend: 'Proxmox.form.KVComboBox',
- alias: ['widget.pveFirewallLogLevels'],
-
- name: 'log',
- fieldLabel: gettext('Log level'),
- value: 'nolog',
- comboItems: [
- ['nolog', 'nolog'],
- ['emerg', 'emerg'],
- ['alert', 'alert'],
- ['crit', 'crit'],
- ['err', 'err'],
- ['warning', 'warning'],
- ['notice', 'notice'],
- ['info', 'info'],
- ['debug', 'debug'],
- ],
-});
-Ext.define('PVE.form.FWMacroSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: 'widget.pveFWMacroSelector',
-
- allowBlank: true,
- autoSelect: false,
- valueField: 'macro',
- displayField: 'macro',
-
- listConfig: {
- columns: [
- {
- header: gettext('Macro'),
- dataIndex: 'macro',
- hideable: false,
- width: 100,
- },
- {
- header: gettext('Description'),
- renderer: Ext.String.htmlEncode,
- flex: 1,
- dataIndex: 'descr',
- },
- ],
- },
- initComponent: function () {
- var me = this;
-
- var store = Ext.create('Ext.data.Store', {
- autoLoad: true,
- fields: ['macro', 'descr'],
- idProperty: 'macro',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/firewall/macros',
- },
- sorters: {
- property: 'macro',
- direction: 'ASC',
- },
- });
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.form.ICMPTypeSelector', {
- extend: 'Proxmox.form.ComboGrid',
- alias: 'widget.pveICMPTypeSelector',
-
- allowBlank: true,
- autoSelect: false,
- valueField: 'name',
- displayField: 'name',
-
- listConfig: {
- columns: [
- {
- header: gettext('Type'),
- dataIndex: 'type',
- hideable: false,
- sortable: false,
- width: 50,
- },
- {
- header: gettext('Name'),
- dataIndex: 'name',
- hideable: false,
- sortable: false,
- flex: 1,
- },
- ],
- },
- setName: function (value) {
- this.name = value;
- },
-});
-
-let ICMP_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', {
- field: ['type', 'name'],
- data: [
- { type: 'any', name: 'any' },
- { type: '0', name: 'echo-reply' },
- { type: '3', name: 'destination-unreachable' },
- { type: '3/0', name: 'network-unreachable' },
- { type: '3/1', name: 'host-unreachable' },
- { type: '3/2', name: 'protocol-unreachable' },
- { type: '3/3', name: 'port-unreachable' },
- { type: '3/4', name: 'fragmentation-needed' },
- { type: '3/5', name: 'source-route-failed' },
- { type: '3/6', name: 'network-unknown' },
- { type: '3/7', name: 'host-unknown' },
- { type: '3/9', name: 'network-prohibited' },
- { type: '3/10', name: 'host-prohibited' },
- { type: '3/11', name: 'TOS-network-unreachable' },
- { type: '3/12', name: 'TOS-host-unreachable' },
- { type: '3/13', name: 'communication-prohibited' },
- { type: '3/14', name: 'host-precedence-violation' },
- { type: '3/15', name: 'precedence-cutoff' },
- { type: '4', name: 'source-quench' },
- { type: '5', name: 'redirect' },
- { type: '5/0', name: 'network-redirect' },
- { type: '5/1', name: 'host-redirect' },
- { type: '5/2', name: 'TOS-network-redirect' },
- { type: '5/3', name: 'TOS-host-redirect' },
- { type: '8', name: 'echo-request' },
- { type: '9', name: 'router-advertisement' },
- { type: '10', name: 'router-solicitation' },
- { type: '11', name: 'time-exceeded' },
- { type: '11/0', name: 'ttl-zero-during-transit' },
- { type: '11/1', name: 'ttl-zero-during-reassembly' },
- { type: '12', name: 'parameter-problem' },
- { type: '12/0', name: 'ip-header-bad' },
- { type: '12/1', name: 'required-option-missing' },
- { type: '13', name: 'timestamp-request' },
- { type: '14', name: 'timestamp-reply' },
- { type: '17', name: 'address-mask-request' },
- { type: '18', name: 'address-mask-reply' },
- ],
-});
-let ICMPV6_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', {
- field: ['type', 'name'],
- data: [
- { type: '1', name: 'destination-unreachable' },
- { type: '1/0', name: 'no-route' },
- { type: '1/1', name: 'communication-prohibited' },
- { type: '1/2', name: 'beyond-scope' },
- { type: '1/3', name: 'address-unreachable' },
- { type: '1/4', name: 'port-unreachable' },
- { type: '1/5', name: 'failed-policy' },
- { type: '1/6', name: 'reject-route' },
- { type: '2', name: 'packet-too-big' },
- { type: '3', name: 'time-exceeded' },
- { type: '3/0', name: 'ttl-zero-during-transit' },
- { type: '3/1', name: 'ttl-zero-during-reassembly' },
- { type: '4', name: 'parameter-problem' },
- { type: '4/0', name: 'bad-header' },
- { type: '4/1', name: 'unknown-header-type' },
- { type: '4/2', name: 'unknown-option' },
- { type: '128', name: 'echo-request' },
- { type: '129', name: 'echo-reply' },
- { type: '133', name: 'router-solicitation' },
- { type: '134', name: 'router-advertisement' },
- { type: '135', name: 'neighbour-solicitation' },
- { type: '136', name: 'neighbour-advertisement' },
- { type: '137', name: 'redirect' },
- ],
-});
-
-let DEFAULT_ALLOWED_DIRECTIONS = ['in', 'out'];
-
-let ALLOWED_DIRECTIONS = {
- dc: ['in', 'out', 'forward'],
- node: ['in', 'out', 'forward'],
- group: ['in', 'out', 'forward'],
- vm: ['in', 'out'],
- vnet: ['forward'],
-};
-
-let DEFAULT_ALLOWED_ACTIONS = ['ACCEPT', 'REJECT', 'DROP'];
-
-let ALLOWED_ACTIONS = {
- in: ['ACCEPT', 'REJECT', 'DROP'],
- out: ['ACCEPT', 'REJECT', 'DROP'],
- forward: ['ACCEPT', 'DROP'],
-};
-
-Ext.define('PVE.FirewallRulePanel', {
- extend: 'Proxmox.panel.InputPanel',
-
- allow_iface: false,
-
- list_refs_url: undefined,
-
- firewall_type: undefined,
- action_selector: undefined,
- forward_warning: undefined,
-
- onGetValues: function (values) {
- var _me = this;
-
- // hack: editable ComboGrid returns nothing when empty, so we need to set ''
- // Also, disabled text fields return nothing, so we need to set ''
-
- Ext.Array.each(
- ['source', 'dest', 'macro', 'proto', 'sport', 'dport', 'icmp-type', 'log'],
- function (key) {
- if (values[key] === undefined) {
- values[key] = '';
- }
- },
- );
-
- delete values.modified_marker;
-
- return values;
- },
-
- setValidActions: function (type) {
- let me = this;
-
- let allowed_actions = ALLOWED_ACTIONS[type] ?? DEFAULT_ALLOWED_ACTIONS;
- me.action_selector.setComboItems(allowed_actions.map((action) => [action, action]));
- },
-
- setForwardWarning: function (type) {
- let me = this;
- me.forward_warning.setHidden(type !== 'forward');
- },
-
- onSetValues: function (values) {
- let me = this;
-
- if (values.type) {
- me.setValidActions(values.type);
- me.setForwardWarning(values.type);
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.list_refs_url) {
- throw 'no list_refs_url specified';
- }
-
- let allowed_directions = ALLOWED_DIRECTIONS[me.firewall_type] ?? DEFAULT_ALLOWED_DIRECTIONS;
-
- me.action_selector = Ext.create('Proxmox.form.KVComboBox', {
- xtype: 'proxmoxKVComboBox',
- name: 'action',
- value: 'ACCEPT',
- comboItems: DEFAULT_ALLOWED_ACTIONS.map((action) => [action, action]),
- fieldLabel: gettext('Action'),
- allowBlank: false,
- });
-
- me.forward_warning = Ext.create('Proxmox.form.field.DisplayEdit', {
- userCls: 'pmx-hint',
- value: gettext(
- 'Forward rules only take effect when the nftables firewall is activated in the host options',
- ),
- hidden: true,
- });
-
- me.column1 = [
- {
- // hack: we use this field to mark the form 'dirty' when the
- // record has errors- so that the user can safe the unmodified
- // form again.
- xtype: 'hiddenfield',
- name: 'modified_marker',
- value: '',
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'type',
- value: allowed_directions[0],
- comboItems: allowed_directions.map((dir) => [dir, dir]),
- fieldLabel: gettext('Direction'),
- allowBlank: false,
- listeners: {
- change: function (f, value) {
- me.setValidActions(value);
- me.setForwardWarning(value);
- },
- },
- },
- me.action_selector,
- ];
-
- if (me.allow_iface) {
- me.column1.push({
- xtype: 'proxmoxtextfield',
- name: 'iface',
- deleteEmpty: !me.isCreate,
- value: '',
- fieldLabel: gettext('Interface'),
- });
- } else {
- me.column1.push({
- xtype: 'displayfield',
- fieldLabel: '',
- value: '',
- });
- }
-
- me.column1.push(
- {
- xtype: 'displayfield',
- fieldLabel: '',
- height: 7,
- value: '',
- },
- {
- xtype: 'pveIPRefSelector',
- name: 'source',
- autoSelect: false,
- editable: true,
- base_url: me.list_refs_url,
- fieldLabel: gettext('Source'),
- maxLength: 512,
- maxLengthText: gettext('Too long, consider using IP sets.'),
- },
- {
- xtype: 'pveIPRefSelector',
- name: 'dest',
- autoSelect: false,
- editable: true,
- base_url: me.list_refs_url,
- fieldLabel: gettext('Destination'),
- maxLength: 512,
- maxLengthText: gettext('Too long, consider using IP sets.'),
- },
- );
-
- me.column2 = [
- {
- xtype: 'proxmoxcheckbox',
- name: 'enable',
- checked: false,
- uncheckedValue: 0,
- fieldLabel: gettext('Enable'),
- },
- {
- xtype: 'pveFWMacroSelector',
- name: 'macro',
- fieldLabel: gettext('Macro'),
- editable: true,
- allowBlank: true,
- listeners: {
- change: function (f, value) {
- if (value === null) {
- me.down('field[name=proto]').setDisabled(false);
- me.down('field[name=sport]').setDisabled(false);
- me.down('field[name=dport]').setDisabled(false);
- } else {
- me.down('field[name=proto]').setDisabled(true);
- me.down('field[name=proto]').setValue('');
- me.down('field[name=sport]').setDisabled(true);
- me.down('field[name=sport]').setValue('');
- me.down('field[name=dport]').setDisabled(true);
- me.down('field[name=dport]').setValue('');
- }
- },
- },
- },
- {
- xtype: 'pveIPProtocolSelector',
- name: 'proto',
- autoSelect: false,
- editable: true,
- value: '',
- fieldLabel: gettext('Protocol'),
- listeners: {
- change: function (f, value) {
- if (value === 'icmp' || value === 'icmpv6' || value === 'ipv6-icmp') {
- me.down('field[name=dport]').setHidden(true);
- me.down('field[name=dport]').setDisabled(true);
- if (value === 'icmp') {
- me.down('#icmpv4-type').setHidden(false);
- me.down('#icmpv4-type').setDisabled(false);
- me.down('#icmpv6-type').setHidden(true);
- me.down('#icmpv6-type').setDisabled(true);
- } else {
- me.down('#icmpv6-type').setHidden(false);
- me.down('#icmpv6-type').setDisabled(false);
- me.down('#icmpv4-type').setHidden(true);
- me.down('#icmpv4-type').setDisabled(true);
- }
- } else {
- me.down('#icmpv4-type').setHidden(true);
- me.down('#icmpv4-type').setDisabled(true);
- me.down('#icmpv6-type').setHidden(true);
- me.down('#icmpv6-type').setDisabled(true);
- me.down('field[name=dport]').setHidden(false);
- me.down('field[name=dport]').setDisabled(false);
- }
- },
- },
- },
- {
- xtype: 'displayfield',
- fieldLabel: '',
- height: 7,
- value: '',
- },
- {
- xtype: 'textfield',
- name: 'sport',
- value: '',
- fieldLabel: gettext('Source port'),
- },
- {
- xtype: 'textfield',
- name: 'dport',
- value: '',
- fieldLabel: gettext('Dest. port'),
- },
- {
- xtype: 'pveICMPTypeSelector',
- name: 'icmp-type',
- id: 'icmpv4-type',
- autoSelect: false,
- editable: true,
- hidden: true,
- disabled: true,
- value: '',
- fieldLabel: gettext('ICMP type'),
- store: ICMP_TYPE_NAMES_STORE,
- },
- {
- xtype: 'pveICMPTypeSelector',
- name: 'icmp-type',
- id: 'icmpv6-type',
- autoSelect: false,
- editable: true,
- hidden: true,
- disabled: true,
- value: '',
- fieldLabel: gettext('ICMP type'),
- store: ICMPV6_TYPE_NAMES_STORE,
- },
- ];
-
- me.advancedColumn1 = [
- {
- xtype: 'pveFirewallLogLevels',
- },
- ];
-
- me.columnB = [
- {
- xtype: 'textfield',
- name: 'comment',
- value: '',
- fieldLabel: gettext('Comment'),
- },
- me.forward_warning,
- ];
-
- me.callParent();
-
- if (me.isCreate) {
- // on create we never change the values, so we need to trigger this
- // manually
- me.setValidActions(me.getValues().type);
- me.setForwardWarning(me.getValues().type);
- }
- },
-});
-
-Ext.define('PVE.FirewallRuleEdit', {
- extend: 'Proxmox.window.Edit',
-
- base_url: undefined,
- list_refs_url: undefined,
-
- allow_iface: false,
-
- firewall_type: undefined,
-
- initComponent: function () {
- var me = this;
-
- if (!me.base_url) {
- throw 'no base_url specified';
- }
- if (!me.list_refs_url) {
- throw 'no list_refs_url specified';
- }
-
- me.isCreate = me.rule_pos === undefined;
-
- if (me.isCreate) {
- me.url = '/api2/extjs' + me.base_url;
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
- me.method = 'PUT';
- }
-
- var ipanel = Ext.create('PVE.FirewallRulePanel', {
- isCreate: me.isCreate,
- list_refs_url: me.list_refs_url,
- allow_iface: me.allow_iface,
- rule_pos: me.rule_pos,
- firewall_type: me.firewall_type,
- });
-
- Ext.apply(me, {
- subject: gettext('Rule'),
- isAdd: true,
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
- ipanel.setValues(values);
- // set icmp-type again after protocol has been set
- if (values['icmp-type'] !== undefined) {
- ipanel.setValues({ 'icmp-type': values['icmp-type'] });
- }
- if (values.errors) {
- let field = me.query('[isFormField][name=modified_marker]')[0];
- field.setValue(1);
- Ext.Function.defer(function () {
- var form = ipanel.up('form').getForm();
- form.markInvalid(values.errors);
- }, 100);
- }
- },
- });
- } else if (me.rec) {
- ipanel.setValues(me.rec.data);
- }
- },
-});
-
-Ext.define('PVE.FirewallGroupRuleEdit', {
- extend: 'Proxmox.window.Edit',
-
- base_url: undefined,
-
- allow_iface: false,
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = me.rule_pos === undefined;
-
- if (me.isCreate) {
- me.url = '/api2/extjs' + me.base_url;
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
- me.method = 'PUT';
- }
-
- var column1 = [
- {
- xtype: 'hiddenfield',
- name: 'type',
- value: 'group',
- },
- {
- xtype: 'pveSecurityGroupsSelector',
- name: 'action',
- value: '',
- fieldLabel: gettext('Security Group'),
- allowBlank: false,
- },
- ];
-
- if (me.allow_iface) {
- column1.push({
- xtype: 'proxmoxtextfield',
- name: 'iface',
- deleteEmpty: !me.isCreate,
- value: '',
- fieldLabel: gettext('Interface'),
- });
- }
-
- var ipanel = Ext.create('Proxmox.panel.InputPanel', {
- isCreate: me.isCreate,
- column1: column1,
- column2: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'enable',
- checked: false,
- uncheckedValue: 0,
- fieldLabel: gettext('Enable'),
- },
- ],
- columnB: [
- {
- xtype: 'textfield',
- name: 'comment',
- value: '',
- fieldLabel: gettext('Comment'),
- },
- ],
- });
-
- Ext.apply(me, {
- subject: gettext('Rule'),
- isAdd: true,
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-
-Ext.define(
- 'PVE.FirewallRules',
- {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveFirewallRules',
-
- onlineHelp: 'chapter_pve_firewall',
- emptyText: gettext('No firewall rule configured here.'),
-
- stateful: true,
- stateId: 'grid-firewall-rules',
-
- base_url: undefined,
- list_refs_url: undefined,
-
- addBtn: undefined,
- removeBtn: undefined,
- editBtn: undefined,
- groupBtn: undefined,
-
- tbar_prefix: undefined,
-
- allow_groups: true,
- allow_iface: false,
-
- firewall_type: undefined,
-
- setBaseUrl: function (url) {
- var me = this;
-
- me.base_url = url;
-
- if (url === undefined) {
- me.addBtn.setDisabled(true);
- if (me.groupBtn) {
- me.groupBtn.setDisabled(true);
- }
- me.store.removeAll();
- } else {
- if (me.canEdit) {
- me.addBtn.setDisabled(false);
- if (me.groupBtn) {
- me.groupBtn.setDisabled(false);
- }
- }
- me.removeBtn.baseurl = url + '/';
-
- me.store.setProxy({
- type: 'proxmox',
- url: '/api2/json' + url,
- });
-
- me.store.load();
- }
- },
-
- moveRule: function (from, to) {
- var me = this;
-
- if (!me.base_url) {
- return;
- }
-
- Proxmox.Utils.API2Request({
- url: me.base_url + '/' + from,
- method: 'PUT',
- params: { moveto: to },
- waitMsgTarget: me,
- failure: function (response, options) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- callback: function () {
- me.store.load();
- },
- });
- },
-
- updateRule: function (rule) {
- var me = this;
-
- if (!me.base_url) {
- return;
- }
-
- rule.enable = rule.enable ? 1 : 0;
-
- var pos = rule.pos;
- delete rule.pos;
- delete rule.errors;
-
- Proxmox.Utils.API2Request({
- url: me.base_url + '/' + pos.toString(),
- method: 'PUT',
- params: rule,
- waitMsgTarget: me,
- failure: function (response, options) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- callback: function () {
- me.store.load();
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.list_refs_url) {
- throw 'no list_refs_url specified';
- }
-
- var store = Ext.create('Ext.data.Store', {
- model: 'pve-fw-rule',
- });
-
- var reload = function () {
- store.load();
- };
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- me.caps = Ext.state.Manager.get('GuiCap');
- me.canEdit =
- !!me.caps.vms['VM.Config.Network'] ||
- !!me.caps.dc['Sys.Modify'] ||
- !!me.caps.nodes['Sys.Modify'];
-
- var run_editor = function () {
- var rec = sm.getSelection()[0];
- if (!rec || !me.canEdit) {
- return;
- }
- var type = rec.data.type;
-
- var editor;
- if (type === 'in' || type === 'out' || type === 'forward') {
- editor = 'PVE.FirewallRuleEdit';
- } else if (type === 'group') {
- editor = 'PVE.FirewallGroupRuleEdit';
- } else {
- return;
- }
-
- var win = Ext.create(editor, {
- firewall_type: me.firewall_type,
- digest: rec.data.digest,
- allow_iface: me.allow_iface,
- base_url: me.base_url,
- list_refs_url: me.list_refs_url,
- rule_pos: rec.data.pos,
- });
-
- win.show();
- win.on('destroy', reload);
- };
-
- me.editBtn = Ext.create('Proxmox.button.Button', {
- text: gettext('Edit'),
- disabled: true,
- enableFn: (rec) => me.canEdit,
- selModel: sm,
- handler: run_editor,
- });
-
- me.addBtn = Ext.create('Ext.Button', {
- text: gettext('Add'),
- disabled: true,
- handler: function () {
- var win = Ext.create('PVE.FirewallRuleEdit', {
- firewall_type: me.firewall_type,
- allow_iface: me.allow_iface,
- base_url: me.base_url,
- list_refs_url: me.list_refs_url,
- });
- win.on('destroy', reload);
- win.show();
- },
- });
-
- var run_copy_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- let type = rec.data.type;
- if (!(type === 'in' || type === 'out' || type === 'forward')) {
- return;
- }
-
- let win = Ext.create('PVE.FirewallRuleEdit', {
- firewall_type: me.firewall_type,
- allow_iface: me.allow_iface,
- base_url: me.base_url,
- list_refs_url: me.list_refs_url,
- rec: rec,
- });
- win.show();
- win.on('destroy', reload);
- };
-
- me.copyBtn = Ext.create('Proxmox.button.Button', {
- text: gettext('Copy'),
- selModel: sm,
- enableFn: ({ data }) =>
- (data.type === 'in' || data.type === 'out' || data.type === 'forward') &&
- me.canEdit,
- disabled: true,
- handler: run_copy_editor,
- });
-
- if (me.allow_groups) {
- me.groupBtn = Ext.create('Ext.Button', {
- text: gettext('Insert') + ': ' + gettext('Security Group'),
- disabled: true,
- handler: function () {
- var win = Ext.create('PVE.FirewallGroupRuleEdit', {
- allow_iface: me.allow_iface,
- base_url: me.base_url,
- });
- win.on('destroy', reload);
- win.show();
- },
- });
- }
-
- me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
- enableFn: (rec) => me.canEdit,
- selModel: sm,
- baseurl: me.base_url + '/',
- confirmMsg: false,
- getRecordName: function (rec) {
- var rule = rec.data;
- return rule.pos.toString() + '?digest=' + encodeURIComponent(rule.digest);
- },
- callback: function () {
- me.store.load();
- },
- });
-
- let tbar = me.tbar_prefix ? [me.tbar_prefix] : [];
- tbar.push(me.addBtn, me.copyBtn);
- if (me.groupBtn) {
- tbar.push(me.groupBtn);
- }
- tbar.push(me.removeBtn, me.editBtn);
-
- let render_errors = function (name, value, metaData, record) {
- let errors = record.data.errors;
- if (errors && errors[name]) {
- metaData.tdCls = 'proxmox-invalid-row';
- let html = Ext.htmlEncode(`${Ext.htmlEncode(errors[name])}`);
- metaData.tdAttr =
- 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html + '"';
- }
- return Ext.htmlEncode(value);
- };
-
- let columns = [
- {
- // similar to xtype: 'rownumberer',
- dataIndex: 'pos',
- resizable: false,
- minWidth: 65,
- maxWidth: 83,
- flex: 1,
- sortable: false,
- hideable: false,
- menuDisabled: true,
- renderer: function (value, metaData, record, rowIdx, colIdx) {
- metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
- let dragHandle =
- " ";
- if (value >= 0) {
- return dragHandle + value;
- }
- return dragHandle;
- },
- },
- {
- xtype: 'checkcolumn',
- header: gettext('On'),
- dataIndex: 'enable',
- listeners: {
- checkchange: function (column, recordIndex, checked) {
- var record = me.getStore().getData().items[recordIndex];
- record.commit();
- var data = {};
- Ext.Array.forEach(record.getFields(), function (field) {
- data[field.name] = record.get(field.name);
- });
- if (!me.allow_iface || !data.iface) {
- delete data.iface;
- }
- me.updateRule(data);
- },
- },
- width: 40,
- },
- {
- header: gettext('Type'),
- dataIndex: 'type',
- renderer: function (value, metaData, record) {
- return render_errors('type', value, metaData, record);
- },
- minWidth: 60,
- maxWidth: 80,
- flex: 2,
- },
- {
- header: gettext('Action'),
- dataIndex: 'action',
- renderer: function (value, metaData, record) {
- return render_errors('action', value, metaData, record);
- },
- minWidth: 80,
- maxWidth: 200,
- flex: 2,
- },
- {
- header: gettext('Macro'),
- dataIndex: 'macro',
- renderer: function (value, metaData, record) {
- return render_errors('macro', value, metaData, record);
- },
- minWidth: 80,
- flex: 2,
- },
- ];
-
- if (me.allow_iface) {
- columns.push({
- header: gettext('Interface'),
- dataIndex: 'iface',
- renderer: function (value, metaData, record) {
- return render_errors('iface', value, metaData, record);
- },
- minWidth: 80,
- flex: 2,
- });
- }
-
- columns.push(
- {
- header: gettext('Protocol'),
- dataIndex: 'proto',
- renderer: function (value, metaData, record) {
- return render_errors('proto', value, metaData, record);
- },
- width: 75,
- },
- {
- header: gettext('Source'),
- dataIndex: 'source',
- renderer: function (value, metaData, record) {
- return render_errors('source', value, metaData, record);
- },
- minWidth: 100,
- flex: 2,
- },
- {
- header: gettext('S.Port'),
- dataIndex: 'sport',
- renderer: function (value, metaData, record) {
- return render_errors('sport', value, metaData, record);
- },
- width: 75,
- },
- {
- header: gettext('Destination'),
- dataIndex: 'dest',
- renderer: function (value, metaData, record) {
- return render_errors('dest', value, metaData, record);
- },
- minWidth: 100,
- flex: 2,
- },
- {
- header: gettext('D.Port'),
- dataIndex: 'dport',
- renderer: function (value, metaData, record) {
- return render_errors('dport', value, metaData, record);
- },
- width: 75,
- },
- {
- header: gettext('Log level'),
- dataIndex: 'log',
- renderer: function (value, metaData, record) {
- return render_errors('log', value, metaData, record);
- },
- width: 100,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- flex: 10,
- minWidth: 75,
- renderer: function (value, metaData, record) {
- let comment = render_errors('comment', value, metaData, record) || '';
- if (comment.length * 12 > metaData.column.cellWidth) {
- comment = `${comment} `;
- }
- return comment;
- },
- },
- );
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: tbar,
- viewConfig: {
- plugins: [
- {
- ptype: 'gridviewdragdrop',
- dragGroup: 'FWRuleDDGroup',
- dropGroup: 'FWRuleDDGroup',
- },
- ],
- listeners: {
- beforedrop: function (node, data, dropRec, dropPosition) {
- if (!dropRec) {
- return false; // empty view
- }
- let moveto = dropRec.get('pos');
- if (dropPosition === 'after') {
- moveto++;
- }
- let pos = data.records[0].get('pos');
- me.moveRule(pos, moveto);
- return 0;
- },
- itemdblclick: run_editor,
- },
- },
- sortableColumns: false,
- columns: columns,
- });
-
- me.callParent();
-
- if (me.base_url) {
- me.setBaseUrl(me.base_url); // load
- }
- },
- },
- function () {
- Ext.define('pve-fw-rule', {
- extend: 'Ext.data.Model',
- fields: [
- { name: 'enable', type: 'boolean' },
- 'type',
- 'action',
- 'macro',
- 'source',
- 'dest',
- 'proto',
- 'iface',
- 'dport',
- 'sport',
- 'comment',
- 'pos',
- 'digest',
- 'errors',
- ],
- idProperty: 'pos',
- });
- },
-);
-Ext.define('PVE.pool.AddVM', {
- extend: 'Proxmox.window.Edit',
-
- width: 800,
- height: 600,
- resizable: true,
-
- isAdd: true,
- isCreate: true,
-
- extraRequestParams: {
- 'allow-move': 1,
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.pool) {
- throw 'no pool specified';
- }
-
- me.url = '/pools/';
- me.method = 'PUT';
- me.extraRequestParams.poolid = me.pool;
-
- var vmsField = Ext.create('Ext.form.field.Text', {
- name: 'vms',
- hidden: true,
- allowBlank: false,
- });
-
- let basicFilter = (data) =>
- (data.type === 'lxc' || data.type === 'qemu') && data.pool !== me.pool;
-
- var vmStore = Ext.create('Ext.data.Store', {
- model: 'PVEResources',
- sorters: [
- {
- property: 'vmid',
- direction: 'ASC',
- },
- ],
- filters: [(item) => basicFilter(item.data)],
- });
-
- var vmGrid = Ext.create('widget.grid', {
- store: vmStore,
- border: true,
- height: 480,
- scrollable: true,
- selModel: {
- selType: 'checkboxmodel',
- mode: 'SIMPLE',
- listeners: {
- selectionchange: function (model, selected, opts) {
- var selectedVms = [];
- selected.forEach(function (vm) {
- selectedVms.push(vm.data.vmid);
- });
- vmsField.setValue(selectedVms);
- },
- },
- },
- tbar: [
- '->',
- gettext('Filter') + ':',
- ' ',
- {
- xtype: 'textfield',
- width: 200,
- enableKeyEvents: true,
- emptyText: gettext('Name, Node, VMID'),
- submitValue: false,
- listeners: {
- keyup: {
- buffer: 350,
- fn: function (field) {
- let needle = field.getValue().toLocaleLowerCase();
- if (needle?.length === 0) {
- this.triggers.clear.setVisible(false);
- }
- let matchesNeedle = (v) => v?.toLocaleLowerCase().includes(needle);
- vmStore.clearFilter(true);
- vmStore.filter([
- {
- filterFn: ({ data }) =>
- basicFilter(data) &&
- (matchesNeedle(data.vmid.toString()) ||
- matchesNeedle(data.name) ||
- matchesNeedle(data.node)),
- },
- ]);
- },
- },
- change: function (field, newValue, oldValue) {
- if (newValue !== this.originalValue) {
- this.triggers.clear.setVisible(true);
- }
- },
- },
- triggers: {
- clear: {
- cls: 'pmx-clear-trigger',
- weight: -1,
- hidden: true,
- handler: function () {
- this.triggers.clear.setVisible(false);
- this.setValue(this.originalValue);
- vmStore.clearFilter(true);
- vmStore.filter([
- {
- filterFn: ({ data }) => basicFilter(data),
- },
- ]);
- },
- },
- },
- },
- ],
- columns: [
- {
- header: 'ID',
- dataIndex: 'vmid',
- width: 60,
- },
- {
- header: gettext('Node'),
- dataIndex: 'node',
- },
- {
- header: gettext('Current Pool'),
- dataIndex: 'pool',
- },
- {
- header: gettext('Status'),
- dataIndex: 'uptime',
- renderer: (v) => (v ? Proxmox.Utils.runningText : Proxmox.Utils.stoppedText),
- },
- {
- header: gettext('Name'),
- dataIndex: 'name',
- flex: 1,
- },
- {
- header: gettext('Type'),
- dataIndex: 'type',
- },
- ],
- });
-
- Ext.apply(me, {
- subject: gettext('Virtual Machine'),
- items: [
- vmsField,
- vmGrid,
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- 'Selected guests who are already part of a pool will be removed from it first.',
- ),
- },
- ],
- });
-
- me.callParent();
- vmStore.load();
- },
-});
-
-Ext.define('PVE.pool.AddStorage', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- if (!me.pool) {
- throw 'no pool specified';
- }
-
- me.isCreate = true;
- me.isAdd = true;
- me.url = '/pools/';
- me.method = 'PUT';
- me.extraRequestParams.poolid = me.pool;
-
- Ext.apply(me, {
- subject: gettext('Storage'),
- width: 350,
- items: [
- {
- xtype: 'pveStorageSelector',
- name: 'storage',
- nodename: 'localhost',
- autoSelect: false,
- value: '',
- fieldLabel: gettext('Storage'),
- },
- ],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.grid.PoolMembers', {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pvePoolMembers'],
-
- stateful: true,
- stateId: 'grid-pool-members',
-
- initComponent: function () {
- var me = this;
-
- if (!me.pool) {
- throw 'no pool specified';
- }
-
- me.rstore = Ext.create('Proxmox.data.UpdateStore', {
- interval: 10000,
- model: 'PVEResources',
- proxy: {
- type: 'proxmox',
- root: 'data[0].members',
- url: `/api2/json/pools/?poolid=${me.pool}`,
- },
- autoStart: true,
- });
-
- let store = Ext.create('Proxmox.data.DiffStore', {
- rstore: me.rstore,
- sorters: [
- {
- property: 'type',
- direction: 'ASC',
- },
- ],
- });
-
- var coldef = PVE.data.ResourceStore.defaultColumns().filter(
- (c) => c.dataIndex !== 'tags' && c.dataIndex !== 'lock',
- );
-
- const reload = function () {
- me.rstore.load();
- };
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var remove_btn = new Proxmox.button.Button({
- text: gettext('Remove'),
- disabled: true,
- selModel: sm,
- confirmMsg: function (rec) {
- return Ext.String.format(
- gettext('Are you sure you want to remove entry {0}'),
- "'" + rec.data.id + "'",
- );
- },
- handler: function (btn, event, rec) {
- var params = { delete: 1, poolid: me.pool };
- if (rec.data.type === 'storage') {
- params.storage = rec.data.storage;
- } else if (
- rec.data.type === 'qemu' ||
- rec.data.type === 'lxc' ||
- rec.data.type === 'openvz'
- ) {
- params.vms = rec.data.vmid;
- } else {
- throw 'unknown resource type';
- }
-
- Proxmox.Utils.API2Request({
- url: '/pools/',
- method: 'PUT',
- params: params,
- waitMsgTarget: me,
- callback: function () {
- reload();
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
- });
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: [
- {
- text: gettext('Add'),
- menu: new Ext.menu.Menu({
- items: [
- {
- text: gettext('Virtual Machine'),
- iconCls: 'fa fa-desktop',
- handler: function () {
- var win = Ext.create('PVE.pool.AddVM', { pool: me.pool });
- win.on('destroy', reload);
- win.show();
- },
- },
- {
- text: gettext('Storage'),
- iconCls: 'fa fa-hdd-o',
- handler: function () {
- var win = Ext.create('PVE.pool.AddStorage', { pool: me.pool });
- win.on('destroy', reload);
- win.show();
- },
- },
- ],
- }),
- },
- remove_btn,
- ],
- viewConfig: {
- stripeRows: true,
- },
- columns: coldef,
- listeners: {
- itemcontextmenu: PVE.Utils.createCmdMenu,
- itemdblclick: function (v, record) {
- var ws = me.up('pveStdWorkspace');
- ws.selectById(record.data.id);
- },
- activate: reload,
- destroy: () => me.rstore.stopUpdate(),
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.window.ReplicaEdit', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveReplicaEdit',
-
- subject: gettext('Replication Job'),
-
- url: '/cluster/replication',
- method: 'POST',
-
- initComponent: function () {
- var me = this;
-
- var vmid = me.pveSelNode.data.vmid;
- var nodename = me.pveSelNode.data.node;
-
- var items = [];
-
- items.push({
- xtype: me.isCreate && !vmid ? 'pveGuestIDSelector' : 'displayfield',
- name: 'guest',
- fieldLabel: 'CT/VM ID',
- value: vmid || '',
- });
-
- items.push(
- {
- xtype: me.isCreate ? 'pveNodeSelector' : 'displayfield',
- name: 'target',
- disallowedNodes: [nodename],
- allowBlank: false,
- onlineValidator: true,
- fieldLabel: gettext('Target'),
- },
- {
- xtype: 'pveCalendarEvent',
- fieldLabel: gettext('Schedule'),
- emptyText: '*/15 - ' + Ext.String.format(gettext('Every {0} minutes'), 15),
- name: 'schedule',
- },
- {
- xtype: 'numberfield',
- fieldLabel: gettext('Rate limit') + ' (MB/s)',
- step: 1,
- minValue: 1,
- emptyText: gettext('unlimited'),
- name: 'rate',
- },
- {
- xtype: 'textfield',
- fieldLabel: gettext('Comment'),
- name: 'comment',
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'enabled',
- defaultValue: 'on',
- checked: true,
- fieldLabel: gettext('Enabled'),
- },
- );
-
- me.items = [
- {
- xtype: 'inputpanel',
- itemId: 'ipanel',
- onlineHelp: 'pvesr_schedule_time_format',
-
- onGetValues: function (values) {
- let win = this.up('window');
-
- values.disable = values.enabled ? 0 : 1;
- delete values.enabled;
-
- PVE.Utils.delete_if_default(values, 'rate', '', win.isCreate);
- PVE.Utils.delete_if_default(values, 'disable', 0, win.isCreate);
- PVE.Utils.delete_if_default(values, 'schedule', '*/15', win.isCreate);
- PVE.Utils.delete_if_default(values, 'comment', '', win.isCreate);
-
- if (win.isCreate) {
- values.type = 'local';
- let vm = vmid || values.guest;
- let id = -1;
- if (win.highestids[vm] !== undefined) {
- id = win.highestids[vm];
- }
- id++;
- values.id = vm + '-' + id.toString();
- delete values.guest;
- }
- return values;
- },
- items: items,
- },
- ];
-
- me.callParent();
-
- if (me.isCreate) {
- me.load({
- success: function (response) {
- var jobs = response.result.data;
- var highestids = {};
- Ext.Array.forEach(jobs, function (job) {
- var match = /^([0-9]+)-([0-9]+)$/.exec(job.id);
- if (match) {
- let jobVMID = parseInt(match[1], 10);
- let id = parseInt(match[2], 10);
- if (highestids[jobVMID] === undefined || highestids[jobVMID] < id) {
- highestids[jobVMID] = id;
- }
- }
- });
- me.highestids = highestids;
- },
- });
- } else {
- me.load({
- success: function (response, options) {
- response.result.data.enabled = !response.result.data.disable;
- me.setValues(response.result.data);
- me.digest = response.result.data.digest;
- },
- });
- }
- },
-});
-
-/* callback is a function and string */
-Ext.define(
- 'PVE.grid.ReplicaView',
- {
- extend: 'Ext.grid.Panel',
- xtype: 'pveReplicaView',
-
- onlineHelp: 'chapter_pvesr',
-
- stateful: true,
- stateId: 'grid-pve-replication-status',
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- addJob: function (button, event, rec) {
- let me = this;
- let view = me.getView();
- Ext.create('PVE.window.ReplicaEdit', {
- isCreate: true,
- method: 'POST',
- pveSelNode: view.pveSelNode,
- listeners: {
- destroy: () => me.reload(),
- },
- autoShow: true,
- });
- },
-
- editJob: function (button, event, { data }) {
- let me = this;
- let view = me.getView();
- Ext.create('PVE.window.ReplicaEdit', {
- url: `/cluster/replication/${data.id}`,
- method: 'PUT',
- pveSelNode: view.pveSelNode,
- listeners: {
- destroy: () => me.reload(),
- },
- autoShow: true,
- });
- },
-
- scheduleJobNow: function (button, event, rec) {
- let me = this;
- let view = me.getView();
- Proxmox.Utils.API2Request({
- url: `/api2/extjs/nodes/${view.nodename}/replication/${rec.data.id}/schedule_now`,
- method: 'POST',
- waitMsgTarget: view,
- callback: () => me.reload(),
- failure: (response, opts) =>
- Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- },
-
- showLog: function (button, event, rec) {
- let me = this;
- let view = this.getView();
-
- let logView = Ext.create('Proxmox.panel.LogView', {
- border: false,
- url: `/api2/extjs/nodes/${view.nodename}/replication/${rec.data.id}/log`,
- });
- let task = Ext.TaskManager.newTask({
- run: () => logView.requestUpdate(),
- interval: 1000,
- });
- let win = Ext.create('Ext.window.Window', {
- items: [logView],
- layout: 'fit',
- width: 800,
- height: 400,
- modal: true,
- title: gettext('Replication Log'),
- listeners: {
- destroy: function () {
- task.stop();
- me.reload();
- },
- },
- });
- task.start();
- win.show();
- },
-
- reload: function () {
- this.getView().rstore.load();
- },
-
- dblClick: function (grid, record, item) {
- this.editJob(undefined, undefined, record);
- },
-
- // currently replication is for cluster only, so disable the whole component for non-cluster
- checkPrerequisites: function () {
- let view = this.getView();
- if (PVE.Utils.isStandaloneNode()) {
- view.mask(gettext('Replication needs at least two nodes'), ['pve-static-mask']);
- }
- },
-
- control: {
- '#': {
- itemdblclick: 'dblClick',
- afterlayout: 'checkPrerequisites',
- },
- },
- },
-
- tbar: [
- {
- text: gettext('Add'),
- itemId: 'addButton',
- handler: 'addJob',
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- itemId: 'editButton',
- handler: 'editJob',
- disabled: true,
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- itemId: 'removeButton',
- baseurl: '/api2/extjs/cluster/replication/',
- dangerous: true,
- callback: 'reload',
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Log'),
- itemId: 'logButton',
- handler: 'showLog',
- disabled: true,
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Schedule now'),
- itemId: 'scheduleNowButton',
- handler: 'scheduleJobNow',
- disabled: true,
- },
- ],
-
- initComponent: function () {
- var me = this;
- var mode = '';
- var url = '/cluster/replication';
-
- me.nodename = me.pveSelNode.data.node;
- me.vmid = me.pveSelNode.data.vmid;
-
- me.columns = [
- {
- header: gettext('Enabled'),
- width: 80,
- dataIndex: 'enabled',
- align: 'center',
- renderer: Proxmox.Utils.renderEnabledIcon,
- sortable: true,
- },
- {
- text: 'ID',
- dataIndex: 'id',
- width: 60,
- hidden: true,
- },
- {
- text: gettext('Guest'),
- dataIndex: 'guest',
- width: 75,
- },
- {
- text: gettext('Job'),
- dataIndex: 'jobnum',
- width: 60,
- },
- {
- text: gettext('Target'),
- dataIndex: 'target',
- },
- ];
-
- if (!me.nodename) {
- mode = 'dc';
- me.stateId = 'grid-pve-replication-dc';
- } else if (!me.vmid) {
- mode = 'node';
- url = `/nodes/${me.nodename}/replication`;
- } else {
- mode = 'vm';
- url = `/nodes/${me.nodename}/replication?guest=${me.vmid}`;
- }
-
- if (mode !== 'dc') {
- me.columns.push(
- {
- text: gettext('Status'),
- dataIndex: 'state',
- minWidth: 160,
- flex: 1,
- renderer: function (value, metadata, record) {
- if (record.data.pid) {
- metadata.tdCls = 'x-grid-row-loading';
- return '';
- }
-
- let icons = [],
- states = [];
-
- if (record.data.remove_job) {
- icons.push(
- ' ',
- );
- states.push(gettext('Removal Scheduled'));
- }
- if (record.data.error) {
- icons.push(
- ' ',
- );
- states.push(record.data.error);
- }
- if (icons.length === 0) {
- icons.push(' ');
- states.push(gettext('OK'));
- }
-
- return icons.join(',') + ' ' + states.join(',');
- },
- },
- {
- text: gettext('Last Sync'),
- dataIndex: 'last_sync',
- width: 150,
- renderer: function (value, metadata, record) {
- if (!value) {
- return '-';
- }
- if (record.data.pid) {
- return gettext('syncing');
- }
- return Proxmox.Utils.render_timestamp(value);
- },
- },
- {
- text: gettext('Duration'),
- dataIndex: 'duration',
- width: 60,
- renderer: Proxmox.Utils.render_duration,
- },
- {
- text: gettext('Next Sync'),
- dataIndex: 'next_sync',
- width: 150,
- renderer: 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);
- },
- },
- );
- }
-
- me.columns.push(
- {
- text: gettext('Schedule'),
- width: 75,
- dataIndex: 'schedule',
- },
- {
- text: gettext('Rate limit'),
- dataIndex: 'rate',
- renderer: function (value) {
- if (!value) {
- return gettext('unlimited');
- }
-
- return value.toString() + ' MB/s';
- },
- hidden: true,
- },
- {
- text: gettext('Comment'),
- dataIndex: 'comment',
- renderer: Ext.htmlEncode,
- },
- );
-
- me.rstore = Ext.create('Proxmox.data.UpdateStore', {
- storeid: 'pve-replica-' + me.nodename + me.vmid,
- model: mode === 'dc' ? 'pve-replication' : 'pve-replication-state',
- interval: 3000,
- proxy: {
- type: 'proxmox',
- url: '/api2/json' + url,
- },
- });
-
- me.store = Ext.create('Proxmox.data.DiffStore', {
- rstore: me.rstore,
- sorters: [
- {
- property: 'guest',
- },
- {
- property: 'jobnum',
- },
- ],
- });
-
- me.callParent();
-
- // we cannot access the log and scheduleNow button
- // in the datacenter, because
- // we do not know where/if the jobs runs
- if (mode === 'dc') {
- me.down('#logButton').setHidden(true);
- me.down('#scheduleNowButton').setHidden(true);
- }
-
- // if we set the warning mask, we do not want to load
- // or set the mask on store errors
- if (PVE.Utils.isStandaloneNode()) {
- return;
- }
-
- Proxmox.Utils.monStoreErrors(me, me.rstore);
-
- me.on('destroy', me.rstore.stopUpdate);
- me.rstore.startUpdate();
- },
- },
- function () {
- Ext.define('pve-replication', {
- extend: 'Ext.data.Model',
- fields: [
- 'id',
- 'target',
- 'comment',
- 'rate',
- 'type',
- { name: 'guest', type: 'integer' },
- { name: 'jobnum', type: 'integer' },
- { name: 'schedule', defaultValue: '*/15' },
- { name: 'disable', defaultValue: '' },
- {
- name: 'enabled',
- calculate: function (data) {
- return !data.disable;
- },
- },
- ],
- });
-
- Ext.define('pve-replication-state', {
- extend: 'pve-replication',
- fields: [
- 'last_sync',
- 'next_sync',
- 'error',
- 'duration',
- 'state',
- 'fail_count',
- 'remove_job',
- 'pid',
- ],
- });
- },
-);
-Ext.define('PVE.grid.ResourceGrid', {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveResourceGrid'],
-
- border: false,
- defaultSorter: {
- property: 'type',
- direction: 'ASC',
- },
- userCls: 'proxmox-tags-full',
- initComponent: function () {
- let me = this;
-
- let rstore = PVE.data.ResourceStore;
-
- let store = Ext.create('Ext.data.Store', {
- model: 'PVEResources',
- sorters: me.defaultSorter,
- proxy: {
- type: 'memory',
- },
- });
-
- let textfilter = '';
- let textfilterMatch = function (item) {
- for (const field of ['name', 'storage', 'node', 'type', 'text']) {
- let v = item.data[field];
- if (v && v.toLowerCase().indexOf(textfilter) >= 0) {
- return true;
- }
- }
- return false;
- };
-
- let updateGrid = function () {
- var filterfn = me.viewFilter ? me.viewFilter.filterfn : null;
-
- store.suspendEvents();
-
- let nodeidx = {};
- let gather_child_nodes;
- gather_child_nodes = function (node) {
- if (!node || !node.childNodes) {
- return;
- }
- for (let child of node.childNodes) {
- let orgNode = rstore.data.get(child.data.realId ?? child.data.id);
- if (orgNode) {
- if (
- (!filterfn || filterfn(child)) &&
- (!textfilter || textfilterMatch(child))
- ) {
- nodeidx[child.data.id] = orgNode;
- }
- }
- gather_child_nodes(child);
- }
- };
- gather_child_nodes(me.pveSelNode);
-
- // remove vanished items
- let rmlist = [];
- store.each((olditem) => {
- if (!nodeidx[olditem.data.id]) {
- rmlist.push(olditem);
- }
- });
- if (rmlist.length) {
- store.remove(rmlist);
- }
-
- // add new items
- let addlist = [];
- for (const [_key, item] of Object.entries(nodeidx)) {
- // getById() use find(), which is slow (ExtJS4 DP5)
- let olditem = store.data.get(item.data.id);
- if (!olditem) {
- addlist.push(item);
- continue;
- }
- let changes = false;
- for (let field of PVE.data.ResourceStore.fieldNames) {
- if (field !== 'id' && item.data[field] !== olditem.data[field]) {
- changes = true;
- olditem.beginEdit();
- olditem.set(field, item.data[field]);
- }
- }
- if (changes) {
- olditem.endEdit(true);
- olditem.commit(true);
- }
- }
- if (addlist.length) {
- store.add(addlist);
- }
- store.sort();
- store.resumeEvents();
- store.fireEvent('refresh', store);
- };
-
- Ext.apply(me, {
- store: store,
- stateful: true,
- stateId: 'grid-resource',
- tbar: [
- '->',
- gettext('Search') + ':',
- ' ',
- {
- xtype: 'textfield',
- width: 200,
- value: textfilter,
- enableKeyEvents: true,
- listeners: {
- buffer: 500,
- keyup: function (field, e) {
- textfilter = field.getValue().toLowerCase();
- updateGrid();
- },
- },
- },
- ],
- viewConfig: {
- stripeRows: true,
- },
- listeners: {
- itemcontextmenu: PVE.Utils.createCmdMenu,
- itemdblclick: function (v, record) {
- var ws = me.up('pveStdWorkspace');
- ws.selectById(record.data.id);
- },
- afterrender: function () {
- updateGrid();
- },
- },
- columns: rstore.defaultColumns(),
- });
- me.callParent();
- me.mon(rstore, 'load', () => updateGrid());
- },
-});
-/*
- * Base class for all the multitab config panels
- *
- * How to use this:
- *
- * You create a subclass of this, and then define your wanted tabs
- * as items like this:
- *
- * items: [{
- * title: "myTitle",
- * xytpe: "somextype",
- * iconCls: 'fa fa-icon',
- * groups: ['somegroup'],
- * expandedOnInit: true,
- * itemId: 'someId'
- * }]
- *
- * this has to be in the declarative syntax, else we
- * cannot save them for later
- * (so no Ext.create or Ext.apply of an item in the subclass)
- *
- * the groups array expects the itemids of the items
- * which are the parents, which have to come before they
- * are used
- *
- * if you want following the tree:
- *
- * Option1
- * Option2
- * -> SubOption1
- * -> SubSubOption1
- *
- * the suboption1 group array has to look like this:
- * groups: ['itemid-of-option2']
- *
- * and of subsuboption1:
- * groups: ['itemid-of-option2', 'itemid-of-suboption1']
- *
- * setting the expandedOnInit determines if the item/group is expanded
- * initially (false by default)
- */
-Ext.define('PVE.panel.Config', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pvePanelConfig',
-
- showSearch: true, // add a resource grid with a search button as first tab
- viewFilter: undefined, // a filter to pass to that resource grid
-
- tbarSpacing: true, // if true, adds a spacer after the title in tbar
-
- dockedItems: [
- {
- // this is needed for the overflow handler
- xtype: 'toolbar',
- overflowHandler: 'scroller',
- dock: 'left',
- style: {
- padding: 0,
- margin: 0,
- },
- cls: 'pve-toolbar-bg',
- items: {
- xtype: 'treelist',
- itemId: 'menu',
- ui: 'pve-nav',
- expanderOnly: true,
- expanderFirst: false,
- animation: false,
- singleExpand: false,
- listeners: {
- selectionchange: function (treeList, selection) {
- if (!selection) {
- return;
- }
- let view = this.up('panel');
- view.suspendLayout = true;
- view.activateCard(selection.data.id);
- view.suspendLayout = false;
- view.updateLayout();
- },
- itemclick: function (treelist, info) {
- var olditem = treelist.getSelection();
- var newitem = info.node;
-
- // when clicking on the expand arrow, we don't select items, but still want the original behaviour
- if (info.select === false) {
- return;
- }
-
- // click on a different, open item then leave it open, else toggle the clicked item
- if (olditem.data.id !== newitem.data.id && newitem.data.expanded === true) {
- info.toggle = false;
- } else {
- info.toggle = true;
- }
- },
- },
- },
- },
- {
- xtype: 'toolbar',
- itemId: 'toolbar',
- dock: 'top',
- height: 36,
- overflowHandler: 'scroller',
- },
- ],
-
- firstItem: '',
- layout: 'card',
- border: 0,
-
- // used for automated test
- selectById: function (cardid) {
- var me = this;
-
- var root = me.store.getRoot();
- var selection = root.findChild('id', cardid, true);
-
- if (selection) {
- selection.expand();
- let menu = me.down('#menu');
- menu.setSelection(selection);
- return cardid;
- }
- return '';
- },
-
- activateCard: function (cardid) {
- var me = this;
- if (me.savedItems[cardid]) {
- let curcard = me.getLayout().getActiveItem();
- let newcard = me.add(me.savedItems[cardid]);
- me.helpButton.setOnlineHelp(newcard.onlineHelp || me.onlineHelp);
- if (curcard) {
- me.setActiveItem(cardid);
- me.remove(curcard, true);
-
- // trigger state change
-
- let ncard = cardid;
- // Note: '' is alias for first tab.
- // First tab can be 'search' or something else
- if (cardid === me.firstItem) {
- ncard = '';
- }
- if (me.hstateid) {
- me.sp.set(me.hstateid, { value: ncard });
- }
- }
- }
- },
-
- initComponent: function () {
- var me = this;
-
- var stateid = me.hstateid;
-
- me.sp = Ext.state.Manager.getProvider();
-
- var activeTab; // leaving this undefined means items[0] will be the default tab
-
- if (stateid) {
- let state = me.sp.get(stateid);
- if (state && state.value) {
- // if this tab does not exist, it chooses the first
- activeTab = state.value;
- }
- }
-
- // get title
- var title = me.title || me.pveSelNode.data.text;
- me.title = undefined;
-
- // create toolbar
- var tbar = me.tbar || [];
- me.tbar = undefined;
-
- if (!me.onlineHelp) {
- // use the onlineHelp property indirection to enforce checking reference validity
- let typeToOnlineHelp = {
- 'type/lxc': { onlineHelp: 'chapter_pct' },
- 'type/node': { onlineHelp: 'chapter_system_administration' },
- 'type/pool': { onlineHelp: 'pveum_pools' },
- 'type/qemu': { onlineHelp: 'chapter_virtual_machines' },
- 'type/sdn': { onlineHelp: 'chapter_pvesdn' },
- 'type/network': { onlineHelp: 'chapter_pvesdn' },
- 'type/storage': { onlineHelp: 'chapter_storage' },
- };
- me.onlineHelp = typeToOnlineHelp[me.pveSelNode.data.id]?.onlineHelp;
- }
-
- if (me.tbarSpacing) {
- tbar.unshift('->');
- }
- tbar.unshift({
- xtype: 'tbtext',
- text: title,
- baseCls: 'x-panel-header-text',
- });
-
- me.helpButton = Ext.create('Proxmox.button.Help', {
- hidden: false,
- listenToGlobalEvent: false,
- onlineHelp: me.onlineHelp || undefined,
- });
-
- tbar.push(me.helpButton);
-
- me.dockedItems[1].items = tbar;
-
- // include search tab
- me.items = me.items || [];
- if (me.showSearch) {
- me.items.unshift({
- xtype: 'pveResourceGrid',
- itemId: 'search',
- title: gettext('Search'),
- iconCls: 'fa fa-search',
- pveSelNode: me.pveSelNode,
- });
- }
-
- me.savedItems = {};
- if (me.items[0]) {
- me.firstItem = me.items[0].itemId;
- }
-
- me.store = Ext.create('Ext.data.TreeStore', {
- root: {
- expanded: true,
- },
- });
- var root = me.store.getRoot();
- me.insertNodes(me.items);
-
- delete me.items;
- me.defaults = me.defaults || {};
- Ext.apply(me.defaults, {
- pveSelNode: me.pveSelNode,
- viewFilter: me.viewFilter,
- workspace: me.workspace,
- border: 0,
- });
-
- me.callParent();
-
- var menu = me.down('#menu');
- var selection = root.findChild('id', activeTab, true) || root.firstChild;
- var node = selection;
- while (node !== root) {
- node.expand();
- node = node.parentNode;
- }
- menu.setStore(me.store);
- menu.setSelection(selection);
-
- // on a state change,
- // select the new item
- var statechange = function (sp, key, state) {
- // it the state change is for this panel
- if (stateid && key === stateid && state) {
- // get active item
- let acard = me.getLayout().getActiveItem().itemId;
- // get the itemid of the new value
- let ncard = state.value || me.firstItem;
- if (ncard && acard !== ncard) {
- // select the chosen item
- menu.setSelection(root.findChild('id', ncard, true) || root.firstChild);
- }
- }
- };
-
- if (stateid) {
- me.mon(me.sp, 'statechange', statechange);
- }
- },
-
- insertNodes: function (items) {
- var me = this;
- var root = me.store.getRoot();
-
- items.forEach(function (item) {
- var treeitem = Ext.create('Ext.data.TreeModel', {
- id: item.itemId,
- text: item.title,
- iconCls: item.iconCls,
- leaf: true,
- expanded: item.expandedOnInit,
- });
- item.header = false;
- if (me.savedItems[item.itemId] !== undefined) {
- throw 'itemId already exists, please use another';
- }
- me.savedItems[item.itemId] = item;
-
- var group;
- var curnode = root;
-
- // get/create the group items
- while (Ext.isArray(item.groups) && item.groups.length > 0) {
- group = item.groups.shift();
-
- let child = curnode.findChild('id', group);
- if (child === null) {
- // did not find the group item
- // so add it where we are
- break;
- }
- curnode = child;
- }
-
- // insert the item
-
- // lets see if it already exists
- var node = curnode.findChild('id', item.itemId);
-
- if (node === null) {
- curnode.appendChild(treeitem);
- } else {
- // should not happen!
- throw 'id already exists';
- }
- });
- },
-});
-/*
- * Input panel for advanced backup options intended to be used as part of an edit/create window.
- */
-Ext.define('PVE.panel.BackupAdvancedOptions', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveBackupAdvancedOptionsPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- cbindData: function () {
- let me = this;
- me.isCreate = !!me.isCreate;
- return {};
- },
-
- viewModel: {
- data: {},
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- toggleFleecing: function (cb, value) {
- let me = this;
- me.lookup('fleecingStorage').setDisabled(!value);
- },
-
- control: {
- 'proxmoxcheckbox[reference=fleecingEnabled]': {
- change: 'toggleFleecing',
- },
- },
- },
-
- onGetValues: function (formValues) {
- let me = this;
- if (me.needMask) {
- // isMasked() may not yet be true if not rendered once
- return {};
- }
-
- if (!formValues.id && me.isCreate) {
- formValues.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
- }
-
- let options = {};
-
- if (!me.isCreate) {
- options.delete = []; // to avoid having to check this all the time
- }
- const deletePropertyOnEdit = me.isCreate
- ? () => {
- /* no-op on create */
- }
- : (key) => options.delete.push(key);
-
- let fleecing = {},
- fleecingOptions = ['fleecing-enabled', 'fleecing-storage'];
- let performance = {},
- performanceOptions = ['max-workers', 'pbs-entries-max'];
-
- for (const [key, value] of Object.entries(formValues)) {
- if (performanceOptions.includes(key)) {
- performance[key] = value;
- // deleteEmpty is not currently implemented for pveBandwidthField
- } else if (key === 'bwlimit' && value === '') {
- deletePropertyOnEdit('bwlimit');
- } else if (key === 'delete') {
- if (Array.isArray(value)) {
- value
- .filter((opt) => !performanceOptions.includes(opt))
- .forEach((opt) => deletePropertyOnEdit(opt));
- } else if (!performanceOptions.includes(formValues.delete)) {
- deletePropertyOnEdit(value);
- }
- } else if (fleecingOptions.includes(key)) {
- let fleecingKey = key.slice('fleecing-'.length);
- fleecing[fleecingKey] = value;
- } else {
- options[key] = value;
- }
- }
-
- if (Object.keys(performance).length > 0) {
- options.performance = PVE.Parser.printPropertyString(performance);
- } else {
- deletePropertyOnEdit('performance');
- }
-
- if (Object.keys(fleecing).length > 0) {
- options.fleecing = PVE.Parser.printPropertyString(fleecing);
- } else {
- deletePropertyOnEdit('fleecing');
- }
-
- if (me.isCreate) {
- delete options.delete;
- }
-
- return options;
- },
-
- onSetValues: function (values) {
- if (values.fleecing) {
- for (const [key, value] of Object.entries(values.fleecing)) {
- values[`fleecing-${key}`] = value;
- }
- delete values.fleecing;
- }
- if (values['pbs-change-detection-mode'] === '__default__') {
- delete values['pbs-change-detection-mode'];
- }
- return values;
- },
-
- updateCompression: function (value, disabled) {
- this.lookup('zstdThreadCount').setDisabled(disabled || value !== 'zstd');
- },
-
- items: [
- {
- xtype: 'pveTwoColumnContainer',
- startColumn: {
- xtype: 'pmxDisplayEditField',
- vtype: 'ConfigId',
- fieldLabel: gettext('Job ID'),
- emptyText: gettext('Autogenerate'),
- name: 'id',
- allowBlank: true,
- cbind: {
- editable: '{isCreate}',
- },
- },
- endFlex: 2,
- endColumn: {
- xtype: 'displayfield',
- value: gettext('Can be used in notification matchers to match this job.'),
- },
- },
- {
- xtype: 'pveTwoColumnContainer',
- startColumn: {
- xtype: 'pveBandwidthField',
- name: 'bwlimit',
- fieldLabel: gettext('Bandwidth Limit'),
- emptyText: gettext('Fallback'),
- backendUnit: 'KiB',
- allowZero: true,
- emptyValue: '',
- autoEl: {
- tag: 'div',
- 'data-qtip': Ext.String.format(gettext('Use {0} for unlimited'), 0),
- },
- },
- endFlex: 2,
- endColumn: {
- xtype: 'displayfield',
- value: `${gettext('Limit I/O bandwidth.')} ${Ext.String.format(gettext('Schema default: {0}'), 0)}`,
- },
- },
- {
- xtype: 'pveTwoColumnContainer',
- startColumn: {
- xtype: 'proxmoxintegerfield',
- name: 'zstd',
- reference: 'zstdThreadCount',
- fieldLabel: gettext('Zstd Threads'),
- fieldStyle: 'text-align: right',
- emptyText: gettext('Fallback'),
- minValue: 0,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('With 0, half of the available cores are used'),
- },
- },
- endFlex: 2,
- endColumn: {
- xtype: 'displayfield',
- value: `${gettext('Threads used for zstd compression (non-PBS).')} ${Ext.String.format(gettext('Schema default: {0}'), 1)}`,
- },
- },
- {
- xtype: 'pveTwoColumnContainer',
- startColumn: {
- xtype: 'proxmoxintegerfield',
- name: 'max-workers',
- minValue: 1,
- maxValue: 256,
- fieldLabel: gettext('IO-Workers'),
- fieldStyle: 'text-align: right',
- emptyText: gettext('Fallback'),
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- endFlex: 2,
- endColumn: {
- xtype: 'displayfield',
- value: `${gettext('I/O workers in the QEMU process (VMs only).')} ${Ext.String.format(gettext('Schema default: {0}'), 16)}`,
- },
- },
- {
- xtype: 'pveTwoColumnContainer',
- startColumn: {
- xtype: 'proxmoxcheckbox',
- name: 'fleecing-enabled',
- reference: 'fleecingEnabled',
- fieldLabel: gettext('Fleecing'),
- uncheckedValue: 0,
- value: 0,
- },
- endFlex: 2,
- endColumn: {
- xtype: 'displayfield',
- value: gettext(
- 'Backup write cache that can reduce IO pressure inside guests (VMs only).',
- ),
- },
- },
- {
- xtype: 'pveTwoColumnContainer',
- startColumn: {
- xtype: 'pveStorageSelector',
- name: 'fleecing-storage',
- fieldLabel: gettext('Fleecing Storage'),
- reference: 'fleecingStorage',
- clusterView: true,
- storageContent: 'images',
- allowBlank: false,
- disabled: true,
- },
- endFlex: 2,
- endColumn: {
- xtype: 'displayfield',
- value: gettext(
- 'Prefer a fast and local storage, ideally with support for discard and thin-provisioning or sparse files.',
- ),
- },
- },
- {
- // It's part of the 'performance' property string, so have a field to preserve the
- // value, but don't expose it. It's a rather niche setting and difficult to
- // convey/understand what it does.
- xtype: 'proxmoxintegerfield',
- name: 'pbs-entries-max',
- hidden: true,
- fieldLabel: 'TODO',
- fieldStyle: 'text-align: right',
- emptyText: 'TODO',
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'pveTwoColumnContainer',
- startColumn: {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Repeat missed'),
- name: 'repeat-missed',
- uncheckedValue: 0,
- defaultValue: 0,
- cbind: {
- deleteDefaultValue: '{!isCreate}',
- },
- },
- endFlex: 2,
- endColumn: {
- xtype: 'displayfield',
- value: gettext(
- "Run jobs as soon as possible if they couldn't start on schedule, for example, due to the node being offline.",
- ),
- },
- },
- {
- xtype: 'pveTwoColumnContainer',
- startColumn: {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('PBS change detection mode'),
- name: 'pbs-change-detection-mode',
- deleteEmpty: true,
- value: '__default__',
- comboItems: [
- ['__default__', 'Default'],
- ['data', 'Data'],
- ['metadata', 'Metadata'],
- ],
- },
- endFlex: 2,
- endColumn: {
- xtype: 'displayfield',
- value: gettext(
- 'Mode to detect file changes and switch archive encoding format for container backups.',
- ),
- },
- },
- {
- xtype: 'component',
- padding: '5 1',
- html: `${gettext('Note')} : ${gettext(
- "The node-specific 'vzdump.conf' or, if this is not set, the default from the config schema is used to determine fallback values.",
- )}`,
- },
- ],
-});
-/*
- * Input panel for notification options of backup jobs.
- */
-Ext.define('PVE.panel.BackupNotificationOptions', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveBackupNotificationOptionsPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'chapter_notifications',
-
- cbindData: function () {
- let me = this;
- me.isCreate = !!me.isCreate;
- return {};
- },
-
- viewModel: {
- data: {
- notificationMode: undefined,
- },
- formulas: {
- showMailtoFields: (get) => {
- let mode = get('notificationMode');
- return mode['notification-mode'] === 'legacy-sendmail';
- },
- },
- },
-
- onSetValues: function (values) {
- let me = this;
-
- let mode = values['notification-mode'] ?? 'auto';
- let mailto = values.mailto;
-
- let mappedMode = 'legacy-sendmail';
-
- // The 'auto' mode is a bit annoying and confusing, so we try
- // to map it to the equivalent behavior.
- if ((mode === 'auto' && !mailto) || mode === 'notification-system') {
- mappedMode = 'notification-system';
- }
-
- me.getViewModel().set('notificationMode', { 'notification-mode': mappedMode });
-
- values['notification-mode'] = mappedMode;
- return values;
- },
-
- items: [
- {
- xtype: 'radiogroup',
- height: '15px',
- layout: {
- type: 'vbox',
- },
- bind: {
- value: '{notificationMode}',
- },
- items: [
- {
- xtype: 'radiofield',
- name: 'notification-mode',
- inputValue: 'notification-system',
- boxLabel: gettext('Use global notification settings'),
- cbind: {
- checked: '{isCreate}',
- },
- },
- {
- xtype: 'radiofield',
- name: 'notification-mode',
- inputValue: 'legacy-sendmail',
- boxLabel: gettext('Use sendmail to send an email (legacy)'),
- },
- ],
- },
- {
- xtype: 'textfield',
- fieldLabel: gettext('Recipients'),
- emptyText: 'test@example.com, ...',
- name: 'mailto',
- padding: '0 0 0 50',
- disabled: true,
- bind: {
- disabled: '{!showMailtoFields}',
- },
- },
- {
- xtype: 'pveEmailNotificationSelector',
- fieldLabel: gettext('When'),
- name: 'mailnotification',
- padding: '0 0 0 50',
- disabled: true,
- value: 'always',
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- bind: {
- disabled: '{!showMailtoFields}',
- },
- },
- ],
-});
-/*
- * Input panel for prune settings with a keep-all option intended to be used as
- * part of an edit/create window.
- */
-Ext.define('PVE.panel.BackupJobPrune', {
- extend: 'Proxmox.panel.PruneInputPanel',
- xtype: 'pveBackupJobPrunePanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'vzdump_retention',
-
- onGetValues: function (formValues) {
- if (this.needMask) {
- // isMasked() may not yet be true if not rendered once
- return {};
- } else if (this.isCreate && !this.rendered) {
- return this.keepAllDefaultForCreate ? { 'prune-backups': 'keep-all=1' } : {};
- }
-
- let options = { delete: [] };
-
- if ('max-protected-backups' in formValues) {
- options['max-protected-backups'] = formValues['max-protected-backups'];
- } else if (this.hasMaxProtected) {
- options.delete.push('max-protected-backups');
- }
-
- delete formValues['max-protected-backups'];
- delete formValues.delete;
-
- let retention = PVE.Parser.printPropertyString(formValues);
- if (retention === '') {
- options.delete.push('prune-backups');
- } else {
- options['prune-backups'] = retention;
- }
-
- if (this.isCreate) {
- delete options.delete;
- }
-
- return options;
- },
-
- updateComponents: function () {
- let me = this;
-
- let keepAll = me.down('proxmoxcheckbox[name=keep-all]').getValue();
- let anyValue = false;
- me.query('pmxPruneKeepField').forEach((field) => {
- anyValue = anyValue || field.getValue() !== null;
- field.setDisabled(keepAll);
- });
- me.down('component[name=no-keeps-hint]').setHidden(anyValue || keepAll);
- },
-
- listeners: {
- afterrender: function (panel) {
- if (panel.needMask) {
- panel.down('component[name=no-keeps-hint]').setHtml('');
- panel.mask(gettext('Backup content type not available for this storage.'));
- } else if (panel.isCreate && panel.keepAllDefaultForCreate) {
- panel.down('proxmoxcheckbox[name=keep-all]').setValue(true);
- }
- panel.down('component[name=pbs-hint]').setHidden(!panel.showPBSHint);
-
- let maxProtected = panel.down('proxmoxintegerfield[name=max-protected-backups]');
- maxProtected.setDisabled(!panel.hasMaxProtected);
- maxProtected.setHidden(!panel.hasMaxProtected);
-
- panel.query('pmxPruneKeepField').forEach((field) => {
- field.on('change', panel.updateComponents, panel);
- });
- panel.updateComponents();
- },
- },
-
- columnT: {
- xtype: 'proxmoxcheckbox',
- name: 'keep-all',
- boxLabel: gettext('Keep all backups'),
- listeners: {
- change: function (field, newValue) {
- let panel = field.up('pveBackupJobPrunePanel');
- panel.updateComponents();
- },
- },
- },
-
- columnB: [
- {
- xtype: 'component',
- userCls: 'pmx-hint',
- name: 'no-keeps-hint',
- hidden: true,
- padding: '5 1',
- cbind: {
- html: '{fallbackHintHtml}',
- },
- },
- {
- xtype: 'component',
- userCls: 'pmx-hint',
- name: 'pbs-hint',
- hidden: true,
- padding: '5 1',
- html: gettext(
- "It's preferred to configure backup retention directly on the Proxmox Backup Server.",
- ),
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'max-protected-backups',
- fieldLabel: gettext('Maximum Protected'),
- minValue: -1,
- hidden: true,
- disabled: true,
- emptyText: 'unlimited with Datastore.Allocate privilege, 5 otherwise',
- deleteEmpty: true,
- autoEl: {
- tag: 'div',
- 'data-qtip': Ext.String.format(gettext('Use {0} for unlimited'), -1),
- },
- },
- ],
-});
-Ext.define('PVE.widget.HealthWidget', {
- extend: 'Ext.Component',
- alias: 'widget.pveHealthWidget',
-
- data: {
- iconCls: PVE.Utils.get_health_icon(undefined, true),
- text: '',
- title: '',
- },
-
- style: {
- 'text-align': 'center',
- },
-
- tpl: ['
{title} ', ' ', ' ', '{text}'],
-
- updateHealth: function (data) {
- var me = this;
- me.update(Ext.apply(me.data, data));
- },
-
- initComponent: function () {
- var me = this;
-
- if (me.title) {
- me.config.data.title = me.title;
- }
-
- me.callParent();
- },
-});
-Ext.define('pve-fw-ipsets', {
- extend: 'Ext.data.Model',
- fields: ['name', 'comment', 'digest'],
- idProperty: 'name',
-});
-
-Ext.define('PVE.IPSetList', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveIPSetList',
-
- stateful: true,
- stateId: 'grid-firewall-ipsetlist',
-
- ipset_panel: undefined,
-
- base_url: undefined,
-
- addBtn: undefined,
- removeBtn: undefined,
- editBtn: undefined,
-
- initComponent: function () {
- var me = this;
-
- if (typeof me.ipset_panel === 'undefined') {
- throw 'no rule panel specified';
- }
-
- if (typeof me.ipset_panel === 'undefined') {
- throw 'no base_url specified';
- }
-
- var store = new Ext.data.Store({
- model: 'pve-fw-ipsets',
- proxy: {
- type: 'proxmox',
- url: '/api2/json' + me.base_url,
- },
- sorters: {
- property: 'name',
- direction: 'ASC',
- },
- });
-
- var caps = Ext.state.Manager.get('GuiCap');
- let canEdit =
- !!caps.vms['VM.Config.Network'] ||
- !!caps.dc['Sys.Modify'] ||
- !!caps.nodes['Sys.Modify'];
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var reload = function () {
- var oldrec = sm.getSelection()[0];
- store.load(function (records, operation, success) {
- if (oldrec) {
- let rec = store.findRecord('name', oldrec.data.name, 0, false, true, true);
- if (rec) {
- sm.select(rec);
- }
- }
- });
- };
-
- var run_editor = function () {
- var rec = sm.getSelection()[0];
- if (!rec || !canEdit) {
- return;
- }
- var win = Ext.create('Proxmox.window.Edit', {
- subject: "IPSet '" + rec.data.name + "'",
- url: me.base_url,
- method: 'POST',
- digest: rec.data.digest,
- items: [
- {
- xtype: 'hiddenfield',
- name: 'rename',
- value: rec.data.name,
- },
- {
- xtype: 'textfield',
- name: 'name',
- value: rec.data.name,
- fieldLabel: gettext('Name'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'comment',
- value: rec.data.comment,
- fieldLabel: gettext('Comment'),
- },
- ],
- });
- win.show();
- win.on('destroy', reload);
- };
-
- me.editBtn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- enableFn: (rec) => canEdit,
- selModel: sm,
- handler: run_editor,
- });
-
- me.addBtn = new Proxmox.button.Button({
- text: gettext('Create'),
- handler: function () {
- sm.deselectAll();
- var win = Ext.create('Proxmox.window.Edit', {
- subject: 'IPSet',
- url: me.base_url,
- method: 'POST',
- items: [
- {
- xtype: 'textfield',
- name: 'name',
- value: '',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'comment',
- value: '',
- fieldLabel: gettext('Comment'),
- },
- ],
- });
- win.show();
- win.on('destroy', reload);
- },
- });
-
- me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
- enableFn: (rec) => canEdit,
- selModel: sm,
- baseurl: me.base_url + '/',
- callback: reload,
- });
-
- Ext.apply(me, {
- store: store,
- tbar: ['IPSet: ', me.addBtn, me.removeBtn, me.editBtn],
- selModel: sm,
- columns: [
- {
- header: 'IPSet',
- dataIndex: 'name',
- minWidth: 150,
- flex: 1,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- flex: 4,
- },
- ],
- listeners: {
- itemdblclick: run_editor,
- select: function (_, rec) {
- var url = me.base_url + '/' + rec.data.name;
- me.ipset_panel.setBaseUrl(url);
- },
- deselect: function () {
- me.ipset_panel.setBaseUrl(undefined);
- },
- show: reload,
- },
- });
-
- if (!canEdit) {
- me.addBtn.setDisabled(true);
- }
-
- me.callParent();
-
- store.load();
- },
-});
-
-Ext.define('PVE.IPSetCidrEdit', {
- extend: 'Proxmox.window.Edit',
-
- cidr: undefined,
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = me.cidr === undefined;
-
- if (me.isCreate) {
- me.url = '/api2/extjs' + me.base_url;
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs' + me.base_url + '/' + me.cidr;
- me.method = 'PUT';
- }
-
- var column1 = [];
-
- if (me.isCreate) {
- if (!me.list_refs_url) {
- throw 'no alias_base_url specified';
- }
-
- column1.push({
- xtype: 'pveIPRefSelector',
- name: 'cidr',
- ref_type: 'alias',
- autoSelect: false,
- editable: true,
- base_url: me.list_refs_url,
- allowBlank: false,
- fieldLabel: gettext('IP/CIDR'),
- });
- } else {
- column1.push({
- xtype: 'displayfield',
- name: 'cidr',
- value: '',
- fieldLabel: gettext('IP/CIDR'),
- });
- }
-
- var ipanel = Ext.create('Proxmox.panel.InputPanel', {
- isCreate: me.isCreate,
- column1: column1,
- column2: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'nomatch',
- checked: false,
- uncheckedValue: 0,
- fieldLabel: 'nomatch',
- },
- ],
- columnB: [
- {
- xtype: 'textfield',
- name: 'comment',
- value: '',
- fieldLabel: gettext('Comment'),
- },
- ],
- });
-
- Ext.apply(me, {
- subject: gettext('IP/CIDR'),
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-
-Ext.define(
- 'PVE.IPSetGrid',
- {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveIPSetGrid',
-
- stateful: true,
- stateId: 'grid-firewall-ipsets',
-
- base_url: undefined,
- list_refs_url: undefined,
-
- addBtn: undefined,
- removeBtn: undefined,
- editBtn: undefined,
-
- setBaseUrl: function (url) {
- var me = this;
-
- me.base_url = url;
-
- if (url === undefined) {
- me.addBtn.setDisabled(true);
- me.store.removeAll();
- } else {
- if (me.canEdit) {
- me.addBtn.setDisabled(false);
- }
- me.removeBtn.baseurl = url + '/';
- me.store.setProxy({
- type: 'proxmox',
- url: '/api2/json' + url,
- });
-
- me.store.load();
- }
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.list_refs_url) {
- throw 'no1 list_refs_url specified';
- }
-
- var store = new Ext.data.Store({
- model: 'pve-ipset',
- });
-
- var reload = function () {
- store.load();
- };
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- me.caps = Ext.state.Manager.get('GuiCap');
- me.canEdit =
- !!me.caps.vms['VM.Config.Network'] ||
- !!me.caps.dc['Sys.Modify'] ||
- !!me.caps.nodes['Sys.Modify'];
-
- var run_editor = function () {
- var rec = sm.getSelection()[0];
- if (!rec || !me.canEdit) {
- return;
- }
- var win = Ext.create('PVE.IPSetCidrEdit', {
- base_url: me.base_url,
- cidr: rec.data.cidr,
- });
- win.show();
- win.on('destroy', reload);
- };
-
- me.editBtn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- enableFn: (rec) => me.canEdit,
- selModel: sm,
- handler: run_editor,
- });
-
- me.addBtn = new Proxmox.button.Button({
- text: gettext('Add'),
- disabled: true,
- enableFn: (rec) => me.canEdit,
- handler: function () {
- if (!me.base_url) {
- return;
- }
- var win = Ext.create('PVE.IPSetCidrEdit', {
- base_url: me.base_url,
- list_refs_url: me.list_refs_url,
- });
- win.show();
- win.on('destroy', reload);
- },
- });
-
- me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
- disabled: true,
- enableFn: (rec) => me.canEdit,
- selModel: sm,
- baseurl: me.base_url + '/',
- callback: reload,
- });
-
- var render_errors = function (value, metaData, record) {
- var errors = record.data.errors;
- if (errors) {
- let msg = errors.cidr || errors.nomatch;
- if (msg) {
- metaData.tdCls = 'proxmox-invalid-row';
- let html = Ext.htmlEncode(`${Ext.htmlEncode(msg)}
`);
- metaData.tdAttr = `data-qwidth=600 data-qtitle="ERROR" data-qtip="${html}"`;
- }
- }
- return Ext.htmlEncode(value);
- };
-
- Ext.apply(me, {
- tbar: ['IP/CIDR: ', me.addBtn, me.removeBtn, me.editBtn],
- store: store,
- selModel: sm,
- listeners: {
- itemdblclick: run_editor,
- },
- columns: [
- {
- xtype: 'rownumberer',
- // cannot use width on instantiation as rownumberer hard-wires that in the
- // constructor to avoid being overridden by applyDefaults
- minWidth: 40,
- },
- {
- header: gettext('IP/CIDR'),
- dataIndex: 'cidr',
- minWidth: 150,
- flex: 1,
- renderer: function (value, metaData, record) {
- value = render_errors(value, metaData, record);
- if (record.data.nomatch) {
- return '! ' + value;
- }
- return value;
- },
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- flex: 3,
- renderer: function (value) {
- return Ext.util.Format.htmlEncode(value);
- },
- },
- ],
- });
-
- me.callParent();
-
- if (me.base_url) {
- me.setBaseUrl(me.base_url); // load
- }
- },
- },
- function () {
- Ext.define('pve-ipset', {
- extend: 'Ext.data.Model',
- fields: [{ name: 'nomatch', type: 'boolean' }, 'cidr', 'comment', 'errors'],
- idProperty: 'cidr',
- });
- },
-);
-
-Ext.define('PVE.IPSet', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveIPSet',
-
- title: 'IPSet',
-
- onlineHelp: 'pve_firewall_ip_sets',
-
- list_refs_url: undefined,
-
- initComponent: function () {
- var me = this;
-
- if (!me.list_refs_url) {
- throw 'no list_refs_url specified';
- }
-
- var ipset_panel = Ext.createWidget('pveIPSetGrid', {
- region: 'center',
- list_refs_url: me.list_refs_url,
- border: false,
- });
-
- var ipset_list = Ext.createWidget('pveIPSetList', {
- region: 'west',
- ipset_panel: ipset_panel,
- base_url: me.base_url,
- width: '50%',
- border: false,
- split: true,
- });
-
- Ext.apply(me, {
- layout: 'border',
- items: [ipset_list, ipset_panel],
- listeners: {
- show: function () {
- ipset_list.fireEvent('show', ipset_list);
- },
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.window.IPInfo', {
- extend: 'Ext.window.Window',
- width: 600,
- title: gettext('Network Information'),
- height: 300,
- layout: {
- type: 'fit',
- },
- modal: true,
- items: [
- {
- xtype: 'grid',
- store: {},
- emptyText: gettext('No network information'),
- viewConfig: {
- enableTextSelection: true,
- },
- columns: [
- {
- dataIndex: 'name',
- text: gettext('Name'),
- renderer: Ext.htmlEncode,
- flex: 3,
- },
- {
- dataIndex: 'hardware-address',
- text: gettext('MAC address'),
- renderer: Ext.htmlEncode,
- width: 140,
- },
- {
- dataIndex: 'ip-addresses',
- text: gettext('IP address'),
- align: 'right',
- flex: 4,
- renderer: function (val) {
- if (!Ext.isArray(val)) {
- return '';
- }
- var ips = [];
- val.forEach(function (ip) {
- var addr = ip['ip-address'];
- var pref = ip.prefix;
- if (addr && pref) {
- ips.push(Ext.htmlEncode(addr + '/' + pref));
- }
- });
- return ips.join(' ');
- },
- },
- ],
- },
- ],
-});
-
-Ext.define('PVE.panel.IPViewBase', {
- extend: 'Ext.container.Container',
- xtype: 'pveIPViewBase',
-
- layout: {
- type: 'hbox',
- align: 'top',
- },
-
- nics: [],
-
- items: [
- {
- xtype: 'box',
- html: ' IPs',
- },
- {
- xtype: 'container',
- flex: 1,
- layout: {
- type: 'vbox',
- align: 'right',
- pack: 'end',
- },
- items: [
- {
- xtype: 'label',
- flex: 1,
- itemId: 'ipBox',
- style: {
- 'text-align': 'right',
- },
- },
- {
- xtype: 'button',
- itemId: 'moreBtn',
- hidden: true,
- ui: 'default-toolbar',
- handler: function (btn) {
- let view = this.up('pveIPViewBase');
-
- var win = Ext.create('PVE.window.IPInfo');
- win.down('grid').getStore().setData(view.nics);
- win.show();
- },
- text: gettext('More'),
- },
- ],
- },
- ],
-
- getDefaultIps: function (nics) {
- var _me = this;
- var ips = [];
- nics.forEach(function (nic) {
- if (
- nic['hardware-address'] &&
- nic['hardware-address'] !== '00:00:00:00:00:00' &&
- nic['hardware-address'] !== '0:0:0:0:0:0'
- ) {
- let nic_ips = nic['ip-addresses'] || [];
- nic_ips.forEach(function (ip) {
- var p = ip['ip-address'];
- // show 2 ips at maximum
- if (ips.length < 2) {
- ips.push(Ext.htmlEncode(p));
- }
- });
- }
- });
-
- return ips;
- },
-
- createUpdateStore: function (nodename, vmid) {
- // implement me in sub-class
- },
-
- startIPStore: function (store, records, success) {
- // implement me in sub-class
- },
-
- updateStatus: function (unsuccessful, defaultText) {
- // implement me in sub-class
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.rstore) {
- throw 'rstore not given';
- }
-
- if (!me.pveSelNode) {
- throw 'pveSelNode not given';
- }
-
- me.callParent();
-
- let { node, vmid } = me.pveSelNode.data;
- me.createUpdateStore(node, vmid);
-
- me.on('destroy', me.ipStore.stopUpdate, me.ipStore);
-
- // if we already have info about the vm, use it immediately
- if (me.rstore.getCount()) {
- me.startIPStore(me.rstore, me.rstore.getData(), false);
- }
-
- me.mon(me.rstore, 'load', me.startIPStore, me);
- },
-});
-
-Ext.define('PVE.panel.IPViewQEMU', {
- extend: 'PVE.panel.IPViewBase',
- xtype: 'pveIPViewQEMU',
-
- createUpdateStore: function (nodename, vmid) {
- let me = this;
-
- me.ipStore = Ext.create('Proxmox.data.UpdateStore', {
- interval: 10000,
- storeid: `pve-qemu-agent-${vmid}`,
- method: 'POST',
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${nodename}/qemu/${vmid}/agent/network-get-interfaces`,
- },
- });
-
- me.mon(me.ipStore, 'load', function (_store, records, success) {
- me.nics = records?.[0]?.data.result;
- me.updateStatus(!success);
- });
- },
-
- updateStatus: function (unsuccessful, defaultText) {
- let me = this;
-
- let text = defaultText || gettext('No network information');
- let more = false;
- if (unsuccessful) {
- text = gettext('Guest Agent not running');
- } else if (me.agent && me.running) {
- if (Ext.isArray(me.nics) && me.nics.length) {
- more = true;
- let ips = me.getDefaultIps(me.nics);
- if (ips.length !== 0) {
- text = ips.join(' ');
- }
- } else if (me.nics && me.nics.error) {
- let msg = gettext('Cannot get info from Guest Agent Error: {0}');
- text = Ext.String.format(msg, Ext.htmlEncode(me.nics.error.desc));
- }
- } else if (me.agent) {
- text = gettext('Guest Agent not running');
- } else {
- text = gettext('No Guest Agent configured');
- }
-
- me.down('#ipBox').update(text);
- me.down('#moreBtn').setVisible(more);
- },
-
- startIPStore: function (store, records, success) {
- let me = this;
-
- let agentRec = store.getById('agent');
- let state = store.getById('status');
-
- me.agent = agentRec && agentRec.data.value === 1;
- me.running = state && state.data.value === 'running';
-
- let caps = Ext.state.Manager.get('GuiCap');
- if (!caps.vms['VM.GuestAgent.Audit']) {
- me.updateStatus(
- false,
- Ext.String.format(gettext("Requires '{0}' Privileges"), 'VM.GuestAgent.Audit'),
- );
- return;
- }
-
- if (me.agent && me.running && me.ipStore.isStopped) {
- me.ipStore.startUpdate();
- } else if (me.ipStore.isStopped) {
- me.updateStatus();
- }
- },
-});
-
-Ext.define('PVE.panel.IPViewLXC', {
- extend: 'PVE.panel.IPViewBase',
- xtype: 'pveIPViewLXC',
-
- createUpdateStore: function (nodename, vmid) {
- let me = this;
-
- me.ipStore = Ext.create('Proxmox.data.UpdateStore', {
- interval: 10000,
- storeid: `lxc-interfaces-${vmid}`,
- method: 'GET',
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${nodename}/lxc/${vmid}/interfaces`,
- },
- });
-
- me.mon(me.ipStore, 'load', function (_store, records, success) {
- me.nics = records?.map((r) => r.data);
- me.updateStatus(!success);
- });
- },
-
- updateStatus: function (_unsuccessful, defaultText) {
- let me = this;
-
- let text = defaultText || gettext('No network information');
- let more = false;
- if (Ext.isArray(me.nics) && me.nics.length) {
- more = true;
- let ips = me.getDefaultIps(me.nics);
- if (ips.length !== 0) {
- text = ips.join(' ');
- }
- }
- me.down('#ipBox').update(text);
- me.down('#moreBtn').setVisible(more);
- },
-
- startIPStore: function (store, records, success) {
- let me = this;
-
- let state = store.getById('status');
- me.running = state && state.data.value === 'running';
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- if (!caps.vms['VM.Audit']) {
- me.updateStatus(
- false,
- Ext.String.format(gettext("Requires '{0}' Privileges"), 'VM.Audit'),
- );
- return;
- }
-
- if (me.running && me.ipStore.isStopped) {
- me.ipStore.startUpdate();
- } else if (me.ipStore.isStopped) {
- me.updateStatus();
- }
- },
-});
-/*
- * This is a running chart widget you add time datapoints to it, and we only
- * show the last x of it used for ceph performance charts
- */
-Ext.define('PVE.widget.RunningChart', {
- extend: 'Ext.container.Container',
- alias: 'widget.pveRunningChart',
-
- layout: {
- type: 'hbox',
- align: 'center',
- },
- items: [
- {
- width: 80,
- xtype: 'box',
- itemId: 'title',
- data: {
- title: '',
- },
- tpl: '{title}: ',
- },
- {
- flex: 1,
- xtype: 'cartesian',
- height: '100%',
- itemId: 'chart',
- border: false,
- axes: [
- {
- type: 'numeric',
- position: 'left',
- hidden: true,
- minimum: 0,
- },
- {
- type: 'numeric',
- position: 'bottom',
- hidden: true,
- },
- ],
-
- store: {
- trackRemoved: false,
- data: {},
- },
-
- sprites: [
- {
- id: 'valueSprite',
- type: 'text',
- text: '0 B/s',
- textAlign: 'end',
- textBaseline: 'middle',
- fontSize: 14,
- },
- ],
-
- series: [
- {
- type: 'line',
- xField: 'time',
- yField: 'val',
- fill: 'true',
- colors: ['#cfcfcf'],
- tooltip: {
- trackMouse: true,
- renderer: function (tooltip, record, ctx) {
- if (!record || !record.data) {
- return;
- }
- const view = this.getChart();
- const date = new Date(record.data.time);
- const value = view.up().renderer(record.data.val);
- const line1 = `${view.up().title}: ${value}`;
- const line2 = Ext.Date.format(date, 'H:i:s');
- tooltip.setHtml(`${line1} ${line2}`);
- },
- },
- style: {
- lineWidth: 1.5,
- opacity: 0.6,
- },
- marker: {
- opacity: 0,
- scaling: 0.01,
- fx: {
- duration: 200,
- easing: 'easeOut',
- },
- },
- highlightCfg: {
- opacity: 1,
- scaling: 1.5,
- },
- },
- ],
- },
- ],
-
- // the renderer for the tooltip and last value, default just the value
- renderer: Ext.identityFn,
-
- // show the last x seconds default is 5 minutes
- timeFrame: 5 * 60,
-
- checkThemeColors: function () {
- let me = this;
- let rootStyle = getComputedStyle(document.documentElement);
-
- // get color
- let background = rootStyle.getPropertyValue('--pwt-panel-background').trim() || '#ffffff';
- let text = rootStyle.getPropertyValue('--pwt-text-color').trim() || '#000000';
-
- // set the colors
- me.chart.setBackground(background);
- me.chart.valuesprite.setAttributes({ fillStyle: text }, true);
- me.chart.redraw();
- },
-
- addDataPoint: function (value, time) {
- let view = this.chart;
- let panel = view.up();
- let now = new Date().getTime();
- let begin = new Date(now - 1000 * panel.timeFrame).getTime();
-
- view.store.add({
- time: time || now,
- val: value || 0,
- });
-
- // delete all old records when we have 20 times more datapoints
- // than seconds in our timeframe (so even a subsecond graph does
- // not trigger this often)
- //
- // records in the store do not take much space, but like this,
- // we prevent a memory leak when someone has the site open for a long time
- // with minimal graphical glitches
- if (view.store.count() > panel.timeFrame * 20) {
- let oldData = view.store.getData().createFiltered(function (item) {
- return item.data.time < begin;
- });
-
- view.store.remove(oldData.getRange());
- }
-
- view.timeaxis.setMinimum(begin);
- view.timeaxis.setMaximum(now);
- view.valuesprite.setText(panel.renderer(value || 0).toString());
- view.valuesprite.setAttributes(
- {
- x: view.getWidth() - 15,
- y: view.getHeight() / 2,
- },
- true,
- );
- view.redraw();
- },
-
- setTitle: function (title) {
- this.title = title;
- let titlebox = this.getComponent('title');
- titlebox.update({ title: title });
- },
-
- initComponent: function () {
- var me = this;
- me.callParent();
-
- if (me.title) {
- me.getComponent('title').update({ title: me.title });
- }
- me.chart = me.getComponent('chart');
- me.chart.timeaxis = me.chart.getAxes()[1];
- me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite');
- if (me.color) {
- me.chart.series[0].setStyle({
- fill: me.color,
- stroke: me.color,
- });
- }
-
- me.checkThemeColors();
-
- // switch colors on media query changes
- me.mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
- me.themeListener = (e) => {
- me.checkThemeColors();
- };
- me.mediaQueryList.addEventListener('change', me.themeListener);
- },
-
- doDestroy: function () {
- let me = this;
-
- me.mediaQueryList.removeEventListener('change', me.themeListener);
-
- me.callParent();
- },
-});
-/*
- * This class describes the bottom panel
- */
-Ext.define('PVE.panel.StatusPanel', {
- extend: 'Ext.tab.Panel',
- alias: 'widget.pveStatusPanel',
-
- //title: "Logs",
- //tabPosition: 'bottom',
-
- initComponent: function () {
- var me = this;
-
- var stateid = 'ltab';
- var sp = Ext.state.Manager.getProvider();
-
- var state = sp.get(stateid);
- if (state && state.value) {
- me.activeTab = state.value;
- }
-
- Ext.apply(me, {
- listeners: {
- tabchange: function () {
- var atab = me.getActiveTab().itemId;
- let tabstate = { value: atab };
- sp.set(stateid, tabstate);
- },
- },
- items: [
- {
- itemId: 'tasks',
- title: gettext('Tasks'),
- xtype: 'pveClusterTasks',
- },
- {
- itemId: 'clog',
- title: gettext('Cluster log'),
- xtype: 'pveClusterLog',
- },
- ],
- });
-
- me.callParent();
-
- me.items.get(0).fireEvent('show', me.items.get(0));
-
- var statechange = function (_, key, newstate) {
- if (key === stateid) {
- let atab = me.getActiveTab().itemId;
- let ntab = newstate.value;
- if (newstate && ntab && atab !== ntab) {
- me.setActiveTab(ntab);
- }
- }
- };
-
- sp.on('statechange', statechange);
- me.on('destroy', function () {
- sp.un('statechange', statechange);
- });
- },
-});
-Ext.define('PVE.panel.GuestStatusView', {
- extend: 'Proxmox.panel.StatusView',
- alias: 'widget.pveGuestStatusView',
- mixins: ['Proxmox.Mixin.CBind'],
-
- cbindData: function (initialConfig) {
- var me = this;
- return {
- isQemu: me.pveSelNode.data.type === 'qemu',
- isLxc: me.pveSelNode.data.type === 'lxc',
- };
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- if (view.pveSelNode.data.type !== 'lxc') {
- return;
- }
-
- const nodename = view.pveSelNode.data.node;
- const vmid = view.pveSelNode.data.vmid;
-
- Proxmox.Utils.API2Request({
- url: `/api2/extjs/nodes/${nodename}/lxc/${vmid}/config`,
- waitMsgTargetView: view,
- method: 'GET',
- success: ({ result }) => {
- view.down('#unprivileged').updateValue(
- Proxmox.Utils.format_boolean(result.data.unprivileged),
- );
- view.ostype = Ext.htmlEncode(result.data.ostype);
- },
- });
- },
- },
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- defaults: {
- xtype: 'pmxInfoWidget',
- padding: '2 25',
- },
- items: [
- {
- xtype: 'box',
- height: 20,
- },
- {
- itemId: 'status',
- title: gettext('Status'),
- iconCls: 'fa fa-info fa-fw',
- printBar: false,
- multiField: true,
- renderer: function (record) {
- var _me = this;
- var text = record.data.status;
- var qmpstatus = record.data.qmpstatus;
- if (qmpstatus && qmpstatus !== record.data.status) {
- text += ' (' + qmpstatus + ')';
- }
- return text;
- },
- },
- {
- itemId: 'hamanaged',
- iconCls: 'fa fa-heartbeat fa-fw',
- title: gettext('HA State'),
- printBar: false,
- textField: 'ha',
- renderer: PVE.Utils.format_ha,
- },
- {
- itemId: 'node',
- iconCls: 'fa fa-building fa-fw',
- title: gettext('Node'),
- cbind: {
- text: '{pveSelNode.data.node}',
- },
- printBar: false,
- },
- {
- itemId: 'unprivileged',
- iconCls: 'fa fa-lock fa-fw',
- title: gettext('Unprivileged'),
- printBar: false,
- cbind: {
- hidden: '{isQemu}',
- },
- },
- {
- xtype: 'box',
- height: 10,
- },
- {
- itemId: 'cpu',
- iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
- title: gettext('CPU usage'),
- valueField: 'cpu',
- maxField: 'cpus',
- renderer: Proxmox.Utils.render_cpu_usage,
- // in this specific api call
- // we already have the correct value for the usage
- calculate: Ext.identityFn,
- },
- {
- itemId: 'memory',
- iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
- title: gettext('Memory usage'),
- valueField: 'mem',
- maxField: 'maxmem',
- warningThreshold: 0.9,
- criticalThreshold: 0.975,
- },
- {
- itemId: 'memory-host',
- iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
- title: gettext('Host memory usage'),
- valueField: 'memhost',
- printBar: false,
- renderer: function (used, max) {
- return Proxmox.Utils.render_size(used);
- },
- cbind: {
- hidden: '{isLxc}',
- disabled: '{isLxc}',
- },
- },
- {
- itemId: 'swap',
- iconCls: 'fa fa-refresh fa-fw',
- title: gettext('SWAP usage'),
- valueField: 'swap',
- maxField: 'maxswap',
- cbind: {
- hidden: '{isQemu}',
- disabled: '{isQemu}',
- },
- },
- {
- itemId: 'rootfs',
- iconCls: 'fa fa-hdd-o fa-fw',
- title: gettext('Bootdisk size'),
- valueField: 'disk',
- maxField: 'maxdisk',
- printBar: false,
- renderer: function (used, max) {
- var me = this;
- me.setPrintBar(used > 0);
- if (used === 0) {
- return Proxmox.Utils.render_size(max);
- } else {
- return Proxmox.Utils.render_size_usage(used, max);
- }
- },
- },
- {
- xtype: 'box',
- height: 10,
- },
- {
- cbind: {
- xtype: (get) => (get('isQemu') ? 'pveIPViewQEMU' : 'pveIPViewLXC'),
- rstore: '{rstore}',
- pveSelNode: '{pveSelNode}',
- },
- },
- ],
-
- updateTitle: function () {
- var me = this;
- var uptime = me.getRecordValue('uptime');
-
- var text = '';
- if (Number(uptime) > 0) {
- text =
- ' (' + gettext('Uptime') + ': ' + Proxmox.Utils.format_duration_long(uptime) + ')';
- }
-
- let title = `${me.getRecordValue('name') + text}
`;
-
- if (me.pveSelNode.data.type === 'lxc' && me.ostype && me.ostype !== 'unmanaged') {
- // Manual mappings for distros with special casing
- const namemap = {
- archlinux: 'Arch Linux',
- nixos: 'NixOS',
- opensuse: 'openSUSE',
- centos: 'CentOS',
- };
-
- const distro = namemap[me.ostype] ?? Ext.String.capitalize(me.ostype);
- title += ` ${distro}
`;
- }
-
- me.setTitle(title);
- },
-});
-Ext.define('PVE.guest.Summary', {
- extend: 'Ext.panel.Panel',
- xtype: 'pveGuestSummary',
-
- scrollable: true,
- bodyPadding: 5,
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- if (!me.workspace) {
- throw 'no workspace specified';
- }
-
- if (!me.statusStore) {
- throw 'no status storage specified';
- }
-
- var type = me.pveSelNode.data.type;
- var template = !!me.pveSelNode.data.template;
- var rstore = me.statusStore;
-
- let hideMemhostStateKey = 'pve-vm-hide-memhost';
- let sp = Ext.state.Manager.getProvider();
-
- let memoryStats = {
- fields: ['maxmem', 'mem'],
- fieldTitles: [gettext('Total'), gettext('Used')],
- };
- if (type === 'qemu') {
- memoryStats.fields.push({
- type: 'line',
- fill: false,
- yField: 'memhost',
- title: gettext('Host Memory Usage'),
- hidden: sp.get(hideMemhostStateKey, true),
- style: {
- lineWidth: 2.5,
- opacity: 1,
- },
- });
- }
-
- var items = [
- {
- xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
- flex: 1,
- padding: template ? '5' : '0 5 0 0',
- itemId: 'gueststatus',
- pveSelNode: me.pveSelNode,
- rstore: rstore,
- },
- {
- xtype: 'pmxNotesView',
- flex: 1,
- padding: template ? '5' : '0 0 0 5',
- itemId: 'notesview',
- pveSelNode: me.pveSelNode,
- },
- ];
-
- var rrdstore;
- if (!template) {
- // in non-template mode put the two panels always together
- items = [
- {
- xtype: 'container',
- height: 300,
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
- items: items,
- },
- ];
-
- rrdstore = Ext.create('Proxmox.data.RRDStore', {
- rrdurl: `/api2/json/nodes/${nodename}/${type}/${vmid}/rrddata`,
- model: 'pve-rrd-guest',
- });
-
- items.push(
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('CPU Usage'),
- pveSelNode: me.pveSelNode,
- fields: ['cpu'],
- fieldTitles: [gettext('CPU usage')],
- unit: 'percent',
- store: rrdstore,
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Memory Usage'),
- pveSelNode: me.pveSelNode,
- fields: memoryStats.fields,
- fieldTitles: memoryStats.fieldTitles,
- colors: ['#94ae0a', '#115fa6', '#c4c0c0'],
- unit: 'bytes',
- powerOfTwo: true,
- store: rrdstore,
- onLegendChange: function (_legend, record, _, seriesIndex) {
- if (seriesIndex === 2) {
- // third data series is clicked -> hostmem
- sp.set(hideMemhostStateKey, record.data.disabled);
- }
- },
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Network Traffic'),
- pveSelNode: me.pveSelNode,
- fields: ['netin', 'netout'],
- fieldTitles: [gettext('Incoming'), gettext('Outgoing')],
- store: rrdstore,
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Disk IO'),
- pveSelNode: me.pveSelNode,
- fields: ['diskread', 'diskwrite'],
- fieldTitles: [gettext('Reads'), gettext('Writes')],
- store: rrdstore,
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('CPU Pressure Stall'),
- pveSelNode: me.pveSelNode,
- fieldTitles: ['Some', 'Full'],
- fields: ['pressurecpusome', 'pressurecpufull'],
- colors: ['#FFD13E', '#A61120'],
- store: rrdstore,
- unit: 'percent',
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('IO Pressure Stall'),
- pveSelNode: me.pveSelNode,
- fieldTitles: ['Some', 'Full'],
- fields: ['pressureiosome', 'pressureiofull'],
- colors: ['#FFD13E', '#A61120'],
- store: rrdstore,
- unit: 'percent',
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Memory Pressure Stall'),
- pveSelNode: me.pveSelNode,
- fieldTitles: ['Some', 'Full'],
- fields: ['pressurememorysome', 'pressurememoryfull'],
- colors: ['#FFD13E', '#A61120'],
- store: rrdstore,
- unit: 'percent',
- },
- );
- }
-
- Ext.apply(me, {
- tbar: ['->', { xtype: 'proxmoxRRDTypeSelector' }],
- items: [
- {
- xtype: 'container',
- itemId: 'itemcontainer',
- layout: {
- type: 'column',
- },
- minWidth: 700,
- defaults: {
- minHeight: 360,
- padding: 5,
- },
- items: items,
- listeners: {
- resize: function (container) {
- Proxmox.Utils.updateColumns(container);
- },
- },
- },
- ],
- });
-
- me.callParent();
- if (!template) {
- rrdstore.startUpdate();
- me.on('destroy', rrdstore.stopUpdate);
- }
- me.mon(sp, 'statechange', function (provider, key, value) {
- if (key !== 'summarycolumns') {
- return;
- }
- Proxmox.Utils.updateColumns(me.getComponent('itemcontainer'));
- });
- },
-});
-Ext.define('PVE.panel.TemplateStatusView', {
- extend: 'Proxmox.panel.StatusView',
- alias: 'widget.pveTemplateStatusView',
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- defaults: {
- xtype: 'pmxInfoWidget',
- printBar: false,
- padding: '2 25',
- },
- items: [
- {
- xtype: 'box',
- height: 20,
- },
- {
- itemId: 'hamanaged',
- iconCls: 'fa fa-heartbeat fa-fw',
- title: gettext('HA State'),
- printBar: false,
- textField: 'ha',
- renderer: PVE.Utils.format_ha,
- },
- {
- itemId: 'node',
- iconCls: 'fa fa-fw fa-building',
- title: gettext('Node'),
- },
- {
- xtype: 'box',
- height: 20,
- },
- {
- itemId: 'cpus',
- iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
- title: gettext('Processors'),
- textField: 'cpus',
- },
- {
- itemId: 'memory',
- iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
- title: gettext('Memory'),
- textField: 'maxmem',
- renderer: Proxmox.Utils.render_size,
- },
- {
- itemId: 'swap',
- iconCls: 'fa fa-refresh fa-fw',
- title: gettext('Swap'),
- textField: 'maxswap',
- renderer: Proxmox.Utils.render_size,
- },
- {
- itemId: 'disk',
- iconCls: 'fa fa-hdd-o fa-fw',
- title: gettext('Bootdisk size'),
- textField: 'maxdisk',
- renderer: Proxmox.Utils.render_size,
- },
- {
- xtype: 'box',
- height: 20,
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- var name = me.pveSelNode.data.name;
- if (!name) {
- throw 'no name specified';
- }
-
- me.title = name;
-
- me.callParent();
- if (me.pveSelNode.data.type !== 'lxc') {
- me.remove(me.getComponent('swap'));
- }
- me.getComponent('node').updateValue(me.pveSelNode.data.node);
- },
-});
-Ext.define('PVE.panel.MultiDiskPanel', {
- extend: 'Ext.panel.Panel',
-
- mixins: ['Proxmox.Mixin.CBind'],
-
- setNodename: function (nodename) {
- this.items.each((panel) => panel.setNodename(nodename));
- },
-
- border: false,
- bodyBorder: false,
-
- importDisk: false, // allow import panel
-
- layout: 'card',
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- vmconfig: {},
-
- onAdd: function () {
- this.addDiskChecked(false);
- },
-
- onImport: function () {
- this.addDiskChecked(true);
- },
-
- addDiskChecked: function (importDisk) {
- let me = this;
- me.lookup('addButton').setDisabled(true);
- me.lookup('addImportButton').setDisabled(true);
- me.addDisk(importDisk);
- let count = me.lookup('grid').getStore().getCount() + 1; // +1 is from ide2
- me.lookup('addButton').setDisabled(count >= me.maxCount);
- me.lookup('addImportButton').setDisabled(count >= me.maxCount);
- },
-
- getNextFreeDisk: function (vmconfig) {
- throw 'implement in subclass';
- },
-
- addPanel: function (itemId, vmconfig, nextFreeDisk, importDisk) {
- throw 'implement in subclass';
- },
-
- // define in subclass
- diskSorter: undefined,
-
- addDisk: function (importDisk) {
- let me = this;
- let grid = me.lookup('grid');
- let store = grid.getStore();
-
- // get free disk id
- let vmconfig = me.getVMConfig(true);
- let nextFreeDisk = me.getNextFreeDisk(vmconfig);
- if (!nextFreeDisk) {
- return;
- }
-
- // add store entry + panel
- let itemId = 'disk-card-' + ++Ext.idSeed;
- let rec = store.add({
- name: nextFreeDisk.confid,
- itemId,
- })[0];
-
- let panel = me.addPanel(itemId, vmconfig, nextFreeDisk, importDisk);
- panel.updateVMConfig(vmconfig);
-
- // we need to setup a validitychange handler, so that we can show
- // that a disk has invalid fields
- let fields = panel.query('field');
- fields.forEach((el) =>
- el.on('validitychange', () => {
- let valid = fields.every((field) => field.isValid());
- rec.set('valid', valid);
- me.checkValidity();
- }),
- );
-
- store.sort(me.diskSorter);
-
- // select if the panel added is the only one
- if (store.getCount() === 1) {
- grid.getSelectionModel().select(0, false);
- }
- },
-
- getBaseVMConfig: function () {
- throw 'implement in subclass';
- },
-
- getVMConfig: function (all) {
- let me = this;
-
- let vmconfig = me.getBaseVMConfig();
-
- me.lookup('grid')
- .getStore()
- .each((rec) => {
- if (all || rec.get('valid')) {
- vmconfig[rec.get('name')] = rec.get('itemId');
- }
- });
-
- return vmconfig;
- },
-
- checkValidity: function () {
- let me = this;
- let valid = me.lookup('grid').getStore().findExact('valid', false) === -1;
- me.lookup('validationfield').setValue(valid);
- },
-
- updateVMConfig: function () {
- let me = this;
- let view = me.getView();
- let grid = me.lookup('grid');
- let store = grid.getStore();
-
- let vmconfig = me.getVMConfig();
-
- let valid = true;
-
- store.each((rec) => {
- let itemId = rec.get('itemId');
- let name = rec.get('name');
- let panel = view.getComponent(itemId);
- if (!panel) {
- throw 'unexpected missing panel';
- }
-
- // copy config for each panel and remote its own id
- let panel_vmconfig = Ext.apply({}, vmconfig);
- if (panel_vmconfig[name] === itemId) {
- delete panel_vmconfig[name];
- }
-
- if (!rec.get('valid')) {
- valid = false;
- }
-
- panel.updateVMConfig(panel_vmconfig);
- });
-
- me.lookup('validationfield').setValue(valid);
-
- return vmconfig;
- },
-
- onChange: function (panel, newVal) {
- let me = this;
- let store = me.lookup('grid').getStore();
-
- let el = store.findRecord('itemId', panel.itemId, 0, false, true, true);
- if (el.get('name') === newVal) {
- // do not update if there was no change
- return;
- }
-
- el.set('name', newVal);
- el.commit();
-
- store.sort(me.diskSorter);
-
- // so that it happens after the layouting
- setTimeout(function () {
- me.updateVMConfig();
- }, 10);
- },
-
- onRemove: function (tableview, rowIndex, colIndex, item, event, record) {
- let me = this;
- let grid = me.lookup('grid');
- let store = grid.getStore();
- let removed_idx = store.indexOf(record);
-
- let selection = grid.getSelection()[0];
- let selected_idx = store.indexOf(selection);
-
- if (selected_idx === removed_idx) {
- let newidx = store.getCount() > removed_idx + 1 ? removed_idx + 1 : removed_idx - 1;
- grid.getSelectionModel().select(newidx, false);
- }
-
- store.remove(record);
- me.getView().remove(record.get('itemId'));
- me.lookup('addButton').setDisabled(false);
- me.lookup('addImportButton').setDisabled(false);
- me.updateVMConfig();
- me.checkValidity();
- },
-
- onSelectionChange: function (grid, selection) {
- let me = this;
- if (!selection || selection.length < 1) {
- return;
- }
-
- me.getView().setActiveItem(selection[0].data.itemId);
- },
-
- control: {
- inputpanel: {
- diskidchange: 'onChange',
- },
- 'grid[reference=grid]': {
- selectionchange: 'onSelectionChange',
- },
- },
-
- init: function (view) {
- let me = this;
- me.onAdd();
- me.lookup('grid').getSelectionModel().select(0, false);
- },
- },
-
- dockedItems: [
- {
- xtype: 'container',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- dock: 'left',
- border: false,
- width: 130,
- cbind: {}, // for nested cbinds
- items: [
- {
- xtype: 'grid',
- hideHeaders: true,
- reference: 'grid',
- flex: 1,
- emptyText: gettext('No Disks'),
- margin: '0 0 5 0',
- store: {
- fields: ['name', 'itemId', 'valid'],
- data: [],
- },
- columns: [
- {
- dataIndex: 'name',
- renderer: function (val, md, rec) {
- let warn = '';
- if (!rec.get('valid')) {
- warn = ' ';
- }
- return val + warn;
- },
- flex: 1,
- },
- {
- xtype: 'actioncolumn',
- width: 30,
- align: 'center',
- menuDisabled: true,
- items: [
- {
- iconCls: 'x-fa fa-trash critical',
- tooltip: 'Delete',
- handler: 'onRemove',
- isActionDisabled: 'deleteDisabled',
- },
- ],
- },
- ],
- },
- {
- xtype: 'button',
- reference: 'addButton',
- text: gettext('Add'),
- iconCls: 'fa fa-plus-circle',
- handler: 'onAdd',
- },
- {
- xtype: 'button',
- reference: 'addImportButton',
- text: gettext('Import'),
- iconCls: 'fa fa-cloud-download',
- handler: 'onImport',
- margin: '5 0 0 0',
- cbind: {
- disabled: '{!importDisk}',
- hidden: '{!importDisk}',
- },
- },
- {
- // dummy field to control wizard validation
- xtype: 'textfield',
- hidden: true,
- reference: 'validationfield',
- submitValue: false,
- value: true,
- validator: (val) => !!val,
- },
- ],
- },
- ],
-});
-Ext.define('PVE.panel.TagConfig', {
- extend: 'PVE.panel.Config',
- alias: 'widget.pveTagConfig',
-
- onlineHelp: 'gui_tags',
-});
-/*
- * Left Treepanel, containing all the resources we manage in this datacenter: server nodes, server storages, VMs and Containers
- */
-Ext.define('PVE.tree.ResourceTree', {
- extend: 'Ext.tree.TreePanel',
- alias: ['widget.pveResourceTree'],
-
- userCls: 'proxmox-tags-circle',
-
- statics: {
- typeDefaults: {
- node: {
- iconCls: 'fa fa-building',
- text: gettext('Nodes'),
- },
- pool: {
- iconCls: 'fa fa-tags',
- text: gettext('Resource Pool'),
- },
- storage: {
- iconCls: 'fa fa-database',
- text: gettext('Storage'),
- },
- sdn: {
- iconCls: 'fa fa-th',
- text: gettext('SDN'),
- },
- network: {
- iconCls: 'fa fa-globe',
- text: gettext('Network'),
- },
- qemu: {
- iconCls: 'fa fa-desktop',
- text: gettext('Virtual Machine'),
- },
- lxc: {
- iconCls: 'fa fa-cube',
- text: gettext('LXC Container'),
- },
- template: {
- iconCls: 'fa fa-file-o',
- },
- tag: {
- iconCls: 'fa fa-tag',
- },
- },
- },
-
- columns: [
- {
- xtype: 'treecolumn',
- flex: 1,
- dataIndex: 'text',
- renderer: function (val, meta, rec) {
- let info = rec.data;
-
- let text = info.text;
- let status = '';
- if (info.type === 'storage') {
- let usage = info.disk / info.maxdisk;
- if (usage >= 0.0 && usage <= 1.0) {
- let barHeight = (usage * 100).toFixed(0);
- let remainingHeight = (100 - barHeight).toFixed(0);
- status = '';
- status += `
`;
- status += `
`;
- status += '
';
- }
- }
- if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
- if (PVE.UIOptions.getTreeSortingValue('sort-field') !== 'vmid') {
- text = `${info.name} (${String(info.vmid)})`;
- }
- }
- text = `${status}${text} `;
- text += PVE.Utils.renderTags(info.tags, PVE.UIOptions.tagOverrides);
-
- if (info.id === 'root' && PVE.ClusterName) {
- text += ` (${PVE.ClusterName})`;
- }
-
- return (info.renderedText = text);
- },
- },
- ],
-
- useArrows: true,
-
- // private
- getTypeOrder: function (type) {
- switch (type) {
- case 'lxc':
- return 0;
- case 'qemu':
- return 1;
- case 'node':
- return 2;
- case 'sdn':
- return 3;
- case 'network':
- return 3.5;
- case 'storage':
- return 4;
- default:
- return 9;
- }
- },
-
- // private
- nodeSortFn: function (node1, node2) {
- let me = this;
- let n1 = node1.data,
- n2 = node2.data;
-
- if (!n1.groupbyid === !n2.groupbyid) {
- let n1IsGuest = n1.type === 'qemu' || n1.type === 'lxc';
- let n2IsGuest = n2.type === 'qemu' || n2.type === 'lxc';
- if (me['group-guest-types'] || !n1IsGuest || !n2IsGuest) {
- // first sort (group) by type
- let res = me.getTypeOrder(n1.type) - me.getTypeOrder(n2.type);
- if (res !== 0) {
- return res;
- }
- }
-
- // then sort (group) by ID
- if (n1IsGuest) {
- if (me['group-templates'] && !n1.template !== !n2.template) {
- return n1.template ? 1 : -1; // sort templates after regular VMs
- }
- if (me['sort-field'] === 'vmid') {
- if (n1.vmid > n2.vmid) {
- // prefer VMID as metric for guests
- return 1;
- } else if (n1.vmid < n2.vmid) {
- return -1;
- }
- } else {
- return n1.name.localeCompare(n2.name);
- }
- }
- // same types but not a guest
- return n1.id > n2.id ? 1 : n1.id < n2.id ? -1 : 0;
- } else if (n1.groupbyid) {
- return -1;
- } else if (n2.groupbyid) {
- return 1;
- }
- return 0; // should not happen
- },
-
- // private: fast binary search
- findInsertIndex: function (node, child, start, end) {
- let me = this;
-
- let diff = end - start;
- if (diff <= 0) {
- return start;
- }
- let mid = start + (diff >> 1);
-
- let res = me.nodeSortFn(child, node.childNodes[mid]);
- if (res <= 0) {
- return me.findInsertIndex(node, child, start, mid);
- } else {
- return me.findInsertIndex(node, child, mid + 1, end);
- }
- },
-
- setIconCls: function (info) {
- let cls = PVE.Utils.get_object_icon_class(info.type, info);
- if (cls !== '') {
- info.iconCls = cls;
- }
- },
-
- getToolTip: function (info) {
- let qtips = [];
- if (info.qmpstatus || info.status) {
- qtips.push(Ext.String.format(gettext('Status: {0}'), info.qmpstatus || info.status));
- }
- if (info.lock) {
- qtips.push(Ext.String.format(gettext('Config locked ({0})'), info.lock));
- }
- if (info.hastate !== 'unmanaged') {
- qtips.push(Ext.String.format(gettext('HA State: {0}'), info.hastate));
- }
- if (info.type === 'storage') {
- let usage = info.disk / info.maxdisk;
- if (usage >= 0.0 && usage <= 1.0) {
- qtips.push(Ext.String.format(gettext('Usage: {0}%'), (usage * 100).toFixed(2)));
- }
- }
-
- if (qtips.length === 0) {
- return undefined;
- }
-
- let tip = qtips.join(', ');
- info.tip = tip;
- return tip;
- },
-
- // private
- addChildSorted: function (node, info) {
- let me = this;
-
- me.setIconCls(info);
-
- if (info.groupbyid) {
- if (me.viewFilter.groupRenderer) {
- info.text = me.viewFilter.groupRenderer(info);
- } else if (info.type === 'type') {
- let defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid];
- if (defaults && defaults.text) {
- info.text = defaults.text;
- }
- } else {
- info.text = info.groupbyid;
- }
- }
- let child = Ext.create('PVETree', info);
-
- if (node.childNodes) {
- let pos = me.findInsertIndex(node, child, 0, node.childNodes.length);
- node.insertBefore(child, node.childNodes[pos]);
- } else {
- node.insertBefore(child);
- }
-
- return child;
- },
-
- // private
- groupChild: function (node, info, groups, level) {
- let me = this;
-
- let groupBy = groups[level];
- let v = info[groupBy];
-
- if (v) {
- let group = node.findChild('groupbyid', v);
- if (!group) {
- let groupinfo;
- if (info.type === groupBy) {
- groupinfo = info;
- } else {
- groupinfo = {
- type: groupBy,
- id: groupBy + '/' + v,
- };
- if (groupBy !== 'type') {
- groupinfo[groupBy] = v;
- }
- }
- groupinfo.leaf = false;
- groupinfo.groupbyid = v;
- group = me.addChildSorted(node, groupinfo);
- }
- if (info.type === groupBy) {
- return group;
- }
- if (group) {
- return me.groupChild(group, info, groups, level + 1);
- }
- }
-
- return me.addChildSorted(node, info);
- },
-
- saveSortingOptions: function () {
- let me = this;
- let changed = false;
- for (const key of ['sort-field', 'group-templates', 'group-guest-types']) {
- let newValue = PVE.UIOptions.getTreeSortingValue(key);
- if (me[key] !== newValue) {
- me[key] = newValue;
- changed = true;
- }
- }
- return changed;
- },
-
- initComponent: function () {
- let me = this;
- me.saveSortingOptions();
-
- let rstore = PVE.data.ResourceStore;
- let sp = Ext.state.Manager.getProvider();
-
- if (!me.viewFilter) {
- me.viewFilter = {};
- }
-
- let pdata = {
- dataIndex: {},
- updateCount: 0,
- };
-
- let store = Ext.create('Ext.data.TreeStore', {
- model: 'PVETree',
- root: {
- expanded: true,
- id: 'root',
- text: gettext('Datacenter'),
- iconCls: 'fa fa-server',
- },
- });
-
- let stateid = 'rid';
-
- const changedFields = [
- 'disk',
- 'maxdisk',
- 'vmid',
- 'name',
- 'type',
- 'running',
- 'template',
- 'status',
- 'qmpstatus',
- 'hastate',
- 'lock',
- 'tags',
- ];
-
- // special case ids from the tag view, since they change the id in the state
- let idMapFn = function (id) {
- if (!id) {
- return undefined;
- }
- if (id.startsWith('qemu') || id.startsWith('lxc')) {
- let [realId, _tag] = id.split('-');
- return realId;
- }
- return id;
- };
-
- let findNode = function (rootNode, id) {
- if (!id) {
- return undefined;
- }
- let node = rootNode.findChild('id', id, true);
- if (!node) {
- node = rootNode.findChildBy(
- (r) => idMapFn(r.data.id) === idMapFn(id),
- undefined,
- true,
- );
- }
- return node;
- };
-
- let firstUpdate = true;
-
- let updateTree = function () {
- store.suspendEvents();
-
- let rootnode;
- if (firstUpdate) {
- rootnode = Ext.create('PVETree', {
- expanded: true,
- id: 'root',
- text: gettext('Datacenter'),
- iconCls: 'fa fa-server',
- });
- } else {
- rootnode = me.store.getRootNode();
- }
- // remember selected node (and all parents)
- let sm = me.getSelectionModel();
- let lastsel = sm.getSelection()[0];
- let parents = [];
- let sorting_changed = me.saveSortingOptions();
- for (let node = lastsel; node; node = node.parentNode) {
- parents.push(node);
- }
-
- let groups = me.viewFilter.groups || [];
- // explicitly check for node/template, as those are not always grouping attributes
- let attrMoveChecks = me.viewFilter.attrMoveChecks ?? {};
-
- // also check for name for when the tree is sorted by name
- let moveCheckAttrs = groups.concat(['node', 'template', 'name']);
- let filterFn = me.viewFilter.getFilterFn ? me.viewFilter.getFilterFn() : Ext.identityFn;
-
- let reselect = false; // for disappeared nodes
- let index = pdata.dataIndex;
- // remove vanished or moved items and update changed items in-place
- for (const [key, olditem] of Object.entries(index)) {
- // getById() use find(), which is slow (ExtJS4 DP5)
- let oldid = olditem.data.id;
- let id = idMapFn(olditem.data.id);
- let item = rstore.data.get(id);
-
- let changed = sorting_changed,
- moved = sorting_changed;
- if (item) {
- // test if any grouping attributes changed, catches migrated tree-nodes in server view too
- for (const attr of moveCheckAttrs) {
- if (attrMoveChecks[attr]) {
- if (attrMoveChecks[attr](olditem, item)) {
- moved = true;
- break;
- }
- } else if (item.data[attr] !== olditem.data[attr]) {
- moved = true;
- break;
- }
- }
-
- // tree item has been updated
- for (const field of changedFields) {
- if (item.data[field] !== olditem.data[field]) {
- changed = true;
- break;
- }
- }
- // FIXME: also test filterfn()?
- }
-
- if (changed) {
- olditem.beginEdit();
- let info = olditem.data;
- Ext.apply(info, item.data);
- if (info.id !== oldid) {
- info.id = oldid;
- }
- me.setIconCls(info);
- olditem.commit();
- }
- if ((!item || moved) && olditem.isLeaf()) {
- delete index[key];
- let parentNode = olditem.parentNode;
- // a selected item moved (migration) or disappeared (destroyed), so deselect that
- // node now and try to reselect the moved (or its parent) node later
- if (lastsel && olditem.data.id === lastsel.data.id) {
- reselect = true;
- sm.deselect(olditem);
- }
- // store events are suspended, so remove the item manually
- store.remove(olditem);
- parentNode.removeChild(olditem, true);
- if (parentNode.childNodes.length < 1 && parentNode.parentNode) {
- let grandParent = parentNode.parentNode;
- grandParent.removeChild(parentNode, true);
- }
- }
- }
-
- let items = rstore.getData().items.flatMap(me.viewFilter.itemMap ?? Ext.identityFn);
- items.forEach(function (item) {
- // add new items
- let olditem = index[item.data.id];
- if (olditem) {
- return;
- }
- if (filterFn && !filterFn(item)) {
- return;
- }
- let info = Ext.apply({ leaf: true }, item.data);
-
- let child = me.groupChild(rootnode, info, groups, 0);
- if (child) {
- index[item.data.id] = child;
- }
- });
-
- store.resumeEvents();
- store.fireEvent('refresh', store);
-
- let foundChild = findNode(rootnode, lastsel?.data.id);
-
- // select parent node if original selected node vanished
- if (lastsel && !foundChild) {
- lastsel = rootnode;
- for (const node of parents) {
- if (rootnode.findChild('id', node.data.id, true)) {
- lastsel = node;
- break;
- }
- }
- me.selectById(lastsel.data.id);
- } else if (lastsel && reselect) {
- me.selectById(lastsel.data.id);
- }
-
- if (firstUpdate) {
- me.store.setRoot(rootnode);
- firstUpdate = false;
- }
-
- // on first tree load set the selection from the stateful provider
- if (!pdata.updateCount) {
- rootnode.expand();
- me.applyState(sp.get(stateid));
- }
-
- pdata.updateCount++;
- };
-
- sp.on('statechange', (_sp, key, value) => {
- if (key === stateid) {
- me.applyState(value);
- }
- });
-
- Ext.apply(me, {
- allowSelection: true,
- store: store,
- viewConfig: {
- animate: false, // note: animate cause problems with applyState
- },
- listeners: {
- itemcontextmenu: PVE.Utils.createCmdMenu,
- destroy: function () {
- rstore.un('load', updateTree);
- },
- beforecellmousedown: function (tree, td, cellIndex, record, tr, rowIndex, ev) {
- let sm = me.getSelectionModel();
- // disable selection when right clicking except if the record is already selected
- me.allowSelection = ev.button !== 2 || sm.isSelected(record);
- },
- beforeselect: function (tree, record, index, eopts) {
- let allow = me.allowSelection;
- me.allowSelection = true;
- return allow;
- },
- itemdblclick: PVE.Utils.openTreeConsole,
- afterrender: function () {
- if (me.tip) {
- return;
- }
- let selectors = [
- '.x-tree-node-text > span:not(.proxmox-tag-dark):not(.proxmox-tag-light)',
- '.x-tree-icon',
- ];
- me.tip = Ext.create('Ext.tip.ToolTip', {
- target: me.el,
- delegate: selectors.join(', '),
- trackMouse: true,
- renderTo: Ext.getBody(),
- listeners: {
- beforeshow: function (tip) {
- let rec = me.getView().getRecord(tip.triggerElement);
- let tipText = me.getToolTip(rec.data);
- if (tipText) {
- tip.update(tipText);
- return true;
- }
- return false;
- },
- },
- });
- },
- },
- setViewFilter: function (view) {
- me.viewFilter = view;
- me.clearTree();
- updateTree();
- },
- clearTree: function () {
- pdata.updateCount = 0;
- let rootnode = me.store.getRootNode();
- rootnode.collapse();
- rootnode.removeAll();
- pdata.dataIndex = {};
- me.getSelectionModel().deselectAll();
- },
- selectExpand: function (node) {
- let sm = me.getSelectionModel();
- if (!sm.isSelected(node)) {
- sm.select(node);
- for (let iter = node; iter; iter = iter.parentNode) {
- if (!iter.isExpanded()) {
- iter.expand();
- }
- }
- me.getView().focusRow(node);
- }
- },
- selectById: function (nodeid) {
- let rootnode = me.store.getRootNode();
- let node;
- if (nodeid === 'root') {
- node = rootnode;
- } else {
- node = findNode(rootnode, nodeid);
- }
- if (node) {
- me.selectExpand(node);
- }
- return node;
- },
- applyState: function (state) {
- if (state && state.value) {
- me.selectById(state.value);
- } else {
- me.getSelectionModel().deselectAll();
- }
- },
- });
-
- me.callParent();
-
- me.getSelectionModel().on('select', (_sm, n) => sp.set(stateid, { value: n.data.id }));
-
- rstore.on('load', updateTree);
- rstore.startUpdate();
-
- me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
- me.store.getRootNode().cascadeBy({
- before: function (node) {
- if (node.data.groupbyid) {
- node.beginEdit();
- let info = node.data;
- me.setIconCls(info);
- if (me.viewFilter.groupRenderer) {
- info.text = me.viewFilter.groupRenderer(info);
- }
- node.commit();
- }
- return true;
- },
- });
- });
- },
-});
-Ext.define('PVE.guest.SnapshotTree', {
- extend: 'Ext.tree.Panel',
- xtype: 'pveGuestSnapshotTree',
-
- stateful: true,
- stateId: 'grid-snapshots',
-
- viewModel: {
- data: {
- // should be 'qemu' or 'lxc'
- type: undefined,
- nodename: undefined,
- vmid: undefined,
- vmname: undefined,
- snapshotAllowed: false,
- rollbackAllowed: false,
- snapshotFeature: false,
- running: false,
- selected: '',
- load_delay: 3000,
- },
- formulas: {
- canSnapshot: (get) => get('snapshotAllowed') && get('snapshotFeature'),
- canRollback: (get) => get('rollbackAllowed') && get('isSnapshot'),
- canRemove: (get) => get('snapshotAllowed') && get('isSnapshot'),
- isSnapshot: (get) => get('selected') && get('selected') !== 'current',
- buttonText: (get) => (get('snapshotAllowed') ? gettext('Edit') : gettext('View')),
- showMemory: (get) => get('type') === 'qemu',
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- newSnapshot: function () {
- this.run_editor(false);
- },
-
- editSnapshot: function () {
- this.run_editor(true);
- },
-
- run_editor: function (edit) {
- let me = this;
- let vm = me.getViewModel();
- let snapname;
- if (edit) {
- snapname = vm.get('selected');
- if (!snapname || snapname === 'current') {
- return;
- }
- }
- let win = Ext.create('PVE.window.Snapshot', {
- nodename: vm.get('nodename'),
- vmid: vm.get('vmid'),
- vmname: vm.get('vmname'),
- viewonly: !vm.get('snapshotAllowed'),
- type: vm.get('type'),
- isCreate: !edit,
- submitText: !edit ? gettext('Take Snapshot') : undefined,
- snapname: snapname,
- running: vm.get('running'),
- });
- win.show();
- me.mon(win, 'destroy', me.reload, me);
- },
-
- snapshotAction: function (action, method) {
- let me = this;
- let view = me.getView();
- let vm = me.getViewModel();
- let snapname = vm.get('selected');
- if (!snapname) {
- return;
- }
-
- let nodename = vm.get('nodename');
- let type = vm.get('type');
- let vmid = vm.get('vmid');
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/${type}/${vmid}/snapshot/${snapname}/${action}`,
- method: method,
- waitMsgTarget: view,
- callback: function () {
- me.reload();
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response, options) {
- var upid = response.result.data;
- var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
- win.show();
- },
- });
- },
-
- rollback: function () {
- this.snapshotAction('rollback', 'POST');
- },
- remove: function () {
- this.snapshotAction('', 'DELETE');
- },
- cancel: function () {
- this.load_task.cancel();
- },
-
- reload: function () {
- let me = this;
- let view = me.getView();
- let vm = me.getViewModel();
- let nodename = vm.get('nodename');
- let vmid = vm.get('vmid');
- let type = vm.get('type');
- let load_delay = vm.get('load_delay');
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/${type}/${vmid}/snapshot`,
- method: 'GET',
- failure: function (response, opts) {
- if (me.destroyed) {
- return;
- }
- Proxmox.Utils.setErrorMask(view, response.htmlStatus);
- me.load_task.delay(load_delay);
- },
- success: function (response, opts) {
- if (me.destroyed) {
- // this is in a delayed task, avoid dragons if view has
- // been destroyed already and go home.
- return;
- }
- Proxmox.Utils.setErrorMask(view, false);
- var digest = 'invalid';
- var idhash = {};
- var root = { name: '__root', expanded: true, children: [] };
- Ext.Array.each(response.result.data, function (item) {
- item.leaf = true;
- item.children = [];
- if (item.name === 'current') {
- vm.set('running', !!item.running);
- digest = item.digest + item.running;
- item.iconCls = PVE.Utils.get_object_icon_class(vm.get('type'), item);
- } else {
- item.iconCls = 'fa fa-fw fa-history x-fa-tree';
- }
- idhash[item.name] = item;
- });
-
- if (digest !== me.old_digest) {
- me.old_digest = digest;
-
- Ext.Array.each(response.result.data, function (item) {
- if (item.parent && idhash[item.parent]) {
- let parent_item = idhash[item.parent];
- parent_item.children.push(item);
- parent_item.leaf = false;
- parent_item.expanded = true;
- parent_item.expandable = false;
- } else {
- root.children.push(item);
- }
- });
-
- me.getView().setRootNode(root);
- }
-
- me.load_task.delay(load_delay);
- },
- });
-
- // if we do not have the permissions, we don't have to check
- // if we can create a snapshot, since the butten stays disabled
- if (!vm.get('snapshotAllowed')) {
- return;
- }
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/${type}/${vmid}/feature`,
- params: { feature: 'snapshot' },
- method: 'GET',
- success: function (response, options) {
- if (me.destroyed) {
- // this is in a delayed task, the current view could been
- // destroyed already; then we mustn't do viemodel set
- return;
- }
- let res = response.result.data;
- vm.set('snapshotFeature', !!res.hasFeature);
- },
- });
- },
-
- select: function (grid, val) {
- let vm = this.getViewModel();
- if (val.length < 1) {
- vm.set('selected', '');
- return;
- }
- vm.set('selected', val[0].data.name);
- },
-
- init: function (view) {
- let me = this;
- let vm = me.getViewModel();
- me.load_task = new Ext.util.DelayedTask(me.reload, me);
-
- if (!view.type) {
- throw 'guest type not set';
- }
- vm.set('type', view.type);
-
- if (!view.pveSelNode.data.node) {
- throw 'no node name specified';
- }
- vm.set('nodename', view.pveSelNode.data.node);
-
- if (!view.pveSelNode.data.vmid) {
- throw 'no VM ID specified';
- }
- vm.set('vmid', view.pveSelNode.data.vmid);
-
- vm.set('vmname', view.pveSelNode.data.name);
-
- let caps = Ext.state.Manager.get('GuiCap');
- vm.set('snapshotAllowed', !!caps.vms['VM.Snapshot']);
- vm.set('rollbackAllowed', !!caps.vms['VM.Snapshot.Rollback']);
-
- view.getStore().sorters.add({
- property: 'order',
- direction: 'ASC',
- });
-
- me.reload();
- },
- },
-
- listeners: {
- selectionchange: 'select',
- itemdblclick: 'editSnapshot',
- beforedestroy: 'cancel',
- },
-
- layout: 'fit',
- rootVisible: false,
- animate: false,
- sortableColumns: false,
-
- tbar: [
- {
- xtype: 'proxmoxButton',
- text: gettext('Take Snapshot'),
- disabled: true,
- bind: {
- disabled: '{!canSnapshot}',
- },
- handler: 'newSnapshot',
- },
- '-',
- {
- xtype: 'proxmoxButton',
- text: gettext('Rollback'),
- disabled: true,
- bind: {
- disabled: '{!canRollback}',
- },
- confirmMsg: function () {
- let view = this.up('treepanel');
- let rec = view.getSelection()[0];
- let vmid = view.getViewModel().get('vmid');
- let vmname = view.getViewModel().get('vmname');
- let message =
- PVE.Utils.formatGuestTaskConfirmation('qmrollback', vmid, vmname) +
- ` '${rec.data.name}'? ${gettext('Current state will be lost.')}`;
- return Ext.htmlEncode(message);
- },
- handler: 'rollback',
- },
- '-',
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- bind: {
- text: '{buttonText}',
- disabled: '{!isSnapshot}',
- },
- disabled: true,
- edit: true,
- handler: 'editSnapshot',
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Remove'),
- disabled: true,
- dangerous: true,
- bind: {
- disabled: '{!canRemove}',
- },
- confirmMsg: function () {
- let view = this.up('treepanel');
- let { data } = view.getSelection()[0];
- return Ext.String.format(
- gettext('Are you sure you want to remove entry {0}'),
- `'${data.name}'`,
- );
- },
- handler: 'remove',
- },
- {
- xtype: 'label',
- text: gettext('The current guest configuration does not support taking new snapshots'),
- hidden: true,
- bind: {
- hidden: '{canSnapshot}',
- },
- },
- ],
-
- columnLines: true,
-
- fields: [
- 'name',
- 'description',
- 'snapstate',
- 'vmstate',
- 'running',
- { name: 'snaptime', type: 'date', dateFormat: 'timestamp' },
- {
- name: 'order',
- calculate: function (data) {
- return data.snaptime || (data.name === 'current' ? 'ZZZ' : data.snapstate);
- },
- },
- ],
-
- columns: [
- {
- xtype: 'treecolumn',
- text: gettext('Name'),
- dataIndex: 'name',
- width: 200,
- renderer: (value, _, { data }) => (data.name !== 'current' ? value : gettext('NOW')),
- },
- {
- text: gettext('RAM'),
- hidden: true,
- bind: {
- hidden: '{!showMemory}',
- },
- align: 'center',
- resizable: false,
- dataIndex: 'vmstate',
- width: 50,
- renderer: (value, _, { data }) =>
- data.name !== 'current' ? Proxmox.Utils.format_boolean(value) : '',
- },
- {
- text: gettext('Date') + '/' + gettext('Status'),
- dataIndex: 'snaptime',
- width: 150,
- renderer: function (value, metaData, record) {
- if (record.data.snapstate) {
- return record.data.snapstate;
- } else if (value) {
- return Ext.Date.format(value, 'Y-m-d H:i:s');
- }
- return '';
- },
- },
- {
- text: gettext('Description'),
- dataIndex: 'description',
- flex: 1,
- renderer: function (value, metaData, record) {
- if (record.data.name === 'current') {
- return gettext('You are here!');
- } else {
- return Ext.String.htmlEncode(value);
- }
- },
- },
- ],
-});
-Ext.define('PVE.tree.ResourceMapTree', {
- extend: 'Ext.tree.Panel',
- alias: 'widget.pveResourceMapTree',
- mixins: ['Proxmox.Mixin.CBind'],
-
- rootVisible: false,
-
- emptyText: gettext('No Mapping found'),
-
- // will be opened on edit
- editWindowClass: undefined,
-
- // The base url of the resource
- baseUrl: undefined,
-
- // icon class to show on the entries
- mapIconCls: undefined,
-
- // if given, should be a function that takes a nodename and returns
- // the url for getting the data to check the status
- getStatusCheckUrl: undefined,
-
- // the result of above api call and the nodename is passed and can set the status
- checkValidity: undefined,
-
- // the property that denotes a single map entry for a node
- entryIdProperty: undefined,
-
- cbindData: function (initialConfig) {
- let me = this;
- const caps = Ext.state.Manager.get('GuiCap');
- me.canConfigure = !!caps.mapping['Mapping.Modify'];
-
- return {};
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- addMapping: function () {
- let me = this;
- let view = me.getView();
- Ext.create(view.editWindowClass, {
- url: view.baseUrl,
- autoShow: true,
- listeners: {
- destroy: () => me.load(),
- },
- });
- },
-
- add: function (_grid, _rI, _cI, _item, _e, rec) {
- let me = this;
- if (rec.data.type !== 'entry') {
- return;
- }
-
- me.openMapEditWindow(rec.data.name);
- },
-
- editDblClick: function () {
- let me = this;
- let view = me.getView();
- let selection = view.getSelection();
- if (!selection || selection.length < 1) {
- return;
- }
-
- me.edit(selection[0]);
- },
-
- editAction: function (_grid, _rI, _cI, _item, _e, rec) {
- this.edit(rec);
- },
-
- edit: function (rec) {
- let me = this;
- if (rec.data.type === 'map') {
- return;
- }
-
- me.openMapEditWindow(rec.data.name, rec.data.node, rec.data.type === 'entry');
- },
-
- openMapEditWindow: function (name, nodename, entryOnly) {
- let me = this;
- let view = me.getView();
-
- Ext.create(view.editWindowClass, {
- url: `${view.baseUrl}/${name}`,
- autoShow: true,
- autoLoad: true,
- entryOnly,
- nodename,
- name,
- listeners: {
- destroy: () => me.load(),
- },
- });
- },
-
- remove: function (_grid, _rI, _cI, _item, _e, rec) {
- let me = this;
- let msg, id;
- let view = me.getView();
- let confirmMsg;
- switch (rec.data.type) {
- case 'entry':
- msg = gettext("Are you sure you want to remove '{0}'");
- confirmMsg = Ext.String.format(msg, rec.data.name);
- break;
- case 'node':
- msg = gettext("Are you sure you want to remove '{0}' entries for '{1}'");
- confirmMsg = Ext.String.format(msg, rec.data.node, rec.data.name);
- break;
- case 'map':
- msg = gettext("Are you sure you want to remove '{0}' on '{1}' for '{2}'");
- id = rec.data[view.entryIdProperty];
- confirmMsg = Ext.String.format(msg, id, rec.data.node, rec.data.name);
- break;
- default:
- throw 'invalid type';
- }
- Ext.Msg.confirm(gettext('Confirm'), confirmMsg, function (btn) {
- if (btn === 'yes') {
- me.executeRemove(rec.data);
- }
- });
- },
-
- executeRemove: function (data) {
- let me = this;
- let view = me.getView();
-
- let url = `${view.baseUrl}/${data.name}`;
- let method = 'PUT';
- let params = {
- digest: me.lookup[data.name].digest,
- };
- let map = me.lookup[data.name].map;
- switch (data.type) {
- case 'entry':
- method = 'DELETE';
- params = undefined;
- break;
- case 'node':
- params.map = PVE.Parser.filterPropertyStringList(
- map,
- (e) => e.node !== data.node,
- );
- break;
- case 'map':
- params.map = PVE.Parser.filterPropertyStringList(map, (e) =>
- Object.entries(e).some(([key, value]) => data[key] !== value),
- );
- break;
- default:
- throw 'invalid type';
- }
- if (!params?.map.length) {
- method = 'DELETE';
- params = undefined;
- }
- Proxmox.Utils.API2Request({
- url,
- method,
- params,
- success: function () {
- me.load();
- },
- });
- },
-
- load: function () {
- let me = this;
- let view = me.getView();
- Proxmox.Utils.API2Request({
- url: view.baseUrl,
- method: 'GET',
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: function ({ result: { data } }) {
- let lookup = {};
- data.forEach((entry) => {
- lookup[entry.id] = Ext.apply({}, entry);
- entry.iconCls = 'fa fa-fw fa-folder-o';
- entry.name = entry.id;
- entry.text = entry.id;
- entry.type = 'entry';
-
- let nodes = {};
- for (const map of entry.map) {
- let parsed = PVE.Parser.parsePropertyString(map);
- parsed.iconCls = view.mapIconCls;
- parsed.leaf = true;
- parsed.name = entry.id;
- parsed.text = parsed[view.entryIdProperty];
- parsed.type = 'map';
-
- if (nodes[parsed.node] === undefined) {
- nodes[parsed.node] = {
- children: [],
- expanded: true,
- iconCls: 'fa fa-fw fa-building-o',
- leaf: false,
- name: entry.id,
- node: parsed.node,
- text: parsed.node,
- type: 'node',
- };
- }
- nodes[parsed.node].children.push(parsed);
- }
- delete entry.id;
- entry.children = Object.values(nodes);
- entry.leaf = entry.children.length === 0;
- });
- me.lookup = lookup;
- if (view.getStatusCheckUrl !== undefined && view.checkValidity !== undefined) {
- me.loadStatusData();
- }
- view.setRootNode({
- children: data,
- });
- let root = view.getRootNode();
- root.expand();
- root.childNodes.forEach((node) => node.expand());
- },
- });
- },
-
- nodeLoadingState: {},
-
- loadStatusData: function () {
- let me = this;
- let view = me.getView();
- PVE.data.ResourceStore.getNodes().forEach(({ node }) => {
- me.nodeLoadingState[node] = true;
- let url = view.getStatusCheckUrl(node);
- Proxmox.Utils.API2Request({
- url,
- method: 'GET',
- failure: function (response) {
- me.nodeLoadingState[node] = false;
- view.getRootNode()?.cascade(function (rec) {
- if (rec.data.node !== node) {
- return;
- }
-
- rec.set('valid', 0);
- rec.set('errmsg', response.htmlStatus);
- rec.commit();
- });
- },
- success: function ({ result: { data } }) {
- me.nodeLoadingState[node] = false;
- view.checkValidity(data, node);
- },
- });
- });
- },
-
- renderStatus: function (value, _metadata, record) {
- let me = this;
- if (record.data.type !== 'map') {
- return '';
- }
- let iconCls;
- let status;
- if (value === undefined) {
- if (me.nodeLoadingState[record.data.node]) {
- iconCls = 'fa-spinner fa-spin';
- status = gettext('Loading...');
- } else {
- iconCls = 'fa-question-circle';
- status = gettext('Unknown Node');
- }
- } else {
- let state = value ? 'good' : 'critical';
- iconCls = PVE.Utils.get_health_icon(state, true);
- status = value
- ? gettext('Mapping matches host data')
- : record.data.errmsg || Proxmox.Utils.unknownText;
- }
- return ` ${status}`;
- },
-
- getAddClass: function (v, mD, rec) {
- let cls = 'fa fa-plus-circle';
- if (
- rec.data.type !== 'entry' ||
- rec.data.children?.length >= PVE.data.ResourceStore.getNodes().length
- ) {
- cls += ' pmx-action-hidden';
- }
- return cls;
- },
-
- isAddDisabled: function (v, r, c, i, rec) {
- return (
- rec.data.type !== 'entry' ||
- rec.data.children?.length >= PVE.data.ResourceStore.getNodes().length
- );
- },
-
- init: function (view) {
- let me = this;
-
- ['editWindowClass', 'baseUrl', 'mapIconCls', 'entryIdProperty'].forEach((property) => {
- if (view[property] === undefined) {
- throw `No ${property} defined`;
- }
- });
-
- me.load();
- },
- },
-
- store: {
- sorters: 'text',
- data: {},
- },
-
- tbar: [
- {
- text: gettext('Add'),
- handler: 'addMapping',
- cbind: {
- disabled: '{!canConfigure}',
- },
- },
- ],
-
- listeners: {
- itemdblclick: 'editDblClick',
- },
-
- initComponent: function () {
- let me = this;
-
- let columns = [...me.columns];
- columns.splice(1, 0, {
- xtype: 'actioncolumn',
- text: gettext('Actions'),
- width: 80,
- items: [
- {
- getTip: (v, m, { data }) =>
- Ext.String.format(gettext("Add new host mapping for '{0}'"), data.name),
- getClass: 'getAddClass',
- isActionDisabled: 'isAddDisabled',
- handler: 'add',
- },
- {
- iconCls: 'fa fa-pencil',
- getTip: (v, m, { data }) =>
- data.type === 'entry'
- ? Ext.String.format(gettext("Edit Mapping '{0}'"), data.name)
- : Ext.String.format(
- gettext("Edit Mapping '{0}' for '{1}'"),
- data.name,
- data.node,
- ),
- getClass: (v, m, { data }) =>
- data.type !== 'map' ? 'fa fa-pencil' : 'pmx-hidden',
- isActionDisabled: (v, r, c, i, rec) => rec.data.type === 'map',
- handler: 'editAction',
- },
- {
- iconCls: 'fa fa-trash-o',
- getTip: (v, m, { data }) =>
- data.type === 'entry'
- ? Ext.String.format(gettext("Remove '{0}'"), data.name)
- : data.type === 'node'
- ? Ext.String.format(gettext("Remove mapping for '{0}'"), data.node)
- : Ext.String.format(gettext("Remove mapping '{0}'"), data.path),
- handler: 'remove',
- },
- ],
- });
- me.columns = columns;
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.DhcpTree', {
- extend: 'Ext.tree.Panel',
- xtype: 'pveDhcpTree',
-
- layout: 'fit',
- rootVisible: false,
- animate: false,
-
- store: {
- sorters: ['ip', 'name'],
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- reload: function () {
- let me = this;
-
- Proxmox.Utils.API2Request({
- url: `/cluster/sdn/ipams/pve/status`,
- method: 'GET',
- success: function (response, opts) {
- let root = {
- name: '__root',
- expanded: true,
- children: [],
- };
-
- let zones = {};
- let vnets = {};
- let subnets = {};
-
- response.result.data.forEach((element) => {
- element.leaf = true;
-
- if (!(element.zone in zones)) {
- let zone = {
- name: element.zone,
- type: 'zone',
- iconCls: 'fa fa-th',
- expanded: true,
- children: [],
- };
-
- zones[element.zone] = zone;
- root.children.push(zone);
- }
-
- if (!(element.vnet in vnets)) {
- let vnet = {
- name: element.vnet,
- zone: element.zone,
- type: 'vnet',
- iconCls: 'fa fa-network-wired x-fa-treepanel',
- expanded: true,
- children: [],
- };
-
- vnets[element.vnet] = vnet;
- zones[element.zone].children.push(vnet);
- }
-
- if (!(element.subnet in subnets)) {
- let subnet = {
- name: element.subnet,
- zone: element.zone,
- vnet: element.vnet,
- type: 'subnet',
- iconCls: 'x-tree-icon-none',
- expanded: true,
- children: [],
- };
-
- subnets[element.subnet] = subnet;
- vnets[element.vnet].children.push(subnet);
- }
-
- element.name = element.vmid; // for sorting
- element.type = 'mapping';
- element.iconCls = 'x-tree-icon-none';
- subnets[element.subnet].children.push(element);
- });
-
- me.getView().setRootNode(root);
- },
- });
- },
-
- init: function (view) {
- let me = this;
- me.reload();
- },
-
- onDelete: function (table, rI, cI, item, e, { data }) {
- let me = this;
- let view = me.getView();
-
- Ext.Msg.show({
- title: gettext('Confirm'),
- icon: Ext.Msg.WARNING,
- message: Ext.String.format(
- gettext('Are you sure you want to remove DHCP mapping {0}'),
- `${data.mac} / ${data.ip}`,
- ),
- buttons: Ext.Msg.YESNO,
- defaultFocus: 'no',
- callback: function (btn) {
- if (btn !== 'yes') {
- return;
- }
-
- let params = {
- zone: data.zone,
- mac: data.mac,
- ip: data.ip,
- };
-
- let encodedParams = Ext.Object.toQueryString(params);
-
- let url = `/cluster/sdn/vnets/${data.vnet}/ips?${encodedParams}`;
-
- Proxmox.Utils.API2Request({
- url,
- method: 'DELETE',
- waitMsgTarget: view,
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- callback: me.reload.bind(me),
- });
- },
- });
- },
-
- editAction: function (_grid, _rI, _cI, _item, _e, rec) {
- this.edit(rec);
- },
-
- editDblClick: function () {
- let me = this;
-
- let view = me.getView();
- let selection = view.getSelection();
-
- if (!selection || selection.length < 1) {
- return;
- }
-
- me.edit(selection[0]);
- },
-
- edit: function (rec) {
- let me = this;
-
- if (rec.data.type === 'mapping' && !rec.data.gateway) {
- me.openEditWindow(rec.data);
- }
- },
-
- openEditWindow: function (data) {
- let me = this;
-
- let extraRequestParams = {
- mac: data.mac,
- zone: data.zone,
- vnet: data.vnet,
- };
-
- if (data.vmid) {
- extraRequestParams.vmid = data.vmid;
- }
-
- Ext.create('PVE.sdn.IpamEdit', {
- autoShow: true,
- mapping: data,
- extraRequestParams,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
- },
-
- listeners: {
- itemdblclick: 'editDblClick',
- },
-
- tbar: [
- {
- xtype: 'proxmoxButton',
- text: gettext('Reload'),
- handler: 'reload',
- },
- ],
-
- columns: [
- {
- xtype: 'treecolumn',
- text: gettext('Name / VMID'),
- dataIndex: 'name',
- width: 200,
- renderer: function (value, meta, record) {
- if (record.get('gateway')) {
- return gettext('Gateway');
- }
-
- return record.get('name') ?? record.get('vmid') ?? ' ';
- },
- },
- {
- text: gettext('IP Address'),
- dataIndex: 'ip',
- width: 200,
- },
- {
- text: 'MAC',
- dataIndex: 'mac',
- width: 200,
- },
- {
- text: gettext('Gateway'),
- dataIndex: 'gateway',
- width: 200,
- },
- {
- header: gettext('Actions'),
- xtype: 'actioncolumn',
- dataIndex: 'text',
- width: 150,
- items: [
- {
- handler: function (table, rI, cI, item, e, { data }) {
- let me = this;
-
- Ext.create('PVE.sdn.IpamEdit', {
- autoShow: true,
- mapping: {},
- isCreate: true,
- extraRequestParams: {
- vnet: data.name,
- zone: data.zone,
- },
- listeners: {
- destroy: () => {
- me.up('pveDhcpTree').controller.reload();
- },
- },
- });
- },
- getTip: (v, m, rec) => gettext('Add'),
- getClass: (v, m, { data }) => {
- if (data.type === 'vnet') {
- return 'fa fa-plus-square';
- }
-
- return 'pmx-hidden';
- },
- },
- {
- handler: 'editAction',
- getTip: (v, m, rec) => gettext('Edit'),
- getClass: (v, m, { data }) => {
- if (data.type === 'mapping' && !data.gateway) {
- return 'fa fa-pencil fa-fw';
- }
-
- return 'pmx-hidden';
- },
- },
- {
- handler: 'onDelete',
- getTip: (v, m, rec) => gettext('Delete'),
- getClass: (v, m, { data }) => {
- if (data.type === 'mapping' && !data.gateway) {
- return 'fa critical fa-trash-o';
- }
-
- return 'pmx-hidden';
- },
- },
- ],
- },
- ],
-});
-Ext.define('PVE.window.Backup', {
- extend: 'Ext.window.Window',
-
- resizable: false,
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.vmid) {
- throw 'no VM ID specified';
- }
-
- if (!me.vmtype) {
- throw 'no VM type specified';
- }
-
- let compressionSelector = Ext.create('PVE.form.BackupCompressionSelector', {
- name: 'compress',
- value: 'zstd',
- fieldLabel: gettext('Compression'),
- });
-
- let modeSelector = Ext.create('PVE.form.BackupModeSelector', {
- fieldLabel: gettext('Mode'),
- value: 'snapshot',
- name: 'mode',
- });
-
- let mailtoField = Ext.create('Ext.form.field.Text', {
- fieldLabel: gettext('Send email to'),
- name: 'mailto',
- hidden: true,
- emptyText: Proxmox.Utils.noneText,
- });
-
- let notificationModeSelector = Ext.create({
- xtype: 'proxmoxKVComboBox',
- comboItems: [
- ['notification-system', gettext('Use global settings')],
- // TRANSLATORS: sendmail is a piece of software
- ['legacy-sendmail', gettext('Use sendmail (legacy)')],
- ],
- fieldLabel: gettext('Notification'),
- name: 'notification-mode',
- value: 'notification-system',
- listeners: {
- change: function (field, value) {
- mailtoField.setHidden(value === 'notification-system');
- },
- },
- });
-
- let pbsChangeDetectionModeSelector = Ext.create({
- xtype: 'proxmoxKVComboBox',
- flex: 1,
- disabled: true,
- name: 'pbs-change-detection-mode',
- deleteEmpty: true,
- value: '__default__',
- comboItems: [
- ['__default__', 'Default'],
- ['data', 'Data'],
- ['metadata', 'Metadata'],
- ],
- });
-
- let pbsChangeDetection = Ext.create('Ext.form.FieldContainer', {
- fieldLabel: gettext('PBS change detection mode'),
- hidden: true,
- layout: {
- type: 'hbox',
- align: 'center',
- },
- items: [
- pbsChangeDetectionModeSelector,
- {
- xtype: 'box',
- html: ` `,
- padding: 5,
- },
- ],
- });
-
- const keepNames = [
- ['keep-last', gettext('Keep Last')],
- ['keep-hourly', gettext('Keep Hourly')],
- ['keep-daily', gettext('Keep Daily')],
- ['keep-weekly', gettext('Keep Weekly')],
- ['keep-monthly', gettext('Keep Monthly')],
- ['keep-yearly', gettext('Keep Yearly')],
- ];
-
- let pruneSettings = keepNames.map((name) =>
- Ext.create('Ext.form.field.Display', {
- name: name[0],
- fieldLabel: name[1],
- hidden: true,
- }),
- );
-
- let removeCheckbox = Ext.create('Proxmox.form.Checkbox', {
- name: 'remove',
- checked: false,
- hidden: true,
- uncheckedValue: 0,
- fieldLabel: gettext('Prune'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Prune older backups afterwards'),
- },
- handler: function (checkbox, value) {
- pruneSettings.forEach((field) => field.setHidden(!value));
- me.down('label[name="pruneLabel"]').setHidden(!value);
- },
- });
-
- let initialDefaults = false;
-
- var storagesel = Ext.create('PVE.form.StorageSelector', {
- nodename: me.nodename,
- name: 'storage',
- fieldLabel: gettext('Storage'),
- storageContent: 'backup',
- allowBlank: false,
- listeners: {
- change: function (f, v) {
- if (!initialDefaults) {
- me.setLoading(false);
- }
-
- if (v === null || v === undefined || v === '') {
- return;
- }
-
- let store = f.getStore();
- let rec = store.findRecord('storage', v, 0, false, true, true);
-
- if (rec && rec.data && rec.data.type === 'pbs') {
- compressionSelector.setValue('zstd');
- compressionSelector.setDisabled(true);
- if (me.vmtype === 'lxc') {
- pbsChangeDetectionModeSelector.setValue('__default__');
- pbsChangeDetectionModeSelector.setDisabled(false);
- pbsChangeDetection.setHidden(false);
- } else {
- pbsChangeDetectionModeSelector.setDisabled(true);
- pbsChangeDetection.setHidden(true);
- }
- } else {
- if (!compressionSelector.getEditable()) {
- compressionSelector.setDisabled(false);
- }
- pbsChangeDetectionModeSelector.setDisabled(true);
- pbsChangeDetection.setHidden(true);
- }
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${me.nodename}/vzdump/defaults`,
- method: 'GET',
- params: {
- storage: v,
- },
- waitMsgTarget: me,
- success: function (response, opts) {
- const data = response.result.data;
-
- if (!initialDefaults) {
- let notificationMode = data['notification-mode'] ?? 'auto';
- let mailto = data.mailto;
-
- if (notificationMode === 'auto' && mailto !== undefined) {
- notificationMode = 'legacy-sendmail';
- }
- if (notificationMode === 'auto' && mailto === undefined) {
- notificationMode = 'notification-system';
- }
-
- notificationModeSelector.setValue(notificationMode);
- if (mailto !== undefined) {
- mailtoField.setValue(mailto);
- }
- }
- if (!initialDefaults && data.mode !== undefined) {
- modeSelector.setValue(data.mode);
- }
- if (!initialDefaults && (data['notes-template'] ?? false)) {
- me.down('field[name=notes-template]').setValue(
- PVE.Utils.unEscapeNotesTemplate(data['notes-template']),
- );
- }
-
- initialDefaults = true;
-
- // always update storage dependent properties
- if (data['prune-backups'] !== undefined) {
- const keepParams = PVE.Parser.parsePropertyString(
- data['prune-backups'],
- );
- if (!keepParams['keep-all']) {
- removeCheckbox.setHidden(false);
- pruneSettings.forEach(function (field) {
- const keep = keepParams[field.name];
- if (keep) {
- field.setValue(keep);
- } else {
- field.reset();
- }
- });
- return;
- }
- }
-
- // no defaults or keep-all=1
- removeCheckbox.setHidden(true);
- removeCheckbox.setValue(false);
- pruneSettings.forEach((field) => field.reset());
- },
- failure: function (response, opts) {
- initialDefaults = true;
-
- removeCheckbox.setHidden(true);
- removeCheckbox.setValue(false);
- pruneSettings.forEach((field) => field.reset());
-
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
- },
- });
-
- let protectedCheckbox = Ext.create('Proxmox.form.Checkbox', {
- name: 'protected',
- checked: false,
- uncheckedValue: 0,
- fieldLabel: gettext('Protected'),
- // Tiny amount of padding to stop the UI from shifting
- // when the 'mailto' field is shown.
- padding: '0 0 1 0',
- });
-
- me.formPanel = Ext.create('Proxmox.panel.InputPanel', {
- bodyPadding: 10,
- border: false,
- column1: [storagesel, modeSelector, protectedCheckbox, pbsChangeDetection],
- column2: [compressionSelector, notificationModeSelector, mailtoField, removeCheckbox],
- columnB: [
- {
- xtype: 'textareafield',
- name: 'notes-template',
- fieldLabel: gettext('Notes'),
- anchor: '100%',
- value: '{{guestname}}',
- },
- {
- xtype: 'box',
- style: {
- margin: '8px 0px',
- 'line-height': '1.5em',
- },
- html: Ext.String.format(
- gettext('Possible template variables are: {0}'),
- PVE.Utils.notesTemplateVars.map((v) => `{{${v}}}`).join(', '),
- ),
- },
- {
- xtype: 'label',
- name: 'pruneLabel',
- text: gettext('Storage Retention Configuration') + ':',
- hidden: true,
- },
- {
- layout: 'hbox',
- border: false,
- defaults: {
- border: false,
- layout: 'anchor',
- flex: 1,
- },
- items: [
- {
- padding: '0 10 0 0',
- defaults: {
- labelWidth: 110,
- },
- items: [pruneSettings[0], pruneSettings[2], pruneSettings[4]],
- },
- {
- padding: '0 0 0 10',
- defaults: {
- labelWidth: 110,
- },
- items: [pruneSettings[1], pruneSettings[3], pruneSettings[5]],
- },
- ],
- },
- ],
- });
-
- var submitBtn = Ext.create('Ext.Button', {
- text: gettext('Backup'),
- handler: function () {
- var storage = storagesel.getValue();
- let values = me.formPanel.getValues();
- var params = {
- storage: storage,
- vmid: me.vmid,
- mode: values.mode,
- remove: values.remove,
- };
-
- if (values.mailto) {
- params.mailto = values.mailto;
- }
-
- if (values['notification-mode']) {
- params['notification-mode'] = values['notification-mode'];
- }
-
- if (values.compress) {
- params.compress = values.compress;
- }
-
- if (values.protected) {
- params.protected = values.protected;
- }
-
- if (values['pbs-change-detection-mode']) {
- params['pbs-change-detection-mode'] = values['pbs-change-detection-mode'];
- }
-
- if (values['notes-template']) {
- params['notes-template'] = PVE.Utils.escapeNotesTemplate(
- values['notes-template'],
- );
- }
-
- Proxmox.Utils.API2Request({
- url: '/nodes/' + me.nodename + '/vzdump',
- params: params,
- method: 'POST',
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- success: function (response, options) {
- // close later so we reload the grid
- // after the task has completed
- me.hide();
-
- var upid = response.result.data;
-
- var win = Ext.create('Proxmox.window.TaskViewer', {
- upid: upid,
- listeners: {
- close: function () {
- me.close();
- },
- },
- });
- win.show();
- },
- });
- },
- });
-
- var helpBtn = Ext.create('Proxmox.button.Help', {
- onlineHelp: 'chapter_vzdump',
- listenToGlobalEvent: false,
- hidden: false,
- });
-
- let guestTypeStr = me.vmtype === 'lxc' ? 'CT' : 'VM';
- let formattedGuestIdentifier = PVE.Utils.getFormattedGuestIdentifier(me.vmid, me.vmname);
- let title = `${gettext('Backup')} ${guestTypeStr} ${formattedGuestIdentifier}`;
-
- Ext.apply(me, {
- title: title,
- modal: true,
- layout: 'auto',
- border: false,
- width: 600,
- items: [me.formPanel],
- buttons: [helpBtn, '->', submitBtn],
- listeners: {
- afterrender: function () {
- /// cleared within the storage selector's change listener
- me.setLoading(gettext('Please wait...'));
- storagesel.setValue(me.storage);
- },
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.window.BackupConfig', {
- extend: 'Ext.window.Window',
- title: gettext('Configuration'),
- width: 600,
- height: 400,
- layout: 'fit',
- modal: true,
- items: {
- xtype: 'component',
- itemId: 'configtext',
- autoScroll: true,
- style: {
- 'white-space': 'pre',
- 'font-family': 'monospace',
- padding: '5px',
- },
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.volume) {
- throw 'no volume specified';
- }
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- me.callParent();
-
- Proxmox.Utils.API2Request({
- url: '/nodes/' + nodename + '/vzdump/extractconfig',
- method: 'GET',
- params: {
- volume: me.volume,
- },
- failure: function (response, opts) {
- me.close();
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- success: function (response, options) {
- me.show();
- me.down('#configtext').update(Ext.htmlEncode(response.result.data));
- },
- });
- },
-});
-Ext.define('PVE.window.BulkAction', {
- extend: 'Ext.window.Window',
-
- resizable: true,
- width: 800,
- height: 600,
- modal: true,
- layout: {
- type: 'fit',
- },
- border: false,
-
- // the action to set, currently there are: `startall`, `migrateall`, `stopall`, `suspendall`
- action: undefined,
-
- // if set to true, the 'vms' parameter will be sent as an array'
- // necessary for the cluster-wide api call
- vmsAsArray: false,
-
- submit: function (params) {
- let me = this;
-
- let url;
- if (me.nodename) {
- url = `/nodes/${me.nodename}/${me.action}`;
- } else {
- url = `/cluster/bulk-action/guest/${me.action}`;
- }
-
- if (me.vmsAsArray) {
- params.vms = params.vms.split(/[,; ]/);
- }
-
- Proxmox.Utils.API2Request({
- params: params,
- url,
- waitMsgTarget: me,
- method: 'POST',
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- success: function ({ result }, options) {
- Ext.create('Proxmox.window.TaskViewer', {
- autoShow: true,
- upid: result.data,
- listeners: {
- destroy: () => me.close(),
- },
- });
- me.hide();
- },
- });
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.action) {
- throw 'no action specified';
- }
- if (!me.btnText) {
- throw 'no button text specified';
- }
- if (!me.title) {
- throw 'no title specified';
- }
-
- let items = [];
- if (me.action === 'migrateall' || me.action === 'migrate') {
- let disallowedNodes = [];
- if (me.nodename) {
- disallowedNodes.push(me.nodename);
- }
- items.push(
- {
- xtype: 'fieldcontainer',
- layout: 'hbox',
- items: [
- {
- flex: 1,
- xtype: 'pveNodeSelector',
- name: 'target',
- disallowedNodes,
- fieldLabel: gettext('Target node'),
- labelWidth: 200,
- allowBlank: false,
- onlineValidator: true,
- padding: '0 10 0 0',
- },
- {
- xtype: 'proxmoxintegerfield',
- // TODO: change to newer max-worker spelling for PVE 10
- name: 'maxworkers',
- minValue: 1,
- maxValue: 64,
- value: 1,
- fieldLabel: gettext('Parallel jobs'),
- allowBlank: false,
- flex: 1,
- },
- ],
- },
- {
- xtype: 'fieldcontainer',
- layout: 'hbox',
- items: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Allow local disk migration'),
- name: 'with-local-disks',
- labelWidth: 200,
- checked: true,
- uncheckedValue: 0,
- flex: 1,
- padding: '0 10 0 0',
- },
- {
- itemId: 'lxcwarning',
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: 'Warning: Running CTs will be migrated in Restart Mode.',
- hidden: true, // only visible if running container chosen
- flex: 1,
- },
- ],
- },
- );
- if (me.action === 'migrate') {
- items.push({
- xtype: 'hiddenfield',
- name: 'online',
- value: 1,
- });
- }
- } else if (me.action === 'startall') {
- items.push({
- xtype: 'hiddenfield',
- name: 'force',
- value: 1,
- });
- } else if (me.action === 'stopall' || me.action === 'shutdown') {
- items.push({
- xtype: 'fieldcontainer',
- layout: 'hbox',
- items: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'force-stop',
- labelWidth: 120,
- fieldLabel: gettext('Force Stop'),
- boxLabel: gettext('Force stop guest if shutdown times out.'),
- checked: true,
- uncheckedValue: 0,
- flex: 7,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'timeout',
- fieldLabel: gettext('Timeout (s)'),
- labelWidth: 120,
- emptyText: '180',
- minValue: 0,
- maxValue: 7200,
- allowBlank: true,
- flex: 3,
- },
- ],
- });
- }
- if (me.action !== 'migrateall' && me.action !== 'migrate') {
- items.push({
- xtype: 'fieldcontainer',
- layout: 'hbox',
- items: [
- {
- xtype: 'box',
- flex: 7,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'max-workers',
- minValue: 1,
- maxValue: 64,
- emptyText: 'auto',
- fieldLabel: gettext('Parallel jobs'),
- labelWidth: 120,
- allowBlank: true,
- flex: 3,
- },
- ],
- });
- }
-
- let refreshLxcWarning = function (vmids, records) {
- let showWarning = records.some(
- (item) =>
- vmids.includes(item.data.vmid) &&
- item.data.type === 'lxc' &&
- item.data.status === 'running',
- );
- me.down('#lxcwarning').setVisible(showWarning);
- };
-
- let defaulStatusMap = {
- migrateall: '',
- migrate: '',
- startall: 'stopped',
- start: 'stopped',
- stopall: 'running',
- shutdown: 'running',
- suspendall: 'running',
- suspend: 'running',
- };
- let defaultStatus = defaulStatusMap[me.action] ?? '';
- let defaultType = me.action === 'suspendall' ? 'qemu' : '';
-
- let statusMap = [];
- let poolMap = [];
- let haMap = [];
- let tagMap = [];
- PVE.data.ResourceStore.each((rec) => {
- if (['qemu', 'lxc'].indexOf(rec.data.type) !== -1) {
- statusMap[rec.data.status] = true;
- }
- if (rec.data.type === 'pool') {
- poolMap[rec.data.pool] = true;
- }
- if (rec.data.hastate !== '') {
- haMap[rec.data.hastate] = true;
- }
- if (rec.data.tags !== '') {
- rec.data.tags.split(/[,; ]/).forEach((tag) => {
- if (tag !== '') {
- tagMap[tag] = true;
- }
- });
- }
- });
-
- let statusList = Object.keys(statusMap).map((key) => [key, key]);
- statusList.unshift(['', gettext('All')]);
- let poolList = Object.keys(poolMap).map((key) => [key, key]);
- let tagList = Object.keys(tagMap).map((key) => ({ value: key }));
- let haList = Object.keys(haMap).map((key) => [key, key]);
-
- let clearFilters = function () {
- me.down('#namefilter').setValue('');
- ['name', 'status', 'pool', 'type', 'hastate', 'includetag', 'excludetag'].forEach(
- (filter) => {
- me.down(`#${filter}filter`).setValue('');
- },
- );
- };
-
- let filterChange = function () {
- let nameValue = me.down('#namefilter').getValue();
- let filterCount = 0;
-
- if (nameValue !== '') {
- filterCount++;
- }
-
- let arrayFiltersData = [];
- ['pool', 'hastate'].forEach((filter) => {
- let selected = me.down(`#${filter}filter`).getValue() ?? [];
- if (selected.length) {
- filterCount++;
- arrayFiltersData.push([filter, [...selected]]);
- }
- });
-
- let singleFiltersData = [];
- ['status', 'type'].forEach((filter) => {
- let selected = me.down(`#${filter}filter`).getValue() ?? '';
- if (selected.length) {
- filterCount++;
- singleFiltersData.push([filter, selected]);
- }
- });
-
- let includeTags = me.down('#includetagfilter').getValue() ?? [];
- if (includeTags.length) {
- filterCount++;
- }
- let excludeTags = me.down('#excludetagfilter').getValue() ?? [];
- if (excludeTags.length) {
- filterCount++;
- }
-
- let fieldSet = me.down('#filters');
- let clearBtn = me.down('#clearBtn');
- if (filterCount) {
- fieldSet.setTitle(Ext.String.format(gettext('Filters ({0})'), filterCount));
- clearBtn.setDisabled(false);
- } else {
- fieldSet.setTitle(gettext('Filters'));
- clearBtn.setDisabled(true);
- }
-
- let filterFn = function (value) {
- let name = value.data.name.toLowerCase().indexOf(nameValue.toLowerCase()) !== -1;
- let arrayFilters = arrayFiltersData.every(
- ([filter, selected]) =>
- !selected.length || selected.indexOf(value.data[filter]) !== -1,
- );
- let singleFilters = singleFiltersData.every(
- ([filter, selected]) =>
- !selected.length || value.data[filter].indexOf(selected) !== -1,
- );
- let tags = value.data.tags.split(/[;, ]/).filter((t) => !!t);
- let includeFilter =
- !includeTags.length || tags.some((tag) => includeTags.indexOf(tag) !== -1);
- let excludeFilter =
- !excludeTags.length || tags.every((tag) => excludeTags.indexOf(tag) === -1);
-
- return name && arrayFilters && singleFilters && includeFilter && excludeFilter;
- };
- let vmselector = me.down('#vms');
- vmselector.getStore().setFilters({
- id: 'customFilter',
- filterFn,
- });
- vmselector.checkChange();
- if (me.action === 'migrateall') {
- let records = vmselector.getSelection();
- refreshLxcWarning(vmselector.getValue(), records);
- }
- };
-
- items.push({
- xtype: 'fieldset',
- itemId: 'filters',
- collapsible: true,
- title: gettext('Filters'),
- layout: 'hbox',
- items: [
- {
- xtype: 'container',
- flex: 1,
- padding: 5,
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- defaults: {
- listeners: {
- change: filterChange,
- },
- isFormField: false,
- },
- items: [
- {
- fieldLabel: gettext('Name'),
- itemId: 'namefilter',
- xtype: 'textfield',
- },
- {
- xtype: 'combobox',
- itemId: 'statusfilter',
- fieldLabel: gettext('Status'),
- emptyText: gettext('All'),
- editable: false,
- value: defaultStatus,
- store: statusList,
- },
- {
- xtype: 'combobox',
- itemId: 'poolfilter',
- fieldLabel: gettext('Pool'),
- emptyText: gettext('All'),
- editable: false,
- multiSelect: true,
- store: poolList,
- },
- ],
- },
- {
- xtype: 'container',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- flex: 1,
- padding: 5,
- defaults: {
- listeners: {
- change: filterChange,
- },
- isFormField: false,
- },
- items: [
- {
- xtype: 'combobox',
- itemId: 'typefilter',
- fieldLabel: gettext('Type'),
- emptyText: gettext('All'),
- editable: false,
- value: defaultType,
- store: [
- ['', gettext('All')],
- ['lxc', gettext('CT')],
- ['qemu', gettext('VM')],
- ],
- },
- {
- xtype: 'proxmoxComboGrid',
- itemId: 'includetagfilter',
- fieldLabel: gettext('Include Tags'),
- emptyText: gettext('All'),
- editable: false,
- multiSelect: true,
- valueField: 'value',
- displayField: 'value',
- listConfig: {
- userCls: 'proxmox-tags-full',
- columns: [
- {
- dataIndex: 'value',
- flex: 1,
- renderer: (value) =>
- PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
- },
- ],
- },
- store: {
- data: tagList,
- },
- listeners: {
- change: filterChange,
- },
- },
- {
- xtype: 'proxmoxComboGrid',
- itemId: 'excludetagfilter',
- fieldLabel: gettext('Exclude Tags'),
- emptyText: gettext('None'),
- multiSelect: true,
- editable: false,
- valueField: 'value',
- displayField: 'value',
- listConfig: {
- userCls: 'proxmox-tags-full',
- columns: [
- {
- dataIndex: 'value',
- flex: 1,
- renderer: (value) =>
- PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
- },
- ],
- },
- store: {
- data: tagList,
- },
- listeners: {
- change: filterChange,
- },
- },
- ],
- },
- {
- xtype: 'container',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- flex: 1,
- padding: 5,
- defaults: {
- listeners: {
- change: filterChange,
- },
- isFormField: false,
- },
- items: [
- {
- xtype: 'combobox',
- itemId: 'hastatefilter',
- fieldLabel: gettext('HA status'),
- emptyText: gettext('All'),
- multiSelect: true,
- editable: false,
- store: haList,
- listeners: {
- change: filterChange,
- },
- },
- {
- xtype: 'container',
- layout: {
- type: 'vbox',
- align: 'end',
- },
- items: [
- {
- xtype: 'button',
- itemId: 'clearBtn',
- text: gettext('Clear Filters'),
- disabled: true,
- handler: clearFilters,
- },
- ],
- },
- ],
- },
- ],
- });
-
- items.push({
- xtype: 'vmselector',
- itemId: 'vms',
- name: 'vms',
- flex: 1,
- height: 300,
- selectAll: true,
- allowBlank: false,
- plugins: '',
- nodename: me.nodename,
- listeners: {
- selectionchange: function (vmselector, records) {
- if (me.action === 'migrateall') {
- let vmids = me.down('#vms').getValue();
- refreshLxcWarning(vmids, records);
- }
- },
- },
- });
-
- me.formPanel = Ext.create('Ext.form.Panel', {
- bodyPadding: 10,
- border: false,
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- fieldDefaults: {
- anchor: '100%',
- },
- items: items,
- });
-
- let form = me.formPanel.getForm();
-
- let submitBtn = Ext.create('Ext.Button', {
- text: me.btnText,
- handler: function () {
- form.isValid();
- me.submit(form.getValues());
- },
- });
-
- Ext.apply(me, {
- items: [me.formPanel],
- buttons: [submitBtn],
- });
-
- me.callParent();
-
- if (me.prefilterIncludeTag) {
- me.down('#includetagfilter').setValue(me.prefilterIncludeTag);
- }
-
- form.on('validitychange', function () {
- let valid = form.isValid();
- submitBtn.setDisabled(!valid);
- });
- form.isValid();
-
- filterChange();
- },
-});
-Ext.define('PVE.ceph.Install', {
- extend: 'Ext.window.Window',
- xtype: 'pveCephInstallWindow',
- mixins: ['Proxmox.Mixin.CBind'],
-
- width: 220,
- header: false,
- resizable: false,
- draggable: false,
- modal: true,
- nodename: undefined,
- shadow: false,
- border: false,
- bodyBorder: false,
- closable: false,
- cls: 'install-mask',
- bodyCls: 'install-mask',
- layout: {
- align: 'stretch',
- pack: 'center',
- type: 'vbox',
- },
- viewModel: {
- data: {
- isInstalled: false,
- },
- formulas: {
- buttonText: function (get) {
- if (get('isInstalled')) {
- return gettext('Configure Ceph');
- } else {
- return gettext('Install Ceph');
- }
- },
- windowText: function (get) {
- if (get('isInstalled')) {
- return `
- ${gettext('Ceph is not initialized.')}
- ${gettext('You need to create an initial config once.')}
`;
- } else {
- return (
- '' +
- gettext('Ceph is not installed on this node.') +
- ' ' +
- gettext('Would you like to install it now?') +
- '
'
- );
- }
- },
- },
- },
- items: [
- {
- bind: {
- html: '{windowText}',
- },
- border: false,
- padding: 5,
- bodyCls: 'install-mask',
- },
- {
- xtype: 'button',
- bind: {
- text: '{buttonText}',
- },
- viewModel: {},
- cbind: {
- nodename: '{nodename}',
- },
- handler: function () {
- let view = this.up('pveCephInstallWindow');
- let wizard = Ext.create('PVE.ceph.CephInstallWizard', {
- nodename: view.nodename,
- });
- wizard.getViewModel().set('isInstalled', this.getViewModel().get('isInstalled'));
- wizard.show();
- view.mon(wizard, 'beforeClose', function () {
- view.fireEvent('cephInstallWindowClosed');
- view.close();
- });
- },
- },
- ],
-});
-Ext.define('PVE.window.Clone', {
- extend: 'Ext.window.Window',
-
- resizable: false,
-
- isTemplate: false,
-
- onlineHelp: 'qm_copy_and_clone',
-
- controller: {
- xclass: 'Ext.app.ViewController',
- control: {
- 'panel[reference=cloneform]': {
- validitychange: 'disableSubmit',
- },
- },
- disableSubmit: function (form) {
- this.lookupReference('submitBtn').setDisabled(!form.isValid());
- },
- },
-
- statics: {
- // display a snapshot selector only if needed
- wrap: function (nodename, vmid, vmname, isTemplate, guestType) {
- Proxmox.Utils.API2Request({
- url: '/nodes/' + nodename + '/' + guestType + '/' + vmid + '/snapshot',
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- success: function (response, opts) {
- var snapshotList = response.result.data;
- var hasSnapshots = !(
- snapshotList.length === 1 && snapshotList[0].name === 'current'
- );
-
- Ext.create('PVE.window.Clone', {
- nodename: nodename,
- guestType: guestType,
- vmid: vmid,
- vmname: vmname,
- isTemplate: isTemplate,
- hasSnapshots: hasSnapshots,
- }).show();
- },
- });
- },
- },
-
- create_clone: function (values) {
- var me = this;
-
- var params = { newid: values.newvmid };
-
- if (values.snapname && values.snapname !== 'current') {
- params.snapname = values.snapname;
- }
-
- if (values.pool) {
- params.pool = values.pool;
- }
-
- if (values.name) {
- if (me.guestType === 'lxc') {
- params.hostname = values.name;
- } else {
- params.name = values.name;
- }
- }
-
- if (values.target) {
- params.target = values.target;
- }
-
- if (values.clonemode === 'copy') {
- params.full = 1;
- if (values.hdstorage) {
- params.storage = values.hdstorage;
- if (values.diskformat && me.guestType !== 'lxc') {
- params.format = values.diskformat;
- }
- }
- }
-
- Proxmox.Utils.API2Request({
- params: params,
- url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone',
- waitMsgTarget: me,
- method: 'POST',
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- success: function (response, options) {
- me.close();
- },
- });
- },
-
- // disable the Storage selector when clone mode is linked clone
- updateVisibility: function () {
- var me = this;
- var clonemode = me.lookupReference('clonemodesel').getValue();
- var disksel = me.lookup('diskselector');
- disksel.setDisabled(clonemode === 'clone');
- },
-
- // add to the list of valid nodes each node where
- // all the VM disks are available
- verifyFeature: function () {
- var me = this;
-
- var snapname = me.lookupReference('snapshotsel').getValue();
- var clonemode = me.lookupReference('clonemodesel').getValue();
-
- var params = { feature: clonemode };
- if (snapname !== 'current') {
- params.snapname = snapname;
- }
-
- Proxmox.Utils.API2Request({
- waitMsgTarget: me,
- url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature',
- params: params,
- method: 'GET',
- failure: function (response, opts) {
- me.lookupReference('submitBtn').setDisabled(true);
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- success: function (response, options) {
- var res = response.result.data;
-
- me.lookupReference('targetsel').allowedNodes = res.nodes;
- me.lookupReference('targetsel').validate();
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.vmid) {
- throw 'no VM ID specified';
- }
-
- if (!me.snapname) {
- me.snapname = 'current';
- }
-
- if (!me.guestType) {
- throw 'no Guest Type specified';
- }
-
- var titletext = me.guestType === 'lxc' ? 'CT' : 'VM';
- if (me.isTemplate) {
- titletext += ' Template';
- }
-
- let formattedGuestIdentifier = PVE.Utils.getFormattedGuestIdentifier(me.vmid, me.vmname);
- me.title = `Clone ${titletext} ${formattedGuestIdentifier}`;
-
- var col1 = [];
- var col2 = [];
-
- col1.push({
- xtype: 'pveNodeSelector',
- name: 'target',
- reference: 'targetsel',
- fieldLabel: gettext('Target node'),
- selectCurNode: true,
- allowBlank: false,
- onlineValidator: true,
- listeners: {
- change: function (f, value) {
- me.lookup('diskselector').getComponent('hdstorage').setTargetNode(value);
- },
- },
- });
-
- var modelist = [['copy', gettext('Full Clone')]];
- if (me.isTemplate) {
- modelist.push(['clone', gettext('Linked Clone')]);
- }
-
- col1.push(
- {
- xtype: 'pveGuestIDSelector',
- name: 'newvmid',
- guestType: me.guestType,
- value: '',
- loadNextFreeID: true,
- validateExists: false,
- },
- {
- xtype: 'textfield',
- name: 'name',
- vtype: 'DnsName',
- allowBlank: true,
- fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name'),
- },
- {
- xtype: 'pvePoolSelector',
- fieldLabel: gettext('Resource Pool'),
- name: 'pool',
- value: '',
- allowBlank: true,
- },
- );
-
- col2.push(
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('Mode'),
- name: 'clonemode',
- reference: 'clonemodesel',
- allowBlank: false,
- hidden: !me.isTemplate,
- value: me.isTemplate ? 'clone' : 'copy',
- comboItems: modelist,
- listeners: {
- change: function (t, value) {
- me.updateVisibility();
- me.verifyFeature();
- },
- },
- },
- {
- xtype: 'PVE.form.SnapshotSelector',
- name: 'snapname',
- reference: 'snapshotsel',
- fieldLabel: gettext('Snapshot'),
- nodename: me.nodename,
- guestType: me.guestType,
- vmid: me.vmid,
- hidden: !!(me.isTemplate || !me.hasSnapshots),
- disabled: false,
- allowBlank: false,
- value: me.snapname,
- listeners: {
- change: function (f, value) {
- me.verifyFeature();
- },
- },
- },
- {
- xtype: 'pveDiskStorageSelector',
- reference: 'diskselector',
- nodename: me.nodename,
- autoSelect: false,
- hideSize: true,
- hideSelection: true,
- storageLabel: gettext('Target Storage'),
- allowBlank: true,
- storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir',
- emptyText: gettext('Same as source'),
- disabled: !!me.isTemplate, // because default mode is clone for templates
- },
- );
-
- var formPanel = Ext.create('Ext.form.Panel', {
- bodyPadding: 10,
- reference: 'cloneform',
- border: false,
- layout: 'hbox',
- defaultType: 'container',
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%',
- },
- items: [
- {
- flex: 1,
- padding: '0 10 0 0',
- layout: 'anchor',
- items: col1,
- },
- {
- flex: 1,
- padding: '0 0 0 10',
- layout: 'anchor',
- items: col2,
- },
- ],
- });
-
- Ext.apply(me, {
- modal: true,
- width: 600,
- height: 250,
- border: false,
- layout: 'fit',
- buttons: [
- {
- xtype: 'proxmoxHelpButton',
- listenToGlobalEvent: false,
- hidden: false,
- onlineHelp: me.onlineHelp,
- },
- '->',
- {
- reference: 'submitBtn',
- text: gettext('Clone'),
- disabled: true,
- handler: function () {
- var cloneForm = me.lookupReference('cloneform');
- if (cloneForm.isValid()) {
- me.create_clone(cloneForm.getValues());
- }
- },
- },
- ],
- items: [formPanel],
- });
-
- me.callParent();
-
- me.verifyFeature();
- },
-});
-/*
- * ConfirmRemoveDialog window with additional checkboxes for removing resources
- */
-Ext.define('PVE.window.ConfirmRemoveResource', {
- extend: 'Proxmox.window.ConfirmRemoveDialog',
- alias: 'widget.pveConfirmRemoveResource',
-
- additionalItems: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'purge',
- reference: 'purgeCheckbox',
- boxLabel: gettext('Purge resource from referenced HA rules'),
- padding: '5 0 0 0',
- checked: true,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Also removes resource from HA rules and removes rule if there are no other resources in it',
- ),
- },
- },
- ],
-
- getText: function () {
- let me = this;
-
- me.text = `Are you sure you want to remove resource '${me.getItem().id}'?`;
-
- return me.callParent();
- },
-
- getParams: function () {
- let me = this;
-
- const purgeCheckbox = me.lookupReference('purgeCheckbox');
- me.params.purge = purgeCheckbox.checked ? 1 : 0;
-
- return me.callParent();
- },
-});
-Ext.define('PVE.FirewallEnableEdit', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveFirewallEnableEdit'],
- mixins: ['Proxmox.Mixin.CBind'],
-
- subject: gettext('Firewall'),
- cbindData: {
- defaultValue: 0,
- },
- width: 350,
-
- items: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'enable',
- uncheckedValue: 0,
- cbind: {
- defaultValue: '{defaultValue}',
- checked: '{defaultValue}',
- },
- deleteDefaultValue: false,
- fieldLabel: gettext('Firewall'),
- },
- {
- xtype: 'displayfield',
- name: 'warning',
- userCls: 'pmx-hint',
- value: gettext('Warning: Firewall still disabled at datacenter level!'),
- hidden: true,
- },
- ],
-
- beforeShow: function () {
- var me = this;
-
- Proxmox.Utils.API2Request({
- url: '/api2/extjs/cluster/firewall/options',
- method: 'GET',
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response, opts) {
- if (!response.result.data.enable) {
- me.down('displayfield[name=warning]').setVisible(true);
- }
- },
- });
- },
-});
-Ext.define('PVE.FirewallLograteInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveFirewallLograteInputPanel',
-
- viewModel: {},
-
- items: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'enable',
- reference: 'enable',
- fieldLabel: gettext('Enable'),
- value: true,
- },
- {
- layout: 'hbox',
- border: false,
- items: [
- {
- xtype: 'numberfield',
- name: 'rate',
- fieldLabel: gettext('Log rate limit'),
- minValue: 1,
- maxValue: 99,
- allowBlank: false,
- flex: 2,
- value: 1,
- },
- {
- xtype: 'box',
- html: '/
',
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'unit',
- comboItems: [
- ['second', 'second'],
- ['minute', 'minute'],
- ['hour', 'hour'],
- ['day', 'day'],
- ],
- allowBlank: false,
- flex: 1,
- value: 'second',
- },
- ],
- },
- {
- xtype: 'numberfield',
- name: 'burst',
- fieldLabel: gettext('Log burst limit'),
- minValue: 1,
- maxValue: 99,
- value: 5,
- },
- ],
-
- onGetValues: function (values) {
- let _me = this;
-
- let cfg = {
- enable: values.enable !== undefined ? 1 : 0,
- rate: values.rate + '/' + values.unit,
- burst: values.burst,
- };
- let properties = PVE.Parser.printPropertyString(cfg, undefined);
- if (properties === '') {
- return { delete: 'log_ratelimit' };
- }
- return { log_ratelimit: properties };
- },
-
- setValues: function (values) {
- let me = this;
-
- let properties = {};
- if (values.log_ratelimit !== undefined) {
- properties = PVE.Parser.parsePropertyString(values.log_ratelimit, 'enable');
- if (properties.rate) {
- let matches = properties.rate.match(/^(\d+)\/(second|minute|hour|day)$/);
- if (matches) {
- properties.rate = matches[1];
- properties.unit = matches[2];
- }
- }
- }
- me.callParent([properties]);
- },
-});
-
-Ext.define('PVE.FirewallLograteEdit', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveFirewallLograteEdit',
-
- subject: gettext('Log rate limit'),
-
- items: [
- {
- xtype: 'pveFirewallLograteInputPanel',
- },
- ],
- autoLoad: true,
-});
-/*global u2f*/
-Ext.define('PVE.window.LoginWindow', {
- extend: 'Ext.window.Window',
-
- viewModel: {
- data: {
- openid: false,
- },
- formulas: {
- button_text: function (get) {
- if (get('openid') === true) {
- return gettext('Login (OpenID redirect)');
- } else {
- return gettext('Login');
- }
- },
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: async function () {
- if (Proxmox.ConsentText) {
- let oidc_auth_redirect = Proxmox.Utils.getOpenIDRedirectionAuthorization();
- if (oidc_auth_redirect === undefined) {
- Ext.create('Proxmox.window.ConsentModal', {
- autoShow: true,
- consent: Proxmox.Markdown.parse(
- Proxmox.Utils.base64ToUtf8(Proxmox.ConsentText),
- ),
- });
- }
- }
- },
-
- onLogon: async function () {
- var me = this;
-
- var form = this.lookupReference('loginForm');
- var unField = this.lookupReference('usernameField');
- var saveunField = this.lookupReference('saveunField');
- var view = this.getView();
-
- if (!form.isValid()) {
- return;
- }
-
- let creds = form.getValues();
-
- if (this.getViewModel().data.openid === true) {
- const redirectURL = location.origin;
- Proxmox.Utils.API2Request({
- url: '/api2/extjs/access/openid/auth-url',
- params: {
- realm: creds.realm,
- 'redirect-url': redirectURL,
- },
- method: 'POST',
- success: function (resp, opts) {
- window.location = resp.result.data;
- },
- failure: function (resp, opts) {
- Proxmox.Utils.authClear();
- form.unmask();
- Ext.MessageBox.alert(
- gettext('Error'),
- gettext('OpenID redirect failed.') + ` ${resp.htmlStatus}`,
- );
- },
- });
- return;
- }
-
- view.el.mask(gettext('Please wait...'), 'x-mask-loading');
-
- // set or clear username
- var sp = Ext.state.Manager.getProvider();
- if (saveunField.getValue() === true) {
- sp.set(unField.getStateId(), unField.getValue());
- } else {
- sp.clear(unField.getStateId());
- }
- sp.set(saveunField.getStateId(), saveunField.getValue());
-
- try {
- // Request updated authentication mechanism:
- creds['new-format'] = 1;
-
- let resp = await Proxmox.Async.api2({
- url: '/api2/extjs/access/ticket',
- params: creds,
- method: 'POST',
- });
-
- let data = resp.result.data;
- if (data.ticket.startsWith('PVE:!tfa!')) {
- // Store first factor login information first:
- data.LoggedOut = true;
- Proxmox.Utils.setAuthData(data);
-
- data = await me.performTFAChallenge(data);
-
- // Fill in what we copy over from the 1st factor:
- data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
- data.username = Proxmox.UserName;
- me.success(data);
- } else if (Ext.isDefined(data.NeedTFA)) {
- // Store first factor login information first:
- data.LoggedOut = true;
- Proxmox.Utils.setAuthData(data);
-
- if (Ext.isDefined(data.U2FChallenge)) {
- me.perform_u2f(data);
- } else {
- me.perform_otp();
- }
- } else {
- me.success(data);
- }
- } catch (error) {
- me.failure(error);
- }
- },
-
- /* START NEW TFA CODE (pbs copy) */
- performTFAChallenge: async function (data) {
- let _me = this;
-
- let userid = data.username;
- let ticket = data.ticket;
- let challenge = JSON.parse(
- decodeURIComponent(ticket.split(':')[1].slice('!tfa!'.length)),
- );
-
- let resp = await new Promise((resolve, reject) => {
- Ext.create('Proxmox.window.TfaLoginWindow', {
- userid,
- ticket,
- challenge,
- onResolve: (value) => resolve(value),
- onReject: reject,
- }).show();
- });
-
- return resp.result.data;
- },
- /* END NEW TFA CODE (pbs copy) */
-
- failure: function (resp) {
- var me = this;
- var view = me.getView();
- view.el.unmask();
- var handler = function () {
- var uf = me.lookupReference('usernameField');
- uf.focus(true, true);
- };
-
- let emsg = gettext('Login failed. Please try again');
- if (resp.status) {
- emsg = Ext.String.format(
- '{0} {1} {2}',
- gettext('Login failed:'),
- resp.htmlStatus,
- gettext('Please try again'),
- );
- }
-
- if (resp.failureType === 'connect') {
- emsg = gettext(
- 'Connection failure. Network error or Proxmox VE services not running?',
- );
- }
-
- Ext.MessageBox.alert(gettext('Error'), emsg, handler);
- },
- success: function (data) {
- var me = this;
- var view = me.getView();
- var handler = view.handler || Ext.emptyFn;
- handler.call(me, data);
- view.close();
- },
-
- perform_otp: function () {
- var me = this;
- var win = Ext.create('PVE.window.TFALoginWindow', {
- onLogin: function (value) {
- me.finish_tfa(value);
- },
- onCancel: function () {
- Proxmox.LoggedOut = false;
- Proxmox.Utils.authClear();
- me.getView().show();
- },
- });
- win.show();
- },
-
- perform_u2f: function (data) {
- var me = this;
- // Show the message:
- var msg = Ext.Msg.show({
- title: 'U2F: ' + gettext('Verification'),
- message: gettext('Please press the button on your U2F Device'),
- buttons: [],
- });
- var chlg = data.U2FChallenge;
- var key = {
- version: chlg.version,
- keyHandle: chlg.keyHandle,
- };
- u2f.sign(chlg.appId, chlg.challenge, [key], function (res) {
- msg.close();
- if (res.errorCode) {
- Proxmox.Utils.authClear();
- Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode));
- return;
- }
- delete res.errorCode;
- me.finish_tfa(JSON.stringify(res));
- });
- },
- finish_tfa: function (res) {
- var me = this;
- var view = me.getView();
- view.el.mask(gettext('Please wait...'), 'x-mask-loading');
- Proxmox.Utils.API2Request({
- url: '/api2/extjs/access/tfa',
- params: {
- response: res,
- },
- method: 'POST',
- timeout: 5000, // it'll delay both success & failure
- success: function (resp, opts) {
- view.el.unmask();
- // Fill in what we copy over from the 1st factor:
- var data = resp.result.data;
- data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
- data.username = Proxmox.UserName;
- // Finish logging in:
- me.success(data);
- },
- failure: function (resp, opts) {
- Proxmox.Utils.authClear();
- me.failure(resp);
- },
- });
- },
-
- control: {
- 'field[name=username]': {
- specialkey: function (f, e) {
- if (e.getKey() === e.ENTER) {
- let pf = this.lookupReference('passwordField');
- if (!pf.getValue()) {
- pf.focus(false);
- }
- }
- },
- },
- 'field[name=lang]': {
- change: function (f, value) {
- var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
- Ext.util.Cookies.set('PVELangCookie', value, dt);
- this.getView().mask(gettext('Please wait...'), 'x-mask-loading');
- window.location.reload();
- },
- },
- 'field[name=realm]': {
- change: function (f, value) {
- let record = f.store.getById(value);
- if (record === undefined) {
- return;
- }
- let data = record.data;
- this.getViewModel().set('openid', data.type === 'openid');
- },
- },
- 'button[reference=loginButton]': {
- click: 'onLogon',
- },
- '#': {
- show: function () {
- var me = this;
-
- var sp = Ext.state.Manager.getProvider();
- var checkboxField = this.lookupReference('saveunField');
- var unField = this.lookupReference('usernameField');
-
- var checked = sp.get(checkboxField.getStateId());
- checkboxField.setValue(checked);
-
- if (checked === true) {
- let username = sp.get(unField.getStateId());
- unField.setValue(username);
- let pwField = this.lookupReference('passwordField');
- pwField.focus();
- }
-
- let auth = Proxmox.Utils.getOpenIDRedirectionAuthorization();
- if (auth !== undefined) {
- Proxmox.Utils.authClear();
-
- let loginForm = this.lookupReference('loginForm');
- loginForm.mask(gettext('OpenID login - please wait...'), 'x-mask-loading');
-
- const redirectURL = location.origin;
-
- Proxmox.Utils.API2Request({
- url: '/api2/extjs/access/openid/login',
- params: {
- state: auth.state,
- code: auth.code,
- 'redirect-url': redirectURL,
- },
- method: 'POST',
- failure: function (response) {
- loginForm.unmask();
- let error = response.htmlStatus;
- Ext.MessageBox.alert(
- gettext('Error'),
- gettext('OpenID login failed, please try again') +
- ` ${error}`,
- () => {
- window.location = redirectURL;
- },
- );
- },
- success: function (response, options) {
- loginForm.unmask();
- let data = response.result.data;
- history.replaceState(null, '', redirectURL);
- me.success(data);
- },
- });
- }
- },
- },
- },
- },
-
- width: 400,
- modal: true,
- border: false,
- draggable: true,
- closable: false,
- resizable: false,
- layout: 'auto',
-
- title: gettext('Proxmox VE Login'),
-
- defaultFocus: 'usernameField',
- defaultButton: 'loginButton',
-
- items: [
- {
- xtype: 'form',
- layout: 'form',
- url: '/api2/extjs/access/ticket',
- reference: 'loginForm',
-
- fieldDefaults: {
- labelAlign: 'right',
- allowBlank: false,
- },
-
- items: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('User name'),
- name: 'username',
- itemId: 'usernameField',
- reference: 'usernameField',
- stateId: 'login-username',
- inputAttrTpl: 'autocomplete=username',
- bind: {
- visible: '{!openid}',
- disabled: '{openid}',
- },
- },
- {
- xtype: 'textfield',
- inputType: 'password',
- fieldLabel: gettext('Password'),
- name: 'password',
- reference: 'passwordField',
- inputAttrTpl: 'autocomplete=current-password',
- bind: {
- visible: '{!openid}',
- disabled: '{openid}',
- },
- },
- {
- xtype: 'pmxRealmComboBox',
- name: 'realm',
- },
- {
- xtype: 'proxmoxLanguageSelector',
- fieldLabel: gettext('Language'),
- value: PVE.Utils.getUiLanguage(),
- name: 'lang',
- reference: 'langField',
- submitValue: false,
- },
- ],
- buttons: [
- {
- xtype: 'checkbox',
- fieldLabel: gettext('Save User name'),
- name: 'saveusername',
- reference: 'saveunField',
- stateId: 'login-saveusername',
- labelWidth: 250,
- labelAlign: 'right',
- submitValue: false,
- bind: {
- visible: '{!openid}',
- },
- },
- {
- bind: {
- text: '{button_text}',
- },
- reference: 'loginButton',
- },
- ],
- },
- ],
-});
-Ext.define('PVE.window.Migrate', {
- extend: 'Ext.window.Window',
-
- vmtype: undefined,
- nodename: undefined,
- vmid: undefined,
- vmname: undefined,
- maxHeight: 450,
-
- viewModel: {
- data: {
- vmid: undefined,
- nodename: undefined,
- vmtype: undefined,
- running: false,
- qemu: {
- onlineHelp: 'qm_migration',
- commonName: 'VM',
- },
- lxc: {
- onlineHelp: 'pct_migration',
- commonName: 'CT',
- },
- migration: {
- possible: true,
- preconditions: [],
- 'with-local-disks': 0,
- mode: undefined,
- allowedNodes: undefined,
- overwriteLocalResourceCheck: false,
- hasLocalResources: false,
- withConntrackState: true,
- bothHaveDbusVmstate: false,
- },
- },
- formulas: {
- setMigrationMode: function (get) {
- if (get('running')) {
- if (get('vmtype') === 'qemu') {
- return gettext('Online');
- } else {
- return gettext('Restart Mode');
- }
- } else {
- return gettext('Offline');
- }
- },
- setStorageselectorHidden: function (get) {
- if (get('migration.with-local-disks') && get('running')) {
- return false;
- } else {
- return true;
- }
- },
- setLocalResourceCheckboxHidden: function (get) {
- if (
- get('running') ||
- !get('migration.hasLocalResources') ||
- Proxmox.UserName !== 'root@pam'
- ) {
- return true;
- } else {
- return false;
- }
- },
- conntrackStateCheckboxHidden: (get) =>
- !get('running') ||
- get('vmtype') !== 'qemu' ||
- !get('migration.bothHaveDbusVmstate'),
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
- control: {
- 'panel[reference=formPanel]': {
- validityChange: function (panel, isValid) {
- this.getViewModel().set('migration.possible', isValid);
- this.checkMigratePreconditions();
- },
- },
- },
-
- init: function (view) {
- var me = this,
- vm = view.getViewModel();
-
- if (!view.nodename) {
- throw 'missing custom view config: nodename';
- }
- vm.set('nodename', view.nodename);
-
- if (!view.vmid) {
- throw 'missing custom view config: vmid';
- }
- vm.set('vmid', view.vmid);
-
- if (!view.vmtype) {
- throw 'missing custom view config: vmtype';
- }
- vm.set('vmtype', view.vmtype);
-
- let title = Ext.String.format(
- '{0} {1} {2}',
- gettext('Migrate'),
- vm.get(view.vmtype).commonName,
- PVE.Utils.getFormattedGuestIdentifier(view.vmid, view.vmname),
- );
- view.setTitle(title);
-
- me.lookup('proxmoxHelpButton').setHelpConfig({
- onlineHelp: vm.get(view.vmtype).onlineHelp,
- });
- me.lookup('formPanel').isValid();
- },
-
- onTargetChange: function (nodeSelector) {
- // Always display the storages of the currently selected migration target
- this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
- this.checkMigratePreconditions(true);
- },
-
- startMigration: function () {
- var me = this,
- view = me.getView(),
- vm = me.getViewModel();
-
- var values = me.lookup('formPanel').getValues();
- var params = {
- target: values.target,
- };
-
- if (vm.get('migration.mode')) {
- params[vm.get('migration.mode')] = 1;
- }
- if (vm.get('migration.with-local-disks')) {
- params['with-local-disks'] = 1;
- }
- //offline migration to a different storage currently might fail at a late stage
- //(i.e. after some disks have been moved), so don't expose it yet in the GUI
- if (vm.get('migration.with-local-disks') && vm.get('running') && values.targetstorage) {
- params.targetstorage = values.targetstorage;
- }
-
- if (vm.get('migration.overwriteLocalResourceCheck')) {
- params.force = 1;
- }
-
- if (vm.get('migration.bothHaveDbusVmstate') && vm.get('migration.withConntrackState')) {
- params['with-conntrack-state'] = 1;
- }
-
- Proxmox.Utils.API2Request({
- params: params,
- url:
- '/nodes/' +
- vm.get('nodename') +
- '/' +
- vm.get('vmtype') +
- '/' +
- vm.get('vmid') +
- '/migrate',
- waitMsgTarget: view,
- method: 'POST',
- failure: function (response, opts) {
- Ext.Msg.alert(Proxmox.Utils.errorText, response.htmlStatus);
- },
- success: function (response, options) {
- var upid = response.result.data;
- var extraTitle = Ext.String.format(
- ' ({0} ---> {1})',
- vm.get('nodename'),
- params.target,
- );
-
- Ext.create('Proxmox.window.TaskViewer', {
- upid: upid,
- extraTitle: extraTitle,
- }).show();
-
- view.close();
- },
- });
- },
-
- checkMigratePreconditions: async function (resetMigrationPossible) {
- var me = this,
- vm = me.getViewModel();
-
- var vmrec = PVE.data.ResourceStore.findRecord(
- 'vmid',
- vm.get('vmid'),
- 0,
- false,
- false,
- true,
- );
- if (vmrec && vmrec.data && vmrec.data.running) {
- vm.set('running', true);
- }
-
- me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
-
- if (vm.get('vmtype') === 'qemu') {
- await me.checkQemuPreconditions(resetMigrationPossible);
- } else {
- await me.checkLxcPreconditions(resetMigrationPossible);
- }
-
- // Only allow nodes where the local storage is available in case of offline migration
- // where storage migration is not possible
- me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes');
-
- me.lookup('formPanel').isValid();
- },
-
- checkQemuPreconditions: async function (resetMigrationPossible) {
- let me = this,
- vm = me.getViewModel(),
- migrateStats;
-
- if (vm.get('running')) {
- vm.set('migration.mode', 'online');
- }
-
- try {
- if (
- me.fetchingNodeMigrateInfo &&
- me.fetchingNodeMigrateInfo === vm.get('nodename')
- ) {
- return;
- }
- me.fetchingNodeMigrateInfo = vm.get('nodename');
- let { result } = await Proxmox.Async.api2({
- url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`,
- method: 'GET',
- });
- migrateStats = result.data;
- } catch (error) {
- if (error?.result?.status !== 501) {
- Ext.Msg.alert(Proxmox.Utils.errorText, error.htmlStatus);
- }
- me.fetchingNodeMigrateInfo = false;
- return;
- }
-
- const target = me.lookup('pveNodeSelector').value;
- let targetCapabilities = {};
-
- try {
- const { result } = await Proxmox.Async.api2({
- url: `/nodes/${target}/capabilities/qemu/migration`,
- method: 'GET',
- });
- targetCapabilities = result.data;
- } catch (err) {
- // Only emit a warning in the case the target node does not (yet) support the
- // `capabilites/qemu/migration` endpoint and simply treat all features as unsupported.
- console.warn(`failed to query /capabilites/qemu/migration on '${target}':`, err);
- }
-
- me.fetchingNodeMigrateInfo = false;
-
- if (migrateStats.running) {
- vm.set('running', true);
- }
- // Get migration object from viewmodel to prevent to many bind callbacks
- let migration = vm.get('migration');
- if (resetMigrationPossible) {
- migration.possible = true;
- }
- migration.preconditions = [];
- let disallowed = migrateStats.not_allowed_nodes?.[target] ?? {};
-
- if (migrateStats.allowed_nodes && !vm.get('running')) {
- migration.allowedNodes = migrateStats.allowed_nodes;
- if (target.length && !migrateStats.allowed_nodes.includes(target)) {
- if (disallowed.unavailable_storages !== undefined) {
- let missingStorages = disallowed.unavailable_storages.join(', ');
- const text = Ext.String.format(
- gettext(
- 'Storage(s) ({0}) not available on selected target. Start VM to use live storage migration or select other target node.',
- ),
- missingStorages,
- );
-
- migration.possible = false;
- migration.preconditions.push({ text, severity: 'error' });
- }
- }
- }
-
- if (disallowed['unavailable-resources'] !== undefined) {
- let unavailableResources = disallowed['unavailable-resources'].join(', ');
- const text = Ext.String.format(
- gettext('Mapped Resources ({0}) not available on selected target.'),
- unavailableResources,
- );
-
- migration.possible = false;
- migration.preconditions.push({ text, severity: 'error' });
- }
-
- let blockingResources = [];
- let mappedResources = migrateStats['mapped-resource-info'] ?? {};
-
- for (const res of migrateStats.local_resources) {
- if (!mappedResources[res]) {
- blockingResources.push(res);
- }
- }
-
- if (blockingResources.length) {
- migration.hasLocalResources = true;
- if (!migration.overwriteLocalResourceCheck || vm.get('running')) {
- const text = Ext.String.format(
- gettext('Cannot migrate VM with local resources: {0}'),
- blockingResources.join(', '),
- );
-
- migration.possible = false;
- migration.preconditions.push({ text, severity: 'error' });
- } else {
- const text = Ext.String.format(
- gettext(
- 'Migrating VM with local resources: {0}. This might fail if the resources are not available on the target node.',
- ),
- blockingResources.join(', '),
- );
-
- migration.preconditions.push({ text, severity: 'warning' });
- }
- }
-
- if (vm.get('running')) {
- let allowed = [];
- let notAllowed = [];
- for (const [key, resource] of Object.entries(mappedResources)) {
- if (resource['live-migration']) {
- allowed.push(key);
- } else {
- notAllowed.push(key);
- }
- }
- if (notAllowed.length > 0) {
- const text = Ext.String.format(
- gettext('Cannot migrate running VM with mapped resources: {0}'),
- notAllowed.join(', '),
- );
-
- migration.possible = false;
- migration.preconditions.push({ text, severity: 'error' });
- } else if (allowed.length > 0) {
- const text = Ext.String.format(
- gettext(
- 'Live-migrating running VM with mapped resources (Experimental): {0}',
- ),
- allowed.join(', '),
- );
-
- migration.preconditions.push({ text, severity: 'warning' });
- }
- }
-
- if (migrateStats.local_disks.length) {
- migrateStats.local_disks.forEach(function (disk) {
- if (disk.cdrom && disk.cdrom === 1) {
- if (!disk.volid.includes('vm-' + vm.get('vmid') + '-cloudinit')) {
- migration.possible = false;
- migration.preconditions.push({
- text: gettext('Cannot migrate VM with local CD/DVD'),
- severity: 'error',
- });
- }
- } else {
- let size = disk.size
- ? '(' + Proxmox.Utils.render_size(disk.size) + ')'
- : '';
- const text = Ext.String.format(
- gettext('Migration with local disk might take long: {0} {1}'),
- disk.volid,
- size,
- );
-
- migration['with-local-disks'] = 1;
- migration.preconditions.push({ text, severity: 'warning' });
- }
- });
- }
-
- migration.bothHaveDbusVmstate =
- migrateStats['has-dbus-vmstate'] && targetCapabilities['has-dbus-vmstate'];
- if (vm.get('running')) {
- if (migration.withConntrackState && !migrateStats['has-dbus-vmstate']) {
- migration.preconditions.push({
- text: gettext(
- 'Cannot migrate conntrack state, source node is lacking support.',
- ),
- // user cannot really do anything about this, do not bother with scaring them!
- severity: 'info',
- });
- }
- if (migration.withConntrackState && !targetCapabilities['has-dbus-vmstate']) {
- migration.preconditions.push({
- text: gettext(
- 'Cannot migrate conntrack state, target node is lacking support. Active network connections might get dropped.',
- ),
- severity: 'warning',
- });
- }
-
- if (migration.bothHaveDbusVmstate && !migration.withConntrackState) {
- migration.preconditions.push({
- text: gettext(
- 'Conntrack state migration disabled. Active network connections might get dropped.',
- ),
- severity: 'warning',
- });
- }
- }
-
- let blockingHAResources = disallowed['blocking-ha-resources'] ?? [];
- if (blockingHAResources.length) {
- migration.possible = false;
-
- for (const { sid, cause } of blockingHAResources) {
- let reasonText;
- if (cause === 'resource-affinity') {
- reasonText = Ext.String.format(
- gettext(
- 'HA resource {0} with negative affinity to VM on selected target node',
- ),
- sid,
- );
- } else {
- reasonText = Ext.String.format(
- gettext('blocking HA resource {0} on selected target node'),
- sid,
- );
- }
-
- migration.preconditions.push({
- severity: 'error',
- text: Ext.String.format(
- gettext('Cannot migrate VM, because {0}.'),
- reasonText,
- ),
- });
- }
- }
-
- let dependentHAResources = migrateStats['dependent-ha-resources'];
- if (dependentHAResources !== undefined) {
- for (const sid of dependentHAResources) {
- const text = Ext.String.format(
- gettext(
- 'HA resource {0} with positive affinity to VM is also migrated to selected target node.',
- ),
- sid,
- );
-
- migration.preconditions.push({ text, severity: 'warning' });
- }
- }
-
- vm.set('migration', migration);
- },
- checkLxcPreconditions: async function (resetMigrationPossible) {
- let me = this;
- let vm = me.getViewModel();
- let migrateStats;
-
- if (vm.get('running')) {
- vm.set('migration.mode', 'restart');
- }
-
- try {
- if (
- me.fetchingNodeMigrateInfo &&
- me.fetchingNodeMigrateInfo === vm.get('nodename')
- ) {
- return;
- }
- me.fetchingNodeMigrateInfo = vm.get('nodename');
- let { result } = await Proxmox.Async.api2({
- url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`,
- method: 'GET',
- });
- migrateStats = result.data;
- me.fetchingNodeMigrateInfo = false;
- } catch (error) {
- if (error?.result?.status !== 501) {
- Ext.Msg.alert(Proxmox.Utils.errorText, error.htmlStatus);
- }
- me.fetchingNodeMigrateInfo = false;
- return;
- }
-
- if (migrateStats.running) {
- vm.set('running', true);
- }
-
- // Get migration object from viewmodel to prevent to many bind callbacks
- let migration = vm.get('migration');
- if (resetMigrationPossible) {
- migration.possible = true;
- }
- migration.preconditions = [];
- let targetNode = me.lookup('pveNodeSelector').value;
- let disallowed = migrateStats['not-allowed-nodes']?.[targetNode] ?? {};
-
- let blockingHAResources = disallowed['blocking-ha-resources'] ?? [];
- if (blockingHAResources.length) {
- migration.possible = false;
-
- for (const { sid, cause } of blockingHAResources) {
- let reasonText;
- if (cause === 'resource-affinity') {
- reasonText = Ext.String.format(
- gettext(
- 'HA resource {0} with negative affinity to container on selected target node',
- ),
- sid,
- );
- } else {
- reasonText = Ext.String.format(
- gettext('blocking HA resource {0} on selected target node'),
- sid,
- );
- }
-
- migration.preconditions.push({
- severity: 'error',
- text: Ext.String.format(
- gettext('Cannot migrate container, because {0}.'),
- reasonText,
- ),
- });
- }
- }
-
- let dependentHAResources = migrateStats['dependent-ha-resources'];
- if (dependentHAResources !== undefined) {
- for (const sid of dependentHAResources) {
- const text = Ext.String.format(
- gettext(
- 'HA resource {0} with positive affinity to container is also migrated to selected target node.',
- ),
- sid,
- );
-
- migration.preconditions.push({ text, severity: 'warning' });
- }
- }
-
- vm.set('migration', migration);
- },
- },
-
- width: 600,
- modal: true,
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- border: false,
- items: [
- {
- xtype: 'form',
- reference: 'formPanel',
- bodyPadding: 10,
- border: false,
- layout: 'hbox',
- items: [
- {
- xtype: 'container',
- flex: 1,
- items: [
- {
- xtype: 'displayfield',
- name: 'source',
- fieldLabel: gettext('Source node'),
- bind: {
- value: '{nodename}',
- },
- },
- {
- xtype: 'displayfield',
- reference: 'migrationMode',
- fieldLabel: gettext('Mode'),
- bind: {
- value: '{setMigrationMode}',
- },
- },
- ],
- },
- {
- xtype: 'container',
- flex: 1,
- items: [
- {
- xtype: 'pveNodeSelector',
- reference: 'pveNodeSelector',
- name: 'target',
- fieldLabel: gettext('Target node'),
- allowBlank: false,
- disallowedNodes: undefined,
- onlineValidator: true,
- listeners: {
- change: 'onTargetChange',
- },
- },
- {
- xtype: 'pveStorageSelector',
- reference: 'pveDiskStorageSelector',
- name: 'targetstorage',
- fieldLabel: gettext('Target storage'),
- storageContent: 'images',
- allowBlank: true,
- autoSelect: false,
- emptyText: gettext('Current layout'),
- bind: {
- hidden: '{setStorageselectorHidden}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'overwriteLocalResourceCheck',
- fieldLabel: gettext('Force'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Overwrite local resources unavailable check'),
- },
- bind: {
- hidden: '{setLocalResourceCheckboxHidden}',
- value: '{migration.overwriteLocalResourceCheck}',
- },
- listeners: {
- change: {
- fn: 'checkMigratePreconditions',
- extraArg: true,
- },
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'withConntrackState',
- // TRANSLATORS: See https://www.kernel.org/doc/html/next/networking/netlink_spec/conntrack.html
- fieldLabel: gettext('Conntrack state'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Enables live migration of conntrack entries for this VM.',
- ),
- },
- bind: {
- hidden: '{conntrackStateCheckboxHidden}',
- value: '{migration.withConntrackState}',
- },
- listeners: {
- change: {
- fn: 'checkMigratePreconditions',
- extraArg: true,
- },
- },
- },
- ],
- },
- ],
- },
- {
- xtype: 'gridpanel',
- reference: 'preconditionGrid',
- selectable: false,
- flex: 1,
- columns: [
- {
- text: '',
- dataIndex: 'severity',
- renderer: function (v) {
- switch (v) {
- case 'warning':
- return ' ';
- case 'error':
- return ' ';
- case 'info':
- return ' ';
- default:
- return v;
- }
- },
- width: 35,
- },
- {
- text: 'Info',
- dataIndex: 'text',
- cellWrap: true,
- flex: 1,
- },
- ],
- bind: {
- hidden: '{!migration.preconditions.length}',
- store: {
- fields: ['severity', 'text'],
- data: '{migration.preconditions}',
- sorters: 'text',
- },
- },
- },
- ],
- buttons: [
- {
- xtype: 'proxmoxHelpButton',
- reference: 'proxmoxHelpButton',
- onlineHelp: 'pct_migration',
- listenToGlobalEvent: false,
- hidden: false,
- },
- '->',
- {
- xtype: 'button',
- reference: 'submitButton',
- text: gettext('Migrate'),
- handler: 'startMigration',
- bind: {
- disabled: '{!migration.possible}',
- },
- },
- ],
-});
-Ext.define('pve-prune-list', {
- extend: 'Ext.data.Model',
- fields: [
- 'type',
- 'vmid',
- {
- name: 'ctime',
- type: 'date',
- dateFormat: 'timestamp',
- },
- ],
-});
-
-Ext.define('PVE.PruneInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pvePruneInputPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onGetValues: function (values) {
- let me = this;
-
- // the API expects a single prune-backups property string
- let pruneBackups = PVE.Parser.printPropertyString(values);
- values = {
- 'prune-backups': pruneBackups,
- type: me.backup_type,
- vmid: me.backup_id,
- };
-
- return values;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- if (!view.url) {
- throw 'no url specified';
- }
- if (!view.backup_type) {
- throw 'no backup_type specified';
- }
- if (!view.backup_id) {
- throw 'no backup_id specified';
- }
-
- this.reload(); // initial load
- },
-
- reload: function () {
- let view = this.getView();
-
- // helper to allow showing why a backup is kept
- let addKeepReasons = function (backups, params) {
- const rules = [
- 'keep-last',
- 'keep-hourly',
- 'keep-daily',
- 'keep-weekly',
- 'keep-monthly',
- 'keep-yearly',
- 'keep-all', // when all keep options are not set
- ];
- let counter = {};
-
- backups.sort((a, b) => b.ctime - a.ctime);
-
- let ruleIndex = -1;
- let nextRule = function () {
- let rule;
- do {
- ruleIndex++;
- rule = rules[ruleIndex];
- } while (!params[rule] && rule !== 'keep-all');
- counter[rule] = 0;
- return rule;
- };
-
- let rule = nextRule();
- for (let backup of backups) {
- if (backup.mark === 'keep') {
- counter[rule]++;
- if (rule !== 'keep-all') {
- backup.keepReason = rule + ': ' + counter[rule];
- if (counter[rule] >= params[rule]) {
- rule = nextRule();
- }
- } else {
- backup.keepReason = rule;
- }
- }
- }
- };
-
- let params = view.getValues();
- let keepParams = PVE.Parser.parsePropertyString(params['prune-backups']);
-
- Proxmox.Utils.API2Request({
- url: view.url,
- method: 'GET',
- params: params,
- callback: function () {
- // for easy breakpoint setting
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response, options) {
- var data = response.result.data;
- addKeepReasons(data, keepParams);
- view.pruneStore.setData(data);
- },
- });
- },
-
- control: {
- field: { change: 'reload' },
- },
- },
-
- column1: [
- {
- xtype: 'pmxPruneKeepField',
- name: 'keep-last',
- fieldLabel: gettext('keep-last'),
- },
- {
- xtype: 'pmxPruneKeepField',
- name: 'keep-hourly',
- fieldLabel: gettext('keep-hourly'),
- },
- {
- xtype: 'pmxPruneKeepField',
- name: 'keep-daily',
- fieldLabel: gettext('keep-daily'),
- },
- {
- xtype: 'pmxPruneKeepField',
- name: 'keep-weekly',
- fieldLabel: gettext('keep-weekly'),
- },
- {
- xtype: 'pmxPruneKeepField',
- name: 'keep-monthly',
- fieldLabel: gettext('keep-monthly'),
- },
- {
- xtype: 'pmxPruneKeepField',
- name: 'keep-yearly',
- fieldLabel: gettext('keep-yearly'),
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- me.pruneStore = Ext.create('Ext.data.Store', {
- model: 'pve-prune-list',
- sorters: { property: 'ctime', direction: 'DESC' },
- });
-
- me.column2 = [
- {
- xtype: 'grid',
- height: 200,
- store: me.pruneStore,
- columns: [
- {
- header: gettext('Backup Time'),
- sortable: true,
- dataIndex: 'ctime',
- renderer: function (value, metaData, record) {
- let text = Ext.Date.format(value, 'Y-m-d H:i:s');
- if (record.data.mark === 'remove') {
- return (
- '' + text + '
'
- );
- } else {
- return text;
- }
- },
- flex: 1,
- },
- {
- text: 'Keep (reason)',
- dataIndex: 'mark',
- renderer: function (value, metaData, record) {
- if (record.data.mark === 'keep') {
- return 'true (' + record.data.keepReason + ')';
- } else if (record.data.mark === 'protected') {
- return 'true (protected)';
- } else if (record.data.mark === 'renamed') {
- return 'true (renamed)';
- } else {
- return 'false';
- }
- },
- flex: 1,
- },
- ],
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.window.Prune', {
- extend: 'Proxmox.window.Edit',
-
- method: 'DELETE',
- submitText: gettext('Prune'),
-
- fieldDefaults: { labelWidth: 130 },
-
- isCreate: true,
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no nodename specified';
- }
- if (!me.storage) {
- throw 'no storage specified';
- }
- if (!me.backup_type) {
- throw 'no backup_type specified';
- }
- if (me.backup_type !== 'qemu' && me.backup_type !== 'lxc') {
- throw 'unknown backup type: ' + me.backup_type;
- }
- if (!me.backup_id) {
- throw 'no backup_id specified';
- }
-
- let title = Ext.String.format(
- gettext("Prune Backups for '{0}' on Storage '{1}'"),
- me.backup_type + '/' + me.backup_id,
- me.storage,
- );
-
- Ext.apply(me, {
- url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + '/prunebackups',
- title: title,
- items: [
- {
- xtype: 'pvePruneInputPanel',
- url:
- '/api2/extjs/nodes/' +
- me.nodename +
- '/storage/' +
- me.storage +
- '/prunebackups',
- backup_type: me.backup_type,
- backup_id: me.backup_id,
- storage: me.storage,
- },
- ],
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.window.Restore', {
- extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit?
-
- resizable: false,
- width: 500,
- modal: true,
- layout: 'auto',
- border: false,
-
- controller: {
- xclass: 'Ext.app.ViewController',
- control: {
- '#liveRestore': {
- change: function (el, newVal) {
- let liveWarning = this.lookupReference('liveWarning');
- liveWarning.setHidden(!newVal);
- let start = this.lookupReference('start');
- start.setDisabled(newVal);
- },
- },
- form: {
- validitychange: function (f, valid) {
- this.lookupReference('doRestoreBtn').setDisabled(!valid);
- },
- },
- },
-
- doRestore: function () {
- let me = this;
- let view = me.getView();
-
- let values = view.down('form').getForm().getValues();
-
- let params = {
- vmid: view.vmid || values.vmid,
- force: view.vmid ? 1 : 0,
- };
- if (values.unique) {
- params.unique = 1;
- }
- if (values.start && !values['live-restore']) {
- params.start = 1;
- }
- if (values['ha-managed']) {
- params['ha-managed'] = 1;
- }
- if (values['live-restore']) {
- params['live-restore'] = 1;
- }
- if (values.storage) {
- params.storage = values.storage;
- }
-
- ['bwlimit', 'cores', 'name', 'memory', 'sockets'].forEach((opt) => {
- if ((values[opt] ?? '') !== '') {
- params[opt] = values[opt];
- }
- });
-
- if (params.name && view.vmtype === 'lxc') {
- params.hostname = params.name;
- delete params.name;
- }
-
- let taskDescription;
- if (view.vmtype === 'lxc') {
- params.ostemplate = view.volid;
- params.restore = 1;
- if (values.unprivileged !== 'keep') {
- params.unprivileged = values.unprivileged;
- }
- taskDescription = Proxmox.Utils.format_task_description('vzrestore', params.vmid);
- } else if (view.vmtype === 'qemu') {
- params.archive = view.volid;
- taskDescription = Proxmox.Utils.format_task_description('qmrestore', params.vmid);
- } else {
- throw 'unknown VM type';
- }
- let confirmMsg = Ext.htmlEncode(taskDescription);
-
- let executeRestore = () => {
- Proxmox.Utils.API2Request({
- url: `/nodes/${view.nodename}/${view.vmtype}`,
- params: params,
- method: 'POST',
- waitMsgTarget: view,
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: function (response, options) {
- Ext.create('Proxmox.window.TaskViewer', {
- autoShow: true,
- upid: response.result.data,
- });
- view.close();
- },
- });
- };
-
- if (view.vmid) {
- if (view.vmtype === 'lxc') {
- confirmMsg += `. ${gettext('This will permanently erase current CT data.')}`;
- confirmMsg += ` ${gettext('Mount point volumes are also erased.')}`;
- } else {
- confirmMsg += `. ${gettext('This will permanently erase current VM data.')}`;
- }
- Ext.Msg.confirm(gettext('Confirm'), confirmMsg, function (btn) {
- if (btn === 'yes') {
- executeRestore();
- }
- });
- } else {
- executeRestore();
- }
- },
-
- afterRender: function () {
- let view = this.getView();
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${view.nodename}/vzdump/extractconfig`,
- method: 'GET',
- waitMsgTarget: view,
- params: {
- volume: view.volid,
- },
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- success: function (response, options) {
- let allStoragesAvailable = true;
-
- response.result.data.split('\n').forEach((line) => {
- let [_, key, value] = line.match(/^([^:]+):\s*(\S+)\s*$/) ?? [];
-
- if (!key) {
- return;
- }
-
- if (key === '#qmdump#map') {
- let match = value.match(/^(\S+):(\S+):(\S*):(\S*):$/) ?? [];
- // if a /dev/XYZ disk was backed up, there is no storage hint
- allStoragesAvailable &&=
- !!match[3] &&
- !!PVE.data.ResourceStore.getById(
- `storage/${view.nodename}/${match[3]}`,
- );
- } else if (key === 'name' || key === 'hostname') {
- view.lookupReference('nameField').setEmptyText(value);
- } else if (key === 'memory' || key === 'cores' || key === 'sockets') {
- view.lookupReference(`${key}Field`).setEmptyText(value);
- }
- });
-
- if (!allStoragesAvailable) {
- let storagesel = view.down('pveStorageSelector[name=storage]');
- storagesel.allowBlank = false;
- storagesel.setEmptyText('');
- }
- },
- });
- },
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
- if (!me.volid) {
- throw 'no volume ID specified';
- }
- if (!me.vmtype) {
- throw 'no vmtype specified';
- }
-
- let storagesel = Ext.create('PVE.form.StorageSelector', {
- nodename: me.nodename,
- name: 'storage',
- value: '',
- fieldLabel: gettext('Storage'),
- storageContent: me.vmtype === 'lxc' ? 'rootdir' : 'images',
- // when restoring a container without specifying a storage, the backend defaults
- // to 'local', which is unintuitive and 'rootdir' might not even be allowed on it
- allowBlank: me.vmtype !== 'lxc',
- emptyText: me.vmtype === 'lxc' ? '' : gettext('From backup configuration'),
- autoSelect: me.vmtype === 'lxc',
- });
-
- let items = [
- {
- xtype: 'displayfield',
- value: me.volidText || me.volid,
- fieldLabel: gettext('Source'),
- },
- storagesel,
- {
- xtype: 'pmxDisplayEditField',
- name: 'vmid',
- fieldLabel: me.vmtype === 'lxc' ? 'CT' : 'VM',
- value: me.vmid,
- editable: !me.vmid,
- editConfig: {
- xtype: 'pveGuestIDSelector',
- guestType: me.vmtype,
- loadNextFreeID: true,
- validateExists: false,
- },
- },
- {
- xtype: 'pveBandwidthField',
- name: 'bwlimit',
- backendUnit: 'KiB',
- allowZero: true,
- fieldLabel: gettext('Bandwidth Limit'),
- emptyText: gettext('Defaults to target storage restore limit'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext("Use '0' to disable all bandwidth limits."),
- },
- },
- {
- xtype: 'fieldcontainer',
- layout: 'hbox',
- items: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'unique',
- fieldLabel: gettext('Unique'),
- flex: 1,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Autogenerate unique properties, e.g., MAC addresses',
- ),
- },
- checked: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'start',
- reference: 'start',
- flex: 1,
- fieldLabel: gettext('Start after restore'),
- labelWidth: 105,
- checked: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'ha-managed',
- reference: 'ha-managed',
- flex: 1,
- fieldLabel: gettext('Add to HA'),
- labelWidth: 120,
- checked: false,
- },
- ],
- },
- ];
-
- if (me.vmtype === 'lxc') {
- items.push({
- xtype: 'radiogroup',
- fieldLabel: gettext('Privilege Level'),
- reference: 'noVNCScalingGroup',
- height: '15px', // renders faster with value assigned
- layout: {
- type: 'hbox',
- algin: 'stretch',
- },
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Choose if you want to keep or override the privilege level of the restored Container.',
- ),
- },
- items: [
- {
- xtype: 'radiofield',
- name: 'unprivileged',
- inputValue: 'keep',
- boxLabel: gettext('From Backup'),
- flex: 1,
- checked: true,
- },
- {
- xtype: 'radiofield',
- name: 'unprivileged',
- inputValue: '1',
- boxLabel: gettext('Unprivileged'),
- flex: 1,
- },
- {
- xtype: 'radiofield',
- name: 'unprivileged',
- inputValue: '0',
- boxLabel: gettext('Privileged'),
- flex: 1,
- //margin: '0 0 0 10',
- },
- ],
- });
- } else if (me.vmtype === 'qemu') {
- items.push(
- {
- xtype: 'proxmoxcheckbox',
- name: 'live-restore',
- itemId: 'liveRestore',
- flex: 1,
- fieldLabel: gettext('Live restore'),
- checked: false,
- hidden: !me.isPBS,
- },
- {
- xtype: 'displayfield',
- reference: 'liveWarning',
- // TODO: Remove once more tested/stable?
- value: gettext(
- 'Note: If anything goes wrong during the live-restore, new data written by the VM may be lost.',
- ),
- userCls: 'pmx-hint',
- hidden: true,
- },
- );
- }
-
- items.push({
- xtype: 'fieldset',
- title: `${gettext('Override Settings')}:`,
- layout: 'hbox',
- defaults: {
- border: false,
- layout: 'anchor',
- flex: 1,
- },
- items: [
- {
- padding: '0 10 0 0',
- items: [
- {
- xtype: 'textfield',
- fieldLabel: me.vmtype === 'lxc' ? gettext('Hostname') : gettext('Name'),
- name: 'name',
- vtype: 'DnsName',
- reference: 'nameField',
- allowBlank: true,
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: gettext('Cores'),
- name: 'cores',
- reference: 'coresField',
- minValue: 1,
- maxValue: 128,
- allowBlank: true,
- },
- ],
- },
- {
- padding: '0 0 0 10',
- items: [
- {
- xtype: 'pveMemoryField',
- fieldLabel: gettext('Memory'),
- name: 'memory',
- reference: 'memoryField',
- value: '',
- allowBlank: true,
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: gettext('Sockets'),
- name: 'sockets',
- reference: 'socketsField',
- minValue: 1,
- maxValue: 4,
- allowBlank: true,
- hidden: me.vmtype !== 'qemu',
- disabled: me.vmtype !== 'qemu',
- },
- ],
- },
- ],
- });
-
- let title = gettext('Restore') + ': ' + (me.vmtype === 'lxc' ? 'CT' : 'VM');
- if (me.vmid) {
- let formattedGuestIdentifier = PVE.Utils.getFormattedGuestIdentifier(
- me.vmid,
- me.vmname,
- );
- title = `${gettext('Overwrite')} ${title} ${formattedGuestIdentifier}`;
- }
-
- Ext.apply(me, {
- title: title,
- items: [
- {
- xtype: 'form',
- bodyPadding: 10,
- border: false,
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%',
- },
- items: items,
- },
- ],
- buttons: [
- {
- text: gettext('Restore'),
- reference: 'doRestoreBtn',
- handler: 'doRestore',
- },
- ],
- });
-
- me.callParent();
- },
-});
-/*
- * SafeDestroy window with additional checkboxes for removing guests
- */
-Ext.define('PVE.window.SafeDestroyGuest', {
- extend: 'Proxmox.window.SafeDestroy',
- alias: 'widget.pveSafeDestroyGuest',
-
- additionalItems: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'purge',
- reference: 'purgeCheckbox',
- boxLabel: gettext('Purge from job configurations'),
- checked: false,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Remove from replication, HA and backup jobs'),
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'destroyUnreferenced',
- reference: 'destroyUnreferencedCheckbox',
- boxLabel: gettext('Destroy unreferenced disks owned by guest'),
- checked: false,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Scan all enabled storages for unreferenced disks and delete them.',
- ),
- },
- },
- ],
-
- note: gettext('Referenced disks will always be destroyed.'),
-
- getParams: function () {
- let me = this;
-
- const purgeCheckbox = me.lookupReference('purgeCheckbox');
- me.params.purge = purgeCheckbox.checked ? 1 : 0;
-
- const destroyUnreferencedCheckbox = me.lookupReference('destroyUnreferencedCheckbox');
- me.params['destroy-unreferenced-disks'] = destroyUnreferencedCheckbox.checked ? 1 : 0;
-
- return me.callParent();
- },
-});
-/*
- * SafeDestroy window with additional checkboxes for removing a storage on the disk level.
- */
-Ext.define('PVE.window.SafeDestroyStorage', {
- extend: 'Proxmox.window.SafeDestroy',
- alias: 'widget.pveSafeDestroyStorage',
-
- showProgress: true,
-
- additionalItems: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'wipeDisks',
- reference: 'wipeDisksCheckbox',
- boxLabel: gettext('Cleanup Disks'),
- checked: true,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Wipe labels and other left-overs'),
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'cleanupConfig',
- reference: 'cleanupConfigCheckbox',
- boxLabel: gettext('Cleanup Storage Configuration'),
- checked: true,
- },
- ],
-
- getParams: function () {
- let me = this;
-
- me.params['cleanup-disks'] = me.lookupReference('wipeDisksCheckbox').checked ? 1 : 0;
- me.params['cleanup-config'] = me.lookupReference('cleanupConfigCheckbox').checked ? 1 : 0;
-
- return me.callParent();
- },
-});
-Ext.define('PVE.window.Settings', {
- extend: 'Ext.window.Window',
-
- width: '800px',
- title: gettext('My Settings'),
- iconCls: 'fa fa-gear',
- modal: true,
- bodyPadding: 10,
- resizable: false,
-
- buttons: [
- {
- xtype: 'proxmoxHelpButton',
- onlineHelp: 'gui_my_settings',
- hidden: false,
- },
- '->',
- {
- text: gettext('Close'),
- handler: function () {
- this.up('window').close();
- },
- },
- ],
-
- layout: 'hbox',
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- var me = this;
- var sp = Ext.state.Manager.getProvider();
-
- var username = sp.get('login-username') || Proxmox.Utils.noneText;
- me.lookupReference('savedUserName').setValue(Ext.String.htmlEncode(username));
- var vncMode = sp.get('novnc-scaling') || 'auto';
- me.lookupReference('noVNCScalingGroup').setValue({ noVNCScalingField: vncMode });
-
- let summarycolumns = sp.get('summarycolumns', 'auto');
- me.lookup('summarycolumns').setValue(summarycolumns);
-
- me.lookup('guestNotesCollapse').setValue(sp.get('guest-notes-collapse', 'never'));
- me.lookup('editNotesOnDoubleClick').setValue(
- sp.get('edit-notes-on-double-click', false),
- );
-
- var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
- settings.forEach(function (setting) {
- var val = localStorage.getItem('pve-xterm-' + setting);
- if (val !== undefined && val !== null) {
- let field = me.lookup(setting);
- field.setValue(val);
- field.resetOriginalValue();
- }
- });
- },
-
- set_button_status: function () {
- let me = this;
- let form = me.lookup('xtermform');
-
- let valid = form.isValid(),
- dirty = form.isDirty();
- let hasValues = Object.values(form.getValues()).some((v) => !!v);
-
- me.lookup('xtermsave').setDisabled(!dirty || !valid);
- me.lookup('xtermreset').setDisabled(!hasValues);
- },
-
- control: {
- '#xtermjs form': {
- dirtychange: 'set_button_status',
- validitychange: 'set_button_status',
- },
- '#xtermjs button': {
- click: function (button) {
- var me = this;
- var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
- settings.forEach(function (setting) {
- var field = me.lookup(setting);
- if (button.reference === 'xtermsave') {
- let value = field.getValue();
- if (value) {
- localStorage.setItem('pve-xterm-' + setting, value);
- } else {
- localStorage.removeItem('pve-xterm-' + setting);
- }
- } else if (button.reference === 'xtermreset') {
- field.setValue(undefined);
- localStorage.removeItem('pve-xterm-' + setting);
- }
- field.resetOriginalValue();
- });
- me.set_button_status();
- },
- },
- 'button[name=reset]': {
- click: function () {
- let blacklist = ['GuiCap', 'login-username', 'dash-storages'];
- let sp = Ext.state.Manager.getProvider();
- for (const state of Object.keys(sp.state)) {
- if (!blacklist.includes(state)) {
- sp.clear(state);
- }
- }
- window.location.reload();
- },
- },
- 'button[name=clear-username]': {
- click: function () {
- let me = this;
- me.lookupReference('savedUserName').setValue(Proxmox.Utils.noneText);
- Ext.state.Manager.getProvider().clear('login-username');
- },
- },
- 'grid[reference=dashboard-storages]': {
- selectionchange: function (grid, selected) {
- var _me = this;
- var sp = Ext.state.Manager.getProvider();
-
- // saves the selected storageids as "id1,id2,id3,..." or clears the variable
- if (selected.length > 0) {
- sp.set('dash-storages', Ext.Array.pluck(selected, 'id').join(','));
- } else {
- sp.clear('dash-storages');
- }
- },
- afterrender: function (grid) {
- let store = grid.getStore();
- let storages = Ext.state.Manager.getProvider().get('dash-storages') || '';
-
- let items = [];
- storages.split(',').forEach((storage) => {
- if (storage !== '') {
- // we have to get the records to be able to select them
- let item = store.getById(storage);
- if (item) {
- items.push(item);
- }
- }
- });
- grid.suspendEvent('selectionchange');
- grid.getSelectionModel().select(items);
- grid.resumeEvent('selectionchange');
- },
- },
- 'field[reference=summarycolumns]': {
- change: (el, newValue) =>
- Ext.state.Manager.getProvider().set('summarycolumns', newValue),
- },
- 'field[reference=guestNotesCollapse]': {
- change: (e, v) => Ext.state.Manager.getProvider().set('guest-notes-collapse', v),
- },
- 'field[reference=editNotesOnDoubleClick]': {
- change: (e, v) =>
- Ext.state.Manager.getProvider().set('edit-notes-on-double-click', v),
- },
- },
- },
-
- items: [
- {
- xtype: 'fieldset',
- flex: 1,
- title: gettext('Webinterface Settings'),
- margin: '5',
- layout: {
- type: 'vbox',
- align: 'left',
- },
- defaults: {
- width: '100%',
- margin: '0 0 10 0',
- },
- items: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Dashboard Storages'),
- labelAlign: 'left',
- labelWidth: '50%',
- },
- {
- xtype: 'grid',
- maxHeight: 150,
- reference: 'dashboard-storages',
- selModel: {
- selType: 'checkboxmodel',
- },
- columns: [
- {
- header: gettext('Name'),
- dataIndex: 'storage',
- flex: 1,
- },
- {
- header: gettext('Node'),
- dataIndex: 'node',
- flex: 1,
- },
- ],
- store: {
- type: 'diff',
- field: ['type', 'storage', 'id', 'node'],
- rstore: PVE.data.ResourceStore,
- filters: [
- {
- property: 'type',
- value: 'storage',
- },
- ],
- sorters: ['node', 'storage'],
- },
- },
- {
- xtype: 'box',
- autoEl: { tag: 'hr' },
- },
- {
- xtype: 'container',
- layout: 'hbox',
- items: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Saved User Name') + ':',
- labelWidth: 150,
- stateId: 'login-username',
- reference: 'savedUserName',
- flex: 1,
- value: '',
- },
- {
- xtype: 'button',
- cls: 'x-btn-default-toolbar-small proxmox-inline-button',
- text: gettext('Reset'),
- name: 'clear-username',
- },
- ],
- },
- {
- xtype: 'box',
- autoEl: { tag: 'hr' },
- },
- {
- xtype: 'container',
- layout: 'hbox',
- items: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Layout') + ':',
- flex: 1,
- },
- {
- xtype: 'button',
- cls: 'x-btn-default-toolbar-small proxmox-inline-button',
- text: gettext('Reset'),
- tooltip: gettext(
- 'Reset all layout changes (for example, column widths)',
- ),
- name: 'reset',
- },
- ],
- },
- {
- xtype: 'box',
- autoEl: { tag: 'hr' },
- },
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('Summary columns') + ':',
- labelWidth: 125,
- stateId: 'summarycolumns',
- reference: 'summarycolumns',
- comboItems: [
- ['auto', 'auto'],
- ['1', '1'],
- ['2', '2'],
- ['3', '3'],
- ],
- },
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('Guest Notes') + ':',
- labelWidth: 125,
- stateId: 'guest-notes-collapse',
- reference: 'guestNotesCollapse',
- comboItems: [
- ['never', 'Show by default'],
- ['always', 'Collapse by default'],
- ['auto', 'auto (Collapse if empty)'],
- ],
- },
- {
- xtype: 'checkbox',
- fieldLabel: gettext('Notes'),
- labelWidth: 125,
- boxLabel: gettext('Open editor on double-click'),
- reference: 'editNotesOnDoubleClick',
- inputValue: true,
- uncheckedValue: false,
- },
- ],
- },
- {
- xtype: 'container',
- layout: 'vbox',
- flex: 1,
- margin: '5',
- defaults: {
- width: '100%',
- // right margin ensures that the right border of the fieldsets
- // is shown
- margin: '0 2 10 0',
- },
- items: [
- {
- xtype: 'fieldset',
- itemId: 'xtermjs',
- title: gettext('xterm.js Settings'),
- items: [
- {
- xtype: 'form',
- reference: 'xtermform',
- border: false,
- layout: {
- type: 'vbox',
- algin: 'left',
- },
- defaults: {
- width: '100%',
- margin: '0 0 10 0',
- },
- items: [
- {
- xtype: 'textfield',
- name: 'fontFamily',
- reference: 'fontFamily',
- emptyText: Proxmox.Utils.defaultText,
- fieldLabel: gettext('Font-Family'),
- },
- {
- xtype: 'proxmoxintegerfield',
- emptyText: Proxmox.Utils.defaultText,
- name: 'fontSize',
- reference: 'fontSize',
- minValue: 1,
- fieldLabel: gettext('Font-Size'),
- },
- {
- xtype: 'numberfield',
- name: 'letterSpacing',
- reference: 'letterSpacing',
- emptyText: Proxmox.Utils.defaultText,
- fieldLabel: gettext('Letter Spacing'),
- },
- {
- xtype: 'numberfield',
- name: 'lineHeight',
- minValue: 0.1,
- reference: 'lineHeight',
- emptyText: Proxmox.Utils.defaultText,
- fieldLabel: gettext('Line Height'),
- },
- {
- xtype: 'container',
- layout: {
- type: 'hbox',
- pack: 'end',
- },
- defaults: {
- margin: '0 0 0 5',
- },
- items: [
- {
- xtype: 'button',
- reference: 'xtermreset',
- disabled: true,
- text: gettext('Reset'),
- },
- {
- xtype: 'button',
- reference: 'xtermsave',
- disabled: true,
- text: gettext('Save'),
- },
- ],
- },
- ],
- },
- ],
- },
- {
- xtype: 'fieldset',
- title: gettext('noVNC Settings'),
- items: [
- {
- xtype: 'radiogroup',
- fieldLabel: gettext('Scaling mode'),
- reference: 'noVNCScalingGroup',
- height: '15px', // renders faster with value assigned
- layout: {
- type: 'hbox',
- },
- items: [
- {
- xtype: 'radiofield',
- name: 'noVNCScalingField',
- inputValue: 'auto',
- boxLabel: 'Auto',
- },
- {
- xtype: 'radiofield',
- name: 'noVNCScalingField',
- inputValue: 'scale',
- boxLabel: 'Local Scaling',
- margin: '0 0 0 10',
- },
- {
- xtype: 'radiofield',
- name: 'noVNCScalingField',
- inputValue: 'off',
- boxLabel: 'Off',
- margin: '0 0 0 10',
- },
- ],
- listeners: {
- change: function (el, { noVNCScalingField }) {
- let provider = Ext.state.Manager.getProvider();
- if (noVNCScalingField === 'auto') {
- provider.clear('novnc-scaling');
- } else {
- provider.set('novnc-scaling', noVNCScalingField);
- }
- },
- },
- },
- ],
- },
- ],
- },
- ],
-});
-Ext.define('PVE.window.Snapshot', {
- extend: 'Proxmox.window.Edit',
-
- viewModel: {
- data: {
- type: undefined,
- isCreate: undefined,
- running: false,
- guestAgentEnabled: false,
- },
- formulas: {
- runningWithoutGuestAgent: (get) =>
- get('type') === 'qemu' && get('running') && !get('guestAgentEnabled'),
- shouldWarnAboutFS: (get) =>
- get('isCreate') && get('runningWithoutGuestAgent') && get('!vmstate.checked'),
- },
- },
-
- onGetValues: function (values) {
- let me = this;
-
- if (me.type === 'lxc') {
- delete values.vmstate;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
- var vm = me.getViewModel();
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.vmid) {
- throw 'no VM ID specified';
- }
-
- if (!me.type) {
- throw 'no type specified';
- }
-
- vm.set('type', me.type);
- vm.set('running', me.running);
- vm.set('isCreate', me.isCreate);
-
- if (me.type === 'qemu' && me.isCreate) {
- Proxmox.Utils.API2Request({
- url: `/nodes/${me.nodename}/${me.type}/${me.vmid}/config`,
- params: { current: '1' },
- method: 'GET',
- success: function (response, options) {
- let res = response.result.data;
- let enabled = PVE.Parser.parsePropertyString(res.agent, 'enabled');
- vm.set('guestAgentEnabled', !!PVE.Parser.parseBoolean(enabled.enabled));
- },
- });
- }
-
- me.items = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'snapname',
- value: me.snapname,
- fieldLabel: gettext('Name'),
- vtype: 'ConfigId',
- allowBlank: false,
- },
- {
- xtype: 'displayfield',
- hidden: me.isCreate,
- disabled: me.isCreate,
- name: 'snaptime',
- renderer: PVE.Utils.render_timestamp_human_readable,
- fieldLabel: gettext('Timestamp'),
- },
- {
- xtype: 'proxmoxcheckbox',
- hidden: me.type !== 'qemu' || !me.isCreate || !me.running,
- disabled: me.type !== 'qemu' || !me.isCreate || !me.running,
- name: 'vmstate',
- reference: 'vmstate',
- uncheckedValue: 0,
- defaultValue: 0,
- checked: 1,
- fieldLabel: gettext('Include RAM'),
- },
- {
- xtype: 'textareafield',
- grow: true,
- editable: !me.viewonly,
- name: 'description',
- fieldLabel: gettext('Description'),
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- name: 'fswarning',
- hidden: true,
- value: gettext(
- 'It is recommended to either include the RAM or use the QEMU Guest Agent when taking a snapshot of a running VM to avoid inconsistencies.',
- ),
- bind: {
- hidden: '{!shouldWarnAboutFS}',
- },
- },
- {
- title: gettext('Settings'),
- hidden: me.isCreate,
- xtype: 'grid',
- itemId: 'summary',
- border: true,
- height: 200,
- store: {
- model: 'KeyValue',
- sorters: [
- {
- property: 'key',
- direction: 'ASC',
- },
- ],
- },
- columns: [
- {
- header: gettext('Key'),
- width: 150,
- dataIndex: 'key',
- },
- {
- header: gettext('Value'),
- flex: 1,
- dataIndex: 'value',
- },
- ],
- },
- ];
-
- me.url = `/nodes/${me.nodename}/${me.type}/${me.vmid}/snapshot`;
-
- let subject;
- if (me.isCreate) {
- let guestTypeStr = me.type === 'qemu' ? 'VM' : 'CT';
- let formattedGuestIdentifier = PVE.Utils.getFormattedGuestIdentifier(
- me.vmid,
- me.vmname,
- );
- subject = `${guestTypeStr} ${formattedGuestIdentifier} ${gettext('Snapshot')}`;
- me.method = 'POST';
- me.showTaskViewer = true;
- } else {
- subject = `${gettext('Snapshot')} ${me.snapname}`;
- me.url += `/${me.snapname}/config`;
- }
-
- Ext.apply(me, {
- subject: subject,
- width: me.isCreate ? 450 : 620,
- height: me.isCreate ? undefined : 420,
- });
-
- me.callParent();
-
- if (!me.snapname) {
- return;
- }
-
- me.load({
- success: function (response) {
- let kvarray = [];
- Ext.Object.each(response.result.data, function (key, value) {
- if (key === 'description' || key === 'snaptime') {
- return;
- }
- kvarray.push({ key: key, value: value });
- });
-
- let summarystore = me.down('#summary').getStore();
- summarystore.suspendEvents();
- summarystore.add(kvarray);
- summarystore.sort();
- summarystore.resumeEvents();
- summarystore.fireEvent('refresh', summarystore);
-
- me.setValues(response.result.data);
- },
- });
- },
-});
-Ext.define('PVE.panel.StartupInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- onlineHelp: 'qm_startup_and_shutdown',
-
- onGetValues: function (values) {
- var _me = this;
-
- var res = PVE.Parser.printStartup(values);
-
- if (res === undefined || res === '') {
- return { delete: 'startup' };
- }
-
- return { startup: res };
- },
-
- setStartup: function (value) {
- var me = this;
-
- var startup = PVE.Parser.parseStartup(value);
- if (startup) {
- me.setValues(startup);
- }
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: 'textfield',
- name: 'order',
- defaultValue: '',
- emptyText: 'any',
- fieldLabel: gettext('Start/Shutdown order'),
- },
- {
- xtype: 'textfield',
- name: 'up',
- defaultValue: '',
- emptyText: 'default',
- fieldLabel: gettext('Startup delay'),
- },
- {
- xtype: 'textfield',
- name: 'down',
- defaultValue: '',
- emptyText: 'default',
- fieldLabel: gettext('Shutdown timeout'),
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.window.StartupEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveWindowStartupEdit',
- onlineHelp: undefined,
-
- initComponent: function () {
- let me = this;
-
- let ipanelConfig = me.onlineHelp ? { onlineHelp: me.onlineHelp } : {};
- let ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig);
-
- Ext.applyIf(me, {
- subject: gettext('Start/Shutdown order'),
- fieldDefaults: {
- labelWidth: 120,
- },
- items: [ipanel],
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- me.vmconfig = response.result.data;
- ipanel.setStartup(me.vmconfig.startup);
- },
- });
- },
-});
-Ext.define('PVE.window.DownloadUrlToStorage', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveStorageDownloadUrl',
- mixins: ['Proxmox.Mixin.CBind'],
-
- isCreate: true,
-
- method: 'POST',
-
- showTaskViewer: true,
-
- title: gettext('Download from URL'),
- submitText: gettext('Download'),
-
- cbindData: function (initialConfig) {
- var me = this;
- return {
- nodename: me.nodename,
- storage: me.storage,
- content: me.content,
- };
- },
-
- cbind: {
- url: '/nodes/{nodename}/storage/{storage}/download-url',
- },
-
- viewModel: {
- data: {
- size: '-',
- mimetype: '-',
- enableQuery: true,
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- urlChange: function (field) {
- this.resetMetaInfo();
- this.setQueryEnabled();
- },
- setQueryEnabled: function () {
- this.getViewModel().set('enableQuery', true);
- },
- resetMetaInfo: function () {
- let vm = this.getViewModel();
- vm.set('size', '-');
- vm.set('mimetype', '-');
- },
-
- urlCheck: function (field) {
- let me = this;
- let view = me.getView();
-
- const queryParam = view.getValues();
-
- me.getViewModel().set('enableQuery', false);
- me.resetMetaInfo();
- let urlField = view.down('[name=url]');
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${view.nodename}/query-url-metadata`,
- method: 'GET',
- params: {
- url: queryParam.url,
- 'verify-certificates': queryParam['verify-certificates'],
- },
- waitMsgTarget: view,
- failure: (res) => {
- urlField.setValidation(res.result.message);
- urlField.validate();
- Ext.MessageBox.alert(gettext('Error'), res.htmlStatus);
- // re-enable so one can directly requery, e.g., if it was just a network hiccup
- me.setQueryEnabled();
- },
- success: function (res, opt) {
- urlField.setValidation();
- urlField.validate();
-
- let data = res.result.data;
-
- let filename = data.filename || '';
- let compression = '__default__';
- if (view.content === 'iso') {
- const matches = filename.match(/^(.+)\.(gz|lzo|zst|bz2)$/i);
- if (matches) {
- filename = matches[1];
- compression = matches[2].toLowerCase();
- }
- } else if (view.content === 'import') {
- if (filename.endsWith('.img')) {
- filename += '.raw';
- }
- }
-
- view.setValues({
- filename,
- compression,
- size:
- (data.size && Proxmox.Utils.format_size(data.size)) ||
- gettext('Unknown'),
- mimetype: data.mimetype || gettext('Unknown'),
- });
- },
- });
- },
-
- hashChange: function (field) {
- let checksum = Ext.getCmp('downloadUrlChecksum');
- if (field.getValue() === '__default__') {
- checksum.setDisabled(true);
- checksum.setValue('');
- checksum.allowBlank = true;
- } else {
- checksum.setDisabled(false);
- checksum.allowBlank = false;
- }
- },
- },
-
- items: [
- {
- xtype: 'inputpanel',
- border: false,
- onGetValues: function (values) {
- if (typeof values.checksum === 'string') {
- values.checksum = values.checksum.trim();
- }
- return values;
- },
- columnT: [
- {
- xtype: 'fieldcontainer',
- layout: 'hbox',
- fieldLabel: gettext('URL'),
- items: [
- {
- xtype: 'textfield',
- name: 'url',
- emptyText: gettext('Enter URL to download'),
- allowBlank: false,
- flex: 1,
- listeners: {
- change: 'urlChange',
- },
- },
- {
- xtype: 'button',
- name: 'check',
- text: gettext('Query URL'),
- margin: '0 0 0 5',
- bind: {
- disabled: '{!enableQuery}',
- },
- listeners: {
- click: 'urlCheck',
- },
- },
- ],
- },
- {
- xtype: 'textfield',
- name: 'filename',
- allowBlank: false,
- fieldLabel: gettext('File name'),
- emptyText: gettext('Please (re-)query URL to get meta information'),
- },
- ],
- column1: [
- {
- xtype: 'displayfield',
- name: 'size',
- fieldLabel: gettext('File size'),
- bind: {
- value: '{size}',
- },
- },
- ],
- column2: [
- {
- xtype: 'displayfield',
- name: 'mimetype',
- fieldLabel: gettext('MIME type'),
- bind: {
- value: '{mimetype}',
- },
- },
- ],
- advancedColumn1: [
- {
- xtype: 'pveHashAlgorithmSelector',
- name: 'checksum-algorithm',
- fieldLabel: gettext('Hash algorithm'),
- allowBlank: true,
- hasNoneOption: true,
- value: '__default__',
- listeners: {
- change: 'hashChange',
- },
- },
- {
- xtype: 'textfield',
- name: 'checksum',
- fieldLabel: gettext('Checksum'),
- allowBlank: true,
- disabled: true,
- emptyText: gettext('none'),
- id: 'downloadUrlChecksum',
- },
- ],
- advancedColumn2: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'verify-certificates',
- fieldLabel: gettext('Verify certificates'),
- uncheckedValue: 0,
- checked: true,
- listeners: {
- change: 'setQueryEnabled',
- },
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'compression',
- fieldLabel: gettext('Decompression algorithm'),
- allowBlank: true,
- hasNoneOption: true,
- deleteEmpty: false,
- value: '__default__',
- comboItems: [
- ['__default__', Proxmox.Utils.NoneText],
- ['lzo', 'LZO'],
- ['gz', 'GZIP'],
- ['zst', 'ZSTD'],
- ['bz2', 'BZIP2'],
- ],
- cbind: {
- hidden: (get) => get('content') !== 'iso',
- },
- },
- ],
- },
- {
- xtype: 'hiddenfield',
- name: 'content',
- cbind: {
- value: '{content}',
- },
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
- if (!me.storage) {
- throw 'no storage ID specified';
- }
- me.callParent();
- },
-});
-Ext.define('PVE.window.UploadToStorage', {
- extend: 'Ext.window.Window',
- alias: 'widget.pveStorageUpload',
- mixins: ['Proxmox.Mixin.CBind'],
-
- resizable: false,
- modal: true,
-
- title: gettext('Upload'),
-
- acceptedExtensions: {
- import: ['.ova', '.qcow2', '.raw', '.vmdk'],
- iso: ['.img', '.iso'],
- vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'],
- },
-
- // accepted for file selection, will be renamed to real extension
- extensionAliases: {
- import: {
- '.img': '.raw',
- },
- },
-
- cbindData: function (initialConfig) {
- const me = this;
- const ext = me.acceptedExtensions[me.content] || [];
-
- me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
-
- let fileSelectorExt = ext.concat(Object.keys(me.extensionAliases[me.content] ?? {}));
-
- return {
- extensions: fileSelectorExt.join(', '),
- filenameRegex: new RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
- };
- },
-
- viewModel: {
- data: {
- size: '-',
- mimetype: '-',
- filename: '',
- },
- },
-
- controller: {
- submit: function (button) {
- const view = this.getView();
- const form = this.lookup('formPanel').getForm();
- const abortBtn = this.lookup('abortBtn');
- const pbar = this.lookup('progressBar');
-
- const updateProgress = function (per, bytes) {
- let text = (per * 100).toFixed(2) + '%';
- if (bytes) {
- text += ' (' + Proxmox.Utils.format_size(bytes) + ')';
- }
- pbar.updateProgress(per, text);
- };
-
- const fd = new FormData();
-
- button.setDisabled(true);
- abortBtn.setDisabled(false);
-
- fd.append('content', view.content);
-
- const fileField = form.findField('file');
- const file = fileField.fileInputEl.dom.files[0];
- fileField.setDisabled(true);
-
- const filenameField = form.findField('filename');
- const filename = filenameField.getValue();
- filenameField.setDisabled(true);
-
- const algorithmField = form.findField('checksum-algorithm');
- algorithmField.setDisabled(true);
- if (algorithmField.getValue() !== '__default__') {
- fd.append('checksum-algorithm', algorithmField.getValue());
-
- const checksumField = form.findField('checksum');
- fd.append('checksum', checksumField.getValue()?.trim());
- checksumField.setDisabled(true);
- }
-
- fd.append('filename', file, filename);
-
- pbar.setVisible(true);
- updateProgress(0);
-
- const xhr = new XMLHttpRequest();
- view.xhr = xhr;
-
- xhr.addEventListener(
- 'load',
- function (e) {
- if (xhr.status === 200) {
- view.hide();
-
- const result = JSON.parse(xhr.response);
- const upid = result.data;
- Ext.create('Proxmox.window.TaskViewer', {
- autoShow: true,
- upid: upid,
- taskDone: view.taskDone,
- listeners: {
- destroy: function () {
- view.close();
- },
- },
- });
-
- return;
- }
- const err = Ext.htmlEncode(xhr.statusText);
- let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`;
- if (xhr.responseText !== '') {
- const result = Ext.decode(xhr.responseText);
- result.message = msg;
- msg = Proxmox.Utils.extractRequestError(result, true);
- }
- Ext.Msg.alert(gettext('Error'), msg, (btn) => view.close());
- },
- false,
- );
-
- xhr.addEventListener('error', function (e) {
- const err = e.target.status.toString();
- const msg = `Error '${err}' occurred while receiving the document.`;
- Ext.Msg.alert(gettext('Error'), msg, (btn) => view.close());
- });
-
- xhr.upload.addEventListener(
- 'progress',
- function (evt) {
- if (evt.lengthComputable) {
- const percentComplete = evt.loaded / evt.total;
- updateProgress(percentComplete, evt.loaded);
- }
- },
- false,
- );
-
- xhr.open('POST', `/api2/json${view.url}`, true);
- xhr.send(fd);
- },
-
- validitychange: function (f, valid) {
- const submitBtn = this.lookup('submitBtn');
- submitBtn.setDisabled(!valid);
- },
-
- fileChange: function (input) {
- const me = this;
- const vm = me.getViewModel();
- const view = me.getView();
- let name = input.value.replace(/^.*(\/|\\)/, '');
- for (const [alias, real] of Object.entries(view.extensionAliases[view.content] ?? {})) {
- if (name.endsWith(alias)) {
- name += real;
- }
- }
- const fileInput = input.fileInputEl.dom;
- vm.set('filename', name);
- vm.set(
- 'size',
- (fileInput.files[0] && Proxmox.Utils.format_size(fileInput.files[0].size)) || '-',
- );
- vm.set('mimetype', (fileInput.files[0] && fileInput.files[0].type) || '-');
- },
-
- hashChange: function (field, value) {
- const checksum = this.lookup('downloadUrlChecksum');
- if (value === '__default__') {
- checksum.setDisabled(true);
- checksum.setValue('');
- } else {
- checksum.setDisabled(false);
- }
- },
- },
-
- items: [
- {
- xtype: 'form',
- reference: 'formPanel',
- method: 'POST',
- waitMsgTarget: true,
- bodyPadding: 10,
- border: false,
- width: 400,
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%',
- },
- items: [
- {
- xtype: 'filefield',
- name: 'file',
- buttonText: gettext('Select File'),
- allowBlank: false,
- fieldLabel: gettext('File'),
- cbind: {
- accept: '{extensions}',
- },
- listeners: {
- change: 'fileChange',
- },
- },
- {
- xtype: 'textfield',
- name: 'filename',
- allowBlank: false,
- fieldLabel: gettext('File name'),
- bind: {
- value: '{filename}',
- },
- cbind: {
- regex: '{filenameRegex}',
- },
- regexText: gettext('Wrong file extension'),
- },
- {
- xtype: 'displayfield',
- name: 'size',
- fieldLabel: gettext('File size'),
- bind: {
- value: '{size}',
- },
- },
- {
- xtype: 'displayfield',
- name: 'mimetype',
- fieldLabel: gettext('MIME type'),
- bind: {
- value: '{mimetype}',
- },
- },
- {
- xtype: 'pveHashAlgorithmSelector',
- name: 'checksum-algorithm',
- fieldLabel: gettext('Hash algorithm'),
- allowBlank: true,
- hasNoneOption: true,
- value: '__default__',
- listeners: {
- change: 'hashChange',
- },
- },
- {
- xtype: 'textfield',
- name: 'checksum',
- fieldLabel: gettext('Checksum'),
- allowBlank: false,
- disabled: true,
- emptyText: gettext('none'),
- reference: 'downloadUrlChecksum',
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- "Uploads are stored temporarily in '/var/tmp/', make sure there is enough free space.",
- ),
- },
- {
- xtype: 'progressbar',
- text: 'Ready',
- hidden: true,
- reference: 'progressBar',
- },
- {
- xtype: 'hiddenfield',
- name: 'content',
- cbind: {
- value: '{content}',
- },
- },
- ],
- listeners: {
- validitychange: 'validitychange',
- },
- },
- ],
-
- buttons: [
- {
- xtype: 'button',
- text: gettext('Abort'),
- reference: 'abortBtn',
- disabled: true,
- handler: function () {
- const me = this;
- me.up('pveStorageUpload').close();
- },
- },
- {
- text: gettext('Upload'),
- reference: 'submitBtn',
- disabled: true,
- handler: 'submit',
- },
- ],
-
- listeners: {
- close: function () {
- const me = this;
- if (me.xhr) {
- me.xhr.abort();
- delete me.xhr;
- }
- },
- },
-
- initComponent: function () {
- const me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
- if (!me.storage) {
- throw 'no storage ID specified';
- }
- if (!me.acceptedExtensions[me.content]) {
- throw 'content type not supported';
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.window.ScheduleSimulator', {
- extend: 'Ext.window.Window',
-
- title: gettext('Job Schedule Simulator'),
-
- viewModel: {
- data: {
- simulatedOnce: false,
- },
- formulas: {
- gridEmptyText: (get) =>
- get('simulatedOnce') ? Proxmox.Utils.NoneText : gettext('No simulation done'),
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
- close: function () {
- this.getView().close();
- },
- simulate: function () {
- let me = this;
- let schedule = me.lookup('schedule').getValue();
- if (!schedule) {
- return;
- }
- let iterations = me.lookup('iterations').getValue() || 10;
- Proxmox.Utils.API2Request({
- url: '/cluster/jobs/schedule-analyze',
- method: 'GET',
- params: {
- schedule,
- iterations,
- },
- failure: (response) => {
- me.getViewModel().set('simulatedOnce', true);
- me.lookup('grid').getStore().setData([]);
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response) {
- let schedules = response.result.data;
- me.lookup('grid').getStore().setData(schedules);
- me.getViewModel().set('simulatedOnce', true);
- },
- });
- },
-
- scheduleChanged: function (field, value) {
- this.lookup('simulateBtn').setDisabled(!value);
- },
-
- renderDate: function (value) {
- let date = new Date(value * 1000);
- return date.toLocaleDateString();
- },
-
- renderTime: function (value) {
- let date = new Date(value * 1000);
- return date.toLocaleTimeString();
- },
-
- init: function (view) {
- let me = this;
- if (view.schedule) {
- me.lookup('schedule').setValue(view.schedule);
- }
- },
- },
-
- bodyPadding: 10,
- modal: true,
- resizable: false,
- width: 600,
-
- layout: 'fit',
-
- items: [
- {
- xtype: 'inputpanel',
- column1: [
- {
- xtype: 'pveCalendarEvent',
- reference: 'schedule',
- fieldLabel: gettext('Schedule'),
- listeners: {
- change: 'scheduleChanged',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- reference: 'iterations',
- fieldLabel: gettext('Iterations'),
- minValue: 1,
- maxValue: 100,
- value: 10,
- },
- {
- xtype: 'container',
- layout: 'hbox',
- items: [
- {
- xtype: 'box',
- flex: 1,
- },
- {
- xtype: 'button',
- reference: 'simulateBtn',
- text: gettext('Simulate'),
- handler: 'simulate',
- disabled: true,
- },
- ],
- },
- ],
-
- column2: [
- {
- xtype: 'grid',
- reference: 'grid',
- bind: {
- emptyText: '{gridEmptyText}',
- },
- scrollable: true,
- height: 300,
- columns: [
- {
- text: gettext('Date'),
- renderer: 'renderDate',
- dataIndex: 'timestamp',
- flex: 1,
- },
- {
- text: gettext('Time'),
- renderer: 'renderTime',
- dataIndex: 'timestamp',
- align: 'right',
- flex: 1,
- },
- ],
- store: {
- fields: ['timestamp'],
- data: [],
- sorter: 'timestamp',
- },
- },
- ],
- },
- ],
-
- buttons: [
- {
- text: gettext('Done'),
- handler: 'close',
- },
- ],
-});
-Ext.define('PVE.window.Wizard', {
- extend: 'Ext.window.Window',
-
- activeTitle: '', // used for automated testing
-
- width: 720,
- height: 540,
-
- modal: true,
- border: false,
-
- draggable: true,
- closable: true,
- resizable: false,
-
- layout: 'border',
-
- getValues: function (dirtyOnly) {
- let me = this;
-
- let values = {};
-
- me.down('form')
- .getForm()
- .getFields()
- .each((field) => {
- if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
- Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
- }
- });
-
- me.query('inputpanel').forEach((panel) => {
- Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
- });
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- var tabs = me.items || [];
- delete me.items;
-
- /*
- * Items may have the following functions:
- * validator(): per tab custom validation
- * onSubmit(): submit handler
- * onGetValues(): overwrite getValues results
- */
-
- Ext.Array.each(tabs, function (tab) {
- tab.disabled = true;
- });
- tabs[0].disabled = false;
-
- let maxidx = 0,
- curidx = 0;
-
- let check_card = function (card) {
- let fields = card.query('field, fieldcontainer');
- if (card.isXType('fieldcontainer')) {
- fields.unshift(card);
- }
- let valid = true;
- for (const field of fields) {
- // Note: not all fielcontainer have isValid()
- if (Ext.isFunction(field.isValid) && !field.isValid()) {
- valid = false;
- }
- }
- if (Ext.isFunction(card.validator)) {
- return card.validator();
- }
- return valid;
- };
-
- let disableTab = function (card) {
- let tp = me.down('#wizcontent');
- for (let idx = tp.items.indexOf(card); idx < tp.items.getCount(); idx++) {
- let tab = tp.items.getAt(idx);
- if (tab) {
- tab.disable();
- }
- }
- };
-
- let tabchange = function (tp, newcard, oldcard) {
- if (newcard.onSubmit) {
- me.down('#next').setVisible(false);
- me.down('#submit').setVisible(true);
- } else {
- me.down('#next').setVisible(true);
- me.down('#submit').setVisible(false);
- }
- let valid = check_card(newcard);
- me.down('#next').setDisabled(!valid);
- me.down('#submit').setDisabled(!valid);
- me.down('#back').setDisabled(tp.items.indexOf(newcard) === 0);
-
- let idx = tp.items.indexOf(newcard);
- if (idx > maxidx) {
- maxidx = idx;
- }
- curidx = idx;
-
- let ntab = tp.items.getAt(idx + 1);
- if (valid && ntab && !newcard.onSubmit) {
- ntab.enable();
- }
- };
-
- if (me.subject && !me.title) {
- me.title = Proxmox.Utils.dialog_title(me.subject, true, false);
- }
-
- let sp = Ext.state.Manager.getProvider();
- let advancedOn = sp.get('proxmox-advanced-cb');
-
- Ext.apply(me, {
- items: [
- {
- xtype: 'form',
- region: 'center',
- layout: 'fit',
- border: false,
- margins: '5 5 0 5',
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%',
- },
- items: [
- {
- itemId: 'wizcontent',
- xtype: 'tabpanel',
- activeItem: 0,
- bodyPadding: 0,
- listeners: {
- afterrender: function (tp) {
- tabchange(tp, this.getActiveTab());
- },
- tabchange: function (tp, newcard, oldcard) {
- tabchange(tp, newcard, oldcard);
- },
- },
- defaults: {
- padding: 10,
- },
- items: tabs,
- },
- ],
- },
- ],
- fbar: [
- {
- xtype: 'proxmoxHelpButton',
- itemId: 'help',
- },
- '->',
- {
- xtype: 'proxmoxcheckbox',
- boxLabelAlign: 'before',
- boxLabel: gettext('Advanced'),
- value: advancedOn,
- listeners: {
- change: function (_, value) {
- let tp = me.down('#wizcontent');
- tp.query('inputpanel').forEach(function (ip) {
- ip.setAdvancedVisible(value);
- });
- sp.set('proxmox-advanced-cb', value);
- },
- },
- },
- {
- text: gettext('Back'),
- disabled: true,
- itemId: 'back',
- minWidth: 60,
- handler: function () {
- let tp = me.down('#wizcontent');
- let prev = tp.items.indexOf(tp.getActiveTab()) - 1;
- if (prev < 0) {
- return;
- }
- let ntab = tp.items.getAt(prev);
- if (ntab) {
- tp.setActiveTab(ntab);
- }
- },
- },
- {
- text: gettext('Next'),
- disabled: true,
- itemId: 'next',
- minWidth: 60,
- handler: function () {
- let tp = me.down('#wizcontent');
- let activeTab = tp.getActiveTab();
- if (!check_card(activeTab)) {
- return;
- }
- let next = tp.items.indexOf(activeTab) + 1;
- let ntab = tp.items.getAt(next);
- if (ntab) {
- ntab.enable();
- tp.setActiveTab(ntab);
- }
- },
- },
- {
- text: gettext('Finish'),
- minWidth: 60,
- hidden: true,
- itemId: 'submit',
- handler: function () {
- let tp = me.down('#wizcontent');
- tp.getActiveTab().onSubmit();
- },
- },
- ],
- });
- me.callParent();
-
- Ext.Array.each(me.query('inputpanel'), function (panel) {
- panel.setAdvancedVisible(advancedOn);
- });
-
- Ext.Array.each(me.query('field'), function (field) {
- let validcheck = function () {
- let tp = me.down('#wizcontent');
-
- // check validity for current to last enabled tab, as local change may affect validity of a later one
- for (let i = curidx; i <= maxidx && i < tp.items.getCount(); i++) {
- let tab = tp.items.getAt(i);
- let valid = check_card(tab);
-
- // only set the buttons on the current panel
- if (i === curidx) {
- me.down('#next').setDisabled(!valid);
- me.down('#submit').setDisabled(!valid);
- }
- // if a panel is invalid, then disable all following, else enable the next tab
- let nextTab = tp.items.getAt(i + 1);
- if (!valid) {
- disableTab(nextTab);
- return;
- } else if (nextTab && !tab.onSubmit) {
- nextTab.enable();
- }
- }
- };
- field.on('change', validcheck);
- field.on('validitychange', validcheck);
- });
- },
-});
-Ext.define('PVE.window.GuestDiskReassign', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- resizable: false,
- modal: true,
- width: 350,
- border: false,
- layout: 'fit',
- showReset: false,
- showProgress: true,
- method: 'POST',
-
- viewModel: {
- data: {
- mpType: '',
- },
- formulas: {
- mpMaxCount: (get) =>
- get('mpType') === 'mp'
- ? PVE.Utils.lxc_mp_counts.mps - 1
- : PVE.Utils.lxc_mp_counts.unused - 1,
- },
- },
-
- cbindData: function () {
- let me = this;
- return {
- vmid: me.vmid,
- disk: me.disk,
- isQemu: me.type === 'qemu',
- nodename: me.nodename,
- url: () => {
- let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume';
- return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`;
- },
- };
- },
-
- cbind: {
- title: (get) => (get('isQemu') ? gettext('Reassign Disk') : gettext('Reassign Volume')),
- submitText: (get) => get('title'),
- qemu: '{isQemu}',
- url: '{url}',
- },
-
- getValues: function () {
- let me = this;
- let values = me.formPanel.getForm().getValues();
-
- let params = {
- vmid: me.vmid,
- 'target-vmid': values.targetVmid,
- };
-
- params[me.qemu ? 'disk' : 'volume'] = me.disk;
-
- if (me.qemu) {
- params['target-disk'] = `${values.controller}${values.deviceid}`;
- } else {
- params['target-volume'] = `${values.mpType}${values.mpId}`;
- }
- return params;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- initViewModel: function (model) {
- let view = this.getView();
- let mpTypeValue = view.disk.match(/^unused\d+/) ? 'unused' : 'mp';
- model.set('mpType', mpTypeValue);
- },
-
- onMpTypeChange: function (value) {
- let view = this.getView();
- view.getViewModel().set('mpType', value.getValue());
- view.lookup('mpIdSelector').validate();
- },
-
- onTargetVMChange: function (f, vmid) {
- let me = this;
- let view = me.getView();
- let diskSelector = view.lookup('diskSelector');
- if (!vmid) {
- diskSelector.setVMConfig(null);
- me.VMConfig = null;
- return;
- }
-
- let url = `/nodes/${view.nodename}/${view.type}/${vmid}/config`;
- Proxmox.Utils.API2Request({
- url: url,
- method: 'GET',
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: function ({ result }, options) {
- if (view.qemu) {
- diskSelector.setVMConfig(result.data);
- diskSelector.setDisabled(false);
- } else {
- let mpIdSelector = view.lookup('mpIdSelector');
- let mpType = view.lookup('mpType');
-
- view.VMConfig = result.data;
-
- mpIdSelector.setValue(
- PVE.Utils.nextFreeLxcMP(
- view.getViewModel().get('mpType'),
- view.VMConfig,
- ).id,
- );
-
- mpType.setDisabled(false);
- mpIdSelector.setDisabled(false);
- mpIdSelector.validate();
- }
- },
- });
- },
- },
-
- defaultFocus: 'sourceDisk',
- items: [
- {
- xtype: 'displayfield',
- name: 'sourceDisk',
- fieldLabel: gettext('Source'),
- cbind: {
- name: (get) => (get('isQemu') ? 'disk' : 'volume'),
- value: '{disk}',
- },
- allowBlank: false,
- },
- {
- xtype: 'vmComboSelector',
- name: 'targetVmid',
- allowBlank: false,
- fieldLabel: gettext('Target Guest'),
- store: {
- model: 'PVEResources',
- autoLoad: true,
- sorters: 'vmid',
- cbind: {}, // for nested cbinds
- filters: [
- {
- property: 'type',
- cbind: { value: '{type}' },
- },
- {
- property: 'node',
- cbind: { value: '{nodename}' },
- },
- // FIXME: remove, artificial restriction that doesn't gains us anything..
- {
- property: 'vmid',
- operator: '!=',
- cbind: { value: '{vmid}' },
- },
- {
- property: 'template',
- value: 0,
- },
- ],
- },
- listeners: { change: 'onTargetVMChange' },
- },
- {
- xtype: 'pveControllerSelector',
- reference: 'diskSelector',
- withUnused: true,
- disabled: true,
- cbind: {
- hidden: '{!isQemu}',
- },
- },
- {
- xtype: 'container',
- layout: 'hbox',
- cbind: {
- hidden: '{isQemu}',
- disabled: '{isQemu}',
- },
- items: [
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: (get) => !get('disk').match(/^unused\d+/),
- value: (get) => (get('disk').match(/^unused\d+/) ? 'unused' : 'mp'),
- },
- disabled: true,
- name: 'mpType',
- reference: 'mpType',
- fieldLabel: gettext('Add as'),
- submitValue: true,
- flex: 4,
- editConfig: {
- xtype: 'proxmoxKVComboBox',
- name: 'mpTypeCombo',
- deleteEmpty: false,
- cbind: {
- hidden: '{isQemu}',
- },
- comboItems: [
- ['mp', gettext('Mount Point')],
- ['unused', gettext('Unused')],
- ],
- listeners: { change: 'onMpTypeChange' },
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'mpId',
- reference: 'mpIdSelector',
- minValue: 0,
- flex: 1,
- allowBlank: false,
- validateOnChange: true,
- disabled: true,
- bind: {
- maxValue: '{mpMaxCount}',
- },
- validator: function (value) {
- let view = this.up('window');
- let type = view.getViewModel().get('mpType');
- if (Ext.isDefined(view.VMConfig[`${type}${value}`])) {
- return 'Mount point is already in use.';
- }
- return true;
- },
- },
- ],
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.vmid) {
- throw 'no VM ID specified';
- }
-
- if (!me.type) {
- throw 'no type specified';
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.GuestStop', {
- extend: 'Ext.window.MessageBox',
-
- closeAction: 'destroy',
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
- if (!me.vm) {
- throw 'no vm specified';
- }
-
- let isQemuVM = me.vm.type === 'qemu';
- let overruleTaskType = isQemuVM ? 'qmshutdown' : 'vzshutdown';
-
- me.taskType = isQemuVM ? 'qmstop' : 'vzstop';
- me.url = `/nodes/${me.nodename}/${me.vm.type}/${me.vm.vmid}/status/stop`;
-
- let caps = Ext.state.Manager.get('GuiCap');
- let hasSysModify = !!caps.nodes['Sys.Modify'];
-
- // offer to overrule if there is at least one matching shutdown task and the guest is not
- // HA-enabled. Also allow users to abort tasks started by one of their API tokens.
- let activeShutdownTask =
- Ext.getStore('pve-cluster-tasks')?.findBy(
- (task) =>
- (hasSysModify || task.data.user === Proxmox.UserName) &&
- task.data.id === me.vm.vmid.toString() &&
- task.data.status === undefined &&
- task.data.type === overruleTaskType,
- ) !== -1;
- let haEnabled = me.vm.hastate && me.vm.hastate !== 'unmanaged';
-
- me.callParent();
-
- // message box has its actual content in a sub-container, the top one is just for layouting
- me.promptContainer.add({
- xtype: 'proxmoxcheckbox',
- name: 'overrule-shutdown',
- checked: !haEnabled && activeShutdownTask,
- boxLabel: gettext('Overrule active shutdown tasks'),
- hidden: !(hasSysModify || activeShutdownTask),
- disabled: !(hasSysModify || activeShutdownTask) || haEnabled,
- padding: '3 0 0 0',
- });
- },
-
- handler: function (btn) {
- let me = this;
- if (btn === 'yes') {
- let overruleField = me.promptContainer.down('proxmoxcheckbox[name=overrule-shutdown]');
- let params =
- !overruleField.isDisabled() && overruleField.getSubmitValue()
- ? { 'overrule-shutdown': 1 }
- : undefined;
- Proxmox.Utils.API2Request({
- url: me.url,
- waitMsgTarget: me,
- method: 'POST',
- params: params,
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- });
- }
- },
-
- show: function () {
- let me = this;
- let cfg = {
- title: gettext('Confirm'),
- icon: Ext.Msg.WARNING,
- msg: PVE.Utils.formatGuestTaskConfirmation(me.taskType, me.vm.vmid, me.vm.name),
- buttons: Ext.Msg.YESNO,
- callback: (btn) => me.handler(btn),
- };
- me.callParent([cfg]);
- },
-});
-Ext.define('PVE.window.TreeSettingsEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveTreeSettingsEdit',
-
- title: gettext('Tree Settings'),
- isCreate: false,
-
- url: '#', // ignored as submit() gets overridden here, but the parent class requires it
-
- width: 450,
- fieldDefaults: {
- labelWidth: 150,
- },
-
- items: [
- {
- xtype: 'inputpanel',
- items: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'sort-field',
- fieldLabel: gettext('Sort Key'),
- comboItems: [
- ['__default__', `${Proxmox.Utils.defaultText} (VMID)`],
- ['vmid', 'VMID'],
- ['name', gettext('Name')],
- ],
- defaultValue: '__default__',
- value: '__default__',
- deleteEmpty: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'group-templates',
- fieldLabel: gettext('Group Templates'),
- comboItems: [
- ['__default__', `${Proxmox.Utils.defaultText} (${gettext('Yes')})`],
- [1, gettext('Yes')],
- [0, gettext('No')],
- ],
- defaultValue: '__default__',
- value: '__default__',
- deleteEmpty: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'group-guest-types',
- fieldLabel: gettext('Group Guest Types'),
- comboItems: [
- ['__default__', `${Proxmox.Utils.defaultText} (${gettext('Yes')})`],
- [1, gettext('Yes')],
- [0, gettext('No')],
- ],
- defaultValue: '__default__',
- value: '__default__',
- deleteEmpty: false,
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext('Settings are saved in the local storage of the browser'),
- },
- ],
- },
- ],
-
- submit: function () {
- let me = this;
-
- let localStorage = Ext.state.Manager.getProvider();
- localStorage.set('pve-tree-sorting', me.down('inputpanel').getValues() || null);
-
- me.apiCallDone();
- me.close();
- },
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- let localStorage = Ext.state.Manager.getProvider();
- me.down('inputpanel').setValues(localStorage.get('pve-tree-sorting'));
- },
-});
-Ext.define('PVE.window.PCIMapEditWindow', {
- extend: 'Proxmox.window.Edit',
-
- mixins: ['Proxmox.Mixin.CBind'],
-
- width: 800,
-
- subject: gettext('PCI mapping'),
-
- onlineHelp: 'resource_mapping',
-
- method: 'POST',
-
- cbindData: function (initialConfig) {
- let me = this;
- me.isCreate = (!me.name || !me.nodename) && !me.entryOnly;
- me.method = me.name ? 'PUT' : 'POST';
- me.hideMapping = !!me.entryOnly;
- me.globalEdit = !me.name || me.entryOnly;
- me.hideNodeSelector = me.nodename || me.entryOnly;
- me.hideNode = !me.nodename || !me.hideNodeSelector;
- return {
- name: me.name,
- nodename: me.nodename,
- };
- },
-
- submitUrl: function (_url, data) {
- let me = this;
- let name = me.method === 'PUT' ? me.name : '';
- return `/cluster/mapping/pci/${name}`;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- onGetValues: function (values) {
- let me = this;
- let view = me.getView();
- if (view.method === 'POST') {
- delete me.digest;
- }
-
- if (values.iommugroup === -1) {
- delete values.iommugroup;
- }
-
- let nodename = values.node ?? view.nodename;
- delete values.node;
- if (me.originalMap) {
- let otherMaps = PVE.Parser.filterPropertyStringList(
- me.originalMap,
- (e) => e.node !== nodename,
- );
- if (otherMaps.length) {
- values.map = values.map.concat(otherMaps);
- }
- }
-
- return values;
- },
-
- onSetValues: function (values) {
- let me = this;
- let view = me.getView();
- me.originalMap = [...values.map];
- let configuredNodes = [];
- values.map = PVE.Parser.filterPropertyStringList(values.map, (e) => {
- configuredNodes.push(e.node);
- return e.node === view.nodename;
- });
-
- me.lookup('nodeselector').disallowedNodes = configuredNodes;
- return values;
- },
-
- checkIommu: function (store, records, success) {
- let me = this;
- if (!success || !records.length) {
- return;
- }
- me.lookup('iommu_warning').setVisible(
- records.every((val) => val.data.iommugroup === -1),
- );
-
- let value = me.lookup('pciselector').getValue();
- me.checkIsolated(value);
- },
-
- checkIsolated: function (value) {
- let me = this;
-
- let store = me.lookup('pciselector').getStore();
-
- let isIsolated = function (entry) {
- let isolated = true;
- let parsed = PVE.Parser.parsePropertyString(entry);
- parsed.iommugroup = parseInt(parsed.iommugroup, 10);
- if (!parsed.iommugroup) {
- return isolated;
- }
- store.each(({ data }) => {
- let isSubDevice = data.id.startsWith(parsed.path);
- if (
- data.iommugroup === parsed.iommugroup &&
- data.id !== parsed.path &&
- !isSubDevice
- ) {
- isolated = false;
- return false;
- }
- return true;
- });
- return isolated;
- };
-
- let showWarning = false;
- if (Ext.isArray(value)) {
- for (const entry of value) {
- if (!isIsolated(entry)) {
- showWarning = true;
- break;
- }
- }
- } else {
- showWarning = isIsolated(value);
- }
- me.lookup('group_warning').setVisible(showWarning);
- },
-
- mdevChange: function (mdevField, value) {
- this.lookup('pciselector').setMdev(value);
- },
-
- nodeChange: function (field, value) {
- if (!field.isDisabled()) {
- this.lookup('pciselector').setNodename(value);
- }
- },
-
- pciChange: function (_field, value) {
- let me = this;
- me.lookup('multiple_warning').setVisible(Ext.isArray(value) && value.length > 1);
- me.checkIsolated(value);
- },
-
- control: {
- 'field[name=mdev]': {
- change: 'mdevChange',
- },
- pveNodeSelector: {
- change: 'nodeChange',
- },
- pveMultiPCISelector: {
- change: 'pciChange',
- },
- },
- },
-
- items: [
- {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- return this.up('window').getController().onGetValues(values);
- },
-
- onSetValues: function (values) {
- return this.up('window').getController().onSetValues(values);
- },
-
- columnT: [
- {
- xtype: 'displayfield',
- reference: 'iommu_warning',
- hidden: true,
- columnWidth: 1,
- padding: '0 0 10 0',
- value: gettext(
- 'No IOMMU detected, please activate it. See Documentation for further information.',
- ),
- userCls: 'pmx-hint',
- },
- {
- xtype: 'displayfield',
- reference: 'multiple_warning',
- hidden: true,
- columnWidth: 1,
- padding: '0 0 10 0',
- value: gettext(
- 'When multiple devices are selected, the first free one will be chosen on guest start.',
- ),
- userCls: 'pmx-hint',
- },
- {
- xtype: 'displayfield',
- reference: 'group_warning',
- hidden: true,
- columnWidth: 1,
- padding: '0 0 10 0',
- itemId: 'iommuwarning',
- value: gettext(
- 'A selected device is not in a separate IOMMU group, make sure this is intended.',
- ),
- userCls: 'pmx-hint',
- },
- ],
-
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- fieldLabel: gettext('Name'),
- labelWidth: 120,
- cbind: {
- editable: '{!name}',
- value: '{name}',
- submitValue: '{isCreate}',
- },
- name: 'id',
- allowBlank: false,
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Mapping on Node'),
- labelWidth: 120,
- name: 'node',
- cbind: {
- value: '{nodename}',
- disabled: '{hideNode}',
- hidden: '{hideNode}',
- },
- allowBlank: false,
- },
- {
- xtype: 'pveNodeSelector',
- reference: 'nodeselector',
- fieldLabel: gettext('Mapping on Node'),
- labelWidth: 120,
- name: 'node',
- cbind: {
- disabled: '{hideNodeSelector}',
- hidden: '{hideNodeSelector}',
- },
- allowBlank: false,
- },
- ],
-
- column2: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Use with Mediated Devices'),
- labelWidth: 200,
- reference: 'mdev',
- name: 'mdev',
- cbind: {
- deleteEmpty: '{!isCreate}',
- disabled: '{!globalEdit}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Live Migration Capable'),
- labelWidth: 200,
- boxLabel: ` ${gettext('Experimental')}`,
- reference: 'live-migration-capable',
- name: 'live-migration-capable',
- cbind: {
- deleteEmpty: '{!isCreate}',
- disabled: '{!globalEdit}',
- },
- },
- ],
-
- columnB: [
- {
- xtype: 'pveMultiPCISelector',
- fieldLabel: gettext('Device'),
- labelWidth: 120,
- height: 300,
- reference: 'pciselector',
- name: 'map',
- cbind: {
- nodename: '{nodename}',
- disabled: '{hideMapping}',
- hidden: '{hideMapping}',
- },
- allowBlank: false,
- onLoadCallBack: 'checkIommu',
- margin: '0 0 10 0',
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Comment'),
- labelWidth: 120,
- submitValue: true,
- name: 'description',
- cbind: {
- deleteEmpty: '{!isCreate}',
- disabled: '{!globalEdit}',
- hidden: '{!globalEdit}',
- },
- },
- ],
- },
- ],
-});
-Ext.define('PVE.window.USBMapEditWindow', {
- extend: 'Proxmox.window.Edit',
-
- mixins: ['Proxmox.Mixin.CBind'],
-
- cbindData: function (initialConfig) {
- let me = this;
- me.isCreate = !me.name;
- me.method = me.isCreate ? 'POST' : 'PUT';
- me.hideMapping = !!me.entryOnly;
- me.hideComment = me.name && !me.entryOnly;
- me.hideNodeSelector = me.nodename || me.entryOnly;
- me.hideNode = !me.nodename || !me.hideNodeSelector;
- return {
- name: me.name,
- nodename: me.nodename,
- };
- },
-
- submitUrl: function (_url, data) {
- let me = this;
- let name = me.isCreate ? '' : me.name;
- return `/cluster/mapping/usb/${name}`;
- },
-
- title: gettext('Add USB mapping'),
-
- onlineHelp: 'resource_mapping',
-
- method: 'POST',
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- onGetValues: function (values) {
- let me = this;
- let view = me.getView();
- values.node ??= view.nodename;
-
- let type = me.getView().down('radiofield').getGroupValue();
- let name = values.name;
- let description = values.description;
- delete values.description;
- delete values.name;
-
- if (type === 'path') {
- let usbsel = me.lookup(type);
- let usbDev = usbsel
- .getStore()
- .findRecord('usbid', values[type], 0, false, true, true);
-
- if (!usbDev) {
- return {};
- }
- values.id = `${usbDev.data.vendid}:${usbDev.data.prodid}`;
- }
-
- let map = [];
- if (me.originalMap) {
- map = PVE.Parser.filterPropertyStringList(
- me.originalMap,
- (e) => e.node !== values.node,
- );
- }
- if (values.id) {
- map.push(PVE.Parser.printPropertyString(values));
- }
-
- values = { map };
- if (description) {
- values.description = description;
- }
-
- if (view.isCreate) {
- values.id = name;
- }
-
- return values;
- },
-
- onSetValues: function (values) {
- let me = this;
- let view = me.getView();
- me.originalMap = [...values.map];
- let configuredNodes = [];
- PVE.Parser.filterPropertyStringList(values.map, (e) => {
- configuredNodes.push(e.node);
- if (e.node === view.nodename) {
- values = e;
- }
- return false;
- });
-
- me.lookup('nodeselector').disallowedNodes = configuredNodes;
- if (values.path) {
- values.usb = 'path';
- }
-
- return values;
- },
-
- modeChange: function (field, value) {
- let me = this;
- let type = field.inputValue;
- let usbsel = me.lookup(type);
- usbsel.setDisabled(!value);
- },
-
- nodeChange: function (field, value) {
- if (!field.isDisabled()) {
- this.lookup('id').setNodename(value);
- this.lookup('path').setNodename(value);
- }
- },
-
- init: function (view) {
- let _me = this;
-
- if (!view.nodename) {
- //throw "no nodename given";
- }
- },
-
- control: {
- radiofield: {
- change: 'modeChange',
- },
- pveNodeSelector: {
- change: 'nodeChange',
- },
- },
- },
-
- items: [
- {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- return this.up('window').getController().onGetValues(values);
- },
-
- onSetValues: function (values) {
- return this.up('window').getController().onSetValues(values);
- },
-
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- fieldLabel: gettext('Name'),
- cbind: {
- editable: '{!name}',
- value: '{name}',
- submitValue: '{isCreate}',
- },
- name: 'name',
- allowBlank: false,
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Mapping on Node'),
- labelWidth: 120,
- name: 'node',
- cbind: {
- value: '{nodename}',
- disabled: '{hideNode}',
- hidden: '{hideNode}',
- },
- allowBlank: false,
- },
- {
- xtype: 'pveNodeSelector',
- reference: 'nodeselector',
- fieldLabel: gettext('Mapping on Node'),
- labelWidth: 120,
- name: 'node',
- cbind: {
- disabled: '{hideNodeSelector}',
- hidden: '{hideNodeSelector}',
- },
- allowBlank: false,
- },
- ],
-
- column2: [
- {
- xtype: 'fieldcontainer',
- defaultType: 'radiofield',
- layout: 'fit',
- cbind: {
- disabled: '{hideMapping}',
- hidden: '{hideMapping}',
- },
- items: [
- {
- name: 'usb',
- inputValue: 'id',
- checked: true,
- boxLabel: gettext('Use USB Vendor/Device ID'),
- submitValue: false,
- },
- {
- xtype: 'pveUSBSelector',
- type: 'device',
- reference: 'id',
- name: 'id',
- cbind: {
- nodename: '{nodename}',
- disabled: '{hideMapping}',
- },
- editable: true,
- allowBlank: false,
- fieldLabel: gettext('Choose Device'),
- labelAlign: 'right',
- },
- {
- name: 'usb',
- inputValue: 'path',
- boxLabel: gettext('Use USB Port'),
- submitValue: false,
- },
- {
- xtype: 'pveUSBSelector',
- disabled: true,
- name: 'path',
- reference: 'path',
- cbind: {
- nodename: '{nodename}',
- },
- editable: true,
- type: 'port',
- allowBlank: false,
- fieldLabel: gettext('Choose Port'),
- labelAlign: 'right',
- },
- ],
- },
- ],
-
- columnB: [
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Comment'),
- submitValue: true,
- name: 'description',
- cbind: {
- disabled: '{hideComment}',
- hidden: '{hideComment}',
- },
- },
- ],
- },
- ],
-});
-Ext.define('PVE.window.DirMapEditWindow', {
- extend: 'Proxmox.window.Edit',
-
- mixins: ['Proxmox.Mixin.CBind'],
-
- cbindData: function (initialConfig) {
- let me = this;
- me.isCreate = !me.name;
- me.method = me.isCreate ? 'POST' : 'PUT';
- me.hideMapping = !!me.entryOnly;
- me.hideComment = me.name && !me.entryOnly;
- me.hideNodeSelector = me.nodename || me.entryOnly;
- me.hideNode = !me.nodename || !me.hideNodeSelector;
- return {
- name: me.name,
- nodename: me.nodename,
- };
- },
-
- submitUrl: function (_url, data) {
- let me = this;
- let name = me.isCreate ? '' : me.name;
- return `/cluster/mapping/dir/${name}`;
- },
-
- title: gettext('Add Directory Mapping'),
-
- onlineHelp: 'resource_mapping',
-
- method: 'POST',
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- onGetValues: function (values) {
- let me = this;
- let view = me.getView();
- values.node ??= view.nodename;
-
- let name = values.name;
- let description = values.description;
- let deletes = values.delete;
-
- delete values.description;
- delete values.name;
- delete values.delete;
-
- let map = [];
- if (me.originalMap) {
- map = PVE.Parser.filterPropertyStringList(
- me.originalMap,
- (e) => e.node !== values.node,
- );
- }
- if (values.path) {
- // TODO: Remove this when property string supports quotation of properties
- if (!/^\/[^;,=()]+/.test(values.path)) {
- let errMsg =
- 'Value does not look like a valid absolute path.' +
- ' These symbols are currently not allowed in path: ;,=()\n';
- Ext.Msg.alert(gettext('Error'), errMsg);
- // prevent sending a broken property string to the API
- throw errMsg;
- }
- map.push(PVE.Parser.printPropertyString(values));
- }
- values = { map };
-
- if (description) {
- values.description = description;
- }
- if (deletes && !view.isCreate) {
- values.delete = deletes;
- }
- if (view.isCreate) {
- values.id = name;
- }
-
- return values;
- },
-
- onSetValues: function (values) {
- let me = this;
- let view = me.getView();
- me.originalMap = [...values.map];
- let configuredNodes = [];
- PVE.Parser.filterPropertyStringList(values.map, (e) => {
- configuredNodes.push(e.node);
- if (e.node === view.nodename) {
- values = e;
- }
- return false;
- });
-
- me.lookup('nodeselector').disallowedNodes = configuredNodes;
-
- return values;
- },
-
- init: function (view) {
- let _me = this;
-
- if (!view.nodename) {
- //throw "no nodename given";
- }
- },
- },
-
- items: [
- {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- return this.up('window').getController().onGetValues(values);
- },
-
- onSetValues: function (values) {
- return this.up('window').getController().onSetValues(values);
- },
-
- columnT: [
- {
- xtype: 'displayfield',
- reference: 'directory-hint',
- columnWidth: 1,
- value: 'Make sure the directory exists.',
- cbind: {
- disabled: '{hideMapping}',
- hidden: '{hideMapping}',
- },
- userCls: 'pmx-hint',
- },
- ],
-
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- fieldLabel: gettext('Name'),
- cbind: {
- editable: '{!name}',
- value: '{name}',
- submitValue: '{isCreate}',
- },
- name: 'name',
- allowBlank: false,
- },
- {
- xtype: 'pveNodeSelector',
- reference: 'nodeselector',
- fieldLabel: gettext('Node'),
- name: 'node',
- cbind: {
- disabled: '{hideNodeSelector}',
- hidden: '{hideNodeSelector}',
- },
- allowBlank: false,
- },
- ],
-
- column2: [
- {
- xtype: 'fieldcontainer',
- defaultType: 'radiofield',
- layout: 'fit',
- cbind: {
- disabled: '{hideMapping}',
- hidden: '{hideMapping}',
- },
- items: [
- {
- xtype: 'textfield',
- name: 'path',
- reference: 'path',
- value: '',
- emptyText: gettext('/some/path'),
- cbind: {
- nodename: '{nodename}',
- disabled: '{hideMapping}',
- },
- allowBlank: false,
- fieldLabel: gettext('Path'),
- },
- ],
- },
- ],
-
- columnB: [
- {
- xtype: 'fieldcontainer',
- defaultType: 'radiofield',
- layout: 'fit',
- cbind: {
- disabled: '{hideComment}',
- hidden: '{hideComment}',
- },
- items: [
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Comment'),
- submitValue: true,
- name: 'description',
- deleteEmpty: true,
- },
- ],
- },
- ],
- },
- ],
-});
-Ext.define('PVE.window.GuestImport', {
- extend: 'Proxmox.window.Edit', // fixme: Proxmox.window.Edit?
- alias: 'widget.pveGuestImportWindow',
-
- title: gettext('Import Guest'),
-
- onlineHelp: 'qm_import_virtual_machines',
-
- width: 720,
- bodyPadding: 0,
-
- submitUrl: function () {
- let me = this;
- return `/nodes/${me.nodename}/qemu`;
- },
-
- isAdd: true,
- isCreate: true,
- submitText: gettext('Import'),
- showTaskViewer: true,
- method: 'POST',
-
- loadUrl: function (_url, { storage, nodename, volumeName }) {
- let args = Ext.Object.toQueryString({ volume: volumeName });
- return `/nodes/${nodename}/storage/${storage}/import-metadata?${args}`;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- setNodename: function (_column, widget) {
- let me = this;
- let view = me.getView();
- widget.setNodename(view.nodename);
- },
-
- diskStorageChange: function (storageSelector, value) {
- let me = this;
-
- let grid = me.lookup('diskGrid');
- let rec = storageSelector.getWidgetRecord();
- let validFormats = storageSelector.store.getById(value)?.data.format;
- grid.query('pveDiskFormatSelector').some((selector) => {
- if (selector.getWidgetRecord().data.id !== rec.data.id) {
- return false;
- }
-
- if (validFormats?.[0]?.qcow2) {
- selector.setDisabled(false);
- selector.setValue('qcow2');
- } else {
- selector.setValue('raw');
- selector.setDisabled(true);
- }
-
- return true;
- });
- },
-
- isoStorageChange: function (storageSelector, value) {
- let me = this;
-
- let grid = me.lookup('cdGrid');
- let rec = storageSelector.getWidgetRecord();
- grid.query('pveFileSelector').some((selector) => {
- if (selector.getWidgetRecord().data.id !== rec.data.id) {
- return false;
- }
-
- selector.setStorage(value);
- if (!value) {
- selector.setValue('');
- }
-
- return true;
- });
- },
-
- onOSBaseChange: function (_field, value) {
- let me = this;
- let ostype = me.lookup('ostype');
- let store = ostype.getStore();
- store.setData(PVE.Utils.kvm_ostypes[value]);
- let old_val = ostype.getValue();
- if (old_val && store.find('val', old_val) !== -1) {
- ostype.setValue(old_val);
- } else {
- ostype.setValue(store.getAt(0));
- }
- },
-
- calculateConfig: function () {
- let me = this;
- let inputPanel = me.lookup('mainInputPanel');
- let summaryGrid = me.lookup('summaryGrid');
- let values = inputPanel.getValues();
- summaryGrid
- .getStore()
- .setData(Object.entries(values).map(([key, value]) => ({ key, value })));
- },
-
- calculateAdditionalCDIdx: function () {
- let me = this;
-
- let maxIde = me.getMaxControllerId('ide');
- let maxSata = me.getMaxControllerId('sata');
- // only ide0 and ide2 can be used reliably for isos (e.g. for q35)
- if (maxIde < 0) {
- return 'ide0';
- }
- if (maxIde < 2) {
- return 'ide2';
- }
- if (maxSata < PVE.Utils.diskControllerMaxIDs.sata - 1) {
- return `sata${maxSata + 1}`;
- }
-
- return '';
- },
-
- // assume assigned sata disks indices are continuous, so without holes
- getMaxControllerId: function (controller) {
- let me = this;
- let view = me.getView();
- if (!controller) {
- return -1;
- }
-
- let max = view[`max${controller}`];
- if (max !== undefined) {
- return max;
- }
-
- max = -1;
- for (const key of Object.keys(me.getView().vmConfig)) {
- if (!key.toLowerCase().startsWith(controller)) {
- continue;
- }
- let idx = parseInt(key.slice(controller.length), 10);
- if (idx > max) {
- max = idx;
- }
- }
- me.lookup('diskGrid')
- .getStore()
- .each((rec) => {
- if (!rec.data.id.toLowerCase().startsWith(controller)) {
- return;
- }
- let idx = parseInt(rec.data.id.slice(controller.length), 10);
- if (idx > max) {
- max = idx;
- }
- });
- me.lookup('cdGrid')
- .getStore()
- .each((rec) => {
- if (!rec.data.id.toLowerCase().startsWith(controller) || rec.data.hidden) {
- return;
- }
- let idx = parseInt(rec.data.id.slice(controller.length), 10);
- if (idx > max) {
- max = idx;
- }
- });
-
- view[`max${controller}`] = max;
- return max;
- },
-
- renderDisk: function (value, metaData, record, rowIndex, colIndex, store, tableView) {
- let diskGrid = tableView.grid ?? this.lookup('diskGrid');
- if (diskGrid.diskMap) {
- let mappedID = diskGrid.diskMap[value];
- if (mappedID) {
- let prefix = '';
- if (mappedID === value) {
- // mapped to the same value means we ran out of IDs
- let warning = gettext('Too many disks, could not map to SATA.');
- prefix = ` `;
- }
- return `${prefix}${mappedID}`;
- }
- }
- return value;
- },
-
- refreshGrids: function () {
- this.lookup('diskGrid').reconfigure();
- this.lookup('cdGrid').reconfigure();
- this.lookup('netGrid').reconfigure();
- },
-
- onOSTypeChange: function (_cb, value) {
- let me = this;
- if (!value) {
- return;
- }
- let store = me.lookup('cdGrid').getStore();
- let collection = store.getData().getSource() ?? store.getData();
- let rec = collection.find('autogenerated', true);
-
- let isWindows = (value ?? '').startsWith('w');
- if (rec) {
- rec.set('hidden', !isWindows);
- rec.commit();
- }
- let prepareVirtio = me.lookup('prepareForVirtIO').getValue();
- let defaultScsiHw = me.getView().vmConfig.scsihw ?? '__default__';
- me.lookup('scsihw').setValue(
- prepareVirtio && isWindows ? 'virtio-scsi-single' : defaultScsiHw,
- );
-
- me.refreshGrids();
- },
-
- onPrepareVirtioChange: function (_cb, value) {
- let me = this;
- let view = me.getView();
- let diskGrid = me.lookup('diskGrid');
-
- diskGrid.diskMap = {};
- if (value) {
- const hasAdditionalSataCDROM =
- me.getViewModel().get('isWindows') && view.additionalCdIdx?.startsWith('sata');
-
- diskGrid.getStore().each((rec) => {
- let diskID = rec.data.id;
- if (!diskID.toLowerCase().startsWith('scsi')) {
- return; // continue
- }
- let offset = parseInt(diskID.slice(4), 10);
- let newIdx = offset + me.getMaxControllerId('sata') + 1;
- if (hasAdditionalSataCDROM) {
- newIdx++;
- }
- let mappedID = `sata${newIdx}`;
- if (newIdx >= PVE.Utils.diskControllerMaxIDs.sata) {
- mappedID = diskID; // map to self so that the renderer can detect that we're out of IDs
- }
- diskGrid.diskMap[diskID] = mappedID;
- });
- }
-
- let scsihw = me.lookup('scsihw');
- scsihw.suspendEvents();
- scsihw.setValue(value ? 'virtio-scsi-single' : me.getView().vmConfig.scsihw);
- scsihw.resumeEvents();
-
- me.refreshGrids();
- },
-
- onScsiHwChange: function (_field, value) {
- let me = this;
- me.getView().vmConfig.scsihw = value;
- },
-
- onUniqueMACChange: function (_cb, value) {
- let me = this;
-
- me.getViewModel().set('uniqueMACAdresses', value);
-
- me.lookup('netGrid').reconfigure();
- },
-
- renderMacAddress: function (value, metaData, record, rowIndex, colIndex, store, view) {
- let me = this;
- let vm = me.getViewModel();
-
- return !vm.get('uniqueMACAdresses') && value ? value : 'auto';
- },
-
- control: {
- 'grid field': {
- // update records from widgetcolumns
- change: function (widget, value) {
- let rec = widget.getWidgetRecord();
- rec.set(widget.name, value);
- rec.commit();
- },
- },
- 'grid[reference=diskGrid] pveStorageSelector': {
- change: 'diskStorageChange',
- },
- 'grid[reference=cdGrid] pveStorageSelector': {
- change: 'isoStorageChange',
- },
- 'field[name=osbase]': {
- change: 'onOSBaseChange',
- },
- 'panel[reference=summaryTab]': {
- activate: 'calculateConfig',
- },
- 'proxmoxcheckbox[reference=prepareForVirtIO]': {
- change: 'onPrepareVirtioChange',
- },
- 'combobox[name=ostype]': {
- change: 'onOSTypeChange',
- },
- pveScsiHwSelector: {
- change: 'onScsiHwChange',
- },
- 'proxmoxcheckbox[name=uniqueMACs]': {
- change: 'onUniqueMACChange',
- },
- },
- },
-
- viewModel: {
- data: {
- coreCount: 1,
- socketCount: 1,
- liveImport: false,
- os: 'l26',
- maxCdDrives: false,
- uniqueMACAdresses: false,
- isOva: false,
- warnings: [],
- },
-
- formulas: {
- totalCoreCount: (get) => get('socketCount') * get('coreCount'),
- hideWarnings: (get) => get('warnings').length === 0,
- warningsText: (get) =>
- '' +
- get('warnings')
- .map((w) => `${w} `)
- .join('') +
- ' ',
- liveImportNote: (get) =>
- !get('liveImport')
- ? ''
- : gettext(
- 'Note: If anything goes wrong during the live-import, new data written by the VM may be lost.',
- ),
- isWindows: (get) => (get('os') ?? '').startsWith('w'),
- liveImportText: (get) =>
- get('isOva')
- ? gettext('Starts a VM and imports the disks in the background')
- : gettext(
- 'Starts a previously stopped VM on Proxmox VE and imports the disks in the background.',
- ),
- },
- },
-
- items: [
- {
- xtype: 'tabpanel',
- defaults: {
- bodyPadding: 10,
- },
- items: [
- {
- title: gettext('General'),
- xtype: 'inputpanel',
- reference: 'mainInputPanel',
- onGetValues: function (values) {
- let me = this;
- let view = me.up('pveGuestImportWindow');
- let vm = view.getViewModel();
- let diskGrid = view.lookup('diskGrid');
-
- // from pveDiskStorageSelector
- let defaultStorage = values.hdstorage;
- let defaultFormat = values.diskformat;
- delete values.hdstorage;
- delete values.diskformat;
-
- let defaultBridge = values.defaultBridge;
- delete values.defaultBridge;
-
- let config = { ...view.vmConfig };
- Ext.apply(config, values);
-
- if (config.scsi0) {
- config.scsi0 = config.scsi0.replace(
- 'local:0,',
- 'local:0,format=qcow2,',
- );
- }
-
- let parsedBoot = PVE.Parser.parsePropertyString(config.boot ?? '');
- if (parsedBoot.order) {
- parsedBoot.order = parsedBoot.order.split(';');
- }
-
- let diskMap = diskGrid.diskMap ?? {};
- diskGrid.getStore().each((rec) => {
- if (!rec.data.enable) {
- return;
- }
- let id = diskMap[rec.data.id] ?? rec.data.id;
- if (id !== rec.data.id && parsedBoot?.order) {
- let idx = parsedBoot.order.indexOf(rec.data.id);
- if (idx !== -1) {
- parsedBoot.order[idx] = id;
- }
- }
- let data = {
- ...rec.data,
- };
- delete data.enable;
- delete data.id;
- delete data.size;
- if (!data.file) {
- data.file = defaultStorage;
- data.format = defaultFormat;
- }
- data.file += ':0'; // for our special api format
- if (id === 'efidisk0') {
- data.efitype = '4m';
- delete data['import-from'];
- }
- config[id] = PVE.Parser.printQemuDrive(data);
- });
-
- if (parsedBoot.order) {
- parsedBoot.order = parsedBoot.order.join(';');
- }
- config.boot = PVE.Parser.printPropertyString(parsedBoot);
-
- view.lookup('netGrid')
- .getStore()
- .each((rec) => {
- if (!rec.data.enable) {
- return;
- }
- let id = rec.data.id;
- let data = {
- ...rec.data,
- };
- delete data.enable;
- delete data.id;
- if (!data.bridge) {
- data.bridge = defaultBridge;
- }
- if (vm.get('uniqueMACAdresses')) {
- data.macaddr = undefined;
- }
- config[id] = PVE.Parser.printQemuNetwork(data);
- });
-
- view.lookup('cdGrid')
- .getStore()
- .each((rec) => {
- if (!rec.data.enable) {
- return;
- }
- let id = rec.data.id;
- let cd = {
- media: 'cdrom',
- file: rec.data.file ? rec.data.file : 'none',
- };
- config[id] = PVE.Parser.printPropertyString(cd);
- });
-
- config.scsihw = view.lookup('scsihw').getValue();
-
- if (view.lookup('liveimport').getValue()) {
- config['live-restore'] = 1;
- }
-
- // remove __default__ values
- for (const [key, value] of Object.entries(config)) {
- if (value === '__default__') {
- delete config[key];
- }
- }
-
- if (config['import-working-storage'] === '') {
- delete config['import-working-storage'];
- }
-
- return config;
- },
-
- column1: [
- {
- xtype: 'pveGuestIDSelector',
- name: 'vmid',
- fieldLabel: 'VM',
- guestType: 'qemu',
- loadNextFreeID: true,
- validateExists: false,
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: gettext('Sockets'),
- name: 'sockets',
- reference: 'socketsField',
- value: 1,
- minValue: 1,
- maxValue: 128,
- allowBlank: true,
- bind: {
- value: '{socketCount}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: gettext('Cores'),
- name: 'cores',
- reference: 'coresField',
- value: 1,
- minValue: 1,
- maxValue: 1024,
- allowBlank: true,
- bind: {
- value: '{coreCount}',
- },
- },
- {
- xtype: 'pveMemoryField',
- fieldLabel: gettext('Memory') + ' (MiB)',
- name: 'memory',
- reference: 'memoryField',
- value: 512,
- allowBlank: true,
- },
- { xtype: 'displayfield' }, // spacer
- { xtype: 'displayfield' }, // spacer
- {
- xtype: 'pveDiskStorageSelector',
- reference: 'defaultStorage',
- storageLabel: gettext('Default Storage'),
- storageContent: 'images',
- autoSelect: true,
- hideSize: true,
- name: 'defaultStorage',
- },
- ],
-
- column2: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Name'),
- name: 'name',
- vtype: 'DnsName',
- reference: 'nameField',
- allowBlank: true,
- },
- {
- xtype: 'CPUModelSelector',
- name: 'cpu',
- reference: 'cputype',
- value: 'x86-64-v2-AES',
- fieldLabel: gettext('CPU Type'),
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Total cores'),
- name: 'totalcores',
- isFormField: false,
- bind: {
- value: '{totalCoreCount}',
- },
- },
- {
- xtype: 'combobox',
- submitValue: false,
- name: 'osbase',
- fieldLabel: gettext('OS Type'),
- editable: false,
- queryMode: 'local',
- value: 'Linux',
- store: Object.keys(PVE.Utils.kvm_ostypes),
- },
- {
- xtype: 'combobox',
- name: 'ostype',
- reference: 'ostype',
- fieldLabel: gettext('Version'),
- value: 'l26',
- allowBlank: false,
- editable: false,
- queryMode: 'local',
- valueField: 'val',
- displayField: 'desc',
- bind: {
- value: '{os}',
- },
- store: {
- fields: ['desc', 'val'],
- data: PVE.Utils.kvm_ostypes.Linux,
- },
- },
- { xtype: 'displayfield' }, // spacer
- {
- xtype: 'PVE.form.BridgeSelector',
- reference: 'defaultBridge',
- name: 'defaultBridge',
- allowBlank: false,
- fieldLabel: gettext('Default Bridge'),
- },
- {
- xtype: 'pveStorageSelector',
- reference: 'extractionStorage',
- fieldLabel: gettext('Import Working Storage'),
- storageContent: 'images',
- emptyText: gettext('Source Storage'),
- autoSelect: false,
- name: 'import-working-storage',
- disabled: true,
- hidden: true,
- allowBlank: true,
- bind: {
- disabled: '{!isOva}',
- hidden: '{!isOva}',
- },
- },
- ],
-
- columnB: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Live Import'),
- reference: 'liveimport',
- isFormField: false,
- bind: {
- value: '{liveImport}',
- boxLabel: '{liveImportText}',
- },
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint black',
- value: gettext(
- 'Note: If anything goes wrong during the live-import, new data written by the VM may be lost.',
- ),
- bind: {
- hidden: '{!liveImport}',
- },
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Warnings'),
- labelWidth: 200,
- hidden: true,
- bind: {
- hidden: '{hideWarnings}',
- },
- },
- {
- xtype: 'displayfield',
- reference: 'warningText',
- userCls: 'pmx-hint',
- hidden: true,
- bind: {
- hidden: '{hideWarnings}',
- value: '{warningsText}',
- },
- },
- ],
- },
- {
- title: gettext('Advanced'),
- xtype: 'inputpanel',
-
- // the first inputpanel handles all values, so prevent value leakage here
- onGetValues: () => ({}),
-
- columnT: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Disks'),
- labelWidth: 200,
- },
- {
- xtype: 'grid',
- reference: 'diskGrid',
- minHeight: 60,
- maxHeight: 150,
- store: {
- data: [],
- sorters: ['id'],
- },
- columns: [
- {
- xtype: 'checkcolumn',
- header: gettext('Use'),
- width: 50,
- dataIndex: 'enable',
- listeners: {
- checkchange: function (
- _column,
- _rowIndex,
- _checked,
- record,
- ) {
- record.commit();
- },
- },
- },
- {
- text: gettext('Disk'),
- dataIndex: 'id',
- renderer: 'renderDisk',
- },
- {
- text: gettext('Source'),
- dataIndex: 'import-from',
- flex: 1,
- renderer: function (value) {
- return value.replace(/^.*\//, '');
- },
- },
- {
- text: gettext('Size'),
- dataIndex: 'size',
- renderer: (value) => {
- if (Ext.isNumeric(value)) {
- return Proxmox.Utils.render_size(value);
- }
- return value ?? Proxmox.Utils.unknownText;
- },
- },
- {
- text: gettext('Storage'),
- dataIndex: 'file',
- xtype: 'widgetcolumn',
- width: 150,
- widget: {
- xtype: 'pveStorageSelector',
- isFormField: false,
- autoSelect: false,
- allowBlank: true,
- emptyText: gettext('From Default'),
- name: 'file',
- storageContent: 'images',
- },
- onWidgetAttach: 'setNodename',
- },
- {
- text: gettext('Format'),
- dataIndex: 'format',
- xtype: 'widgetcolumn',
- width: 150,
- widget: {
- xtype: 'pveDiskFormatSelector',
- name: 'format',
- disabled: true,
- isFormField: false,
- matchFieldWidth: false,
- },
- },
- ],
- },
- ],
-
- column1: [
- {
- xtype: 'proxmoxcheckbox',
- boxLabel: gettext('Prepare for VirtIO-SCSI'),
- reference: 'prepareForVirtIO',
- name: 'prepareForVirtIO',
- submitValue: false,
- disabled: true,
- bind: {
- disabled: '{!isWindows}',
- },
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Maps SCSI disks to SATA and changes the SCSI Controller. Useful for a quicker switch to VirtIO-SCSI attached disks',
- ),
- },
- },
- ],
-
- column2: [
- {
- xtype: 'pveScsiHwSelector',
- reference: 'scsihw',
- name: 'scsihw',
- value: '__default__',
- submitValue: false,
- fieldLabel: gettext('SCSI Controller'),
- },
- ],
-
- columnB: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('CD/DVD Drives'),
- labelWidth: 200,
- },
- {
- xtype: 'grid',
- reference: 'cdGrid',
- minHeight: 60,
- maxHeight: 150,
- store: {
- data: [],
- sorters: ['id'],
- filters: [
- function (rec) {
- return !rec.data.hidden;
- },
- ],
- },
- columns: [
- {
- xtype: 'checkcolumn',
- header: gettext('Use'),
- width: 50,
- dataIndex: 'enable',
- listeners: {
- checkchange: function (
- _column,
- _rowIndex,
- _checked,
- record,
- ) {
- record.commit();
- },
- },
- },
- {
- text: gettext('Slot'),
- dataIndex: 'id',
- sorted: true,
- },
- {
- text: gettext('Storage'),
- xtype: 'widgetcolumn',
- width: 150,
- widget: {
- xtype: 'pveStorageSelector',
- isFormField: false,
- autoSelect: false,
- allowBlank: true,
- emptyText: Proxmox.Utils.noneText,
- storageContent: 'iso',
- },
- onWidgetAttach: 'setNodename',
- },
- {
- text: gettext('ISO'),
- dataIndex: 'file',
- xtype: 'widgetcolumn',
- flex: 1,
- widget: {
- xtype: 'pveFileSelector',
- name: 'file',
- isFormField: false,
- allowBlank: true,
- emptyText: Proxmox.Utils.noneText,
- storageContent: 'iso',
- },
- onWidgetAttach: 'setNodename',
- },
- ],
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Network Interfaces'),
- labelWidth: 200,
- style: {
- paddingTop: '10px',
- },
- },
- {
- xtype: 'grid',
- minHeight: 58,
- maxHeight: 150,
- reference: 'netGrid',
- store: {
- data: [],
- sorters: ['id'],
- },
- columns: [
- {
- xtype: 'checkcolumn',
- header: gettext('Use'),
- width: 50,
- dataIndex: 'enable',
- listeners: {
- checkchange: function (
- _column,
- _rowIndex,
- _checked,
- record,
- ) {
- record.commit();
- },
- },
- },
- {
- text: gettext('ID'),
- dataIndex: 'id',
- },
- {
- text: gettext('MAC address'),
- flex: 7,
- dataIndex: 'macaddr',
- renderer: 'renderMacAddress',
- },
- {
- text: gettext('Model'),
- flex: 7,
- dataIndex: 'model',
- xtype: 'widgetcolumn',
- widget: {
- xtype: 'pveNetworkCardSelector',
- name: 'model',
- isFormField: false,
- allowBlank: false,
- },
- },
- {
- text: gettext('Bridge'),
- dataIndex: 'bridge',
- xtype: 'widgetcolumn',
- flex: 6,
- widget: {
- xtype: 'PVE.form.BridgeSelector',
- name: 'bridge',
- isFormField: false,
- autoSelect: false,
- allowBlank: true,
- emptyText: gettext('From Default'),
- },
- onWidgetAttach: 'setNodename',
- },
- {
- text: gettext('VLAN Tag'),
- dataIndex: 'tag',
- xtype: 'widgetcolumn',
- flex: 5,
- widget: {
- xtype: 'pveVlanField',
- fieldLabel: undefined,
- name: 'tag',
- isFormField: false,
- allowBlank: true,
- },
- },
- ],
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'uniqueMACs',
- boxLabel: gettext('Unique MAC addresses'),
- uncheckedValue: false,
- value: false,
- },
- ],
- },
- {
- title: gettext('Resulting Config'),
- reference: 'summaryTab',
- items: [
- {
- xtype: 'grid',
- reference: 'summaryGrid',
- maxHeight: 400,
- scrollable: true,
- store: {
- model: 'KeyValue',
- sorters: [
- {
- property: 'key',
- direction: 'ASC',
- },
- ],
- },
- columns: [
- { header: 'Key', width: 150, dataIndex: 'key' },
- { header: 'Value', flex: 1, dataIndex: 'value' },
- ],
- },
- ],
- },
- ],
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.volumeName) {
- throw 'no volumeName given';
- }
-
- if (!me.storage) {
- throw 'no storage given';
- }
-
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- me.callParent();
-
- me.setTitle(
- Ext.String.format(gettext('Import Guest - {0}'), `${me.storage}:${me.volumeName}`),
- );
-
- me.lookup('defaultStorage').setNodename(me.nodename);
- me.lookup('defaultBridge').setNodename(me.nodename);
- me.lookup('extractionStorage').setNodename(me.nodename);
-
- let renderWarning = (w) => {
- const warningsCatalogue = {
- 'cdrom-image-ignored': gettext(
- "CD-ROM images cannot get imported, if required you can reconfigure the '{0}' drive in the 'Advanced' tab.",
- ),
- 'nvme-unsupported': gettext(
- "NVMe disks are currently not supported, '{0}' will get attached as SCSI",
- ),
- 'ovmf-with-lsi-unsupported': gettext(
- "OVMF is built without LSI drivers, scsi hardware was set to '{1}'",
- ),
- 'serial-port-socket-only': gettext(
- "Serial socket '{0}' will be mapped to a socket",
- ),
- 'guest-is-running': gettext(
- 'Virtual guest seems to be running on source host. Import might fail or have inconsistent state!',
- ),
- 'efi-state-lost': Ext.String.format(
- gettext(
- 'EFI state cannot be imported, you may need to reconfigure the boot order (see {0})',
- ),
- 'OVMF/UEFI Boot Entries ',
- ),
- 'ova-needs-extracting': gettext(
- 'Importing an OVA temporarily requires extra space on the working storage while extracting the contained disks for further processing.',
- ),
- };
- let message = warningsCatalogue[w.type];
- if (!w.type || !message) {
- return w.message ?? w.type ?? gettext('Unknown warning');
- }
- return Ext.String.format(message, w.key ?? 'unknown', w.value ?? 'unknown');
- };
-
- me.load({
- success: function (response) {
- let data = response.result.data;
- me.vmConfig = data['create-args'];
-
- let disks = [];
- for (const [id, value] of Object.entries(data.disks ?? {})) {
- let volid = Ext.htmlEncode('');
- let size = 'auto';
- if (Ext.isObject(value)) {
- volid = value.volid;
- size = value.size;
- }
- disks.push({
- id,
- enable: true,
- size,
- 'import-from': volid,
- format: 'raw',
- });
- }
-
- let nets = [];
- for (const [id, parsed] of Object.entries(data.net ?? {})) {
- parsed.id = id;
- parsed.enable = true;
- nets.push(parsed);
- }
-
- let cdroms = [];
- for (const [id, value] of Object.entries(me.vmConfig)) {
- if (!Ext.isString(value) || !value.match(/media=cdrom/)) {
- continue;
- }
- cdroms.push({
- enable: true,
- hidden: false,
- id,
- });
- delete me.vmConfig[id];
- }
-
- me.lookup('diskGrid').getStore().setData(disks);
- me.lookup('netGrid').getStore().setData(nets);
- me.lookup('cdGrid').getStore().setData(cdroms);
-
- let additionalCdIdx = me.getController().calculateAdditionalCDIdx();
- if (additionalCdIdx === '') {
- me.getViewModel().set('maxCdDrives', true);
- } else if (cdroms.length === 0) {
- me.additionalCdIdx = additionalCdIdx;
- me.lookup('cdGrid')
- .getStore()
- .add({
- enable: true,
- hidden: !(me.vmConfig.ostype ?? '').startsWith('w'),
- id: additionalCdIdx,
- autogenerated: true,
- });
- }
-
- me.getViewModel().set(
- 'warnings',
- data.warnings.map((w) => renderWarning(w)),
- );
- me.getViewModel().set(
- 'isOva',
- data.warnings.map((w) => w.type).indexOf('ova-needs-extracting') !== -1,
- );
-
- let osinfo = PVE.Utils.get_kvm_osinfo(me.vmConfig.ostype ?? '');
- let prepareForVirtIO =
- (me.vmConfig.ostype ?? '').startsWith('w') &&
- (me.vmConfig.bios ?? '').indexOf('ovmf') !== -1;
-
- me.setValues({
- osbase: osinfo.base,
- ...me.vmConfig,
- });
-
- me.lookup('prepareForVirtIO').setValue(prepareForVirtIO);
- },
- });
- },
-});
-Ext.define(
- 'PVE.ha.FencingView',
- {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveFencingView'],
-
- onlineHelp: 'ha_manager_fencing',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-ha-fencing',
- data: [],
- });
-
- Ext.apply(me, {
- store: store,
- stateful: false,
- viewConfig: {
- trackOver: false,
- deferEmptyText: false,
- emptyText: gettext('Use watchdog based fencing.'),
- },
- columns: [
- {
- header: gettext('Node'),
- width: 100,
- sortable: true,
- dataIndex: 'node',
- },
- {
- header: gettext('Command'),
- flex: 1,
- dataIndex: 'command',
- },
- ],
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-ha-fencing', {
- extend: 'Ext.data.Model',
- fields: ['node', 'command', 'digest'],
- });
- },
-);
-Ext.define('PVE.ha.VMResourceInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- onlineHelp: 'ha_manager_resource_config',
- vmid: undefined,
-
- onGetValues: function (values) {
- var me = this;
-
- if (values.vmid) {
- values.sid = values.vmid;
- }
- delete values.vmid;
-
- PVE.Utils.delete_if_default(values, 'failback', '1', me.isCreate);
- PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate);
- PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate);
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
- var MIN_QUORUM_VOTES = 3;
-
- var disabledHint = Ext.createWidget({
- xtype: 'displayfield', // won't get submitted by default
- userCls: 'pmx-hint',
- value:
- 'Disabling the resource will stop the guest system. ' +
- 'See the online help for details.',
- hidden: true,
- });
-
- var fewVotesHint = Ext.createWidget({
- itemId: 'fewVotesHint',
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: 'At least three quorum votes are recommended for reliable HA.',
- hidden: true,
- });
-
- Proxmox.Utils.API2Request({
- url: '/cluster/config/nodes',
- method: 'GET',
- failure: function (response) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response) {
- var nodes = response.result.data;
- var votes = 0;
- Ext.Array.forEach(nodes, function (node) {
- var vote = parseInt(node.quorum_votes, 10); // parse as base 10
- votes += vote || 0; // parseInt might return NaN, which is false
- });
-
- if (votes < MIN_QUORUM_VOTES) {
- fewVotesHint.setVisible(true);
- }
- },
- });
-
- var vmidStore = me.vmid
- ? {}
- : {
- model: 'PVEResources',
- autoLoad: true,
- sorters: 'vmid',
- filters: [
- {
- property: 'type',
- value: /lxc|qemu/,
- },
- {
- property: 'hastate',
- value: /unmanaged/,
- },
- ],
- };
-
- // value is a string above, but a number below
- me.column1 = [
- {
- xtype: me.vmid ? 'displayfield' : 'vmComboSelector',
- submitValue: me.isCreate,
- name: 'vmid',
- fieldLabel: me.vmid && me.guestType === 'ct' ? 'CT' : 'VM',
- value: me.vmid,
- store: vmidStore,
- allowBlank: false,
- validateExists: true,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'max_restart',
- fieldLabel: gettext('Max. Restart'),
- value: 1,
- minValue: 0,
- maxValue: 10,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'max_relocate',
- fieldLabel: gettext('Max. Relocate'),
- value: 1,
- minValue: 0,
- maxValue: 10,
- allowBlank: false,
- },
- ];
-
- me.column2 = [
- {
- xtype: 'proxmoxcheckbox',
- name: 'failback',
- fieldLabel: gettext('Failback'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Enable if HA resource should automatically adjust to HA rules.',
- ),
- },
- uncheckedValue: 0,
- value: 1,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'state',
- value: 'started',
- fieldLabel: gettext('Request State'),
- comboItems: [
- ['started', 'started'],
- ['stopped', 'stopped'],
- ['ignored', 'ignored'],
- ['disabled', 'disabled'],
- ],
- listeners: {
- change: function (field, newValue) {
- if (newValue === 'disabled') {
- disabledHint.setVisible(true);
- } else if (disabledHint.isVisible()) {
- disabledHint.setVisible(false);
- }
- },
- },
- },
- disabledHint,
- ];
-
- me.columnB = [
- {
- xtype: 'textfield',
- name: 'comment',
- fieldLabel: gettext('Comment'),
- },
- fewVotesHint,
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.ha.VMResourceEdit', {
- extend: 'Proxmox.window.Edit',
-
- vmid: undefined,
- guestType: undefined,
- isCreate: undefined,
- defaultFocus: undefined,
-
- initComponent: function () {
- var me = this;
-
- if (me.isCreate === undefined) {
- me.isCreate = !me.vmid;
- }
-
- if (me.isCreate) {
- me.url = '/api2/extjs/cluster/ha/resources';
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid;
- me.method = 'PUT';
- }
-
- var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', {
- isCreate: me.isCreate,
- vmid: me.vmid,
- guestType: me.guestType,
- });
-
- Ext.apply(me, {
- subject:
- gettext('Resource') +
- ': ' +
- gettext('Container') +
- '/' +
- gettext('Virtual Machine'),
- isAdd: true,
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
-
- var regex = /^(\S+):(\S+)$/;
- var res = regex.exec(values.sid);
-
- if (res[1] !== 'vm' && res[1] !== 'ct') {
- throw 'got unexpected resource type';
- }
-
- values.vmid = res[2];
-
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-Ext.define('PVE.ha.ResourcesView', {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveHAResourcesView'],
-
- onlineHelp: 'ha_manager_resources',
-
- stateful: true,
- stateId: 'grid-ha-resources',
-
- initComponent: function () {
- let me = this;
-
- if (!me.rstore) {
- throw 'no store given';
- }
-
- Proxmox.Utils.monStoreErrors(me, me.rstore);
- let store = Ext.create('Proxmox.data.DiffStore', {
- rstore: me.rstore,
- filters: {
- property: 'type',
- value: 'service',
- },
- });
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- let sid = rec.data.sid;
-
- let res = sid.match(/^(\S+):(\S+)$/);
- if (!res || (res[1] !== 'vm' && res[1] !== 'ct')) {
- console.warn(`unknown HA service ID type ${sid}`);
- return;
- }
- let [, guestType, vmid] = res;
- Ext.create('PVE.ha.VMResourceEdit', {
- guestType: guestType,
- vmid: vmid,
- listeners: {
- destroy: () => me.rstore.load(),
- },
- autoShow: true,
- });
- };
-
- let caps = Ext.state.Manager.get('GuiCap');
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- tbar: [
- {
- text: gettext('Add'),
- disabled: !caps.nodes['Sys.Console'],
- handler: function () {
- Ext.create('PVE.ha.VMResourceEdit', {
- listeners: {
- destroy: () => me.rstore.load(),
- },
- autoShow: true,
- });
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Remove'),
- selModel: sm,
- itemId: 'removeBtn',
- disabled: true,
- handler: function (btn, e, rec) {
- Ext.create('PVE.window.ConfirmRemoveResource', {
- url: `/cluster/ha/resources/${rec.data.sid}`,
- item: {
- id: rec.data.sid,
- },
- apiCallDone: () => me.rstore.load(),
- }).show();
- },
- },
- ],
- columns: [
- {
- header: 'ID',
- width: 100,
- sortable: true,
- dataIndex: 'sid',
- },
- {
- header: gettext('State'),
- width: 100,
- sortable: true,
- dataIndex: 'state',
- },
- {
- header: gettext('Node'),
- width: 100,
- sortable: true,
- dataIndex: 'node',
- },
- {
- header: gettext('Request State'),
- width: 100,
- hidden: true,
- sortable: true,
- renderer: (v) => v || 'started',
- dataIndex: 'request_state',
- },
- {
- header: gettext('CRM State'),
- width: 100,
- hidden: true,
- sortable: true,
- dataIndex: 'crm_state',
- },
- {
- header: gettext('Name'),
- width: 100,
- sortable: true,
- dataIndex: 'vname',
- },
- {
- header: gettext('Max. Restart'),
- width: 100,
- sortable: true,
- renderer: (v) => (v === undefined ? '1' : v),
- dataIndex: 'max_restart',
- },
- {
- header: gettext('Max. Relocate'),
- width: 100,
- sortable: true,
- renderer: (v) => (v === undefined ? '1' : v),
- dataIndex: 'max_relocate',
- },
- {
- header: gettext('Failback'),
- width: 100,
- sortable: true,
- dataIndex: 'failback',
- },
- {
- header: gettext('Description'),
- flex: 1,
- renderer: Ext.String.htmlEncode,
- dataIndex: 'comment',
- },
- ],
- listeners: {
- beforeselect: (grid, record, index, eOpts) => caps.nodes['Sys.Console'],
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.ha.RuleInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
-
- onlineHelp: 'ha_manager_rules',
-
- formatResourceListString: function (resources) {
- let me = this;
-
- return resources.map((vmid) => {
- if (me.resourcesStore.getById(`qemu/${vmid}`)) {
- return `vm:${vmid}`;
- } else if (me.resourcesStore.getById(`lxc/${vmid}`)) {
- return `ct:${vmid}`;
- } else {
- Ext.Msg.alert(gettext('Error'), `Could not find resource type for ${vmid}`);
- throw `Unknown resource type: ${vmid}`;
- }
- });
- },
-
- onGetValues: function (values) {
- let me = this;
-
- values.type = me.ruleType;
-
- if (me.isCreate) {
- values.rule = 'ha-rule-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
- }
-
- if (values.enable) {
- if (!me.isCreate) {
- Proxmox.Utils.assemble_field_data(values, { delete: 'disable' });
- }
- } else {
- values.disable = 1;
- }
- delete values.enable;
-
- values.resources = me.formatResourceListString(values.resources);
-
- return values;
- },
-
- initComponent: function () {
- let me = this;
-
- let resourcesStore = Ext.create('Ext.data.Store', {
- model: 'PVEResources',
- autoLoad: true,
- sorters: 'vmid',
- filters: [
- {
- property: 'type',
- value: /lxc|qemu/,
- },
- {
- property: 'hastate',
- operator: '!=',
- value: 'unmanaged',
- },
- ],
- });
-
- Ext.apply(me, {
- resourcesStore: resourcesStore,
- });
-
- me.column1 = me.column1 ?? [];
- me.column1.unshift(
- {
- xtype: 'proxmoxcheckbox',
- name: 'enable',
- fieldLabel: gettext('Enable'),
- uncheckedValue: 0,
- defaultValue: 1,
- checked: true,
- },
- {
- xtype: 'vmComboSelector',
- name: 'resources',
- fieldLabel: gettext('HA Resources'),
- store: me.resourcesStore,
- allowBlank: false,
- autoSelect: false,
- multiSelect: true,
- validateExists: true,
- },
- );
-
- me.column2 = me.column2 ?? [];
-
- me.columnB = me.columnB ?? [];
- me.columnB.unshift({
- xtype: 'textfield',
- name: 'comment',
- fieldLabel: gettext('Comment'),
- allowBlank: true,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.ha.RuleEdit', {
- extend: 'Proxmox.window.Edit',
-
- defaultFocus: undefined, // prevent the vmComboSelector to be expanded when focusing the window
-
- initComponent: function () {
- let me = this;
-
- me.isCreate = !me.ruleId;
-
- if (me.isCreate) {
- me.url = '/api2/extjs/cluster/ha/rules';
- me.method = 'POST';
- } else {
- me.url = `/api2/extjs/cluster/ha/rules/${me.ruleId}`;
- me.method = 'PUT';
- }
-
- let inputPanel = Ext.create(me.panelType, {
- ruleId: me.ruleId,
- ruleType: me.ruleType,
- isCreate: me.isCreate,
- });
-
- Ext.apply(me, {
- subject: me.panelName,
- isAdd: true,
- items: [inputPanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: (response, options) => {
- let values = response.result.data;
-
- values.resources = values.resources
- .split(',')
- .map((resource) => resource.split(':')[1]);
-
- values.enable = values.disable ? 0 : 1;
-
- inputPanel.setValues(values);
- },
- });
- }
- },
-});
-Ext.define('PVE.ha.RuleErrorsModal', {
- extend: 'Ext.window.Window',
- alias: ['widget.pveHARulesErrorsModal'],
- mixins: ['Proxmox.Mixin.CBind'],
-
- modal: true,
- scrollable: true,
- resizable: false,
-
- title: gettext('HA rule errors'),
-
- initComponent: function () {
- let me = this;
-
- let renderHARuleErrors = (errors) => {
- if (!errors) {
- return gettext('The HA rule has no errors.');
- }
-
- let errorListItemsHtml = '';
-
- for (let [opt, messages] of Object.entries(errors)) {
- errorListItemsHtml += messages
- .map((message) => `${Ext.htmlEncode(`${opt}: ${message}`)} `)
- .join('');
- }
-
- return `
-
${gettext('The HA rule has the following errors:')}
-
-
`;
- };
-
- Ext.apply(me, {
- modal: true,
- border: false,
- layout: 'fit',
- items: [
- {
- xtype: 'displayfield',
- padding: 20,
- scrollable: true,
- value: renderHARuleErrors(me.errors),
- },
- ],
- });
-
- me.callParent();
- },
-});
-Ext.define('pve-ha-rules', {
- extend: 'Ext.data.Model',
- fields: [
- 'rule',
- 'type',
- 'nodes',
- 'digest',
- 'errors',
- 'disable',
- 'comment',
- 'affinity',
- 'resources',
- {
- name: 'strict',
- type: 'boolean',
- },
- ],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/ha/rules',
- },
- idProperty: 'rule',
-});
-Ext.define('pve-ha-rules-memory', {
- extend: 'pve-ha-rules',
- proxy: {
- type: 'memory',
- },
-});
-
-Ext.define('PVE.ha.RulesBaseView', {
- extend: 'Ext.grid.GridPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- store: {
- model: 'pve-ha-rules-memory',
- cbind: {}, // empty cbind to ensure mixin iterates into filter array.
- filters: [
- {
- property: 'type',
- cbind: {
- value: '{ruleType}',
- },
- },
- ],
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.ruleType) {
- throw 'no rule type given';
- }
-
- let reloadStore = () => me.up('pveHARulesView').store.load();
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let createRuleEditWindow = (ruleId) => {
- if (!me.inputPanel) {
- throw `no editor registered for ha rule type: ${me.ruleType}`;
- }
-
- Ext.create('PVE.ha.RuleEdit', {
- panelType: `PVE.ha.rules.${me.inputPanel}`,
- panelName: me.ruleTitle,
- ruleType: me.ruleType,
- ruleId: ruleId,
- autoShow: true,
- listeners: {
- destroy: reloadStore,
- },
- });
- };
-
- let runEditor = () => {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- let { rule } = rec.data;
- createRuleEditWindow(rule);
- };
-
- let childColumns = me.columns || [];
-
- Ext.apply(me, {
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- emptyText: Ext.String.format(gettext('No {0} rules configured.'), me.ruleTitle),
- tbar: [
- {
- text: gettext('Add'),
- handler: () => createRuleEditWindow(),
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: runEditor,
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- selModel: sm,
- baseurl: '/cluster/ha/rules/',
- callback: reloadStore,
- },
- ],
- columns: [
- {
- header: gettext('ID'),
- dataIndex: 'rule',
- width: 160,
- hidden: true,
- sortable: true,
- },
- {
- header: gettext('Enabled'),
- width: 80,
- dataIndex: 'disable',
- align: 'center',
- renderer: (value) => Proxmox.Utils.renderEnabledIcon(!value),
- sortable: true,
- },
- {
- header: gettext('State'),
- xtype: 'actioncolumn',
- width: 65,
- align: 'center',
- dataIndex: 'errors',
- items: [
- {
- handler: (table, rowIndex, colIndex, item, event, { data }) => {
- if (Object.keys(data.errors ?? {}.length)) {
- Ext.create('PVE.ha.RuleErrorsModal', {
- autoShow: true,
- errors: data.errors,
- });
- }
- },
- getTip: (value) =>
- Object.keys(value ?? {}).length
- ? gettext('HA Rule has conflicts and/or errors.')
- : gettext('HA Rule is OK.'),
- getClass: (value) =>
- Object.keys(value ?? {}).length
- ? 'fa fa-exclamation-triangle'
- : 'fa fa-check pmx-unclickable',
- },
- ],
- },
- ...childColumns,
- {
- header: gettext('Comment'),
- flex: 1,
- renderer: Ext.String.htmlEncode,
- dataIndex: 'comment',
- },
- ],
- listeners: {
- activate: reloadStore,
- itemdblclick: runEditor,
- },
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.ha.RulesView', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveHARulesView',
-
- onlineHelp: 'ha_manager_rules',
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- view.store = new Ext.data.Store({
- model: 'pve-ha-rules',
- storeId: 'pve-ha-rules',
- autoLoad: true,
- });
- view.store.on('load', this.onStoreLoad, this);
- },
-
- onStoreLoad: function (store, records, success) {
- let me = this;
- let view = me.getView();
-
- for (const grid of view.query('grid[ruleType]')) {
- grid.getStore().setRecords(records);
- }
- },
- },
-
- items: [
- {
- title: gettext('HA Node Affinity Rules'),
- xtype: 'pveHANodeAffinityRulesView',
- flex: 2,
- border: 0,
- },
- {
- xtype: 'splitter',
- collapsible: false,
- performCollapse: false,
- },
- {
- title: gettext('HA Resource Affinity Rules'),
- xtype: 'pveHAResourceAffinityRulesView',
- flex: 3,
- border: 0,
- },
- ],
-});
-Ext.define('PVE.ha.Status', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveHAStatus',
-
- onlineHelp: 'chapter_ha_manager',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- initComponent: function () {
- var me = this;
-
- me.rstore = Ext.create('Proxmox.data.ObjectStore', {
- interval: me.interval,
- model: 'pve-ha-status',
- storeid: 'pve-store-' + ++Ext.idSeed,
- groupField: 'type',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/ha/status/current',
- },
- });
-
- me.items = [
- {
- xtype: 'pveHAStatusView',
- title: gettext('Status'),
- rstore: me.rstore,
- border: 0,
- collapsible: true,
- padding: '0 0 20 0',
- },
- {
- xtype: 'pveHAResourcesView',
- flex: 1,
- collapsible: true,
- title: gettext('Resources'),
- border: 0,
- rstore: me.rstore,
- },
- ];
-
- me.callParent();
- me.on('activate', me.rstore.startUpdate);
- },
-});
-Ext.define(
- 'PVE.ha.StatusView',
- {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveHAStatusView'],
-
- onlineHelp: 'chapter_ha_manager',
-
- sortPriority: {
- quorum: 1,
- master: 2,
- lrm: 3,
- service: 4,
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.rstore) {
- throw 'no rstore given';
- }
-
- Proxmox.Utils.monStoreErrors(me, me.rstore);
-
- var store = Ext.create('Proxmox.data.DiffStore', {
- rstore: me.rstore,
- sortAfterUpdate: true,
- sorters: [
- {
- sorterFn: function (rec1, rec2) {
- var p1 = me.sortPriority[rec1.data.type];
- var p2 = me.sortPriority[rec2.data.type];
- return p1 !== p2 ? (p1 > p2 ? 1 : -1) : 0;
- },
- },
- ],
- filters: {
- property: 'type',
- value: 'service',
- operator: '!=',
- },
- });
-
- Ext.apply(me, {
- store: store,
- stateful: false,
- viewConfig: {
- trackOver: false,
- },
- columns: [
- {
- header: gettext('Type'),
- width: 80,
- dataIndex: 'type',
- },
- {
- header: gettext('Status'),
- width: 80,
- flex: 1,
- dataIndex: 'status',
- },
- ],
- });
-
- me.callParent();
-
- me.on('activate', me.rstore.startUpdate);
- me.on('destroy', me.rstore.stopUpdate);
- },
- },
- function () {
- Ext.define('pve-ha-status', {
- extend: 'Ext.data.Model',
- fields: [
- 'id',
- 'type',
- 'node',
- 'status',
- 'sid',
- 'state',
- 'comment',
- {
- name: 'failback',
- type: 'boolean',
- },
- 'max_restart',
- 'max_relocate',
- 'type',
- 'crm_state',
- 'request_state',
- {
- name: 'vname',
- convert: function (value, record) {
- let sid = record.data.sid;
- if (!sid) {
- return '';
- }
-
- let res = sid.match(/^(\S+):(\S+)$/);
- if (res[1] !== 'vm' && res[1] !== 'ct') {
- return '-';
- }
- let vmid = res[2];
- return PVE.data.ResourceStore.guestName(vmid);
- },
- },
- ],
- idProperty: 'id',
- });
- },
-);
-Ext.define('PVE.ha.rules.NodeAffinityInputPanel', {
- extend: 'PVE.ha.RuleInputPanel',
-
- initComponent: function () {
- let me = this;
-
- /* TODO Node selector should be factored out in its own component */
- let update_nodefield, update_node_selection;
-
- let sm = Ext.create('Ext.selection.CheckboxModel', {
- mode: 'SIMPLE',
- listeners: {
- selectionchange: function (model, selected) {
- update_nodefield(selected);
- },
- },
- });
-
- let store = Ext.create('Ext.data.Store', {
- fields: ['node', 'mem', 'cpu', 'priority'],
- data: PVE.data.ResourceStore.getNodes(), // use already cached data to avoid an API call
- proxy: {
- type: 'memory',
- reader: { type: 'json' },
- },
- sorters: [
- {
- property: 'node',
- direction: 'ASC',
- },
- ],
- });
-
- var nodegrid = Ext.createWidget('grid', {
- store: store,
- border: true,
- height: 300,
- selModel: sm,
- columns: [
- {
- header: gettext('Node'),
- flex: 1,
- dataIndex: 'node',
- },
- {
- header: gettext('Memory usage') + ' %',
- renderer: PVE.Utils.render_mem_usage_percent,
- sortable: true,
- width: 150,
- dataIndex: 'mem',
- },
- {
- header: gettext('CPU usage'),
- renderer: Proxmox.Utils.render_cpu,
- sortable: true,
- width: 150,
- dataIndex: 'cpu',
- },
- {
- header: gettext('Priority'),
- xtype: 'widgetcolumn',
- dataIndex: 'priority',
- sortable: true,
- stopSelection: true,
- widget: {
- xtype: 'proxmoxintegerfield',
- minValue: 0,
- maxValue: 1000,
- isFormField: false,
- listeners: {
- change: function (numberfield, value, old_value) {
- let record = numberfield.getWidgetRecord();
- record.set('priority', value);
- update_nodefield(sm.getSelection());
- record.commit();
- },
- },
- },
- },
- ],
- });
-
- let nodefield = Ext.create('Ext.form.field.Hidden', {
- name: 'nodes',
- value: '',
- listeners: {
- change: function (field, value) {
- update_node_selection(value);
- },
- },
- isValid: function () {
- let value = this.getValue();
- return value && value.length !== 0;
- },
- });
-
- update_node_selection = function (string) {
- sm.deselectAll(true);
-
- string.split(',').forEach(function (e, idx, array) {
- let [node, priority] = e.split(':');
- store.each(function (record) {
- if (record.get('node') === node) {
- sm.select(record, true);
- record.set('priority', priority);
- record.commit();
- }
- });
- });
- nodegrid.reconfigure(store);
- };
-
- update_nodefield = function (selected) {
- let nodes = selected
- .map(({ data }) => data.node + (data.priority ? `:${data.priority}` : ''))
- .join(',');
-
- // nodefield change listener calls us again, which results in a
- // endless recursion, suspend the event temporary to avoid this
- nodefield.suspendEvent('change');
- nodefield.setValue(nodes);
- nodefield.resumeEvent('change');
- };
-
- me.column2 = [
- {
- xtype: 'proxmoxcheckbox',
- name: 'strict',
- fieldLabel: gettext('Strict'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Enable if the HA Resources must be restricted to the nodes.',
- ),
- },
- uncheckedValue: 0,
- defaultValue: 0,
- },
- nodefield,
- ];
-
- me.columnB = [nodegrid];
-
- me.callParent();
- },
-});
-Ext.define('PVE.ha.NodeAffinityRulesView', {
- extend: 'PVE.ha.RulesBaseView',
- alias: 'widget.pveHANodeAffinityRulesView',
-
- ruleType: 'node-affinity',
- ruleTitle: gettext('HA Node Affinity'),
- inputPanel: 'NodeAffinityInputPanel',
- faIcon: 'map-pin',
-
- stateful: true,
- stateId: 'grid-ha-node-affinity-rules',
-
- columns: [
- {
- header: gettext('Strict'),
- width: 75,
- dataIndex: 'strict',
- },
- {
- header: gettext('HA Resources'),
- flex: 3,
- dataIndex: 'resources',
- },
- {
- header: gettext('Nodes'),
- flex: 2,
- dataIndex: 'nodes',
- },
- ],
-});
-Ext.define('PVE.ha.rules.ResourceAffinityInputPanel', {
- extend: 'PVE.ha.RuleInputPanel',
-
- initComponent: function () {
- let me = this;
-
- me.column1 = [];
-
- me.column2 = [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'affinity',
- fieldLabel: gettext('Affinity'),
- allowBlank: false,
- comboItems: [
- ['positive', gettext('Keep Together')],
- ['negative', gettext('Keep Separate')],
- ],
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.ha.ResourceAffinityRulesView', {
- extend: 'PVE.ha.RulesBaseView',
- alias: 'widget.pveHAResourceAffinityRulesView',
-
- ruleType: 'resource-affinity',
- ruleTitle: gettext('HA Resource Affinity'),
- inputPanel: 'ResourceAffinityInputPanel',
- faIcon: 'link',
-
- stateful: true,
- stateId: 'grid-ha-resource-affinity-rules',
-
- columns: [
- {
- header: gettext('Affinity'),
- width: 100,
- dataIndex: 'affinity',
- },
- {
- header: gettext('HA Resources'),
- flex: 5,
- dataIndex: 'resources',
- },
- ],
-});
-Ext.define('PVE.dc.ACLAdd', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveACLAdd'],
-
- url: '/access/acl',
- method: 'PUT',
- isAdd: true,
- isCreate: true,
-
- width: 400,
-
- initComponent: function () {
- let me = this;
-
- let items = [
- {
- xtype: me.path ? 'hiddenfield' : 'pvePermPathSelector',
- name: 'path',
- value: me.path,
- allowBlank: false,
- fieldLabel: gettext('Path'),
- },
- ];
-
- if (me.aclType === 'group') {
- me.subject = gettext('Group Permission');
- items.push({
- xtype: 'pveGroupSelector',
- name: 'groups',
- fieldLabel: gettext('Group'),
- });
- } else if (me.aclType === 'user') {
- me.subject = gettext('User Permission');
- items.push({
- xtype: 'pmxUserSelector',
- name: 'users',
- fieldLabel: gettext('User'),
- });
- } else if (me.aclType === 'token') {
- me.subject = gettext('API Token Permission');
- items.push({
- xtype: 'pveTokenSelector',
- name: 'tokens',
- fieldLabel: gettext('API Token'),
- });
- } else {
- throw 'unknown ACL type';
- }
-
- items.push({
- xtype: 'pmxRoleSelector',
- name: 'roles',
- value: 'NoAccess',
- fieldLabel: gettext('Role'),
- });
-
- if (!me.path) {
- items.push({
- xtype: 'proxmoxcheckbox',
- name: 'propagate',
- checked: true,
- uncheckedValue: 0,
- fieldLabel: gettext('Propagate'),
- });
- }
-
- let ipanel = Ext.create('Proxmox.panel.InputPanel', {
- items: items,
- onlineHelp: 'pveum_permission_management',
- });
-
- Ext.apply(me, {
- items: [ipanel],
- });
-
- me.callParent();
- },
-});
-
-Ext.define(
- 'PVE.dc.ACLView',
- {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveACLView'],
-
- onlineHelp: 'chapter_user_management',
-
- stateful: true,
- stateId: 'grid-acls',
-
- // use fixed path
- path: undefined,
-
- initComponent: function () {
- let me = this;
-
- let store = Ext.create('Ext.data.Store', {
- model: 'pve-acl',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/access/acl',
- },
- sorters: {
- property: 'path',
- direction: 'ASC',
- },
- });
-
- if (me.path) {
- store.addFilter(
- Ext.create('Ext.util.Filter', {
- filterFn: (item) => item.data.path === me.path,
- }),
- );
- }
-
- let render_ugid = function (ugid, metaData, record) {
- if (record.data.type === 'group') {
- return '@' + ugid;
- }
-
- return Ext.String.htmlEncode(ugid);
- };
-
- let columns = [
- {
- header: gettext('User') + '/' + gettext('Group') + '/' + gettext('API Token'),
- flex: 1,
- sortable: true,
- renderer: render_ugid,
- dataIndex: 'ugid',
- },
- {
- header: gettext('Role'),
- flex: 1,
- sortable: true,
- dataIndex: 'roleid',
- },
- ];
-
- if (!me.path) {
- columns.unshift({
- header: gettext('Path'),
- flex: 1,
- sortable: true,
- dataIndex: 'path',
- });
- columns.push({
- header: gettext('Propagate'),
- width: 80,
- sortable: true,
- dataIndex: 'propagate',
- });
- }
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let remove_btn = new Proxmox.button.Button({
- text: gettext('Remove'),
- disabled: true,
- selModel: sm,
- confirmMsg: gettext('Are you sure you want to remove this entry'),
- handler: function (btn, event, rec) {
- var params = {
- delete: 1,
- path: rec.data.path,
- roles: rec.data.roleid,
- };
- if (rec.data.type === 'group') {
- params.groups = rec.data.ugid;
- } else if (rec.data.type === 'user') {
- params.users = rec.data.ugid;
- } else if (rec.data.type === 'token') {
- params.tokens = rec.data.ugid;
- } else {
- throw 'unknown data type';
- }
-
- Proxmox.Utils.API2Request({
- url: '/access/acl',
- params: params,
- method: 'PUT',
- waitMsgTarget: me,
- callback: () => store.load(),
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- },
- });
-
- Proxmox.Utils.monStoreErrors(me, store);
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: [
- {
- text: gettext('Add'),
- menu: {
- xtype: 'menu',
- items: [
- {
- text: gettext('Group Permission'),
- iconCls: 'fa fa-fw fa-group',
- handler: function () {
- var win = Ext.create('PVE.dc.ACLAdd', {
- aclType: 'group',
- path: me.path,
- });
- win.on('destroy', () => store.load());
- win.show();
- },
- },
- {
- text: gettext('User Permission'),
- iconCls: 'fa fa-fw fa-user',
- handler: function () {
- var win = Ext.create('PVE.dc.ACLAdd', {
- aclType: 'user',
- path: me.path,
- });
- win.on('destroy', () => store.load());
- win.show();
- },
- },
- {
- text: gettext('API Token Permission'),
- iconCls: 'fa fa-fw fa-user-o',
- handler: function () {
- let win = Ext.create('PVE.dc.ACLAdd', {
- aclType: 'token',
- path: me.path,
- });
- win.on('destroy', () => store.load());
- win.show();
- },
- },
- ],
- },
- },
- remove_btn,
- ],
- viewConfig: {
- trackOver: false,
- },
- columns: columns,
- listeners: {
- activate: () => store.load(),
- },
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-acl', {
- extend: 'Ext.data.Model',
- fields: [
- 'path',
- 'type',
- 'ugid',
- 'roleid',
- {
- name: 'propagate',
- type: 'boolean',
- },
- ],
- });
- },
-);
-Ext.define('pve-acme-accounts', {
- extend: 'Ext.data.Model',
- fields: ['name'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/acme/account',
- },
- idProperty: 'name',
-});
-
-Ext.define('pve-acme-plugins', {
- extend: 'Ext.data.Model',
- fields: ['type', 'plugin', 'api'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/acme/plugins',
- },
- idProperty: 'plugin',
-});
-
-Ext.define('PVE.dc.ACMEClusterView', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveACMEClusterView',
-
- onlineHelp: 'sysadmin_certificate_management',
-
- items: [
- {
- region: 'north',
- border: false,
- xtype: 'pmxACMEAccounts',
- acmeUrl: '/cluster/acme',
- },
- {
- region: 'center',
- border: false,
- xtype: 'pmxACMEPluginView',
- acmeUrl: '/cluster/acme',
- },
- ],
-});
-Ext.define('PVE.panel.AuthBase', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveAuthBasePanel',
-
- type: '',
-
- onGetValues: function (values) {
- let me = this;
-
- if (!values.port) {
- if (!me.isCreate) {
- Proxmox.Utils.assemble_field_data(values, { delete: 'port' });
- }
- delete values.port;
- }
-
- if (me.isCreate) {
- values.type = me.type;
- }
-
- return values;
- },
-
- initComponent: function () {
- let me = this;
-
- let options = PVE.Utils.authSchema[me.type];
-
- if (!me.column1) {
- me.column1 = [];
- }
- if (!me.column2) {
- me.column2 = [];
- }
- if (!me.columnB) {
- me.columnB = [];
- }
-
- // first field is name
- me.column1.unshift({
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'realm',
- fieldLabel: gettext('Realm'),
- value: me.realm,
- allowBlank: false,
- });
-
- // last field is default'
- me.column1.push({
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Default'),
- name: 'default',
- uncheckedValue: 0,
- });
-
- if (options.tfa) {
- // last field of column2is tfa
- me.column2.push({
- xtype: 'pveTFASelector',
- deleteEmpty: !me.isCreate,
- });
- }
-
- me.columnB.push({
- xtype: 'textfield',
- name: 'comment',
- fieldLabel: gettext('Comment'),
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.dc.AuthEditBase', {
- extend: 'Proxmox.window.Edit',
-
- onlineHelp: 'pveum_authentication_realms',
-
- isAdd: true,
-
- fieldDefaults: {
- labelWidth: 120,
- },
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.realm;
-
- if (me.isCreate) {
- me.url = '/api2/extjs/access/domains';
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs/access/domains/' + me.realm;
- me.method = 'PUT';
- }
-
- let authConfig = PVE.Utils.authSchema[me.authType];
- if (!authConfig) {
- throw 'unknown auth type';
- } else if (!authConfig.add && me.isCreate) {
- throw 'trying to add non addable realm';
- }
-
- me.subject = authConfig.name;
-
- let items;
- let bodyPadding;
- if (authConfig.syncipanel) {
- bodyPadding = 0;
- items = {
- xtype: 'tabpanel',
- region: 'center',
- layout: 'fit',
- bodyPadding: 10,
- items: [
- {
- title: gettext('General'),
- realm: me.realm,
- xtype: authConfig.ipanel,
- isCreate: me.isCreate,
- type: me.authType,
- },
- {
- title: gettext('Sync Options'),
- realm: me.realm,
- xtype: authConfig.syncipanel,
- isCreate: me.isCreate,
- type: me.authType,
- },
- ],
- };
- } else {
- items = [
- {
- realm: me.realm,
- xtype: authConfig.ipanel,
- isCreate: me.isCreate,
- type: me.authType,
- },
- ];
- }
-
- Ext.apply(me, {
- items,
- bodyPadding,
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var data = response.result.data || {};
- // just to be sure (should not happen)
- if (data.type !== me.authType) {
- me.close();
- throw 'got wrong auth type';
- }
- me.setValues(data);
- },
- });
- }
- },
-});
-Ext.define('PVE.panel.ADInputPanel', {
- extend: 'PVE.panel.AuthBase',
- xtype: 'pveAuthADPanel',
-
- initComponent: function () {
- let me = this;
-
- if (me.type !== 'ad') {
- throw 'invalid type';
- }
-
- me.column1 = [
- {
- xtype: 'textfield',
- name: 'domain',
- fieldLabel: gettext('Domain'),
- emptyText: 'company.net',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Case-Sensitive'),
- name: 'case-sensitive',
- uncheckedValue: 0,
- checked: true,
- },
- ];
-
- me.column2 = [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Server'),
- name: 'server1',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Fallback Server'),
- deleteEmpty: !me.isCreate,
- name: 'server2',
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'port',
- fieldLabel: gettext('Port'),
- minValue: 1,
- maxValue: 65535,
- emptyText: gettext('Default'),
- submitEmptyText: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'mode',
- fieldLabel: gettext('Mode'),
- editable: false,
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (LDAP)'],
- ['ldap', 'LDAP'],
- ['ldap+starttls', 'STARTTLS'],
- ['ldaps', 'LDAPS'],
- ],
- value: '__default__',
- deleteEmpty: !me.isCreate,
- listeners: {
- change: function (field, newValue) {
- let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]');
- if (newValue === 'ldap' || newValue === '__default__') {
- verifyCheckbox.disable();
- verifyCheckbox.setValue(0);
- } else {
- verifyCheckbox.enable();
- }
- },
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Verify Certificate'),
- name: 'verify',
- uncheckedValue: 0,
- disabled: true,
- checked: false,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Verify TLS certificate of the server'),
- },
- },
- ];
-
- me.advancedItems = [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Check connection'),
- name: 'check-connection',
- uncheckedValue: 0,
- checked: true,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Verify connection parameters and bind credentials on save',
- ),
- },
- },
- ];
-
- me.callParent();
- },
- onGetValues: function (values) {
- let me = this;
-
- if (!values.verify) {
- if (!me.isCreate) {
- Proxmox.Utils.assemble_field_data(values, { delete: 'verify' });
- }
- delete values.verify;
- }
-
- if (!me.isCreate) {
- // Delete old `secure` parameter. It has been deprecated in favor to the
- // `mode` parameter. Migration happens automatically in `onSetValues`.
- Proxmox.Utils.assemble_field_data(values, { delete: 'secure' });
- }
-
- return me.callParent([values]);
- },
-
- onSetValues(values) {
- let me = this;
-
- if (values.secure !== undefined && !values.mode) {
- // If `secure` is set, use it to determine the correct setting for `mode`
- // `secure` is later deleted by `onSetValues` .
- // In case *both* are set, we simply ignore `secure` and use
- // whatever `mode` is set to.
- values.mode = values.secure ? 'ldaps' : 'ldap';
- }
-
- return me.callParent([values]);
- },
-});
-Ext.define('PVE.panel.LDAPInputPanel', {
- extend: 'PVE.panel.AuthBase',
- xtype: 'pveAuthLDAPPanel',
-
- initComponent: function () {
- let me = this;
-
- if (me.type !== 'ldap') {
- throw 'invalid type';
- }
-
- me.column1 = [
- {
- xtype: 'textfield',
- name: 'base_dn',
- fieldLabel: gettext('Base Domain Name'),
- emptyText: 'CN=Users,DC=Company,DC=net',
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'user_attr',
- emptyText: 'uid / sAMAccountName',
- fieldLabel: gettext('User Attribute Name'),
- allowBlank: false,
- },
- ];
-
- me.column2 = [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Server'),
- name: 'server1',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Fallback Server'),
- deleteEmpty: !me.isCreate,
- name: 'server2',
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'port',
- fieldLabel: gettext('Port'),
- minValue: 1,
- maxValue: 65535,
- emptyText: gettext('Default'),
- submitEmptyText: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'mode',
- fieldLabel: gettext('Mode'),
- editable: false,
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (LDAP)'],
- ['ldap', 'LDAP'],
- ['ldap+starttls', 'STARTTLS'],
- ['ldaps', 'LDAPS'],
- ],
- value: '__default__',
- deleteEmpty: !me.isCreate,
- listeners: {
- change: function (field, newValue) {
- let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]');
- if (newValue === 'ldap' || newValue === '__default__') {
- verifyCheckbox.disable();
- verifyCheckbox.setValue(0);
- } else {
- verifyCheckbox.enable();
- }
- },
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Verify Certificate'),
- name: 'verify',
- uncheckedValue: 0,
- disabled: true,
- checked: false,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Verify TLS certificate of the server'),
- },
- },
- ];
-
- me.advancedItems = [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Check connection'),
- name: 'check-connection',
- uncheckedValue: 0,
- checked: true,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Verify connection parameters and bind credentials on save',
- ),
- },
- },
- ];
-
- me.callParent();
- },
- onGetValues: function (values) {
- let me = this;
-
- if (!values.verify) {
- if (!me.isCreate) {
- Proxmox.Utils.assemble_field_data(values, { delete: 'verify' });
- }
- delete values.verify;
- }
-
- if (!me.isCreate) {
- // Delete old `secure` parameter. It has been deprecated in favor to the
- // `mode` parameter. Migration happens automatically in `onSetValues`.
- Proxmox.Utils.assemble_field_data(values, { delete: 'secure' });
- }
-
- return me.callParent([values]);
- },
-
- onSetValues(values) {
- let me = this;
-
- if (values.secure !== undefined && !values.mode) {
- // If `secure` is set, use it to determine the correct setting for `mode`
- // `secure` is later deleted by `onSetValues` .
- // In case *both* are set, we simply ignore `secure` and use
- // whatever `mode` is set to.
- values.mode = values.secure ? 'ldaps' : 'ldap';
- }
-
- return me.callParent([values]);
- },
-});
-
-Ext.define('PVE.panel.LDAPSyncInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveAuthLDAPSyncPanel',
-
- editableAttributes: ['email'],
- editableDefaults: ['scope', 'enable-new'],
- default_opts: {},
- sync_attributes: {},
-
- // (de)construct the sync-attributes from the list above,
- // not touching all others
- onGetValues: function (values) {
- let me = this;
- me.editableDefaults.forEach((attr) => {
- if (values[attr]) {
- me.default_opts[attr] = values[attr];
- delete values[attr];
- } else {
- delete me.default_opts[attr];
- }
- });
- let vanished_opts = [];
- ['acl', 'entry', 'properties'].forEach((prop) => {
- if (values[`remove-vanished-${prop}`]) {
- vanished_opts.push(prop);
- }
- delete values[`remove-vanished-${prop}`];
- });
- me.default_opts['remove-vanished'] = vanished_opts.join(';');
-
- values['sync-defaults-options'] = PVE.Parser.printPropertyString(me.default_opts);
- me.editableAttributes.forEach((attr) => {
- if (values[attr]) {
- me.sync_attributes[attr] = values[attr];
- delete values[attr];
- } else {
- delete me.sync_attributes[attr];
- }
- });
- values.sync_attributes = PVE.Parser.printPropertyString(me.sync_attributes);
-
- PVE.Utils.delete_if_default(values, 'sync-defaults-options');
- PVE.Utils.delete_if_default(values, 'sync_attributes');
-
- // Force values.delete to be an array
- if (typeof values.delete === 'string') {
- values.delete = values.delete.split(',');
- }
-
- if (me.isCreate) {
- delete values.delete; // on create we cannot delete values
- }
-
- return values;
- },
-
- setValues: function (values) {
- let me = this;
- if (values.sync_attributes) {
- me.sync_attributes = PVE.Parser.parsePropertyString(values.sync_attributes);
- delete values.sync_attributes;
- me.editableAttributes.forEach((attr) => {
- if (me.sync_attributes[attr]) {
- values[attr] = me.sync_attributes[attr];
- }
- });
- }
- if (values['sync-defaults-options']) {
- me.default_opts = PVE.Parser.parsePropertyString(values['sync-defaults-options']);
- delete values.default_opts;
- me.editableDefaults.forEach((attr) => {
- if (me.default_opts[attr]) {
- values[attr] = me.default_opts[attr];
- }
- });
-
- if (me.default_opts['remove-vanished']) {
- let opts = me.default_opts['remove-vanished'].split(';');
- for (const opt of opts) {
- values[`remove-vanished-${opt}`] = 1;
- }
- }
- }
- return me.callParent([values]);
- },
-
- column1: [
- {
- xtype: 'proxmoxtextfield',
- name: 'bind_dn',
- deleteEmpty: true,
- emptyText: Proxmox.Utils.noneText,
- fieldLabel: gettext('Bind User'),
- },
- {
- xtype: 'proxmoxtextfield',
- inputType: 'password',
- name: 'password',
- emptyText: gettext('Unchanged'),
- fieldLabel: gettext('Bind Password'),
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'email',
- fieldLabel: gettext('E-Mail attribute'),
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'group_name_attr',
- deleteEmpty: true,
- fieldLabel: gettext('Groupname attr.'),
- },
- {
- xtype: 'displayfield',
- value: gettext('Default Sync Options'),
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'scope',
- emptyText: Proxmox.Utils.NoneText,
- fieldLabel: gettext('Scope'),
- value: '__default__',
- deleteEmpty: false,
- comboItems: [
- ['__default__', Proxmox.Utils.NoneText],
- ['users', gettext('Users')],
- ['groups', gettext('Groups')],
- ['both', gettext('Users and Groups')],
- ],
- },
- ],
-
- column2: [
- {
- xtype: 'proxmoxtextfield',
- name: 'user_classes',
- fieldLabel: gettext('User classes'),
- deleteEmpty: true,
- emptyText: 'inetorgperson, posixaccount, person, user',
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'group_classes',
- fieldLabel: gettext('Group classes'),
- deleteEmpty: true,
- emptyText: 'groupOfNames, group, univentionGroup, ipausergroup',
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'filter',
- fieldLabel: gettext('User Filter'),
- deleteEmpty: true,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'group_filter',
- fieldLabel: gettext('Group Filter'),
- deleteEmpty: true,
- },
- {
- // fake for spacing
- xtype: 'displayfield',
- value: ' ',
- },
- {
- xtype: 'proxmoxKVComboBox',
- value: '__default__',
- deleteEmpty: false,
- comboItems: [
- [
- '__default__',
- Ext.String.format(
- gettext('{0} ({1})'),
- Proxmox.Utils.yesText,
- Proxmox.Utils.defaultText,
- ),
- ],
- ['1', Proxmox.Utils.yesText],
- ['0', Proxmox.Utils.noText],
- ],
- name: 'enable-new',
- fieldLabel: gettext('Enable new users'),
- },
- ],
-
- columnB: [
- {
- xtype: 'fieldset',
- title: gettext('Remove Vanished Options'),
- items: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('ACL'),
- name: 'remove-vanished-acl',
- boxLabel: gettext('Remove ACLs of vanished users and groups.'),
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Entry'),
- name: 'remove-vanished-entry',
- boxLabel: gettext('Remove vanished user and group entries.'),
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Properties'),
- name: 'remove-vanished-properties',
- boxLabel: gettext('Remove vanished properties from synced users.'),
- },
- ],
- },
- ],
-});
-Ext.define('PVE.panel.OpenIDInputPanel', {
- extend: 'PVE.panel.AuthBase',
- xtype: 'pveAuthOpenIDPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onGetValues: function (values) {
- let me = this;
-
- if (!values.verify) {
- if (!me.isCreate) {
- Proxmox.Utils.assemble_field_data(values, { delete: 'verify' });
- }
- delete values.verify;
- }
-
- return me.callParent([values]);
- },
-
- columnT: [
- {
- xtype: 'textfield',
- name: 'issuer-url',
- fieldLabel: gettext('Issuer URL'),
- allowBlank: false,
- },
- ],
-
- column1: [
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Client ID'),
- name: 'client-id',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Client Key'),
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- name: 'client-key',
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'scopes',
- fieldLabel: gettext('Scopes'),
- emptyText: `${Proxmox.Utils.defaultText} (email profile)`,
- submitEmpty: false,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-
- column2: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Autocreate Users'),
- name: 'autocreate',
- value: 0,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'pmxDisplayEditField',
- name: 'username-claim',
- fieldLabel: gettext('Username Claim'),
- editConfig: {
- xtype: 'proxmoxKVComboBox',
- editable: true,
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText],
- ['subject', 'subject'],
- ['username', 'username'],
- ['email', 'email'],
- ],
- },
- cbind: {
- value: (get) => (get('isCreate') ? '__default__' : Proxmox.Utils.defaultText),
- deleteEmpty: '{!isCreate}',
- editable: '{isCreate}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Autocreate Groups'),
- name: 'groups-autocreate',
- value: 0,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'groups-claim',
- fieldLabel: gettext('Groups Claim'),
- emptyText: `${Proxmox.Utils.defaultText} ${gettext('(none)')}`,
- submitEmpty: false,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Overwrite Groups'),
- name: 'groups-overwrite',
- value: 0,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'prompt',
- fieldLabel: gettext('Prompt'),
- editable: true,
- emptyText: gettext('Auth-Provider Default'),
- comboItems: [
- ['__default__', gettext('Auth-Provider Default')],
- ['none', 'none'],
- ['login', 'login'],
- ['consent', 'consent'],
- ['select_account', 'select_account'],
- ],
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-
- advancedColumnB: [
- {
- xtype: 'proxmoxtextfield',
- name: 'acr-values',
- fieldLabel: gettext('ACR Values'),
- submitEmpty: false,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Query userinfo endpoint'),
- name: 'query-userinfo',
- checked: true,
- uncheckedValue: 0,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (me.type !== 'openid') {
- throw 'invalid type';
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.dc.AuthView', {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveAuthView'],
-
- onlineHelp: 'pveum_authentication_realms',
-
- stateful: true,
- stateId: 'grid-authrealms',
-
- viewConfig: {
- trackOver: false,
- },
-
- columns: [
- {
- header: gettext('Realm'),
- width: 100,
- sortable: true,
- dataIndex: 'realm',
- },
- {
- header: gettext('Type'),
- width: 100,
- sortable: true,
- dataIndex: 'type',
- },
- {
- header: gettext('TFA'),
- width: 100,
- sortable: true,
- dataIndex: 'tfa',
- },
- {
- header: gettext('Comment'),
- sortable: false,
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- ],
-
- store: {
- model: 'pmx-domains',
- sorters: {
- property: 'realm',
- direction: 'ASC',
- },
- },
-
- openEditWindow: function (authType, realm) {
- let me = this;
- Ext.create('PVE.dc.AuthEditBase', {
- authType,
- realm,
- listeners: {
- destroy: () => me.reload(),
- },
- }).show();
- },
-
- reload: function () {
- let me = this;
- me.getStore().load();
- },
-
- run_editor: function () {
- let me = this;
- let rec = me.getSelection()[0];
- if (!rec) {
- return;
- }
- me.openEditWindow(rec.data.type, rec.data.realm);
- },
-
- open_sync_window: function () {
- let me = this;
- let rec = me.getSelection()[0];
- if (!rec) {
- return;
- }
- Ext.create('PVE.dc.SyncWindow', {
- realm: rec.data.realm,
- listeners: {
- destroy: () => me.reload(),
- },
- }).show();
- },
-
- initComponent: function () {
- var me = this;
-
- let items = [];
- for (const [authType, config] of Object.entries(PVE.Utils.authSchema)) {
- if (!config.add) {
- continue;
- }
- items.push({
- text: config.name,
- iconCls: 'fa fa-fw ' + (config.iconCls || 'fa-address-book-o'),
- handler: () => me.openEditWindow(authType),
- });
- }
-
- Ext.apply(me, {
- tbar: [
- {
- text: gettext('Add'),
- menu: {
- items: items,
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- disabled: true,
- handler: () => me.run_editor(),
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- baseurl: '/access/domains/',
- enableFn: (rec) => PVE.Utils.authSchema[rec.data.type].add,
- callback: () => me.reload(),
- },
- '-',
- {
- xtype: 'proxmoxButton',
- text: gettext('Sync'),
- disabled: true,
- enableFn: (rec) => Boolean(PVE.Utils.authSchema[rec.data.type].syncipanel),
- handler: () => me.open_sync_window(),
- },
- ],
- listeners: {
- itemdblclick: () => me.run_editor(),
- },
- });
-
- me.callParent();
- me.reload();
- },
-});
-Ext.define('PVE.dc.BackupDiskTree', {
- extend: 'Ext.tree.Panel',
- alias: 'widget.pveBackupDiskTree',
-
- folderSort: true,
- rootVisible: false,
-
- store: {
- sorters: 'id',
- data: {},
- },
-
- tools: [
- {
- type: 'expand',
- tooltip: gettext('Expand All'),
- callback: (panel) => panel.expandAll(),
- },
- {
- type: 'collapse',
- tooltip: gettext('Collapse All'),
- callback: (panel) => panel.collapseAll(),
- },
- ],
-
- columns: [
- {
- xtype: 'treecolumn',
- text: gettext('Guest Image'),
- renderer: function (value, meta, record) {
- if (record.data.type) {
- // guest level
- let ret = value;
- if (record.data.name) {
- ret += ' (' + record.data.name + ')';
- }
- return ret;
- } else {
- // extJS needs unique IDs but we only want to show the volumes key from "vmid:key"
- return value.split(':')[1] + ' - ' + record.data.name;
- }
- },
- dataIndex: 'id',
- flex: 6,
- },
- {
- text: gettext('Type'),
- dataIndex: 'type',
- flex: 1,
- },
- {
- text: gettext('Backup Job'),
- renderer: PVE.Utils.render_backup_status,
- dataIndex: 'included',
- flex: 3,
- },
- ],
-
- reload: function () {
- let me = this;
- let sm = me.getSelectionModel();
-
- Proxmox.Utils.API2Request({
- url: `/cluster/backup/${me.jobid}/included_volumes`,
- waitMsgTarget: me,
- method: 'GET',
- failure: function (response, opts) {
- Proxmox.Utils.setErrorMask(me, response.htmlStatus);
- },
- success: function (response, opts) {
- sm.deselectAll();
- me.setRootNode(response.result.data);
- me.expandAll();
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.jobid) {
- throw 'no job id specified';
- }
-
- var sm = Ext.create('Ext.selection.TreeModel', {});
-
- Ext.apply(me, {
- selModel: sm,
- fields: [
- 'id',
- 'type',
- {
- type: 'string',
- name: 'iconCls',
- calculate: function (data) {
- var txt = 'fa x-fa-tree fa-';
- if (data.leaf && !data.type) {
- return txt + 'hdd-o';
- } else if (data.type === 'qemu') {
- return txt + 'desktop';
- } else if (data.type === 'lxc') {
- return txt + 'cube';
- } else {
- return txt + 'question-circle';
- }
- },
- },
- ],
- header: {
- items: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Search'),
- labelWidth: 50,
- emptyText: 'Name, VMID, Type',
- width: 200,
- padding: '0 5 0 0',
- enableKeyEvents: true,
- listeners: {
- buffer: 500,
- keyup: function (field) {
- let searchValue = field.getValue().toLowerCase();
- me.store.clearFilter(true);
- me.store.filterBy(function (record) {
- let data = {};
- if (record.data.depth === 0) {
- return true;
- } else if (record.data.depth === 1) {
- data = record.data;
- } else if (record.data.depth === 2) {
- data = record.parentNode.data;
- }
-
- for (const property of ['name', 'id', 'type']) {
- if (!data[property]) {
- continue;
- }
- let v = data[property].toString();
- if (v !== undefined) {
- v = v.toLowerCase();
- if (v.includes(searchValue)) {
- return true;
- }
- }
- }
- return false;
- });
- },
- },
- },
- ],
- },
- });
-
- me.callParent();
-
- me.reload();
- },
-});
-
-Ext.define('PVE.dc.BackupInfo', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveBackupInfo',
-
- viewModel: {
- data: {
- retentionType: 'none',
- hideRecipients: true,
- },
- formulas: {
- hasRetention: (get) => get('retentionType') !== 'none',
- retentionKeepAll: (get) => get('retentionType') === 'all',
- },
- },
-
- padding: '5 0 5 10',
-
- column1: [
- {
- xtype: 'displayfield',
- name: 'node',
- fieldLabel: gettext('Node'),
- renderer: (value) => value || `-- ${gettext('All')} --`,
- },
- {
- xtype: 'displayfield',
- name: 'storage',
- fieldLabel: gettext('Storage'),
- },
- {
- xtype: 'displayfield',
- name: 'schedule',
- fieldLabel: gettext('Schedule'),
- },
- {
- xtype: 'displayfield',
- name: 'next-run',
- fieldLabel: gettext('Next Run'),
- renderer: PVE.Utils.render_next_event,
- },
- {
- xtype: 'displayfield',
- name: 'selMode',
- fieldLabel: gettext('Selection mode'),
- },
- ],
- column2: [
- {
- xtype: 'displayfield',
- name: 'notification-mode',
- fieldLabel: gettext('Notification'),
- renderer: function (value) {
- value = value ?? 'auto';
- let record = this.up('pveBackupInfo')?.record;
- let mailto = record?.mailto;
- let mailnotification = record?.mailnotification ?? 'always';
-
- if ((value === 'auto' && mailto === undefined) || value === 'notification-system') {
- return gettext('Use global notification settings');
- } else if (mailnotification === 'always') {
- return gettext('Always send email');
- } else {
- return gettext('Send email on failure');
- }
- },
- },
- {
- xtype: 'displayfield',
- name: 'mailto',
- fieldLabel: gettext('Recipients'),
- hidden: true,
- bind: {
- hidden: '{hideRecipients}',
- },
- renderer: function (value) {
- if (!value) {
- return gettext('No recipients configured');
- }
-
- return value;
- },
- },
- {
- xtype: 'displayfield',
- name: 'compress',
- fieldLabel: gettext('Compression'),
- },
- {
- xtype: 'displayfield',
- name: 'mode',
- fieldLabel: gettext('Mode'),
- renderer: function (value) {
- const modeToDisplay = {
- snapshot: gettext('Snapshot'),
- stop: gettext('Stop'),
- suspend: gettext('Suspend'),
- };
- return modeToDisplay[value] ?? gettext('Unknown');
- },
- },
- {
- xtype: 'displayfield',
- name: 'enabled',
- fieldLabel: gettext('Enabled'),
- renderer: (v) =>
- PVE.Parser.parseBoolean(v.toString()) ? gettext('Yes') : gettext('No'),
- },
- {
- xtype: 'displayfield',
- name: 'pool',
- fieldLabel: gettext('Pool to backup'),
- },
- ],
-
- columnB: [
- {
- xtype: 'displayfield',
- name: 'comment',
- fieldLabel: gettext('Comment'),
- renderer: Ext.String.htmlEncode,
- },
- {
- xtype: 'fieldset',
- title: gettext('Retention Configuration'),
- layout: 'hbox',
- collapsible: true,
- defaults: {
- border: false,
- layout: 'anchor',
- flex: 1,
- },
- bind: {
- hidden: '{!hasRetention}',
- },
- items: [
- {
- padding: '0 10 0 0',
- defaults: {
- labelWidth: 110,
- },
- items: [
- {
- xtype: 'displayfield',
- name: 'keep-all',
- fieldLabel: gettext('Keep All'),
- renderer: Proxmox.Utils.format_boolean,
- bind: {
- hidden: '{!retentionKeepAll}',
- },
- },
- ].concat(
- [
- ['keep-last', gettext('Keep Last')],
- ['keep-hourly', gettext('Keep Hourly')],
- ].map((name) => ({
- xtype: 'displayfield',
- name: name[0],
- fieldLabel: name[1],
- bind: {
- hidden: '{!hasRetention || retentionKeepAll}',
- },
- })),
- ),
- },
- {
- padding: '0 0 0 10',
- defaults: {
- labelWidth: 110,
- },
- items: [
- ['keep-daily', gettext('Keep Daily')],
- ['keep-weekly', gettext('Keep Weekly')],
- ].map((name) => ({
- xtype: 'displayfield',
- name: name[0],
- fieldLabel: name[1],
- bind: {
- hidden: '{!hasRetention || retentionKeepAll}',
- },
- })),
- },
- {
- padding: '0 0 0 10',
- defaults: {
- labelWidth: 110,
- },
- items: [
- ['keep-monthly', gettext('Keep Monthly')],
- ['keep-yearly', gettext('Keep Yearly')],
- ].map((name) => ({
- xtype: 'displayfield',
- name: name[0],
- fieldLabel: name[1],
- bind: {
- hidden: '{!hasRetention || retentionKeepAll}',
- },
- })),
- },
- ],
- },
- ],
-
- setValues: function (values) {
- var me = this;
- let vm = me.getViewModel();
-
- Ext.iterate(values, function (fieldId, val) {
- let field = me.query('[isFormField][name=' + fieldId + ']')[0];
- if (field) {
- field.setValue(val);
- }
- });
-
- if (values['prune-backups']) {
- let keepValues;
- if (values['prune-backups']) {
- keepValues = values['prune-backups'];
- } else {
- keepValues = { 'keep-all': 1 };
- }
-
- vm.set('retentionType', keepValues['keep-all'] ? 'all' : 'other');
-
- // set values of all keep-X fields
- ['all', 'last', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'].forEach((time) => {
- let name = `keep-${time}`;
- me.query(`[isFormField][name=${name}]`)[0]?.setValue(keepValues[name]);
- });
- } else {
- vm.set('retentionType', 'none');
- }
-
- let notificationMode = values['notification-mode'] ?? 'auto';
- let mailto = values.mailto;
-
- let hideRecipients =
- (notificationMode === 'auto' && mailto === undefined) ||
- notificationMode === 'notification-system';
- vm.set('hideRecipients', hideRecipients);
-
- // selection Mode depends on the presence/absence of several keys
- let selModeField = me.query('[isFormField][name=selMode]')[0];
- let selMode = 'none';
- if (values.vmid) {
- selMode = gettext('Include selected VMs');
- }
- if (values.all) {
- selMode = gettext('All');
- }
- if (values.exclude) {
- selMode = gettext('Exclude selected VMs');
- }
- if (values.pool) {
- selMode = gettext('Pool based');
- }
- selModeField.setValue(selMode);
-
- if (!values.pool) {
- let poolField = me.query('[isFormField][name=pool]')[0];
- poolField.setVisible(0);
- }
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.record) {
- throw 'no data provided';
- }
- me.callParent();
-
- me.setValues(me.record);
- },
-});
-
-Ext.define('PVE.dc.BackedGuests', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveBackedGuests',
-
- stateful: true,
- stateId: 'grid-dc-backed-guests',
-
- textfilter: '',
-
- columns: [
- {
- header: gettext('Type'),
- dataIndex: 'type',
- renderer: PVE.Utils.render_resource_type,
- flex: 1,
- sortable: true,
- },
- {
- header: 'VMID',
- dataIndex: 'vmid',
- flex: 1,
- sortable: true,
- },
- {
- header: gettext('Name'),
- dataIndex: 'name',
- flex: 2,
- sortable: true,
- },
- ],
- viewConfig: {
- stripeRows: true,
- trackOver: false,
- },
-
- initComponent: function () {
- let me = this;
-
- me.store.clearFilter(true);
-
- Ext.apply(me, {
- tbar: [
- '->',
- gettext('Search') + ':',
- ' ',
- {
- xtype: 'textfield',
- width: 200,
- emptyText: 'Name, VMID, Type',
- enableKeyEvents: true,
- listeners: {
- buffer: 500,
- keyup: function (field) {
- let searchValue = field.getValue().toLowerCase();
- me.store.clearFilter(true);
- me.store.filterBy(function (record) {
- let data = record.data;
- for (const property of ['name', 'vmid', 'type']) {
- if (data[property] === null) {
- continue;
- }
- let v = data[property].toString();
- if (v !== undefined) {
- if (v.toLowerCase().includes(searchValue)) {
- return true;
- }
- }
- }
- return false;
- });
- },
- },
- },
- ],
- });
- me.callParent();
- },
-});
-Ext.define('PVE.dc.BackupEdit', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveDcBackupEdit'],
-
- mixins: ['Proxmox.Mixin.CBind'],
-
- defaultFocus: undefined,
-
- subject: gettext('Backup Job'),
- width: 720,
- bodyPadding: 0,
-
- url: '/api2/extjs/cluster/backup',
- method: 'POST',
- isCreate: true,
-
- cbindData: function () {
- let me = this;
- if (me.jobid) {
- me.isCreate = false;
- me.method = 'PUT';
- me.url += `/${me.jobid}`;
- }
- return {};
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- onGetValues: function (values) {
- let me = this;
- let isCreate = me.getView().isCreate;
- if (!values.node) {
- if (!isCreate) {
- Proxmox.Utils.assemble_field_data(values, { delete: 'node' });
- }
- delete values.node;
- }
-
- let selMode = values.selMode;
- delete values.selMode;
-
- if (selMode === 'all') {
- values.all = 1;
- values.exclude = '';
- delete values.vmid;
- } else if (selMode === 'exclude') {
- values.all = 1;
- values.exclude = values.vmid;
- delete values.vmid;
- } else if (selMode === 'pool') {
- delete values.vmid;
- }
-
- if (selMode !== 'pool') {
- delete values.pool;
- }
- return values;
- },
-
- nodeChange: function (f, value) {
- let me = this;
- me.lookup('storageSelector').setNodename(value);
- let vmgrid = me.lookup('vmgrid');
- let store = vmgrid.getStore();
-
- store.clearFilter();
- store.filterBy(function (rec) {
- return !value || rec.get('node') === value;
- });
-
- let mode = me.lookup('modeSelector').getValue();
- if (mode === 'all') {
- vmgrid.selModel.selectAll(true);
- }
- if (mode === 'pool') {
- me.selectPoolMembers();
- }
- },
-
- storageChange: function (f, v) {
- let me = this;
- let rec = f.getStore().findRecord('storage', v, 0, false, true, true);
- let compressionSelector = me.lookup('compressionSelector');
-
- if (rec?.data?.type === 'pbs') {
- compressionSelector.setValue('zstd');
- compressionSelector.setDisabled(true);
- } else if (!compressionSelector.getEditable()) {
- compressionSelector.setDisabled(false);
- }
- },
-
- selectPoolMembers: function () {
- let me = this;
- let mode = me.lookup('modeSelector').getValue();
-
- if (mode !== 'pool') {
- return;
- }
-
- let vmgrid = me.lookup('vmgrid');
- let poolid = me.lookup('poolSelector').getValue();
-
- vmgrid.getSelectionModel().deselectAll(true);
- if (!poolid) {
- return;
- }
- vmgrid.getStore().filter([
- {
- id: 'poolFilter',
- property: 'pool',
- value: poolid,
- },
- ]);
- vmgrid.selModel.selectAll(true);
- },
-
- modeChange: function (f, value, oldValue) {
- let me = this;
- let vmgrid = me.lookup('vmgrid');
- vmgrid.getStore().removeFilter('poolFilter');
-
- if (oldValue === 'all' && value !== 'all') {
- vmgrid.getSelectionModel().deselectAll(true);
- }
-
- if (value === 'all') {
- vmgrid.getSelectionModel().selectAll(true);
- }
-
- if (value === 'pool') {
- me.selectPoolMembers();
- }
- },
-
- compressionChange: function (f, value, oldValue) {
- this.getView().lookup('backupAdvanced').updateCompression(value, f.isDisabled());
- },
-
- compressionDisable: function (f) {
- this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), true);
- },
-
- compressionEnable: function (f) {
- this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), false);
- },
-
- prepareValues: function (data) {
- let me = this;
- let viewModel = me.getViewModel();
-
- if (data.exclude) {
- data.vmid = data.exclude;
- data.selMode = 'exclude';
- } else if (data.all) {
- data.vmid = '';
- data.selMode = 'all';
- } else if (data.pool) {
- data.selMode = 'pool';
- data.selPool = data.pool;
- } else {
- data.selMode = 'include';
- }
- viewModel.set('selMode', data.selMode);
-
- if (data['prune-backups']) {
- Object.assign(data, data['prune-backups']);
- delete data['prune-backups'];
- }
-
- if (data['notes-template']) {
- data['notes-template'] = PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
- }
-
- if (data.performance) {
- Object.assign(data, data.performance);
- delete data.performance;
- }
-
- return data;
- },
-
- init: function (view) {
- let me = this;
-
- if (view.isCreate) {
- me.lookup('modeSelector').setValue('include');
- } else {
- view.load({
- success: function (response, _options) {
- let values = me.prepareValues(response.result.data);
- view.setValues(values);
- },
- });
- }
- },
- },
-
- viewModel: {
- data: {
- selMode: 'include',
- },
-
- formulas: {
- poolMode: (get) => get('selMode') === 'pool',
- disableVMSelection: (get) =>
- get('selMode') !== 'include' && get('selMode') !== 'exclude',
- },
- },
-
- items: [
- {
- xtype: 'tabpanel',
- region: 'center',
- layout: 'fit',
- bodyPadding: 10,
- items: [
- {
- title: gettext('General'),
- xtype: 'inputpanel',
- onlineHelp: 'chapter_vzdump',
- column1: [
- {
- xtype: 'pveNodeSelector',
- name: 'node',
- fieldLabel: gettext('Node'),
- allowBlank: true,
- editable: true,
- autoSelect: false,
- emptyText: '-- ' + gettext('All') + ' --',
- listeners: {
- change: 'nodeChange',
- },
- },
- {
- xtype: 'pveStorageSelector',
- reference: 'storageSelector',
- fieldLabel: gettext('Storage'),
- clusterView: true,
- storageContent: 'backup',
- allowBlank: false,
- name: 'storage',
- listeners: {
- change: 'storageChange',
- },
- },
- {
- xtype: 'pveCalendarEvent',
- fieldLabel: gettext('Schedule'),
- allowBlank: false,
- name: 'schedule',
- },
- {
- xtype: 'proxmoxKVComboBox',
- reference: 'modeSelector',
- comboItems: [
- ['include', gettext('Include selected VMs')],
- ['all', gettext('All')],
- ['exclude', gettext('Exclude selected VMs')],
- ['pool', gettext('Pool based')],
- ],
- fieldLabel: gettext('Selection mode'),
- name: 'selMode',
- value: '',
- bind: {
- value: '{selMode}',
- },
- listeners: {
- change: 'modeChange',
- },
- },
- {
- xtype: 'pvePoolSelector',
- reference: 'poolSelector',
- fieldLabel: gettext('Pool to backup'),
- hidden: true,
- allowBlank: false,
- name: 'pool',
- listeners: {
- change: 'selectPoolMembers',
- },
- bind: {
- hidden: '{!poolMode}',
- disabled: '{!poolMode}',
- },
- },
- ],
- column2: [
- {
- xtype: 'pveBackupCompressionSelector',
- reference: 'compressionSelector',
- fieldLabel: gettext('Compression'),
- name: 'compress',
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- value: 'zstd',
- listeners: {
- change: 'compressionChange',
- disable: 'compressionDisable',
- enable: 'compressionEnable',
- },
- },
- {
- xtype: 'pveBackupModeSelector',
- fieldLabel: gettext('Mode'),
- value: 'snapshot',
- name: 'mode',
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Enable'),
- name: 'enabled',
- uncheckedValue: 0,
- defaultValue: 1,
- checked: true,
- },
- ],
- columnB: [
- {
- xtype: 'proxmoxtextfield',
- name: 'comment',
- fieldLabel: gettext('Job Comment'),
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Description of the job'),
- },
- },
- {
- xtype: 'vmselector',
- reference: 'vmgrid',
- height: 300,
- name: 'vmid',
- disabled: true,
- allowBlank: false,
- columnSelection: ['vmid', 'node', 'status', 'name', 'type'],
- bind: {
- disabled: '{disableVMSelection}',
- },
- },
- ],
- onGetValues: function (values) {
- return this.up('window').getController().onGetValues(values);
- },
- },
- {
- xtype: 'pveBackupNotificationOptionsPanel',
- title: gettext('Notifications'),
- cbind: {
- isCreate: '{isCreate}',
- },
- },
- {
- xtype: 'pveBackupJobPrunePanel',
- title: gettext('Retention'),
- cbind: {
- isCreate: '{isCreate}',
- },
- keepAllDefaultForCreate: false,
- showPBSHint: false,
- fallbackHintHtml: gettext(
- "Without any keep option, the storage's configuration or node's vzdump.conf is used as fallback",
- ),
- },
- {
- xtype: 'inputpanel',
- onlineHelp: 'chapter_vzdump',
- title: gettext('Note Template'),
- region: 'center',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- onGetValues: function (values) {
- if (values['notes-template']) {
- values['notes-template'] = PVE.Utils.escapeNotesTemplate(
- values['notes-template'],
- );
- }
- return values;
- },
- items: [
- {
- xtype: 'textarea',
- name: 'notes-template',
- fieldLabel: gettext('Backup Notes'),
- height: 100,
- maxLength: 512,
- cbind: {
- deleteEmpty: '{!isCreate}',
- value: (get) => (get('isCreate') ? '{{guestname}}' : undefined),
- },
- },
- {
- xtype: 'box',
- style: {
- margin: '8px 0px',
- 'line-height': '1.5em',
- },
- html:
- gettext('The notes are added to each backup created by this job.') +
- ' ' +
- Ext.String.format(
- gettext('Possible template variables are: {0}'),
- PVE.Utils.notesTemplateVars
- .map((v) => `{{${v}}}`)
- .join(', '),
- ),
- },
- ],
- },
- {
- xtype: 'pveBackupAdvancedOptionsPanel',
- onlineHelp: 'chapter_vzdump',
- reference: 'backupAdvanced',
- title: gettext('Advanced'),
- cbind: {
- isCreate: '{isCreate}',
- },
- },
- ],
- },
- ],
-});
-
-Ext.define(
- 'PVE.dc.BackupView',
- {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveDcBackupView'],
-
- onlineHelp: 'chapter_vzdump',
-
- allText: '-- ' + gettext('All') + ' --',
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pve-cluster-backup',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/backup',
- },
- });
-
- let not_backed_store = new Ext.data.Store({
- sorters: 'vmid',
- proxy: {
- type: 'proxmox',
- url: 'api2/json/cluster/backup-info/not-backed-up',
- },
- });
-
- let noBackupJobInfoButton;
- let reload = function () {
- store.load();
- not_backed_store.load({
- callback: (records) => noBackupJobInfoButton.setVisible(records.length > 0),
- });
- };
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
-
- Ext.create('PVE.dc.BackupEdit', {
- autoShow: true,
- jobid: rec.data.id,
- listeners: {
- destroy: () => reload(),
- },
- });
- };
-
- let run_detail = function () {
- let record = sm.getSelection()[0];
- if (!record) {
- return;
- }
- Ext.create('Ext.window.Window', {
- modal: true,
- width: 800,
- height: Ext.getBody().getViewSize().height > 1000 ? 800 : 600, // factor out as common infra?
- resizable: true,
- layout: 'fit',
- title: gettext('Backup Details'),
- items: [
- {
- xtype: 'panel',
- region: 'center',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- items: [
- {
- xtype: 'pveBackupInfo',
- flex: 0,
- layout: 'fit',
- record: record.data,
- },
- {
- xtype: 'pveBackupDiskTree',
- title: gettext('Included disks'),
- flex: 1,
- jobid: record.data.id,
- },
- ],
- },
- ],
- }).show();
- };
-
- let run_backup_now = function (job) {
- job = Ext.clone(job);
-
- let jobNode = job.node;
- // Remove properties related to scheduling
- delete job.enabled;
- delete job.starttime;
- delete job.dow;
- delete job.id;
- delete job.schedule;
- delete job.type;
- delete job.node;
- delete job.comment;
- delete job['next-run'];
- delete job['repeat-missed'];
- job.all = job.all === true ? 1 : 0;
-
- ['performance', 'prune-backups', 'fleecing'].forEach((key) => {
- if (job[key]) {
- job[key] = PVE.Parser.printPropertyString(job[key]);
- }
- });
-
- let allNodes = PVE.data.ResourceStore.getNodes();
- let nodes = allNodes
- .filter((node) => node.status === 'online')
- .map((node) => node.node);
- let errors = [];
-
- if (jobNode !== undefined) {
- if (!nodes.includes(jobNode)) {
- Ext.Msg.alert(
- 'Error',
- "Node '" + jobNode + "' from backup job isn't online!",
- );
- return;
- }
- nodes = [jobNode];
- } else {
- let unkownNodes = allNodes.filter((node) => node.status !== 'online');
- if (unkownNodes.length > 0) {
- errors.push(
- unkownNodes.map(
- (node) => node.node + ': ' + gettext('Node is offline'),
- ),
- );
- }
- }
- let jobTotalCount = nodes.length,
- jobsStarted = 0;
-
- Ext.Msg.show({
- title: gettext('Please wait...'),
- closable: false,
- progress: true,
- progressText: '0/' + jobTotalCount,
- });
-
- let postRequest = function () {
- jobsStarted++;
- Ext.Msg.updateProgress(
- jobsStarted / jobTotalCount,
- jobsStarted + '/' + jobTotalCount,
- );
-
- if (jobsStarted === jobTotalCount) {
- Ext.Msg.hide();
- if (errors.length > 0) {
- Ext.Msg.alert(
- 'Error',
- 'Some errors have been encountered: ' + errors.join(' '),
- );
- }
- }
- };
-
- nodes.forEach((node) =>
- Proxmox.Utils.API2Request({
- url: '/nodes/' + node + '/vzdump',
- method: 'POST',
- params: job,
- failure: function (response, opts) {
- errors.push(node + ': ' + response.htmlStatus);
- postRequest();
- },
- success: postRequest,
- }),
- );
- };
-
- var edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- var run_btn = new Proxmox.button.Button({
- text: gettext('Run now'),
- disabled: true,
- selModel: sm,
- handler: function () {
- var rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
-
- Ext.Msg.show({
- title: gettext('Confirm'),
- icon: Ext.Msg.QUESTION,
- msg: gettext('Start the selected backup job now?'),
- buttons: Ext.Msg.YESNO,
- callback: function (btn) {
- if (btn !== 'yes') {
- return;
- }
- run_backup_now(rec.data);
- },
- });
- },
- });
-
- var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/cluster/backup',
- callback: function () {
- reload();
- },
- });
-
- var detail_btn = new Proxmox.button.Button({
- text: gettext('Job Detail'),
- disabled: true,
- tooltip: gettext(
- 'Show job details and which guests and volumes are affected by the backup job',
- ),
- selModel: sm,
- handler: run_detail,
- });
-
- noBackupJobInfoButton = new Proxmox.button.Button({
- text: `${gettext('Show')}: ${gettext('Guests Without Backup Job')}`,
- tooltip: gettext('Some guests are not covered by any backup job.'),
- iconCls: 'fa fa-fw fa-exclamation-circle',
- hidden: true,
- handler: () => {
- Ext.create('Ext.window.Window', {
- autoShow: true,
- modal: true,
- width: 600,
- height: 500,
- resizable: true,
- layout: 'fit',
- title: gettext('Guests Without Backup Job'),
- items: [
- {
- xtype: 'panel',
- region: 'center',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- items: [
- {
- xtype: 'pveBackedGuests',
- flex: 1,
- layout: 'fit',
- store: not_backed_store,
- },
- ],
- },
- ],
- });
- },
- });
-
- Proxmox.Utils.monStoreErrors(me, store);
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- stateful: true,
- stateId: 'grid-dc-backup',
- viewConfig: {
- trackOver: false,
- },
- dockedItems: [
- {
- xtype: 'toolbar',
- overflowHandler: 'scroller',
- dock: 'top',
- items: [
- {
- text: gettext('Add'),
- handler: function () {
- var win = Ext.create('PVE.dc.BackupEdit', {});
- win.on('destroy', reload);
- win.show();
- },
- },
- '-',
- remove_btn,
- edit_btn,
- detail_btn,
- '-',
- run_btn,
- '->',
- noBackupJobInfoButton,
- '-',
- {
- xtype: 'proxmoxButton',
- selModel: null,
- text: gettext('Schedule Simulator'),
- handler: () => {
- let record = sm.getSelection()[0];
- let schedule;
- if (record) {
- schedule = record.data.schedule;
- }
- Ext.create('PVE.window.ScheduleSimulator', {
- autoShow: true,
- schedule,
- });
- },
- },
- ],
- },
- ],
- columns: [
- {
- header: gettext('Enabled'),
- width: 80,
- dataIndex: 'enabled',
- align: 'center',
- renderer: Proxmox.Utils.renderEnabledIcon,
- sortable: true,
- },
- {
- header: gettext('ID'),
- dataIndex: 'id',
- hidden: true,
- },
- {
- header: gettext('Node'),
- width: 100,
- sortable: true,
- dataIndex: 'node',
- renderer: function (value) {
- if (value) {
- return value;
- }
- return me.allText;
- },
- },
- {
- header: gettext('Schedule'),
- width: 150,
- dataIndex: 'schedule',
- },
- {
- text: gettext('Next Run'),
- dataIndex: 'next-run',
- width: 150,
- renderer: PVE.Utils.render_next_event,
- },
- {
- header: gettext('Storage'),
- width: 100,
- sortable: true,
- dataIndex: 'storage',
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- renderer: Ext.htmlEncode,
- sorter: (a, b) =>
- (a.data.comment || '').localeCompare(b.data.comment || ''),
- flex: 1,
- },
- {
- header: gettext('Retention'),
- dataIndex: 'prune-backups',
- renderer: (v) =>
- v
- ? PVE.Parser.printPropertyString(v)
- : gettext('Fallback from storage config'),
- flex: 2,
- },
- {
- header: gettext('Selection'),
- flex: 4,
- sortable: false,
- dataIndex: 'vmid',
- renderer: PVE.Utils.render_backup_selection,
- },
- ],
- listeners: {
- activate: reload,
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-cluster-backup', {
- extend: 'Ext.data.Model',
- fields: [
- 'id',
- 'compress',
- 'dow',
- 'exclude',
- 'mailto',
- 'mode',
- 'node',
- 'pool',
- 'prune-backups',
- 'starttime',
- 'storage',
- 'vmid',
- { name: 'enabled', type: 'boolean' },
- { name: 'all', type: 'boolean' },
- ],
- });
- },
-);
-Ext.define('pve-cluster-nodes', {
- extend: 'Ext.data.Model',
- fields: [
- 'node',
- { type: 'integer', name: 'nodeid' },
- 'ring0_addr',
- 'ring1_addr',
- { type: 'integer', name: 'quorum_votes' },
- ],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/config/nodes',
- },
- idProperty: 'nodeid',
-});
-
-Ext.define('pve-cluster-info', {
- extend: 'Ext.data.Model',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/config/join',
- },
-});
-
-Ext.define('PVE.ClusterAdministration', {
- extend: 'Ext.panel.Panel',
- xtype: 'pveClusterAdministration',
-
- title: gettext('Cluster Administration'),
- onlineHelp: 'chapter_pvecm',
-
- border: false,
- defaults: { border: false },
-
- viewModel: {
- parent: null,
- data: {
- totem: {},
- nodelist: [],
- preferred_node: {
- name: '',
- fp: '',
- addr: '',
- },
- isInCluster: false,
- nodecount: 0,
- },
- },
-
- items: [
- {
- xtype: 'panel',
- title: gettext('Cluster Information'),
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- view.store = Ext.create('Proxmox.data.UpdateStore', {
- autoStart: true,
- interval: 15 * 1000,
- storeid: 'pve-cluster-info',
- model: 'pve-cluster-info',
- });
- view.store.on('load', this.onLoad, this);
- view.on('destroy', view.store.stopUpdate);
- },
-
- onLoad: function (store, records, success, operation) {
- let vm = this.getViewModel();
-
- let data = records?.[0]?.data;
- if (!success || !data || !data.nodelist?.length) {
- let error = operation.getError();
- if (error) {
- let msg = Proxmox.Utils.getResponseErrorMessage(error);
- if (error.status !== 424 && !msg.match(/node is not in a cluster/i)) {
- // an actual error, not just the "not in a cluster one", so show it!
- Proxmox.Utils.setErrorMask(this.getView(), msg);
- }
- }
- vm.set('totem', {});
- vm.set('isInCluster', false);
- vm.set('nodelist', []);
- vm.set('preferred_node', {
- name: '',
- addr: '',
- fp: '',
- });
- return;
- }
- vm.set('totem', data.totem);
- vm.set('isInCluster', !!data.totem.cluster_name);
- vm.set('nodelist', data.nodelist);
-
- let nodeinfo = data.nodelist.find((el) => el.name === data.preferred_node);
-
- let links = {};
- let ring_addr = [];
- PVE.Utils.forEachCorosyncLink(nodeinfo, (num, link) => {
- links[num] = link;
- ring_addr.push(link);
- });
-
- vm.set('preferred_node', {
- name: data.preferred_node,
- addr: nodeinfo.pve_addr,
- peerLinks: links,
- ring_addr: ring_addr,
- fp: nodeinfo.pve_fp,
- });
- },
-
- onCreate: function () {
- let view = this.getView();
- view.store.stopUpdate();
- Ext.create('PVE.ClusterCreateWindow', {
- autoShow: true,
- listeners: {
- destroy: function () {
- view.store.startUpdate();
- },
- },
- });
- },
-
- onClusterInfo: function () {
- let vm = this.getViewModel();
- Ext.create('PVE.ClusterInfoWindow', {
- autoShow: true,
- joinInfo: {
- ipAddress: vm.get('preferred_node.addr'),
- fingerprint: vm.get('preferred_node.fp'),
- peerLinks: vm.get('preferred_node.peerLinks'),
- ring_addr: vm.get('preferred_node.ring_addr'),
- totem: vm.get('totem'),
- },
- });
- },
-
- onJoin: function () {
- let view = this.getView();
- view.store.stopUpdate();
- Ext.create('PVE.ClusterJoinNodeWindow', {
- autoShow: true,
- listeners: {
- destroy: function () {
- view.store.startUpdate();
- },
- },
- });
- },
- },
- tbar: [
- {
- text: gettext('Create Cluster'),
- reference: 'createButton',
- handler: 'onCreate',
- bind: {
- disabled: '{isInCluster}',
- },
- },
- {
- text: gettext('Join Information'),
- reference: 'addButton',
- handler: 'onClusterInfo',
- bind: {
- disabled: '{!isInCluster}',
- },
- },
- {
- text: gettext('Join Cluster'),
- reference: 'joinButton',
- handler: 'onJoin',
- bind: {
- disabled: '{isInCluster}',
- },
- },
- ],
- layout: 'hbox',
- bodyPadding: 5,
- items: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Cluster Name'),
- bind: {
- value: '{totem.cluster_name}',
- hidden: '{!isInCluster}',
- },
- flex: 1,
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Config Version'),
- bind: {
- value: '{totem.config_version}',
- hidden: '{!isInCluster}',
- },
- flex: 1,
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Number of Nodes'),
- labelWidth: 120,
- bind: {
- value: '{nodecount}',
- hidden: '{!isInCluster}',
- },
- flex: 1,
- },
- {
- xtype: 'displayfield',
- value: gettext('Standalone node - no cluster defined'),
- bind: {
- hidden: '{isInCluster}',
- },
- flex: 1,
- },
- ],
- },
- {
- xtype: 'grid',
- title: gettext('Cluster Nodes'),
- autoScroll: true,
- enableColumnHide: false,
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- view.rstore = Ext.create('Proxmox.data.UpdateStore', {
- autoLoad: true,
- xtype: 'update',
- interval: 5 * 1000,
- autoStart: true,
- storeid: 'pve-cluster-nodes',
- model: 'pve-cluster-nodes',
- });
- view.setStore(
- Ext.create('Proxmox.data.DiffStore', {
- rstore: view.rstore,
- sorters: {
- property: 'nodeid',
- direction: 'ASC',
- },
- }),
- );
- Proxmox.Utils.monStoreErrors(view, view.rstore);
- view.rstore.on('load', this.onLoad, this);
- view.on('destroy', view.rstore.stopUpdate);
- },
-
- onLoad: function (store, records, success) {
- let view = this.getView();
- let vm = this.getViewModel();
-
- if (!success || !records || !records.length) {
- vm.set('nodecount', 0);
- return;
- }
- vm.set('nodecount', records.length);
-
- // show/hide columns according to used links
- let linkIndex = view.columns.length;
- Ext.each(view.columns, (col, i) => {
- if (col.linkNumber !== undefined) {
- col.setHidden(true);
- // save offset at which link columns start, so we can address them directly below
- if (i < linkIndex) {
- linkIndex = i;
- }
- }
- });
-
- PVE.Utils.forEachCorosyncLink(records[0].data, (linknum, val) => {
- if (linknum > 7) {
- return;
- }
- view.columns[linkIndex + linknum].setHidden(false);
- });
- },
- },
- columns: {
- items: [
- {
- header: gettext('Nodename'),
- hidden: false,
- dataIndex: 'name',
- },
- {
- header: gettext('ID'),
- minWidth: 100,
- width: 100,
- flex: 0,
- hidden: false,
- dataIndex: 'nodeid',
- },
- {
- header: gettext('Votes'),
- minWidth: 100,
- width: 100,
- flex: 0,
- hidden: false,
- dataIndex: 'quorum_votes',
- },
- {
- header: Ext.String.format(gettext('Link {0}'), 0),
- dataIndex: 'ring0_addr',
- linkNumber: 0,
- },
- {
- header: Ext.String.format(gettext('Link {0}'), 1),
- dataIndex: 'ring1_addr',
- linkNumber: 1,
- },
- {
- header: Ext.String.format(gettext('Link {0}'), 2),
- dataIndex: 'ring2_addr',
- linkNumber: 2,
- },
- {
- header: Ext.String.format(gettext('Link {0}'), 3),
- dataIndex: 'ring3_addr',
- linkNumber: 3,
- },
- {
- header: Ext.String.format(gettext('Link {0}'), 4),
- dataIndex: 'ring4_addr',
- linkNumber: 4,
- },
- {
- header: Ext.String.format(gettext('Link {0}'), 5),
- dataIndex: 'ring5_addr',
- linkNumber: 5,
- },
- {
- header: Ext.String.format(gettext('Link {0}'), 6),
- dataIndex: 'ring6_addr',
- linkNumber: 6,
- },
- {
- header: Ext.String.format(gettext('Link {0}'), 7),
- dataIndex: 'ring7_addr',
- linkNumber: 7,
- },
- ],
- defaults: {
- flex: 1,
- hidden: true,
- minWidth: 150,
- },
- },
- },
- ],
-});
-Ext.define('PVE.ClusterCreateWindow', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveClusterCreateWindow',
-
- title: gettext('Create Cluster'),
- width: 600,
-
- method: 'POST',
- url: '/cluster/config',
-
- isCreate: true,
- subject: gettext('Cluster'),
- showTaskViewer: true,
-
- onlineHelp: 'pvecm_create_cluster',
-
- items: {
- xtype: 'inputpanel',
- items: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Cluster Name'),
- allowBlank: false,
- maxLength: 15,
- name: 'clustername',
- },
- {
- xtype: 'fieldcontainer',
- fieldLabel: gettext('Cluster Network'),
- items: [
- {
- xtype: 'pveCorosyncLinkEditor',
- infoText: gettext(
- 'Multiple links are used as failover, lower numbers have higher priority.',
- ),
- name: 'links',
- },
- ],
- },
- ],
- },
-});
-
-Ext.define('PVE.ClusterInfoWindow', {
- extend: 'Ext.window.Window',
- xtype: 'pveClusterInfoWindow',
- mixins: ['Proxmox.Mixin.CBind'],
-
- width: 800,
- modal: true,
- resizable: false,
- title: gettext('Cluster Join Information'),
-
- joinInfo: {
- ipAddress: undefined,
- fingerprint: undefined,
- totem: {},
- },
-
- items: [
- {
- xtype: 'component',
- border: false,
- padding: '10 10 10 10',
- html: gettext('Copy the Join Information here and use it on the node you want to add.'),
- },
- {
- xtype: 'container',
- layout: 'form',
- border: false,
- padding: '0 10 10 10',
- items: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('IP Address'),
- cbind: {
- value: '{joinInfo.ipAddress}',
- },
- editable: false,
- },
- {
- xtype: 'textfield',
- fieldLabel: gettext('Fingerprint'),
- cbind: {
- value: '{joinInfo.fingerprint}',
- },
- editable: false,
- },
- {
- xtype: 'textarea',
- inputId: 'pveSerializedClusterInfo',
- fieldLabel: gettext('Join Information'),
- grow: true,
- cbind: {
- joinInfo: '{joinInfo}',
- },
- editable: false,
- listeners: {
- afterrender: function (field) {
- if (!field.joinInfo) {
- return;
- }
- var jsons = Ext.JSON.encode(field.joinInfo);
- var base64s = Ext.util.Base64.encode(jsons);
- field.setValue(base64s);
- },
- },
- },
- ],
- },
- ],
- dockedItems: [
- {
- dock: 'bottom',
- xtype: 'toolbar',
- items: [
- {
- xtype: 'button',
- handler: function (b) {
- var el = document.getElementById('pveSerializedClusterInfo');
- el.select();
- document.execCommand('copy');
- },
- text: gettext('Copy Information'),
- iconCls: 'fa fa-clipboard',
- },
- ],
- },
- ],
-});
-
-Ext.define('PVE.ClusterJoinNodeWindow', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveClusterJoinNodeWindow',
-
- title: gettext('Cluster Join'),
- width: 800,
-
- method: 'POST',
- url: '/cluster/config/join',
-
- defaultFocus: 'textarea[name=serializedinfo]',
- isCreate: true,
- bind: {
- submitText: '{submittxt}',
- },
- showTaskViewer: true,
-
- onlineHelp: 'pvecm_join_node_to_cluster',
-
- viewModel: {
- parent: null,
- data: {
- info: {
- fp: '',
- ip: '',
- clusterName: '',
- },
- hasAssistedInfo: false,
- },
- formulas: {
- submittxt: function (get) {
- let cn = get('info.clusterName');
- if (cn) {
- return Ext.String.format(gettext('Join {0}'), `'${cn}'`);
- }
- return gettext('Join');
- },
- showClusterFields: (get) => {
- let manualMode = !get('assistedEntry.checked');
- return get('hasAssistedInfo') || manualMode;
- },
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
- control: {
- '#': {
- close: function () {
- delete PVE.Utils.silenceAuthFailures;
- },
- },
- 'proxmoxcheckbox[name=assistedEntry]': {
- change: 'onInputTypeChange',
- },
- 'textarea[name=serializedinfo]': {
- change: 'recomputeSerializedInfo',
- enable: 'resetField',
- },
- textfield: {
- disable: 'resetField',
- },
- },
- resetField: function (field) {
- field.reset();
- },
- onInputTypeChange: function (field, assistedInput) {
- let linkEditor = this.lookup('linkEditor');
-
- // this also clears all links
- linkEditor.setAllowNumberEdit(!assistedInput);
-
- if (!assistedInput) {
- linkEditor.setInfoText();
- linkEditor.setDefaultLinks();
- }
- },
- recomputeSerializedInfo: function (field, value) {
- let vm = this.getViewModel();
-
- let assistedEntryBox = this.lookup('assistedEntry');
-
- if (!assistedEntryBox.getValue()) {
- // not in assisted entry mode, nothing to do
- vm.set('hasAssistedInfo', false);
- return;
- }
-
- let linkEditor = this.lookup('linkEditor');
-
- let jsons = Ext.util.Base64.decode(value);
- let joinInfo = Ext.JSON.decode(jsons, true);
-
- let info = {
- fp: '',
- ip: '',
- clusterName: '',
- };
-
- if (!(joinInfo && joinInfo.totem)) {
- field.valid = false;
- linkEditor.setLinks([]);
- linkEditor.setInfoText();
- vm.set('hasAssistedInfo', false);
- } else {
- let interfaces = joinInfo.totem.interface;
- let links = Object.values(interfaces).map((iface) => {
- let linkNumber = iface.linknumber;
- let peerLink;
- if (joinInfo.peerLinks) {
- peerLink = joinInfo.peerLinks[linkNumber];
- }
- return {
- number: linkNumber,
- value: '',
- text: peerLink
- ? Ext.String.format(gettext("peer's link address: {0}"), peerLink)
- : '',
- allowBlank: false,
- };
- });
-
- linkEditor.setInfoText();
- if (
- links.length === 1 &&
- joinInfo.ring_addr !== undefined &&
- joinInfo.ring_addr[0] === joinInfo.ipAddress
- ) {
- links[0].allowBlank = true;
- links[0].emptyText = gettext("IP resolved by node's hostname");
- }
-
- linkEditor.setLinks(links);
-
- info = {
- ip: joinInfo.ipAddress,
- fp: joinInfo.fingerprint,
- clusterName: joinInfo.totem.cluster_name,
- };
- field.valid = true;
- vm.set('hasAssistedInfo', true);
- }
- vm.set('info', info);
- },
- },
-
- submit: function () {
- // joining may produce temporarily auth failures, ignore as long the task runs
- PVE.Utils.silenceAuthFailures = true;
- this.callParent();
- },
-
- taskDone: function (success) {
- delete PVE.Utils.silenceAuthFailures;
- if (success) {
- // reload always (if user wasn't faster), but wait a bit for pveproxy
- Ext.defer(function () {
- window.location.reload(true);
- }, 5000);
- let txt = gettext(
- 'Cluster join task finished, node certificate may have changed, reload GUI!',
- );
- // ensure user cannot do harm
- Ext.getBody().mask(txt, ['pve-static-mask']);
- // TaskView may hide above mask, so tell him directly
- Ext.Msg.show({
- title: gettext('Join Task Finished'),
- icon: Ext.Msg.INFO,
- msg: txt,
- });
- }
- },
-
- items: [
- {
- xtype: 'proxmoxcheckbox',
- reference: 'assistedEntry',
- name: 'assistedEntry',
- itemId: 'assistedEntry',
- submitValue: false,
- value: true,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Select if join information should be extracted from pasted cluster information, deselect for manual entering',
- ),
- },
- boxLabel: gettext(
- 'Assisted join: Paste encoded cluster join information and enter password.',
- ),
- },
- {
- xtype: 'textarea',
- name: 'serializedinfo',
- submitValue: false,
- allowBlank: false,
- fieldLabel: gettext('Information'),
- emptyText: gettext('Paste encoded Cluster Information here'),
- validator: function (val) {
- return (
- val === '' ||
- this.valid ||
- gettext('Does not seem like a valid encoded Cluster Information!')
- );
- },
- bind: {
- disabled: '{!assistedEntry.checked}',
- hidden: '{!assistedEntry.checked}',
- },
- value: '',
- },
- {
- xtype: 'panel',
- width: 776,
- layout: {
- type: 'hbox',
- align: 'center',
- },
- bind: {
- hidden: '{!showClusterFields}',
- },
- items: [
- {
- xtype: 'textfield',
- flex: 1,
- margin: '0 5px 0 0',
- fieldLabel: gettext('Peer Address'),
- allowBlank: false,
- bind: {
- value: '{info.ip}',
- readOnly: '{assistedEntry.checked}',
- },
- name: 'hostname',
- },
- {
- xtype: 'textfield',
- flex: 1,
- margin: '0 0 10px 5px',
- inputType: 'password',
- emptyText: gettext("Peer's root password"),
- fieldLabel: gettext('Password'),
- allowBlank: false,
- name: 'password',
- },
- ],
- },
- {
- xtype: 'textfield',
- fieldLabel: gettext('Fingerprint'),
- allowBlank: false,
- bind: {
- value: '{info.fp}',
- readOnly: '{assistedEntry.checked}',
- hidden: '{!showClusterFields}',
- },
- name: 'fingerprint',
- },
- {
- xtype: 'fieldcontainer',
- fieldLabel: gettext('Cluster Network'),
- bind: {
- hidden: '{!showClusterFields}',
- },
- items: [
- {
- xtype: 'pveCorosyncLinkEditor',
- itemId: 'linkEditor',
- reference: 'linkEditor',
- allowNumberEdit: false,
- },
- ],
- },
- ],
-});
-Ext.define('PVE.dc.CmdMenu', {
- extend: 'Ext.menu.Menu',
- xtype: 'datacenterCmdMenu',
-
- showSeparator: false,
-
- extraHandlerArgs: {},
-
- items: [
- {
- text: gettext('Bulk Start'),
- itemId: 'bulkstart',
- iconCls: 'fa fa-fw fa-play',
- handler: function () {
- let extraArgs = this.up('datacenterCmdMenu').extraHandlerArgs ?? {};
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- vmsAsArray: true,
- title: gettext('Bulk Start'),
- btnText: gettext('Start'),
- action: 'start',
- ...extraArgs,
- });
- },
- },
- {
- text: gettext('Bulk Shutdown'),
- itemId: 'bulkstop',
- iconCls: 'fa fa-fw fa-stop',
- handler: function () {
- let extraArgs = this.up('datacenterCmdMenu').extraHandlerArgs ?? {};
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- vmsAsArray: true,
- title: gettext('Bulk Shutdown'),
- btnText: gettext('Shutdown'),
- action: 'shutdown',
- ...extraArgs,
- });
- },
- },
- {
- text: gettext('Bulk Suspend'),
- itemId: 'bulksuspend',
- iconCls: 'fa fa-fw fa-download',
- handler: function () {
- let extraArgs = this.up('datacenterCmdMenu').extraHandlerArgs ?? {};
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- vmsAsArray: true,
- title: gettext('Bulk Suspend'),
- btnText: gettext('Suspend'),
- action: 'suspend',
- ...extraArgs,
- });
- },
- },
- {
- text: gettext('Bulk Migrate'),
- itemId: 'bulkmigrate',
- iconCls: 'fa fa-fw fa-send-o',
- handler: function () {
- let extraArgs = this.up('datacenterCmdMenu').extraHandlerArgs ?? {};
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- vmsAsArray: true,
- title: gettext('Bulk Migrate'),
- btnText: gettext('Migrate'),
- action: 'migrate',
- ...extraArgs,
- });
- },
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.title) {
- me.title = gettext('Datacenter');
- if (PVE.ClusterName?.length) {
- me.title += ` (${PVE.ClusterName})`;
- me.minWidth = 220;
- }
- }
-
- me.callParent();
-
- let caps = Ext.state.Manager.get('GuiCap');
-
- if (!caps.vms['VM.Migrate']) {
- me.getComponent('bulkmigrate').setDisabled(true);
- }
- if (!caps.vms['VM.PowerMgmt']) {
- me.getComponent('bulkstart').setDisabled(true);
- me.getComponent('bulkstop').setDisabled(true);
- me.getComponent('bulksuspend').setDisabled(true);
- }
- if (PVE.Utils.isStandaloneNode()) {
- me.getComponent('bulkmigrate').setVisible(false);
- }
- },
-});
-
-Ext.define('PVE.dc.TagCmdMenu', {
- extend: 'PVE.dc.CmdMenu',
- xtype: 'tagCmdMenu',
-
- minWidth: 220,
-
- initComponent: function () {
- let me = this;
-
- if (!me.tag) {
- throw 'no tag specified';
- }
-
- me.title = `${gettext('Tag')} '${me.tag}'`;
- if (PVE.ClusterName?.length) {
- me.title += ` (${me.nodename})`;
- }
-
- me.extraHandlerArgs = {
- prefilterIncludeTag: me.tag,
- };
-
- me.callParent();
- },
-});
-/*
- * Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected
- */
-
-Ext.define('PVE.dc.Config', {
- extend: 'PVE.panel.Config',
- alias: 'widget.PVE.dc.Config',
-
- onlineHelp: 'pve_admin_guide',
-
- initComponent: function () {
- var me = this;
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- me.items = [];
-
- let actionBtn = Ext.create('Ext.Button', {
- text: gettext('Bulk Actions'),
- iconCls: 'fa fa-fw fa-ellipsis-v',
- disabled: !caps.vms['VM.PowerMgmt'] && !caps.vms['VM.Migrate'],
- menu: new Ext.menu.Menu({
- items: [
- {
- text: gettext('Bulk Start'),
- iconCls: 'fa fa-fw fa-play',
- disabled: !caps.vms['VM.PowerMgmt'],
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- vmsAsArray: true,
- title: gettext('Bulk Start'),
- btnText: gettext('Start'),
- action: 'start',
- });
- },
- },
- {
- text: gettext('Bulk Shutdown'),
- iconCls: 'fa fa-fw fa-stop',
- disabled: !caps.vms['VM.PowerMgmt'],
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- vmsAsArray: true,
- title: gettext('Bulk Shutdown'),
- btnText: gettext('Shutdown'),
- action: 'shutdown',
- });
- },
- },
- {
- text: gettext('Bulk Suspend'),
- iconCls: 'fa fa-fw fa-download',
- disabled: !caps.vms['VM.PowerMgmt'],
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- vmsAsArray: true,
- title: gettext('Bulk Suspend'),
- btnText: gettext('Suspend'),
- action: 'suspend',
- });
- },
- },
- {
- text: gettext('Bulk Migrate'),
- iconCls: 'fa fa-fw fa-send-o',
- disabled: !caps.vms['VM.Migrate'],
- hidden: PVE.Utils.isStandaloneNode(),
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- vmsAsArray: true,
- title: gettext('Bulk Migrate'),
- btnText: gettext('Migrate'),
- action: 'migrate',
- });
- },
- },
- ],
- }),
- });
-
- Ext.apply(me, {
- title: gettext('Datacenter'),
- hstateid: 'dctab',
- tbar: [actionBtn],
- });
-
- if (caps.dc['Sys.Audit']) {
- me.items.push(
- {
- title: gettext('Summary'),
- xtype: 'pveDcSummary',
- iconCls: 'fa fa-book',
- itemId: 'summary',
- },
- {
- xtype: 'pmxNotesView',
- title: gettext('Notes'),
- iconCls: 'fa fa-sticky-note-o',
- itemId: 'notes',
- },
- {
- title: gettext('Cluster'),
- xtype: 'pveClusterAdministration',
- iconCls: 'fa fa-server',
- itemId: 'cluster',
- },
- {
- title: 'Ceph',
- itemId: 'ceph',
- iconCls: 'fa fa-ceph',
- xtype: 'pveNodeCephStatus',
- },
- {
- xtype: 'pveDcOptionView',
- title: gettext('Options'),
- iconCls: 'fa fa-gear',
- itemId: 'options',
- },
- );
- }
-
- if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) {
- me.items.push({
- xtype: 'pveStorageView',
- title: gettext('Storage'),
- iconCls: 'fa fa-database',
- itemId: 'storage',
- });
- }
-
- if (caps.dc['Sys.Audit']) {
- me.items.push(
- {
- xtype: 'pveDcBackupView',
- iconCls: 'fa fa-floppy-o',
- title: gettext('Backup'),
- itemId: 'backup',
- },
- {
- xtype: 'pveReplicaView',
- iconCls: 'fa fa-retweet',
- title: gettext('Replication'),
- itemId: 'replication',
- },
- {
- xtype: 'pveACLView',
- title: gettext('Permissions'),
- iconCls: 'fa fa-unlock',
- itemId: 'permissions',
- expandedOnInit: true,
- },
- );
- }
-
- me.items.push({
- xtype: 'pveUserView',
- groups: ['permissions'],
- iconCls: 'fa fa-user',
- title: gettext('Users'),
- itemId: 'users',
- });
-
- me.items.push({
- xtype: 'pveTokenView',
- groups: ['permissions'],
- iconCls: 'fa fa-user-o',
- title: gettext('API Tokens'),
- itemId: 'apitokens',
- });
-
- me.items.push({
- xtype: 'pmxTfaView',
- title: gettext('Two Factor'),
- groups: ['permissions'],
- iconCls: 'fa fa-key',
- itemId: 'tfa',
- yubicoEnabled: true,
- issuerName: `Proxmox VE - ${PVE.ClusterName || Proxmox.NodeName}`,
- });
-
- if (caps.dc['Sys.Audit']) {
- me.items.push(
- {
- xtype: 'pveGroupView',
- title: gettext('Groups'),
- iconCls: 'fa fa-users',
- groups: ['permissions'],
- itemId: 'groups',
- },
- {
- xtype: 'pvePoolView',
- title: gettext('Pools'),
- iconCls: 'fa fa-tags',
- groups: ['permissions'],
- itemId: 'pools',
- },
- {
- xtype: 'pveRoleView',
- title: gettext('Roles'),
- iconCls: 'fa fa-male',
- groups: ['permissions'],
- itemId: 'roles',
- },
- {
- title: gettext('Realms'),
- xtype: 'panel',
- layout: {
- type: 'border',
- },
- groups: ['permissions'],
- iconCls: 'fa fa-address-book-o',
- itemId: 'domains',
- items: [
- {
- xtype: 'pveAuthView',
- region: 'center',
- border: false,
- },
- {
- xtype: 'pveRealmSyncJobView',
- title: gettext('Realm Sync Jobs'),
- region: 'south',
- collapsible: true,
- animCollapse: false,
- border: false,
- height: '50%',
- },
- ],
- },
- {
- xtype: 'pveHAStatus',
- title: 'HA',
- iconCls: 'fa fa-heartbeat',
- itemId: 'ha',
- },
- {
- title: gettext('Affinity Rules'),
- groups: ['ha'],
- xtype: 'pveHARulesView',
- iconCls: 'fa fa-gears',
- itemId: 'ha-rules',
- },
- {
- title: gettext('Fencing'),
- groups: ['ha'],
- iconCls: 'fa fa-bolt',
- xtype: 'pveFencingView',
- itemId: 'ha-fencing',
- },
- );
- // always show on initial load, will be hiddea later if the SDN API calls don't exist,
- // else it won't be shown at first if the user initially loads with DC selected
- if (PVE.SDNInfo || PVE.SDNInfo === undefined) {
- me.items.push(
- {
- xtype: 'pveSDNStatus',
- title: gettext('SDN'),
- iconCls: 'fa fa-sdn x-fa-sdn-treelist',
- hidden: true,
- itemId: 'sdn',
- expandedOnInit: true,
- },
- {
- xtype: 'pveSDNZoneView',
- groups: ['sdn'],
- title: gettext('Zones'),
- hidden: true,
- iconCls: 'fa fa-th',
- itemId: 'sdnzone',
- },
- {
- xtype: 'pveSDNVnet',
- groups: ['sdn'],
- title: 'VNets',
- hidden: true,
- iconCls: 'fa fa-network-wired x-fa-sdn-treelist',
- itemId: 'sdnvnet',
- },
- {
- xtype: 'pveSDNOptions',
- groups: ['sdn'],
- title: gettext('Options'),
- hidden: true,
- iconCls: 'fa fa-gear',
- itemId: 'sdnoptions',
- },
- {
- xtype: 'pveDhcpTree',
- groups: ['sdn'],
- title: gettext('IPAM'),
- hidden: true,
- iconCls: 'fa fa-map-signs',
- itemId: 'sdnmappings',
- },
- {
- xtype: 'pveSDNFirewall',
- groups: ['sdn'],
- title: gettext('VNet Firewall'),
- hidden: true,
- iconCls: 'fa fa-shield',
- itemId: 'sdnfirewall',
- },
- {
- xtype: 'pveSDNFabricView',
- groups: ['sdn'],
- title: gettext('Fabrics'),
- hidden: true,
- iconCls: 'fa fa-road',
- itemId: 'sdnfabrics',
- },
- );
- }
-
- if (Proxmox.UserName === 'root@pam') {
- me.items.push({
- xtype: 'pveACMEClusterView',
- title: 'ACME',
- iconCls: 'fa fa-certificate',
- itemId: 'acme',
- });
- }
-
- me.items.push(
- {
- xtype: 'pveFirewallRules',
- title: gettext('Firewall'),
- allow_iface: true,
- base_url: '/cluster/firewall/rules',
- list_refs_url: '/cluster/firewall/refs',
- iconCls: 'fa fa-shield',
- itemId: 'firewall',
- firewall_type: 'dc',
- },
- {
- xtype: 'pveFirewallOptions',
- title: gettext('Options'),
- groups: ['firewall'],
- iconCls: 'fa fa-gear',
- base_url: '/cluster/firewall/options',
- onlineHelp: 'pve_firewall_cluster_wide_setup',
- fwtype: 'dc',
- itemId: 'firewall-options',
- },
- {
- xtype: 'pveSecurityGroups',
- title: gettext('Security Group'),
- groups: ['firewall'],
- iconCls: 'fa fa-group',
- itemId: 'firewall-sg',
- },
- {
- xtype: 'pveFirewallAliases',
- title: gettext('Alias'),
- groups: ['firewall'],
- iconCls: 'fa fa-external-link',
- base_url: '/cluster/firewall/aliases',
- itemId: 'firewall-aliases',
- },
- {
- xtype: 'pveIPSet',
- title: 'IPSet',
- groups: ['firewall'],
- iconCls: 'fa fa-list-ol',
- base_url: '/cluster/firewall/ipset',
- list_refs_url: '/cluster/firewall/refs',
- itemId: 'firewall-ipset',
- },
- {
- xtype: 'pveMetricServerView',
- title: gettext('Metric Server'),
- iconCls: 'fa fa-bar-chart',
- itemId: 'metricservers',
- onlineHelp: 'external_metric_server',
- },
- );
- }
-
- if (
- caps.mapping['Mapping.Audit'] ||
- caps.mapping['Mapping.Use'] ||
- caps.mapping['Mapping.Modify']
- ) {
- me.items.push(
- {
- xtype: 'container',
- onlineHelp: 'resource_mapping',
- title: gettext('Resource Mappings'),
- itemId: 'resources',
- iconCls: 'fa fa-folder-o',
- layout: {
- type: 'vbox',
- align: 'stretch',
- multi: true,
- },
- scrollable: true,
- defaults: {
- border: false,
- },
- items: [
- {
- xtype: 'pveDcPCIMapView',
- title: gettext('PCI Devices'),
- flex: 1,
- },
- {
- xtype: 'splitter',
- collapsible: false,
- performCollapse: false,
- },
- {
- xtype: 'pveDcUSBMapView',
- title: gettext('USB Devices'),
- flex: 1,
- },
- ],
- },
- {
- xtype: 'pveDcDirMapView',
- itemId: 'directories',
- title: gettext('Directory Mappings'),
- iconCls: 'fa fa-folder',
- },
- );
- }
-
- if (
- caps.mapping['Mapping.Audit'] ||
- caps.mapping['Mapping.Use'] ||
- caps.mapping['Mapping.Modify']
- ) {
- me.items.push({
- xtype: 'pmxNotificationConfigView',
- title: gettext('Notifications'),
- itemId: 'notification-targets',
- iconCls: 'fa fa-bell-o',
- baseUrl: '/cluster/notifications',
- });
- }
-
- if (caps.dc['Sys.Audit']) {
- me.items.push({
- xtype: 'pveDcSupport',
- title: gettext('Support'),
- itemId: 'support',
- iconCls: 'fa fa-comments-o',
- });
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.form.CorosyncLinkEditorController', {
- extend: 'Ext.app.ViewController',
- alias: 'controller.pveCorosyncLinkEditorController',
-
- addLinkIfEmpty: function () {
- let view = this.getView();
- if (view.items || view.items.length === 0) {
- this.addLink();
- }
- },
-
- addEmptyLink: function () {
- this.addLink(); // discard parameters to allow being called from 'handler'
- },
-
- addLink: function (link) {
- let me = this;
- let view = me.getView();
- let vm = view.getViewModel();
-
- let linkCount = vm.get('linkCount');
- if (linkCount >= vm.get('maxLinkCount')) {
- return;
- }
-
- link = link || {};
-
- if (link.number === undefined) {
- link.number = me.getNextFreeNumber();
- }
- if (link.value === undefined) {
- link.value = me.getNextFreeNetwork();
- }
-
- let linkSelector = Ext.create('PVE.form.CorosyncLinkSelector', {
- maxLinkNumber: vm.get('maxLinkCount') - 1,
- allowNumberEdit: vm.get('allowNumberEdit'),
- allowBlankNetwork: link.allowBlank,
- initNumber: link.number,
- initNetwork: link.value,
- text: link.text,
- emptyText: link.emptyText,
-
- // needs to be set here, because we need to update the viewmodel
- removeBtnHandler: function () {
- let curLinkCount = vm.get('linkCount');
-
- if (curLinkCount <= 1) {
- return;
- }
-
- vm.set('linkCount', curLinkCount - 1);
-
- // 'this' is the linkSelector here
- view.remove(this);
-
- me.updateDeleteButtonState();
- },
- });
-
- view.add(linkSelector);
-
- linkCount++;
- vm.set('linkCount', linkCount);
-
- me.updateDeleteButtonState();
- },
-
- // ExtJS trips on binding this for some reason, so do it manually
- updateDeleteButtonState: function () {
- let view = this.getView();
- let vm = view.getViewModel();
-
- let disabled = vm.get('linkCount') <= 1;
-
- let deleteButtons = view.query('button[cls=removeLinkBtn]');
- Ext.Array.each(deleteButtons, (btn) => {
- btn.setDisabled(disabled);
- });
- },
-
- getNextFreeNetwork: function () {
- let view = this.getView();
- let vm = view.getViewModel();
-
- let networksInUse = view.query('proxmoxNetworkSelector').map((selector) => selector.value);
-
- for (const network of vm.get('networks')) {
- if (!networksInUse.includes(network)) {
- return network;
- }
- }
- return undefined; // default to empty field, user has to set up link manually
- },
-
- getNextFreeNumber: function () {
- let view = this.getView();
- let vm = view.getViewModel();
-
- let numbersInUse = view.query('numberfield').map((field) => field.value);
-
- for (let i = 0; i < vm.get('maxLinkCount'); i++) {
- if (!numbersInUse.includes(i)) {
- return i;
- }
- }
- // all numbers in use, this should never happen since add button is disabled automatically
- return 0;
- },
-});
-
-Ext.define('PVE.form.CorosyncLinkSelector', {
- extend: 'Ext.panel.Panel',
- xtype: 'pveCorosyncLinkSelector',
-
- mixins: ['Proxmox.Mixin.CBind'],
- cbindData: [],
-
- // config
- maxLinkNumber: 7,
- allowNumberEdit: true,
- allowBlankNetwork: false,
- removeBtnHandler: undefined,
- emptyText: '',
-
- // values
- initNumber: 0,
- initNetwork: '',
- text: '',
-
- layout: 'hbox',
- bodyPadding: 5,
- border: 0,
-
- items: [
- {
- xtype: 'displayfield',
- fieldLabel: 'Link',
- cbind: {
- hidden: '{allowNumberEdit}',
- value: '{initNumber}',
- },
- width: 45,
- labelWidth: 30,
- allowBlank: false,
- },
- {
- xtype: 'numberfield',
- fieldLabel: 'Link',
- cbind: {
- maxValue: '{maxLinkNumber}',
- hidden: '{!allowNumberEdit}',
- value: '{initNumber}',
- },
- width: 80,
- labelWidth: 30,
- minValue: 0,
- submitValue: false, // see getSubmitValue of network selector
- allowBlank: false,
- },
- {
- xtype: 'proxmoxNetworkSelector',
- cbind: {
- allowBlank: '{allowBlankNetwork}',
- value: '{initNetwork}',
- emptyText: '{emptyText}',
- },
- autoSelect: false,
- valueField: 'address',
- displayField: 'address',
- width: 220,
- margin: '0 5px 0 5px',
- getSubmitValue: function () {
- let me = this;
- // link number is encoded into key, so we need to set field name before value retrieval
- let linkNumber = me.prev('numberfield').getValue(); // always the correct one
- me.name = 'link' + linkNumber;
- return me.getValue();
- },
- },
- {
- xtype: 'button',
- iconCls: 'fa fa-trash-o',
- cls: 'removeLinkBtn',
- cbind: {
- hidden: '{!allowNumberEdit}',
- },
- handler: function () {
- let me = this;
- let parent = me.up('pveCorosyncLinkSelector');
- if (parent.removeBtnHandler !== undefined) {
- parent.removeBtnHandler();
- }
- },
- },
- {
- xtype: 'label',
- margin: '-1px 0 0 5px',
-
- // for muted effect
- cls: 'x-form-item-label-default',
-
- cbind: {
- text: '{text}',
- },
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- let numSelect = me.down('numberfield');
- let netSelect = me.down('proxmoxNetworkSelector');
-
- numSelect.validator = me.createNoDuplicatesValidator(
- 'numberfield',
- gettext('Duplicate link number not allowed.'),
- );
-
- netSelect.validator = me.createNoDuplicatesValidator(
- 'proxmoxNetworkSelector',
- gettext('Duplicate link address not allowed.'),
- );
- },
-
- createNoDuplicatesValidator: function (queryString, errorMsg) {
- // linkSelector generator
- let view = this;
- /** @this is the field itself, as the validator this is called from scopes it that way */
- return function (val) {
- let me = this;
- let form = view.up('form');
- let linkEditor = view.up('pveCorosyncLinkEditor');
-
- if (!form.validating) {
- // avoid recursion/double validation by setting temporary states
- me.validating = true;
- form.validating = true;
-
- // validate all other fields as well, to always mark both
- // parties involved in a 'duplicate' error
- form.isValid();
-
- form.validating = false;
- me.validating = false;
- } else if (me.validating) {
- // we'll be validated by the original call in the other if-branch, avoid double work
- return true;
- }
-
- if (val === undefined || (val instanceof String && val.length === 0)) {
- return true; // let this be caught by allowBlank, if at all
- }
-
- let allFields = linkEditor.query(queryString);
- for (const field of allFields) {
- if (field !== me && String(field.getValue()) === String(val)) {
- return errorMsg;
- }
- }
- return true;
- };
- },
-});
-
-Ext.define('PVE.form.CorosyncLinkEditor', {
- extend: 'Ext.panel.Panel',
- xtype: 'pveCorosyncLinkEditor',
-
- controller: 'pveCorosyncLinkEditorController',
-
- // only initial config, use setter otherwise
- allowNumberEdit: true,
-
- viewModel: {
- data: {
- linkCount: 0,
- maxLinkCount: 8,
- networks: null,
- allowNumberEdit: true,
- infoText: '',
- },
- formulas: {
- addDisabled: function (get) {
- return !get('allowNumberEdit') || get('linkCount') >= get('maxLinkCount');
- },
- dockHidden: function (get) {
- return !(get('allowNumberEdit') || get('infoText'));
- },
- },
- },
-
- dockedItems: [
- {
- xtype: 'toolbar',
- dock: 'bottom',
- defaultButtonUI: 'default',
- border: false,
- padding: '6 0 6 0',
- bind: {
- hidden: '{dockHidden}',
- },
- items: [
- {
- xtype: 'button',
- text: gettext('Add'),
- bind: {
- disabled: '{addDisabled}',
- hidden: '{!allowNumberEdit}',
- },
- handler: 'addEmptyLink',
- },
- {
- xtype: 'label',
- bind: {
- text: '{infoText}',
- },
- },
- ],
- },
- ],
-
- setInfoText: function (text) {
- let me = this;
- let vm = me.getViewModel();
-
- vm.set('infoText', text || '');
- },
-
- setLinks: function (links) {
- let me = this;
- let controller = me.getController();
- let vm = me.getViewModel();
-
- me.removeAll();
- vm.set('linkCount', 0);
-
- Ext.Array.each(links, (link) => controller.addLink(link));
- },
-
- setDefaultLinks: function () {
- let me = this;
- let controller = me.getController();
- let vm = me.getViewModel();
-
- me.removeAll();
- vm.set('linkCount', 0);
- controller.addLink();
- },
-
- // clears all links
- setAllowNumberEdit: function (allow) {
- let me = this;
- let vm = me.getViewModel();
- vm.set('allowNumberEdit', allow);
- me.removeAll();
- vm.set('linkCount', 0);
- },
-
- items: [
- {
- // No links is never a valid scenario, but can occur during a slow load
- xtype: 'hiddenfield',
- submitValue: false,
- isValid: function () {
- let me = this;
- let vm = me.up('pveCorosyncLinkEditor').getViewModel();
- return vm.get('linkCount') > 0;
- },
- },
- ],
-
- initComponent: function () {
- let me = this;
- let vm = me.getViewModel();
- let controller = me.getController();
-
- vm.set('allowNumberEdit', me.allowNumberEdit);
- vm.set('infoText', me.infoText || '');
-
- me.callParent();
-
- // Request local node networks to pre-populate first link.
- Proxmox.Utils.API2Request({
- url: '/nodes/localhost/network',
- method: 'GET',
- waitMsgTarget: me,
- success: (response) => {
- let data = response.result.data;
- if (data.length > 0) {
- data.sort((a, b) => a.iface.localeCompare(b.iface));
- let addresses = [];
- for (let net of data) {
- if (net.address) {
- addresses.push(net.address);
- }
- if (net.address6) {
- addresses.push(net.address6);
- }
- }
-
- vm.set('networks', addresses);
- }
-
- // Always have at least one link, but account for delay in API,
- // someone might have called 'setLinks' in the meantime -
- // except if 'allowNumberEdit' is false, in which case we're
- // probably waiting for the user to input the join info
- if (vm.get('allowNumberEdit')) {
- controller.addLinkIfEmpty();
- }
- },
- failure: () => {
- if (vm.get('allowNumberEdit')) {
- controller.addLinkIfEmpty();
- }
- },
- });
- },
-});
-Ext.define('PVE.dc.GroupEdit', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveDcGroupEdit'],
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.groupid;
-
- var url;
- var method;
-
- if (me.isCreate) {
- url = '/api2/extjs/access/groups';
- method = 'POST';
- } else {
- url = '/api2/extjs/access/groups/' + me.groupid;
- method = 'PUT';
- }
-
- Ext.applyIf(me, {
- subject: gettext('Group'),
- url: url,
- method: method,
- items: [
- {
- xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
- fieldLabel: gettext('Name'),
- name: 'groupid',
- value: me.groupid,
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- fieldLabel: gettext('Comment'),
- name: 'comment',
- allowBlank: true,
- },
- ],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load();
- }
- },
-});
-Ext.define('PVE.dc.GroupView', {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveGroupView'],
-
- onlineHelp: 'pveum_groups',
-
- stateful: true,
- stateId: 'grid-groups',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-groups',
- sorters: {
- property: 'groupid',
- direction: 'ASC',
- },
- });
-
- var reload = function () {
- store.load();
- };
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- callback: function () {
- reload();
- },
- baseurl: '/access/groups/',
- });
-
- var run_editor = function () {
- var rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
-
- var win = Ext.create('PVE.dc.GroupEdit', {
- groupid: rec.data.groupid,
- });
- win.on('destroy', reload);
- win.show();
- };
-
- var edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- var tbar = [
- {
- text: gettext('Create'),
- handler: function () {
- var win = Ext.create('PVE.dc.GroupEdit', {});
- win.on('destroy', reload);
- win.show();
- },
- },
- edit_btn,
- remove_btn,
- ];
-
- Proxmox.Utils.monStoreErrors(me, store);
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: tbar,
- viewConfig: {
- trackOver: false,
- },
- columns: [
- {
- header: gettext('Name'),
- width: 200,
- sortable: true,
- dataIndex: 'groupid',
- },
- {
- header: gettext('Comment'),
- sortable: false,
- renderer: Ext.String.htmlEncode,
- dataIndex: 'comment',
- flex: 1,
- },
- {
- header: gettext('Users'),
- sortable: false,
- dataIndex: 'users',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- ],
- listeners: {
- activate: reload,
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.dc.Guests', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveDcGuests',
-
- title: gettext('Guests'),
- height: 250,
- layout: {
- type: 'table',
- columns: 2,
- tableAttrs: {
- style: {
- width: '100%',
- },
- },
- },
- bodyPadding: '0 20 20 20',
-
- defaults: {
- xtype: 'box',
- padding: '0 50 0 50',
- style: {
- 'text-align': 'center',
- 'line-height': '1.5em',
- 'font-size': '14px',
- },
- },
- items: [
- {
- itemId: 'qemu',
- data: {
- running: 0,
- paused: 0,
- stopped: 0,
- template: 0,
- },
- cls: 'centered-flex-column',
- tpl: [
- '' + gettext('Virtual Machines') + ' ',
- '',
- '
',
- ' ',
- gettext('Running'),
- '
',
- '
{running}
',
- '
',
- '',
- '',
- '
',
- ' ',
- gettext('Paused'),
- '
',
- '
{paused}
',
- '
',
- ' ',
- '',
- '
',
- ' ',
- gettext('Stopped'),
- '
',
- '
{stopped}
',
- '
',
- '',
- '',
- '
',
- ' ',
- gettext('Templates'),
- '
',
- '
{template}
',
- '
',
- ' ',
- ],
- },
- {
- itemId: 'lxc',
- data: {
- running: 0,
- paused: 0,
- stopped: 0,
- template: 0,
- },
- cls: 'centered-flex-column',
- tpl: [
- '' + gettext('LXC Container') + ' ',
- '',
- '
',
- ' ',
- gettext('Running'),
- '
',
- '
{running}
',
- '
',
- '',
- '',
- '
',
- ' ',
- gettext('Paused'),
- '
',
- '
{paused}
',
- '
',
- ' ',
- '',
- '
',
- ' ',
- gettext('Stopped'),
- '
',
- '
{stopped}
',
- '
',
- '',
- '',
- '
',
- ' ',
- gettext('Templates'),
- '
',
- '
{template}
',
- '
',
- ' ',
- ],
- },
- {
- itemId: 'error',
- colspan: 2,
- data: {
- num: 0,
- },
- columnWidth: 1,
- padding: '10 250 0 250',
- tpl: [
- '',
- '',
- ' ',
- gettext('Error'),
- '
',
- '{num}
',
- ' ',
- ],
- },
- ],
-
- updateValues: function (qemu, lxc, error) {
- let me = this;
-
- let lazyUpdate = (query, newData) => {
- let el = me.getComponent(query);
- let currentData = el.data;
-
- let keys = Object.keys(newData);
- if (keys.length === Object.keys(currentData).length) {
- if (keys.every((k) => newData[k] === currentData[k])) {
- return; // all stayed the same here, return early to avoid bogus regeneration
- }
- }
- el.update(newData);
- };
- lazyUpdate('qemu', qemu);
- lazyUpdate('lxc', lxc);
- lazyUpdate('error', { num: error });
- },
-});
-Ext.define('PVE.dc.Health', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveDcHealth',
-
- title: gettext('Health'),
-
- bodyPadding: 10,
- height: 250,
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
-
- defaults: {
- flex: 1,
- xtype: 'box',
- style: {
- 'text-align': 'center',
- },
- },
-
- nodeList: [],
- nodeIndex: 0,
-
- updateStatus: function (store, records, success) {
- let me = this;
- if (!success) {
- return;
- }
-
- let cluster = {
- iconCls: PVE.Utils.get_health_icon('good', true),
- text: gettext('Standalone node - no cluster defined'),
- };
- let nodes = {
- online: 0,
- offline: 0,
- };
- let numNodes = 1; // by default we have one node
- for (const { data } of records) {
- if (data.type === 'node') {
- nodes[data.online === 1 ? 'online' : 'offline']++;
- } else if (data.type === 'cluster') {
- cluster.text = `${gettext('Cluster')}: ${data.name}, ${gettext('Quorate')}: `;
- cluster.text += Proxmox.Utils.format_boolean(data.quorate);
- if (data.quorate !== 1) {
- cluster.iconCls = PVE.Utils.get_health_icon('critical', true);
- }
- numNodes = data.nodes;
- }
- }
-
- if (numNodes !== nodes.online + nodes.offline) {
- nodes.offline = numNodes - nodes.online;
- }
-
- me.getComponent('clusterstatus').updateHealth(cluster);
- me.getComponent('nodestatus').update(nodes);
- },
-
- updateCeph: function (store, records, success) {
- let me = this;
- let cephstatus = me.getComponent('ceph');
- if (!success || records.length < 1) {
- if (cephstatus.isVisible()) {
- return; // if ceph status is already visible don't stop to update
- }
- // try all nodes until we either get a successful api call, or we tried all nodes
- if (++me.nodeIndex >= me.nodeList.length) {
- me.cephstore.stopUpdate();
- } else {
- store
- .getProxy()
- .setUrl(`/api2/json/nodes/${me.nodeList[me.nodeIndex].node}/ceph/status`);
- }
- return;
- }
-
- let state = PVE.Utils.render_ceph_health(records[0].data.health || {});
- cephstatus.updateHealth(state);
- cephstatus.setVisible(true);
- },
-
- listeners: {
- destroy: function () {
- let me = this;
- me.cephstore.stopUpdate();
- },
- },
-
- items: [
- {
- itemId: 'clusterstatus',
- xtype: 'pveHealthWidget',
- title: gettext('Status'),
- },
- {
- itemId: 'nodestatus',
- data: {
- online: 0,
- offline: 0,
- },
- tpl: [
- '' + gettext('Nodes') + ' ',
- '',
- '
',
- ' ',
- gettext('Online'),
- '
',
- '
{online}
',
- '
',
- '
',
- ' ',
- gettext('Offline'),
- '
',
- '
{offline}
',
- '
',
- ],
- },
- {
- itemId: 'ceph',
- width: 250,
- columnWidth: undefined,
- userCls: 'pointer',
- title: 'Ceph',
- xtype: 'pveHealthWidget',
- hidden: true,
- listeners: {
- element: 'el',
- click: function () {
- Ext.state.Manager.getProvider().set('dctab', { value: 'ceph' }, true);
- },
- },
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- me.nodeList = PVE.data.ResourceStore.getNodes();
- me.nodeIndex = 0;
- me.cephstore = Ext.create('Proxmox.data.UpdateStore', {
- interval: 3000,
- storeid: 'pve-cluster-ceph',
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodeList[me.nodeIndex].node}/ceph/status`,
- },
- });
- me.callParent();
- me.mon(me.cephstore, 'load', me.updateCeph, me);
- me.cephstore.startUpdate();
- },
-});
-/* This class defines the "Cluster log" tab of the bottom status panel
- * A log entry is a timestamp associated with an action on a cluster
- */
-
-Ext.define('PVE.dc.Log', {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveClusterLog'],
-
- initComponent: function () {
- let me = this;
-
- let logstore = Ext.create('Proxmox.data.UpdateStore', {
- storeid: 'pve-cluster-log',
- model: 'proxmox-cluster-log',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/log',
- },
- });
- let store = Ext.create('Proxmox.data.DiffStore', {
- rstore: logstore,
- appendAtStart: true,
- });
-
- Ext.apply(me, {
- store: store,
- stateful: false,
-
- viewConfig: {
- trackOver: false,
- stripeRows: true,
- getRowClass: function (record, index) {
- let pri = record.get('pri');
- if (pri && pri <= 3) {
- return 'proxmox-invalid-row';
- }
- return undefined;
- },
- },
- sortableColumns: false,
- columns: [
- {
- header: gettext('Time'),
- dataIndex: 'time',
- width: 150,
- renderer: function (value) {
- return Ext.Date.format(value, 'M d H:i:s');
- },
- },
- {
- header: gettext('Node'),
- dataIndex: 'node',
- width: 150,
- },
- {
- header: gettext('Service'),
- dataIndex: 'tag',
- width: 100,
- },
- {
- header: 'PID',
- dataIndex: 'pid',
- width: 100,
- },
- {
- header: gettext('User name'),
- dataIndex: 'user',
- renderer: Ext.String.htmlEncode,
- width: 150,
- },
- {
- header: gettext('Severity'),
- dataIndex: 'pri',
- renderer: PVE.Utils.render_serverity,
- width: 100,
- },
- {
- header: gettext('Message'),
- dataIndex: 'msg',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- ],
- listeners: {
- activate: () => logstore.startUpdate(),
- deactivate: () => logstore.stopUpdate(),
- destroy: () => logstore.stopUpdate(),
- },
- });
-
- me.callParent();
- },
-});
-Ext.define(
- 'PVE.dc.NodeView',
- {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveDcNodeView',
-
- title: gettext('Nodes'),
- disableSelection: true,
- scrollable: true,
-
- columns: [
- {
- header: gettext('Name'),
- flex: 1,
- sortable: true,
- dataIndex: 'name',
- },
- {
- header: 'ID',
- width: 40,
- sortable: true,
- dataIndex: 'nodeid',
- },
- {
- header: gettext('Online'),
- width: 60,
- sortable: true,
- dataIndex: 'online',
- renderer: function (value) {
- var cls = value ? 'good' : 'critical';
- return ' ';
- },
- },
- {
- header: gettext('Support'),
- width: 100,
- sortable: true,
- dataIndex: 'level',
- renderer: PVE.Utils.render_support_level,
- },
- {
- header: gettext('Server Address'),
- width: 115,
- sortable: true,
- dataIndex: 'ip',
- },
- {
- header: gettext('CPU usage'),
- sortable: true,
- width: 110,
- dataIndex: 'cpuusage',
- tdCls: 'x-progressbar-default-cell',
- xtype: 'widgetcolumn',
- widget: {
- xtype: 'pveProgressBar',
- },
- },
- {
- header: gettext('Memory usage'),
- width: 110,
- sortable: true,
- tdCls: 'x-progressbar-default-cell',
- dataIndex: 'memoryusage',
- xtype: 'widgetcolumn',
- widget: {
- xtype: 'pveProgressBar',
- warningThreshold: 0.9,
- criticalThreshold: 0.975,
- },
- },
- {
- header: gettext('Uptime'),
- sortable: true,
- dataIndex: 'uptime',
- align: 'right',
- renderer: Proxmox.Utils.render_uptime,
- },
- ],
-
- stateful: true,
- stateId: 'grid-cluster-nodes',
- tools: [
- {
- type: 'up',
- handler: function () {
- let view = this.up('grid');
- view.setHeight(Math.max(view.getHeight() - 50, 250));
- },
- },
- {
- type: 'down',
- handler: function () {
- let view = this.up('grid');
- view.setHeight(view.getHeight() + 50);
- },
- },
- ],
- },
- function () {
- Ext.define('pve-dc-nodes', {
- extend: 'Ext.data.Model',
- fields: ['id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'],
- idProperty: 'id',
- });
- },
-);
-
-Ext.define('PVE.widget.ProgressBar', {
- extend: 'Ext.Progress',
- alias: 'widget.pveProgressBar',
-
- animate: true,
- textTpl: ['{percent}%'],
-
- warningThreshold: 0.8,
- criticalThreshold: 0.9,
-
- setValue: function (value) {
- let me = this;
-
- me.callParent([value]);
-
- me.removeCls(['warning', 'critical']);
-
- if (value >= me.criticalThreshold) {
- me.addCls('critical');
- } else if (value >= me.warningThreshold) {
- me.addCls('warning');
- }
- },
-});
-Ext.define('PVE.dc.OptionView', {
- extend: 'Proxmox.grid.ObjectGrid',
- alias: ['widget.pveDcOptionView'],
-
- onlineHelp: 'datacenter_configuration_file',
-
- monStoreErrors: true,
- userCls: 'proxmox-tags-full',
-
- add_inputpanel_row: function (name, text, opts) {
- var me = this;
-
- opts = opts || {};
- me.rows = me.rows || {};
-
- let canEdit = !Object.hasOwn(opts, 'caps') || opts.caps;
- me.rows[name] = {
- required: true,
- defaultValue: opts.defaultValue,
- header: text,
- renderer: opts.renderer,
- editor: canEdit
- ? {
- xtype: 'proxmoxWindowEdit',
- width: opts.width || 350,
- subject: text,
- onlineHelp: opts.onlineHelp,
- fieldDefaults: {
- labelWidth: opts.labelWidth || 100,
- },
- setValues: function (values) {
- var edit_value = values[name];
-
- if (opts.parseBeforeSet) {
- edit_value = PVE.Parser.parsePropertyString(edit_value);
- }
-
- Ext.Array.each(this.query('inputpanel'), function (panel) {
- panel.setValues(edit_value);
- });
- },
- url: opts.url,
- items: [
- {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- if (values === undefined || Object.keys(values).length === 0) {
- return { delete: name };
- }
- var ret_val = {};
- ret_val[name] = PVE.Parser.printPropertyString(values);
- return ret_val;
- },
- items: opts.items,
- },
- ],
- }
- : undefined,
- };
- },
-
- render_bwlimits: function (value) {
- if (!value) {
- return gettext('None');
- }
-
- let parsed = PVE.Parser.parsePropertyString(value);
- return Object.entries(parsed)
- .map(([k, v]) => k + ': ' + Proxmox.Utils.format_size(v * 1024) + '/s')
- .join(',');
- },
-
- initComponent: function () {
- var me = this;
-
- me.add_combobox_row('keyboard', gettext('Keyboard Layout'), {
- renderer: PVE.Utils.render_kvm_language,
- comboItems: Object.entries(PVE.Utils.kvm_keymaps),
- defaultValue: '__default__',
- deleteEmpty: true,
- });
- me.add_text_row('http_proxy', gettext('HTTP proxy'), {
- renderer: Ext.htmlEncode,
- defaultValue: Proxmox.Utils.noneText,
- vtype: 'HttpProxy',
- deleteEmpty: true,
- });
- me.add_combobox_row('console', gettext('Console Viewer'), {
- renderer: PVE.Utils.render_console_viewer,
- comboItems: Object.entries(PVE.Utils.console_map),
- defaultValue: '__default__',
- deleteEmpty: true,
- });
- me.add_text_row('email_from', gettext('Email from address'), {
- deleteEmpty: true,
- vtype: 'proxmoxMail',
- defaultValue: 'root@$hostname',
- });
- me.add_text_row('mac_prefix', gettext('MAC address prefix'), {
- deleteEmpty: true,
- vtype: 'MacPrefix',
- defaultValue: 'BC:24:11',
- });
- me.add_inputpanel_row('migration', gettext('Migration Settings'), {
- renderer: PVE.Utils.render_as_property_string,
- labelWidth: 120,
- url: '/api2/extjs/cluster/options',
- defaultKey: 'type',
- items: [
- {
- xtype: 'displayfield',
- name: 'type',
- fieldLabel: gettext('Type'),
- value: 'secure',
- submitValue: true,
- },
- {
- xtype: 'proxmoxNetworkSelector',
- name: 'network',
- fieldLabel: gettext('Network'),
- value: null,
- emptyText: Proxmox.Utils.defaultText,
- autoSelect: false,
- skipEmptyText: true,
- editable: true,
- notFoundIsValid: true,
- vtype: 'IP64CIDRAddress',
- type: 'include_sdn',
- },
- ],
- });
- me.add_inputpanel_row('replication', gettext('Replication Settings'), {
- renderer: PVE.Utils.render_as_property_string,
- labelWidth: 120,
- url: '/api2/extjs/cluster/options',
- defaultKey: 'type',
- items: [
- {
- xtype: 'displayfield',
- name: 'type',
- fieldLabel: gettext('Type'),
- value: 'secure',
- submitValue: true,
- },
- {
- xtype: 'proxmoxNetworkSelector',
- name: 'network',
- fieldLabel: gettext('Network'),
- value: null,
- emptyText: Proxmox.Utils.defaultText,
- autoSelect: false,
- skipEmptyText: true,
- editable: true,
- notFoundIsValid: true,
- vtype: 'IP64CIDRAddress',
- type: 'include_sdn',
- },
- ],
- });
- me.add_inputpanel_row('ha', gettext('HA Settings'), {
- renderer: PVE.Utils.render_dc_ha_opts,
- labelWidth: 120,
- url: '/api2/extjs/cluster/options',
- onlineHelp: 'ha_manager_shutdown_policy',
- items: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'shutdown_policy',
- fieldLabel: gettext('Shutdown Policy'),
- deleteEmpty: false,
- value: '__default__',
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (conditional)'],
- ['freeze', 'freeze'],
- ['failover', 'failover'],
- ['migrate', 'migrate'],
- ['conditional', 'conditional'],
- ],
- defaultValue: '__default__',
- },
- ],
- });
- me.add_inputpanel_row('crs', gettext('Cluster Resource Scheduling'), {
- renderer: PVE.Utils.render_as_property_string,
- width: 450,
- labelWidth: 120,
- url: '/api2/extjs/cluster/options',
- onlineHelp: 'ha_manager_crs',
- items: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'ha',
- fieldLabel: gettext('HA Scheduling'),
- deleteEmpty: false,
- value: '__default__',
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (basic)'],
- ['basic', 'Basic (Resource Count)'],
- ['static', 'Static Load'],
- ],
- defaultValue: '__default__',
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'ha-rebalance-on-start',
- fieldLabel: gettext('Rebalance on Start'),
- boxLabel: gettext(
- 'Use CRS to select the least loaded node when starting an HA service',
- ),
- value: 0,
- },
- ],
- });
- me.add_inputpanel_row('u2f', gettext('U2F Settings'), {
- renderer: (v) =>
- !v ? Proxmox.Utils.NoneText : Ext.htmlEncode(PVE.Parser.printPropertyString(v)),
- width: 450,
- url: '/api2/extjs/cluster/options',
- onlineHelp: 'pveum_configure_u2f',
- items: [
- {
- xtype: 'textfield',
- name: 'appid',
- fieldLabel: gettext('U2F AppID URL'),
- emptyText: gettext('Defaults to origin'),
- value: '',
- deleteEmpty: true,
- skipEmptyText: true,
- submitEmptyText: false,
- },
- {
- xtype: 'textfield',
- name: 'origin',
- fieldLabel: gettext('U2F Origin'),
- emptyText: gettext('Defaults to requesting host URI'),
- value: '',
- deleteEmpty: true,
- skipEmptyText: true,
- submitEmptyText: false,
- },
- {
- xtype: 'box',
- height: 25,
- html:
- `${gettext('Note:')} ` +
- gettext('U2F is deprecated, use WebAuthn'),
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext('NOTE: Changing an AppID breaks existing U2F registrations!'),
- },
- ],
- });
- me.add_inputpanel_row('webauthn', gettext('WebAuthn Settings'), {
- renderer: (v) =>
- !v ? Proxmox.Utils.NoneText : Ext.htmlEncode(PVE.Parser.printPropertyString(v)),
- width: 450,
- url: '/api2/extjs/cluster/options',
- onlineHelp: 'pveum_configure_webauthn',
- items: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Name'),
- name: 'rp', // NOTE: relying party consists of name and id, this is the name
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- fieldLabel: gettext('Origin'),
- emptyText: Ext.String.format(
- gettext('Domain Lockdown (e.g., {0})'),
- document.location.origin,
- ),
- name: 'origin',
- allowBlank: true,
- },
- {
- xtype: 'textfield',
- fieldLabel: 'ID',
- name: 'id',
- allowBlank: false,
- listeners: {
- dirtychange: (f, isDirty) =>
- f
- .up('panel')
- .down('box[id=idChangeWarning]')
- .setHidden(!f.originalValue || !isDirty),
- },
- },
- {
- xtype: 'container',
- layout: 'hbox',
- items: [
- {
- xtype: 'box',
- flex: 1,
- },
- {
- xtype: 'button',
- text: gettext('Auto-fill'),
- iconCls: 'fa fa-fw fa-pencil-square-o',
- handler: function (button, ev) {
- let panel = this.up('panel');
- let fqdn = document.location.hostname;
-
- panel.down('field[name=rp]').setValue(fqdn);
-
- let idField = panel.down('field[name=id]');
- let currentID = idField.getValue();
- if (!currentID || currentID.length === 0) {
- idField.setValue(fqdn);
- }
- },
- },
- ],
- },
- {
- xtype: 'box',
- height: 25,
- html:
- `${gettext('Note:')} ` +
- gettext('WebAuthn requires using a trusted certificate.'),
- },
- {
- xtype: 'box',
- id: 'idChangeWarning',
- hidden: true,
- padding: '5 0 0 0',
- html:
- ' ' +
- gettext('Changing the ID breaks existing WebAuthn TFA entries.'),
- },
- ],
- });
- me.add_inputpanel_row('bwlimit', gettext('Bandwidth Limits'), {
- renderer: me.render_bwlimits,
- width: 450,
- url: '/api2/extjs/cluster/options',
- parseBeforeSet: true,
- labelWidth: 120,
- items: [
- {
- xtype: 'pveBandwidthField',
- name: 'default',
- fieldLabel: gettext('Default'),
- emptyText: gettext('none'),
- backendUnit: 'KiB',
- },
- {
- xtype: 'pveBandwidthField',
- name: 'restore',
- fieldLabel: gettext('Backup Restore'),
- emptyText: gettext('default'),
- backendUnit: 'KiB',
- },
- {
- xtype: 'pveBandwidthField',
- name: 'migration',
- fieldLabel: gettext('Migration'),
- emptyText: gettext('default'),
- backendUnit: 'KiB',
- },
- {
- xtype: 'pveBandwidthField',
- name: 'clone',
- fieldLabel: gettext('Clone'),
- emptyText: gettext('default'),
- backendUnit: 'KiB',
- },
- {
- xtype: 'pveBandwidthField',
- name: 'move',
- fieldLabel: gettext('Disk Move'),
- emptyText: gettext('default'),
- backendUnit: 'KiB',
- },
- ],
- });
- me.add_integer_row('max_workers', gettext('Maximal Workers/bulk-action'), {
- deleteEmpty: true,
- defaultValue: 4,
- minValue: 1,
- maxValue: 64, // arbitrary but generous limit as limits are good
- });
- me.add_inputpanel_row('next-id', gettext('Next Free VMID Range'), {
- renderer: PVE.Utils.render_as_property_string,
- url: '/api2/extjs/cluster/options',
- items: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'lower',
- fieldLabel: gettext('Lower'),
- emptyText: '100',
- minValue: 100,
- maxValue: 1000 * 1000 * 1000 - 1,
- submitValue: true,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'upper',
- fieldLabel: gettext('Upper'),
- emptyText: '1.000.000',
- minValue: 100,
- maxValue: 1000 * 1000 * 1000 - 1,
- submitValue: true,
- },
- ],
- });
- me.rows['tag-style'] = {
- required: true,
- renderer: (value) => {
- if (value === undefined) {
- return gettext('No Overrides');
- }
- let colors = PVE.UIOptions.parseTagOverrides(value?.['color-map']);
- let shape = value.shape;
- let shapeText = PVE.UIOptions.tagTreeStyles[shape ?? '__default__'];
- let txt = Ext.String.format(gettext('Tree Shape: {0}'), shapeText);
- let orderText = PVE.UIOptions.tagOrderOptions[value.ordering ?? '__default__'];
- txt += `, ${Ext.String.format(gettext('Ordering: {0}'), orderText)}`;
- if (value['case-sensitive']) {
- txt += `, ${gettext('Case-Sensitive')}`;
- }
- if (Object.keys(colors).length > 0) {
- txt += `, ${gettext('Color Overrides')}: `;
- for (const tag of Object.keys(colors)) {
- txt += Proxmox.Utils.getTagElement(tag, colors);
- }
- }
- return txt;
- },
- header: gettext('Tag Style Override'),
- editor: {
- xtype: 'proxmoxWindowEdit',
- width: 800,
- subject: gettext('Tag Color Override'),
- onlineHelp: 'gui_tags',
- fieldDefaults: {
- labelWidth: 100,
- },
- url: '/api2/extjs/cluster/options',
- items: [
- {
- xtype: 'inputpanel',
- setValues: function (values) {
- if (values === undefined) {
- return undefined;
- }
- values = values?.['tag-style'] ?? {};
- values.shape = values.shape || '__default__';
- values.colors = values['color-map'];
- return Proxmox.panel.InputPanel.prototype.setValues.call(this, values);
- },
- onGetValues: function (values) {
- let style = {};
- if (values.colors) {
- style['color-map'] = values.colors;
- }
- if (values.shape && values.shape !== '__default__') {
- style.shape = values.shape;
- }
- if (values.ordering) {
- style.ordering = values.ordering;
- }
- if (values['case-sensitive']) {
- style['case-sensitive'] = 1;
- }
- let value = PVE.Parser.printPropertyString(style);
- if (value === '') {
- return {
- delete: 'tag-style',
- };
- }
- return {
- 'tag-style': value,
- };
- },
- items: [
- {
- name: 'shape',
- xtype: 'proxmoxComboGrid',
- fieldLabel: gettext('Tree Shape'),
- valueField: 'value',
- displayField: 'display',
- allowBlank: false,
- listConfig: {
- columns: [
- {
- header: gettext('Option'),
- dataIndex: 'display',
- flex: 1,
- },
- {
- header: gettext('Preview'),
- dataIndex: 'value',
- renderer: function (value) {
- let cls = value ?? '__default__';
- if (value === '__default__') {
- cls = 'circle';
- }
- let tags = PVE.Utils.renderTags('preview');
- return `${tags}
`;
- },
- flex: 1,
- },
- ],
- },
- store: {
- data: Object.entries(PVE.UIOptions.tagTreeStyles).map((v) => ({
- value: v[0],
- display: v[1],
- })),
- },
- deleteDefault: true,
- defaultValue: '__default__',
- deleteEmpty: true,
- },
- {
- name: 'ordering',
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('Ordering'),
- comboItems: Object.entries(PVE.UIOptions.tagOrderOptions),
- defaultValue: '__default__',
- value: '__default__',
- deleteEmpty: true,
- },
- {
- name: 'case-sensitive',
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Case-Sensitive'),
- boxLabel: gettext('Applies to new edits'),
- value: 0,
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Color Overrides'),
- },
- {
- name: 'colors',
- xtype: 'pveTagColorGrid',
- deleteEmpty: true,
- height: 300,
- },
- ],
- },
- ],
- },
- };
-
- me.rows['user-tag-access'] = {
- required: true,
- renderer: (value) => {
- if (value === undefined) {
- return Ext.String.format(gettext('Mode: {0}'), 'free');
- }
- let mode = value?.['user-allow'] ?? 'free';
- let list = value?.['user-allow-list']?.join(',') ?? '';
- let modeTxt = Ext.String.format(gettext('Mode: {0}'), mode);
- let overrides = PVE.UIOptions.tagOverrides;
- let tags = PVE.Utils.renderTags(list, overrides);
- let listTxt = tags !== '' ? `, ${gettext('Pre-defined:')} ${tags}` : '';
- return `${modeTxt}${listTxt}`;
- },
- header: gettext('User Tag Access'),
- editor: {
- xtype: 'pveUserTagAccessEdit',
- },
- };
-
- me.rows['registered-tags'] = {
- required: true,
- renderer: (value) => {
- if (value === undefined) {
- return gettext('No Registered Tags');
- }
- let overrides = PVE.UIOptions.tagOverrides;
- return PVE.Utils.renderTags(value.join(','), overrides);
- },
- header: gettext('Registered Tags'),
- editor: {
- xtype: 'pveRegisteredTagEdit',
- },
- };
-
- me.add_textareafield_row('consent-text', gettext('Consent Text'), {
- deleteEmpty: true,
- fieldOpts: {
- maxLength: 64 * 1024,
- },
- onlineHelp: 'gui_consent_banner',
- });
-
- me.selModel = Ext.create('Ext.selection.RowModel', {});
-
- Ext.apply(me, {
- tbar: [
- {
- text: gettext('Edit'),
- xtype: 'proxmoxButton',
- disabled: true,
- handler: function () {
- me.run_editor();
- },
- selModel: me.selModel,
- },
- ],
- url: '/api2/json/cluster/options',
- editorConfig: {
- url: '/api2/extjs/cluster/options',
- },
- interval: 5000,
- cwidth1: 200,
- listeners: {
- itemdblclick: me.run_editor,
- },
- });
-
- me.callParent();
-
- // set the new value for the default console
- me.mon(me.rstore, 'load', function (store, records, success) {
- if (!success) {
- return;
- }
-
- var rec = store.getById('console');
- PVE.UIOptions.options.console = rec.data.value;
- if (rec.data.value === '__default__') {
- delete PVE.UIOptions.options.console;
- }
-
- PVE.UIOptions.options['tag-style'] = store.getById('tag-style')?.data?.value;
- PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']);
- PVE.UIOptions.fireUIConfigChanged();
- });
-
- me.on('activate', me.rstore.startUpdate);
- me.on('destroy', me.rstore.stopUpdate);
- me.on('deactivate', me.rstore.stopUpdate);
- },
-});
-Ext.define('pve-permissions', {
- extend: 'Ext.data.TreeModel',
- fields: [
- 'text',
- 'type',
- {
- type: 'boolean',
- name: 'propagate',
- },
- ],
-});
-
-Ext.define('PVE.dc.PermissionGridPanel', {
- extend: 'Ext.tree.Panel',
- alias: 'widget.pveUserPermissionGrid',
-
- onlineHelp: 'chapter_user_management',
-
- scrollable: true,
- layout: 'fit',
- rootVisible: false,
- animate: false,
- sortableColumns: false,
-
- columns: [
- {
- xtype: 'treecolumn',
- header: gettext('Path') + '/' + gettext('Permission'),
- dataIndex: 'text',
- flex: 6,
- },
- {
- header: gettext('Propagate'),
- dataIndex: 'propagate',
- flex: 1,
- renderer: function (value) {
- if (Ext.isDefined(value)) {
- return Proxmox.Utils.format_boolean(value);
- }
- return '';
- },
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- Proxmox.Utils.API2Request({
- url: '/access/permissions?userid=' + me.userid,
- method: 'GET',
- failure: function (response, opts) {
- Proxmox.Utils.setErrorMask(me, response.htmlStatus);
- me.load_task.delay(me.load_delay);
- },
- success: function (response, opts) {
- Proxmox.Utils.setErrorMask(me, false);
- let result = Ext.decode(response.responseText);
- let data = result.data || {};
-
- let root = {
- name: '__root',
- expanded: true,
- children: [],
- };
- let idhash = {
- '/': {
- children: [],
- text: '/',
- type: 'path',
- },
- };
- Ext.Object.each(data, function (path, perms) {
- let path_item = {
- text: path,
- type: 'path',
- children: [],
- };
- Ext.Object.each(perms, function (perm, propagate) {
- let perm_item = {
- text: perm,
- type: 'perm',
- propagate: propagate === 1,
- iconCls: 'fa fa-fw fa-unlock',
- leaf: true,
- };
- path_item.children.push(perm_item);
- path_item.expandable = true;
- });
- idhash[path] = path_item;
- });
-
- Ext.Object.each(idhash, function (path, item) {
- let parent_item = idhash['/'];
- if (path === '/') {
- parent_item = root;
- item.expanded = true;
- } else {
- let split_path = path.split('/');
- while (split_path.pop()) {
- let parent_path = split_path.join('/');
- if (idhash[parent_path]) {
- parent_item = idhash[parent_path];
- break;
- }
- }
- }
- parent_item.children.push(item);
- });
-
- me.setRootNode(root);
- },
- });
-
- me.callParent();
-
- me.store.sorters.add(
- new Ext.util.Sorter({
- sorterFn: function (rec1, rec2) {
- let v1 = rec1.data.text,
- v2 = rec2.data.text;
- if (rec1.data.type !== rec2.data.type) {
- v2 = rec1.data.type;
- v1 = rec2.data.type;
- }
- if (v1 > v2) {
- return 1;
- } else if (v1 < v2) {
- return -1;
- }
- return 0;
- },
- }),
- );
- },
-});
-
-Ext.define('PVE.dc.PermissionView', {
- extend: 'Ext.window.Window',
- alias: 'widget.userShowPermissionWindow',
- mixins: ['Proxmox.Mixin.CBind'],
-
- scrollable: true,
- width: 800,
- height: 600,
- layout: 'fit',
- cbind: {
- title: (get) =>
- Ext.String.htmlEncode(get('userid')) + ` - ${gettext('Granted Permissions')}`,
- },
- items: [
- {
- xtype: 'pveUserPermissionGrid',
- cbind: {
- userid: '{userid}',
- },
- },
- ],
-});
-Ext.define('PVE.dc.PoolEdit', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveDcPoolEdit'],
- mixins: ['Proxmox.Mixin.CBind'],
-
- subject: gettext('Pool'),
-
- cbindData: {
- poolid: '',
- isCreate: (cfg) => !cfg.poolid,
- },
-
- cbind: {
- url: (get) => `/api2/extjs/pools/${!get('isCreate') ? '?poolid=' + get('poolid') : ''}`,
- method: (get) => (get('isCreate') ? 'POST' : 'PUT'),
- },
-
- items: [
- {
- xtype: 'pmxDisplayEditField',
- fieldLabel: gettext('Name'),
- cbind: {
- editable: '{isCreate}',
- value: '{poolid}',
- },
- name: 'poolid',
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- fieldLabel: gettext('Comment'),
- name: 'comment',
- allowBlank: true,
- },
- ],
-
- initComponent: function () {
- let me = this;
- me.callParent();
- if (me.poolid) {
- me.load({
- success: function (response) {
- let data = response.result.data;
- if (Ext.isArray(data)) {
- me.setValues(data[0]);
- } else {
- me.setValues(data);
- }
- },
- });
- }
- },
-});
-Ext.define('PVE.dc.PoolView', {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pvePoolView'],
-
- onlineHelp: 'pveum_pools',
-
- stateful: true,
- stateId: 'grid-pools',
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-pools',
- sorters: {
- property: 'poolid',
- direction: 'ASC',
- },
- });
-
- var reload = function () {
- store.load();
- };
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/pools/',
- callback: function () {
- reload();
- },
- getUrl: function (rec) {
- return '/pools/?poolid=' + rec.getId();
- },
- });
-
- var run_editor = function () {
- var rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
-
- var win = Ext.create('PVE.dc.PoolEdit', {
- poolid: rec.data.poolid,
- });
- win.on('destroy', reload);
- win.show();
- };
-
- var edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- var tbar = [
- {
- text: gettext('Create'),
- handler: function () {
- var win = Ext.create('PVE.dc.PoolEdit', {});
- win.on('destroy', reload);
- win.show();
- },
- },
- edit_btn,
- remove_btn,
- ];
-
- Proxmox.Utils.monStoreErrors(me, store);
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: tbar,
- viewConfig: {
- trackOver: false,
- },
- columns: [
- {
- header: gettext('Name'),
- width: 200,
- sortable: true,
- dataIndex: 'poolid',
- },
- {
- header: gettext('Comment'),
- sortable: false,
- renderer: Ext.String.htmlEncode,
- dataIndex: 'comment',
- flex: 1,
- },
- ],
- listeners: {
- activate: reload,
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.dc.RoleEdit', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveDcRoleEdit',
-
- width: 400,
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.roleid;
-
- var url;
- var method;
-
- if (me.isCreate) {
- url = '/api2/extjs/access/roles';
- method = 'POST';
- } else {
- url = '/api2/extjs/access/roles/' + me.roleid;
- method = 'PUT';
- }
-
- Ext.applyIf(me, {
- subject: gettext('Role'),
- url: url,
- method: method,
- items: [
- {
- xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
- name: 'roleid',
- value: me.roleid,
- allowBlank: false,
- fieldLabel: gettext('Name'),
- },
- {
- xtype: 'pvePrivilegesSelector',
- name: 'privs',
- value: me.privs,
- allowBlank: false,
- fieldLabel: gettext('Privileges'),
- },
- ],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response) {
- var data = response.result.data;
- var keys = Ext.Object.getKeys(data);
-
- me.setValues({
- privs: keys,
- roleid: me.roleid,
- });
- },
- });
- }
- },
-});
-Ext.define('PVE.dc.RoleView', {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveRoleView'],
-
- onlineHelp: 'pveum_roles',
-
- stateful: true,
- stateId: 'grid-roles',
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pmx-roles',
- sorters: {
- property: 'roleid',
- direction: 'ASC',
- },
- });
- Proxmox.Utils.monStoreErrors(me, store);
-
- let sm = Ext.create('Ext.selection.RowModel', {});
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- if (rec.data.special) {
- return;
- }
- Ext.create('PVE.dc.RoleEdit', {
- roleid: rec.data.roleid,
- privs: rec.data.privs,
- listeners: {
- destroy: () => store.load(),
- },
- autoShow: true,
- });
- };
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
-
- viewConfig: {
- trackOver: false,
- },
- columns: [
- {
- header: gettext('Built-In'),
- width: 65,
- sortable: true,
- dataIndex: 'special',
- renderer: Proxmox.Utils.format_boolean,
- },
- {
- header: gettext('Name'),
- width: 150,
- sortable: true,
- dataIndex: 'roleid',
- },
- {
- itemid: 'privs',
- header: gettext('Privileges'),
- sortable: false,
- renderer: (value, metaData) => {
- if (!value) {
- return '-';
- }
- metaData.style = 'white-space:normal;'; // allow word wrap
- return value.replace(/,/g, ' ');
- },
- variableRowHeight: true,
- dataIndex: 'privs',
- flex: 1,
- },
- ],
- listeners: {
- activate: function () {
- store.load();
- },
- itemdblclick: run_editor,
- },
- tbar: [
- {
- text: gettext('Create'),
- handler: function () {
- Ext.create('PVE.dc.RoleEdit', {
- listeners: {
- destroy: () => store.load(),
- },
- autoShow: true,
- });
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- enableFn: (rec) => !rec.data.special,
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- selModel: sm,
- callback: () => store.load(),
- baseurl: '/access/roles/',
- enableFn: (rec) => !rec.data.special,
- },
- ],
- });
-
- me.callParent();
- },
-});
-Ext.define('pve-security-groups', {
- extend: 'Ext.data.Model',
-
- fields: ['group', 'comment', 'digest'],
- idProperty: 'group',
-});
-
-Ext.define('PVE.SecurityGroupEdit', {
- extend: 'Proxmox.window.Edit',
-
- base_url: '/cluster/firewall/groups',
-
- allow_iface: false,
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = me.group_name === undefined;
-
- var subject;
-
- me.url = '/api2/extjs' + me.base_url;
- me.method = 'POST';
-
- var items = [
- {
- xtype: 'textfield',
- name: 'group',
- value: me.group_name || '',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'comment',
- value: me.group_comment || '',
- fieldLabel: gettext('Comment'),
- },
- ];
-
- if (me.isCreate) {
- subject = gettext('Security Group');
- } else {
- subject = gettext('Security Group') + " '" + me.group_name + "'";
- items.push({
- xtype: 'hiddenfield',
- name: 'rename',
- value: me.group_name,
- });
- }
-
- var ipanel = Ext.create('Proxmox.panel.InputPanel', {
- // InputPanel does not have a 'create' property, does it need a 'isCreate'
- isCreate: me.isCreate,
- items: items,
- });
-
- Ext.apply(me, {
- subject: subject,
- items: [ipanel],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.SecurityGroupList', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveSecurityGroupList',
-
- stateful: true,
- stateId: 'grid-securitygroups',
-
- rulePanel: undefined,
-
- addBtn: undefined,
- removeBtn: undefined,
- editBtn: undefined,
-
- base_url: '/cluster/firewall/groups',
-
- initComponent: function () {
- let me = this;
- if (!me.base_url) {
- throw 'no base_url specified';
- }
-
- let store = new Ext.data.Store({
- model: 'pve-security-groups',
- proxy: {
- type: 'proxmox',
- url: '/api2/json' + me.base_url,
- },
- sorters: {
- property: 'group',
- direction: 'ASC',
- },
- });
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let caps = Ext.state.Manager.get('GuiCap');
- let canEdit = !!caps.dc['Sys.Modify'];
-
- let reload = function () {
- let oldrec = sm.getSelection()[0];
- store.load((records, operation, success) => {
- if (oldrec) {
- let rec = store.findRecord('group', oldrec.data.group, 0, false, true, true);
- if (rec) {
- sm.select(rec);
- }
- }
- });
- };
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec || !canEdit) {
- return;
- }
- Ext.create('PVE.SecurityGroupEdit', {
- digest: rec.data.digest,
- group_name: rec.data.group,
- group_comment: rec.data.comment,
- listeners: {
- destroy: () => reload(),
- },
- autoShow: true,
- });
- };
-
- me.editBtn = new Proxmox.button.Button({
- text: gettext('Edit'),
- enableFn: (rec) => canEdit,
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
- me.addBtn = new Proxmox.button.Button({
- text: gettext('Create'),
- disabled: !canEdit,
- handler: function () {
- sm.deselectAll();
- var win = Ext.create('PVE.SecurityGroupEdit', {});
- win.show();
- win.on('destroy', reload);
- },
- });
-
- me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: me.base_url + '/',
- enableFn: (rec) => canEdit && rec && me.base_url,
- callback: () => reload(),
- });
-
- Ext.apply(me, {
- store: store,
- tbar: ['' + gettext('Group') + ': ', me.addBtn, me.removeBtn, me.editBtn],
- selModel: sm,
- columns: [
- {
- header: gettext('Group'),
- dataIndex: 'group',
- width: '100',
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- flex: 1,
- },
- ],
- listeners: {
- itemdblclick: run_editor,
- select: function (_sm, rec) {
- if (!me.rulePanel) {
- me.rulePanel = me.up('panel').down('pveFirewallRules');
- }
- me.rulePanel.setBaseUrl(`/cluster/firewall/groups/${rec.data.group}`);
- },
- deselect: function () {
- if (!me.rulePanel) {
- me.rulePanel = me.up('panel').down('pveFirewallRules');
- }
- me.rulePanel.setBaseUrl(undefined);
- },
- show: reload,
- },
- });
-
- me.callParent();
-
- store.load();
- },
-});
-
-Ext.define('PVE.SecurityGroups', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveSecurityGroups',
-
- title: 'Security Groups',
- onlineHelp: 'pve_firewall_security_groups',
-
- layout: 'border',
-
- items: [
- {
- xtype: 'pveFirewallRules',
- region: 'center',
- allow_groups: false,
- list_refs_url: '/cluster/firewall/refs',
- tbar_prefix: '' + gettext('Rules') + ': ',
- border: false,
- firewall_type: 'group',
- },
- {
- xtype: 'pveSecurityGroupList',
- region: 'west',
- width: '25%',
- border: false,
- split: true,
- },
- ],
- listeners: {
- show: function () {
- let sglist = this.down('pveSecurityGroupList');
- sglist.fireEvent('show', sglist);
- },
- },
-});
-Ext.define(
- 'PVE.dc.StorageView',
- {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveStorageView'],
-
- onlineHelp: 'chapter_storage',
-
- stateful: true,
- stateId: 'grid-dc-storage',
-
- createStorageEditWindow: function (type, sid) {
- let schema = PVE.Utils.storageSchema[type];
- if (!schema || !schema.ipanel) {
- throw 'no editor registered for storage type: ' + type;
- }
-
- Ext.create('PVE.storage.BaseEdit', {
- paneltype: 'PVE.storage.' + schema.ipanel,
- type: type,
- storageId: sid,
- canDoBackups: schema.backups,
- autoShow: true,
- listeners: {
- destroy: this.reloadStore,
- },
- });
- },
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pve-storage',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/storage',
- },
- sorters: {
- property: 'storage',
- direction: 'ASC',
- },
- });
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- let { type, storage } = rec.data;
- me.createStorageEditWindow(type, storage);
- };
-
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
- let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/storage/',
- callback: () => store.load(),
- });
-
- // else we cannot dynamically generate the add menu handlers
- let addHandleGenerator = function (type) {
- return function () {
- me.createStorageEditWindow(type);
- };
- };
- let addMenuItems = [];
- for (const [type, storage] of Object.entries(PVE.Utils.storageSchema)) {
- if (storage.hideAdd) {
- continue;
- }
- addMenuItems.push({
- text: PVE.Utils.format_storage_type(type),
- iconCls: 'fa fa-fw fa-' + storage.faIcon,
- handler: addHandleGenerator(type),
- });
- }
-
- Ext.apply(me, {
- store: store,
- reloadStore: () => store.load(),
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- tbar: [
- {
- text: gettext('Add'),
- menu: new Ext.menu.Menu({
- items: addMenuItems,
- }),
- },
- remove_btn,
- edit_btn,
- ],
- columns: [
- {
- header: 'ID',
- flex: 2,
- sortable: true,
- dataIndex: 'storage',
- },
- {
- header: gettext('Type'),
- flex: 1,
- sortable: true,
- dataIndex: 'type',
- renderer: PVE.Utils.format_storage_type,
- },
- {
- header: gettext('Content'),
- flex: 3,
- sortable: true,
- dataIndex: 'content',
- renderer: PVE.Utils.format_content_types,
- },
- {
- header: gettext('Path') + '/' + gettext('Target'),
- flex: 2,
- sortable: true,
- dataIndex: 'path',
- renderer: function (value, metaData, record) {
- if (record.data.target) {
- return record.data.target;
- }
- return value;
- },
- },
- {
- header: gettext('Shared'),
- flex: 1,
- sortable: true,
- dataIndex: 'shared',
- renderer: Proxmox.Utils.format_boolean,
- },
- {
- header: gettext('Enabled'),
- flex: 1,
- sortable: true,
- dataIndex: 'disable',
- renderer: Proxmox.Utils.format_neg_boolean,
- },
- {
- header: gettext('Bandwidth Limit'),
- flex: 2,
- sortable: true,
- dataIndex: 'bwlimit',
- },
- ],
- listeners: {
- activate: () => store.load(),
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-storage', {
- extend: 'Ext.data.Model',
- fields: [
- 'path',
- 'type',
- 'content',
- 'server',
- 'portal',
- 'target',
- 'export',
- 'storage',
- { name: 'shared', type: 'boolean' },
- { name: 'disable', type: 'boolean' },
- ],
- idProperty: 'storage',
- });
- },
-);
-Ext.define('PVE.dc.Summary', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveDcSummary',
-
- scrollable: true,
-
- bodyPadding: 5,
-
- layout: 'column',
-
- defaults: {
- padding: 5,
- columnWidth: 1,
- },
-
- items: [
- {
- itemId: 'dcHealth',
- xtype: 'pveDcHealth',
- },
- {
- itemId: 'dcGuests',
- xtype: 'pveDcGuests',
- },
- {
- title: gettext('Resources'),
- xtype: 'panel',
- minHeight: 250,
- bodyPadding: 5,
- layout: 'hbox',
- defaults: {
- xtype: 'proxmoxGauge',
- flex: 1,
- },
- items: [
- {
- title: gettext('CPU'),
- itemId: 'cpu',
- },
- {
- title: gettext('Memory'),
- itemId: 'memory',
- warningThreshold: 0.9,
- criticalThreshold: 0.975,
- },
- {
- title: gettext('Storage'),
- itemId: 'storage',
- },
- ],
- },
- {
- itemId: 'nodeview',
- xtype: 'pveDcNodeView',
- height: 250,
- },
- {
- title: gettext('Subscriptions'),
- height: 220,
- items: [
- {
- xtype: 'pveHealthWidget',
- itemId: 'subscriptions',
- userCls: 'pointer',
- listeners: {
- element: 'el',
- click: function () {
- if (this.component.userCls === 'pointer') {
- window.open(
- 'https://www.proxmox.com/en/proxmox-virtual-environment/pricing',
- '_blank',
- );
- }
- },
- },
- },
- ],
- },
- ],
-
- listeners: {
- resize: function (panel) {
- Proxmox.Utils.updateColumns(panel);
- },
- },
-
- initComponent: function () {
- var me = this;
-
- var rstore = Ext.create('Proxmox.data.UpdateStore', {
- interval: 3000,
- storeid: 'pve-cluster-status',
- model: 'pve-dc-nodes',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/status',
- },
- });
-
- var gridstore = Ext.create('Proxmox.data.DiffStore', {
- rstore: rstore,
- filters: {
- property: 'type',
- value: 'node',
- },
- sorters: {
- property: 'id',
- direction: 'ASC',
- },
- });
-
- me.callParent();
-
- me.getComponent('nodeview').setStore(gridstore);
-
- var gueststatus = me.getComponent('dcGuests');
-
- var cpustat = me.down('#cpu');
- var memorystat = me.down('#memory');
- var storagestat = me.down('#storage');
- var sp = Ext.state.Manager.getProvider();
-
- me.mon(PVE.data.ResourceStore, 'load', function (curstore, results) {
- me.suspendLayout = true;
-
- let cpu = 0,
- maxcpu = 0;
- let memory = 0,
- maxmem = 0;
-
- let used = 0,
- total = 0;
- let countedStorage = {},
- usableStorages = {};
- let storages = sp.get('dash-storages') || '';
- storages
- .split(',')
- .filter((v) => v !== '')
- .forEach((storage) => {
- usableStorages[storage] = true;
- });
-
- let qemu = {
- running: 0,
- paused: 0,
- stopped: 0,
- template: 0,
- };
- let lxc = {
- running: 0,
- paused: 0,
- stopped: 0,
- template: 0,
- };
- let error = 0;
-
- for (const { data } of results) {
- switch (data.type) {
- case 'node':
- cpu += data.cpu * data.maxcpu;
- maxcpu += data.maxcpu || 0;
- memory += data.mem || 0;
- maxmem += data.maxmem || 0;
-
- if (gridstore.getById(data.id)) {
- let griditem = gridstore.getById(data.id);
- griditem.set('cpuusage', data.cpu);
- let max = data.maxmem || 1;
- let val = data.mem || 0;
- griditem.set('memoryusage', val / max);
- griditem.set('uptime', data.uptime);
- griditem.commit(); // else the store marks the field as dirty
- }
- break;
- case 'storage': {
- let sid = !data.shared || data.storage === 'local' ? data.id : data.storage;
- if (!Ext.Object.isEmpty(usableStorages)) {
- if (usableStorages[data.id] !== true) {
- break;
- }
- sid = data.id;
- } else if (countedStorage[sid]) {
- break;
- }
-
- if (data.status === 'unknown') {
- break;
- }
-
- used += data.disk;
- total += data.maxdisk;
- countedStorage[sid] = true;
- break;
- }
- case 'qemu':
- qemu[data.template ? 'template' : data.status]++;
- if (data.hastate === 'error') {
- error++;
- }
- break;
- case 'lxc':
- lxc[data.template ? 'template' : data.status]++;
- if (data.hastate === 'error') {
- error++;
- }
- break;
- default:
- break;
- }
- }
-
- let text = Ext.String.format(gettext('of {0} CPU(s)'), maxcpu);
- cpustat.updateValue(cpu / maxcpu, text);
-
- text = Ext.String.format(
- gettext('{0} of {1}'),
- Proxmox.Utils.render_size(memory),
- Proxmox.Utils.render_size(maxmem),
- );
- memorystat.updateValue(memory / maxmem, text);
-
- text = Ext.String.format(
- gettext('{0} of {1}'),
- Proxmox.Utils.render_size(used),
- Proxmox.Utils.render_size(total),
- );
- storagestat.updateValue(used / total, text);
-
- gueststatus.updateValues(qemu, lxc, error);
-
- me.suspendLayout = false;
- me.updateLayout(true);
- });
-
- let dcHealth = me.getComponent('dcHealth');
- me.mon(rstore, 'load', dcHealth.updateStatus, dcHealth);
-
- let subs = me.down('#subscriptions');
- me.mon(rstore, 'load', function (store, records, success) {
- var level;
- var mixed = false;
- for (let i = 0; i < records.length; i++) {
- let node = records[i];
- if (node.get('type') !== 'node' || node.get('status') === 'offline') {
- continue;
- }
-
- let curlevel = node.get('level');
- if (curlevel === '') {
- // no subscription beats all, set it and break the loop
- level = '';
- break;
- }
-
- if (level === undefined) {
- // save level
- level = curlevel;
- } else if (level !== curlevel) {
- // detect different levels
- mixed = true;
- }
- }
-
- let data = {
- title: Proxmox.Utils.unknownText,
- text: Proxmox.Utils.unknownText,
- iconCls: PVE.Utils.get_health_icon(undefined, true),
- };
- if (level === '') {
- data = {
- title: gettext('No Subscription'),
- iconCls: PVE.Utils.get_health_icon('critical', true),
- text: gettext('You have at least one node without subscription.'),
- };
- subs.setUserCls('pointer');
- } else if (mixed) {
- data = {
- title: gettext('Mixed Subscriptions'),
- iconCls: PVE.Utils.get_health_icon('warning', true),
- text: gettext('Warning: Your subscription levels are not the same.'),
- };
- subs.setUserCls('pointer');
- } else if (level) {
- data = {
- title: PVE.Utils.render_support_level(level),
- iconCls: PVE.Utils.get_health_icon('good', true),
- text: gettext('Your subscription status is valid.'),
- };
- subs.setUserCls('');
- }
-
- subs.setData(data);
- });
-
- me.on('destroy', function () {
- rstore.stopUpdate();
- });
-
- me.mon(sp, 'statechange', function (provider, key, value) {
- if (key !== 'summarycolumns') {
- return;
- }
- Proxmox.Utils.updateColumns(me);
- });
-
- rstore.startUpdate();
- },
-});
-Ext.define('PVE.dc.Support', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveDcSupport',
- pveGuidePath: '/pve-docs/index.html',
- onlineHelp: 'getting_help',
-
- invalidHtml: 'No valid subscription ' + PVE.Utils.noSubKeyHtml,
-
- communityHtml:
- 'Please use the public community forum for any questions.',
-
- activeHtml:
- 'Please use our support portal for any questions. You can also use the public community forum to get additional information.',
-
- bugzillaHtml:
- 'Bug Tracking Our bug tracking system is available here .',
-
- docuHtml: function () {
- var me = this;
- var guideUrl = window.location.origin + me.pveGuidePath;
- var text = Ext.String.format(
- 'Documentation ' +
- 'The official Proxmox VE Administration Guide' +
- ' is included with this installation and can be browsed at ' +
- '{0} ',
- guideUrl,
- );
- return text;
- },
-
- updateActive: function (data) {
- var me = this;
-
- var html = '' + data.productname + ' ' + me.activeHtml;
- html += ' ' + me.docuHtml();
- html += ' ' + me.bugzillaHtml;
-
- me.update(html);
- },
-
- updateCommunity: function (data) {
- var me = this;
-
- var html = '' + data.productname + ' ' + me.communityHtml;
- html += ' ' + me.docuHtml();
- html += ' ' + me.bugzillaHtml;
-
- me.update(html);
- },
-
- updateInactive: function (data) {
- var me = this;
- me.update(me.invalidHtml);
- },
-
- initComponent: function () {
- let me = this;
-
- let reload = function () {
- Proxmox.Utils.API2Request({
- url: '/nodes/localhost/subscription',
- method: 'GET',
- waitMsgTarget: me,
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- me.update(
- `${gettext('Unable to load subscription status')}: ${response.htmlStatus}`,
- );
- },
- success: function (response, opts) {
- let data = response.result.data;
- if (data?.status.toLowerCase() === 'active') {
- if (data.level === 'c') {
- me.updateCommunity(data);
- } else {
- me.updateActive(data);
- }
- } else {
- me.updateInactive(data);
- }
- },
- });
- };
-
- Ext.apply(me, {
- autoScroll: true,
- bodyStyle: 'padding:10px',
- listeners: {
- activate: reload,
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.dc.SyncWindow', {
- extend: 'Ext.window.Window',
-
- title: gettext('Realm Sync'),
-
- width: 600,
- bodyPadding: 10,
- modal: true,
- resizable: false,
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- control: {
- form: {
- validitychange: function (field, valid) {
- let me = this;
- me.lookup('preview_btn').setDisabled(!valid);
- me.lookup('sync_btn').setDisabled(!valid);
- },
- },
- button: {
- click: function (btn) {
- if (btn.reference === 'help_btn') {
- return;
- }
- this.sync_realm(btn.reference === 'preview_btn');
- },
- },
- },
-
- sync_realm: function (is_preview) {
- let me = this;
- let view = me.getView();
- let ipanel = me.lookup('ipanel');
- let params = ipanel.getValues();
-
- let vanished_opts = [];
- ['acl', 'entry', 'properties'].forEach((prop) => {
- if (params[`remove-vanished-${prop}`]) {
- vanished_opts.push(prop);
- }
- delete params[`remove-vanished-${prop}`];
- });
- if (vanished_opts.length > 0) {
- params['remove-vanished'] = vanished_opts.join(';');
- } else {
- params['remove-vanished'] = 'none';
- }
-
- params['dry-run'] = is_preview ? 1 : 0;
- Proxmox.Utils.API2Request({
- url: `/access/domains/${view.realm}/sync`,
- waitMsgTarget: view,
- method: 'POST',
- params,
- failure: function (response) {
- view.show();
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response) {
- view.hide();
- Ext.create('Proxmox.window.TaskViewer', {
- upid: response.result.data,
- listeners: {
- destroy: function () {
- if (is_preview) {
- view.show();
- } else {
- view.close();
- }
- },
- },
- }).show();
- },
- });
- },
- },
-
- items: [
- {
- xtype: 'form',
- reference: 'form',
- border: false,
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%',
- },
- items: [
- {
- xtype: 'inputpanel',
- reference: 'ipanel',
- column1: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'scope',
- fieldLabel: gettext('Scope'),
- value: '',
- emptyText: gettext('No default available'),
- deleteEmpty: false,
- allowBlank: false,
- comboItems: [
- ['users', gettext('Users')],
- ['groups', gettext('Groups')],
- ['both', gettext('Users and Groups')],
- ],
- },
- ],
-
- column2: [
- {
- xtype: 'proxmoxKVComboBox',
- value: '1',
- deleteEmpty: false,
- allowBlank: false,
- comboItems: [
- ['1', Proxmox.Utils.yesText],
- ['0', Proxmox.Utils.noText],
- ],
- name: 'enable-new',
- fieldLabel: gettext('Enable new'),
- },
- ],
-
- columnB: [
- {
- xtype: 'fieldset',
- title: gettext('Remove Vanished Options'),
- items: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('ACL'),
- name: 'remove-vanished-acl',
- boxLabel: gettext('Remove ACLs of vanished users and groups.'),
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Entry'),
- name: 'remove-vanished-entry',
- boxLabel: gettext('Remove vanished user and group entries.'),
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Properties'),
- name: 'remove-vanished-properties',
- boxLabel: gettext(
- 'Remove vanished properties from synced users.',
- ),
- },
- ],
- },
- {
- xtype: 'displayfield',
- reference: 'defaulthint',
- value: gettext('Default sync options can be set by editing the realm.'),
- userCls: 'pmx-hint',
- hidden: true,
- },
- ],
- },
- ],
- },
- ],
-
- buttons: [
- {
- xtype: 'proxmoxHelpButton',
- reference: 'help_btn',
- onlineHelp: 'pveum_ldap_sync',
- hidden: false,
- },
- '->',
- {
- text: gettext('Preview'),
- reference: 'preview_btn',
- },
- {
- text: gettext('Sync'),
- reference: 'sync_btn',
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.realm) {
- throw 'no realm defined';
- }
-
- me.callParent();
-
- Proxmox.Utils.API2Request({
- url: `/access/domains/${me.realm}`,
- waitMsgTarget: me,
- method: 'GET',
- failure: function (response) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- me.close();
- },
- success: function (response) {
- let default_options = response.result.data['sync-defaults-options'];
- if (default_options) {
- let options = PVE.Parser.parsePropertyString(default_options);
- if (options['remove-vanished']) {
- let opts = options['remove-vanished'].split(';');
- for (const opt of opts) {
- options[`remove-vanished-${opt}`] = 1;
- }
- }
- let ipanel = me.lookup('ipanel');
- ipanel.setValues(options);
- } else {
- me.lookup('defaulthint').setVisible(true);
- }
-
- // check validity for button state
- me.lookup('form').isValid();
- },
- });
- },
-});
-/* This class defines the "Tasks" tab of the bottom status panel
- * Tasks are jobs with a start, end and log output
- */
-
-Ext.define('PVE.dc.Tasks', {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveClusterTasks'],
-
- initComponent: function () {
- let me = this;
-
- let taskstore = Ext.create('Proxmox.data.UpdateStore', {
- storeId: 'pve-cluster-tasks',
- model: 'proxmox-tasks',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/tasks',
- },
- });
- let store = Ext.create('Proxmox.data.DiffStore', {
- rstore: taskstore,
- sortAfterUpdate: true,
- appendAtStart: true,
- sorters: [
- {
- property: 'pid',
- direction: 'DESC',
- },
- {
- property: 'starttime',
- direction: 'DESC',
- },
- ],
- });
-
- let run_task_viewer = function () {
- var sm = me.getSelectionModel();
- var rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
-
- Ext.create('Proxmox.window.TaskViewer', {
- autoShow: true,
- upid: rec.data.upid,
- endtime: rec.data.endtime,
- });
- };
-
- Ext.apply(me, {
- store: store,
- stateful: false,
- viewConfig: {
- trackOver: false,
- stripeRows: true, // does not work with getRowClass()
- getRowClass: function (record, index) {
- let taskState = record.get('status');
- if (taskState) {
- let parsed = Proxmox.Utils.parse_task_status(taskState);
- if (parsed === 'warning') {
- return 'proxmox-warning-row';
- } else if (parsed !== 'ok') {
- return 'proxmox-invalid-row';
- }
- }
- return '';
- },
- },
- sortableColumns: false,
- columns: [
- {
- header: gettext('Start Time'),
- dataIndex: 'starttime',
- width: 150,
- renderer: function (value) {
- return Ext.Date.format(value, 'M d H:i:s');
- },
- },
- {
- header: gettext('End Time'),
- dataIndex: 'endtime',
- width: 150,
- align: 'inherit',
- renderer: function (value, metaData, record) {
- metaData.tdStyle = 'text-align: left;';
- if (record.data.pid) {
- if (
- record.data.type === 'vncproxy' ||
- record.data.type === 'vncshell' ||
- record.data.type === 'spiceproxy'
- ) {
- metaData.tdStyle = 'text-align: center;';
- return ' ';
- } else {
- metaData.tdCls = 'x-grid-row-loading';
- }
- return '';
- }
-
- return Ext.Date.format(value, 'M d H:i:s');
- },
- },
- {
- header: gettext('Node'),
- dataIndex: 'node',
- width: 100,
- },
- {
- header: gettext('User name'),
- dataIndex: 'user',
- renderer: Ext.String.htmlEncode,
- width: 150,
- },
- {
- header: gettext('Description'),
- dataIndex: 'upid',
- flex: 1,
- renderer: Proxmox.Utils.render_upid,
- },
- {
- header: gettext('Status'),
- dataIndex: 'status',
- width: 200,
- renderer: function (value, metaData, record) {
- if (record.data.pid) {
- if (record.data.type !== 'vncproxy') {
- metaData.tdCls = 'x-grid-row-loading';
- }
- return '';
- }
- return Proxmox.Utils.format_task_status(value);
- },
- },
- {
- xtype: 'actioncolumn',
- width: 30,
- align: 'center',
- tooltip: gettext('Actions'),
- items: [
- {
- iconCls: 'fa fa-chevron-right',
- tooltip: gettext('View Task'),
- handler: function (_grid, _rowIndex, _colIndex, _item, _e, rec) {
- Ext.create('Proxmox.window.TaskViewer', {
- autoShow: true,
- upid: rec.data.upid,
- endtime: rec.data.endtime,
- });
- },
- },
- ],
- },
- ],
- listeners: {
- itemdblclick: run_task_viewer,
- show: () => taskstore.startUpdate(),
- destroy: () => taskstore.stopUpdate(),
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.dc.TokenEdit', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveDcTokenEdit'],
- mixins: ['Proxmox.Mixin.CBind'],
-
- subject: gettext('Token'),
- onlineHelp: 'pveum_tokens',
-
- isAdd: true,
- isCreate: false,
-
- method: 'POST',
- url: '/api2/extjs/access/users/',
-
- defaultFocus: 'field[disabled=false][hidden=false][name=tokenid]',
-
- items: {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- let me = this;
- let win = me.up('pveDcTokenEdit');
- win.url = '/api2/extjs/access/users/';
- let uid = encodeURIComponent(values.userid);
- let tid = encodeURIComponent(values.tokenid);
- delete values.userid;
- delete values.tokenid;
-
- win.url += `${uid}/token/${tid}`;
- return values;
- },
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: '{isCreate}',
- },
- submitValue: true,
- editConfig: {
- xtype: 'pmxUserSelector',
- allowBlank: false,
- },
- name: 'userid',
- value: Proxmox.UserName,
- renderer: Ext.String.htmlEncode,
- fieldLabel: gettext('User'),
- },
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: '{isCreate}',
- },
- name: 'tokenid',
- fieldLabel: gettext('Token ID'),
- submitValue: true,
- minLength: 2,
- allowBlank: false,
- },
- ],
- column2: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'privsep',
- checked: true,
- uncheckedValue: 0,
- fieldLabel: gettext('Privilege Separation'),
- },
- {
- xtype: 'pmxExpireDate',
- name: 'expire',
- },
- ],
- columnB: [
- {
- xtype: 'textfield',
- name: 'comment',
- fieldLabel: gettext('Comment'),
- },
- ],
- },
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- me.setValues(response.result.data);
- },
- });
- }
- },
- apiCallDone: function (success, response, options) {
- let res = response.result.data;
- if (!success || !res.value) {
- return;
- }
-
- Ext.create('PVE.dc.TokenShow', {
- autoShow: true,
- tokenid: res['full-tokenid'],
- secret: res.value,
- });
- },
-});
-
-Ext.define('PVE.dc.TokenShow', {
- extend: 'Ext.window.Window',
- alias: ['widget.pveTokenShow'],
- mixins: ['Proxmox.Mixin.CBind'],
-
- width: 600,
- modal: true,
- resizable: false,
- title: gettext('Token Secret'),
-
- items: [
- {
- xtype: 'container',
- layout: 'form',
- bodyPadding: 10,
- border: false,
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%',
- },
- padding: '0 10 10 10',
- items: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Token ID'),
- cbind: {
- value: '{tokenid}',
- },
- editable: false,
- },
- {
- xtype: 'textfield',
- fieldLabel: gettext('Secret'),
- inputId: 'token-secret-value',
- cbind: {
- value: '{secret}',
- },
- editable: false,
- },
- ],
- },
- {
- xtype: 'component',
- border: false,
- padding: '10 10 10 10',
- userCls: 'pmx-hint',
- html: gettext('Please record the API token secret - it will only be displayed now'),
- },
- ],
- buttons: [
- {
- handler: function (b) {
- document.getElementById('token-secret-value').select();
- document.execCommand('copy');
- },
- text: gettext('Copy Secret Value'),
- iconCls: 'fa fa-clipboard',
- },
- ],
-});
-Ext.define('PVE.dc.TokenView', {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveTokenView'],
-
- onlineHelp: 'chapter_user_management',
-
- stateful: true,
- stateId: 'grid-tokens',
-
- initComponent: function () {
- let me = this;
-
- let caps = Ext.state.Manager.get('GuiCap');
-
- let store = new Ext.data.Store({
- id: 'tokens',
- model: 'pve-tokens',
- sorters: 'id',
- });
-
- let reload = function () {
- Proxmox.Utils.API2Request({
- url: '/access/users/?full=1',
- method: 'GET',
- failure: function (response, opts) {
- Proxmox.Utils.setErrorMask(me, response.htmlStatus);
- me.load_task.delay(me.load_delay);
- },
- success: function (response, opts) {
- Proxmox.Utils.setErrorMask(me, false);
- let result = Ext.decode(response.responseText);
- let data = result.data || [];
- let records = [];
- Ext.Array.each(data, function (user) {
- let tokens = user.tokens || [];
- Ext.Array.each(tokens, function (token) {
- let r = {};
- r.id = user.userid + '!' + token.tokenid;
- r.userid = user.userid;
- r.tokenid = token.tokenid;
- r.comment = token.comment;
- r.expire = token.expire;
- r.privsep = token.privsep === 1;
- records.push(r);
- });
- });
- store.loadData(records);
- },
- });
- };
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let urlFromRecord = (rec) => {
- let uid = encodeURIComponent(rec.data.userid);
- let tid = encodeURIComponent(rec.data.tokenid);
- return `/access/users/${uid}/token/${tid}`;
- };
-
- let hasTokenCRUDPermissions = function (userid) {
- return userid === Proxmox.UserName || !!caps.access['User.Modify'];
- };
-
- let run_editor = function (rec) {
- if (!hasTokenCRUDPermissions(rec.data.userid)) {
- return;
- }
-
- let win = Ext.create('PVE.dc.TokenEdit', {
- method: 'PUT',
- url: urlFromRecord(rec),
- });
- win.setValues(rec.data);
- win.on('destroy', reload);
- win.show();
- };
-
- let tbar = [
- {
- text: gettext('Add'),
- handler: function (btn, e) {
- let data = {};
- let win = Ext.create('PVE.dc.TokenEdit', {
- isCreate: true,
- });
- win.setValues(data);
- win.on('destroy', reload);
- win.show();
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- disabled: true,
- enableFn: (rec) => hasTokenCRUDPermissions(rec.data.userid),
- selModel: sm,
- handler: (btn, e, rec) => run_editor(rec),
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- selModel: sm,
- enableFn: (rec) => hasTokenCRUDPermissions(rec.data.userid),
- callback: reload,
- getUrl: urlFromRecord,
- },
- '-',
- {
- xtype: 'proxmoxButton',
- text: gettext('Show Permissions'),
- disabled: true,
- selModel: sm,
- handler: function (btn, event, rec) {
- Ext.create('PVE.dc.PermissionView', {
- autoShow: true,
- userid: rec.data.id,
- });
- },
- },
- ];
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: tbar,
- viewConfig: {
- trackOver: false,
- },
- columns: [
- {
- header: gettext('User name'),
- dataIndex: 'userid',
- renderer: (uid) => {
- let realmIndex = uid.lastIndexOf('@');
- let user = Ext.String.htmlEncode(uid.substr(0, realmIndex));
- let realm = Ext.String.htmlEncode(uid.substr(realmIndex));
- return `${user} ${realm} `;
- },
- flex: 2,
- },
- {
- header: gettext('Token Name'),
- dataIndex: 'tokenid',
- hideable: false,
- flex: 1,
- },
- {
- header: gettext('Expire'),
- dataIndex: 'expire',
- hideable: false,
- renderer: Proxmox.Utils.format_expire,
- flex: 1,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- renderer: Ext.String.htmlEncode,
- flex: 3,
- },
- {
- header: gettext('Privilege Separation'),
- dataIndex: 'privsep',
- hideable: false,
- renderer: Proxmox.Utils.format_boolean,
- flex: 1,
- },
- ],
- listeners: {
- activate: reload,
- itemdblclick: (view, rec) => run_editor(rec),
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.dc.UserEdit', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveDcUserEdit'],
-
- isAdd: true,
-
- initComponent: function () {
- let me = this;
-
- me.isCreate = !me.userid;
-
- let url = '/api2/extjs/access/users';
- let method = 'POST';
- if (!me.isCreate) {
- url += '/' + encodeURIComponent(me.userid);
- method = 'PUT';
- }
-
- let verifypw, pwfield;
- let validate_pw = function () {
- if (verifypw.getValue() !== pwfield.getValue()) {
- return gettext('Passwords do not match');
- }
- return true;
- };
- verifypw = Ext.createWidget('textfield', {
- inputType: 'password',
- fieldLabel: gettext('Confirm password'),
- name: 'verifypassword',
- submitValue: false,
- disabled: true,
- hidden: true,
- validator: validate_pw,
- });
-
- pwfield = Ext.createWidget('textfield', {
- inputType: 'password',
- fieldLabel: gettext('Password'),
- minLength: 8,
- name: 'password',
- disabled: true,
- hidden: true,
- validator: validate_pw,
- });
-
- let column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'userid',
- fieldLabel: gettext('User name'),
- value: me.userid,
- renderer: Ext.String.htmlEncode,
- allowBlank: false,
- submitValue: !!me.isCreate,
- },
- pwfield,
- verifypw,
- {
- xtype: 'pveGroupSelector',
- name: 'groups',
- multiSelect: true,
- allowBlank: true,
- fieldLabel: gettext('Group'),
- },
- {
- xtype: 'pmxExpireDate',
- name: 'expire',
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Enabled'),
- name: 'enable',
- uncheckedValue: 0,
- defaultValue: 1,
- checked: true,
- },
- ];
-
- let column2 = [
- {
- xtype: 'textfield',
- name: 'firstname',
- fieldLabel: gettext('First Name'),
- },
- {
- xtype: 'textfield',
- name: 'lastname',
- fieldLabel: gettext('Last Name'),
- },
- {
- xtype: 'textfield',
- name: 'email',
- fieldLabel: gettext('E-Mail'),
- vtype: 'proxmoxMail',
- },
- ];
-
- if (me.isCreate) {
- column1.splice(1, 0, {
- xtype: 'pmxRealmComboBox',
- name: 'realm',
- fieldLabel: gettext('Realm'),
- allowBlank: false,
- matchFieldWidth: false,
- listConfig: { width: 300 },
- listeners: {
- change: function (combo, realm) {
- me.realm = realm;
- pwfield.setVisible(realm === 'pve');
- pwfield.setDisabled(realm !== 'pve');
- verifypw.setVisible(realm === 'pve');
- verifypw.setDisabled(realm !== 'pve');
- },
- },
- submitValue: false,
- });
- }
-
- var ipanel = Ext.create('Proxmox.panel.InputPanel', {
- column1: column1,
- column2: column2,
- columnB: [
- {
- xtype: 'textfield',
- name: 'comment',
- fieldLabel: gettext('Comment'),
- },
- ],
- advancedItems: [
- {
- xtype: 'textfield',
- name: 'keys',
- fieldLabel: gettext('Key IDs'),
- },
- ],
- onGetValues: function (values) {
- if (me.realm) {
- values.userid = values.userid + '@' + me.realm;
- }
- if (!values.password) {
- delete values.password;
- }
- return values;
- },
- });
-
- Ext.applyIf(me, {
- subject: gettext('User'),
- url: url,
- method: method,
- fieldDefaults: {
- labelWidth: 110, // some translation are quite long (e.g., Spanish)
- },
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var data = response.result.data;
- me.setValues(data);
- if (data.keys) {
- if (
- data.keys === 'x' ||
- data.keys === 'x!oath' ||
- data.keys === 'x!u2f' ||
- data.keys === 'x!yubico'
- ) {
- me.down('[name="keys"]').setDisabled(1);
- }
- }
- },
- });
- }
- },
-});
-Ext.define('PVE.dc.UserView', {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveUserView'],
-
- onlineHelp: 'pveum_users',
-
- stateful: true,
- stateId: 'grid-users',
-
- initComponent: function () {
- var me = this;
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- var store = new Ext.data.Store({
- id: 'users',
- model: 'pmx-users',
- sorters: {
- property: 'userid',
- direction: 'ASC',
- },
- });
- let reload = () => store.load();
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/access/users/',
- dangerous: true,
- enableFn: (rec) => caps.access['User.Modify'] && rec.data.userid !== 'root@pam',
- callback: () => reload(),
- });
- let run_editor = function () {
- var rec = sm.getSelection()[0];
- if (!rec || !caps.access['User.Modify']) {
- return;
- }
- Ext.create('PVE.dc.UserEdit', {
- userid: rec.data.userid,
- autoShow: true,
- listeners: {
- destroy: () => reload(),
- },
- });
- };
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- enableFn: function (rec) {
- return !!caps.access['User.Modify'];
- },
- selModel: sm,
- handler: run_editor,
- });
- let pwchange_btn = new Proxmox.button.Button({
- text: gettext('Password'),
- disabled: true,
- selModel: sm,
- enableFn: function (record) {
- let type = record.data['realm-type'];
- if (type) {
- if (PVE.Utils.authSchema[type]) {
- return !!PVE.Utils.authSchema[type].pwchange;
- }
- }
- return false;
- },
- handler: function (btn, event, rec) {
- let hintHtml;
- if (rec.data['realm-type'] === 'pam') {
- hintHtml = gettext(
- 'For the PAM realm, this applies only to the connected node.',
- );
- }
-
- Ext.create('Proxmox.window.PasswordEdit', {
- userid: rec.data.userid,
- confirmCurrentPassword: Proxmox.UserName !== 'root@pam',
- autoShow: true,
- hintHtml,
- minLength: 8,
- listeners: {
- destroy: () => reload(),
- },
- });
- },
- });
-
- var perm_btn = new Proxmox.button.Button({
- text: gettext('Permissions'),
- disabled: true,
- selModel: sm,
- handler: function (btn, event, rec) {
- Ext.create('PVE.dc.PermissionView', {
- userid: rec.data.userid,
- autoShow: true,
- listeners: {
- destroy: () => reload(),
- },
- });
- },
- });
-
- let unlock_btn = new Proxmox.button.Button({
- text: gettext('Unlock TFA'),
- disabled: true,
- selModel: sm,
- enableFn: (rec) =>
- !!(
- caps.access['User.Modify'] &&
- (rec.data['totp-locked'] || rec.data['tfa-locked-until'])
- ),
- handler: function (btn, event, rec) {
- Ext.Msg.confirm(
- Ext.String.format(
- gettext('Unlock TFA authentication for {0}'),
- rec.data.userid,
- ),
- gettext(
- "Locked 2nd factors can happen if the user's password was leaked. Are you sure you want to unlock the user?",
- ),
- function (btn_response) {
- if (btn_response === 'yes') {
- Proxmox.Utils.API2Request({
- url: `/access/users/${rec.data.userid}/unlock-tfa`,
- waitMsgTarget: me,
- method: 'PUT',
- failure: function (response, options) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response, options) {
- reload();
- },
- });
- }
- },
- );
- },
- });
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: [
- {
- text: gettext('Add'),
- disabled: !caps.access['User.Modify'],
- handler: function () {
- Ext.create('PVE.dc.UserEdit', {
- autoShow: true,
- listeners: {
- destroy: () => reload(),
- },
- });
- },
- },
- '-',
- edit_btn,
- remove_btn,
- '-',
- pwchange_btn,
- '-',
- perm_btn,
- '-',
- unlock_btn,
- ],
- viewConfig: {
- trackOver: false,
- },
- columns: [
- {
- header: gettext('User name'),
- width: 200,
- sortable: true,
- renderer: Proxmox.Utils.render_username,
- dataIndex: 'userid',
- },
- {
- header: gettext('Realm'),
- width: 100,
- sortable: true,
- renderer: Proxmox.Utils.render_realm,
- dataIndex: 'userid',
- },
- {
- header: gettext('Enabled'),
- width: 80,
- sortable: true,
- renderer: Proxmox.Utils.format_boolean,
- dataIndex: 'enable',
- },
- {
- header: gettext('Expire'),
- width: 80,
- sortable: true,
- renderer: Proxmox.Utils.format_expire,
- dataIndex: 'expire',
- },
- {
- header: gettext('Name'),
- width: 150,
- sortable: true,
- renderer: PVE.Utils.render_full_name,
- dataIndex: 'firstname',
- },
- {
- header: 'TFA',
- width: 120,
- sortable: true,
- renderer: function (v, metaData, 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;
- },
- dataIndex: 'keys',
- },
- {
- header: gettext('Groups'),
- dataIndex: 'groups',
- renderer: Ext.htmlEncode,
- flex: 2,
- },
- {
- header: gettext('Comment'),
- sortable: false,
- renderer: Ext.String.htmlEncode,
- dataIndex: 'comment',
- flex: 3,
- },
- ],
- listeners: {
- activate: reload,
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
-
- Proxmox.Utils.monStoreErrors(me, store);
- },
-});
-Ext.define('PVE.dc.MetricServerView', {
- extend: 'Ext.grid.Panel',
- alias: ['widget.pveMetricServerView'],
-
- stateful: true,
- stateId: 'grid-metricserver',
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- render_type: function (value) {
- switch (value) {
- case 'influxdb':
- return 'InfluxDB';
- case 'graphite':
- return 'Graphite';
- case 'opentelemetry':
- return 'OpenTelemetry';
- default:
- return Proxmox.Utils.unknownText;
- }
- },
-
- editWindow: function (xtype, id) {
- let me = this;
- Ext.create(`PVE.dc.${xtype}Edit`, {
- serverid: id,
- autoShow: true,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
-
- addServer: function (button) {
- this.editWindow(button.text);
- },
-
- editServer: function () {
- let me = this;
- let view = me.getView();
- let selection = view.getSelection();
- if (!selection || selection.length < 1) {
- return;
- }
-
- let cfg = selection[0].data;
-
- let xtype = me.render_type(cfg.type);
- me.editWindow(xtype, cfg.id);
- },
-
- reload: function () {
- this.getView().getStore().load();
- },
- },
-
- store: {
- autoLoad: true,
- id: 'metricservers',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/metrics/server',
- },
- },
-
- columns: [
- {
- text: gettext('Name'),
- flex: 2,
- dataIndex: 'id',
- },
- {
- text: gettext('Type'),
- flex: 1,
- dataIndex: 'type',
- renderer: 'render_type',
- },
- {
- text: gettext('Enabled'),
- dataIndex: 'disable',
- width: 100,
- renderer: Proxmox.Utils.format_neg_boolean,
- },
- {
- text: gettext('Server'),
- width: 200,
- dataIndex: 'server',
- },
- {
- text: gettext('Port'),
- width: 100,
- dataIndex: 'port',
- },
- ],
-
- tbar: [
- {
- text: gettext('Add'),
- menu: [
- {
- text: 'Graphite',
- iconCls: 'fa fa-fw fa-bar-chart',
- handler: 'addServer',
- },
- {
- text: 'InfluxDB',
- iconCls: 'fa fa-fw fa-bar-chart',
- handler: 'addServer',
- },
- {
- text: 'OpenTelemetry',
- iconCls: 'fa fa-fw fa-bar-chart',
- handler: 'addServer',
- },
- ],
- },
- {
- text: gettext('Edit'),
- xtype: 'proxmoxButton',
- handler: 'editServer',
- disabled: true,
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- baseurl: `/api2/extjs/cluster/metrics/server`,
- callback: 'reload',
- },
- ],
-
- listeners: {
- itemdblclick: 'editServer',
- },
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
-
- Proxmox.Utils.monStoreErrors(me, me.getStore());
- },
-});
-
-Ext.define('PVE.dc.MetricServerBaseEdit', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- cbindData: function () {
- let me = this;
- me.isCreate = !me.serverid;
- me.serverid = me.serverid || '';
- me.url = `/api2/extjs/cluster/metrics/server/${me.serverid}`;
- me.method = me.isCreate ? 'POST' : 'PUT';
- if (!me.isCreate) {
- me.subject = `${me.subject}: ${me.serverid}`;
- }
- return {};
- },
-
- submitUrl: function (url, values) {
- return this.isCreate ? `${url}/${values.id}` : url;
- },
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- if (me.serverid) {
- me.load({
- success: function (response, options) {
- let values = response.result.data;
- values.enable = !values.disable;
-
- // Handle OpenTelemetry advanced fields conversion
- if (values.type === 'opentelemetry') {
- if (values['otel-headers']) {
- values.headers_advanced = Ext.util.Base64.decode(
- values['otel-headers'],
- );
- }
- if (values['otel-resource-attributes']) {
- values.resource_attributes_advanced = Ext.util.Base64.decode(
- values['otel-resource-attributes'],
- );
- }
- }
-
- me.down('inputpanel').setValues(values);
- },
- });
- }
- },
-});
-
-Ext.define('PVE.dc.InfluxDBEdit', {
- extend: 'PVE.dc.MetricServerBaseEdit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'metric_server_influxdb',
-
- subject: 'InfluxDB',
-
- cbindData: function () {
- let me = this;
- me.callParent();
- me.tokenEmptyText = me.isCreate ? '' : gettext('unchanged');
- return {};
- },
-
- items: [
- {
- xtype: 'inputpanel',
- cbind: {
- isCreate: '{isCreate}',
- },
- onGetValues: function (values) {
- let me = this;
- values.disable = values.enable ? 0 : 1;
- delete values.enable;
- PVE.Utils.delete_if_default(values, 'verify-certificate', '1', me.isCreate);
- return values;
- },
-
- column1: [
- {
- xtype: 'hidden',
- name: 'type',
- value: 'influxdb',
- cbind: {
- submitValue: '{isCreate}',
- },
- },
- {
- xtype: 'pmxDisplayEditField',
- name: 'id',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- cbind: {
- editable: '{isCreate}',
- value: '{serverid}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'server',
- fieldLabel: gettext('Server'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'port',
- fieldLabel: gettext('Port'),
- value: 8089,
- minValue: 1,
- maximum: 65536,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'influxdbproto',
- fieldLabel: gettext('Protocol'),
- value: '__default__',
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- comboItems: [
- ['__default__', 'UDP'],
- ['http', 'HTTP'],
- ['https', 'HTTPS'],
- ],
- listeners: {
- change: function (field, value) {
- let me = this;
- let view = me.up('inputpanel');
- let isUdp = value !== 'http' && value !== 'https';
- view.down('field[name=organization]').setDisabled(isUdp);
- view.down('field[name=bucket]').setDisabled(isUdp);
- view.down('field[name=token]').setDisabled(isUdp);
- view.down('field[name=api-path-prefix]').setDisabled(isUdp);
- view.down('field[name=mtu]').setDisabled(!isUdp);
- view.down('field[name=timeout]').setDisabled(isUdp);
- view.down('field[name=max-body-size]').setDisabled(isUdp);
- view.down('field[name=verify-certificate]').setDisabled(
- value !== 'https',
- );
- },
- },
- },
- ],
-
- column2: [
- {
- xtype: 'checkbox',
- name: 'enable',
- fieldLabel: gettext('Enabled'),
- inputValue: 1,
- uncheckedValue: 0,
- checked: true,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'organization',
- fieldLabel: gettext('Organization'),
- emptyText: 'proxmox',
- disabled: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'bucket',
- fieldLabel: gettext('Bucket'),
- emptyText: 'proxmox',
- disabled: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'token',
- fieldLabel: gettext('Token'),
- disabled: true,
- allowBlank: true,
- deleteEmpty: false,
- submitEmpty: false,
- cbind: {
- disabled: '{!isCreate}',
- emptyText: '{tokenEmptyText}',
- },
- },
- ],
-
- advancedColumn1: [
- {
- xtype: 'proxmoxtextfield',
- name: 'api-path-prefix',
- fieldLabel: gettext('API Path Prefix'),
- allowBlank: true,
- disabled: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'timeout',
- fieldLabel: gettext('Timeout (s)'),
- disabled: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- minValue: 1,
- emptyText: 1,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'verify-certificate',
- fieldLabel: gettext('Verify Certificate'),
- value: 1,
- uncheckedValue: 0,
- disabled: true,
- },
- ],
-
- advancedColumn2: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'max-body-size',
- fieldLabel: gettext('Batch Size (bytes)'),
- minValue: 1,
- emptyText: '25000000',
- submitEmpty: false,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'mtu',
- fieldLabel: 'MTU',
- minValue: 1,
- emptyText: '1500',
- submitEmpty: false,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
- },
- ],
-});
-
-Ext.define('PVE.dc.GraphiteEdit', {
- extend: 'PVE.dc.MetricServerBaseEdit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'metric_server_graphite',
-
- subject: 'Graphite',
-
- items: [
- {
- xtype: 'inputpanel',
-
- onGetValues: function (values) {
- values.disable = values.enable ? 0 : 1;
- delete values.enable;
- return values;
- },
-
- column1: [
- {
- xtype: 'hidden',
- name: 'type',
- value: 'graphite',
- cbind: {
- submitValue: '{isCreate}',
- },
- },
- {
- xtype: 'pmxDisplayEditField',
- name: 'id',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- cbind: {
- editable: '{isCreate}',
- value: '{serverid}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'server',
- fieldLabel: gettext('Server'),
- allowBlank: false,
- },
- ],
-
- column2: [
- {
- xtype: 'checkbox',
- name: 'enable',
- fieldLabel: gettext('Enabled'),
- inputValue: 1,
- uncheckedValue: 0,
- checked: true,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'port',
- fieldLabel: gettext('Port'),
- value: 2003,
- minimum: 1,
- maximum: 65536,
- allowBlank: false,
- },
- {
- fieldLabel: gettext('Path'),
- xtype: 'proxmoxtextfield',
- emptyText: 'proxmox',
- name: 'path',
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-
- advancedColumn1: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'proto',
- fieldLabel: gettext('Protocol'),
- value: '__default__',
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- comboItems: [
- ['__default__', 'UDP'],
- ['tcp', 'TCP'],
- ],
- listeners: {
- change: function (field, value) {
- let me = this;
- me.up('inputpanel')
- .down('field[name=timeout]')
- .setDisabled(value !== 'tcp');
- me.up('inputpanel')
- .down('field[name=mtu]')
- .setDisabled(value === 'tcp');
- },
- },
- },
- ],
-
- advancedColumn2: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'mtu',
- fieldLabel: 'MTU',
- minimum: 1,
- emptyText: '1500',
- submitEmpty: false,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'timeout',
- fieldLabel: gettext('TCP Timeout'),
- disabled: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- minValue: 1,
- emptyText: 1,
- },
- ],
- },
- ],
-});
-
-Ext.define('PVE.dc.OpenTelemetryEdit', {
- extend: 'PVE.dc.MetricServerBaseEdit',
- xtype: 'pveOpenTelemetryEdit',
-
- subject: gettext('OpenTelemetry Server'),
-
- items: [
- {
- xtype: 'inputpanel',
- cbind: {
- isCreate: '{isCreate}',
- },
- onGetValues: function (values) {
- values.disable = values.enable ? 0 : 1;
- delete values.enable;
-
- // Rename advanced fields to their final names and encode as base64 (same as webhook)
- if (values.headers_advanced && values.headers_advanced.trim()) {
- values['otel-headers'] = Ext.util.Base64.encode(values.headers_advanced);
- } else {
- values['otel-headers'] = '';
- }
- delete values.headers_advanced;
-
- if (
- values.resource_attributes_advanced &&
- values.resource_attributes_advanced.trim()
- ) {
- values['otel-resource-attributes'] = Ext.util.Base64.encode(
- values.resource_attributes_advanced,
- );
- } else {
- values['otel-resource-attributes'] = '';
- }
- delete values.resource_attributes_advanced;
-
- return values;
- },
-
- column1: [
- {
- xtype: 'hidden',
- name: 'type',
- value: 'opentelemetry',
- cbind: {
- submitValue: '{isCreate}',
- },
- },
- {
- xtype: 'pmxDisplayEditField',
- name: 'id',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- cbind: {
- editable: '{isCreate}',
- value: '{serverid}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'server',
- fieldLabel: gettext('Server'),
- allowBlank: false,
- // TRANSLATORS: otel-collector is an OpenTelemetry endpoint.
- emptyText: gettext('otel-collector.example.com'),
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'port',
- fieldLabel: gettext('Port'),
- value: 4318,
- minValue: 1,
- maxValue: 65535,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'otel-protocol',
- fieldLabel: gettext('Protocol'),
- value: 'https',
- comboItems: [
- ['http', 'HTTP'],
- ['https', 'HTTPS'],
- ],
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'otel-path',
- fieldLabel: gettext('Path'),
- value: '/v1/metrics',
- allowBlank: false,
- },
- ],
-
- column2: [
- {
- xtype: 'checkbox',
- name: 'enable',
- fieldLabel: gettext('Enabled'),
- inputValue: 1,
- uncheckedValue: 0,
- checked: true,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'otel-timeout',
- fieldLabel: gettext('Timeout (s)'),
- value: 5,
- minValue: 1,
- maxValue: 300,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'otel-verify-ssl',
- fieldLabel: gettext('Verify SSL'),
- inputValue: 1,
- uncheckedValue: 0,
- defaultValue: 1,
- cbind: {
- value: function (get) {
- return get('isCreate') ? 1 : undefined;
- },
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'otel-max-body-size',
- fieldLabel: gettext('Max Body Size (bytes)'),
- value: 10000000,
- minValue: 1024,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'otel-compression',
- fieldLabel: gettext('Compression'),
- value: 'gzip',
- comboItems: [
- ['none', gettext('None')],
- ['gzip', 'Gzip'],
- ],
- allowBlank: false,
- },
- ],
-
- columnB: [
- {
- xtype: 'fieldset',
- title: gettext('Advanced JSON Configuration'),
- collapsible: true,
- collapsed: true,
- items: [
- {
- xtype: 'textarea',
- name: 'headers_advanced',
- fieldLabel: gettext('HTTP Headers (JSON)'),
- labelAlign: 'top',
- // TRANSLATORS: These are sample json payloads, only translate the values
- emptyText: gettext(
- '{\n "Authorization": "Bearer token",\n "X-Custom-Header": "value"\n}',
- ),
- rows: 4,
- validator: function (value) {
- if (!value || value.trim() === '') {
- return true;
- }
- try {
- JSON.parse(value);
- return true;
- } catch (_e) {
- return gettext('Invalid JSON format');
- }
- },
- },
- {
- xtype: 'textarea',
- name: 'resource_attributes_advanced',
- fieldLabel: gettext('Resource Attributes (JSON)'),
- labelAlign: 'top',
- // TRANSLATORS: These are sample json payloads, only translate the values
- emptyText: gettext(
- '{\n "environment": "production",\n "datacenter": "dc1",\n "region": "us-east-1"\n}',
- ),
- rows: 4,
- validator: function (value) {
- if (!value || value.trim() === '') {
- return true;
- }
- try {
- JSON.parse(value);
- return true;
- } catch (_e) {
- return gettext('Invalid JSON format');
- }
- },
- },
- ],
- },
- ],
- },
- ],
-
- initComponent: function () {
- var me = this;
- var initialLoad = true;
-
- me.callParent();
-
- // Auto-adjust port when protocol changes (only for user interaction)
- me.on('afterrender', function () {
- var protocolField = me.down('[name=otel-protocol]');
- var portField = me.down('[name=port]');
-
- if (protocolField && portField) {
- // Set flag to false after initial load
- me.on('loadrecord', function () {
- setTimeout(function () {
- initialLoad = false;
- }, 100);
- });
-
- protocolField.on('change', function (field, newValue) {
- // Only auto-adjust port if this is user interaction, not initial load
- if (!initialLoad) {
- if (newValue === 'https') {
- portField.setValue(4318);
- } else {
- portField.setValue(4317);
- }
- }
- });
- }
- });
- },
-});
-Ext.define('PVE.dc.UserTagAccessEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveUserTagAccessEdit',
-
- subject: gettext('User Tag Access'),
- onlineHelp: 'gui_tags',
-
- url: '/api2/extjs/cluster/options',
-
- hintText: gettext('NOTE: The following tags are also defined as registered tags.'),
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- tagChange: function (field, value) {
- let me = this;
- let view = me.getView();
- let also_registered = [];
- value = Ext.isArray(value) ? value : value.split(';');
- value.forEach((tag) => {
- if (view.registered_tags.indexOf(tag) !== -1) {
- also_registered.push(tag);
- }
- });
- let hint_field = me.lookup('hintField');
- hint_field.setVisible(also_registered.length > 0);
- if (also_registered.length > 0) {
- hint_field.setValue(`${view.hintText} ${also_registered.join(', ')}`);
- }
- },
- },
-
- items: [
- {
- xtype: 'inputpanel',
- setValues: function (values) {
- this.up('pveUserTagAccessEdit').registered_tags = values?.['registered-tags'] ?? [];
- let data = values?.['user-tag-access'] ?? {};
- return Proxmox.panel.InputPanel.prototype.setValues.call(this, data);
- },
- onGetValues: function (values) {
- if (values === undefined || Object.keys(values).length === 0) {
- return { delete: 'user-tag-access' };
- }
- return {
- 'user-tag-access': PVE.Parser.printPropertyString(values),
- };
- },
- items: [
- {
- name: 'user-allow',
- fieldLabel: gettext('Mode'),
- xtype: 'proxmoxKVComboBox',
- deleteEmpty: false,
- value: '__default__',
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (free)'],
- ['free', 'free'],
- ['existing', 'existing'],
- ['list', 'list'],
- ['none', 'none'],
- ],
- defaultValue: '__default__',
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Predefined Tags'),
- },
- {
- name: 'user-allow-list',
- xtype: 'pveListField',
- emptyText: gettext('No Tags defined'),
- fieldTitle: gettext('Tag'),
- maskRe: PVE.Utils.tagCharRegex,
- gridConfig: {
- height: 200,
- scrollable: true,
- },
- listeners: {
- change: 'tagChange',
- },
- },
- {
- hidden: true,
- xtype: 'displayfield',
- reference: 'hintField',
- userCls: 'pmx-hint',
- },
- ],
- },
- ],
-});
-Ext.define('PVE.dc.RegisteredTagsEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveRegisteredTagEdit',
-
- subject: gettext('Registered Tags'),
- onlineHelp: 'gui_tags',
-
- url: '/api2/extjs/cluster/options',
-
- hintText: gettext('NOTE: The following tags are also defined in the user allow list.'),
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- tagChange: function (field, value) {
- let me = this;
- let view = me.getView();
- let also_allowed = [];
- value = Ext.isArray(value) ? value : value.split(';');
- value.forEach((tag) => {
- if (view.allowed_tags.indexOf(tag) !== -1) {
- also_allowed.push(tag);
- }
- });
- let hint_field = me.lookup('hintField');
- hint_field.setVisible(also_allowed.length > 0);
- if (also_allowed.length > 0) {
- hint_field.setValue(`${view.hintText} ${also_allowed.join(', ')}`);
- }
- },
- },
-
- items: [
- {
- xtype: 'inputpanel',
- setValues: function (values) {
- let allowed_tags = values?.['user-tag-access']?.['user-allow-list'] ?? [];
- this.up('pveRegisteredTagEdit').allowed_tags = allowed_tags;
- let tags = values?.['registered-tags'];
- return Proxmox.panel.InputPanel.prototype.setValues.call(this, { tags });
- },
- onGetValues: function (values) {
- if (!values.tags) {
- return {
- delete: 'registered-tags',
- };
- } else {
- return {
- 'registered-tags': values.tags,
- };
- }
- },
- items: [
- {
- name: 'tags',
- xtype: 'pveListField',
- maskRe: PVE.Utils.tagCharRegex,
- gridConfig: {
- height: 200,
- scrollable: true,
- emptyText: gettext('No Tags defined'),
- },
- listeners: {
- change: 'tagChange',
- },
- },
- {
- hidden: true,
- xtype: 'displayfield',
- reference: 'hintField',
- userCls: 'pmx-hint',
- },
- ],
- },
- ],
-});
-Ext.define('PVE.dc.RealmSyncJobView', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveRealmSyncJobView',
-
- stateful: true,
- stateId: 'grid-realmsyncjobs',
-
- emptyText: Ext.String.format(gettext('No {0} configured'), gettext('Realm Sync Job')),
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- addRealmSyncJob: function (button) {
- let me = this;
- Ext.create(`PVE.dc.RealmSyncJobEdit`, {
- autoShow: true,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
-
- editRealmSyncJob: function () {
- let me = this;
- let view = me.getView();
- let selection = view.getSelection();
- if (!selection || selection.length < 1) {
- return;
- }
-
- Ext.create(`PVE.dc.RealmSyncJobEdit`, {
- jobid: selection[0].data.id,
- autoShow: true,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
-
- runNow: function () {
- let me = this;
- let view = me.getView();
- let selection = view.getSelection();
- if (!selection || selection.length < 1) {
- return;
- }
-
- let params = selection[0].data;
- let realm = params.realm;
-
- let propertiesToDelete = [
- 'comment',
- 'realm',
- 'id',
- 'type',
- 'schedule',
- 'last-run',
- 'next-run',
- 'enabled',
- ];
- for (const prop of propertiesToDelete) {
- delete params[prop];
- }
-
- Proxmox.Utils.API2Request({
- url: `/access/domains/${realm}/sync`,
- params,
- waitMsgTarget: view,
- method: 'POST',
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: function (response, options) {
- Ext.create('Proxmox.window.TaskProgress', {
- autoShow: true,
- upid: response.result.data,
- taskDone: () => {
- me.reload();
- },
- });
- },
- });
- },
-
- reload: function () {
- this.getView().getStore().load();
- },
- },
-
- store: {
- autoLoad: true,
- id: 'realm-syncs',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/jobs/realm-sync',
- },
- },
-
- viewConfig: {
- getRowClass: (record, _index) => (record.get('enabled') ? '' : 'proxmox-disabled-row'),
- },
-
- columns: [
- {
- header: gettext('Enabled'),
- width: 80,
- dataIndex: 'enabled',
- sortable: true,
- align: 'center',
- stopSelection: false,
- renderer: Proxmox.Utils.renderEnabledIcon,
- },
- {
- text: gettext('Name'),
- flex: 1,
- dataIndex: 'id',
- hidden: true,
- },
- {
- text: gettext('Realm'),
- width: 200,
- dataIndex: 'realm',
- },
- {
- header: gettext('Schedule'),
- width: 150,
- dataIndex: 'schedule',
- },
- {
- text: gettext('Next Run'),
- dataIndex: 'next-run',
- width: 150,
- renderer: PVE.Utils.render_next_event,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'comment',
- renderer: Ext.htmlEncode,
- sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''),
- flex: 1,
- },
- ],
-
- tbar: [
- {
- text: gettext('Add'),
- handler: 'addRealmSyncJob',
- },
- {
- text: gettext('Edit'),
- xtype: 'proxmoxButton',
- handler: 'editRealmSyncJob',
- disabled: true,
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- baseurl: `/api2/extjs/cluster/jobs/realm-sync`,
- callback: 'reload',
- },
- {
- xtype: 'proxmoxButton',
- handler: 'runNow',
- disabled: true,
- text: gettext('Run Now'),
- },
- ],
-
- listeners: {
- itemdblclick: 'editRealmSyncJob',
- },
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
-
- Proxmox.Utils.monStoreErrors(me, me.getStore());
- },
-});
-
-Ext.define('PVE.dc.RealmSyncJobEdit', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- subject: gettext('Realm Sync Job'),
- onlineHelp: 'pveum_ldap_sync',
-
- // don't focus the schedule field on edit
- defaultFocus: 'field[name=id]',
-
- cbindData: function () {
- let me = this;
- me.isCreate = !me.jobid;
- me.jobid = me.jobid || '';
- let url = '/api2/extjs/cluster/jobs/realm-sync';
- me.url = me.jobid ? `${url}/${me.jobid}` : url;
- me.method = me.isCreate ? 'POST' : 'PUT';
- if (!me.isCreate) {
- me.subject = `${me.subject}: ${me.jobid}`;
- }
- return {};
- },
-
- submitUrl: function (url, values) {
- return this.isCreate ? `${url}/${values.id}` : url;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- updateDefaults: function (_field, newValue) {
- let me = this;
-
- ['scope', 'enable-new', 'schedule'].forEach((reference) => {
- me.lookup(reference)?.setDisabled(false);
- });
-
- // only update on create
- if (!me.getView().isCreate) {
- return;
- }
- Proxmox.Utils.API2Request({
- url: `/access/domains/${newValue}`,
- success: function (response) {
- // first reset the fields to their default
- ['acl', 'entry', 'properties'].forEach((opt) => {
- me.lookup(`remove-vanished-${opt}`)?.setValue(false);
- });
- me.lookup('enable-new')?.setValue('1');
- me.lookup('scope')?.setValue(undefined);
-
- let options = response?.result?.data?.['sync-defaults-options'];
- if (options) {
- let parsed = PVE.Parser.parsePropertyString(options);
- if (parsed['remove-vanished']) {
- let opts = parsed['remove-vanished'].split(';');
- for (const opt of opts) {
- me.lookup(`remove-vanished-${opt}`)?.setValue(true);
- }
- delete parsed['remove-vanished'];
- }
- for (const [name, value] of Object.entries(parsed)) {
- me.lookup(name)?.setValue(value);
- }
- }
- },
- });
- },
- },
-
- items: [
- {
- xtype: 'inputpanel',
-
- cbind: {
- isCreate: '{isCreate}',
- },
-
- onGetValues: function (values) {
- let me = this;
-
- let vanished_opts = [];
- ['acl', 'entry', 'properties'].forEach((prop) => {
- if (values[`remove-vanished-${prop}`]) {
- vanished_opts.push(prop);
- }
- delete values[`remove-vanished-${prop}`];
- });
-
- if (!values.id && me.isCreate) {
- values.id =
- 'realmsync-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
- }
-
- if (vanished_opts.length > 0) {
- values['remove-vanished'] = vanished_opts.join(';');
- } else {
- values['remove-vanished'] = 'none';
- }
-
- PVE.Utils.delete_if_default(values, 'node', '');
-
- if (me.isCreate) {
- delete values.delete; // on create we cannot delete values
- }
-
- return values;
- },
-
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- editConfig: {
- xtype: 'pmxRealmComboBox',
- storeFilter: (rec) => rec.data.type === 'ldap' || rec.data.type === 'ad',
- },
- listConfig: {
- emptyText: `${gettext('No LDAP/AD Realm found')}
`,
- },
- cbind: {
- editable: '{isCreate}',
- },
- listeners: {
- change: 'updateDefaults',
- },
- fieldLabel: gettext('Realm'),
- name: 'realm',
- reference: 'realm',
- },
- {
- xtype: 'pveCalendarEvent',
- fieldLabel: gettext('Schedule'),
- disabled: true,
- allowBlank: false,
- name: 'schedule',
- reference: 'schedule',
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Enable Job'),
- name: 'enabled',
- reference: 'enabled',
- uncheckedValue: 0,
- defaultValue: 1,
- checked: true,
- },
- ],
-
- column2: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'scope',
- reference: 'scope',
- disabled: true,
- fieldLabel: gettext('Scope'),
- value: '',
- emptyText: gettext('No default available'),
- deleteEmpty: false,
- allowBlank: false,
- comboItems: [
- ['users', gettext('Users')],
- ['groups', gettext('Groups')],
- ['both', gettext('Users and Groups')],
- ],
- },
- {
- xtype: 'proxmoxKVComboBox',
- value: '1',
- deleteEmpty: false,
- disabled: true,
- allowBlank: false,
- comboItems: [
- ['1', Proxmox.Utils.yesText],
- ['0', Proxmox.Utils.noText],
- ],
- name: 'enable-new',
- reference: 'enable-new',
- fieldLabel: gettext('Enable New'),
- },
- ],
-
- columnB: [
- {
- xtype: 'fieldset',
- title: gettext('Remove Vanished Options'),
- items: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('ACL'),
- name: 'remove-vanished-acl',
- reference: 'remove-vanished-acl',
- boxLabel: gettext('Remove ACLs of vanished users and groups.'),
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Entry'),
- name: 'remove-vanished-entry',
- reference: 'remove-vanished-entry',
- boxLabel: gettext('Remove vanished user and group entries.'),
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Properties'),
- name: 'remove-vanished-properties',
- reference: 'remove-vanished-properties',
- boxLabel: gettext('Remove vanished properties from synced users.'),
- },
- ],
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'comment',
- fieldLabel: gettext('Job Comment'),
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Description of the job'),
- },
- },
- {
- xtype: 'displayfield',
- reference: 'defaulthint',
- value: gettext('Default sync options can be set by editing the realm.'),
- userCls: 'pmx-hint',
- hidden: true,
- },
- ],
- },
- ],
-
- initComponent: function () {
- let me = this;
- me.callParent();
- if (me.jobid) {
- me.load({
- success: function (response, options) {
- let values = response.result.data;
-
- if (values['remove-vanished']) {
- let opts = values['remove-vanished'].split(';');
- for (const opt of opts) {
- values[`remove-vanished-${opt}`] = 1;
- }
- }
- me.down('inputpanel').setValues(values);
- },
- });
- }
- },
-});
-Ext.define('pve-resource-pci-tree', {
- extend: 'Ext.data.Model',
- idProperty: 'internalId',
- fields: ['type', 'text', 'path', 'id', 'subsystem-id', 'iommugroup', 'description', 'digest'],
-});
-
-Ext.define('PVE.dc.PCIMapView', {
- extend: 'PVE.tree.ResourceMapTree',
- alias: 'widget.pveDcPCIMapView',
-
- editWindowClass: 'PVE.window.PCIMapEditWindow',
- baseUrl: '/cluster/mapping/pci',
- mapIconCls: 'pve-itype-icon-pci',
- getStatusCheckUrl: (node) => `/nodes/${node}/hardware/pci?pci-class-blacklist=`,
- entryIdProperty: 'path',
-
- checkValidity: function (data, node) {
- let me = this;
- let ids = {};
- data.forEach((entry) => {
- ids[entry.id] = entry;
- });
- // extract the mdev property from the global entry and insert to the individiual entries,
- // so we can reuse the normal checking logic
- let mdev;
- me.getRootNode()?.cascade(function (rec) {
- if (rec.data.type === 'entry') {
- mdev = rec.data.mdev ?? 0;
- }
- if (rec.data.node !== node || rec.data.type !== 'map') {
- return;
- }
- rec.data.mdev = mdev;
-
- let id = rec.data.path;
- if (!id.match(/\.\d$/)) {
- id += '.0';
- }
- let device = ids[id];
- if (!device) {
- rec.set('valid', 0);
- rec.set('errmsg', Ext.String.format(gettext('Cannot find PCI id {0}'), id));
- rec.commit();
- return;
- }
-
- let deviceId = `${device.vendor}:${device.device}`.replace(/0x/g, '');
- let subId = `${device.subsystem_vendor}:${device.subsystem_device}`.replace(/0x/g, '');
-
- let toCheck = {
- id: deviceId,
- 'subsystem-id': subId,
- mdev: device.mdev ?? 0,
- iommugroup: device.iommugroup !== -1 ? device.iommugroup : undefined,
- };
-
- let valid = 1;
- let errors = [];
- let errText = gettext("Configuration for {0} not correct ('{1}' != '{2}')");
- for (const [key, validValue] of Object.entries(toCheck)) {
- if (`${rec.data[key]}` !== `${validValue}`) {
- errors.push(Ext.String.format(errText, key, rec.data[key] ?? '', validValue));
- valid = 0;
- }
- }
-
- rec.set('valid', valid);
- rec.set('errmsg', errors.join(' '));
- rec.commit();
- });
- },
-
- store: {
- sorters: 'text',
- model: 'pve-resource-pci-tree',
- data: {},
- },
-
- columns: [
- {
- xtype: 'treecolumn',
- text: gettext('ID/Node/Path'),
- dataIndex: 'text',
- width: 200,
- },
- {
- text: gettext('Vendor/Device'),
- dataIndex: 'id',
- },
- {
- text: gettext('Subsystem Vendor/Device'),
- dataIndex: 'subsystem-id',
- },
- {
- text: gettext('IOMMU-Group'),
- dataIndex: 'iommugroup',
- },
- {
- header: gettext('Status'),
- dataIndex: 'valid',
- flex: 1,
- renderer: 'renderStatus',
- },
- {
- header: gettext('Comment'),
- dataIndex: 'description',
- renderer: function (value, _meta, record) {
- return Ext.String.htmlEncode(value ?? record.data.comment);
- },
- flex: 1,
- },
- ],
-});
-Ext.define('pve-resource-usb-tree', {
- extend: 'Ext.data.Model',
- idProperty: 'internalId',
- fields: ['type', 'text', 'path', 'id', 'description', 'digest'],
-});
-
-Ext.define('PVE.dc.USBMapView', {
- extend: 'PVE.tree.ResourceMapTree',
- alias: 'widget.pveDcUSBMapView',
-
- editWindowClass: 'PVE.window.USBMapEditWindow',
- baseUrl: '/cluster/mapping/usb',
- mapIconCls: 'fa fa-usb',
- getStatusCheckUrl: (node) => `/nodes/${node}/hardware/usb`,
- entryIdProperty: 'id',
-
- checkValidity: function (data, node) {
- let me = this;
- let ids = {};
- let paths = {};
- data.forEach((entry) => {
- ids[`${entry.vendid}:${entry.prodid}`] = entry;
- paths[`${entry.busnum}-${entry.usbpath}`] = entry;
- });
- me.getRootNode()?.cascade(function (rec) {
- if (rec.data.node !== node || rec.data.type !== 'map') {
- return;
- }
-
- let device;
- if (rec.data.path) {
- device = paths[rec.data.path];
- }
- device ??= ids[rec.data.id];
-
- if (!device) {
- rec.set('valid', 0);
- rec.set(
- 'errmsg',
- Ext.String.format(gettext('Cannot find USB device {0}'), rec.data.id),
- );
- rec.commit();
- return;
- }
-
- let deviceId = `${device.vendid}:${device.prodid}`.replace(/0x/g, '');
-
- let toCheck = {
- id: deviceId,
- };
-
- let valid = 1;
- let errors = [];
- let errText = gettext("Configuration for {0} not correct ('{1}' != '{2}')");
- for (const [key, validValue] of Object.entries(toCheck)) {
- if (rec.data[key] !== validValue) {
- errors.push(Ext.String.format(errText, key, rec.data[key] ?? '', validValue));
- valid = 0;
- }
- }
-
- rec.set('valid', valid);
- rec.set('errmsg', errors.join(' '));
- rec.commit();
- });
- },
-
- store: {
- sorters: 'text',
- model: 'pve-resource-usb-tree',
- data: {},
- },
-
- columns: [
- {
- xtype: 'treecolumn',
- text: gettext('ID/Node/Vendor&Device'),
- dataIndex: 'text',
- width: 200,
- },
- {
- text: gettext('Path'),
- dataIndex: 'path',
- },
- {
- header: gettext('Status'),
- dataIndex: 'valid',
- flex: 1,
- renderer: 'renderStatus',
- },
- {
- header: gettext('Comment'),
- dataIndex: 'description',
- renderer: function (value, _meta, record) {
- return Ext.String.htmlEncode(value ?? record.data.comment);
- },
- flex: 1,
- },
- ],
-});
-Ext.define('pve-resource-dir-tree', {
- extend: 'Ext.data.Model',
- idProperty: 'internalId',
- fields: ['type', 'text', 'path', 'id', 'description', 'digest'],
-});
-
-Ext.define('PVE.dc.DirMapView', {
- extend: 'PVE.tree.ResourceMapTree',
- alias: 'widget.pveDcDirMapView',
-
- editWindowClass: 'PVE.window.DirMapEditWindow',
- baseUrl: '/cluster/mapping/dir',
- mapIconCls: 'fa fa-folder',
- entryIdProperty: 'path',
-
- store: {
- sorters: 'text',
- model: 'pve-resource-dir-tree',
- data: {},
- },
-
- columns: [
- {
- xtype: 'treecolumn',
- text: gettext('ID/Node'),
- dataIndex: 'text',
- width: 200,
- },
- {
- header: gettext('Comment'),
- dataIndex: 'description',
- renderer: function (value, _meta, record) {
- return Ext.String.htmlEncode(value ?? record.data.comment);
- },
- flex: 1,
- },
- ],
-});
-Ext.define('PVE.lxc.CmdMenu', {
- extend: 'Ext.menu.Menu',
-
- showSeparator: false,
- initComponent: function () {
- let me = this;
-
- let info = me.pveSelNode.data;
- if (!info.node) {
- throw 'no node name specified';
- }
- if (!info.vmid) {
- throw 'no CT ID specified';
- }
-
- let vm_command = function (cmd, params) {
- Proxmox.Utils.API2Request({
- params: params,
- url: `/nodes/${info.node}/${info.type}/${info.vmid}/status/${cmd}`,
- method: 'POST',
- failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- };
- let confirmedVMCommand = (cmd, params) => {
- let msg = PVE.Utils.formatGuestTaskConfirmation(`vz${cmd}`, info.vmid, info.name);
- Ext.Msg.confirm(gettext('Confirm'), msg, (btn) => {
- if (btn === 'yes') {
- vm_command(cmd, params);
- }
- });
- };
-
- let caps = Ext.state.Manager.get('GuiCap');
- let standalone = PVE.Utils.isStandaloneNode();
-
- let running = false,
- stopped = true,
- suspended = false;
- switch (info.status) {
- case 'running':
- running = true;
- stopped = false;
- break;
- case 'paused':
- stopped = false;
- suspended = true;
- break;
- default:
- break;
- }
-
- me.title = 'CT ' + info.vmid;
-
- me.items = [
- {
- text: gettext('Start'),
- iconCls: 'fa fa-fw fa-play',
- disabled: running,
- handler: () => vm_command('start'),
- },
- {
- text: gettext('Shutdown'),
- iconCls: 'fa fa-fw fa-power-off',
- disabled: stopped || suspended,
- handler: () => confirmedVMCommand('shutdown'),
- },
- {
- text: gettext('Stop'),
- iconCls: 'fa fa-fw fa-stop',
- disabled: stopped,
- tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
- handler: () => {
- Ext.create('PVE.GuestStop', {
- nodename: info.node,
- vm: info,
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Reboot'),
- iconCls: 'fa fa-fw fa-refresh',
- disabled: stopped,
- tooltip: Ext.String.format(gettext('Reboot {0}'), 'CT'),
- handler: () => confirmedVMCommand('reboot'),
- },
- {
- xtype: 'menuseparator',
- hidden:
- (standalone || !caps.vms['VM.Migrate']) &&
- !caps.vms['VM.Allocate'] &&
- !caps.vms['VM.Clone'],
- },
- {
- text: gettext('Clone'),
- iconCls: 'fa fa-fw fa-clone',
- hidden: !caps.vms['VM.Clone'],
- handler: () =>
- PVE.window.Clone.wrap(info.node, info.vmid, info.name, me.isTemplate, 'lxc'),
- },
- {
- text: gettext('Migrate'),
- iconCls: 'fa fa-fw fa-send-o',
- hidden: standalone || !caps.vms['VM.Migrate'],
- handler: function () {
- Ext.create('PVE.window.Migrate', {
- vmtype: 'lxc',
- nodename: info.node,
- vmid: info.vmid,
- vmname: info.name,
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Convert to template'),
- iconCls: 'fa fa-fw fa-file-o',
- handler: function () {
- let msg = PVE.Utils.formatGuestTaskConfirmation(
- 'vztemplate',
- info.vmid,
- info.name,
- );
- Ext.Msg.confirm(gettext('Confirm'), msg, function (btn) {
- if (btn === 'yes') {
- Proxmox.Utils.API2Request({
- url: `/nodes/${info.node}/lxc/${info.vmid}/template`,
- method: 'POST',
- failure: (response, opts) =>
- Ext.Msg.alert('Error', response.htmlStatus),
- });
- }
- });
- },
- },
- { xtype: 'menuseparator' },
- {
- text: gettext('Take Snapshot'),
- iconCls: 'fa fa-fw fa-history',
- itemId: 'takeSnapshotBtn',
- disabled: true,
- handler: () => {
- Ext.create('PVE.window.Snapshot', {
- nodename: info.node,
- vmid: info.vmid,
- vmname: info.name,
- viewonly: false,
- type: info.type,
- isCreate: true,
- submitText: gettext('Take Snapshot'),
- autoShow: true,
- running: running,
- });
- },
- },
- {
- text: gettext('Backup now'),
- iconCls: 'fa fa-fw fa-floppy-o',
- disabled: !caps.vms['VM.Backup'],
- handler: () => {
- Ext.create('PVE.window.Backup', {
- nodename: info.node,
- vmid: info.vmid,
- vmtype: info.type,
- vmname: info.name,
- autoShow: true,
- });
- },
- },
- { xtype: 'menuseparator' },
- {
- text: gettext('Console'),
- iconCls: 'fa fa-fw fa-terminal',
- handler: () =>
- PVE.Utils.openDefaultConsoleWindow(
- true,
- 'lxc',
- info.vmid,
- info.node,
- info.vmname,
- ),
- },
- ];
-
- me.callParent();
-
- if (caps.vms['VM.Snapshot']) {
- Proxmox.Utils.API2Request({
- url: `/nodes/${info.node}/${info.type}/${info.vmid}/feature`,
- params: { feature: 'snapshot' },
- method: 'GET',
- success: (response) => {
- let hasFeature = response.result.data.hasFeature;
- let btn = me.down('#takeSnapshotBtn');
- if (btn) {
- btn.setDisabled(!hasFeature);
- }
- },
- });
- }
- },
-});
-Ext.define('PVE.lxc.Config', {
- extend: 'PVE.panel.Config',
- alias: 'widget.pveLXCConfig',
-
- onlineHelp: 'chapter_pct',
-
- userCls: 'proxmox-tags-full',
-
- initComponent: function () {
- var me = this;
- var vm = me.pveSelNode.data;
-
- var nodename = vm.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = vm.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- var template = !!vm.template;
-
- var running = !!vm.uptime;
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- var base_url = '/nodes/' + nodename + '/lxc/' + vmid;
-
- me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
- url: '/api2/json' + base_url + '/status/current',
- interval: 1000,
- });
-
- var vm_command = function (cmd, params) {
- Proxmox.Utils.API2Request({
- params: params,
- url: base_url + '/status/' + cmd,
- waitMsgTarget: me,
- method: 'POST',
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- });
- };
-
- var startBtn = Ext.create('Ext.Button', {
- text: gettext('Start'),
- disabled: !caps.vms['VM.PowerMgmt'] || running,
- hidden: template,
- handler: function () {
- vm_command('start');
- },
- iconCls: 'fa fa-play',
- });
-
- var shutdownBtn = Ext.create('PVE.button.Split', {
- text: gettext('Shutdown'),
- disabled: !caps.vms['VM.PowerMgmt'] || !running,
- hidden: template,
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation('vzshutdown', vmid, vm.name),
- handler: function () {
- vm_command('shutdown');
- },
- menu: {
- items: [
- {
- text: gettext('Reboot'),
- disabled: !caps.vms['VM.PowerMgmt'],
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation(
- 'vzreboot',
- vmid,
- vm.name,
- ),
- tooltip: Ext.String.format(gettext('Reboot {0}'), 'CT'),
- handler: function () {
- vm_command('reboot');
- },
- iconCls: 'fa fa-refresh',
- },
- {
- text: gettext('Stop'),
- disabled: !caps.vms['VM.PowerMgmt'],
- tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
- handler: function () {
- Ext.create('PVE.GuestStop', {
- nodename: nodename,
- vm: vm,
- autoShow: true,
- });
- },
- iconCls: 'fa fa-stop',
- },
- ],
- },
- iconCls: 'fa fa-power-off',
- });
-
- var migrateBtn = Ext.create('Ext.Button', {
- text: gettext('Migrate'),
- disabled: !caps.vms['VM.Migrate'],
- hidden: PVE.Utils.isStandaloneNode(),
- handler: function () {
- var win = Ext.create('PVE.window.Migrate', {
- vmtype: 'lxc',
- nodename: nodename,
- vmid: vmid,
- vmname: vm.name,
- });
- win.show();
- },
- iconCls: 'fa fa-send-o',
- });
-
- var moreBtn = Ext.create('Proxmox.button.Button', {
- text: gettext('More'),
- menu: {
- items: [
- {
- text: gettext('Clone'),
- iconCls: 'fa fa-fw fa-clone',
- hidden: !caps.vms['VM.Clone'],
- handler: function () {
- PVE.window.Clone.wrap(nodename, vmid, vm.name, template, 'lxc');
- },
- },
- {
- text: gettext('Convert to template'),
- disabled: template,
- xtype: 'pveMenuItem',
- iconCls: 'fa fa-fw fa-file-o',
- hidden: !caps.vms['VM.Allocate'],
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation(
- 'vztemplate',
- vmid,
- vm.name,
- ),
- handler: function () {
- Proxmox.Utils.API2Request({
- url: base_url + '/template',
- waitMsgTarget: me,
- method: 'POST',
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- });
- },
- },
- {
- iconCls: 'fa fa-heartbeat ',
- hidden: !caps.nodes['Sys.Console'],
- text: gettext('Manage HA'),
- handler: function () {
- var ha = vm.hastate;
- Ext.create('PVE.ha.VMResourceEdit', {
- vmid: vmid,
- guestType: 'ct',
- isCreate: !ha || ha === 'unmanaged',
- }).show();
- },
- },
- {
- text: gettext('Remove'),
- disabled: !caps.vms['VM.Allocate'],
- itemId: 'removeBtn',
- handler: function () {
- Ext.create('PVE.window.SafeDestroyGuest', {
- url: base_url,
- item: {
- type: 'CT',
- id: vmid,
- formattedIdentifier: PVE.Utils.getFormattedGuestIdentifier(
- vmid,
- vm.name,
- ),
- },
- taskName: 'vzdestroy',
- }).show();
- },
- iconCls: 'fa fa-trash-o',
- },
- ],
- },
- });
-
- var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
- disabled: !caps.vms['VM.Console'],
- consoleType: 'lxc',
- consoleName: vm.name,
- hidden: template,
- nodename: nodename,
- vmid: vmid,
- });
-
- var statusTxt = Ext.create('Ext.toolbar.TextItem', {
- data: {
- lock: undefined,
- },
- tpl: ['', ' ({lock})', ' '],
- });
-
- let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
- tags: vm.tags,
- canEdit: !!caps.vms['VM.Config.Options'],
- listeners: {
- change: function (tags) {
- Proxmox.Utils.API2Request({
- url: base_url + '/config',
- method: 'PUT',
- params: {
- tags,
- },
- success: function () {
- me.statusStore.load();
- },
- failure: function (response) {
- Ext.Msg.alert('Error', response.htmlStatus);
- me.statusStore.load();
- },
- });
- },
- },
- });
-
- let vm_text = `${vm.vmid} (${vm.name})`;
-
- Ext.apply(me, {
- title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm_text, nodename),
- hstateid: 'lxctab',
- tbarSpacing: false,
- tbar: [
- statusTxt,
- tagsContainer,
- '->',
- startBtn,
- shutdownBtn,
- migrateBtn,
- consoleBtn,
- moreBtn,
- ],
- defaults: { statusStore: me.statusStore },
- items: [
- {
- title: gettext('Summary'),
- xtype: 'pveGuestSummary',
- iconCls: 'fa fa-book',
- itemId: 'summary',
- },
- ],
- });
-
- if (caps.vms['VM.Console'] && !template) {
- me.items.push({
- title: gettext('Console'),
- itemId: 'consolejs',
- iconCls: 'fa fa-terminal',
- xtype: 'pveNoVncConsole',
- vmid: vmid,
- consoleType: 'lxc',
- xtermjs: true,
- nodename: nodename,
- });
- }
-
- me.items.push(
- {
- title: gettext('Resources'),
- itemId: 'resources',
- expandedOnInit: true,
- iconCls: 'fa fa-cube',
- xtype: 'pveLxcRessourceView',
- },
- {
- title: gettext('Network'),
- iconCls: 'fa fa-exchange',
- itemId: 'network',
- xtype: 'pveLxcNetworkView',
- },
- {
- title: gettext('DNS'),
- iconCls: 'fa fa-globe',
- itemId: 'dns',
- xtype: 'pveLxcDNS',
- },
- {
- title: gettext('Options'),
- itemId: 'options',
- iconCls: 'fa fa-gear',
- xtype: 'pveLxcOptions',
- },
- {
- title: gettext('Task History'),
- itemId: 'tasks',
- iconCls: 'fa fa-list-alt',
- xtype: 'proxmoxNodeTasks',
- nodename: nodename,
- preFilter: {
- vmid,
- },
- },
- );
-
- if (caps.vms['VM.Backup']) {
- me.items.push(
- {
- title: gettext('Backup'),
- iconCls: 'fa fa-floppy-o',
- xtype: 'pveBackupView',
- itemId: 'backup',
- },
- {
- title: gettext('Replication'),
- iconCls: 'fa fa-retweet',
- xtype: 'pveReplicaView',
- itemId: 'replication',
- },
- );
- }
-
- if (
- (caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback'] || caps.vms['VM.Audit']) &&
- !template
- ) {
- me.items.push({
- title: gettext('Snapshots'),
- iconCls: 'fa fa-history',
- xtype: 'pveGuestSnapshotTree',
- type: 'lxc',
- itemId: 'snapshot',
- });
- }
-
- if (caps.vms['VM.Audit']) {
- me.items.push(
- {
- xtype: 'pveFirewallRules',
- title: gettext('Firewall'),
- iconCls: 'fa fa-shield',
- allow_iface: true,
- base_url: base_url + '/firewall/rules',
- list_refs_url: base_url + '/firewall/refs',
- itemId: 'firewall',
- firewall_type: 'vm',
- },
- {
- xtype: 'pveFirewallOptions',
- groups: ['firewall'],
- iconCls: 'fa fa-gear',
- onlineHelp: 'pve_firewall_vm_container_configuration',
- title: gettext('Options'),
- base_url: base_url + '/firewall/options',
- fwtype: 'vm',
- itemId: 'firewall-options',
- },
- {
- xtype: 'pveFirewallAliases',
- title: gettext('Alias'),
- groups: ['firewall'],
- iconCls: 'fa fa-external-link',
- base_url: base_url + '/firewall/aliases',
- itemId: 'firewall-aliases',
- },
- {
- xtype: 'pveIPSet',
- title: gettext('IPSet'),
- groups: ['firewall'],
- iconCls: 'fa fa-list-ol',
- base_url: base_url + '/firewall/ipset',
- list_refs_url: base_url + '/firewall/refs',
- itemId: 'firewall-ipset',
- },
- );
- }
-
- if (caps.vms['VM.Console']) {
- me.items.push({
- title: gettext('Log'),
- groups: ['firewall'],
- iconCls: 'fa fa-list',
- onlineHelp: 'chapter_pve_firewall',
- itemId: 'firewall-fwlog',
- xtype: 'proxmoxLogView',
- url: '/api2/extjs' + base_url + '/firewall/log',
- log_select_timespan: true,
- submitFormat: 'U',
- });
- }
-
- if (caps.vms['Permissions.Modify']) {
- me.items.push({
- xtype: 'pveACLView',
- title: gettext('Permissions'),
- itemId: 'permissions',
- iconCls: 'fa fa-unlock',
- path: '/vms/' + vmid,
- });
- }
-
- me.callParent();
-
- var prevStatus = 'unknown';
- me.mon(me.statusStore, 'load', function (s, records, success) {
- var status;
- var lock;
- var rec;
-
- if (!success) {
- status = 'unknown';
- } else {
- rec = s.data.get('status');
- status = rec ? rec.data.value : 'unknown';
- rec = s.data.get('template');
- template = rec ? rec.data.value : false;
- rec = s.data.get('lock');
- lock = rec ? rec.data.value : undefined;
- }
-
- statusTxt.update({ lock: lock });
-
- rec = s.data.get('tags');
- tagsContainer.loadTags(rec?.data?.value);
-
- startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template);
- shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
- me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
- consoleBtn.setDisabled(template);
-
- if (prevStatus === 'stopped' && status === 'running') {
- let con = me.down('#consolejs');
- if (con) {
- con.reload();
- }
- }
-
- prevStatus = status;
- });
-
- me.on('afterrender', function () {
- me.statusStore.startUpdate();
- });
-
- me.on('destroy', function () {
- me.statusStore.stopUpdate();
- });
- },
-});
-Ext.define('PVE.lxc.CreateWizard', {
- extend: 'PVE.window.Wizard',
- mixins: ['Proxmox.Mixin.CBind'],
-
- viewModel: {
- data: {
- nodename: '',
- storage: '',
- unprivileged: true,
- },
- formulas: {
- cgroupMode: function (get) {
- const nodeInfo = PVE.data.ResourceStore.getNodes().find(
- (node) => node.node === get('nodename'),
- );
- return nodeInfo ? nodeInfo['cgroup-mode'] : 2;
- },
- },
- },
-
- cbindData: {
- nodename: undefined,
- },
-
- subject: gettext('LXC Container'),
-
- items: [
- {
- xtype: 'inputpanel',
- title: gettext('General'),
- onlineHelp: 'pct_general',
- column1: [
- {
- xtype: 'pveNodeSelector',
- name: 'nodename',
- cbind: {
- selectCurNode: '{!nodename}',
- preferredValue: '{nodename}',
- },
- bind: {
- value: '{nodename}',
- },
- fieldLabel: gettext('Node'),
- allowBlank: false,
- onlineValidator: true,
- },
- {
- xtype: 'pveGuestIDSelector',
- name: 'vmid', // backend only knows vmid
- guestType: 'lxc',
- value: '',
- loadNextFreeID: true,
- validateExists: false,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'hostname',
- vtype: 'DnsName',
- value: '',
- fieldLabel: gettext('Hostname'),
- skipEmptyText: true,
- allowBlank: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'unprivileged',
- value: true,
- uncheckedValue: 0,
- bind: {
- value: '{unprivileged}',
- },
- fieldLabel: gettext('Unprivileged container'),
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'features',
- inputValue: 'nesting=1',
- value: true,
- clearOnDisable: true,
- bind: {
- disabled: '{!unprivileged}',
- },
- fieldLabel: gettext('Nesting'),
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'ha-managed',
- // only submit value of checkbox if checked
- uncheckedValue: undefined,
- fieldLabel: gettext('Add to HA'),
- },
- ],
- column2: [
- {
- xtype: 'pvePoolSelector',
- fieldLabel: gettext('Resource Pool'),
- name: 'pool',
- value: '',
- allowBlank: true,
- },
- {
- xtype: 'textfield',
- inputType: 'password',
- name: 'password',
- value: '',
- fieldLabel: gettext('Password'),
- allowBlank: false,
- minLength: 5,
- change: function (f, value) {
- if (f.rendered) {
- f.up().down('field[name=confirmpw]').validate();
- }
- },
- },
- {
- xtype: 'textfield',
- inputType: 'password',
- name: 'confirmpw',
- value: '',
- fieldLabel: gettext('Confirm password'),
- allowBlank: true,
- submitValue: false,
- validator: function (value) {
- var pw = this.up().down('field[name=password]').getValue();
- if (pw !== value) {
- return 'Passwords do not match!';
- }
- return true;
- },
- },
- {
- xtype: 'textarea',
- name: 'ssh-public-keys',
- value: '',
- fieldLabel: gettext('SSH public key(s)'),
- allowBlank: true,
- validator: function (value) {
- let pwfield = this.up().down('field[name=password]');
- if (value.length) {
- let keys = value.indexOf('\n') !== -1 ? value.split('\n') : [value];
- if (keys.some((key) => key !== '' && !PVE.Parser.parseSSHKey(key))) {
- return 'Failed to recognize ssh key';
- }
- pwfield.allowBlank = true;
- } else {
- pwfield.allowBlank = false;
- }
- pwfield.validate();
- return true;
- },
- afterRender: function () {
- if (!window.FileReader) {
- return; // No FileReader support in this browser
- }
- let cancelEvent = (ev) => {
- ev = ev.event;
- if (ev.preventDefault) {
- ev.preventDefault();
- }
- };
- this.inputEl.on('dragover', cancelEvent);
- this.inputEl.on('dragenter', cancelEvent);
- this.inputEl.on('drop', (ev) => {
- cancelEvent(ev);
- let files = ev.event.dataTransfer.files;
- PVE.Utils.loadSSHKeyFromFile(files[0], (v) => this.setValue(v));
- });
- },
- },
- {
- xtype: 'pveMultiFileButton',
- name: 'file',
- hidden: !window.FileReader,
- text: gettext('Load SSH Key File'),
- listeners: {
- change: function (btn, e, value) {
- e = e.event;
- let field = this.up().down('textarea[name=ssh-public-keys]');
- for (const file of e?.target?.files ?? []) {
- PVE.Utils.loadSSHKeyFromFile(file, (v) => {
- let oldValue = field.getValue();
- field.setValue(
- oldValue ? `${oldValue}\n${v.trim()}` : v.trim(),
- );
- });
- }
- btn.reset();
- },
- },
- },
- ],
- advancedColumnB: [
- {
- xtype: 'pveTagFieldSet',
- name: 'tags',
- maxHeight: 150,
- },
- ],
- },
- {
- xtype: 'inputpanel',
- title: gettext('Template'),
- onlineHelp: 'pct_container_images',
- column1: [
- {
- xtype: 'pveStorageSelector',
- name: 'tmplstorage',
- fieldLabel: gettext('Storage'),
- storageContent: 'vztmpl',
- autoSelect: true,
- allowBlank: false,
- bind: {
- value: '{storage}',
- nodename: '{nodename}',
- },
- },
- {
- xtype: 'pveFileSelector',
- name: 'ostemplate',
- storageContent: 'vztmpl',
- fieldLabel: gettext('Template'),
- bind: {
- storage: '{storage}',
- nodename: '{nodename}',
- },
- allowBlank: false,
- },
- ],
- },
- {
- xtype: 'pveMultiMPPanel',
- title: gettext('Disks'),
- insideWizard: true,
- isCreate: true,
- unused: false,
- confid: 'rootfs',
- },
- {
- xtype: 'pveLxcCPUInputPanel',
- title: gettext('CPU'),
- insideWizard: true,
- },
- {
- xtype: 'pveLxcMemoryInputPanel',
- title: gettext('Memory'),
- insideWizard: true,
- },
- {
- xtype: 'pveLxcNetworkInputPanel',
- title: gettext('Network'),
- insideWizard: true,
- bind: {
- nodename: '{nodename}',
- },
- isCreate: true,
- },
- {
- xtype: 'pveLxcDNSInputPanel',
- title: gettext('DNS'),
- insideWizard: true,
- },
- {
- title: gettext('Confirm'),
- layout: 'fit',
- items: [
- {
- xtype: 'grid',
- store: {
- model: 'KeyValue',
- sorters: [
- {
- property: 'key',
- direction: 'ASC',
- },
- ],
- },
- columns: [
- { header: 'Key', width: 150, dataIndex: 'key' },
- { header: 'Value', flex: 1, dataIndex: 'value', renderer: Ext.htmlEncode },
- ],
- },
- ],
- dockedItems: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'start',
- dock: 'bottom',
- margin: '5 0 0 0',
- boxLabel: gettext('Start after created'),
- },
- ],
- listeners: {
- show: function (panel) {
- let wizard = this.up('window');
- let kv = wizard.getValues();
- let data = [];
- Ext.Object.each(kv, function (key, value) {
- if (key === 'delete' || key === 'tmplstorage') {
- // ignore
- return;
- }
- if (key === 'password') {
- // don't show pw
- return;
- }
- data.push({ key: key, value: value });
- });
-
- let summaryStore = panel.down('grid').getStore();
- summaryStore.suspendEvents();
- summaryStore.removeAll();
- summaryStore.add(data);
- summaryStore.sort();
- summaryStore.resumeEvents();
- summaryStore.fireEvent('refresh');
- },
- },
- onSubmit: function () {
- let wizard = this.up('window');
- let kv = wizard.getValues();
- delete kv.delete;
-
- let nodename = kv.nodename;
- delete kv.nodename;
- delete kv.tmplstorage;
-
- if (!kv.pool.length) {
- delete kv.pool;
- }
- if (!kv.password.length && kv['ssh-public-keys']) {
- delete kv.password;
- }
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/lxc`,
- waitMsgTarget: wizard,
- method: 'POST',
- params: kv,
- success: function (response, opts) {
- Ext.create('Proxmox.window.TaskViewer', {
- autoShow: true,
- upid: response.result.data,
- });
- wizard.close();
- },
- failure: (response, opts) =>
- Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- },
- },
- ],
-});
-Ext.define('PVE.lxc.DeviceInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- autoComplete: false,
-
- controller: {
- xclass: 'Ext.app.ViewController',
- },
-
- setVMConfig: function (vmconfig) {
- let me = this;
- me.vmconfig = vmconfig;
-
- if (me.isCreate) {
- PVE.Utils.forEachLxcDev((i, name) => {
- if (!Ext.isDefined(vmconfig[name])) {
- me.confid = name;
- me.down('field[name=devid]').setValue(i);
- return false;
- }
- return undefined;
- });
- }
- },
-
- onGetValues: function (values) {
- let me = this;
- let confid = me.isCreate ? 'dev' + values.devid : me.confid;
- delete values.devid;
- let val = PVE.Parser.printPropertyString(values, 'path');
- let ret = {};
- ret[confid] = val;
- return ret;
- },
-
- items: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'devid',
- minValue: 0,
- maxValue: PVE.Utils.lxc_dev_count - 1,
- hidden: true,
- allowBlank: false,
- disabled: true,
- cbind: {
- disabled: '{!isCreate}',
- },
- },
- {
- xtype: 'textfield',
- name: 'path',
- fieldLabel: gettext('Device Path'),
- labelWidth: 120,
- editable: true,
- allowBlank: false,
- emptyText: '/dev/xyz',
- validator: (v) =>
- v.startsWith('/dev/') ? true : gettext('Path has to start with /dev/'),
- },
- ],
-
- advancedColumn1: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'uid',
- editable: true,
- fieldLabel: Ext.String.format(gettext('{0} in CT'), 'UID'),
- labelWidth: 120,
- emptyText: '0',
- minValue: 0,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'gid',
- editable: true,
- fieldLabel: Ext.String.format(gettext('{0} in CT'), 'GID'),
- labelWidth: 120,
- emptyText: '0',
- minValue: 0,
- },
- ],
-
- advancedColumn2: [
- {
- xtype: 'textfield',
- name: 'mode',
- editable: true,
- fieldLabel: Ext.String.format(gettext('Access Mode in CT')),
- labelWidth: 120,
- emptyText: '0660',
- validator: function (value) {
- if (/^0[0-7]{3}$|^$/i.test(value)) {
- return true;
- }
- return gettext('Access mode has to be an octal number');
- },
- },
- {
- xtype: 'checkbox',
- name: 'deny-write',
- fieldLabel: gettext('Read only'),
- labelWidth: 120,
- checked: false,
- },
- ],
-});
-
-Ext.define('PVE.lxc.DeviceEdit', {
- extend: 'Proxmox.window.Edit',
-
- vmconfig: undefined,
-
- isAdd: true,
- width: 450,
-
- initComponent: function () {
- let me = this;
-
- me.isCreate = !me.confid;
-
- let ipanel = Ext.create('PVE.lxc.DeviceInputPanel', {
- confid: me.confid,
- isCreate: me.isCreate,
- pveSelNode: me.pveSelNode,
- });
-
- let subject;
- if (me.isCreate) {
- subject = gettext('Device');
- } else {
- subject = gettext('Device') + ' (' + me.confid + ')';
- }
-
- Ext.apply(me, {
- subject: subject,
- items: [ipanel],
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- ipanel.setVMConfig(response.result.data);
- if (me.isCreate) {
- return;
- }
-
- let data = PVE.Parser.parsePropertyString(response.result.data[me.confid], 'path');
-
- let values = {
- path: data.path,
- mode: data.mode,
- uid: data.uid,
- gid: data.gid,
- 'deny-write': data['deny-write'],
- };
-
- ipanel.setValues(values);
- },
- });
- },
-});
-Ext.define('PVE.lxc.DNSInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveLxcDNSInputPanel',
-
- insideWizard: false,
-
- onGetValues: function (values) {
- var me = this;
-
- var deletes = [];
- if (!values.searchdomain && !me.insideWizard) {
- deletes.push('searchdomain');
- }
-
- if (values.nameserver) {
- let list = values.nameserver.split(/[ ,;]+/);
- values.nameserver = list.join(' ');
- } else if (!me.insideWizard) {
- deletes.push('nameserver');
- }
-
- if (deletes.length) {
- values.delete = deletes.join(',');
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- var items = [
- {
- xtype: 'proxmoxtextfield',
- name: 'searchdomain',
- skipEmptyText: true,
- fieldLabel: gettext('DNS domain'),
- emptyText: gettext('use host settings'),
- allowBlank: true,
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('DNS servers'),
- vtype: 'IP64AddressWithSuffixList',
- allowBlank: true,
- emptyText: gettext('use host settings'),
- name: 'nameserver',
- itemId: 'nameserver',
- },
- ];
-
- if (me.insideWizard) {
- me.column1 = items;
- } else {
- me.items = items;
- }
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.lxc.DNSEdit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- var ipanel = Ext.create('PVE.lxc.DNSInputPanel');
-
- Ext.apply(me, {
- subject: gettext('Resources'),
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
-
- if (values.nameserver) {
- values.nameserver.replace(/[,;]/, ' ');
- values.nameserver.replace(/^\s+/, '');
- }
-
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-
-Ext.define('PVE.lxc.DNS', {
- extend: 'Proxmox.grid.PendingObjectGrid',
- alias: ['widget.pveLxcDNS'],
-
- onlineHelp: 'pct_container_network',
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- var rows = {
- hostname: {
- required: true,
- defaultValue: me.pveSelNode.data.name,
- header: gettext('Hostname'),
- editor: caps.vms['VM.Config.Network']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Hostname'),
- items: {
- xtype: 'inputpanel',
- items: {
- fieldLabel: gettext('Hostname'),
- xtype: 'textfield',
- name: 'hostname',
- vtype: 'DnsName',
- allowBlank: true,
- emptyText: 'CT' + vmid.toString(),
- },
- onGetValues: function (values) {
- var params = values;
- if (
- values.hostname === undefined ||
- values.hostname === null ||
- values.hostname === ''
- ) {
- params = { hostname: 'CT' + vmid.toString() };
- }
- return params;
- },
- },
- }
- : undefined,
- },
- searchdomain: {
- header: gettext('DNS domain'),
- defaultValue: '',
- editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
- renderer: function (value) {
- return value || gettext('use host settings');
- },
- },
- nameserver: {
- header: gettext('DNS server'),
- defaultValue: '',
- editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
- renderer: function (value) {
- return value || gettext('use host settings');
- },
- },
- };
-
- var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
-
- var reload = function () {
- me.rstore.load();
- };
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var run_editor = function () {
- var rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
-
- var rowdef = rows[rec.data.key];
- if (!rowdef.editor) {
- return;
- }
-
- var win;
- if (Ext.isString(rowdef.editor)) {
- win = Ext.create(rowdef.editor, {
- pveSelNode: me.pveSelNode,
- confid: rec.data.key,
- url: '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config',
- });
- } else {
- let config = Ext.apply(
- {
- pveSelNode: me.pveSelNode,
- confid: rec.data.key,
- url: '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config',
- },
- rowdef.editor,
- );
- win = Ext.createWidget(rowdef.editor.xtype, config);
- win.load();
- }
- //win.load();
- win.show();
- win.on('destroy', reload);
- };
-
- var edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- enableFn: function (rec) {
- var rowdef = rows[rec.data.key];
- return !!rowdef.editor;
- },
- handler: run_editor,
- });
-
- var revert_btn = new PVE.button.PendingRevert();
-
- var set_button_status = function () {
- let button_sm = me.getSelectionModel();
- let rec = button_sm.getSelection()[0];
-
- if (!rec) {
- edit_btn.disable();
- return;
- }
- let key = rec.data.key;
-
- let rowdef = rows[key];
- edit_btn.setDisabled(!rowdef.editor);
-
- let pending = rec.data.delete || me.hasPendingChanges(key);
- revert_btn.setDisabled(!pending);
- };
-
- Ext.apply(me, {
- url: '/api2/json/nodes/' + nodename + '/lxc/' + vmid + '/pending',
- selModel: sm,
- cwidth1: 150,
- interval: 5000,
- run_editor: run_editor,
- tbar: [edit_btn, revert_btn],
- rows: rows,
- editorConfig: {
- url: '/api2/extjs/' + baseurl,
- },
- listeners: {
- itemdblclick: run_editor,
- selectionchange: set_button_status,
- activate: reload,
- },
- });
-
- me.callParent();
-
- me.on('activate', me.rstore.startUpdate);
- me.on('destroy', me.rstore.stopUpdate);
- me.on('deactivate', me.rstore.stopUpdate);
-
- me.mon(me.getStore(), 'datachanged', function () {
- set_button_status();
- });
- },
-});
-Ext.define('PVE.lxc.FeaturesInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveLxcFeaturesInputPanel',
- onlineHelp: 'pct_options',
-
- // used to save the mounts fstypes until sending
- mounts: [],
-
- fstypes: ['nfs', 'cifs'],
-
- viewModel: {
- parent: null,
- data: {
- unprivileged: false,
- },
- formulas: {
- privilegedOnly: function (get) {
- return get('unprivileged') ? gettext('privileged only') : '';
- },
- unprivilegedOnly: function (get) {
- return !get('unprivileged') ? gettext('unprivileged only') : '';
- },
- },
- },
-
- items: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('keyctl'),
- name: 'keyctl',
- bind: {
- disabled: '{!unprivileged}',
- boxLabel: '{unprivilegedOnly}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Nesting'),
- name: 'nesting',
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'nfs',
- fieldLabel: 'NFS',
- bind: {
- disabled: '{unprivileged}',
- boxLabel: '{privilegedOnly}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'cifs',
- fieldLabel: 'SMB/CIFS',
- bind: {
- disabled: '{unprivileged}',
- boxLabel: '{privilegedOnly}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'fuse',
- fieldLabel: 'FUSE',
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'mknod',
- fieldLabel: gettext('Create Device Nodes'),
- boxLabel: gettext('Experimental'),
- },
- ],
-
- onGetValues: function (values) {
- var me = this;
- var mounts = me.mounts;
- me.fstypes.forEach(function (fs) {
- if (values[fs]) {
- mounts.push(fs);
- }
- delete values[fs];
- });
-
- if (mounts.length) {
- values.mount = mounts.join(';');
- }
-
- var featuresstring = PVE.Parser.printPropertyString(values, undefined);
- if (featuresstring === '') {
- return { delete: 'features' };
- }
- return { features: featuresstring };
- },
-
- setValues: function (values) {
- var me = this;
-
- me.viewModel.set('unprivileged', values.unprivileged);
-
- if (values.features) {
- let res = PVE.Parser.parsePropertyString(values.features);
- me.mounts = [];
- if (res.mount) {
- res.mount.split(/[; ]/).forEach(function (item) {
- if (me.fstypes.indexOf(item) === -1) {
- me.mounts.push(item);
- } else {
- res[item] = 1;
- }
- });
- }
- this.callParent([res]);
- }
- },
-
- initComponent: function () {
- let me = this;
- me.mounts = []; // reset state
- me.callParent();
- },
-});
-
-Ext.define('PVE.lxc.FeaturesEdit', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveLxcFeaturesEdit',
-
- subject: gettext('Features'),
- autoLoad: true,
- width: 350,
-
- items: [
- {
- xtype: 'pveLxcFeaturesInputPanel',
- },
- ],
-});
-Ext.define('PVE.lxc.EnvVariableField', {
- extend: 'Ext.form.FieldContainer',
- mixins: {
- field: 'Ext.form.field.Field',
- },
- xtype: 'pveLxcEnvVariableField',
-
- name: 'variable',
-
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
-
- config: {
- value: null,
- },
-
- // called when X icon-button is clicked, with this field as argument.
- onRemove: Ext.emptyFn,
-
- setValue: function (nameValue) {
- let me = this;
- let viewModel = me.getViewModel();
-
- me.value = nameValue;
- let [name, value] = nameValue?.split('=') ?? ['', ''];
-
- viewModel.set('value', value);
- viewModel.set('name', name);
-
- // TODO: sub-fields might not be available when this is called, so we cannot just set the
- // value on field directly or call resetOriginalValue for correct reset orig. val. behavior
- me.resetOriginalValue();
- },
-
- getValue: function () {
- let viewModel = this.getViewModel();
- let name = viewModel.get('name');
- let value = viewModel.get('value') ?? '';
-
- return name?.length ? `${name}=${value}` : '';
- },
-
- viewModel: {
- parent: null,
- data: {
- name: '',
- value: '',
- },
- formulas: {
- valueEmpty: (get) => !get('value')?.length,
- },
- },
-
- defaultType: 'textfield',
- items: [
- {
- xtype: 'proxmoxtextfield',
- emptyText: gettext('Name'),
- bind: {
- allowBlank: '{valueEmpty}',
- value: '{name}',
- },
- submitValue: false,
- flex: 2,
- },
- {
- xtype: 'box',
- html: '=',
- padding: '0 5',
- },
- {
- xtype: 'proxmoxtextfield',
- emptyText: gettext('Value'),
- bind: {
- value: '{value}',
- },
- submitValue: false,
- flex: 3,
- },
- {
- xtype: 'button',
- cls: 'x-btn-default-toolbar-small proxmox-inline-button',
- iconCls: 'x-btn-icon-el-default-toolbar-small fa fa-trash-o',
- handler: function (button, event) {
- let field = button.up('pveLxcEnvVariableField');
- field.onRemove.call(field, field);
- },
- },
- ],
-});
-
-Ext.define('PVE.lxc.EnvInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveLxcEnvInputPanel',
- onlineHelp: 'pct_options',
-
- onGetValues: function (formValues) {
- let variables = formValues?.variable;
- if (typeof variables === 'string') {
- variables = [variables];
- }
- variables = variables?.filter((v) => typeof v === 'string' && v.length);
-
- let submitValues = {};
- if (variables?.length) {
- submitValues.env = variables.join('\0');
- } else {
- submitValues.delete = 'env';
- }
-
- return submitValues;
- },
-
- items: [
- {
- xtype: 'fieldcontainer',
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
- defaults: {
- padding: '0 4',
- },
- items: [
- {
- xtype: 'displayfield',
- flex: 2,
- value: gettext('Name'),
- },
- {
- xtype: 'box',
- html: ' ',
- },
- {
- xtype: 'displayfield',
- flex: 3,
- value: gettext('Value'),
- },
- ],
- },
- {
- xtype: 'container',
- name: 'variableContainer',
- layout: 'anchor',
- items: [],
- },
- {
- xtype: 'fieldcontainer',
- layout: {
- type: 'hbox',
- align: 'start',
- },
- items: {
- xtype: 'button',
- text: gettext('Add Variable'),
- handler: function (button, event) {
- let variableContainer = button
- .up('pveLxcEnvInputPanel')
- .down('container[name=variableContainer]');
-
- variableContainer.add(
- Ext.create({
- xtype: 'pveLxcEnvVariableField',
- onRemove: (field) => variableContainer.remove(field),
- }),
- );
- },
- },
- },
- ],
-
- setValues: function (values) {
- let me = this;
-
- me.env = values; // TODO: needed?
-
- let variableContainer = me.down('container[name=variableContainer]');
-
- values.env?.split(/\0+/).forEach((value) => {
- variableContainer.add(
- Ext.create({
- xtype: 'pveLxcEnvVariableField',
- onRemove: (field) => variableContainer.remove(field),
- value,
- }),
- );
- });
- },
-
- initComponent: function () {
- let me = this;
- me.mounts = []; // reset state
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.lxc.EnvEdit', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveLxcEnvEdit',
-
- subject: gettext('Environment'),
- autoLoad: true,
- width: 720,
-
- showReset: false, // TODO: fix reset handling for EnvVar inputpanel/fields.
-
- items: [
- {
- xtype: 'pveLxcEnvInputPanel',
- },
- ],
-});
-Ext.define('PVE.lxc.MountPointInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveLxcMountPointInputPanel',
-
- onlineHelp: 'pct_container_storage',
-
- insideWizard: false,
-
- unused: false, // add unused disk imaged
- unprivileged: false,
-
- vmconfig: {}, // used to select unused disks
-
- setUnprivileged: function (unprivileged) {
- var me = this;
- var vm = me.getViewModel();
- me.unprivileged = unprivileged;
- vm.set('unpriv', unprivileged);
- },
-
- onGetValues: function (values) {
- var me = this;
-
- var confid = me.confid || 'mp' + values.mpid;
- me.mp.file = me.down('field[name=file]').getValue();
-
- if (me.unused) {
- confid = 'mp' + values.mpid;
- } else if (me.isCreate) {
- me.mp.file = values.hdstorage + ':' + values.disksize;
- }
-
- // delete unnecessary fields
- delete values.mpid;
- delete values.hdstorage;
- delete values.disksize;
- delete values.diskformat;
-
- let setMPOpt = (k, src, v) => PVE.Utils.propertyStringSet(me.mp, src, k, v);
-
- setMPOpt('mp', values.mp);
- let mountOpts = (values.mountoptions || []).join(';');
- setMPOpt('mountoptions', values.mountoptions, mountOpts);
- setMPOpt('mp', values.mp);
- setMPOpt('backup', values.backup);
- setMPOpt('quota', values.quota);
- setMPOpt('ro', values.ro);
- setMPOpt('acl', values.acl);
- setMPOpt('replicate', values.replicate);
-
- let res = {};
- res[confid] = PVE.Parser.printLxcMountPoint(me.mp);
- return res;
- },
-
- setMountPoint: function (mp) {
- let me = this;
- let vm = me.getViewModel();
- vm.set('mptype', mp.type);
- if (mp.mountoptions) {
- mp.mountoptions = mp.mountoptions.split(';');
- }
- me.mp = mp;
- me.filterMountOptions();
- me.setValues(mp);
- },
-
- filterMountOptions: function () {
- let me = this;
- if (me.confid === 'rootfs') {
- let field = me.down('field[name=mountoptions]');
- let exclude = ['nodev', 'noexec'];
- let filtered = field.comboItems.filter((v) => !exclude.includes(v[0]));
- field.setComboItems(filtered);
- }
- },
-
- updateVMConfig: function (vmconfig) {
- let me = this;
- let vm = me.getViewModel();
- me.vmconfig = vmconfig;
- vm.set('unpriv', vmconfig.unprivileged);
- me.down('field[name=mpid]').validate();
- },
-
- setVMConfig: function (vmconfig) {
- let me = this;
-
- me.updateVMConfig(vmconfig);
- PVE.Utils.forEachLxcMP((bus, i, name) => {
- if (!Ext.isDefined(vmconfig[name])) {
- me.down('field[name=mpid]').setValue(i);
- return false;
- }
- return undefined;
- });
- },
-
- setNodename: function (nodename) {
- let me = this;
- let vm = me.getViewModel();
- vm.set('node', nodename);
- me.down('#diskstorage').setNodename(nodename);
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- control: {
- 'field[name=mpid]': {
- change: function (field, value) {
- let _me = this;
- let view = this.getView();
- if (view.confid !== 'rootfs') {
- view.fireEvent('diskidchange', view, `mp${value}`);
- }
- field.validate();
- },
- },
- '#hdstorage': {
- change: function (field, newValue) {
- let me = this;
- if (!newValue) {
- return;
- }
-
- let rec = field.store.getById(newValue);
- if (!rec) {
- return;
- }
- me.getViewModel().set('type', rec.data.type);
- },
- },
- },
- init: function (view) {
- let _me = this;
- let vm = this.getViewModel();
- view.mp = {};
- vm.set('confid', view.confid);
- vm.set('unused', view.unused);
- vm.set('node', view.nodename);
- vm.set('unpriv', view.unprivileged);
- vm.set('hideStorSelector', view.unused || !view.isCreate);
-
- if (view.isCreate) {
- // can be array if created from unused disk
- vm.set('isIncludedInBackup', true);
- if (view.insideWizard) {
- view.filterMountOptions();
- }
- }
- if (view.selectFree) {
- view.setVMConfig(view.vmconfig);
- }
- },
- },
-
- viewModel: {
- data: {
- unpriv: false,
- unused: false,
- showStorageSelector: false,
- mptype: '',
- type: '',
- confid: '',
- node: '',
- },
-
- formulas: {
- quota: function (get) {
- return !(
- get('type') === 'zfs' ||
- get('type') === 'zfspool' ||
- get('unpriv') ||
- get('isBind')
- );
- },
- hasMP: function (get) {
- return !!get('confid') && !get('unused');
- },
- isRoot: function (get) {
- return get('confid') === 'rootfs';
- },
- isBind: function (get) {
- return get('mptype') === 'bind';
- },
- isBindOrRoot: function (get) {
- return get('isBind') || get('isRoot');
- },
- },
- },
-
- column1: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'mpid',
- fieldLabel: gettext('Mount Point ID'),
- minValue: 0,
- maxValue: PVE.Utils.lxc_mp_counts.mp - 1,
- hidden: true,
- allowBlank: false,
- disabled: true,
- bind: {
- hidden: '{hasMP}',
- disabled: '{hasMP}',
- },
- validator: function (value) {
- let view = this.up('inputpanel');
- if (!view.rendered) {
- return undefined;
- }
- if (Ext.isDefined(view.vmconfig['mp' + value])) {
- return 'Mount point is already in use.';
- }
- return true;
- },
- },
- {
- xtype: 'pveDiskStorageSelector',
- itemId: 'diskstorage',
- storageContent: 'rootdir',
- hidden: true,
- autoSelect: true,
- selectformat: false,
- defaultSize: 8,
- bind: {
- hidden: '{hideStorSelector}',
- disabled: '{hideStorSelector}',
- nodename: '{node}',
- },
- },
- {
- xtype: 'textfield',
- disabled: true,
- submitValue: false,
- fieldLabel: gettext('Disk image'),
- name: 'file',
- bind: {
- hidden: '{!hideStorSelector}',
- },
- },
- ],
-
- column2: [
- {
- xtype: 'textfield',
- name: 'mp',
- value: '',
- emptyText: gettext('/some/path'),
- allowBlank: false,
- disabled: true,
- fieldLabel: gettext('Path'),
- bind: {
- hidden: '{isRoot}',
- disabled: '{isRoot}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'backup',
- fieldLabel: gettext('Backup'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Include volume in backup job'),
- },
- bind: {
- hidden: '{isRoot}',
- disabled: '{isBindOrRoot}',
- value: '{isIncludedInBackup}',
- },
- },
- ],
-
- advancedColumn1: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'quota',
- defaultValue: 0,
- bind: {
- disabled: '{!quota}',
- },
- fieldLabel: gettext('Enable quota'),
- listeners: {
- disable: function () {
- this.reset();
- },
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'ro',
- defaultValue: 0,
- bind: {
- hidden: '{isRoot}',
- disabled: '{isRoot}',
- },
- fieldLabel: gettext('Read-only'),
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'mountoptions',
- fieldLabel: gettext('Mount options'),
- deleteEmpty: false,
- comboItems: [
- ['discard', 'discard'],
- ['lazytime', 'lazytime'],
- ['noatime', 'noatime'],
- ['nodev', 'nodev'],
- ['noexec', 'noexec'],
- ['nosuid', 'nosuid'],
- ],
- multiSelect: true,
- value: [],
- allowBlank: true,
- },
- ],
-
- advancedColumn2: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'acl',
- fieldLabel: 'ACLs',
- deleteEmpty: false,
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText],
- ['1', Proxmox.Utils.enabledText],
- ['0', Proxmox.Utils.disabledText],
- ],
- value: '__default__',
- bind: {
- disabled: '{isBind}',
- },
- allowBlank: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- inputValue: '0', // reverses the logic
- name: 'replicate',
- fieldLabel: gettext('Skip replication'),
- },
- ],
-});
-
-Ext.define('PVE.lxc.MountPointEdit', {
- extend: 'Proxmox.window.Edit',
-
- unprivileged: false,
-
- initComponent: function () {
- let me = this;
-
- let nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- let unused = me.confid && me.confid.match(/^unused\d+$/);
-
- me.isCreate = me.confid ? unused : true;
-
- let ipanel = Ext.create('PVE.lxc.MountPointInputPanel', {
- confid: me.confid,
- nodename: nodename,
- unused: unused,
- unprivileged: me.unprivileged,
- isCreate: me.isCreate,
- });
-
- let subject;
- if (unused) {
- subject = gettext('Unused Disk');
- } else if (me.isCreate) {
- subject = gettext('Mount Point');
- } else {
- subject = gettext('Mount Point') + ' (' + me.confid + ')';
- }
-
- Ext.apply(me, {
- subject: subject,
- defaultFocus: me.confid !== 'rootfs' ? 'textfield[name=mp]' : 'tool',
- items: ipanel,
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- ipanel.setVMConfig(response.result.data);
-
- if (me.confid) {
- let value = response.result.data[me.confid];
- let mp = PVE.Parser.parseLxcMountPoint(value);
- if (!mp) {
- Ext.Msg.alert(gettext('Error'), 'Unable to parse mount point options');
- me.close();
- return;
- }
- ipanel.setMountPoint(mp);
- me.isValid(); // trigger validation
- }
- },
- });
- },
-});
-Ext.define('PVE.window.MPResize', {
- extend: 'Ext.window.Window',
-
- resizable: false,
-
- resize_disk: function (disk, size) {
- var me = this;
- var params = { disk: disk, size: '+' + size + 'G' };
-
- Proxmox.Utils.API2Request({
- params: params,
- url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/resize',
- waitMsgTarget: me,
- method: 'PUT',
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response, opts) {
- var upid = response.result.data;
- var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid });
- win.show();
- me.close();
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.vmid) {
- throw 'no VM ID specified';
- }
-
- var items = [
- {
- xtype: 'displayfield',
- name: 'disk',
- value: me.disk,
- fieldLabel: gettext('Disk'),
- vtype: 'StorageId',
- allowBlank: false,
- },
- ];
-
- me.hdsizesel = Ext.createWidget('numberfield', {
- name: 'size',
- minValue: 0,
- maxValue: 128 * 1024,
- decimalPrecision: 3,
- value: '0',
- fieldLabel: `${gettext('Size Increment')} (${gettext('GiB')})`,
- allowBlank: false,
- });
-
- items.push(me.hdsizesel);
-
- me.formPanel = Ext.create('Ext.form.Panel', {
- bodyPadding: 10,
- border: false,
- fieldDefaults: {
- labelWidth: 120,
- anchor: '100%',
- },
- items: items,
- });
-
- var form = me.formPanel.getForm();
-
- var submitBtn;
-
- me.title = gettext('Resize disk');
- submitBtn = Ext.create('Ext.Button', {
- text: gettext('Resize disk'),
- handler: function () {
- if (form.isValid()) {
- let values = form.getValues();
- me.resize_disk(me.disk, values.size);
- }
- },
- });
-
- Ext.apply(me, {
- modal: true,
- border: false,
- layout: 'fit',
- buttons: [submitBtn],
- items: [me.formPanel],
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.lxc.NetworkInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveLxcNetworkInputPanel',
-
- insideWizard: false,
-
- onlineHelp: 'pct_container_network',
-
- setNodename: function (nodename) {
- let me = this;
-
- if (!nodename || me.nodename === nodename) {
- return;
- }
- me.nodename = nodename;
-
- let bridgeSelector = me.query('[isFormField][name=bridge]')[0];
- bridgeSelector.setNodename(nodename);
- },
-
- onGetValues: function (values) {
- let me = this;
-
- let id;
- if (me.isCreate) {
- id = values.id;
- delete values.id;
- } else {
- id = me.ifname;
- }
- let newdata = {};
- if (id) {
- if (values.ipv6mode !== 'static') {
- values.ip6 = values.ipv6mode;
- }
- if (values.ipv4mode !== 'static') {
- values.ip = values.ipv4mode;
- }
- newdata[id] = PVE.Parser.printLxcNetwork(values);
- }
- return newdata;
- },
-
- initComponent: function () {
- let me = this;
-
- let cdata = {};
- if (me.insideWizard) {
- me.ifname = 'net0';
- cdata.name = 'eth0';
- me.dataCache = {};
- }
- cdata.firewall = me.insideWizard || me.isCreate;
-
- if (!me.dataCache) {
- throw 'no dataCache specified';
- }
-
- if (!me.isCreate) {
- if (!me.ifname) {
- throw 'no interface name specified';
- }
- if (!me.dataCache[me.ifname]) {
- throw "no such interface '" + me.ifname + "'";
- }
- cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]);
- }
-
- for (let i = 0; i < 32; i++) {
- let ifname = 'net' + i.toString();
- if (me.isCreate && !me.dataCache[ifname]) {
- me.ifname = ifname;
- break;
- }
- }
-
- me.column1 = [
- {
- xtype: 'hidden',
- name: 'id',
- value: me.ifname,
- },
- {
- xtype: 'textfield',
- name: 'name',
- fieldLabel: gettext('Name'),
- emptyText: '(e.g., eth0)',
- allowBlank: false,
- value: cdata.name,
- validator: function (value) {
- for (const [key, netRaw] of Object.entries(me.dataCache)) {
- if (!key.match(/^net\d+/) || key === me.ifname) {
- continue;
- }
- let net = PVE.Parser.parseLxcNetwork(netRaw);
- if (net.name === value) {
- return 'interface name already in use';
- }
- }
- return true;
- },
- },
- {
- xtype: 'textfield',
- name: 'hwaddr',
- fieldLabel: gettext('MAC address'),
- vtype: 'MacAddress',
- value: cdata.hwaddr,
- allowBlank: true,
- emptyText: 'auto',
- },
- {
- xtype: 'PVE.form.BridgeSelector',
- name: 'bridge',
- nodename: me.nodename,
- fieldLabel: gettext('Bridge'),
- value: cdata.bridge,
- allowBlank: false,
- },
- {
- xtype: 'pveVlanField',
- name: 'tag',
- value: cdata.tag,
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Firewall'),
- name: 'firewall',
- value: cdata.firewall,
- },
- ];
-
- let dhcp4 = cdata.ip === 'dhcp';
- if (dhcp4) {
- cdata.ip = '';
- cdata.gw = '';
- }
-
- let auto6 = cdata.ip6 === 'auto';
- let dhcp6 = cdata.ip6 === 'dhcp';
- if (auto6 || dhcp6) {
- cdata.ip6 = '';
- cdata.gw6 = '';
- }
-
- me.column2 = [
- {
- layout: {
- type: 'hbox',
- align: 'middle',
- },
- border: false,
- margin: '0 0 5 0',
- items: [
- {
- xtype: 'label',
- text: 'IPv4:', // do not localize
- },
- {
- xtype: 'radiofield',
- boxLabel: gettext('Static'),
- name: 'ipv4mode',
- inputValue: 'static',
- checked: !dhcp4,
- margin: '0 0 0 10',
- listeners: {
- change: function (cb, value) {
- me.down('field[name=ip]').setEmptyText(
- value ? Proxmox.Utils.NoneText : '',
- );
- me.down('field[name=ip]').setDisabled(!value);
- me.down('field[name=gw]').setDisabled(!value);
- },
- },
- },
- {
- xtype: 'radiofield',
- boxLabel: 'DHCP', // do not localize
- name: 'ipv4mode',
- inputValue: 'dhcp',
- checked: dhcp4,
- margin: '0 0 0 10',
- },
- ],
- },
- {
- xtype: 'textfield',
- name: 'ip',
- vtype: 'IPCIDRAddress',
- value: cdata.ip,
- emptyText: dhcp4 ? '' : Proxmox.Utils.NoneText,
- disabled: dhcp4,
- fieldLabel: 'IPv4/CIDR', // do not localize
- },
- {
- xtype: 'textfield',
- name: 'gw',
- value: cdata.gw,
- vtype: 'IPAddress',
- disabled: dhcp4,
- fieldLabel: gettext('Gateway') + ' (IPv4)',
- margin: '0 0 3 0', // override bottom margin to account for the menuseparator
- },
- {
- xtype: 'menuseparator',
- height: '3',
- margin: '0',
- },
- {
- layout: {
- type: 'hbox',
- align: 'middle',
- },
- border: false,
- margin: '0 0 5 0',
- items: [
- {
- xtype: 'label',
- text: 'IPv6:', // do not localize
- },
- {
- xtype: 'radiofield',
- boxLabel: gettext('Static'),
- name: 'ipv6mode',
- inputValue: 'static',
- checked: !(auto6 || dhcp6),
- margin: '0 0 0 10',
- listeners: {
- change: function (cb, value) {
- me.down('field[name=ip6]').setEmptyText(
- value ? Proxmox.Utils.NoneText : '',
- );
- me.down('field[name=ip6]').setDisabled(!value);
- me.down('field[name=gw6]').setDisabled(!value);
- },
- },
- },
- {
- xtype: 'radiofield',
- boxLabel: 'DHCP', // do not localize
- name: 'ipv6mode',
- inputValue: 'dhcp',
- checked: dhcp6,
- margin: '0 0 0 10',
- },
- {
- xtype: 'radiofield',
- boxLabel: 'SLAAC', // do not localize
- name: 'ipv6mode',
- inputValue: 'auto',
- checked: auto6,
- margin: '0 0 0 10',
- },
- ],
- },
- {
- xtype: 'textfield',
- name: 'ip6',
- value: cdata.ip6,
- emptyText: dhcp6 || auto6 ? '' : Proxmox.Utils.NoneText,
- vtype: 'IP6CIDRAddress',
- disabled: dhcp6 || auto6,
- fieldLabel: 'IPv6/CIDR', // do not localize
- },
- {
- xtype: 'textfield',
- name: 'gw6',
- vtype: 'IP6Address',
- value: cdata.gw6,
- disabled: dhcp6 || auto6,
- fieldLabel: gettext('Gateway') + ' (IPv6)',
- },
- ];
-
- me.advancedColumn1 = [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Disconnect'),
- name: 'link_down',
- value: cdata.link_down,
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: 'MTU',
- emptyText: gettext('Same as bridge'),
- name: 'mtu',
- value: cdata.mtu,
- minValue: 576,
- maxValue: 65535,
- },
- ];
-
- me.advancedColumn2 = [
- {
- xtype: 'numberfield',
- name: 'rate',
- fieldLabel: gettext('Rate limit') + ' (MB/s)',
- minValue: 0,
- maxValue: 10 * 1024,
- value: cdata.rate,
- emptyText: 'unlimited',
- allowBlank: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Host-Managed'),
- name: 'host-managed',
- value: cdata['host-managed'],
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.lxc.NetworkEdit', {
- extend: 'Proxmox.window.Edit',
-
- isAdd: true,
-
- initComponent: function () {
- let me = this;
-
- if (!me.dataCache) {
- throw 'no dataCache specified';
- }
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- Ext.apply(me, {
- subject: gettext('Network Device') + ' (veth)',
- digest: me.dataCache.digest,
- items: [
- {
- xtype: 'pveLxcNetworkInputPanel',
- ifname: me.ifname,
- nodename: me.nodename,
- dataCache: me.dataCache,
- isCreate: me.isCreate,
- },
- ],
- });
-
- me.callParent();
- },
-});
-
-Ext.define(
- 'PVE.lxc.NetworkView',
- {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveLxcNetworkView',
-
- onlineHelp: 'pct_container_network',
-
- dataCache: {}, // used to store result of last load
-
- stateful: true,
- stateId: 'grid-lxc-network',
-
- load: async function () {
- let me = this;
-
- Proxmox.Utils.setErrorMask(me, true);
-
- let nodename = me.pveSelNode.data.node;
- let vmid = me.pveSelNode.data.vmid;
-
- try {
- let ifResponse = await Proxmox.Async.api2({
- url: `/nodes/${nodename}/lxc/${vmid}/interfaces`,
- method: 'GET',
- });
- let confResponse = await Proxmox.Async.api2({
- url: me.url,
- });
- Proxmox.Utils.setErrorMask(me, false);
-
- let interfaces = [];
- for (const [, iface] of Object.entries(ifResponse?.result?.data || {})) {
- interfaces[iface['hardware-address']] = iface;
- }
-
- let records = [];
- me.dataCache = confResponse.result.data || {};
- for (const [key, value] of Object.entries(confResponse.result.data)) {
- if (!key.match(/^net\d+/)) {
- continue;
- }
- let config = PVE.Parser.parseLxcNetwork(value);
- let net = structuredClone(config);
- net.id = key;
-
- let iface = interfaces[config.hwaddr.toLowerCase()];
- if (iface) {
- net.name = iface.name;
- net.ip = [];
- net.ip6 = [];
- for (const i of iface['ip-addresses']) {
- let ip_with_prefix = `${i['ip-address']}/${i.prefix}`;
- if (i['ip-address-type'] === 'inet') {
- if (config.ip === ip_with_prefix) {
- net.ip.push(`${ip_with_prefix} (static)`);
- } else {
- // this could be dhcp, but also a static address set directly on the container
- net.ip.push(`${ip_with_prefix} (dynamic)`);
- }
- } else if (i['ip-address-type'] === 'inet6') {
- if (config.ip6 === ip_with_prefix) {
- net.ip6.push(`${ip_with_prefix} (static)`);
- } else {
- // this could be dhcp, slaac, but also a static address set directly on the container
- net.ip6.push(`${ip_with_prefix} (dynamic)`);
- }
- }
- }
- }
- records.push(net);
- }
-
- me.store.loadData(records);
- me.down('button[name=addButton]').setDisabled(records.length >= 32);
- } catch (error) {
- Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + error);
- }
- },
-
- initComponent: function () {
- let me = this;
-
- let nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- let vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- let caps = Ext.state.Manager.get('GuiCap');
-
- me.url = `/nodes/${nodename}/lxc/${vmid}/config`;
-
- let store = new Ext.data.Store({
- model: 'pve-lxc-network',
- sorters: [
- {
- property: 'id',
- direction: 'ASC',
- },
- ],
- });
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec || !caps.vms['VM.Config.Network']) {
- return false; // disable default-propagation when triggered by grid dblclick
- }
- Ext.create('PVE.lxc.NetworkEdit', {
- url: me.url,
- nodename: nodename,
- dataCache: me.dataCache,
- ifname: rec.data.id,
- listeners: {
- destroy: () => me.load(),
- },
- autoShow: true,
- });
- return undefined;
- };
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: [
- {
- text: gettext('Add'),
- name: 'addButton',
- disabled: !caps.vms['VM.Config.Network'],
- handler: function () {
- Ext.create('PVE.lxc.NetworkEdit', {
- url: me.url,
- nodename: nodename,
- isCreate: true,
- dataCache: me.dataCache,
- listeners: {
- destroy: () => me.load(),
- },
- autoShow: true,
- });
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Remove'),
- disabled: true,
- selModel: sm,
- enableFn: function (rec) {
- return !!caps.vms['VM.Config.Network'];
- },
- confirmMsg: ({ data }) =>
- Ext.String.format(
- gettext('Are you sure you want to remove entry {0}'),
- `'${data.id}'`,
- ),
- handler: function (btn, e, rec) {
- Proxmox.Utils.API2Request({
- url: me.url,
- waitMsgTarget: me,
- method: 'PUT',
- params: {
- delete: rec.data.id,
- digest: me.dataCache.digest,
- },
- callback: () => me.load(),
- failure: (response, opts) =>
- Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- selModel: sm,
- disabled: true,
- enableFn: (rec) => !!caps.vms['VM.Config.Network'],
- handler: run_editor,
- },
- ],
- columns: [
- {
- header: 'ID',
- width: 50,
- dataIndex: 'id',
- },
- {
- header: gettext('Name'),
- width: 80,
- dataIndex: 'name',
- },
- {
- header: gettext('Bridge'),
- width: 80,
- dataIndex: 'bridge',
- },
- {
- header: gettext('Firewall'),
- width: 80,
- dataIndex: 'firewall',
- renderer: Proxmox.Utils.format_boolean,
- },
- {
- header: gettext('VLAN Tag'),
- width: 70,
- dataIndex: 'tag',
- },
- {
- header: gettext('MAC address'),
- width: 110,
- dataIndex: 'hwaddr',
- },
- {
- header: gettext('IP address'),
- width: 300,
- dataIndex: 'ip',
- renderer: function (_value, _metaData, rec) {
- const formatIpValue = (value, prefix) => {
- if (Array.isArray(value) && value.length > 0) {
- // multiple addresses (usually from the api)
- return value.join(' ') + ' ';
- } else if (typeof value === 'string') {
- if (value === 'dhcp') {
- // ipv4 and ipv6 dhcp
- return `${prefix}dhcp `;
- } else if (value === 'auto') {
- // ipv6 slaac
- return `${prefix}auto `;
- } else if (value.length > 0) {
- // single address (usually from config)
- return value + ' ';
- }
- }
- return '';
- };
-
- return (
- formatIpValue(rec.data.ip, 'ip: ') +
- formatIpValue(rec.data.ip6, 'ip6: ')
- );
- },
- },
- {
- header: gettext('Gateway'),
- width: 150,
- dataIndex: 'gw',
- renderer: function (value, metaData, rec) {
- if (rec.data.gw && rec.data.gw6) {
- return rec.data.gw + ' ' + rec.data.gw6;
- } else if (rec.data.gw6) {
- return rec.data.gw6;
- } else {
- return rec.data.gw;
- }
- },
- },
- {
- header: gettext('MTU'),
- width: 80,
- dataIndex: 'mtu',
- },
- {
- header: gettext('Disconnected'),
- width: 100,
- dataIndex: 'link_down',
- renderer: Proxmox.Utils.format_boolean,
- },
- ],
- listeners: {
- activate: me.load,
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-lxc-network', {
- extend: 'Ext.data.Model',
- proxy: { type: 'memory' },
- fields: [
- 'id',
- 'name',
- 'hwaddr',
- 'bridge',
- 'ip',
- 'gw',
- 'ip6',
- 'gw6',
- 'tag',
- 'firewall',
- 'mtu',
- 'link_down',
- ],
- });
- },
-);
-Ext.define('PVE.lxc.Options', {
- extend: 'Proxmox.grid.PendingObjectGrid',
- alias: ['widget.pveLxcOptions'],
-
- onlineHelp: 'pct_options',
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- var rows = {
- onboot: {
- header: gettext('Start at boot'),
- defaultValue: '',
- renderer: Proxmox.Utils.format_boolean,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Start at boot'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'onboot',
- uncheckedValue: 0,
- defaultValue: 0,
- fieldLabel: gettext('Start at boot'),
- },
- }
- : undefined,
- },
- startup: {
- header: gettext('Start/Shutdown order'),
- defaultValue: '',
- renderer: PVE.Utils.render_kvm_startup,
- editor:
- caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify']
- ? {
- xtype: 'pveWindowStartupEdit',
- onlineHelp: 'pct_startup_and_shutdown',
- }
- : undefined,
- },
- ostype: {
- header: gettext('OS Type'),
- defaultValue: Proxmox.Utils.unknownText,
- },
- arch: {
- header: gettext('Architecture'),
- defaultValue: Proxmox.Utils.unknownText,
- },
- console: {
- header: '/dev/console',
- defaultValue: 1,
- renderer: Proxmox.Utils.format_enabled_toggle,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: '/dev/console',
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'console',
- uncheckedValue: 0,
- defaultValue: 1,
- deleteDefaultValue: true,
- checked: true,
- fieldLabel: '/dev/console',
- },
- }
- : undefined,
- },
- tty: {
- header: gettext('TTY count'),
- defaultValue: 2,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('TTY count'),
- items: {
- xtype: 'proxmoxintegerfield',
- name: 'tty',
- minValue: 0,
- maxValue: 6,
- fieldLabel: gettext('TTY count'),
- emptyText: gettext('Default'),
- deleteEmpty: true,
- },
- }
- : undefined,
- },
- cmode: {
- header: gettext('Console mode'),
- defaultValue: 'tty',
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Console mode'),
- items: {
- xtype: 'proxmoxKVComboBox',
- name: 'cmode',
- deleteEmpty: true,
- value: '__default__',
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (tty)'],
- ['tty', '/dev/tty[X]'],
- ['console', '/dev/console'],
- ['shell', 'shell'],
- ],
- fieldLabel: gettext('Console mode'),
- },
- }
- : undefined,
- },
- protection: {
- header: gettext('Protection'),
- defaultValue: false,
- renderer: Proxmox.Utils.format_boolean,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Protection'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'protection',
- uncheckedValue: 0,
- defaultValue: 0,
- deleteDefaultValue: true,
- fieldLabel: gettext('Enabled'),
- },
- }
- : undefined,
- },
- unprivileged: {
- header: gettext('Unprivileged container'),
- renderer: Proxmox.Utils.format_boolean,
- defaultValue: 0,
- },
- features: {
- header: gettext('Features'),
- defaultValue: Proxmox.Utils.noneText,
- editor: 'PVE.lxc.FeaturesEdit',
- },
- hookscript: {
- header: gettext('Hookscript'),
- renderer: Ext.htmlEncode,
- },
- entrypoint: {
- header: gettext('Entrypoint'),
- defaultValue: '/sbin/init',
- renderer: Ext.htmlEncode,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Entrypoint Init Command'),
- defaultFocus: undefined,
- items: [
- {
- xtype: 'proxmoxtextfield',
- name: 'entrypoint',
- deleteEmpty: true,
- emptyText: '/sbin/init',
- },
-
- {
- xtype: 'displayfield',
- reference: 'emptyWarning',
- userCls: 'pmx-hint',
- value: gettext(
- 'Changing the entrypoint command can lead to start failure!',
- ),
- },
- ],
- }
- : undefined,
- },
- env: {
- header: gettext('Environment'),
- renderer: (v) => (v ? Ext.htmlEncode(v.replaceAll(/\0+/g, ' ')) : null),
- defaultValue: Proxmox.Utils.noneText,
- editor: 'PVE.lxc.EnvEdit',
- },
- };
-
- var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- enableFn: function (rec) {
- var rowdef = rows[rec.data.key];
- return !!rowdef.editor;
- },
- handler: function () {
- me.run_editor();
- },
- });
-
- var revert_btn = new PVE.button.PendingRevert();
-
- var set_button_status = function () {
- let button_sm = me.getSelectionModel();
- let rec = button_sm.getSelection()[0];
-
- if (!rec) {
- edit_btn.disable();
- return;
- }
-
- var key = rec.data.key;
- var pending = rec.data.delete || me.hasPendingChanges(key);
- var rowdef = rows[key];
-
- if (key === 'features') {
- let unprivileged = me.getStore().getById('unprivileged').data.value;
- let root = Proxmox.UserName === 'root@pam';
- let vmalloc = caps.vms['VM.Allocate'];
- edit_btn.setDisabled(!(root || (vmalloc && unprivileged)));
- } else {
- edit_btn.setDisabled(!rowdef.editor);
- }
-
- revert_btn.setDisabled(!pending);
- };
-
- Ext.apply(me, {
- url: '/api2/json/nodes/' + nodename + '/lxc/' + vmid + '/pending',
- selModel: sm,
- interval: 5000,
- tbar: [edit_btn, revert_btn],
- rows: rows,
- editorConfig: {
- url: '/api2/extjs/' + baseurl,
- },
- listeners: {
- itemdblclick: me.run_editor,
- selectionchange: set_button_status,
- },
- });
-
- me.callParent();
-
- me.on('activate', me.rstore.startUpdate);
- me.on('destroy', me.rstore.stopUpdate);
- me.on('deactivate', me.rstore.stopUpdate);
-
- me.mon(me.getStore(), 'datachanged', function () {
- set_button_status();
- });
- },
-});
-var labelWidth = 120;
-
-Ext.define('PVE.lxc.MemoryEdit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- Ext.apply(me, {
- subject: gettext('Memory'),
- items: Ext.create('PVE.lxc.MemoryInputPanel'),
- });
-
- me.callParent();
-
- me.load();
- },
-});
-
-Ext.define('PVE.lxc.CPUEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveLxcCPUEdit',
-
- viewModel: {
- data: {
- cgroupMode: 2,
- },
- },
-
- initComponent: function () {
- let me = this;
- me.getViewModel().set('cgroupMode', me.cgroupMode);
-
- Ext.apply(me, {
- subject: gettext('CPU'),
- items: Ext.create('PVE.lxc.CPUInputPanel'),
- });
-
- me.callParent();
-
- me.load();
- },
-});
-
-// The view model of the parent should contain a 'cgroupMode' variable (or params for v2 are used).
-Ext.define('PVE.lxc.CPUInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveLxcCPUInputPanel',
-
- onlineHelp: 'pct_cpu',
-
- insideWizard: false,
-
- viewModel: {
- formulas: {
- cpuunitsDefault: (get) => (get('cgroupMode') === 1 ? 1024 : 100),
- cpuunitsMax: (get) => (get('cgroupMode') === 1 ? 500000 : 10000),
- },
- },
-
- onGetValues: function (values) {
- let me = this;
- let cpuunitsDefault = me.getViewModel().get('cpuunitsDefault');
-
- PVE.Utils.delete_if_default(values, 'cpulimit', '0', me.insideWizard);
- PVE.Utils.delete_if_default(values, 'cpuunits', `${cpuunitsDefault}`, me.insideWizard);
-
- return values;
- },
-
- advancedColumn1: [
- {
- xtype: 'numberfield',
- name: 'cpulimit',
- minValue: 0,
- value: '',
- step: 1,
- fieldLabel: gettext('CPU limit'),
- allowBlank: true,
- emptyText: gettext('unlimited'),
- },
- ],
-
- advancedColumn2: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'cpuunits',
- fieldLabel: gettext('CPU units'),
- value: '',
- minValue: 8,
- maxValue: '10000',
- emptyText: '100',
- bind: {
- emptyText: '{cpuunitsDefault}',
- maxValue: '{cpuunitsMax}',
- },
- labelWidth: labelWidth,
- deleteEmpty: true,
- allowBlank: true,
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: 'proxmoxintegerfield',
- name: 'cores',
- minValue: 1,
- maxValue: 8192,
- value: me.insideWizard ? 1 : '',
- fieldLabel: gettext('Cores'),
- allowBlank: true,
- deleteEmpty: true,
- emptyText: gettext('unlimited'),
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.lxc.MemoryInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveLxcMemoryInputPanel',
-
- onlineHelp: 'pct_memory',
-
- insideWizard: false,
-
- initComponent: function () {
- var me = this;
-
- var items = [
- {
- xtype: 'proxmoxintegerfield',
- name: 'memory',
- minValue: 16,
- value: '512',
- step: 32,
- fieldLabel: gettext('Memory') + ' (MiB)',
- labelWidth: labelWidth,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'swap',
- minValue: 0,
- value: '512',
- step: 32,
- fieldLabel: gettext('Swap') + ' (MiB)',
- labelWidth: labelWidth,
- allowBlank: false,
- },
- ];
-
- if (me.insideWizard) {
- me.column1 = items;
- } else {
- me.items = items;
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.lxc.RessourceView', {
- extend: 'Proxmox.grid.PendingObjectGrid',
- alias: ['widget.pveLxcRessourceView'],
-
- onlineHelp: 'pct_configuration',
-
- renderKey: function (key, metaData, rec, rowIndex, colIndex, store) {
- let me = this;
- let rowdef = me.rows[key] || {};
-
- let txt = rowdef.header || key;
- let icon = '';
-
- metaData.tdAttr = 'valign=middle';
- if (rowdef.tdCls) {
- metaData.tdCls = rowdef.tdCls;
- } else if (rowdef.iconCls) {
- icon = ` `;
- metaData.tdCls += ' pve-itype-fa';
- }
- // only return icons in grid but not remove dialog
- if (rowIndex !== undefined) {
- return icon + txt;
- } else {
- return txt;
- }
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- var caps = Ext.state.Manager.get('GuiCap');
- var diskCap = caps.vms['VM.Config.Disk'];
-
- var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined;
-
- const nodeInfo = PVE.data.ResourceStore.getNodes().find((node) => node.node === nodename);
- let cpuEditor = {
- xtype: 'pveLxcCPUEdit',
- cgroupMode: nodeInfo['cgroup-mode'],
- };
-
- var rows = {
- memory: {
- header: gettext('Memory'),
- editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
- defaultValue: 512,
- tdCls: 'pmx-itype-icon-memory',
- group: 1,
- renderer: function (value) {
- return Proxmox.Utils.format_size(value * 1024 * 1024);
- },
- },
- swap: {
- header: gettext('Swap'),
- editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
- defaultValue: 512,
- iconCls: 'refresh',
- group: 2,
- renderer: function (value) {
- return Proxmox.Utils.format_size(value * 1024 * 1024);
- },
- },
- cores: {
- header: gettext('Cores'),
- editor: caps.vms['VM.Config.CPU'] ? cpuEditor : undefined,
- defaultValue: '',
- tdCls: 'pmx-itype-icon-processor',
- group: 3,
- renderer: function (value) {
- var cpulimit = me.getObjectValue('cpulimit');
- var cpuunits = me.getObjectValue('cpuunits');
- var res;
- if (value) {
- res = value;
- } else {
- res = gettext('unlimited');
- }
-
- if (cpulimit) {
- res += ' [cpulimit=' + cpulimit + ']';
- }
-
- if (cpuunits) {
- res += ' [cpuunits=' + cpuunits + ']';
- }
- return res;
- },
- },
- rootfs: {
- header: gettext('Root Disk'),
- defaultValue: Proxmox.Utils.noneText,
- editor: mpeditor,
- iconCls: 'hdd-o',
- group: 4,
- renderer: Ext.htmlEncode,
- },
- cpulimit: {
- visible: false,
- },
- cpuunits: {
- visible: false,
- },
- unprivileged: {
- visible: false,
- },
- };
-
- PVE.Utils.forEachLxcMP(function (bus, i, confid) {
- var group = 5;
- var header;
- if (bus === 'mp') {
- header = gettext('Mount Point') + ' (' + confid + ')';
- } else {
- header = gettext('Unused Disk') + ' ' + i;
- group += 1;
- }
- rows[confid] = {
- group: group,
- order: i,
- iconCls: 'hdd-o',
- editor: mpeditor,
- header: header,
- renderer: Ext.htmlEncode,
- };
- }, true);
-
- let deveditor = Proxmox.UserName === 'root@pam' ? 'PVE.lxc.DeviceEdit' : undefined;
-
- PVE.Utils.forEachLxcDev(function (i, confid) {
- rows[confid] = {
- group: 7,
- order: i,
- tdCls: 'pve-itype-icon-pci',
- editor: deveditor,
- header: gettext('Device') + ' (' + confid + ')',
- renderer: Ext.htmlEncode,
- };
- });
-
- var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
-
- me.selModel = Ext.create('Ext.selection.RowModel', {});
-
- var run_resize = function () {
- var rec = me.selModel.getSelection()[0];
- if (!rec) {
- return;
- }
-
- var win = Ext.create('PVE.window.MPResize', {
- disk: rec.data.key,
- nodename: nodename,
- vmid: vmid,
- });
-
- win.show();
- };
-
- var run_remove = function (b, e, rec) {
- Proxmox.Utils.API2Request({
- url: '/api2/extjs/' + baseurl,
- waitMsgTarget: me,
- method: 'PUT',
- params: {
- delete: rec.data.key,
- },
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- });
- };
-
- let run_move = function () {
- let rec = me.selModel.getSelection()[0];
- if (!rec) {
- return;
- }
-
- var win = Ext.create('PVE.window.HDMove', {
- disk: rec.data.key,
- nodename: nodename,
- vmid: vmid,
- type: 'lxc',
- });
-
- win.show();
-
- win.on('destroy', me.reload, me);
- };
-
- let run_reassign = function () {
- let rec = me.selModel.getSelection()[0];
- if (!rec) {
- return;
- }
-
- Ext.create('PVE.window.GuestDiskReassign', {
- disk: rec.data.key,
- nodename: nodename,
- autoShow: true,
- vmid: vmid,
- type: 'lxc',
- listeners: {
- destroy: () => me.reload(),
- },
- });
- };
-
- var edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- selModel: me.selModel,
- disabled: true,
- enableFn: function (rec) {
- if (!rec) {
- return false;
- }
- var rowdef = rows[rec.data.key];
- return !!rowdef.editor;
- },
- handler: function () {
- me.run_editor();
- },
- });
-
- var remove_btn = new Proxmox.button.Button({
- text: gettext('Remove'),
- defaultText: gettext('Remove'),
- altText: gettext('Detach'),
- selModel: me.selModel,
- disabled: true,
- dangerous: true,
- confirmMsg: function (rec) {
- let warn = Ext.String.format(gettext('Are you sure you want to remove entry {0}'));
- if (this.text === this.altText) {
- warn = gettext('Are you sure you want to detach entry {0}');
- }
- let rendered = me.renderKey(rec.data.key, {}, rec);
- let msg = Ext.String.format(warn, `'${rendered}'`);
-
- if (rec.data.key.match(/^unused\d+$/)) {
- msg += ' ' + gettext('This will permanently erase all data.');
- }
- return msg;
- },
- handler: run_remove,
- listeners: {
- render: function (btn) {
- // hack: calculate the max button width on first display to prevent the whole
- // toolbar to move when we switch between the "Remove" and "Detach" labels
- let def = btn.getSize().width;
-
- btn.setText(btn.altText);
- let alt = btn.getSize().width;
-
- btn.setText(btn.defaultText);
-
- let optimal = alt > def ? alt : def;
- btn.setSize({ width: optimal });
- },
- },
- });
-
- let move_menuitem = new Ext.menu.Item({
- text: gettext('Move Storage'),
- tooltip: gettext('Move volume to another storage'),
- iconCls: 'fa fa-database',
- selModel: me.selModel,
- handler: run_move,
- });
-
- let reassign_menuitem = new Ext.menu.Item({
- text: gettext('Reassign Owner'),
- tooltip: gettext('Reassign volume to another CT'),
- iconCls: 'fa fa-cube',
- handler: run_reassign,
- reference: 'reassing_item',
- });
-
- let resize_menuitem = new Ext.menu.Item({
- text: gettext('Resize'),
- iconCls: 'fa fa-plus',
- selModel: me.selModel,
- handler: run_resize,
- });
-
- let volumeaction_btn = new Proxmox.button.Button({
- text: gettext('Volume Action'),
- disabled: true,
- menu: {
- items: [move_menuitem, reassign_menuitem, resize_menuitem],
- },
- });
-
- let revert_btn = new PVE.button.PendingRevert();
-
- let set_button_status = function () {
- let rec = me.selModel.getSelection()[0];
-
- if (!rec) {
- edit_btn.disable();
- remove_btn.disable();
- volumeaction_btn.disable();
- revert_btn.disable();
- return;
- }
- let { key, value, delete: isDelete } = rec.data;
- let rowdef = rows[key];
-
- let pending = isDelete || me.hasPendingChanges(key);
- let isRootFS = key === 'rootfs';
- let isDisk = isRootFS || key.match(/^(mp|unused)\d+/);
- let isUnusedDisk = key.match(/^unused\d+/);
- let isUsedDisk = isDisk && !isUnusedDisk;
- let isDevice = key.match(/^dev\d+/);
-
- let noedit = isDelete || !rowdef.editor;
- if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) {
- let mp = PVE.Parser.parseLxcMountPoint(value);
- if (mp.type !== 'volume') {
- noedit = true;
- }
- }
- edit_btn.setDisabled(noedit);
-
- volumeaction_btn.setDisabled(!isDisk || !diskCap);
- move_menuitem.setDisabled(isUnusedDisk);
- reassign_menuitem.setDisabled(isRootFS);
- resize_menuitem.setDisabled(isUnusedDisk);
-
- remove_btn.setDisabled(!(isDisk || isDevice) || isRootFS || !diskCap || pending);
- revert_btn.setDisabled(!pending);
-
- remove_btn.setText(isUsedDisk ? remove_btn.altText : remove_btn.defaultText);
- };
-
- let sorterFn = function (rec1, rec2) {
- let v1 = rec1.data.key,
- v2 = rec2.data.key;
-
- let g1 = rows[v1].group || 0,
- g2 = rows[v2].group || 0;
- if (g1 - g2 !== 0) {
- return g1 - g2;
- }
-
- let order1 = rows[v1].order || 0,
- order2 = rows[v2].order || 0;
- if (order1 - order2 !== 0) {
- return order1 - order2;
- }
-
- if (v1 > v2) {
- return 1;
- } else if (v1 < v2) {
- return -1;
- } else {
- return 0;
- }
- };
-
- Ext.apply(me, {
- url: `/api2/json/nodes/${nodename}/lxc/${vmid}/pending`,
- selModel: me.selModel,
- interval: 2000,
- cwidth1: 170,
- tbar: [
- {
- text: gettext('Add'),
- menu: new Ext.menu.Menu({
- items: [
- {
- text: gettext('Mount Point'),
- iconCls: 'fa fa-fw fa-hdd-o black',
- disabled: !caps.vms['VM.Config.Disk'],
- handler: function () {
- Ext.create('PVE.lxc.MountPointEdit', {
- autoShow: true,
- url: `/api2/extjs/${baseurl}`,
- unprivileged: me.getObjectValue('unprivileged'),
- pveSelNode: me.pveSelNode,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
- },
- {
- text: gettext('Device Passthrough'),
- iconCls: 'pve-itype-icon-pci',
- disabled: Proxmox.UserName !== 'root@pam',
- handler: function () {
- Ext.create('PVE.lxc.DeviceEdit', {
- autoShow: true,
- url: `/api2/extjs/${baseurl}`,
- pveSelNode: me.pveSelNode,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
- },
- ],
- }),
- },
- edit_btn,
- remove_btn,
- volumeaction_btn,
- revert_btn,
- ],
- rows: rows,
- sorterFn: sorterFn,
- editorConfig: {
- pveSelNode: me.pveSelNode,
- url: '/api2/extjs/' + baseurl,
- },
- listeners: {
- itemdblclick: me.run_editor,
- selectionchange: set_button_status,
- },
- });
-
- me.callParent();
-
- me.on('activate', me.rstore.startUpdate);
- me.on('destroy', me.rstore.stopUpdate);
- me.on('deactivate', me.rstore.stopUpdate);
-
- me.mon(me.getStore(), 'datachanged', function () {
- set_button_status();
- });
-
- Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') });
- },
-});
-Ext.define('PVE.lxc.MultiMPPanel', {
- extend: 'PVE.panel.MultiDiskPanel',
- alias: 'widget.pveMultiMPPanel',
-
- onlineHelp: 'pct_container_storage',
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- // count of mps + rootfs
- maxCount: PVE.Utils.lxc_mp_counts.mp + 1,
-
- getNextFreeDisk: function (vmconfig) {
- let nextFreeDisk;
- if (!vmconfig.rootfs) {
- return {
- confid: 'rootfs',
- };
- } else {
- for (let i = 0; i < PVE.Utils.lxc_mp_counts.mp; i++) {
- let confid = `mp${i}`;
- if (!vmconfig[confid]) {
- nextFreeDisk = {
- confid,
- };
- break;
- }
- }
- }
- return nextFreeDisk;
- },
-
- addPanel: function (itemId, vmconfig, nextFreeDisk) {
- let me = this;
- return me.getView().add({
- vmconfig,
- border: false,
- showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'),
- xtype: 'pveLxcMountPointInputPanel',
- confid: nextFreeDisk.confid === 'rootfs' ? 'rootfs' : null,
- bind: {
- nodename: '{nodename}',
- unprivileged: '{unprivileged}',
- },
- padding: '0 5 0 10',
- itemId,
- selectFree: true,
- isCreate: true,
- insideWizard: true,
- });
- },
-
- getBaseVMConfig: function () {
- let me = this;
-
- return {
- unprivileged: me.getViewModel().get('unprivileged'),
- };
- },
-
- diskSorter: {
- sorterFn: function (rec1, rec2) {
- if (rec1.data.name === 'rootfs') {
- return -1;
- } else if (rec2.data.name === 'rootfs') {
- return 1;
- }
-
- let mp_match = /^mp(\d+)$/;
- let [, id1] = mp_match.exec(rec1.data.name);
- let [, id2] = mp_match.exec(rec2.data.name);
-
- return parseInt(id1, 10) - parseInt(id2, 10);
- },
- },
-
- deleteDisabled: (view, rI, cI, item, rec) => rec.data.name === 'rootfs',
- },
-});
-Ext.define('PVE.menu.Item', {
- extend: 'Ext.menu.Item',
- alias: 'widget.pveMenuItem',
-
- // set to wrap the handler callback in a confirm dialog showing this text
- confirmMsg: false,
-
- // set to focus 'No' instead of 'Yes' button and show a warning symbol
- dangerous: false,
-
- initComponent: function () {
- let me = this;
- if (me.handler) {
- me.setHandler(me.handler, me.scope);
- }
- me.callParent();
- },
-
- setHandler: function (fn, scope) {
- let me = this;
- me.scope = scope;
- me.handler = function (button, e) {
- if (me.confirmMsg) {
- Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
- Ext.Msg.show({
- title: gettext('Confirm'),
- icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
- msg: me.confirmMsg,
- buttons: Ext.Msg.YESNO,
- defaultFocus: me.dangerous ? 'no' : 'yes',
- callback: function (btn) {
- if (btn === 'yes') {
- Ext.callback(fn, me.scope, [me, e], 0, me);
- }
- },
- });
- } else {
- Ext.callback(fn, me.scope, [me, e], 0, me);
- }
- };
- },
-});
-Ext.define('PVE.menu.TemplateMenu', {
- extend: 'Ext.menu.Menu',
-
- initComponent: function () {
- let me = this;
-
- let info = me.pveSelNode.data;
- if (!info.node) {
- throw 'no node name specified';
- }
- if (!info.vmid) {
- throw 'no VM ID specified';
- }
-
- let guestType = me.pveSelNode.data.type;
- if (guestType !== 'qemu' && guestType !== 'lxc') {
- throw `invalid guest type ${guestType}`;
- }
-
- let template = me.pveSelNode.data.template;
-
- me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + info.vmid;
-
- let caps = Ext.state.Manager.get('GuiCap');
- let standaloneNode = PVE.Utils.isStandaloneNode();
-
- me.items = [
- {
- text: gettext('Migrate'),
- iconCls: 'fa fa-fw fa-send-o',
- hidden: standaloneNode || !caps.vms['VM.Migrate'],
- handler: function () {
- Ext.create('PVE.window.Migrate', {
- vmtype: guestType,
- nodename: info.node,
- vmid: info.vmid,
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Clone'),
- iconCls: 'fa fa-fw fa-clone',
- hidden: !caps.vms['VM.Clone'],
- handler: function () {
- Ext.create('PVE.window.Clone', {
- nodename: info.node,
- guestType: guestType,
- vmid: info.vmid,
- isTemplate: template,
- autoShow: true,
- });
- },
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.ceph.CephInstallWizardInfo', {
- extend: 'Ext.panel.Panel',
- xtype: 'pveCephInstallWizardInfo',
-
- html: `Ceph?
- "Ceph is a unified,
- distributed storage system, designed for excellent performance, reliability,
- and scalability."
-
- Ceph is currently not installed on this node. This wizard
- will guide you through the installation. Click on the next button below
- to begin. After the initial installation, the wizard will offer to create
- an initial configuration. This configuration step is only
- needed once per cluster and will be skipped if a config is already present.
-
-
- Before starting the installation, please take a look at our documentation,
- by clicking the help button below. If you want to gain deeper knowledge about
- Ceph, visit ceph.com .
-
`,
-});
-
-Ext.define('PVE.ceph.CephVersionSelector', {
- extend: 'Ext.form.field.ComboBox',
- xtype: 'pveCephVersionSelector',
-
- fieldLabel: gettext('Ceph version to install'),
-
- displayField: 'display',
- valueField: 'release',
-
- queryMode: 'local',
- editable: false,
- forceSelection: true,
-
- store: {
- fields: [
- 'release',
- 'version',
- {
- name: 'display',
- calculate: (d) => `${d.release} (${d.version})`,
- },
- ],
- proxy: {
- type: 'memory',
- reader: {
- type: 'json',
- },
- },
- data: [{ release: 'squid', version: '19.2' }],
- },
-});
-
-Ext.define('PVE.ceph.CephHighestVersionDisplay', {
- extend: 'Ext.form.field.Display',
- xtype: 'pveCephHighestVersionDisplay',
-
- fieldLabel: gettext('Ceph in the cluster'),
-
- value: 'unknown',
-
- // called on success with (release, versionTxt, versionParts)
- gotNewestVersion: Ext.emptyFn,
-
- initComponent: function () {
- let me = this;
-
- me.callParent(arguments);
-
- Proxmox.Utils.API2Request({
- method: 'GET',
- url: '/cluster/ceph/metadata',
- params: {
- scope: 'versions',
- },
- waitMsgTarget: me,
- success: (response) => {
- let res = response.result;
- if (!res || !res.data || !res.data.node) {
- me.setValue(gettext('Could not detect a ceph installation in the cluster'));
- return;
- }
- let nodes = res.data.node;
- if (me.nodename) {
- // can happen on ceph purge, we do not yet cleanup old version data
- delete nodes[me.nodename];
- }
-
- let maxversion = [];
- let maxversiontext = '';
- for (const [_nodename, data] of Object.entries(nodes)) {
- let version = data.version.parts;
- if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
- maxversion = version;
- maxversiontext = data.version.str;
- }
- }
- // FIXME: get from version selector store
- const major2release = {
- 13: 'luminous',
- 14: 'nautilus',
- 15: 'octopus',
- 16: 'pacific',
- 17: 'quincy',
- 18: 'reef',
- 19: 'squid',
- 20: 'tentacle',
- };
- let release = major2release[maxversion[0]] || 'unknown';
- let newestVersionTxt = `${Ext.String.capitalize(release)} (${maxversiontext})`;
-
- if (release === 'unknown') {
- me.setValue(gettext('Could not detect a ceph installation in the cluster'));
- } else {
- me.setValue(
- Ext.String.format(
- gettext('Newest ceph version in cluster is {0}'),
- newestVersionTxt,
- ),
- );
- }
- me.gotNewestVersion(release, maxversiontext, maxversion);
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
-});
-
-Ext.define('PVE.ceph.CephInstallWizard', {
- extend: 'PVE.window.Wizard',
- alias: 'widget.pveCephInstallWizard',
- mixins: ['Proxmox.Mixin.CBind'],
-
- resizable: false,
- nodename: undefined,
-
- width: 760, // 4:3
- height: 570,
-
- viewModel: {
- data: {
- nodename: '',
- cephRelease: 'squid', // default
- cephRepo: 'enterprise',
- configuration: true,
- isInstalled: false,
- nodeHasSubscription: true, // avoid warning hint until fully loaded
- allHaveSubscription: true, // avoid warning hint until fully loaded
- selectedReleaseIsTechPreview: false, // avoid warning hint until fully loaded
- },
- formulas: {
- repoHintHidden: (get) => get('allHaveSubscription') && get('cephRepo') === 'enterprise',
- repoHint: function (get) {
- let repo = get('cephRepo');
- let nodeSub = get('nodeHasSubscription'),
- allSub = get('allHaveSubscription');
-
- if (repo === 'enterprise') {
- if (!nodeSub) {
- return gettext(
- 'The enterprise repository is enabled, but there is no active subscription!',
- );
- } else if (!allSub) {
- return gettext(
- 'Not all nodes have an active subscription, which is required for cluster-wide enterprise repo access',
- );
- }
- return ''; // should be hidden
- } else if (repo === 'no-subscription') {
- return allSub
- ? gettext(
- 'Cluster has active subscriptions and would be eligible for using the enterprise repository.',
- )
- : gettext(
- 'The no-subscription repository is not the best choice for production setups.',
- );
- } else if (repo === 'manual') {
- return gettext(
- 'The manual repository option expects that the repository is already configured. For example, in combination with the Proxmox Offline Mirror.',
- );
- } else {
- return gettext(
- 'The test repository should only be used for test setups or after consulting the official Proxmox support!',
- );
- }
- },
- },
- },
- cbindData: {
- nodename: undefined,
- },
-
- title: gettext('Setup'),
- navigateNext: function () {
- var tp = this.down('#wizcontent');
- var atab = tp.getActiveTab();
-
- var next = tp.items.indexOf(atab) + 1;
- var ntab = tp.items.getAt(next);
- if (ntab) {
- ntab.enable();
- tp.setActiveTab(ntab);
- }
- },
- setInitialTab: function (index) {
- var tp = this.down('#wizcontent');
- var initialTab = tp.items.getAt(index);
- initialTab.enable();
- tp.setActiveTab(initialTab);
- },
- onShow: function () {
- this.callParent(arguments);
- let viewModel = this.getViewModel();
- var isInstalled = this.getViewModel().get('isInstalled');
- if (isInstalled) {
- viewModel.set('configuration', false);
- this.setInitialTab(2);
- }
-
- PVE.Utils.getClusterSubscriptionLevel().then((subcriptionMap) => {
- viewModel.set('nodeHasSubscription', !!subcriptionMap[this.nodename]);
-
- let allHaveSubscription = Object.values(subcriptionMap).every((level) => !!level);
- viewModel.set('allHaveSubscription', allHaveSubscription);
- });
- },
- items: [
- {
- xtype: 'panel',
- title: gettext('Info'),
- viewModel: {}, // needed to inherit parent viewModel data
- border: false,
- bodyBorder: false,
- onlineHelp: 'chapter_pveceph',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- defaults: {
- border: false,
- bodyBorder: false,
- },
- items: [
- {
- xtype: 'pveCephInstallWizardInfo',
- },
- {
- flex: 1,
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Hint'),
- labelClsExtra: 'pmx-hint',
- submitValue: false,
- labelWidth: 50,
- bind: {
- value: '{repoHint}',
- hidden: '{repoHintHidden}',
- },
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Note'),
- labelClsExtra: 'pmx-hint',
- submitValue: false,
- labelWidth: 50,
- value: gettext(
- 'The selected release is currently considered a Technology Preview. Although we are not aware of any major issues, there may be some bugs and the Enterprise Repository is not yet available.',
- ),
- bind: {
- hidden: '{!selectedReleaseIsTechPreview}',
- },
- },
- {
- xtype: 'pveCephHighestVersionDisplay',
- labelWidth: 150,
- cbind: {
- nodename: '{nodename}',
- },
- gotNewestVersion: function (release, maxversiontext, maxversion) {
- if (release === 'unknown') {
- return;
- }
- let wizard = this.up('pveCephInstallWizard');
- wizard.getViewModel().set('cephRelease', release);
- },
- },
- {
- xtype: 'container',
- layout: 'hbox',
- defaults: {
- border: false,
- layout: 'anchor',
- flex: 1,
- },
- items: [
- {
- xtype: 'pveCephVersionSelector',
- labelWidth: 150,
- padding: '0 10 0 0',
- submitValue: false,
- bind: {
- value: '{cephRelease}',
- },
- listeners: {
- change: function (field, release) {
- let me = this;
- let wizard = this.up('pveCephInstallWizard');
- wizard
- .down('#next')
- .setText(
- Ext.String.format(
- gettext('Start {0} installation'),
- release,
- ),
- );
-
- let record = me.store.findRecord(
- 'release',
- release,
- 0,
- false,
- true,
- true,
- );
- let releaseIsTechPreview = !!record.data.preview;
- wizard
- .getViewModel()
- .set('selectedReleaseIsTechPreview', releaseIsTechPreview);
-
- let repoSelector = wizard.down('#repoSelector');
- if (releaseIsTechPreview) {
- repoSelector.store.filterBy(
- (entry) => entry.get('key') !== 'enterprise',
- );
- } else {
- repoSelector.store.clearFilter();
- }
- },
- },
- },
- {
- xtype: 'proxmoxKVComboBox',
- id: 'repoSelector', // TODO: use name or reference (how to lookup that here?)
- fieldLabel: gettext('Repository'),
- padding: '0 0 0 10',
- comboItems: [
- ['enterprise', gettext('Enterprise (recommended)')],
- ['no-subscription', gettext('No-Subscription')],
- ['test', gettext('Test')],
- ['manual', gettext('Manual')],
- ],
- labelWidth: 150,
- submitValue: false,
- value: 'enterprise',
- bind: {
- value: '{cephRepo}',
- },
- },
- ],
- },
- ],
- listeners: {
- activate: function () {
- // notify owning container that it should display a help button
- if (this.onlineHelp) {
- Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
- }
- let wizard = this.up('pveCephInstallWizard');
- let release = wizard.getViewModel().get('cephRelease');
- wizard.down('#back').hide(true);
- wizard
- .down('#next')
- .setText(Ext.String.format(gettext('Start {0} installation'), release));
- },
- deactivate: function () {
- if (this.onlineHelp) {
- Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
- }
- this.up('pveCephInstallWizard').down('#next').setText(gettext('Next'));
- },
- },
- },
- {
- title: gettext('Installation'),
- xtype: 'panel',
- layout: 'fit',
- cbind: {
- nodename: '{nodename}',
- },
- viewModel: {}, // needed to inherit parent viewModel data
- listeners: {
- afterrender: function () {
- var me = this;
- if (this.getViewModel().get('isInstalled')) {
- this.mask(
- 'Ceph is already installed, click next to create your configuration.',
- ['pve-static-mask'],
- );
- } else {
- me.down('pveNoVncConsole').fireEvent('activate');
- }
- },
- activate: function () {
- let me = this;
- const nodename = me.nodename;
- me.updateStore = Ext.create('Proxmox.data.UpdateStore', {
- storeid: 'ceph-status-' + nodename,
- interval: 1000,
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/' + nodename + '/ceph/status',
- },
- listeners: {
- load: function (rec, response, success, operation) {
- if (success) {
- me.updateStore.stopUpdate();
- me.down('textfield').setValue('success');
- } else if (
- operation.error.statusText.match('not initialized', 'i')
- ) {
- me.updateStore.stopUpdate();
- me.up('pveCephInstallWizard')
- .getViewModel()
- .set('configuration', false);
- me.down('textfield').setValue('success');
- } else if (
- operation.error.statusText.match('rados_connect failed', 'i')
- ) {
- me.updateStore.stopUpdate();
- me.up('pveCephInstallWizard')
- .getViewModel()
- .set('configuration', true);
- me.down('textfield').setValue('success');
- } else if (
- !operation.error.statusText.match('not installed', 'i')
- ) {
- let msg = Ext.htmlEncode(operation.error.statusText);
- Proxmox.Utils.setErrorMask(me, msg);
- }
- },
- },
- });
- me.updateStore.startUpdate();
- },
- destroy: function () {
- var me = this;
- if (me.updateStore) {
- me.updateStore.stopUpdate();
- }
- },
- },
- items: [
- {
- xtype: 'pveNoVncConsole',
- itemId: 'jsconsole',
- consoleType: 'cmd',
- xtermjs: true,
- cbind: {
- nodename: '{nodename}',
- },
- beforeLoad: function () {
- let me = this;
- let wizard = me.up('pveCephInstallWizard');
- let release = wizard.getViewModel().get('cephRelease');
- let repo = wizard.getViewModel().get('cephRepo');
- me.cmdOpts = `--version\0${release}\0--repository\0${repo}`;
- },
- cmd: 'ceph_install',
- },
- {
- xtype: 'textfield',
- name: 'installSuccess',
- value: '',
- allowBlank: false,
- submitValue: false,
- hidden: true,
- },
- ],
- },
- {
- xtype: 'inputpanel',
- title: gettext('Configuration'),
- onlineHelp: 'chapter_pveceph',
- height: 300,
- cbind: {
- nodename: '{nodename}',
- },
- viewModel: {
- data: {
- replicas: undefined,
- minreplicas: undefined,
- },
- },
- listeners: {
- activate: function () {
- this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next'));
- },
- afterrender: function () {
- if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
- this.mask('Configuration already initialized', ['pve-static-mask']);
- } else {
- this.unmask();
- }
- },
- deactivate: function () {
- this.up('pveCephInstallWizard').down('#submit').setText(gettext('Finish'));
- },
- },
- column1: [
- {
- xtype: 'displayfield',
- value: gettext('Ceph cluster configuration') + ':',
- },
- {
- xtype: 'proxmoxNetworkSelector',
- name: 'network',
- value: '',
- fieldLabel: 'Public Network IP/CIDR',
- autoSelect: false,
- type: 'include_sdn',
- bind: {
- allowBlank: '{configuration}',
- },
- cbind: {
- nodename: '{nodename}',
- },
- },
- {
- xtype: 'proxmoxNetworkSelector',
- name: 'cluster-network',
- fieldLabel: 'Cluster Network IP/CIDR',
- allowBlank: true,
- autoSelect: false,
- type: 'include_sdn',
- emptyText: gettext('Same as Public Network'),
- cbind: {
- nodename: '{nodename}',
- },
- },
- // FIXME: add hint about cluster network and/or reference user to docs??
- ],
- column2: [
- {
- xtype: 'displayfield',
- value: gettext('First Ceph monitor') + ':',
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Monitor node'),
- cbind: {
- value: '{nodename}',
- },
- },
- {
- xtype: 'displayfield',
- value: gettext(
- 'Additional monitors are recommended. They can be created at any time in the Monitor tab.',
- ),
- userCls: 'pmx-hint',
- },
- ],
- advancedColumn1: [
- {
- xtype: 'numberfield',
- name: 'size',
- fieldLabel: 'Number of replicas',
- bind: {
- value: '{replicas}',
- },
- maxValue: 7,
- minValue: 2,
- emptyText: '3',
- },
- {
- xtype: 'numberfield',
- name: 'min_size',
- fieldLabel: 'Minimum replicas',
- bind: {
- maxValue: '{replicas}',
- value: '{minreplicas}',
- },
- minValue: 2,
- maxValue: 3,
- setMaxValue: function (value) {
- this.maxValue = Ext.Number.from(value, 2);
- // allow enough to avoid split brains with max 'size', but more makes simply no sense
- if (this.maxValue > 4) {
- this.maxValue = 4;
- }
- this.toggleSpinners();
- this.validate();
- },
- emptyText: '2',
- },
- ],
- onGetValues: function (values) {
- ['cluster-network', 'size', 'min_size'].forEach(function (field) {
- if (!values[field]) {
- delete values[field];
- }
- });
- return values;
- },
- onSubmit: function () {
- var me = this;
- if (!this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
- let wizard = me.up('window');
- let kv = wizard.getValues();
- delete kv.delete;
- let nodename = me.nodename;
- delete kv.nodename;
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/ceph/init`,
- waitMsgTarget: wizard,
- method: 'POST',
- params: kv,
- success: function () {
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/ceph/mon/${nodename}`,
- waitMsgTarget: wizard,
- method: 'POST',
- success: function () {
- me.up('pveCephInstallWizard').navigateNext();
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- } else {
- me.up('pveCephInstallWizard').navigateNext();
- }
- },
- },
- {
- title: gettext('Success'),
- xtype: 'panel',
- border: false,
- bodyBorder: false,
- onlineHelp: 'pve_ceph_install',
- html:
- 'Installation successful! ' +
- 'The basic installation and configuration is complete. Depending on your setup, some of the following steps are required to start using Ceph:
' +
- 'Install Ceph on other nodes ' +
- 'Create additional Ceph Monitors ' +
- 'Create Ceph OSDs ' +
- 'Create Ceph Pools ' +
- 'To learn more, click on the help button below.
',
- listeners: {
- activate: function () {
- // notify owning container that it should display a help button
- if (this.onlineHelp) {
- Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
- }
-
- var tp = this.up('#wizcontent');
- var idx = tp.items.indexOf(this) - 1;
- for (; idx >= 0; idx--) {
- let nc = tp.items.getAt(idx);
- if (nc) {
- nc.disable();
- }
- }
- },
- deactivate: function () {
- if (this.onlineHelp) {
- Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
- }
- },
- },
- onSubmit: function () {
- var wizard = this.up('pveCephInstallWizard');
- wizard.close();
- },
- },
- ],
-});
-Ext.define('PVE.node.CephConfigDb', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveNodeCephConfigDb',
-
- border: false,
- store: {
- proxy: {
- type: 'proxmox',
- },
- },
-
- columns: [
- {
- dataIndex: 'section',
- text: 'WHO',
- width: 100,
- renderer: Ext.htmlEncode,
- },
- {
- dataIndex: 'mask',
- text: 'MASK',
- hidden: true,
- width: 80,
- renderer: Ext.htmlEncode,
- },
- {
- dataIndex: 'level',
- hidden: true,
- text: 'LEVEL',
- renderer: Ext.htmlEncode,
- },
- {
- dataIndex: 'name',
- flex: 1,
- text: 'OPTION',
- renderer: Ext.htmlEncode,
- },
- {
- dataIndex: 'value',
- flex: 1,
- text: 'VALUE',
- renderer: Ext.htmlEncode,
- },
- {
- dataIndex: 'can_update_at_runtime',
- text: 'Runtime Updatable',
- hidden: true,
- width: 80,
- renderer: Proxmox.Utils.format_boolean,
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- me.store.proxy.url = '/api2/json/nodes/' + nodename + '/ceph/cfg/db';
-
- me.callParent();
-
- Proxmox.Utils.monStoreErrors(me, me.getStore());
- me.getStore().load();
- },
-});
-Ext.define('PVE.node.CephConfig', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveNodeCephConfig',
-
- bodyStyle: 'white-space:pre',
- bodyPadding: 5,
- border: false,
- scrollable: true,
- load: function () {
- var me = this;
-
- Proxmox.Utils.API2Request({
- url: me.url,
- waitMsgTarget: me,
- failure: function (response, opts) {
- me.update(gettext('Error') + ' ' + response.htmlStatus);
- var msg = response.htmlStatus;
- PVE.Utils.showCephInstallOrMask(
- me.ownerCt,
- msg,
- me.pveSelNode.data.node,
- function (win) {
- me.mon(win, 'cephInstallWindowClosed', function () {
- me.load();
- });
- },
- );
- },
- success: function (response, opts) {
- var data = response.result.data;
- me.update(Ext.htmlEncode(data));
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- Ext.apply(me, {
- url: '/nodes/' + nodename + '/ceph/cfg/raw',
- listeners: {
- activate: function () {
- me.load();
- },
- },
- });
-
- me.callParent();
-
- me.load();
- },
-});
-
-Ext.define('PVE.node.CephConfigCrush', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveNodeCephConfigCrush',
-
- onlineHelp: 'chapter_pveceph',
-
- layout: 'border',
- items: [
- {
- title: gettext('Configuration'),
- xtype: 'pveNodeCephConfig',
- region: 'center',
- },
- {
- title: 'Crush Map', // do not localize
- xtype: 'pveNodeCephCrushMap',
- region: 'east',
- split: true,
- width: '50%',
- },
- {
- title: gettext('Configuration Database'),
- xtype: 'pveNodeCephConfigDb',
- region: 'south',
- split: true,
- weight: -30,
- height: '50%',
- },
- ],
-
- initComponent: function () {
- var me = this;
- me.defaults = {
- pveSelNode: me.pveSelNode,
- };
- me.callParent();
- },
-});
-Ext.define('PVE.node.CephCrushMap', {
- extend: 'Ext.panel.Panel',
- alias: ['widget.pveNodeCephCrushMap'],
- bodyStyle: 'white-space:pre',
- bodyPadding: 5,
- border: false,
- stateful: true,
- stateId: 'layout-ceph-crush',
- scrollable: true,
- load: function () {
- var me = this;
-
- Proxmox.Utils.API2Request({
- url: me.url,
- waitMsgTarget: me,
- failure: function (response, opts) {
- me.update(gettext('Error') + ' ' + response.htmlStatus);
- var msg = response.htmlStatus;
- PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node, (win) =>
- me.mon(win, 'cephInstallWindowClosed', () => me.load()),
- );
- },
- success: function (response, opts) {
- var data = response.result.data;
- me.update(Ext.htmlEncode(data));
- },
- });
- },
-
- initComponent: function () {
- let me = this;
-
- let nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- Ext.apply(me, {
- url: `/nodes/${nodename}/ceph/crush`,
- listeners: {
- activate: () => me.load(),
- },
- });
-
- me.callParent();
-
- me.load();
- },
-});
-Ext.define('PVE.CephCreateFS', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveCephCreateFS',
-
- showTaskViewer: true,
- onlineHelp: 'pveceph_fs_create',
-
- subject: 'Ceph FS',
- isCreate: true,
- method: 'POST',
-
- setFSName: function (fsName) {
- var me = this;
-
- if (fsName === '' || fsName === undefined) {
- fsName = 'cephfs';
- }
-
- me.url = '/nodes/' + me.nodename + '/ceph/fs/' + fsName;
- },
-
- items: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Name'),
- name: 'name',
- value: 'cephfs',
- listeners: {
- change: function (f, value) {
- this.up('pveCephCreateFS').setFSName(value);
- },
- },
- submitValue: false, // already encoded in apicall URL
- emptyText: 'cephfs',
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: 'Placement Groups',
- name: 'pg_num',
- value: 128,
- emptyText: 128,
- minValue: 8,
- maxValue: 32768,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Add as Storage'),
- value: true,
- name: 'add-storage',
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Add the new CephFS to the cluster storage configuration.'),
- },
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
- me.setFSName();
-
- me.callParent();
- },
-});
-
-Ext.define(
- 'PVE.NodeCephFSPanel',
- {
- extend: 'Ext.panel.Panel',
- xtype: 'pveNodeCephFSPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- title: gettext('CephFS'),
- onlineHelp: 'pveceph_fs',
-
- border: false,
- defaults: {
- border: false,
- cbind: {
- nodename: '{nodename}',
- },
- },
-
- viewModel: {
- parent: null,
- data: {
- mdsCount: 0,
- },
- formulas: {
- canCreateFS: function (get) {
- return get('mdsCount') > 0;
- },
- },
- },
-
- items: [
- {
- xtype: 'grid',
- emptyText: Ext.String.format(gettext('No {0} configured.'), 'CephFS'),
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- view.rstore = Ext.create('Proxmox.data.UpdateStore', {
- autoLoad: true,
- xtype: 'update',
- interval: 5 * 1000,
- autoStart: true,
- storeid: 'pve-ceph-fs',
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${view.nodename}/ceph/fs`,
- },
- model: 'pve-ceph-fs',
- });
- view.setStore(
- Ext.create('Proxmox.data.DiffStore', {
- rstore: view.rstore,
- sorters: {
- property: 'name',
- direction: 'ASC',
- },
- }),
- );
- // manages the "install ceph?" overlay
- PVE.Utils.monitor_ceph_installed(view, view.rstore, view.nodename, true);
- view.on('destroy', () => view.rstore.stopUpdate());
- },
-
- onCreate: function () {
- let view = this.getView();
- view.rstore.stopUpdate();
- Ext.create('PVE.CephCreateFS', {
- autoShow: true,
- nodename: view.nodename,
- listeners: {
- destroy: () => view.rstore.startUpdate(),
- },
- });
- },
- },
- tbar: [
- {
- text: gettext('Create CephFS'),
- reference: 'createButton',
- handler: 'onCreate',
- bind: {
- disabled: '{!canCreateFS}',
- },
- },
- ],
- columns: [
- {
- header: gettext('Name'),
- flex: 1,
- dataIndex: 'name',
- renderer: Ext.htmlEncode,
- },
- {
- header: gettext('Data Pool'),
- flex: 1,
- dataIndex: 'data_pool',
- renderer: Ext.htmlEncode,
- },
- {
- header: gettext('Metadata Pool'),
- flex: 1,
- dataIndex: 'metadata_pool',
- renderer: Ext.htmlEncode,
- },
- ],
- cbind: {
- nodename: '{nodename}',
- },
- },
- {
- xtype: 'pveNodeCephMDSList',
- title: gettext('Metadata Servers'),
- stateId: 'grid-ceph-mds',
- type: 'mds',
- storeLoadCallback: function (store, records, success) {
- var vm = this.getViewModel();
- if (!success || !records) {
- vm.set('mdsCount', 0);
- return;
- }
- let count = 0;
- for (const mds of records) {
- if (mds.data.state === 'up:standby') {
- count++;
- }
- }
- vm.set('mdsCount', count);
- },
- cbind: {
- nodename: '{nodename}',
- },
- },
- ],
- },
- function () {
- Ext.define('pve-ceph-fs', {
- extend: 'Ext.data.Model',
- fields: ['name', 'data_pool', 'metadata_pool'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/localhost/ceph/fs',
- },
- idProperty: 'name',
- });
- },
-);
-Ext.define('PVE.ceph.Log', {
- extend: 'Proxmox.panel.LogView',
- xtype: 'cephLogView',
-
- nodename: undefined,
-
- failCallback: function (response) {
- var me = this;
- var msg = response.htmlStatus;
- var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename, function (win) {
- me.mon(win, 'cephInstallWindowClosed', function () {
- me.loadTask.delay(200);
- });
- });
- if (!windowShow) {
- Proxmox.Utils.setErrorMask(me, msg);
- }
- },
-});
-Ext.define('PVE.node.CephMonMgrList', {
- extend: 'Ext.container.Container',
- xtype: 'pveNodeCephMonMgr',
-
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'chapter_pveceph',
-
- defaults: {
- border: false,
- onlineHelp: 'chapter_pveceph',
- flex: 1,
- },
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- items: [
- {
- xtype: 'pveNodeCephServiceList',
- cbind: { pveSelNode: '{pveSelNode}' },
- type: 'mon',
- additionalColumns: [
- {
- header: gettext('Quorum'),
- width: 70,
- sortable: true,
- renderer: Proxmox.Utils.format_boolean,
- dataIndex: 'quorum',
- },
- ],
- stateId: 'grid-ceph-monitor',
- showCephInstallMask: true,
- title: gettext('Monitor'),
- },
- {
- xtype: 'pveNodeCephServiceList',
- type: 'mgr',
- stateId: 'grid-ceph-manager',
- cbind: { pveSelNode: '{pveSelNode}' },
- title: gettext('Manager'),
- },
- ],
-});
-Ext.define('PVE.CephCreateOsd', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveCephCreateOsd',
-
- subject: 'Ceph OSD',
-
- showProgress: true,
-
- onlineHelp: 'pve_ceph_osds',
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- me.isCreate = true;
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${me.nodename}/ceph/crush`,
- method: 'GET',
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: function ({ result: { data } }) {
- let classes = [
- ...new Set(
- Array.from(
- data.matchAll(/^device\s[0-9]*\sosd\.[0-9]*\sclass\s(.*)$/gim),
- (m) => m[1],
- ).filter((v) => !['hdd', 'ssd', 'nvme'].includes(v)),
- ),
- ].map((v) => [v, v]);
-
- if (classes.length) {
- let kvField = me.down('field[name=crush-device-class]');
- kvField.setComboItems([...kvField.comboItems, ...classes]);
- }
- },
- });
-
- Ext.applyIf(me, {
- url: '/nodes/' + me.nodename + '/ceph/osd',
- method: 'POST',
- items: [
- {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- Object.keys(values || {}).forEach(function (name) {
- if (values[name] === '') {
- delete values[name];
- }
- });
-
- return values;
- },
- column1: [
- {
- xtype: 'pmxDiskSelector',
- name: 'dev',
- nodename: me.nodename,
- diskType: 'unused',
- includePartitions: true,
- fieldLabel: gettext('Disk'),
- allowBlank: false,
- },
- ],
- column2: [
- {
- xtype: 'pmxDiskSelector',
- name: 'db_dev',
- nodename: me.nodename,
- diskType: 'journal_disks',
- includePartitions: true,
- fieldLabel: gettext('DB Disk'),
- value: '',
- autoSelect: false,
- allowBlank: true,
- emptyText: gettext('use OSD disk'),
- listeners: {
- change: function (field, val) {
- me.down('field[name=db_dev_size]').setDisabled(!val);
- },
- },
- },
- {
- xtype: 'numberfield',
- name: 'db_dev_size',
- fieldLabel: `${gettext('DB size')} (${gettext('GiB')})`,
- minValue: 1,
- maxValue: 128 * 1024,
- decimalPrecision: 2,
- allowBlank: true,
- disabled: true,
- emptyText: gettext('Automatic'),
- },
- ],
- advancedColumn1: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'encrypted',
- fieldLabel: gettext('Encrypt OSD'),
- },
- {
- xtype: 'proxmoxKVComboBox',
- comboItems: [
- ['hdd', 'HDD'],
- ['ssd', 'SSD'],
- ['nvme', 'NVMe'],
- ],
- name: 'crush-device-class',
- nodename: me.nodename,
- fieldLabel: gettext('Device Class'),
- value: '',
- autoSelect: false,
- allowBlank: true,
- editable: true,
- emptyText: gettext('auto detect'),
- deleteEmpty: !me.isCreate,
- },
- ],
- advancedColumn2: [
- {
- xtype: 'pmxDiskSelector',
- name: 'wal_dev',
- nodename: me.nodename,
- diskType: 'journal_disks',
- includePartitions: true,
- fieldLabel: gettext('WAL Disk'),
- value: '',
- autoSelect: false,
- allowBlank: true,
- emptyText: gettext('use OSD/DB disk'),
- listeners: {
- change: function (field, val) {
- me.down('field[name=wal_dev_size]').setDisabled(!val);
- },
- },
- },
- {
- xtype: 'numberfield',
- name: 'wal_dev_size',
- fieldLabel: `${gettext('WAL size')} (${gettext('GiB')})`,
- minValue: 0.5,
- maxValue: 128 * 1024,
- decimalPrecision: 2,
- allowBlank: true,
- disabled: true,
- emptyText: gettext('Automatic'),
- },
- ],
- },
- {
- xtype: 'displayfield',
- padding: '5 0 0 0',
- userCls: 'pmx-hint',
- value:
- 'Note: Ceph is not compatible with disks backed by a hardware ' +
- 'RAID controller. For details see ' +
- 'the reference documentation .',
- },
- ],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.CephRemoveOsd', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveCephRemoveOsd'],
-
- isRemove: true,
-
- showProgress: true,
- method: 'DELETE',
- items: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'cleanup',
- checked: true,
- labelWidth: 130,
- fieldLabel: gettext('Cleanup Disks'),
- },
- {
- xtype: 'displayfield',
- name: 'osd-flag-hint',
- userCls: 'pmx-hint',
- value: gettext('Global flags limiting the self healing of Ceph are enabled.'),
- hidden: true,
- },
- {
- xtype: 'displayfield',
- name: 'degraded-objects-hint',
- userCls: 'pmx-hint',
- value: gettext('Objects are degraded. Consider waiting until the cluster is healthy.'),
- hidden: true,
- },
- ],
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
- if (me.osdid === undefined || me.osdid < 0) {
- throw 'no osdid specified';
- }
-
- me.isCreate = true;
-
- me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString();
-
- Ext.applyIf(me, {
- url: '/nodes/' + me.nodename + '/ceph/osd/' + me.osdid.toString(),
- });
-
- me.callParent();
-
- if (me.warnings.flags) {
- me.down('field[name=osd-flag-hint]').setHidden(false);
- }
- if (me.warnings.degraded) {
- me.down('field[name=degraded-objects-hint]').setHidden(false);
- }
- },
-});
-
-Ext.define('PVE.CephSetFlags', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveCephSetFlags',
-
- showProgress: true,
-
- width: 720,
- layout: 'fit',
-
- onlineHelp: 'pve_ceph_osds',
- isCreate: true,
- title: gettext('Manage Global OSD Flags'),
- submitText: gettext('Apply'),
-
- items: [
- {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- let me = this;
- let val = {};
- me.down('#flaggrid')
- .getStore()
- .each((rec) => {
- val[rec.data.name] = rec.data.value ? 1 : 0;
- });
-
- return val;
- },
- items: [
- {
- xtype: 'grid',
- itemId: 'flaggrid',
- store: {
- listeners: {
- update: function () {
- this.commitChanges();
- },
- },
- },
-
- columns: [
- {
- text: gettext('Enable'),
- xtype: 'checkcolumn',
- width: 75,
- dataIndex: 'value',
- },
- {
- text: 'Name',
- dataIndex: 'name',
- },
- {
- text: 'Description',
- flex: 1,
- dataIndex: 'description',
- renderer: Ext.htmlEncode,
- },
- ],
- },
- ],
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- Ext.applyIf(me, {
- url: '/cluster/ceph/flags',
- method: 'PUT',
- });
-
- me.callParent();
-
- let grid = me.down('#flaggrid');
- me.load({
- success: function (response, options) {
- let data = response.result.data;
- grid.getStore().setData(data);
- // re-align after store load, else the window is not centered
- me.alignTo(Ext.getBody(), 'c-c');
- },
- });
- },
-});
-
-Ext.define('PVE.node.CephOsdTree', {
- extend: 'Ext.tree.Panel',
- alias: ['widget.pveNodeCephOsdTree'],
- onlineHelp: 'chapter_pveceph',
-
- viewModel: {
- data: {
- nodename: '',
- flags: [],
- maxversion: '0',
- mixedversions: false,
- versions: {},
- isOsd: false,
- downOsd: false,
- upOsd: false,
- inOsd: false,
- outOsd: false,
- osdid: '',
- osdhost: '',
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- reload: function () {
- let me = this;
- let view = me.getView();
- let vm = me.getViewModel();
- let nodename = vm.get('nodename');
- let sm = view.getSelectionModel();
- Proxmox.Utils.API2Request({
- url: '/nodes/' + nodename + '/ceph/osd',
- waitMsgTarget: view,
- method: 'GET',
- failure: function (response, opts) {
- let msg = response.htmlStatus;
- PVE.Utils.showCephInstallOrMask(view, msg, nodename, (win) =>
- view.mon(win, 'cephInstallWindowClosed', () => {
- me.reload();
- }),
- );
- },
- success: function (response, opts) {
- let data = response.result.data;
- let selected = view.getSelection();
- let name;
- if (selected.length) {
- name = selected[0].data.name;
- }
- data.versions = data.versions || {};
- vm.set('versions', data.versions);
- // extract max version
- let maxversion = '0';
- let mixedversions = false;
- let traverse;
- traverse = function (node, fn) {
- fn(node);
- if (Array.isArray(node.children)) {
- node.children.forEach((c) => {
- traverse(c, fn);
- });
- }
- };
- traverse(data.root, (node) => {
- // compatibility for old api call
- if (node.type === 'host' && !node.version) {
- node.version = data.versions[node.name];
- }
-
- if (node.version === undefined) {
- return;
- }
-
- if (
- PVE.Utils.compare_ceph_versions(node.version, maxversion) !== 0 &&
- maxversion !== '0'
- ) {
- mixedversions = true;
- }
-
- if (PVE.Utils.compare_ceph_versions(node.version, maxversion) > 0) {
- maxversion = node.version;
- }
- });
- vm.set('maxversion', maxversion);
- vm.set('mixedversions', mixedversions);
- sm.deselectAll();
- view.setRootNode(data.root);
- view.expandAll();
- if (name) {
- let node = view.getRootNode().findChild('name', name, true);
- if (node) {
- view.setSelection([node]);
- }
- }
-
- let flags = data.flags.split(',');
- vm.set('flags', flags);
- },
- });
- },
-
- osd_cmd: function (comp) {
- let me = this;
- let vm = this.getViewModel();
- let cmd = comp.cmd;
- let params = comp.params || {};
- let osdid = vm.get('osdid');
-
- let doRequest = function () {
- let targetnode = vm.get('osdhost');
- // cmds not node specific and need to work if the OSD node is down
- if (['in', 'out'].includes(cmd)) {
- targetnode = vm.get('nodename');
- }
- Proxmox.Utils.API2Request({
- url: `/nodes/${targetnode}/ceph/osd/${osdid}/${cmd}`,
- waitMsgTarget: me.getView(),
- method: 'POST',
- params: params,
- success: () => {
- me.reload();
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- };
-
- if (cmd === 'scrub') {
- Ext.MessageBox.defaultButton = params.deep === 1 ? 2 : 1;
- Ext.Msg.show({
- title: gettext('Confirm'),
- icon: params.deep === 1 ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
- msg:
- params.deep !== 1
- ? Ext.String.format(gettext('Scrub OSD.{0}'), osdid)
- : Ext.String.format(gettext('Deep Scrub OSD.{0}'), osdid) +
- ' Caution: This can reduce performance while it is running.',
- buttons: Ext.Msg.YESNO,
- callback: function (btn) {
- if (btn !== 'yes') {
- return;
- }
- doRequest();
- },
- });
- } else {
- doRequest();
- }
- },
-
- create_osd: function () {
- let me = this;
- let vm = this.getViewModel();
- Ext.create('PVE.CephCreateOsd', {
- nodename: vm.get('nodename'),
- taskDone: () => {
- me.reload();
- },
- }).show();
- },
-
- destroy_osd: async function () {
- let me = this;
- let vm = this.getViewModel();
-
- let warnings = {
- flags: false,
- degraded: false,
- };
-
- let flagsPromise = Proxmox.Async.api2({
- url: `/cluster/ceph/flags`,
- method: 'GET',
- });
-
- let statusPromise = Proxmox.Async.api2({
- url: `/cluster/ceph/status`,
- method: 'GET',
- });
-
- me.getView().mask(gettext('Loading...'));
-
- try {
- let result = await Promise.all([flagsPromise, statusPromise]);
-
- let flagsData = result[0].result.data;
- let statusData = result[1].result.data;
-
- let flags = Array.from(
- flagsData.filter((v) => v.value),
- (v) => v.name,
- ).filter((v) => ['norebalance', 'norecover', 'noout'].includes(v));
-
- if (flags.length) {
- warnings.flags = true;
- }
- if (Object.keys(statusData.pgmap).includes('degraded_objects')) {
- warnings.degraded = true;
- }
- } catch (error) {
- Ext.Msg.alert(gettext('Error'), error.htmlStatus);
- me.getView().unmask();
- return;
- }
-
- me.getView().unmask();
- Ext.create('PVE.CephRemoveOsd', {
- nodename: vm.get('osdhost'),
- osdid: vm.get('osdid'),
- warnings: warnings,
- taskDone: () => {
- me.reload();
- },
- autoShow: true,
- });
- },
-
- set_flags: function () {
- let me = this;
- let vm = this.getViewModel();
- Ext.create('PVE.CephSetFlags', {
- nodename: vm.get('nodename'),
- taskDone: () => {
- me.reload();
- },
- }).show();
- },
-
- service_cmd: function (comp) {
- let me = this;
- let vm = this.getViewModel();
- let cmd = comp.cmd || comp;
-
- let doRequest = function () {
- Proxmox.Utils.API2Request({
- url: `/nodes/${vm.get('osdhost')}/ceph/${cmd}`,
- params: { service: 'osd.' + vm.get('osdid') },
- waitMsgTarget: me.getView(),
- method: 'POST',
- success: function (response, options) {
- let upid = response.result.data;
- let win = Ext.create('Proxmox.window.TaskProgress', {
- upid: upid,
- taskDone: () => {
- me.reload();
- },
- });
- win.show();
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- };
-
- if (cmd === 'stop') {
- Proxmox.Utils.API2Request({
- url: `/nodes/${vm.get('osdhost')}/ceph/cmd-safety`,
- params: {
- service: 'osd',
- id: vm.get('osdid'),
- action: 'stop',
- },
- waitMsgTarget: me.getView(),
- method: 'GET',
- success: function ({ result: { data } }) {
- if (!data.safe) {
- Ext.Msg.show({
- title: ngettext('Warning', 'Warnings', 1),
- message: data.status,
- icon: Ext.Msg.WARNING,
- buttons: Ext.Msg.OKCANCEL,
- buttonText: { ok: gettext('Stop OSD') },
- fn: function (selection) {
- if (selection === 'ok') {
- doRequest();
- }
- },
- });
- } else {
- doRequest();
- }
- },
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- } else {
- doRequest();
- }
- },
-
- run_details: function (view, rec) {
- if (rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0) {
- this.details();
- }
- },
-
- details: function () {
- let vm = this.getViewModel();
- Ext.create('PVE.CephOsdDetails', {
- nodename: vm.get('osdhost'),
- osdid: vm.get('osdid'),
- }).show();
- },
-
- set_selection_status: function (tp, selection) {
- if (selection.length < 1) {
- return;
- }
- let rec = selection[0];
- let vm = this.getViewModel();
-
- let isOsd = rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0;
-
- vm.set('isOsd', isOsd);
- vm.set('downOsd', isOsd && rec.data.status === 'down');
- vm.set('upOsd', isOsd && rec.data.status !== 'down');
- vm.set('inOsd', isOsd && rec.data.in);
- vm.set('outOsd', isOsd && !rec.data.in);
- vm.set('osdid', isOsd ? rec.data.id : undefined);
- vm.set('osdhost', isOsd ? rec.data.host : undefined);
- },
-
- render_status: function (value, metaData, rec) {
- if (!value) {
- return value;
- }
- let inout = rec.data.in ? 'in' : 'out';
- let updownicon =
- value === 'up' ? 'good fa-arrow-circle-up' : 'critical fa-arrow-circle-down';
-
- let inouticon = rec.data.in ? 'good fa-circle' : 'warning fa-circle-o';
-
- let text =
- value +
- ' / ' +
- inout +
- ' ';
-
- return text;
- },
-
- render_wal: function (value, metaData, rec) {
- if (!value && rec.data.osdtype === 'bluestore' && rec.data.type === 'osd') {
- return 'N/A';
- }
- return value;
- },
-
- render_version: function (value, metadata, rec) {
- let vm = this.getViewModel();
- let versions = vm.get('versions');
- let icon = '';
- let version = value || '';
- let maxversion = vm.get('maxversion');
- if (value && PVE.Utils.compare_ceph_versions(value, maxversion) !== 0) {
- let host_version = rec.parentNode?.data?.version || versions[rec.data.host] || '';
- if (
- rec.data.type === 'host' ||
- PVE.Utils.compare_ceph_versions(host_version, maxversion) !== 0
- ) {
- icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE');
- } else {
- icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
- }
- } else if (value && vm.get('mixedversions')) {
- icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK');
- }
-
- return icon + version;
- },
-
- render_osd_val: function (value, metaData, rec) {
- return rec.data.type === 'osd' ? value : '';
- },
- render_osd_weight: function (value, metaData, rec) {
- if (rec.data.type !== 'osd') {
- return '';
- }
- return Ext.util.Format.number(value, '0.00###');
- },
-
- render_osd_latency: function (value, metaData, rec) {
- if (rec.data.type !== 'osd') {
- return '';
- }
- let commit_ms = rec.data.commit_latency_ms,
- apply_ms = rec.data.apply_latency_ms;
- return apply_ms + ' / ' + commit_ms;
- },
-
- render_osd_size: function (value, metaData, rec) {
- return this.render_osd_val(Proxmox.Utils.render_size(value), metaData, rec);
- },
-
- control: {
- '#': {
- selectionchange: 'set_selection_status',
- },
- },
-
- init: function (view) {
- let me = this;
- let vm = this.getViewModel();
-
- if (!view.pveSelNode.data.node) {
- throw 'no node name specified';
- }
-
- vm.set('nodename', view.pveSelNode.data.node);
-
- me.callParent();
- me.reload();
- },
- },
-
- stateful: true,
- stateId: 'grid-ceph-osd',
- rootVisible: false,
- useArrows: true,
- listeners: {
- itemdblclick: 'run_details',
- },
-
- columns: [
- {
- xtype: 'treecolumn',
- text: 'Name',
- dataIndex: 'name',
- width: 150,
- },
- {
- text: 'Type',
- dataIndex: 'type',
- hidden: true,
- align: 'right',
- width: 75,
- },
- {
- text: gettext('Class'),
- dataIndex: 'device_class',
- align: 'right',
- width: 75,
- renderer: Ext.htmlEncode,
- },
- {
- text: 'OSD Type',
- dataIndex: 'osdtype',
- align: 'right',
- width: 100,
- },
- {
- text: 'Bluestore Device',
- dataIndex: 'blfsdev',
- align: 'right',
- width: 75,
- hidden: true,
- renderer: Ext.htmlEncode,
- },
- {
- text: 'DB Device',
- dataIndex: 'dbdev',
- align: 'right',
- width: 75,
- hidden: true,
- renderer: Ext.htmlEncode,
- },
- {
- text: 'WAL Device',
- dataIndex: 'waldev',
- align: 'right',
- renderer: 'render_wal',
- width: 75,
- hidden: true,
- },
- {
- text: 'Status',
- dataIndex: 'status',
- align: 'right',
- renderer: 'render_status',
- width: 120,
- },
- {
- text: gettext('Version'),
- dataIndex: 'version',
- align: 'right',
- renderer: 'render_version',
- },
- {
- text: 'weight',
- dataIndex: 'crush_weight',
- align: 'right',
- renderer: 'render_osd_weight',
- width: 90,
- },
- {
- text: 'reweight',
- dataIndex: 'reweight',
- align: 'right',
- renderer: 'render_osd_weight',
- width: 90,
- },
- {
- text: gettext('Used') + ' (%)',
- dataIndex: 'percent_used',
- align: 'right',
- renderer: function (value, metaData, rec) {
- if (rec.data.type !== 'osd') {
- return '';
- }
- return Ext.util.Format.number(value, '0.00');
- },
- width: 100,
- },
- {
- text: gettext('Total'),
- dataIndex: 'total_space',
- align: 'right',
- renderer: 'render_osd_size',
- width: 100,
- },
- {
- text: 'Apply/Commit Latency (ms)',
- dataIndex: 'apply_latency_ms',
- align: 'right',
- renderer: 'render_osd_latency',
- width: 120,
- },
- {
- text: 'PGs',
- dataIndex: 'pgs',
- align: 'right',
- renderer: 'render_osd_val',
- width: 90,
- },
- ],
-
- tbar: {
- items: [
- {
- text: gettext('Reload'),
- iconCls: 'fa fa-refresh',
- handler: 'reload',
- },
- '-',
- {
- text: gettext('Create: OSD'),
- handler: 'create_osd',
- },
- {
- text: gettext('Manage Global Flags'),
- handler: 'set_flags',
- },
- '->',
- {
- xtype: 'tbtext',
- data: {
- osd: undefined,
- },
- bind: {
- data: {
- osd: '{osdid}',
- },
- },
- tpl: [
- '',
- 'osd.{osd}:',
- '',
- gettext('No OSD selected'),
- ' ',
- ],
- },
- {
- text: gettext('Details'),
- iconCls: 'fa fa-info-circle',
- disabled: true,
- bind: {
- disabled: '{!isOsd}',
- },
- handler: 'details',
- },
- {
- text: gettext('Start'),
- iconCls: 'fa fa-play',
- disabled: true,
- bind: {
- disabled: '{!downOsd}',
- },
- cmd: 'start',
- handler: 'service_cmd',
- },
- {
- text: gettext('Stop'),
- iconCls: 'fa fa-stop',
- disabled: true,
- bind: {
- disabled: '{!upOsd}',
- },
- cmd: 'stop',
- handler: 'service_cmd',
- },
- {
- text: gettext('Restart'),
- iconCls: 'fa fa-refresh',
- disabled: true,
- bind: {
- disabled: '{!upOsd}',
- },
- cmd: 'restart',
- handler: 'service_cmd',
- },
- '-',
- {
- text: 'Out',
- iconCls: 'fa fa-circle-o',
- disabled: true,
- bind: {
- disabled: '{!inOsd}',
- },
- cmd: 'out',
- handler: 'osd_cmd',
- },
- {
- text: 'In',
- iconCls: 'fa fa-circle',
- disabled: true,
- bind: {
- disabled: '{!outOsd}',
- },
- cmd: 'in',
- handler: 'osd_cmd',
- },
- '-',
- {
- text: gettext('More'),
- iconCls: 'fa fa-bars',
- disabled: true,
- bind: {
- disabled: '{!isOsd}',
- },
- menu: [
- {
- text: gettext('Scrub'),
- iconCls: 'fa fa-shower',
- cmd: 'scrub',
- handler: 'osd_cmd',
- },
- {
- text: gettext('Deep Scrub'),
- iconCls: 'fa fa-bath',
- cmd: 'scrub',
- params: {
- deep: 1,
- },
- handler: 'osd_cmd',
- },
- {
- text: gettext('Destroy'),
- itemId: 'remove',
- iconCls: 'fa fa-fw fa-trash-o',
- bind: {
- disabled: '{!downOsd}',
- },
- handler: 'destroy_osd',
- },
- ],
- },
- ],
- },
-
- fields: [
- 'name',
- 'type',
- 'status',
- 'host',
- 'in',
- 'id',
- { type: 'number', name: 'reweight' },
- { type: 'number', name: 'percent_used' },
- { type: 'integer', name: 'bytes_used' },
- { type: 'integer', name: 'total_space' },
- { type: 'integer', name: 'apply_latency_ms' },
- { type: 'integer', name: 'commit_latency_ms' },
- { type: 'string', name: 'device_class' },
- { type: 'string', name: 'osdtype' },
- { type: 'string', name: 'blfsdev' },
- { type: 'string', name: 'dbdev' },
- { type: 'string', name: 'waldev' },
- {
- type: 'string',
- name: 'version',
- calculate: function (data) {
- return PVE.Utils.parse_ceph_version(data);
- },
- },
- {
- type: 'string',
- name: 'iconCls',
- calculate: function (data) {
- let iconMap = {
- host: 'fa-building',
- osd: 'fa-hdd-o',
- root: 'fa-server',
- };
- return `fa x-fa-tree ${iconMap[data.type] ?? 'fa-folder-o'}`;
- },
- },
- { type: 'number', name: 'crush_weight' },
- ],
-});
-Ext.define('pve-osd-details-devices', {
- extend: 'Ext.data.Model',
- fields: ['device', 'type', 'physical_device', 'size', 'support_discard', 'dev_node'],
- idProperty: 'device',
-});
-
-Ext.define('PVE.CephOsdDetails', {
- extend: 'Ext.window.Window',
- alias: ['widget.pveCephOsdDetails'],
-
- mixins: ['Proxmox.Mixin.CBind'],
-
- cbindData: function () {
- let me = this;
- me.baseUrl = `/nodes/${me.nodename}/ceph/osd/${me.osdid}`;
- return {
- title: `${gettext('Details')}: OSD ${me.osdid}`,
- };
- },
-
- viewModel: {
- data: {
- device: '',
- },
- },
-
- modal: true,
- width: 650,
- minHeight: 250,
- resizable: true,
- cbind: {
- title: '{title}',
- },
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- defaults: {
- layout: 'fit',
- border: false,
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- reload: function () {
- let view = this.getView();
-
- Proxmox.Utils.API2Request({
- url: `${view.baseUrl}/metadata`,
- waitMsgTarget: view.lookup('detailsTabs'),
- method: 'GET',
- failure: function (response, opts) {
- Proxmox.Utils.setErrorMask(view.lookup('detailsTabs'), response.htmlStatus);
- },
- success: function (response, opts) {
- let d = response.result.data;
- let osdData = Object.keys(d.osd)
- .sort()
- .map((x) => ({ key: x, value: d.osd[x] }));
- view.osdStore.loadData(osdData);
- let devices = view.lookup('devices');
- let deviceStore = devices.getStore();
- deviceStore.loadData(d.devices);
-
- view.lookup('osdGeneral').rstore.fireEvent(
- 'load',
- view.osdStore,
- osdData,
- true,
- );
- view.lookup('osdNetwork').rstore.fireEvent(
- 'load',
- view.osdStore,
- osdData,
- true,
- );
-
- // select 'block' device automatically on first load
- if (devices.getSelection().length === 0) {
- devices.setSelection(deviceStore.findRecord('device', 'block'));
- }
- },
- });
- },
-
- showDevInfo: function (grid, selected) {
- let view = this.getView();
- if (selected[0]) {
- let device = selected[0].data.device;
- this.getViewModel().set('device', device);
-
- let detailStore = view.lookup('volumeDetails');
- detailStore.rstore.getProxy().setUrl(`api2/json${view.baseUrl}/lv-info`);
- detailStore.rstore.getProxy().setExtraParams({ type: device });
- detailStore.setLoading();
- detailStore.rstore.load({ callback: () => detailStore.setLoading(false) });
- }
- },
-
- init: function () {
- this.reload();
- },
-
- control: {
- 'grid[reference=devices]': {
- selectionchange: 'showDevInfo',
- },
- },
- },
- tbar: [
- {
- text: gettext('Reload'),
- iconCls: 'fa fa-refresh',
- handler: 'reload',
- },
- ],
- initComponent: function () {
- let me = this;
-
- me.osdStore = Ext.create('Proxmox.data.ObjectStore');
-
- Ext.applyIf(me, {
- items: [
- {
- xtype: 'tabpanel',
- reference: 'detailsTabs',
- items: [
- {
- xtype: 'proxmoxObjectGrid',
- reference: 'osdGeneral',
- tooltip: gettext('Various information about the OSD'),
- rstore: me.osdStore,
- title: gettext('General'),
- viewConfig: {
- enableTextSelection: true,
- },
- gridRows: [
- {
- xtype: 'text',
- name: 'version',
- text: gettext('Version'),
- },
- {
- xtype: 'text',
- name: 'hostname',
- text: gettext('Hostname'),
- },
- {
- xtype: 'text',
- name: 'osd_data',
- text: gettext('OSD data path'),
- },
- {
- xtype: 'text',
- name: 'osd_objectstore',
- text: gettext('OSD object store'),
- },
- {
- xtype: 'text',
- name: 'mem_usage',
- text: gettext('Memory usage (PSS)'),
- renderer: Proxmox.Utils.render_size,
- },
- {
- xtype: 'text',
- name: 'pid',
- text: `${gettext('Process ID')} (PID)`,
- },
- ],
- },
- {
- xtype: 'proxmoxObjectGrid',
- reference: 'osdNetwork',
- tooltip: gettext('Addresses and ports used by the OSD service'),
- rstore: me.osdStore,
- title: gettext('Network'),
- viewConfig: {
- enableTextSelection: true,
- },
- gridRows: [
- {
- xtype: 'text',
- name: 'front_addr',
- text: `${gettext('Front Address')} (Client & Monitor)`,
- renderer: PVE.Utils.render_ceph_osd_addr,
- },
- {
- xtype: 'text',
- name: 'hb_front_addr',
- text: gettext('Heartbeat Front Address'),
- renderer: PVE.Utils.render_ceph_osd_addr,
- },
- {
- xtype: 'text',
- name: 'back_addr',
- text: `${gettext('Back Address')} (OSD)`,
- renderer: PVE.Utils.render_ceph_osd_addr,
- },
- {
- xtype: 'text',
- name: 'hb_back_addr',
- text: gettext('Heartbeat Back Address'),
- renderer: PVE.Utils.render_ceph_osd_addr,
- },
- ],
- },
- {
- xtype: 'panel',
- title: gettext('Devices'),
- tooltip: gettext('Physical devices used by the OSD'),
- items: [
- {
- xtype: 'grid',
- border: false,
- reference: 'devices',
- store: {
- model: 'pve-osd-details-devices',
- },
- columns: {
- items: [
- { text: gettext('Device'), dataIndex: 'device' },
- { text: gettext('Type'), dataIndex: 'type' },
- {
- text: gettext('Physical Device'),
- dataIndex: 'physical_device',
- },
- {
- text: gettext('Size'),
- dataIndex: 'size',
- renderer: Proxmox.Utils.render_size,
- },
- {
- text: 'Discard',
- dataIndex: 'support_discard',
- hidden: true,
- },
- {
- text: gettext('Device node'),
- dataIndex: 'dev_node',
- hidden: true,
- },
- ],
- defaults: {
- tdCls: 'pointer',
- flex: 1,
- },
- },
- },
- {
- xtype: 'proxmoxObjectGrid',
- reference: 'volumeDetails',
- maskOnLoad: true,
- viewConfig: {
- enableTextSelection: true,
- },
- bind: {
- title: Ext.String.format(
- gettext('Volume Details for {0}'),
- '{device}',
- ),
- },
- rows: {
- creation_time: {
- header: gettext('Creation time'),
- },
- lv_name: {
- header: gettext('LV Name'),
- },
- lv_path: {
- header: gettext('LV Path'),
- },
- lv_uuid: {
- header: gettext('LV UUID'),
- },
- vg_name: {
- header: gettext('VG Name'),
- },
- },
- url: 'nodes/', //placeholder will be set when device is selected
- },
- ],
- },
- ],
- },
- ],
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.CephPoolInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveCephPoolInputPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- showProgress: true,
- onlineHelp: 'pve_ceph_pools',
-
- subject: 'Ceph Pool',
-
- defaultSize: undefined,
- defaultMinSize: undefined,
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- let vm = this.getViewModel();
- if (view.isCreate) {
- vm.set('size', Number(view.defaultSize));
- vm.set('minSize', Number(view.defaultMinSize));
- }
- },
- sizeChange: function (field, val) {
- let vm = this.getViewModel();
- let minSize = Math.round(val / 2);
- if (minSize > 1) {
- vm.set('minSize', minSize);
- }
- vm.set('size', val); // bind does not work in a pmxDisplayEditField, update manually
- },
- },
-
- viewModel: {
- data: {
- minSize: null,
- size: null,
- },
- formulas: {
- minSizeLabel: (get) => {
- if (get('showMinSizeOneWarning') || get('showMinSizeHalfWarning')) {
- return `${gettext('Min. Size')} `;
- }
- return gettext('Min. Size');
- },
- showMinSizeOneWarning: (get) => get('minSize') === 1,
- showMinSizeHalfWarning: (get) => {
- let minSize = get('minSize');
- let size = get('size');
- if (minSize === 1) {
- return false;
- }
- return minSize < size / 2 && minSize !== size;
- },
- },
- },
-
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- fieldLabel: gettext('Name'),
- cbind: {
- editable: '{isCreate}',
- value: '{pool_name}',
- },
- name: 'name',
- allowBlank: false,
- },
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: '{!isErasure}',
- },
- fieldLabel: gettext('Size'),
- name: 'size',
- editConfig: {
- xtype: 'proxmoxintegerfield',
- cbind: {
- value: (get) => get('defaultSize'),
- },
- minValue: 2,
- maxValue: 7,
- allowBlank: false,
- listeners: {
- change: 'sizeChange',
- },
- },
- },
- ],
- column2: [
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('PG Autoscaler Mode'),
- name: 'pg_autoscale_mode',
- comboItems: [
- ['warn', 'warn'],
- ['on', 'on'],
- ['off', 'off'],
- ],
- value: 'on', // FIXME: check ceph version and only default to on on octopus and newer
- allowBlank: false,
- autoSelect: false,
- labelWidth: 140,
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Add as Storage'),
- cbind: {
- value: '{isCreate}',
- hidden: '{!isCreate}',
- },
- name: 'add_storages',
- labelWidth: 140,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Add the new pool to the cluster storage configuration.'),
- },
- },
- ],
- advancedColumn1: [
- {
- xtype: 'proxmoxintegerfield',
- bind: {
- fieldLabel: '{minSizeLabel}',
- value: '{minSize}',
- },
- name: 'min_size',
- cbind: {
- value: (get) => get('defaultMinSize'),
- minValue: (get) => {
- if (Number(get('defaultMinSize')) === 1) {
- return 1;
- } else {
- return get('isCreate') ? 2 : 1;
- }
- },
- },
- maxValue: 7,
- allowBlank: false,
- },
- {
- xtype: 'displayfield',
- bind: {
- hidden: '{!showMinSizeHalfWarning}',
- },
- hidden: true,
- userCls: 'pmx-hint',
- value: gettext(
- 'min_size < size/2 can lead to data loss, incomplete PGs or unfound objects.',
- ),
- },
- {
- xtype: 'displayfield',
- bind: {
- hidden: '{!showMinSizeOneWarning}',
- },
- hidden: true,
- userCls: 'pmx-hint',
- value: gettext('a min_size of 1 is not recommended and can lead to data loss'),
- },
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: '{!isErasure}',
- nodename: '{nodename}',
- isCreate: '{isCreate}',
- },
- fieldLabel: 'Crush Rule', // do not localize
- name: 'crush_rule',
- editConfig: {
- xtype: 'pveCephRuleSelector',
- allowBlank: false,
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: '# of PGs',
- name: 'pg_num',
- value: 128,
- minValue: 1,
- maxValue: 32768,
- allowBlank: false,
- emptyText: 128,
- },
- ],
- advancedColumn2: [
- {
- xtype: 'numberfield',
- fieldLabel: gettext('Target Ratio'),
- name: 'target_size_ratio',
- minValue: 0,
- decimalPrecision: 3,
- allowBlank: true,
- emptyText: '0.0',
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'The ratio of storage amount this pool will consume compared to other pools with ratios. Used for auto-scaling.',
- ),
- },
- },
- {
- xtype: 'pveSizeField',
- name: 'target_size',
- fieldLabel: gettext('Target Size'),
- unit: 'GiB',
- minValue: 0,
- allowBlank: true,
- allowZero: true,
- emptyText: '0',
- emptyValue: 0,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'The amount of data eventually stored in this pool. Used for auto-scaling.',
- ),
- },
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: Ext.String.format(gettext('{0} takes precedence.'), gettext('Target Ratio')), // FIXME: tooltip?
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: 'Min. # of PGs',
- name: 'pg_num_min',
- minValue: 0,
- allowBlank: true,
- emptyText: '0',
- },
- ],
-
- onGetValues: function (values) {
- Object.keys(values || {}).forEach(function (name) {
- if (values[name] === '') {
- delete values[name];
- }
- });
-
- return values;
- },
-});
-
-Ext.define('PVE.Ceph.PoolEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveCephPoolEdit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- cbindData: {
- pool_name: '',
- isCreate: (cfg) => !cfg.pool_name,
- defaultSize: undefined,
- defaultMinSize: undefined,
- },
-
- cbind: {
- autoLoad: (get) => !get('isCreate'),
- url: (get) =>
- get('isCreate')
- ? `/nodes/${get('nodename')}/ceph/pool`
- : `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}`,
- loadUrl: (get) => `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}/status`,
- method: (get) => (get('isCreate') ? 'POST' : 'PUT'),
- },
-
- showProgress: true,
-
- subject: gettext('Ceph Pool'),
-
- items: [
- {
- xtype: 'pveCephPoolInputPanel',
- cbind: {
- nodename: '{nodename}',
- pool_name: '{pool_name}',
- isErasure: '{isErasure}',
- isCreate: '{isCreate}',
- defaultSize: '{defaultSize}',
- defaultMinSize: '{defaultMinSize}',
- },
- },
- ],
-});
-
-Ext.define(
- 'PVE.node.Ceph.PoolList',
- {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveNodeCephPoolList',
-
- onlineHelp: 'chapter_pveceph',
-
- stateful: true,
- stateId: 'grid-ceph-pools',
- bufferedRenderer: false,
-
- features: [{ ftype: 'summary' }],
-
- columns: [
- {
- text: gettext('Pool #'),
- minWidth: 70,
- flex: 1,
- align: 'right',
- sortable: true,
- dataIndex: 'pool',
- },
- {
- text: gettext('Name'),
- minWidth: 120,
- flex: 2,
- sortable: true,
- dataIndex: 'pool_name',
- renderer: Ext.htmlEncode,
- },
- {
- text: gettext('Type'),
- minWidth: 100,
- flex: 1,
- dataIndex: 'type',
- hidden: true,
- },
- {
- text: gettext('Application'),
- minWidth: 100,
- flex: 1,
- dataIndex: 'application_metadata',
- hidden: true,
- renderer: (v, _meta, _rec) => Ext.htmlEncode(Object.keys(v).toString()),
- },
- {
- text: gettext('Size') + '/min',
- minWidth: 100,
- flex: 1,
- align: 'right',
- renderer: (v, meta, rec) => `${v}/${rec.data.min_size}`,
- dataIndex: 'size',
- },
- {
- text: '# of Placement Groups',
- flex: 1,
- minWidth: 100,
- align: 'right',
- dataIndex: 'pg_num',
- },
- {
- text: gettext('Optimal # of PGs'),
- flex: 1,
- minWidth: 100,
- align: 'right',
- dataIndex: 'pg_num_final',
- renderer: function (value, metaData) {
- if (!value) {
- value = ' n/a';
- metaData.tdAttr = 'data-qtip="Needs pg_autoscaler module enabled."';
- }
- return value;
- },
- },
- {
- text: gettext('Min. # of PGs'),
- flex: 1,
- minWidth: 100,
- align: 'right',
- dataIndex: 'pg_num_min',
- hidden: true,
- },
- {
- text: gettext('Target Ratio'),
- flex: 1,
- minWidth: 100,
- align: 'right',
- dataIndex: 'target_size_ratio',
- renderer: Ext.util.Format.numberRenderer('0.0000'),
- hidden: true,
- },
- {
- text: gettext('Target Size'),
- flex: 1,
- minWidth: 100,
- align: 'right',
- dataIndex: 'target_size',
- hidden: true,
- renderer: function (v, metaData, rec) {
- let value = Proxmox.Utils.render_size(v);
- if (rec.data.target_size_ratio > 0) {
- value = ' ' + value;
- metaData.tdAttr =
- 'data-qtip="Target Size Ratio takes precedence over Target Size."';
- }
- return value;
- },
- },
- {
- text: gettext('Autoscaler Mode'),
- flex: 1,
- minWidth: 100,
- align: 'right',
- dataIndex: 'pg_autoscale_mode',
- },
- {
- text: 'CRUSH Rule (ID)',
- flex: 1,
- align: 'right',
- minWidth: 150,
- renderer: (v, meta, rec) => Ext.htmlEncode(`${v} (${rec.data.crush_rule})`),
- dataIndex: 'crush_rule_name',
- },
- {
- text: gettext('Used') + ' (%)',
- flex: 1,
- minWidth: 150,
- sortable: true,
- align: 'right',
- dataIndex: 'bytes_used',
- summaryType: 'sum',
- summaryRenderer: Proxmox.Utils.render_size,
- renderer: function (v, meta, rec) {
- let percentage = Ext.util.Format.percent(rec.data.percent_used, '0.00');
- let used = Proxmox.Utils.render_size(v);
- return `${used} (${percentage})`;
- },
- },
- ],
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var rstore = Ext.create('Proxmox.data.UpdateStore', {
- interval: 3000,
- storeid: 'ceph-pool-list' + nodename,
- model: 'ceph-pool-list',
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${nodename}/ceph/pool`,
- },
- });
- let store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore });
-
- // manages the "install ceph?" overlay
- PVE.Utils.monitor_ceph_installed(me, rstore, nodename);
-
- var run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec || !rec.data.pool_name) {
- return;
- }
- Ext.create('PVE.Ceph.PoolEdit', {
- title: gettext('Edit') + ': Ceph Pool',
- nodename: nodename,
- pool_name: rec.data.pool_name,
- isErasure: rec.data.type === 'erasure',
- autoShow: true,
- listeners: {
- destroy: () => rstore.load(),
- },
- });
- };
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: [
- {
- text: gettext('Create'),
- handler: function () {
- let keys = [
- 'global:osd-pool-default-min-size',
- 'global:osd-pool-default-size',
- ];
- let params = {
- 'config-keys': keys.join(';'),
- };
-
- Proxmox.Utils.API2Request({
- url: '/nodes/localhost/ceph/cfg/value',
- method: 'GET',
- params,
- waitMsgTarget: me.getView(),
- failure: (response) =>
- Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: function ({ result: { data } }) {
- let global = data.global;
- let defaultSize = global?.['osd-pool-default-size'] ?? 3;
- let defaultMinSize = global?.['osd-pool-default-min-size'] ?? 2;
-
- Ext.create('PVE.Ceph.PoolEdit', {
- title: gettext('Create') + ': Ceph Pool',
- isCreate: true,
- isErasure: false,
- defaultSize,
- defaultMinSize,
- nodename: nodename,
- autoShow: true,
- listeners: {
- destroy: () => rstore.load(),
- },
- });
- },
- });
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- selModel: sm,
- disabled: true,
- handler: run_editor,
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Destroy'),
- selModel: sm,
- disabled: true,
- handler: function () {
- let rec = sm.getSelection()[0];
- if (!rec || !rec.data.pool_name) {
- return;
- }
- let poolName = rec.data.pool_name;
- Ext.create('Proxmox.window.SafeDestroy', {
- showProgress: true,
- url: `/nodes/${nodename}/ceph/pool/${poolName}`,
- params: {
- remove_storages: 1,
- },
- item: {
- type: 'CephPool',
- id: poolName,
- },
- taskName: 'cephdestroypool',
- autoShow: true,
- listeners: {
- destroy: () => rstore.load(),
- },
- });
- },
- },
- ],
- listeners: {
- activate: () => rstore.startUpdate(),
- destroy: () => rstore.stopUpdate(),
- itemdblclick: run_editor,
- },
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('ceph-pool-list', {
- extend: 'Ext.data.Model',
- fields: [
- 'pool_name',
- { name: 'pool', type: 'integer' },
- { name: 'size', type: 'integer' },
- { name: 'min_size', type: 'integer' },
- { name: 'pg_num', type: 'integer' },
- { name: 'pg_num_min', type: 'integer' },
- { name: 'bytes_used', type: 'integer' },
- { name: 'percent_used', type: 'number' },
- { name: 'crush_rule', type: 'integer' },
- { name: 'crush_rule_name', type: 'string' },
- { name: 'pg_autoscale_mode', type: 'string' },
- { name: 'pg_num_final', type: 'integer' },
- { name: 'target_size_ratio', type: 'number' },
- { name: 'target_size', type: 'integer' },
- ],
- idProperty: 'pool_name',
- });
- },
-);
-
-Ext.define('PVE.form.CephRuleSelector', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveCephRuleSelector',
-
- allowBlank: false,
- valueField: 'name',
- displayField: 'name',
- editable: false,
- queryMode: 'local',
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- me.originalAllowBlank = me.allowBlank;
- me.allowBlank = true;
-
- Ext.apply(me, {
- store: {
- fields: ['name'],
- sorters: 'name',
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/ceph/rules`,
- },
- autoLoad: {
- callback: (records, op, success) => {
- if (me.isCreate && success && records.length > 0) {
- me.select(records[0]);
- }
-
- me.allowBlank = me.originalAllowBlank;
- delete me.originalAllowBlank;
- me.validate();
- },
- },
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.CephCreateService', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
- xtype: 'pveCephCreateService',
-
- method: 'POST',
- isCreate: true,
- showProgress: true,
- width: 450,
-
- setNode: function (node) {
- let me = this;
- me.nodename = node;
- me.updateUrl();
- },
- setServiceID: function (value) {
- let me = this;
- me.serviceID = value;
- me.updateUrl();
- },
- updateUrl: function () {
- let me = this;
- let node = me.nodename;
- let serviceID = me.serviceID ?? me.nodename;
-
- me.url = `/nodes/${node}/ceph/${me.type}/${serviceID}`;
- },
-
- defaults: {
- labelWidth: 75,
- },
- items: [
- {
- xtype: 'pveNodeSelector',
- fieldLabel: gettext('Host'),
- selectCurNode: true,
- allowBlank: false,
- submitValue: false,
- listeners: {
- change: function (f, value) {
- let view = this.up('pveCephCreateService');
- view.lookup('mds-id').setValue(value);
- view.setNode(value);
- },
- },
- },
- {
- xtype: 'textfield',
- reference: 'mds-id',
- fieldLabel: gettext('MDS ID'),
- regex: /^([a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?)$/,
- regexText: gettext(
- 'ID may consist of alphanumeric characters and hyphen. It cannot start with a number or end in a hyphen.',
- ),
- submitValue: false,
- allowBlank: false,
- cbind: {
- disabled: (get) => get('type') !== 'mds',
- hidden: (get) => get('type') !== 'mds',
- },
- listeners: {
- change: function (f, value) {
- let view = this.up('pveCephCreateService');
- view.setServiceID(value);
- },
- },
- },
- {
- xtype: 'component',
- border: false,
- padding: '5 2',
- style: {
- fontSize: '12px',
- },
- userCls: 'pmx-hint',
- cbind: {
- hidden: (get) => get('type') !== 'mds',
- },
- html: gettext(
- 'By using different IDs, you can have multiple MDS per node, which increases redundancy with more than one CephFS.',
- ),
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
- if (!me.type) {
- throw 'no type specified';
- }
- me.setNode(me.nodename);
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.node.CephServiceController', {
- extend: 'Ext.app.ViewController',
- alias: 'controller.CephServiceList',
-
- render_status: (value, metadata, rec) => Ext.htmlEncode(value),
-
- render_version: function (value, metadata, rec) {
- if (value === undefined) {
- return '';
- }
- let view = this.getView();
- let host = rec.data.host,
- nodev = [0];
- if (view.nodeversions[host] !== undefined) {
- nodev = view.nodeversions[host].version.parts;
- }
-
- let icon = '';
- if (PVE.Utils.compare_ceph_versions(view.maxversion, nodev) > 0) {
- icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE');
- } else if (PVE.Utils.compare_ceph_versions(nodev, value) > 0) {
- icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
- } else if (view.mixedversions) {
- icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK');
- }
- return icon + value;
- },
-
- getMaxVersions: function (store, records, success) {
- if (!success || records.length < 1) {
- return;
- }
- let me = this;
- let view = me.getView();
-
- view.nodeversions = records[0].data.node;
- view.maxversion = [];
- view.mixedversions = false;
- for (const [_nodename, data] of Object.entries(view.nodeversions)) {
- let res = PVE.Utils.compare_ceph_versions(data.version.parts, view.maxversion);
- if (res !== 0 && view.maxversion.length > 0) {
- view.mixedversions = true;
- }
- if (res > 0) {
- view.maxversion = data.version.parts;
- }
- }
- },
-
- init: function (view) {
- if (view.pveSelNode) {
- view.nodename = view.pveSelNode.data.node;
- }
- if (!view.nodename) {
- throw 'no node name specified';
- }
-
- if (!view.type) {
- throw 'no type specified';
- }
-
- view.versionsstore = Ext.create('Proxmox.data.UpdateStore', {
- autoStart: true,
- interval: 10000,
- storeid: `ceph-versions-${view.type}-list${view.nodename}`,
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/ceph/metadata?scope=versions',
- },
- });
- view.versionsstore.on('load', this.getMaxVersions, this);
- view.on('destroy', view.versionsstore.stopUpdate);
-
- view.rstore = Ext.create('Proxmox.data.UpdateStore', {
- autoStart: true,
- interval: 3000,
- storeid: `ceph-${view.type}-list${view.nodename}`,
- model: 'ceph-service-list',
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${view.nodename}/ceph/${view.type}`,
- },
- });
-
- view.setStore(
- Ext.create('Proxmox.data.DiffStore', {
- rstore: view.rstore,
- sorters: [{ property: 'name' }],
- }),
- );
-
- if (view.storeLoadCallback) {
- view.rstore.on('load', view.storeLoadCallback, this);
- }
- view.on('destroy', view.rstore.stopUpdate);
-
- if (view.showCephInstallMask) {
- PVE.Utils.monitor_ceph_installed(view, view.rstore, view.nodename, true);
- }
- },
-
- service_cmd: function (rec, cmd) {
- let view = this.getView();
- if (!rec.data.host) {
- Ext.Msg.alert(gettext('Error'), 'entry has no host');
- return;
- }
- let doRequest = function () {
- Proxmox.Utils.API2Request({
- url: `/nodes/${rec.data.host}/ceph/${cmd}`,
- method: 'POST',
- params: { service: view.type + '.' + rec.data.name },
- success: function (response, options) {
- Ext.create('Proxmox.window.TaskProgress', {
- autoShow: true,
- upid: response.result.data,
- taskDone: () => view.rstore.load(),
- });
- },
- failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- };
- if (cmd === 'stop' && ['mon', 'mds'].includes(view.type)) {
- Proxmox.Utils.API2Request({
- url: `/nodes/${rec.data.host}/ceph/cmd-safety`,
- params: {
- service: view.type,
- id: rec.data.name,
- action: 'stop',
- },
- method: 'GET',
- success: function ({ result: { data } }) {
- let stopText = {
- mon: gettext('Stop MON'),
- mds: gettext('Stop MDS'),
- };
- if (!data.safe) {
- Ext.Msg.show({
- title: ngettext('Warning', 'Warnings', 1),
- message: data.status,
- icon: Ext.Msg.WARNING,
- buttons: Ext.Msg.OKCANCEL,
- buttonText: { ok: stopText[view.type] },
- fn: function (selection) {
- if (selection === 'ok') {
- doRequest();
- }
- },
- });
- } else {
- doRequest();
- }
- },
- failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- } else {
- doRequest();
- }
- },
- onChangeService: function (button) {
- let me = this;
- let record = me.getView().getSelection()[0];
- me.service_cmd(record, button.action);
- },
-
- showSyslog: function () {
- let view = this.getView();
- let rec = view.getSelection()[0];
- let service = `ceph-${view.type}@${rec.data.name}`;
- Ext.create('Ext.window.Window', {
- title: `${gettext('Syslog')}: ${service}`,
- autoShow: true,
- modal: true,
- width: 800,
- height: 400,
- layout: 'fit',
- items: [
- {
- xtype: 'proxmoxLogView',
- url: `/api2/extjs/nodes/${rec.data.host}/syslog?service=${encodeURIComponent(service)}`,
- log_select_timespan: 1,
- },
- ],
- });
- },
-
- onCreate: function () {
- let view = this.getView();
- Ext.create('PVE.CephCreateService', {
- autoShow: true,
- nodename: view.nodename,
- subject: view.getTitle(),
- type: view.type,
- taskDone: () => view.rstore.load(),
- });
- },
-});
-
-Ext.define(
- 'PVE.node.CephServiceList',
- {
- extend: 'Ext.grid.GridPanel',
- xtype: 'pveNodeCephServiceList',
-
- onlineHelp: 'chapter_pveceph',
- emptyText: gettext('No such service configured.'),
-
- stateful: true,
-
- // will be called when the store loads
- storeLoadCallback: Ext.emptyFn,
-
- // if set to true, does shows the ceph install mask if needed
- showCephInstallMask: false,
-
- controller: 'CephServiceList',
-
- tbar: [
- {
- xtype: 'proxmoxButton',
- text: gettext('Start'),
- iconCls: 'fa fa-play',
- action: 'start',
- disabled: true,
- enableFn: (rec) => rec.data.state === 'stopped' || rec.data.state === 'unknown',
- handler: 'onChangeService',
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Stop'),
- iconCls: 'fa fa-stop',
- action: 'stop',
- enableFn: (rec) => rec.data.state !== 'stopped',
- disabled: true,
- handler: 'onChangeService',
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Restart'),
- iconCls: 'fa fa-refresh',
- action: 'restart',
- disabled: true,
- enableFn: (rec) => rec.data.state !== 'stopped',
- handler: 'onChangeService',
- },
- '-',
- {
- text: gettext('Create'),
- reference: 'createButton',
- handler: 'onCreate',
- },
- {
- text: gettext('Destroy'),
- xtype: 'proxmoxStdRemoveButton',
- getUrl: function (rec) {
- let view = this.up('grid');
- if (!rec.data.host) {
- Ext.Msg.alert(gettext('Error'), 'entry has no host, cannot build API url');
- return '';
- }
- return `/nodes/${rec.data.host}/ceph/${view.type}/${rec.data.name}`;
- },
- callback: function (options, success, response) {
- let view = this.up('grid');
- if (!success) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- return;
- }
- Ext.create('Proxmox.window.TaskProgress', {
- autoShow: true,
- upid: response.result.data,
- taskDone: () => view.rstore.load(),
- });
- },
- handler: function (btn, event, rec) {
- let me = this;
- let view = me.up('grid');
- let doRequest = function () {
- Proxmox.button.StdRemoveButton.prototype.handler.call(me, btn, event, rec);
- };
- if (view.type === 'mon') {
- Proxmox.Utils.API2Request({
- url: `/nodes/${rec.data.host}/ceph/cmd-safety`,
- params: {
- service: view.type,
- id: rec.data.name,
- action: 'destroy',
- },
- method: 'GET',
- success: function ({ result: { data } }) {
- if (!data.safe) {
- Ext.Msg.show({
- title: ngettext('Warning', 'Warnings', 1),
- message: data.status,
- icon: Ext.Msg.WARNING,
- buttons: Ext.Msg.OKCANCEL,
- buttonText: { ok: gettext('Destroy MON') },
- fn: function (selection) {
- if (selection === 'ok') {
- doRequest();
- }
- },
- });
- } else {
- doRequest();
- }
- },
- failure: (response, _opts) =>
- Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- } else {
- doRequest();
- }
- },
- },
- '-',
- {
- xtype: 'proxmoxButton',
- text: gettext('Syslog'),
- disabled: true,
- handler: 'showSyslog',
- },
- ],
-
- columns: [
- {
- header: gettext('Name'),
- flex: 1,
- sortable: true,
- renderer: function (v) {
- return this.type + '.' + v;
- },
- dataIndex: 'name',
- },
- {
- header: gettext('Host'),
- flex: 1,
- sortable: true,
- renderer: function (v) {
- return v || Proxmox.Utils.unknownText;
- },
- dataIndex: 'host',
- },
- {
- header: gettext('Status'),
- flex: 1,
- sortable: false,
- renderer: 'render_status',
- dataIndex: 'state',
- },
- {
- header: gettext('Address'),
- flex: 3,
- sortable: true,
- renderer: function (v) {
- return v || Proxmox.Utils.unknownText;
- },
- dataIndex: 'addr',
- },
- {
- header: gettext('Version'),
- flex: 3,
- sortable: true,
- dataIndex: 'version',
- renderer: 'render_version',
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (me.additionalColumns) {
- me.columns = me.columns.concat(me.additionalColumns);
- }
-
- me.callParent();
- },
- },
- function () {
- Ext.define('ceph-service-list', {
- extend: 'Ext.data.Model',
- fields: [
- 'addr',
- 'name',
- 'fs_name',
- 'rank',
- 'host',
- 'quorum',
- 'state',
- 'ceph_version',
- 'ceph_version_short',
- {
- type: 'string',
- name: 'version',
- calculate: (data) => PVE.Utils.parse_ceph_version(data),
- },
- ],
- idProperty: 'name',
- });
- },
-);
-
-Ext.define('PVE.node.CephMDSServiceController', {
- extend: 'PVE.node.CephServiceController',
- alias: 'controller.CephServiceMDSList',
-
- render_status: (value, mD, rec) =>
- Ext.htmlEncode(rec.data.fs_name ? `${value} (${rec.data.fs_name})` : value),
-});
-
-Ext.define('PVE.node.CephMDSList', {
- extend: 'PVE.node.CephServiceList',
- xtype: 'pveNodeCephMDSList',
-
- controller: {
- type: 'CephServiceMDSList',
- },
-});
-Ext.define('PVE.ceph.Services', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveCephServices',
-
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
-
- bodyPadding: '0 5 20',
- defaults: {
- xtype: 'box',
- style: {
- 'text-align': 'center',
- },
- },
-
- items: [
- {
- flex: 1,
- xtype: 'pveCephServiceList',
- itemId: 'mons',
- title: gettext('Monitors'),
- },
- {
- flex: 1,
- xtype: 'pveCephServiceList',
- itemId: 'mgrs',
- title: gettext('Managers'),
- },
- {
- flex: 1,
- xtype: 'pveCephServiceList',
- itemId: 'mdss',
- title: gettext('Metadata Servers'),
- },
- ],
-
- updateAll: function (metadata, status) {
- var me = this;
-
- const healthstates = {
- HEALTH_UNKNOWN: 0,
- HEALTH_ERR: 1,
- HEALTH_WARN: 2,
- HEALTH_UPGRADE: 3,
- HEALTH_OLD: 4,
- HEALTH_OK: 5,
- };
- // order guarantee since es2020, but browsers did so before. Note, integers would break it.
- const healthmap = Object.keys(healthstates);
- let maxversion = '00.0.00';
- Object.values(metadata.node || {}).forEach(function (node) {
- if (PVE.Utils.compare_ceph_versions(node?.version?.parts, maxversion) > 0) {
- maxversion = node?.version?.parts;
- }
- });
- var quorummap = status && status.quorum_names ? status.quorum_names : [];
- let monmessages = {},
- mgrmessages = {},
- mdsmessages = {};
- if (status) {
- if (status.health) {
- Ext.Object.each(status.health.checks, function (key, value, _obj) {
- if (!Ext.String.startsWith(key, 'MON_')) {
- return;
- }
- for (let i = 0; i < value.detail.length; i++) {
- let match = value.detail[i].message.match(/mon.([a-zA-Z0-9\-.]+)/);
- if (!match) {
- continue;
- }
- let monid = match[1];
- if (!monmessages[monid]) {
- monmessages[monid] = {
- worstSeverity: healthstates.HEALTH_OK,
- messages: [],
- };
- }
-
- let severityIcon = PVE.Utils.get_ceph_icon_html(value.severity, true);
- let details = value.detail.reduce((acc, v) => `${acc}\n${v.message}`, '');
- monmessages[monid].messages.push(severityIcon + details);
-
- if (healthstates[value.severity] < monmessages[monid].worstSeverity) {
- monmessages[monid].worstSeverity = healthstates[value.severity];
- }
- }
- });
- }
-
- if (status.mgrmap) {
- mgrmessages[status.mgrmap.active_name] = 'active';
- status.mgrmap.standbys.forEach(function (mgr) {
- mgrmessages[mgr.name] = 'standby';
- });
- }
-
- if (status.fsmap) {
- status.fsmap.by_rank.forEach(function (mds) {
- mdsmessages[mds.name] = 'rank: ' + mds.rank + '; ' + mds.status;
- });
- }
- }
-
- let checks = {
- mon: function (mon) {
- if (quorummap.indexOf(mon.name) !== -1) {
- mon.health = healthstates.HEALTH_OK;
- } else {
- mon.health = healthstates.HEALTH_ERR;
- }
- if (monmessages[mon.name]) {
- if (monmessages[mon.name].worstSeverity < mon.health) {
- mon.health = monmessages[mon.name].worstSeverity;
- }
- Array.prototype.push.apply(mon.messages, monmessages[mon.name].messages);
- }
- return mon;
- },
- mgr: function (mgr) {
- if (mgrmessages[mgr.name] === 'active') {
- mgr.title = '' + mgr.title + ' ';
- mgr.statuses.push(gettext('Status') + ': active ');
- } else if (mgrmessages[mgr.name] === 'standby') {
- mgr.statuses.push(gettext('Status') + ': standby');
- } else if (mgr.health > healthstates.HEALTH_WARN) {
- mgr.health = healthstates.HEALTH_WARN;
- }
-
- return mgr;
- },
- mds: function (mds) {
- if (mdsmessages[mds.name]) {
- mds.title = '' + mds.title + ' ';
- mds.statuses.push(gettext('Status') + ': ' + mdsmessages[mds.name] + ' ');
- } else if (mds.addr !== Proxmox.Utils.unknownText) {
- mds.statuses.push(gettext('Status') + ': standby');
- }
-
- return mds;
- },
- };
-
- for (let type of ['mon', 'mgr', 'mds']) {
- let ids = Object.keys(metadata[type] || {});
- me[type] = {};
-
- for (let id of ids) {
- const [name, host] = id.split('@');
- let result = {
- id: id,
- health: healthstates.HEALTH_OK,
- statuses: [],
- messages: [],
- name: name,
- title: metadata[type][id].name || name,
- host: host,
- version: PVE.Utils.parse_ceph_version(metadata[type][id]),
- service: metadata[type][id].service,
- addr:
- metadata[type][id].addr ||
- metadata[type][id].addrs ||
- Proxmox.Utils.unknownText,
- };
-
- result.statuses = [
- gettext('Host') + ': ' + host,
- gettext('Address') + ': ' + result.addr,
- ];
-
- if (checks[type]) {
- result = checks[type](result);
- }
-
- if (result.service && !result.version) {
- result.messages.push(
- PVE.Utils.get_ceph_icon_html('HEALTH_UNKNOWN', true) + gettext('Stopped'),
- );
- result.health = healthstates.HEALTH_UNKNOWN;
- }
-
- if (!result.version && result.addr === Proxmox.Utils.unknownText) {
- result.health = healthstates.HEALTH_UNKNOWN;
- }
-
- if (result.version) {
- result.statuses.push(gettext('Version') + ': ' + result.version);
-
- if (PVE.Utils.compare_ceph_versions(result.version, maxversion) !== 0) {
- let host_version =
- metadata.node[host]?.version?.parts || metadata.version?.[host] || '';
- if (PVE.Utils.compare_ceph_versions(host_version, maxversion) === 0) {
- if (result.health > healthstates.HEALTH_OLD) {
- result.health = healthstates.HEALTH_OLD;
- }
- result.messages.push(
- PVE.Utils.get_ceph_icon_html('HEALTH_OLD', true) +
- gettext(
- 'A newer version was installed but old version still running, please restart',
- ),
- );
- } else {
- if (result.health > healthstates.HEALTH_UPGRADE) {
- result.health = healthstates.HEALTH_UPGRADE;
- }
- result.messages.push(
- PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE', true) +
- gettext(
- 'Other cluster members use a newer version of this service, please upgrade and restart',
- ),
- );
- }
- }
- }
-
- result.statuses.push(''); // empty line
- result.text = result.statuses.concat(result.messages).join(' ');
-
- result.health = healthmap[result.health];
-
- me[type][id] = result;
- }
- }
-
- me.getComponent('mons').updateAll(Object.values(me.mon));
- me.getComponent('mgrs').updateAll(Object.values(me.mgr));
- me.getComponent('mdss').updateAll(Object.values(me.mds));
- },
-});
-
-Ext.define('PVE.ceph.ServiceList', {
- extend: 'Ext.container.Container',
- xtype: 'pveCephServiceList',
-
- style: {
- 'text-align': 'center',
- },
- defaults: {
- xtype: 'box',
- style: {
- 'text-align': 'center',
- },
- },
-
- items: [
- {
- itemId: 'title',
- data: {
- title: '',
- },
- tpl: '{title} ',
- },
- ],
-
- updateAll: function (list) {
- var me = this;
- me.suspendLayout = true;
-
- list.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0));
- if (!me.ids) {
- me.ids = [];
- }
- let pendingRemoval = {};
- me.ids.forEach((id) => {
- pendingRemoval[id] = true;
- }); // mark all as to-remove first here
-
- for (let i = 0; i < list.length; i++) {
- let service = me.getComponent(list[i].id);
- if (!service) {
- // services and list are sorted, so just insert at i + 1 (first el. is the title)
- service = me.insert(i + 1, {
- xtype: 'pveCephServiceWidget',
- itemId: list[i].id,
- });
- me.ids.push(list[i].id);
- } else {
- delete pendingRemoval[list[i].id]; // drop existing from for-removal
- }
- service.updateService(list[i].title, list[i].text, list[i].health);
- }
- Object.keys(pendingRemoval).forEach((id) => me.remove(id)); // GC
-
- me.suspendLayout = false;
- me.updateLayout();
- },
-
- initComponent: function () {
- var me = this;
- me.callParent();
- me.getComponent('title').update({
- title: me.title,
- });
- },
-});
-
-Ext.define('PVE.ceph.ServiceWidget', {
- extend: 'Ext.Component',
- alias: 'widget.pveCephServiceWidget',
-
- userCls: 'monitor inline-block',
- data: {
- title: '0',
- health: 'HEALTH_ERR',
- text: '',
- iconCls: PVE.Utils.get_health_icon(),
- },
-
- tpl: ['{title}: ', ' '],
-
- updateService: function (title, text, health) {
- var me = this;
-
- me.update(
- Ext.apply(me.data, {
- health: health,
- text: text,
- title: title,
- iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health]),
- }),
- );
-
- if (me.tooltip) {
- me.tooltip.setHtml(text);
- }
- },
-
- listeners: {
- destroy: function () {
- let me = this;
- if (me.tooltip) {
- me.tooltip.destroy();
- delete me.tooltip;
- }
- },
- mouseenter: {
- element: 'el',
- fn: function (events, element) {
- let view = this.component;
- if (!view) {
- return;
- }
- if (!view.tooltip || view.data.text !== view.tooltip.html) {
- view.tooltip = Ext.create('Ext.tip.ToolTip', {
- target: view.el,
- trackMouse: true,
- dismissDelay: 0,
- renderTo: Ext.getBody(),
- html: view.data.text,
- });
- }
- view.tooltip.show();
- },
- },
- mouseleave: {
- element: 'el',
- fn: function (events, element) {
- let view = this.component;
- if (view.tooltip) {
- view.tooltip.destroy();
- delete view.tooltip;
- }
- },
- },
- },
-});
-Ext.define('pve-ceph-warnings', {
- extend: 'Ext.data.Model',
- fields: ['id', 'summary', 'detail', 'severity'],
- idProperty: 'id',
-});
-
-Ext.define('PVE.node.CephStatus', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveNodeCephStatus',
-
- onlineHelp: 'chapter_pveceph',
-
- scrollable: true,
- bodyPadding: 5,
- layout: {
- type: 'column',
- },
-
- defaults: {
- padding: 5,
- },
-
- items: [
- {
- xtype: 'panel',
- title: gettext('Health'),
- bodyPadding: 10,
- plugins: 'responsive',
- responsiveConfig: {
- 'width < 1600': {
- minHeight: 230,
- columnWidth: 1,
- },
- 'width >= 1600': {
- minHeight: 500,
- columnWidth: 0.5,
- },
- },
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
- items: [
- {
- xtype: 'container',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- flex: 1,
- items: [
- {
- xtype: 'pveHealthWidget',
- itemId: 'overallhealth',
- flex: 1,
- title: gettext('Status'),
- },
- {
- xtype: 'displayfield',
- itemId: 'versioninfo',
- fieldLabel: gettext('Ceph Version'),
- value: '',
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'The newest version installed in the Cluster.',
- ),
- },
- padding: '10 0 0 0',
- style: {
- 'text-align': 'center',
- },
- },
- ],
- },
- {
- xtype: 'grid',
- itemId: 'warnings',
- flex: 2,
- maxHeight: 430,
- stateful: true,
- stateId: 'ceph-status-warnings',
- viewConfig: {
- enableTextSelection: true,
- listeners: {
- collapsebody: function (rowNode, record) {
- record.set('expanded', false);
- record.commit();
- },
- expandbody: function (rowNode, record) {
- record.set('expanded', true);
- record.commit();
- },
- },
- },
- // we load the store manually, to show an emptyText specify an empty intermediate store
- store: {
- type: 'diff',
- trackRemoved: false,
- data: [],
- rstore: {
- storeid: 'pve-ceph-warnings',
- type: 'update',
- model: 'pve-ceph-warnings',
- },
- },
- updateHealth: function (health) {
- let checks = health.checks || {};
-
- let checkRecords = Object.keys(checks)
- .sort()
- .map((key) => {
- let check = checks[key];
- let data = {
- id: key,
- summary: check.summary.message,
- detail: check.detail
- .reduce((acc, v) => `${acc}\n${v.message}`, '')
- .trimStart(),
- severity: check.severity,
- };
- data.noDetails = data.detail.length === 0;
- data.detailsCls = data.detail.length === 0 ? 'pmx-opacity-75' : '';
- if (data.detail.length === 0) {
- data.detail = 'no additional data';
- }
- return data;
- });
-
- let rstore = this.getStore().rstore;
- rstore.loadData(checkRecords, false);
- rstore.fireEvent('load', rstore, checkRecords, true);
- },
- emptyText: gettext('No Warnings/Errors'),
- columns: [
- {
- dataIndex: 'severity',
- tooltip: gettext('Severity'),
- align: 'center',
- width: 38,
- renderer: function (value) {
- let health = PVE.Utils.map_ceph_health[value];
- let icon = PVE.Utils.get_health_icon(health);
- return ` `;
- },
- sorter: {
- sorterFn: function (a, b) {
- let health = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK'];
- return (
- health.indexOf(b.data.severity) -
- health.indexOf(a.data.severity)
- );
- },
- },
- },
- {
- dataIndex: 'summary',
- header: gettext('Summary'),
- renderer: function (value, metaData, record, rI, cI, store, view) {
- if (record.get('expanded')) {
- metaData.tdCls = 'pmx-column-wrapped';
- }
- return Ext.htmlEncode(value);
- },
- flex: 1,
- },
- {
- xtype: 'actioncolumn',
- width: 50,
- align: 'center',
- tooltip: gettext('Actions'),
- items: [
- {
- iconCls: 'x-fa fa-clipboard',
- tooltip: gettext('Copy to Clipboard'),
- handler: function (
- grid,
- rowindex,
- colindex,
- item,
- e,
- { data },
- ) {
- let detail = data.noDetails ? '' : `\n${data.detail}`;
- navigator.clipboard
- .writeText(`${data.severity}: ${data.summary}${detail}`)
- .catch((err) => Ext.Msg.alert(gettext('Error'), err));
- },
- },
- ],
- },
- ],
- listeners: {
- itemdblclick: function (view, record, row, rowIdx, e) {
- // inspired by Ext.grid.plugin.RowExpander, but for double click
- let rowNode = view.getNode(rowIdx);
- let normalRow = Ext.fly(rowNode);
-
- let collapsedCls = view.rowBodyFeature.rowCollapsedCls;
-
- if (normalRow.hasCls(collapsedCls)) {
- view.rowBodyFeature.rowExpander.toggleRow(rowIdx, record);
- }
- },
- },
- plugins: [
- {
- ptype: 'rowexpander',
- expandOnDblClick: false,
- scrollIntoViewOnExpand: false,
- rowBodyTpl: [
- '',
- '{detail:htmlEncode}',
- ' ',
- ],
- },
- ],
- },
- ],
- },
- {
- xtype: 'pveCephStatusDetail',
- itemId: 'statusdetail',
- plugins: 'responsive',
- responsiveConfig: {
- 'width < 1600': {
- columnWidth: 1,
- minHeight: 250,
- },
- 'width >= 1600': {
- columnWidth: 0.5,
- minHeight: 300,
- },
- },
- title: gettext('Status'),
- },
- {
- xtype: 'pveCephServices',
- title: gettext('Services'),
- itemId: 'services',
- plugins: 'responsive',
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
- responsiveConfig: {
- 'width < 1600': {
- columnWidth: 1,
- minHeight: 200,
- },
- 'width >= 1600': {
- columnWidth: 0.5,
- minHeight: 200,
- },
- },
- },
- {
- xtype: 'panel',
- title: gettext('Performance'),
- columnWidth: 1,
- bodyPadding: 5,
- layout: {
- type: 'hbox',
- align: 'center',
- },
- items: [
- {
- xtype: 'container',
- flex: 1,
- items: [
- {
- xtype: 'proxmoxGauge',
- itemId: 'space',
- title: gettext('Usage'),
- },
- {
- flex: 1,
- border: false,
- },
- {
- xtype: 'container',
- itemId: 'recovery',
- hidden: true,
- padding: 25,
- items: [
- {
- xtype: 'pveRunningChart',
- itemId: 'recoverychart',
- title: gettext('Recovery') + '/ ' + gettext('Rebalance'),
- renderer: PVE.Utils.render_bandwidth,
- height: 100,
- },
- {
- xtype: 'progressbar',
- itemId: 'recoveryprogress',
- },
- ],
- },
- ],
- },
- {
- xtype: 'container',
- flex: 2,
- defaults: {
- padding: 0,
- height: 100,
- },
- items: [
- {
- xtype: 'pveRunningChart',
- itemId: 'reads',
- title: gettext('Reads'),
- renderer: PVE.Utils.render_bandwidth,
- },
- {
- xtype: 'pveRunningChart',
- itemId: 'writes',
- title: gettext('Writes'),
- renderer: PVE.Utils.render_bandwidth,
- },
- {
- xtype: 'pveRunningChart',
- itemId: 'readiops',
- title: 'IOPS: ' + gettext('Reads'),
- renderer: Ext.util.Format.numberRenderer('0,000'),
- },
- {
- xtype: 'pveRunningChart',
- itemId: 'writeiops',
- title: 'IOPS: ' + gettext('Writes'),
- renderer: Ext.util.Format.numberRenderer('0,000'),
- },
- ],
- },
- ],
- },
- ],
-
- updateAll: function (store, records, success) {
- if (!success || records.length === 0) {
- return;
- }
-
- var me = this;
- var rec = records[0];
- me.status = rec.data;
-
- // add health panel
- me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
- me.down('#warnings').updateHealth(rec.data.health || {}); // add errors to gridstore
-
- me.getComponent('services').updateAll(me.metadata || {}, rec.data);
-
- me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
-
- // add performance data
- let pgmap = rec.data.pgmap;
- let used = pgmap.bytes_used;
- let total = pgmap.bytes_total;
-
- var text = Ext.String.format(
- gettext('{0} of {1}'),
- Proxmox.Utils.render_size(used),
- Proxmox.Utils.render_size(total),
- );
-
- // update the usage widget
- const usage = total > 0 ? used / total : 0;
- me.down('#space').updateValue(usage, text);
-
- let readiops = pgmap.read_op_per_sec;
- let writeiops = pgmap.write_op_per_sec;
- let reads = pgmap.read_bytes_sec || 0;
- let writes = pgmap.write_bytes_sec || 0;
-
- // update the graphs
- me.reads.addDataPoint(reads);
- me.writes.addDataPoint(writes);
- me.readiops.addDataPoint(readiops);
- me.writeiops.addDataPoint(writeiops);
-
- let degraded = pgmap.degraded_objects || 0;
- let misplaced = pgmap.misplaced_objects || 0;
- let unfound = pgmap.unfound_objects || 0;
- let unhealthy = degraded + unfound + misplaced;
- // update recovery
- if (pgmap.recovering_objects_per_sec !== undefined || unhealthy > 0) {
- let toRecoverObjects =
- pgmap.misplaced_total || pgmap.unfound_total || pgmap.degraded_total || 0;
- if (toRecoverObjects === 0) {
- return; // FIXME: unexpected return and leaves things possible visible when it shouldn't?
- }
- let recovered = toRecoverObjects - unhealthy || 0;
- let speed = pgmap.recovering_bytes_per_sec || 0;
-
- let recoveryRatio = recovered / toRecoverObjects;
- let txt = `${(recoveryRatio * 100).toFixed(2)}%`;
- if (speed > 0) {
- let obj_per_sec = speed / (4 * 1024 * 1024); // 4 MiB per Object
- let duration = Proxmox.Utils.format_duration_human(unhealthy / obj_per_sec);
- let speedTxt = PVE.Utils.render_bandwidth(speed);
- txt += ` (${speedTxt} - ${duration} left)`;
- }
-
- me.down('#recovery').setVisible(true);
- me.down('#recoveryprogress').updateValue(recoveryRatio);
- me.down('#recoveryprogress').updateText(txt);
- me.down('#recoverychart').addDataPoint(speed);
- } else {
- me.down('#recovery').setVisible(false);
- me.down('#recoverychart').addDataPoint(0);
- }
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
-
- me.callParent();
- var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
- me.store = Ext.create('Proxmox.data.UpdateStore', {
- storeid: 'ceph-status-' + (nodename || 'cluster'),
- interval: 5000,
- proxy: {
- type: 'proxmox',
- url: baseurl + '/status',
- },
- });
-
- me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
- storeid: 'ceph-metadata-' + (nodename || 'cluster'),
- interval: 15 * 1000,
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/ceph/metadata',
- },
- });
-
- // save references for the updatefunction
- me.iops = me.down('#iops');
- me.readiops = me.down('#readiops');
- me.writeiops = me.down('#writeiops');
- me.reads = me.down('#reads');
- me.writes = me.down('#writes');
-
- // manages the "install ceph?" overlay
- PVE.Utils.monitor_ceph_installed(me, me.store, nodename);
-
- me.mon(me.store, 'load', me.updateAll, me);
- me.mon(
- me.metadatastore,
- 'load',
- function (store, records, success) {
- if (!success || records.length < 1) {
- return;
- }
- me.metadata = records[0].data;
-
- // update services
- me.getComponent('services').updateAll(me.metadata, me.status || {});
-
- // update detailstatus panel
- me.getComponent('statusdetail').updateAll(me.metadata, me.status || {});
-
- let maxversion = [];
- let maxversiontext = '';
- for (const [_nodename, data] of Object.entries(me.metadata.node)) {
- let version = data.version.parts;
- if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
- maxversion = version;
- maxversiontext = data.version.str;
- }
- }
- me.down('#versioninfo').setValue(maxversiontext);
- },
- me,
- );
-
- me.on('destroy', me.store.stopUpdate);
- me.on('destroy', me.metadatastore.stopUpdate);
- me.store.startUpdate();
- me.metadatastore.startUpdate();
- },
-});
-Ext.define('PVE.ceph.StatusDetail', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveCephStatusDetail',
-
- layout: {
- type: 'hbox',
- align: 'stretch',
- },
-
- bodyPadding: '0 5',
- defaults: {
- xtype: 'box',
- style: {
- 'text-align': 'center',
- },
- },
-
- items: [
- {
- flex: 1,
- itemId: 'osds',
- maxHeight: 250,
- scrollable: true,
- padding: '0 10 5 10',
- data: {
- total: 0,
- upin: 0,
- upout: 0,
- downin: 0,
- downout: 0,
- oldOSD: [],
- ghostOSD: [],
- },
- tpl: [
- 'OSDs ',
- '',
- ' ',
- ' ',
- gettext('In'),
- ' ',
- ' ',
- gettext('Out'),
- ' ',
- ' ',
- '',
- ' ',
- gettext('Up'),
- ' ',
- '{upin} ',
- '{upout} ',
- ' ',
- '',
- ' ',
- gettext('Down'),
- ' ',
- '{downin} ',
- '{downout} ',
- ' ',
- '
',
- '',
- gettext('Total'),
- ': {total}',
- '
',
- '',
- ' ' + gettext('Outdated OSDs') + ' ',
- '',
- '
',
- 'osd.{id}:
',
- '{version}
',
- '
',
- ' ',
- '
',
- ' ',
- '',
- '',
- ' ',
- ` ${gettext('Ghost OSDs')} `,
- ``,
- '
',
- 'osd.{id}
',
- '
',
- ' ',
- '
',
- ' ',
- ],
- },
- {
- flex: 1,
- border: false,
- itemId: 'pgchart',
- xtype: 'polar',
- height: 184,
- innerPadding: 5,
- insetPadding: 5,
- colors: ['#CFCFCF', '#21BF4B', '#3892d4', '#FFCC00', '#FF6C59'],
- store: {},
- series: [
- {
- type: 'pie',
- donut: 60,
- angleField: 'count',
- tooltip: {
- trackMouse: true,
- renderer: function (tooltip, record, ctx) {
- var html = record.get('text');
- html += ' ';
- record.get('states').forEach(function (state) {
- html += ' ' + state.state_name + ': ' + state.count.toString();
- });
- tooltip.setHtml(html);
- },
- },
- subStyle: {
- strokeStyle: false,
- },
- },
- ],
- },
- {
- flex: 1.6,
- itemId: 'pgs',
- padding: '0 10',
- maxHeight: 250,
- scrollable: true,
- data: {
- states: [],
- },
- tpl: [
- 'PGs ',
- '',
- ' {state_name}:
',
- '{count}
',
- '
',
- ' ',
- ],
- },
- ],
-
- // similar to mgr dashboard
- pgstates: {
- // clean
- clean: 1,
- active: 1,
-
- // busy
- activating: 2,
- backfill_wait: 2,
- backfilling: 2,
- creating: 2,
- deep: 2,
- forced_backfill: 2,
- forced_recovery: 2,
- peered: 2,
- peering: 2,
- recovering: 2,
- recovery_wait: 2,
- remapped: 2,
- repair: 2,
- scrubbing: 2,
- snaptrim: 2,
- snaptrim_wait: 2,
-
- // warning
- degraded: 3,
- undersized: 3,
-
- // critical
- backfill_toofull: 4,
- backfill_unfound: 4,
- down: 4,
- incomplete: 4,
- inconsistent: 4,
- recovery_toofull: 4,
- recovery_unfound: 4,
- snaptrim_error: 4,
- stale: 4,
- },
-
- statecategories: [
- {
- text: gettext('Unknown'),
- count: 0,
- states: [],
- cls: 'faded',
- },
- {
- text: gettext('Clean'),
- cls: 'good',
- },
- {
- text: gettext('Busy'),
- cls: 'pve-ceph-status-busy',
- },
- {
- text: ngettext('Warning', 'Warnings', 1),
- cls: 'warning',
- },
- {
- text: gettext('Critical'),
- cls: 'critical',
- },
- ],
-
- checkThemeColors: function () {
- let me = this;
- let rootStyle = getComputedStyle(document.documentElement);
-
- // get color
- let background = rootStyle.getPropertyValue('--pwt-panel-background').trim() || '#ffffff';
-
- // set the colors
- me.chart.setBackground(background);
- me.chart.redraw();
- },
-
- updateAll: function (metadata, status) {
- let me = this;
- me.suspendLayout = true;
-
- let maxversion = '0';
- Object.values(metadata.node || {}).forEach(function (node) {
- if (PVE.Utils.compare_ceph_versions(node?.version?.parts, maxversion) > 0) {
- maxversion = node.version.parts;
- }
- });
-
- let oldOSD = [],
- ghostOSD = [];
- metadata.osd?.forEach((osd) => {
- let version = PVE.Utils.parse_ceph_version(osd);
- if (version !== undefined) {
- if (PVE.Utils.compare_ceph_versions(version, maxversion) !== 0) {
- oldOSD.push({
- id: osd.id,
- version: version,
- });
- }
- } else {
- if (Object.keys(osd).length > 1) {
- console.warn('got OSD entry with no valid version but other keys', osd);
- }
- ghostOSD.push({
- id: osd.id,
- });
- }
- });
-
- // update PGs sorted
- let pgmap = status.pgmap || {};
- let pgs_by_state = pgmap.pgs_by_state || [];
- pgs_by_state.sort(function (a, b) {
- return a.state_name < b.state_name ? -1 : a.state_name === b.state_name ? 0 : 1;
- });
-
- me.statecategories.forEach(function (cat) {
- cat.count = 0;
- cat.states = [];
- });
-
- pgs_by_state.forEach(function (state) {
- let states = state.state_name.split(/[^a-z]+/);
- let result = 0;
- for (let i = 0; i < states.length; i++) {
- if (me.pgstates[states[i]] > result) {
- result = me.pgstates[states[i]];
- }
- }
- // for the list
- state.cls = me.statecategories[result].cls;
-
- me.statecategories[result].count += state.count;
- me.statecategories[result].states.push(state);
- });
-
- me.chart.getStore().setData(me.statecategories);
- me.getComponent('pgs').update({ states: pgs_by_state });
-
- let health = status.health || {};
- // we collect monitor/osd information from the checks
- const downinregex = /(\d+) osds down/;
- let downin_osds = 0;
- Ext.Object.each(health.checks, function (key, value, obj) {
- var found = null;
- if (key === 'OSD_DOWN') {
- found = value.summary.message.match(downinregex);
- if (found !== null) {
- downin_osds = parseInt(found[1], 10);
- }
- }
- });
-
- let osdmap = status.osdmap || {};
- if (typeof osdmap.osdmap !== 'undefined') {
- osdmap = osdmap.osdmap;
- }
- // update OSDs counts
- let total_osds = osdmap.num_osds || 0;
- let in_osds = osdmap.num_in_osds || 0;
- let up_osds = osdmap.num_up_osds || 0;
- let down_osds = total_osds - up_osds;
-
- let downout_osds = down_osds - downin_osds;
- let upin_osds = in_osds - downin_osds;
- let upout_osds = up_osds - upin_osds;
-
- let osds = {
- total: total_osds,
- upin: upin_osds,
- upout: upout_osds,
- downin: downin_osds,
- downout: downout_osds,
- oldOSD: oldOSD,
- ghostOSD,
- };
- let osdcomponent = me.getComponent('osds');
- osdcomponent.update(Ext.apply(osdcomponent.data, osds));
-
- me.suspendLayout = false;
- me.updateLayout();
- },
-
- initComponent: function () {
- var me = this;
- me.callParent();
-
- me.chart = me.getComponent('pgchart');
- me.checkThemeColors();
-
- // switch colors on media query changes
- me.mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
- me.themeListener = (e) => {
- me.checkThemeColors();
- };
- me.mediaQueryList.addEventListener('change', me.themeListener);
- },
-
- doDestroy: function () {
- let me = this;
-
- me.mediaQueryList.removeEventListener('change', me.themeListener);
-
- me.callParent();
- },
-});
-Ext.define('PVE.node.ACMEAccountCreate', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- width: 450,
- title: gettext('Register Account'),
- isCreate: true,
- method: 'POST',
- submitText: gettext('Register'),
- url: '/cluster/acme/account',
- showTaskViewer: true,
- defaultExists: false,
- referenceHolder: true,
- onlineHelp: 'sysadmin_certs_acme_account',
-
- viewModel: {
- data: {
- customDirectory: false,
- eabRequired: false,
- },
- formulas: {
- eabEmptyText: function (get) {
- return get('eabRequired') ? gettext('required') : gettext('optional');
- },
- },
- },
-
- items: [
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Account Name'),
- name: 'name',
- cbind: {
- emptyText: (get) => (get('defaultExists') ? '' : 'default'),
- allowBlank: (get) => !get('defaultExists'),
- },
- },
- {
- xtype: 'textfield',
- name: 'contact',
- vtype: 'email',
- allowBlank: false,
- fieldLabel: gettext('E-Mail'),
- },
- {
- xtype: 'proxmoxComboGrid',
- notFoundIsValid: true,
- isFormField: false,
- allowBlank: false,
- valueField: 'url',
- displayField: 'name',
- fieldLabel: gettext('ACME Directory'),
- store: {
- listeners: {
- load: function () {
- this.add({ name: gettext('Custom'), url: '' });
- },
- },
- autoLoad: true,
- fields: ['name', 'url'],
- idProperty: ['name'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/acme/directories',
- },
- },
- listConfig: {
- columns: [
- {
- header: gettext('Name'),
- dataIndex: 'name',
- flex: 1,
- },
- {
- header: gettext('URL'),
- dataIndex: 'url',
- flex: 1,
- },
- ],
- },
- listeners: {
- change: function (combogrid, value) {
- let me = this;
-
- let vm = me.up('window').getViewModel();
- let dirField = me.up('window').lookupReference('directoryInput');
- let tosButton = me.up('window').lookupReference('queryTos');
-
- let isCustom = combogrid.getSelection().get('name') === gettext('Custom');
- vm.set('customDirectory', isCustom);
-
- dirField.setValue(value);
-
- if (!isCustom) {
- tosButton.click();
- } else {
- me.up('window').clearToSFields();
- }
- },
- },
- },
- {
- xtype: 'fieldcontainer',
- layout: 'hbox',
- fieldLabel: gettext('URL'),
- bind: {
- hidden: '{!customDirectory}',
- },
- items: [
- {
- xtype: 'proxmoxtextfield',
- name: 'directory',
- reference: 'directoryInput',
- flex: 1,
- allowBlank: false,
- listeners: {
- change: function (textbox, value) {
- let me = this;
- me.up('window').clearToSFields();
- },
- },
- },
- {
- xtype: 'proxmoxButton',
- margin: '0 0 0 5',
- reference: 'queryTos',
- text: gettext('Query URL'),
- listeners: {
- click: function (button) {
- let me = this;
-
- let w = me.up('window');
- let vm = w.getViewModel();
- let disp = w.down('#tos_url_display');
- let field = w.down('#tos_url');
- let checkbox = w.down('#tos_checkbox');
- let value = w.lookupReference('directoryInput').getValue();
- w.clearToSFields();
-
- if (!value) {
- return;
- } else {
- disp.setValue(gettext('Loading'));
- }
-
- Proxmox.Utils.API2Request({
- url: '/cluster/acme/meta',
- method: 'GET',
- params: {
- directory: value,
- },
- success: function (response, opt) {
- if (
- response.result.data &&
- response.result.data.termsOfService
- ) {
- field.setValue(response.result.data.termsOfService);
- disp.setValue(response.result.data.termsOfService);
- checkbox.setHidden(false);
- } else {
- // Needed to pass input verification and enable register button
- // has no influence on the submitted form
- checkbox.setValue(true);
- disp.setValue('No terms of service agreement required');
- }
- vm.set(
- 'eabRequired',
- !!(
- response.result.data &&
- response.result.data.externalAccountRequired
- ),
- );
- },
- failure: function (response, opt) {
- disp.setValue(undefined);
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
- },
- },
- ],
- },
- {
- xtype: 'displayfield',
- itemId: 'tos_url_display',
- renderer: PVE.Utils.render_optional_url,
- name: 'tos_url_display',
- },
- {
- xtype: 'hidden',
- itemId: 'tos_url',
- name: 'tos_url',
- },
- {
- xtype: 'proxmoxcheckbox',
- itemId: 'tos_checkbox',
- boxLabel: gettext('Accept TOS'),
- submitValue: false,
- validateValue: function (value) {
- if (value && this.checked) {
- return true;
- }
- return false;
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'eab-kid',
- fieldLabel: gettext('EAB Key ID'),
- bind: {
- hidden: '{!customDirectory}',
- allowBlank: '{!eabRequired}',
- emptyText: '{eabEmptyText}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'eab-hmac-key',
- fieldLabel: gettext('EAB Key'),
- bind: {
- hidden: '{!customDirectory}',
- allowBlank: '{!eabRequired}',
- emptyText: '{eabEmptyText}',
- },
- },
- ],
-
- clearToSFields: function () {
- let me = this;
-
- let disp = me.down('#tos_url_display');
- let field = me.down('#tos_url');
- let checkbox = me.down('#tos_checkbox');
-
- disp.setValue('Terms of service not fetched yet');
- field.setValue(undefined);
- checkbox.setValue(undefined);
- checkbox.setHidden(true);
- },
-});
-
-Ext.define('PVE.node.ACMEDomainEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveACMEDomainEdit',
-
- subject: gettext('Domain'),
- isCreate: false,
- width: 450,
- onlineHelp: 'sysadmin_certificate_management',
-
- items: [
- {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- let me = this;
- let win = me.up('pveACMEDomainEdit');
- let nodeconfig = win.nodeconfig;
- let olddomain = win.domain || {};
-
- let params = {
- digest: nodeconfig.digest,
- };
-
- let configkey = olddomain.configkey;
- let acmeObj = PVE.Parser.parseACME(nodeconfig.acme);
-
- if (values.type === 'dns') {
- if (!olddomain.configkey || olddomain.configkey === 'acme') {
- // look for first free slot
- for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
- if (nodeconfig[`acmedomain${i}`] === undefined) {
- configkey = `acmedomain${i}`;
- break;
- }
- }
- if (olddomain.domain) {
- // we have to remove the domain from the acme domainlist
- PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain);
- params.acme = PVE.Parser.printACME(acmeObj);
- }
- }
-
- delete values.type;
- params[configkey] = PVE.Parser.printPropertyString(values, 'domain');
- } else {
- if (olddomain.configkey && olddomain.configkey !== 'acme') {
- // delete the old dns entry
- params.delete = [olddomain.configkey];
- }
-
- // add new, remove old and make entries unique
- PVE.Utils.add_domain_to_acme(acmeObj, values.domain);
- PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain);
- params.acme = PVE.Parser.printACME(acmeObj);
- }
-
- return params;
- },
- items: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'type',
- fieldLabel: gettext('Challenge Type'),
- allowBlank: false,
- value: 'standalone',
- comboItems: [
- ['standalone', 'HTTP'],
- ['dns', 'DNS'],
- ],
- validator: function (value) {
- let me = this;
- let win = me.up('pveACMEDomainEdit');
- let oldconfigkey = win.domain ? win.domain.configkey : undefined;
- let val = me.getValue();
- if (val === 'dns' && (!oldconfigkey || oldconfigkey === 'acme')) {
- // we have to check if there is a 'acmedomain' slot left
- let found = false;
- for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
- if (!win.nodeconfig[`acmedomain${i}`]) {
- found = true;
- }
- }
- if (!found) {
- return gettext('Only 5 Domains with type DNS can be configured');
- }
- }
-
- return true;
- },
- listeners: {
- change: function (cb, value) {
- let me = this;
- let view = me.up('pveACMEDomainEdit');
- let pluginField = view.down('field[name=plugin]');
- pluginField.setDisabled(value !== 'dns');
- pluginField.setHidden(value !== 'dns');
- },
- },
- },
- {
- xtype: 'hidden',
- name: 'alias',
- },
- {
- xtype: 'pveACMEPluginSelector',
- name: 'plugin',
- disabled: true,
- hidden: true,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'domain',
- allowBlank: false,
- vtype: 'DnsName',
- value: '',
- fieldLabel: gettext('Domain'),
- },
- ],
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- if (!me.nodeconfig) {
- throw 'no nodeconfig given';
- }
-
- me.isCreate = !me.domain;
- if (me.isCreate) {
- me.domain = `${me.nodename}.`; // TODO: FQDN of node
- }
-
- me.url = `/api2/extjs/nodes/${me.nodename}/config`;
-
- me.callParent();
-
- if (!me.isCreate) {
- me.setValues(me.domain);
- } else {
- me.setValues({ domain: me.domain });
- }
- },
-});
-
-Ext.define('pve-acme-domains', {
- extend: 'Ext.data.Model',
- fields: ['domain', 'type', 'alias', 'plugin', 'configkey'],
- idProperty: 'domain',
-});
-
-Ext.define('PVE.node.ACME', {
- extend: 'Ext.grid.Panel',
- alias: 'widget.pveACMEView',
-
- margin: '10 0 0 0',
- title: 'ACME',
-
- emptyText: gettext('No Domains configured'),
-
- viewModel: {
- data: {
- domaincount: 0,
- account: undefined, // the account we display
- configaccount: undefined, // the account set in the config
- accountEditable: false,
- accountsAvailable: false,
- },
-
- formulas: {
- canOrder: (get) => !!get('account') && get('domaincount') > 0,
- editBtnIcon: (get) => 'fa black fa-' + (get('accountEditable') ? 'check' : 'pencil'),
- editBtnText: (get) => (get('accountEditable') ? gettext('Apply') : gettext('Edit')),
- accountTextHidden: (get) => get('accountEditable') || !get('accountsAvailable'),
- accountValueHidden: (get) => !get('accountEditable') || !get('accountsAvailable'),
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- let accountSelector = this.lookup('accountselector');
- accountSelector.store.on('load', this.onAccountsLoad, this);
- },
-
- onAccountsLoad: function (store, records, success) {
- let me = this;
- let vm = me.getViewModel();
- let configaccount = vm.get('configaccount');
- vm.set('accountsAvailable', records.length > 0);
- if (me.autoChangeAccount && records.length > 0) {
- me.changeAccount(records[0].data.name, () => {
- vm.set('accountEditable', false);
- me.reload();
- });
- me.autoChangeAccount = false;
- } else if (configaccount) {
- if (store.findExact('name', configaccount) !== -1) {
- vm.set('account', configaccount);
- } else {
- vm.set('account', null);
- }
- }
- },
-
- addDomain: function () {
- let me = this;
- let view = me.getView();
-
- Ext.create('PVE.node.ACMEDomainEdit', {
- nodename: view.nodename,
- nodeconfig: view.nodeconfig,
- apiCallDone: function () {
- me.reload();
- },
- }).show();
- },
-
- editDomain: function () {
- let me = this;
- let view = me.getView();
-
- let selection = view.getSelection();
- if (selection.length < 1) {
- return;
- }
-
- Ext.create('PVE.node.ACMEDomainEdit', {
- nodename: view.nodename,
- nodeconfig: view.nodeconfig,
- domain: selection[0].data,
- apiCallDone: function () {
- me.reload();
- },
- }).show();
- },
-
- removeDomain: function () {
- let me = this;
- let view = me.getView();
- let selection = view.getSelection();
- if (selection.length < 1) {
- return;
- }
-
- let rec = selection[0].data;
- let params = {};
- if (rec.configkey !== 'acme') {
- params.delete = rec.configkey;
- } else {
- let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
- PVE.Utils.remove_domain_from_acme(acme, rec.domain);
- params.acme = PVE.Parser.printACME(acme);
- }
-
- Proxmox.Utils.API2Request({
- method: 'PUT',
- url: `/nodes/${view.nodename}/config`,
- params,
- success: function (response, opt) {
- me.reload();
- },
- failure: function (response, opt) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
-
- toggleEditAccount: function () {
- let me = this;
- let vm = me.getViewModel();
- let editable = vm.get('accountEditable');
- if (editable) {
- me.changeAccount(vm.get('account'), function () {
- vm.set('accountEditable', false);
- me.reload();
- });
- } else {
- vm.set('accountEditable', true);
- }
- },
-
- changeAccount: function (account, callback) {
- let me = this;
- let view = me.getView();
- let params = {};
-
- let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
- acme.account = account;
- params.acme = PVE.Parser.printACME(acme);
-
- Proxmox.Utils.API2Request({
- method: 'PUT',
- waitMsgTarget: view,
- url: `/nodes/${view.nodename}/config`,
- params,
- success: function (response, opt) {
- if (Ext.isFunction(callback)) {
- callback();
- }
- },
- failure: function (response, opt) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
-
- order: function () {
- let me = this;
- let view = me.getView();
-
- Proxmox.Utils.API2Request({
- method: 'POST',
- params: {
- force: 1,
- },
- url: `/nodes/${view.nodename}/certificates/acme/certificate`,
- success: function (response, opt) {
- Ext.create('Proxmox.window.TaskViewer', {
- upid: response.result.data,
- taskDone: function (success) {
- me.orderFinished(success);
- },
- }).show();
- },
- failure: function (response, opt) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
-
- orderFinished: function (success) {
- if (!success) {
- return;
- }
- // reload only if the Web UI is open on the same node that the cert was ordered for
- if (this.getView().nodename !== Proxmox.NodeName) {
- return;
- }
- var txt = gettext(
- 'pveproxy will be restarted with new certificates, please reload the GUI!',
- );
- Ext.getBody().mask(txt, ['pve-static-mask']);
- // reload after 10 seconds automatically
- Ext.defer(function () {
- window.location.reload(true);
- }, 10000);
- },
-
- reload: function () {
- let me = this;
- let view = me.getView();
- view.rstore.load();
- },
-
- addAccount: function () {
- let me = this;
- Ext.create('PVE.node.ACMEAccountCreate', {
- autoShow: true,
- taskDone: function () {
- me.reload();
- let accountSelector = me.lookup('accountselector');
- me.autoChangeAccount = true;
- accountSelector.store.load();
- },
- });
- },
- },
-
- tbar: [
- {
- xtype: 'proxmoxButton',
- text: gettext('Add'),
- handler: 'addDomain',
- selModel: false,
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit'),
- disabled: true,
- handler: 'editDomain',
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- handler: 'removeDomain',
- },
- '-',
- {
- xtype: 'button',
- reference: 'order',
- text: gettext('Order Certificates Now'),
- bind: {
- disabled: '{!canOrder}',
- },
- handler: 'order',
- },
- '-',
- {
- xtype: 'displayfield',
- value: gettext('Using Account') + ':',
- bind: {
- hidden: '{!accountsAvailable}',
- },
- },
- {
- xtype: 'displayfield',
- reference: 'accounttext',
- renderer: (val) => val || Proxmox.Utils.NoneText,
- bind: {
- value: '{account}',
- hidden: '{accountTextHidden}',
- },
- },
- {
- xtype: 'pveACMEAccountSelector',
- hidden: true,
- reference: 'accountselector',
- bind: {
- value: '{account}',
- hidden: '{accountValueHidden}',
- },
- },
- {
- xtype: 'button',
- iconCls: 'fa black fa-pencil',
- bind: {
- iconCls: '{editBtnIcon}',
- text: '{editBtnText}',
- hidden: '{!accountsAvailable}',
- },
- handler: 'toggleEditAccount',
- },
- {
- xtype: 'displayfield',
- value: gettext('No Account available.'),
- bind: {
- hidden: '{accountsAvailable}',
- },
- },
- {
- xtype: 'button',
- hidden: true,
- reference: 'accountlink',
- text: gettext('Add ACME Account'),
- bind: {
- hidden: '{accountsAvailable}',
- },
- handler: 'addAccount',
- },
- ],
-
- updateStore: function (store, records, success) {
- let me = this;
- let data = [];
- let rec;
- if (success && records.length > 0) {
- rec = records[0];
- } else {
- rec = {
- data: {},
- };
- }
-
- me.nodeconfig = rec.data; // save nodeconfig for updates
-
- let account = 'default';
-
- if (rec.data.acme) {
- let obj = PVE.Parser.parseACME(rec.data.acme);
- (obj.domains || []).forEach((domain) => {
- if (domain === '') {
- return;
- }
- let record = {
- domain,
- type: 'standalone',
- configkey: 'acme',
- };
- data.push(record);
- });
-
- if (obj.account) {
- account = obj.account;
- }
- }
-
- let vm = me.getViewModel();
- let oldaccount = vm.get('account');
-
- // account changed, and we do not edit currently, load again to verify
- if (oldaccount !== account && !vm.get('accountEditable')) {
- vm.set('configaccount', account);
- me.lookup('accountselector').store.load();
- }
-
- for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
- let acmedomain = rec.data[`acmedomain${i}`];
- if (!acmedomain) {
- continue;
- }
-
- let record = PVE.Parser.parsePropertyString(acmedomain, 'domain');
- record.type = 'dns';
- record.configkey = `acmedomain${i}`;
- data.push(record);
- }
-
- vm.set('domaincount', data.length);
- me.store.loadData(data, false);
- },
-
- listeners: {
- itemdblclick: 'editDomain',
- },
-
- columns: [
- {
- dataIndex: 'domain',
- flex: 5,
- text: gettext('Domain'),
- },
- {
- dataIndex: 'type',
- flex: 1,
- text: gettext('Type'),
- },
- {
- dataIndex: 'plugin',
- flex: 1,
- text: gettext('Plugin'),
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- me.rstore = Ext.create('Proxmox.data.UpdateStore', {
- interval: 10 * 1000,
- autoStart: true,
- storeid: `pve-node-domains-${me.nodename}`,
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/config`,
- },
- });
-
- me.store = Ext.create('Ext.data.Store', {
- model: 'pve-acme-domains',
- sorters: 'domain',
- });
-
- me.callParent();
- me.mon(me.rstore, 'load', 'updateStore', me);
- Proxmox.Utils.monStoreErrors(me, me.rstore);
- me.on('destroy', me.rstore.stopUpdate, me.rstore);
- },
-});
-Ext.define('PVE.node.CertificateView', {
- extend: 'Ext.container.Container',
- xtype: 'pveCertificatesView',
-
- onlineHelp: 'sysadmin_certificate_management',
-
- mixins: ['Proxmox.Mixin.CBind'],
- scrollable: 'y',
-
- items: [
- {
- xtype: 'pveCertView',
- border: 0,
- cbind: {
- nodename: '{nodename}',
- },
- },
- {
- xtype: 'pveACMEView',
- border: 0,
- cbind: {
- nodename: '{nodename}',
- },
- },
- ],
-});
-
-Ext.define('PVE.node.CertificateViewer', {
- extend: 'Proxmox.window.Edit',
-
- title: gettext('Certificate'),
-
- fieldDefaults: {
- labelWidth: 120,
- },
- width: 800,
-
- items: {
- xtype: 'inputpanel',
- maxHeight: 900,
- scrollable: 'y',
- columnT: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Name'),
- name: 'filename',
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Fingerprint'),
- name: 'fingerprint',
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Issuer'),
- name: 'issuer',
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Subject'),
- name: 'subject',
- },
- ],
- column1: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Public Key Type'),
- name: 'public-key-type',
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Public Key Size'),
- name: 'public-key-bits',
- },
- ],
- column2: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Valid Since'),
- renderer: Proxmox.Utils.render_timestamp,
- name: 'notbefore',
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Expires'),
- renderer: Proxmox.Utils.render_timestamp,
- name: 'notafter',
- },
- ],
- columnB: [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Subject Alternative Names'),
- name: 'san',
- renderer: PVE.Utils.render_san,
- },
- {
- xtype: 'fieldset',
- title: gettext('Raw Certificate'),
- collapsible: true,
- collapsed: true,
- items: [
- {
- xtype: 'textarea',
- name: 'pem',
- editable: false,
- grow: true,
- growMax: 350,
- fieldStyle: {
- 'white-space': 'pre-wrap',
- 'font-family': 'monospace',
- },
- },
- ],
- },
- ],
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.cert) {
- throw 'no cert given';
- }
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- me.url = `/nodes/${me.nodename}/certificates/info`;
- me.callParent();
-
- // hide OK/Reset button, because we just want to show data
- me.down('toolbar[dock=bottom]').setVisible(false);
-
- me.load({
- success: function (response) {
- if (Ext.isArray(response.result.data)) {
- for (const item of response.result.data) {
- if (item.filename === me.cert) {
- me.setValues(item);
- return;
- }
- }
- }
- },
- });
- },
-});
-
-Ext.define('PVE.node.CertUpload', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveCertUpload',
-
- title: gettext('Upload Custom Certificate'),
- resizable: false,
- isCreate: true,
- submitText: gettext('Upload'),
- method: 'POST',
- width: 600,
-
- apiCallDone: function (success, response, options) {
- if (!success) {
- return;
- }
- let txt = gettext(
- 'API server will be restarted to use new certificates, please reload web-interface!',
- );
- Ext.getBody().mask(txt, ['pve-static-mask']);
- Ext.defer(() => window.location.reload(true), 10000); // reload after 10 seconds automatically
- },
-
- items: {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- values.restart = 1;
- values.force = 1;
- if (!values.key) {
- delete values.key;
- }
- return values;
- },
- items: [
- {
- fieldLabel: gettext('Private Key (Optional)'),
- labelAlign: 'top',
- emptyText: gettext('No change'),
- name: 'key',
- xtype: 'textarea',
- },
- {
- xtype: 'filebutton',
- text: gettext('From File'),
- listeners: {
- change: function (btn, e, value) {
- let form = this.up('form');
- for (const file of e.event.target.files) {
- PVE.Utils.loadFile(file, (res) =>
- form.down('field[name=key]').setValue(res),
- );
- }
- btn.reset();
- },
- },
- },
- {
- fieldLabel: gettext('Certificate Chain'),
- labelAlign: 'top',
- allowBlank: false,
- name: 'certificates',
- xtype: 'textarea',
- },
- {
- xtype: 'filebutton',
- text: gettext('From File'),
- listeners: {
- change: function (btn, e, value) {
- let form = this.up('form');
- for (const file of e.event.target.files) {
- PVE.Utils.loadFile(file, (res) =>
- form.down('field[name=certificates]').setValue(res),
- );
- }
- btn.reset();
- },
- },
- },
- ],
- },
-
- initComponent: function () {
- let me = this;
- if (!me.nodename) {
- throw 'no nodename given';
- }
- me.url = `/nodes/${me.nodename}/certificates/custom`;
-
- me.callParent();
- },
-});
-
-Ext.define('pve-certificate', {
- extend: 'Ext.data.Model',
- fields: [
- 'filename',
- 'fingerprint',
- 'issuer',
- 'notafter',
- 'notbefore',
- 'subject',
- 'san',
- 'public-key-bits',
- 'public-key-type',
- ],
- idProperty: 'filename',
-});
-
-Ext.define('PVE.node.Certificates', {
- extend: 'Ext.grid.Panel',
- xtype: 'pveCertView',
-
- tbar: [
- {
- xtype: 'button',
- text: gettext('Upload Custom Certificate'),
- handler: function () {
- let view = this.up('grid');
- Ext.create('PVE.node.CertUpload', {
- nodename: view.nodename,
- listeners: {
- destroy: () => view.reload(),
- },
- autoShow: true,
- });
- },
- },
- {
- xtype: 'proxmoxStdRemoveButton',
- itemId: 'deletebtn',
- text: gettext('Delete Custom Certificate'),
- dangerous: true,
- selModel: false,
- getUrl: function (rec) {
- let view = this.up('grid');
- return `/nodes/${view.nodename}/certificates/custom?restart=1`;
- },
- confirmMsg: gettext('Delete custom certificate and switch to generated one?'),
- callback: function (options, success, response) {
- if (success) {
- let txt = gettext(
- 'API server will be restarted to use new certificates, please reload web-interface!',
- );
- Ext.getBody().mask(txt, ['pve-static-mask']);
- // reload after 10 seconds automatically
- Ext.defer(() => window.location.reload(true), 10000);
- }
- },
- },
- '-',
- {
- xtype: 'proxmoxButton',
- itemId: 'viewbtn',
- disabled: true,
- text: gettext('View Certificate'),
- handler: function () {
- this.up('grid').viewCertificate();
- },
- },
- ],
-
- columns: [
- {
- header: gettext('File'),
- width: 150,
- dataIndex: 'filename',
- },
- {
- header: gettext('Issuer'),
- flex: 1,
- dataIndex: 'issuer',
- },
- {
- header: gettext('Subject'),
- flex: 1,
- dataIndex: 'subject',
- },
- {
- header: gettext('Public Key Algorithm'),
- flex: 1,
- dataIndex: 'public-key-type',
- hidden: true,
- },
- {
- header: gettext('Public Key Size'),
- flex: 1,
- dataIndex: 'public-key-bits',
- hidden: true,
- },
- {
- header: gettext('Valid Since'),
- width: 150,
- dataIndex: 'notbefore',
- renderer: Proxmox.Utils.render_timestamp,
- },
- {
- header: gettext('Expires'),
- width: 150,
- dataIndex: 'notafter',
- renderer: Proxmox.Utils.render_timestamp,
- },
- {
- header: gettext('Subject Alternative Names'),
- flex: 1,
- dataIndex: 'san',
- renderer: PVE.Utils.render_san,
- },
- {
- header: gettext('Fingerprint'),
- dataIndex: 'fingerprint',
- hidden: true,
- },
- {
- header: gettext('PEM'),
- dataIndex: 'pem',
- hidden: true,
- },
- ],
-
- reload: function () {
- this.rstore.load();
- },
-
- viewCertificate: function () {
- let me = this;
- let selection = me.getSelection();
- if (!selection || selection.length < 1) {
- return;
- }
- var win = Ext.create('PVE.node.CertificateViewer', {
- cert: selection[0].data.filename,
- nodename: me.nodename,
- });
- win.show();
- },
-
- listeners: {
- itemdblclick: 'viewCertificate',
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- me.rstore = Ext.create('Proxmox.data.UpdateStore', {
- storeid: 'certs-' + me.nodename,
- model: 'pve-certificate',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/' + me.nodename + '/certificates/info',
- },
- });
-
- me.store = {
- type: 'diff',
- rstore: me.rstore,
- };
-
- me.callParent();
-
- me.mon(me.rstore, 'load', (store) =>
- me.down('#deletebtn').setDisabled(!store.getById('pveproxy-ssl.pem')),
- );
- me.rstore.startUpdate();
- me.on('destroy', me.rstore.stopUpdate, me.rstore);
- },
-});
-Ext.define('PVE.node.CmdMenu', {
- extend: 'Ext.menu.Menu',
- xtype: 'nodeCmdMenu',
-
- showSeparator: false,
-
- items: [
- {
- text: gettext('Create VM'),
- itemId: 'createvm',
- iconCls: 'fa fa-desktop',
- handler: function () {
- Ext.create('PVE.qemu.CreateWizard', {
- nodename: this.up('menu').nodename,
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Create CT'),
- itemId: 'createct',
- iconCls: 'fa fa-cube',
- handler: function () {
- Ext.create('PVE.lxc.CreateWizard', {
- nodename: this.up('menu').nodename,
- autoShow: true,
- });
- },
- },
- { xtype: 'menuseparator' },
- {
- text: gettext('Bulk Start'),
- itemId: 'bulkstart',
- iconCls: 'fa fa-fw fa-play',
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- nodename: this.up('menu').nodename,
- title: gettext('Bulk Start'),
- btnText: gettext('Start'),
- action: 'startall',
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Bulk Shutdown'),
- itemId: 'bulkstop',
- iconCls: 'fa fa-fw fa-stop',
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- nodename: this.up('menu').nodename,
- title: gettext('Bulk Shutdown'),
- btnText: gettext('Shutdown'),
- action: 'stopall',
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Bulk Suspend'),
- itemId: 'bulksuspend',
- iconCls: 'fa fa-fw fa-download',
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- nodename: this.up('menu').nodename,
- title: gettext('Bulk Suspend'),
- btnText: gettext('Suspend'),
- action: 'suspendall',
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Bulk Migrate'),
- itemId: 'bulkmigrate',
- iconCls: 'fa fa-fw fa-send-o',
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- nodename: this.up('menu').nodename,
- title: gettext('Bulk Migrate'),
- btnText: gettext('Migrate'),
- action: 'migrateall',
- autoShow: true,
- });
- },
- },
- { xtype: 'menuseparator' },
- {
- text: gettext('Shell'),
- itemId: 'shell',
- iconCls: 'fa fa-fw fa-terminal',
- handler: function () {
- let nodename = this.up('menu').nodename;
- PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, nodename, undefined);
- },
- },
- { xtype: 'menuseparator' },
- {
- text: gettext('Wake-on-LAN'),
- itemId: 'wakeonlan',
- iconCls: 'fa fa-fw fa-power-off',
- handler: function () {
- let nodename = this.up('menu').nodename;
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/wakeonlan`,
- method: 'POST',
- failure: (response, opts) =>
- Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: function (response, opts) {
- Ext.Msg.show({
- title: 'Success',
- icon: Ext.Msg.INFO,
- msg: Ext.String.format(
- gettext("Wake on LAN packet send for '{0}': '{1}'"),
- nodename,
- response.result.data,
- ),
- });
- },
- });
- },
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no nodename specified';
- }
-
- me.title = gettext('Node') + " '" + me.nodename + "'";
- me.callParent();
-
- let caps = Ext.state.Manager.get('GuiCap');
-
- if (!caps.vms['VM.Allocate']) {
- me.getComponent('createct').setDisabled(true);
- me.getComponent('createvm').setDisabled(true);
- }
- if (!caps.vms['VM.Migrate']) {
- me.getComponent('bulkmigrate').setDisabled(true);
- }
- if (!caps.vms['VM.PowerMgmt']) {
- me.getComponent('bulkstart').setDisabled(true);
- me.getComponent('bulkstop').setDisabled(true);
- me.getComponent('bulksuspend').setDisabled(true);
- }
- if (!caps.nodes['Sys.PowerMgmt']) {
- me.getComponent('wakeonlan').setDisabled(true);
- }
- if (!caps.nodes['Sys.Console']) {
- me.getComponent('shell').setDisabled(true);
- }
- if (me.pveSelNode.data.running) {
- me.getComponent('wakeonlan').setDisabled(true);
- }
-
- if (PVE.Utils.isStandaloneNode()) {
- me.getComponent('bulkmigrate').setVisible(false);
- }
- },
-});
-Ext.define('PVE.node.Config', {
- extend: 'PVE.panel.Config',
- alias: 'widget.PVE.node.Config',
-
- onlineHelp: 'chapter_system_administration',
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
- url: '/api2/json/nodes/' + nodename + '/status',
- interval: 5000,
- });
-
- var node_command = function (cmd) {
- Proxmox.Utils.API2Request({
- params: { command: cmd },
- url: '/nodes/' + nodename + '/status',
- method: 'POST',
- waitMsgTarget: me,
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- };
-
- var actionBtn = Ext.create('Ext.Button', {
- text: gettext('Bulk Actions'),
- iconCls: 'fa fa-fw fa-ellipsis-v',
- disabled: !caps.vms['VM.PowerMgmt'] && !caps.vms['VM.Migrate'],
- menu: new Ext.menu.Menu({
- items: [
- {
- text: gettext('Bulk Start'),
- iconCls: 'fa fa-fw fa-play',
- disabled: !caps.vms['VM.PowerMgmt'],
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- nodename: nodename,
- title: gettext('Bulk Start'),
- btnText: gettext('Start'),
- action: 'startall',
- });
- },
- },
- {
- text: gettext('Bulk Shutdown'),
- iconCls: 'fa fa-fw fa-stop',
- disabled: !caps.vms['VM.PowerMgmt'],
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- nodename: nodename,
- title: gettext('Bulk Shutdown'),
- btnText: gettext('Shutdown'),
- action: 'stopall',
- });
- },
- },
- {
- text: gettext('Bulk Suspend'),
- iconCls: 'fa fa-fw fa-download',
- disabled: !caps.vms['VM.PowerMgmt'],
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- nodename: nodename,
- title: gettext('Bulk Suspend'),
- btnText: gettext('Suspend'),
- action: 'suspendall',
- });
- },
- },
- {
- text: gettext('Bulk Migrate'),
- iconCls: 'fa fa-fw fa-send-o',
- disabled: !caps.vms['VM.Migrate'],
- hidden: PVE.Utils.isStandaloneNode(),
- handler: function () {
- Ext.create('PVE.window.BulkAction', {
- autoShow: true,
- nodename: nodename,
- title: gettext('Bulk Migrate'),
- btnText: gettext('Migrate'),
- action: 'migrateall',
- });
- },
- },
- ],
- }),
- });
-
- let restartBtn = Ext.create('Proxmox.button.Button', {
- text: gettext('Reboot'),
- disabled: !caps.nodes['Sys.PowerMgmt'],
- dangerous: true,
- confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename),
- handler: function () {
- node_command('reboot');
- },
- iconCls: 'fa fa-undo',
- });
-
- var shutdownBtn = Ext.create('Proxmox.button.Button', {
- text: gettext('Shutdown'),
- disabled: !caps.nodes['Sys.PowerMgmt'],
- dangerous: true,
- confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename),
- handler: function () {
- node_command('shutdown');
- },
- iconCls: 'fa fa-power-off',
- });
-
- var shellBtn = Ext.create('PVE.button.ConsoleButton', {
- disabled: !caps.nodes['Sys.Console'],
- text: gettext('Shell'),
- consoleType: 'shell',
- nodename: nodename,
- });
-
- me.items = [];
-
- Ext.apply(me, {
- title: gettext('Node') + " '" + nodename + "'",
- hstateid: 'nodetab',
- defaults: {
- statusStore: me.statusStore,
- },
- tbar: [restartBtn, shutdownBtn, shellBtn, actionBtn],
- });
-
- if (caps.nodes['Sys.Audit']) {
- me.items.push(
- {
- xtype: 'pveNodeSummary',
- title: gettext('Summary'),
- iconCls: 'fa fa-book',
- itemId: 'summary',
- },
- {
- xtype: 'pmxNotesView',
- title: gettext('Notes'),
- iconCls: 'fa fa-sticky-note-o',
- itemId: 'notes',
- },
- );
- }
-
- if (caps.nodes['Sys.Console']) {
- me.items.push({
- xtype: 'pveNoVncConsole',
- title: gettext('Shell'),
- iconCls: 'fa fa-terminal',
- itemId: 'jsconsole',
- consoleType: 'shell',
- xtermjs: true,
- nodename: nodename,
- });
- }
-
- if (caps.nodes['Sys.Audit']) {
- me.items.push(
- {
- xtype: 'proxmoxNodeServiceView',
- title: gettext('System'),
- iconCls: 'fa fa-cogs',
- itemId: 'services',
- expandedOnInit: true,
- restartCommand: 'reload', // avoid disruptions
- startOnlyServices: {
- pveproxy: true,
- pvedaemon: true,
- 'pve-cluster': true,
- },
- nodename: nodename,
- onlineHelp: 'pve_service_daemons',
- },
- {
- xtype: 'proxmoxNodeNetworkView',
- title: gettext('Network'),
- iconCls: 'fa fa-exchange',
- itemId: 'network',
- showApplyBtn: true,
- showAltNames: true,
- groups: ['services'],
- nodename: nodename,
- editOptions: {
- enableBridgeVlanIds: true,
- },
- onlineHelp: 'sysadmin_network_configuration',
- },
- {
- xtype: 'pveCertificatesView',
- title: gettext('Certificates'),
- iconCls: 'fa fa-certificate',
- itemId: 'certificates',
- groups: ['services'],
- nodename: nodename,
- },
- {
- xtype: 'proxmoxNodeDNSView',
- title: gettext('DNS'),
- iconCls: 'fa fa-globe',
- groups: ['services'],
- itemId: 'dns',
- nodename: nodename,
- onlineHelp: 'sysadmin_network_configuration',
- },
- {
- xtype: 'proxmoxNodeHostsView',
- title: gettext('Hosts'),
- iconCls: 'fa fa-globe',
- groups: ['services'],
- itemId: 'hosts',
- nodename: nodename,
- onlineHelp: 'sysadmin_network_configuration',
- },
- {
- xtype: 'proxmoxNodeOptionsView',
- title: gettext('Options'),
- iconCls: 'fa fa-gear',
- groups: ['services'],
- itemId: 'options',
- nodename: nodename,
- onlineHelp: 'proxmox_node_management',
- },
- {
- xtype: 'proxmoxNodeTimeView',
- title: gettext('Time'),
- itemId: 'time',
- groups: ['services'],
- nodename: nodename,
- iconCls: 'fa fa-clock-o',
- },
- );
- }
-
- if (caps.nodes['Sys.Syslog']) {
- me.items.push({
- xtype: 'proxmoxJournalView',
- title: gettext('System Log'),
- iconCls: 'fa fa-list',
- groups: ['services'],
- disabled: !caps.nodes['Sys.Syslog'],
- itemId: 'syslog',
- url: '/api2/extjs/nodes/' + nodename + '/journal',
- });
-
- if (caps.nodes['Sys.Modify']) {
- me.items.push({
- xtype: 'proxmoxNodeAPT',
- title: gettext('Updates'),
- iconCls: 'fa fa-refresh',
- expandedOnInit: true,
- disabled: !caps.nodes['Sys.Console'],
- // do we want to link to system updates instead?
- itemId: 'apt',
- upgradeBtn: {
- xtype: 'pveConsoleButton',
- disabled: Proxmox.UserName !== 'root@pam',
- text: gettext('Upgrade'),
- consoleType: 'upgrade',
- nodename: nodename,
- },
- nodename: nodename,
- });
-
- me.items.push({
- xtype: 'proxmoxNodeAPTRepositories',
- title: gettext('Repositories'),
- iconCls: 'fa fa-files-o',
- itemId: 'aptrepositories',
- nodename: nodename,
- onlineHelp: 'sysadmin_package_repositories',
- groups: ['apt'],
- });
- }
- }
-
- if (caps.nodes['Sys.Audit']) {
- me.items.push(
- {
- xtype: 'pveFirewallRules',
- iconCls: 'fa fa-shield',
- title: gettext('Firewall'),
- allow_iface: true,
- base_url: '/nodes/' + nodename + '/firewall/rules',
- list_refs_url: '/cluster/firewall/refs',
- itemId: 'firewall',
- firewall_type: 'node',
- },
- {
- xtype: 'pveFirewallOptions',
- title: gettext('Options'),
- iconCls: 'fa fa-gear',
- onlineHelp: 'pve_firewall_host_specific_configuration',
- groups: ['firewall'],
- base_url: '/nodes/' + nodename + '/firewall/options',
- fwtype: 'node',
- itemId: 'firewall-options',
- },
- );
- }
-
- if (caps.nodes['Sys.Audit']) {
- me.items.push(
- {
- xtype: 'pmxDiskList',
- title: gettext('Disks'),
- itemId: 'storage',
- expandedOnInit: true,
- iconCls: 'fa fa-hdd-o',
- nodename: nodename,
- includePartitions: true,
- supportsWipeDisk: true,
- },
- {
- xtype: 'pveLVMList',
- title: 'LVM',
- itemId: 'lvm',
- onlineHelp: 'chapter_lvm',
- iconCls: 'fa fa-square',
- groups: ['storage'],
- },
- {
- xtype: 'pveLVMThinList',
- title: 'LVM-Thin',
- itemId: 'lvmthin',
- onlineHelp: 'chapter_lvm',
- iconCls: 'fa fa-square-o',
- groups: ['storage'],
- },
- {
- xtype: 'pveDirectoryList',
- title: Proxmox.Utils.directoryText,
- itemId: 'directory',
- onlineHelp: 'chapter_storage',
- iconCls: 'fa fa-folder',
- groups: ['storage'],
- },
- {
- title: 'ZFS',
- itemId: 'zfs',
- onlineHelp: 'chapter_zfs',
- iconCls: 'fa fa-th-large',
- groups: ['storage'],
- xtype: 'pveZFSList',
- },
- {
- xtype: 'pveNodeCephStatus',
- title: 'Ceph',
- itemId: 'ceph',
- iconCls: 'fa fa-ceph',
- },
- {
- xtype: 'pveNodeCephConfigCrush',
- title: gettext('Configuration'),
- iconCls: 'fa fa-gear',
- groups: ['ceph'],
- itemId: 'ceph-config',
- },
- {
- xtype: 'pveNodeCephMonMgr',
- title: gettext('Monitor'),
- iconCls: 'fa fa-tv',
- groups: ['ceph'],
- itemId: 'ceph-monlist',
- },
- {
- xtype: 'pveNodeCephOsdTree',
- title: 'OSD',
- iconCls: 'fa fa-hdd-o',
- groups: ['ceph'],
- itemId: 'ceph-osdtree',
- },
- {
- xtype: 'pveNodeCephFSPanel',
- title: 'CephFS',
- iconCls: 'fa fa-folder',
- groups: ['ceph'],
- nodename: nodename,
- itemId: 'ceph-cephfspanel',
- },
- {
- xtype: 'pveNodeCephPoolList',
- title: gettext('Pools'),
- iconCls: 'fa fa-sitemap',
- groups: ['ceph'],
- itemId: 'ceph-pools',
- },
- {
- xtype: 'pveReplicaView',
- iconCls: 'fa fa-retweet',
- title: gettext('Replication'),
- itemId: 'replication',
- },
- );
- }
-
- if (caps.nodes['Sys.Syslog']) {
- me.items.push(
- {
- xtype: 'proxmoxLogView',
- title: gettext('Log'),
- iconCls: 'fa fa-list',
- groups: ['firewall'],
- onlineHelp: 'chapter_pve_firewall',
- url: '/api2/extjs/nodes/' + nodename + '/firewall/log',
- itemId: 'firewall-fwlog',
- log_select_timespan: true,
- submitFormat: 'U',
- },
- {
- xtype: 'cephLogView',
- title: gettext('Log'),
- itemId: 'ceph-log',
- iconCls: 'fa fa-list',
- groups: ['ceph'],
- onlineHelp: 'chapter_pveceph',
- url: '/api2/extjs/nodes/' + nodename + '/ceph/log',
- nodename: nodename,
- },
- );
- }
-
- me.items.push(
- {
- title: gettext('Task History'),
- iconCls: 'fa fa-list-alt',
- itemId: 'tasks',
- nodename: nodename,
- xtype: 'proxmoxNodeTasks',
- extraFilter: [
- {
- xtype: 'pveGuestIDSelector',
- fieldLabel: 'VMID',
- allowBlank: true,
- name: 'vmid',
- },
- ],
- },
- {
- title: gettext('Subscription'),
- iconCls: 'fa fa-support',
- itemId: 'support',
- xtype: 'pveNodeSubscription',
- nodename: nodename,
- },
- );
-
- me.callParent();
-
- me.mon(me.statusStore, 'load', function (store, records, success) {
- let uptimerec = store.data.get('uptime');
- let powermgmt = caps.nodes['Sys.PowerMgmt'] && uptimerec && uptimerec.data.value;
-
- restartBtn.setDisabled(!powermgmt);
- shutdownBtn.setDisabled(!powermgmt);
- shellBtn.setDisabled(!powermgmt);
- });
-
- me.on('afterrender', function () {
- me.statusStore.startUpdate();
- });
-
- me.on('destroy', function () {
- me.statusStore.stopUpdate();
- });
- },
-});
-Ext.define('PVE.node.CreateDirectory', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveCreateDirectory',
-
- subject: Proxmox.Utils.directoryText,
-
- showProgress: true,
-
- onlineHelp: 'chapter_storage',
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- me.isCreate = true;
-
- Ext.applyIf(me, {
- url: '/nodes/' + me.nodename + '/disks/directory',
- method: 'POST',
- items: [
- {
- xtype: 'pmxDiskSelector',
- name: 'device',
- nodename: me.nodename,
- diskType: 'unused',
- includePartitions: true,
- fieldLabel: gettext('Disk'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- comboItems: [
- ['ext4', 'ext4'],
- ['xfs', 'xfs'],
- ],
- fieldLabel: gettext('Filesystem'),
- name: 'filesystem',
- value: '',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'name',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'add_storage',
- fieldLabel: gettext('Add Storage'),
- value: '1',
- },
- ],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.node.Directorylist', {
- extend: 'Ext.grid.Panel',
- xtype: 'pveDirectoryList',
-
- viewModel: {
- data: {
- path: '',
- },
- formulas: {
- dirName: (get) => get('path')?.replace('/mnt/pve/', '') || undefined,
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- destroyDirectory: function () {
- let me = this;
- let vm = me.getViewModel();
- let view = me.getView();
-
- const dirName = vm.get('dirName');
-
- if (!view.nodename) {
- throw 'no node name specified';
- }
-
- if (!dirName) {
- throw 'no directory name specified';
- }
-
- Ext.create('PVE.window.SafeDestroyStorage', {
- url: `/nodes/${view.nodename}/disks/directory/${dirName}`,
- item: { id: dirName },
- taskName: 'dirremove',
- taskDone: () => {
- view.reload();
- },
- }).show();
- },
- },
-
- stateful: true,
- stateId: 'grid-node-directory',
- columns: [
- {
- text: gettext('Path'),
- dataIndex: 'path',
- flex: 1,
- },
- {
- header: gettext('Device'),
- flex: 1,
- dataIndex: 'device',
- },
- {
- header: gettext('Type'),
- width: 100,
- dataIndex: 'type',
- },
- {
- header: gettext('Options'),
- width: 100,
- dataIndex: 'options',
- },
- {
- header: gettext('Unit File'),
- hidden: true,
- dataIndex: 'unitfile',
- },
- ],
-
- rootVisible: false,
- useArrows: true,
-
- tbar: [
- {
- text: gettext('Reload'),
- iconCls: 'fa fa-refresh',
- handler: function () {
- this.up('panel').reload();
- },
- },
- {
- text: `${gettext('Create')}: ${gettext('Directory')}`,
- handler: function () {
- let view = this.up('panel');
- Ext.create('PVE.node.CreateDirectory', {
- nodename: view.nodename,
- listeners: {
- destroy: () => view.reload(),
- },
- autoShow: true,
- });
- },
- },
- '->',
- {
- xtype: 'tbtext',
- data: {
- dirName: undefined,
- },
- bind: {
- data: {
- dirName: '{dirName}',
- },
- },
- tpl: [
- '',
- gettext('Directory') + ' {dirName}:',
- '',
- Ext.String.format(gettext('No {0} selected'), gettext('directory')),
- ' ',
- ],
- },
- {
- text: gettext('More'),
- iconCls: 'fa fa-bars',
- disabled: true,
- bind: {
- disabled: '{!dirName}',
- },
- menu: [
- {
- text: gettext('Destroy'),
- itemId: 'remove',
- iconCls: 'fa fa-fw fa-trash-o',
- handler: 'destroyDirectory',
- disabled: true,
- bind: {
- disabled: '{!dirName}',
- },
- },
- ],
- },
- ],
-
- reload: function () {
- let me = this;
- me.store.load();
- me.store.sort();
- },
-
- listeners: {
- activate: function () {
- this.reload();
- },
- selectionchange: function (model, selected) {
- let me = this;
- let vm = me.getViewModel();
-
- vm.set('path', selected[0]?.data.path || '');
- },
- },
-
- initComponent: function () {
- let me = this;
-
- me.nodename = me.pveSelNode.data.node;
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- Ext.apply(me, {
- store: {
- fields: ['path', 'device', 'type', 'options', 'unitfile'],
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/disks/directory`,
- },
- sorters: 'path',
- },
- });
-
- me.callParent();
-
- Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
- me.reload();
- },
-});
-Ext.define('PVE.node.CreateLVM', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveCreateLVM',
-
- onlineHelp: 'chapter_lvm',
- subject: 'LVM Volume Group',
-
- showProgress: true,
- isCreate: true,
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- me.isCreate = true;
-
- Ext.applyIf(me, {
- url: `/nodes/${me.nodename}/disks/lvm`,
- method: 'POST',
- items: [
- {
- xtype: 'pmxDiskSelector',
- name: 'device',
- nodename: me.nodename,
- diskType: 'unused',
- includePartitions: true,
- fieldLabel: gettext('Disk'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'name',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'add_storage',
- fieldLabel: gettext('Add Storage'),
- value: '1',
- },
- ],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.node.LVMList', {
- extend: 'Ext.tree.Panel',
- xtype: 'pveLVMList',
-
- viewModel: {
- data: {
- volumeGroup: '',
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- destroyVolumeGroup: function () {
- let me = this;
- let vm = me.getViewModel();
- let view = me.getView();
-
- const volumeGroup = vm.get('volumeGroup');
-
- if (!view.nodename) {
- throw 'no node name specified';
- }
-
- if (!volumeGroup) {
- throw 'no volume group specified';
- }
-
- Ext.create('PVE.window.SafeDestroyStorage', {
- url: `/nodes/${view.nodename}/disks/lvm/${volumeGroup}`,
- item: { id: volumeGroup },
- taskName: 'lvmremove',
- taskDone: () => {
- view.reload();
- },
- }).show();
- },
- },
-
- emptyText: PVE.Utils.renderNotFound('VGs'),
-
- stateful: true,
- stateId: 'grid-node-lvm',
-
- rootVisible: false,
- useArrows: true,
-
- columns: [
- {
- xtype: 'treecolumn',
- text: gettext('Name'),
- dataIndex: 'name',
- flex: 1,
- },
- {
- text: gettext('Number of LVs'),
- dataIndex: 'lvcount',
- width: 150,
- align: 'right',
- },
- {
- header: gettext('Assigned to LVs'),
- width: 130,
- dataIndex: 'usage',
- tdCls: 'x-progressbar-default-cell',
- xtype: 'widgetcolumn',
- widget: {
- xtype: 'pveProgressBar',
- },
- },
- {
- header: gettext('Size'),
- width: 100,
- align: 'right',
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'size',
- },
- {
- header: gettext('Free'),
- width: 100,
- align: 'right',
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'free',
- },
- ],
-
- tbar: [
- {
- text: gettext('Reload'),
- iconCls: 'fa fa-refresh',
- handler: function () {
- this.up('panel').reload();
- },
- },
- {
- text: gettext('Create') + ': Volume Group',
- handler: function () {
- let view = this.up('panel');
- Ext.create('PVE.node.CreateLVM', {
- nodename: view.nodename,
- taskDone: () => view.reload(),
- autoShow: true,
- });
- },
- },
- '->',
- {
- xtype: 'tbtext',
- data: {
- volumeGroup: undefined,
- },
- bind: {
- data: {
- volumeGroup: '{volumeGroup}',
- },
- },
- tpl: [
- '',
- 'Volume group {volumeGroup}:',
- '',
- Ext.String.format(gettext('No {0} selected'), 'volume group'),
- ' ',
- ],
- },
- {
- text: gettext('More'),
- iconCls: 'fa fa-bars',
- disabled: true,
- bind: {
- disabled: '{!volumeGroup}',
- },
- menu: [
- {
- text: gettext('Destroy'),
- itemId: 'remove',
- iconCls: 'fa fa-fw fa-trash-o',
- handler: 'destroyVolumeGroup',
- disabled: true,
- bind: {
- disabled: '{!volumeGroup}',
- },
- },
- ],
- },
- ],
-
- reload: function () {
- let me = this;
- let sm = me.getSelectionModel();
- Proxmox.Utils.API2Request({
- url: `/nodes/${me.nodename}/disks/lvm`,
- waitMsgTarget: me,
- method: 'GET',
- failure: (response, opts) => Proxmox.Utils.setErrorMask(me, response.htmlStatus),
- success: function (response, opts) {
- sm.deselectAll();
- me.setRootNode(response.result.data);
- me.expandAll();
- },
- });
- },
-
- listeners: {
- activate: function () {
- this.reload();
- },
- selectionchange: function (model, selected) {
- let me = this;
- let vm = me.getViewModel();
-
- if (selected.length < 1 || selected[0].data.parentId !== 'root') {
- vm.set('volumeGroup', '');
- } else {
- vm.set('volumeGroup', selected[0].data.name);
- }
- },
- },
-
- selModel: 'treemodel',
- fields: [
- 'name',
- 'size',
- 'free',
- {
- type: 'string',
- name: 'iconCls',
- calculate: (data) => `fa x-fa-tree fa-${data.leaf ? 'hdd-o' : 'object-group'}`,
- },
- {
- type: 'number',
- name: 'usage',
- calculate: (data) => (data.size - data.free) / data.size,
- },
- ],
- sorters: 'name',
-
- initComponent: function () {
- let me = this;
-
- me.nodename = me.pveSelNode.data.node;
- if (!me.nodename) {
- throw 'no node name specified';
- }
- me.callParent();
-
- me.reload();
- },
-});
-Ext.define('PVE.node.CreateLVMThin', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveCreateLVMThin',
-
- onlineHelp: 'chapter_lvm',
- subject: 'LVM Thinpool',
-
- showProgress: true,
- isCreate: true,
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- Ext.applyIf(me, {
- url: `/nodes/${me.nodename}/disks/lvmthin`,
- method: 'POST',
- items: [
- {
- xtype: 'pmxDiskSelector',
- name: 'device',
- nodename: me.nodename,
- diskType: 'unused',
- includePartitions: true,
- fieldLabel: gettext('Disk'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'name',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'add_storage',
- fieldLabel: gettext('Add Storage'),
- value: '1',
- },
- ],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.node.LVMThinList', {
- extend: 'Ext.grid.Panel',
- xtype: 'pveLVMThinList',
-
- viewModel: {
- data: {
- thinPool: '',
- volumeGroup: '',
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- destroyThinPool: function () {
- let me = this;
- let vm = me.getViewModel();
- let view = me.getView();
-
- const thinPool = vm.get('thinPool');
- const volumeGroup = vm.get('volumeGroup');
-
- if (!view.nodename) {
- throw 'no node name specified';
- }
-
- if (!thinPool) {
- throw 'no thin pool specified';
- }
-
- if (!volumeGroup) {
- throw 'no volume group specified';
- }
-
- Ext.create('PVE.window.SafeDestroyStorage', {
- url: `/nodes/${view.nodename}/disks/lvmthin/${thinPool}`,
- params: { 'volume-group': volumeGroup },
- item: { id: `${volumeGroup}/${thinPool}` },
- taskName: 'lvmthinremove',
- taskDone: () => {
- view.reload();
- },
- }).show();
- },
- },
-
- emptyText: PVE.Utils.renderNotFound('Thin-Pool'),
-
- stateful: true,
- stateId: 'grid-node-lvmthin',
-
- rootVisible: false,
- useArrows: true,
-
- columns: [
- {
- text: gettext('Name'),
- dataIndex: 'lv',
- flex: 1,
- },
- {
- header: 'Volume Group',
- width: 110,
- dataIndex: 'vg',
- },
- {
- header: gettext('Usage'),
- width: 110,
- dataIndex: 'usage',
- tdCls: 'x-progressbar-default-cell',
- xtype: 'widgetcolumn',
- widget: {
- xtype: 'pveProgressBar',
- },
- },
- {
- header: gettext('Size'),
- width: 100,
- align: 'right',
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'lv_size',
- },
- {
- header: gettext('Used'),
- width: 100,
- align: 'right',
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'used',
- },
- {
- header: gettext('Metadata Usage'),
- width: 120,
- dataIndex: 'metadata_usage',
- tdCls: 'x-progressbar-default-cell',
- xtype: 'widgetcolumn',
- widget: {
- xtype: 'pveProgressBar',
- },
- },
- {
- header: gettext('Metadata Size'),
- width: 120,
- align: 'right',
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'metadata_size',
- },
- {
- header: gettext('Metadata Used'),
- width: 125,
- align: 'right',
- sortable: true,
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'metadata_used',
- },
- ],
-
- tbar: [
- {
- text: gettext('Reload'),
- iconCls: 'fa fa-refresh',
- handler: function () {
- this.up('panel').reload();
- },
- },
- {
- text: gettext('Create') + ': Thinpool',
- handler: function () {
- var view = this.up('panel');
- Ext.create('PVE.node.CreateLVMThin', {
- nodename: view.nodename,
- taskDone: () => view.reload(),
- autoShow: true,
- });
- },
- },
- '->',
- {
- xtype: 'tbtext',
- data: {
- thinPool: undefined,
- volumeGroup: undefined,
- },
- bind: {
- data: {
- thinPool: '{thinPool}',
- volumeGroup: '{volumeGroup}',
- },
- },
- tpl: [
- '',
- '',
- 'Thinpool {volumeGroup}/{thinPool}:',
- '', // volumeGroup
- 'Missing volume group (node running old version?)',
- ' ',
- '', // thinPool
- Ext.String.format(gettext('No {0} selected'), 'thinpool'),
- ' ',
- ],
- },
- {
- text: gettext('More'),
- iconCls: 'fa fa-bars',
- disabled: true,
- bind: {
- disabled: '{!volumeGroup || !thinPool}',
- },
- menu: [
- {
- text: gettext('Destroy'),
- itemId: 'remove',
- iconCls: 'fa fa-fw fa-trash-o',
- handler: 'destroyThinPool',
- disabled: true,
- bind: {
- disabled: '{!volumeGroup || !thinPool}',
- },
- },
- ],
- },
- ],
-
- reload: function () {
- let me = this;
- me.store.load();
- me.store.sort();
- },
-
- listeners: {
- activate: function () {
- this.reload();
- },
- selectionchange: function (model, selected) {
- let me = this;
- let vm = me.getViewModel();
-
- vm.set('volumeGroup', selected[0]?.data.vg || '');
- vm.set('thinPool', selected[0]?.data.lv || '');
- },
- },
-
- initComponent: function () {
- let me = this;
-
- me.nodename = me.pveSelNode.data.node;
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- Ext.apply(me, {
- store: {
- fields: [
- 'lv',
- 'lv_size',
- 'used',
- 'metadata_size',
- 'metadata_used',
- {
- type: 'number',
- name: 'usage',
- calculate: (data) => data.used / data.lv_size,
- },
- {
- type: 'number',
- name: 'metadata_usage',
- calculate: (data) => data.metadata_used / data.metadata_size,
- },
- ],
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/disks/lvmthin`,
- },
- sorters: 'lv',
- },
- });
-
- me.callParent();
-
- Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
- me.reload();
- },
-});
-Ext.define('PVE.mod.TempHelper', {
- //singleton: true,
-
- requires: ['Ext.util.Format'],
-
- statics: {
- CELSIUS: 0,
- FAHRENHEIT: 1
- },
-
- srcUnit: null,
- dstUnit: null,
-
- isValidUnit: function (unit) {
- return (
- Ext.isNumber(unit) && (unit === this.self.CELSIUS || unit === this.self.FAHRENHEIT)
- );
- },
-
- constructor: function (config) {
- this.srcUnit = config && this.isValidUnit(config.srcUnit) ? config.srcUnit : this.self.CELSIUS;
- this.dstUnit = config && this.isValidUnit(config.dstUnit) ? config.dstUnit : this.self.CELSIUS;
- },
-
- toFahrenheit: function (tempCelsius) {
- return Ext.isNumber(tempCelsius)
- ? tempCelsius * 9 / 5 + 32
- : NaN;
- },
-
- toCelsius: function (tempFahrenheit) {
- return Ext.isNumber(tempFahrenheit)
- ? (tempFahrenheit - 32) * 5 / 9
- : NaN;
- },
-
- getTemp: function (value) {
- if (this.srcUnit !== this.dstUnit) {
- switch (this.srcUnit) {
- case this.self.CELSIUS:
- switch (this.dstUnit) {
- case this.self.FAHRENHEIT:
- return this.toFahrenheit(value);
-
- default:
- Ext.raise({
- msg:
- 'Unsupported destination temperature unit: ' + this.dstUnit,
- });
- }
- case this.self.FAHRENHEIT:
- switch (this.dstUnit) {
- case this.self.CELSIUS:
- return this.toCelsius(value);
-
- default:
- Ext.raise({
- msg:
- 'Unsupported destination temperature unit: ' + this.dstUnit,
- });
- }
- default:
- Ext.raise({
- msg: 'Unsupported source temperature unit: ' + this.srcUnit,
- });
- }
- } else {
- return value;
- }
- },
-
- getUnit: function(plainText) {
- switch (this.dstUnit) {
- case this.self.CELSIUS:
- return plainText !== true ? '°C' : '\'C';
-
- case this.self.FAHRENHEIT:
- return plainText !== true ? '°F' : '\'F';
-
- default:
- Ext.raise({
- msg: 'Unsupported destination temperature unit: ' + this.srcUnit,
- });
- }
- },
-});
-Ext.define('PVE.node.StatusView', {
- extend: 'Proxmox.panel.StatusView',
- alias: 'widget.pveNodeStatus',
-
- minHeight: 360,
- flex: 1,
- collapsible: true,
- titleCollapse: true,
- bodyPadding: '20 15 20 15',
-
- layout: {
- type: 'table',
- columns: 2,
- trAttrs: { valign: 'top' },
- tableAttrs: {
- style: {
- width: '100%',
- },
- },
- },
-
- defaults: {
- xtype: 'pmxInfoWidget',
- padding: '0 10 2 10',
- },
-
- items: [
- // ========== PRIMARY KPIs (Tier 1) ==========
- {
- xtype: 'box',
- colspan: 2,
- padding: '0',
- html: 'Primary Metrics
',
- },
- {
- itemId: 'cpu',
- iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
- title: gettext('CPU Usage'),
- valueField: 'cpu',
- maxField: 'cpuinfo',
- renderer: function(value, record) {
- let result = Proxmox.Utils.render_node_cpu_usage(value, record);
- // Append CPU model if available
- if (record && record.cpuinfo && record.cpuinfo.model) {
- result += ` (${record.cpuinfo.model})`;
- }
- return result;
- },
- },
- {
- iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
- itemId: 'memory',
- title: gettext('Memory Usage'),
- valueField: 'memory',
- maxField: 'memory',
- warningThreshold: 0.9,
- criticalThreshold: 0.975,
- renderer: Proxmox.Utils.render_node_size_usage,
- },
- {
- itemId: 'ksm',
- iconCls: 'fa fa-fw fa-clone',
- printBar: false,
- title: gettext('KSM sharing'),
- textField: 'ksm',
- renderer: function (record) {
- return Proxmox.Utils.render_size(record.shared);
- },
- },
- {
- itemId: 'gpu',
- iconCls: 'fa fa-fw fa-desktop',
- title: gettext('GPU Usage'),
- printBar: false,
- textField: 'gpuStats',
- renderer: function(gpuStats) {
- if (!gpuStats || !gpuStats.Graphics) {
- return '';
- }
-
- let hasActiveGPU = false;
- let gpuName = '';
-
- // Check Intel GPUs
- if (gpuStats.Graphics.Intel) {
- const keys = Object.keys(gpuStats.Graphics.Intel).sort();
- if (keys.length > 0) {
- const gpuData = gpuStats.Graphics.Intel[keys[0]];
- hasActiveGPU = true;
- gpuName = gpuData.name;
- }
- }
-
- // Check NVIDIA GPUs
- if (gpuStats.Graphics.NVIDIA) {
- const keys = Object.keys(gpuStats.Graphics.NVIDIA).sort();
- if (keys.length > 0) {
- const stats = gpuStats.Graphics.NVIDIA[keys[0]].stats;
- hasActiveGPU = true;
- gpuName = stats.name;
- }
- }
-
- return hasActiveGPU ? gpuName : '';
- },
- },
- {
- itemId: 'gpu_usage',
- iconCls: 'fa fa-fw fa-desktop',
- title: gettext('GPU 0'),
- valueField: 'gpuStats',
- printBar: false,
- textField: 'gpuStats',
- renderer: function(gpuStats) {
- if (!gpuStats || !gpuStats.Graphics) {
- return '';
- }
-
- // Check Intel GPUs
- if (gpuStats.Graphics.Intel) {
- const keys = Object.keys(gpuStats.Graphics.Intel).sort();
- if (keys.length > 0) {
- const gpuData = gpuStats.Graphics.Intel[keys[0]];
- if (gpuData.stats.engines && gpuData.stats.engines['Render/3D']) {
- const usage = gpuData.stats.engines['Render/3D'].busy;
- return `${usage}%`;
- }
- }
- }
-
- // Check NVIDIA GPUs
- if (gpuStats.Graphics.NVIDIA) {
- const keys = Object.keys(gpuStats.Graphics.NVIDIA).sort();
- if (keys.length > 0) {
- const stats = gpuStats.Graphics.NVIDIA[keys[0]].stats;
- if (stats.utilization) {
- return `${stats.utilization.gpu}%`;
- }
- }
- }
-
- return '';
- },
- },
- {
- iconCls: 'fa fa-fw fa-hdd-o',
- itemId: 'rootfs',
- title: gettext('Disk (/) Usage'),
- valueField: 'rootfs',
- maxField: 'rootfs',
- renderer: Proxmox.Utils.render_node_size_usage,
- },
- {
- iconCls: 'fa fa-fw fa-refresh',
- itemId: 'swap',
- title: gettext('SWAP Usage'),
- valueField: 'swap',
- maxField: 'swap',
- warningThreshold: 0.4,
- criticalThreshold: 0.8,
- renderer: Proxmox.Utils.render_node_size_usage,
- },
- // Fill the remaining cell so the next colspan:2 section header starts on a new row.
- {
- xtype: 'box',
- html: '',
- padding: 0,
- },
-
- // ========== SECONDARY DETAILS (Tier 2) ==========
- {
- xtype: 'box',
- colspan: 2,
- padding: '15 0 5 0',
- html: 'Secondary Details
',
- },
- {
- itemId: 'load',
- iconCls: 'fa fa-fw fa-tasks',
- title: gettext('CPU Load Average'),
- printBar: false,
- textField: 'loadavg',
- },
- {
- itemId: 'wait',
- iconCls: 'fa fa-fw fa-clock-o',
- title: gettext('CPU I/O Delay'),
- valueField: 'wait',
- },
- {
- itemId: 'thermalCpu',
- colspan: 2,
- printBar: false,
- title: gettext('CPU Thermal State'),
- iconCls: 'fa fa-fw fa-thermometer-half',
- textField: 'sensorsOutput',
- renderer: function(value){
- // sensors configuration
- const cpuTempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS});
- // display configuration
- const itemsPerRow = 0;
- // ---
- let objValue;
- try {
- objValue = JSON.parse(value) || {};
- objValue = objValue[Object.keys(objValue)[0]] || {};
- } catch(e) {
- objValue = {};
- }
-
- const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort();
- const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort();
- const bINTEL = cpuKeysI.length > 0 ? true : false;
- const INTELPackagePrefix = 'Core' == 'Core' ? 'Core ' : 'Package id';
- const INTELPackageCaption = 'Core' == 'Core' ? 'Core' : 'Package';
- let AMDPackagePrefix = 'Tccd';
- let AMDPackageCaption = 'CCD';
-
- if (cpuKeysA.length > 0) {
- let bTccd = false;
- let bTctl = false;
- let bTdie = false;
- let bCpuCoreTemp = false;
- cpuKeysA.forEach((cpuKey, cpuIndex) => {
- let items = objValue[cpuKey];
- bTccd = Object.keys(items).findIndex(item => { return String(item).startsWith('Tccd'); }) >= 0;
- bTctl = Object.keys(items).findIndex(item => { return String(item).startsWith('Tctl'); }) >= 0;
- bTdie = Object.keys(items).findIndex(item => { return String(item).startsWith('Tdie'); }) >= 0;
- bCpuCoreTemp = Object.keys(items).findIndex(item => { return String(item) === 'CPU Core Temp'; }) >= 0;
- });
- if (bTccd && 'Core' == 'Core') {
- AMDPackagePrefix = 'Tccd';
- AMDPackageCaption = 'ccd';
- } else if (bCpuCoreTemp && 'Core' == 'Package') {
- AMDPackagePrefix = 'CPU Core Temp';
- AMDPackageCaption = 'CPU Core Temp';
- } else if (bTdie) {
- AMDPackagePrefix = 'Tdie';
- AMDPackageCaption = 'die';
- } else if (bTctl) {
- AMDPackagePrefix = 'Tctl';
- AMDPackageCaption = 'ctl';
- } else {
- AMDPackagePrefix = 'temp';
- AMDPackageCaption = 'Temp';
- }
- }
-
- const cpuKeys = bINTEL ? cpuKeysI : cpuKeysA;
- const cpuItemPrefix = bINTEL ? INTELPackagePrefix : AMDPackagePrefix;
- const cpuTempCaption = bINTEL ? INTELPackageCaption : AMDPackageCaption;
- const formatTemp = bINTEL ? '0' : '0.0';
- const cpuCount = cpuKeys.length;
- let temps = [];
-
- cpuKeys.forEach((cpuKey, cpuIndex) => {
- let cpuTemps = [];
- const items = objValue[cpuKey];
- const cpuModel = items.cpu_model || '';
-
- const itemKeys = Object.keys(items).filter(item => {
- if ('Core' == 'Core') {
- // In Core mode: only show individual cores/CCDs, exclude overall CPU temp
- return String(item).includes(cpuItemPrefix) || String(item).startsWith('Tccd');
- } else {
- // In Package mode: show overall CPU temp and package-level readings
- return String(item).includes(cpuItemPrefix) || String(item) === 'CPU Core Temp';
- }
- }).sort((a, b) => {
- // Sort cores numerically
- let numA = parseInt(a.match(/\d+/)?.[0] || '0', 10);
- let numB = parseInt(b.match(/\d+/)?.[0] || '0', 10);
- return numA - numB;
- });
-
- itemKeys.forEach((coreKey) => {
- try {
- let tempVal = NaN, tempMax = NaN, tempCrit = NaN;
- Object.keys(items[coreKey]).forEach((secondLevelKey) => {
- if (secondLevelKey.endsWith('_input')) {
- tempVal = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
- } else if (secondLevelKey.endsWith('_max')) {
- tempMax = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
- } else if (secondLevelKey.endsWith('_crit')) {
- tempCrit = cpuTempHelper.getTemp(parseFloat(items[coreKey][secondLevelKey]));
- }
- });
-
- if (!isNaN(tempVal)) {
- let tempStyle = '';
- if (!isNaN(tempMax) && tempVal >= tempMax) {
- tempStyle = 'color: #FFC300; font-weight: bold;';
- }
- if (!isNaN(tempCrit) && tempVal >= tempCrit) {
- tempStyle = 'color: red; font-weight: bold;';
- }
-
- let tempStr = '';
-
- // Enhanced parsing for AMD temperatures
- if (coreKey.startsWith('Tccd')) {
- let tempIndex = coreKey.match(/Tccd(\d+)/);
- if (tempIndex !== null && tempIndex.length > 1) {
- tempIndex = tempIndex[1];
- tempStr = `${cpuTempCaption} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
- } else {
- tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
- }
- }
- // Handle CPU Core Temp (single overall temperature)
- else if (coreKey === 'CPU Core Temp') {
- tempStr = `${cpuTempCaption}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
- }
- // Enhanced parsing for Intel cores (P-Core, E-Core, regular Core)
- else {
- let tempIndex = coreKey.match(/(?:P\s+Core|E\s+Core|Core)\s*(\d+)/);
- if (tempIndex !== null && tempIndex.length > 1) {
- tempIndex = tempIndex[1];
- let coreType = coreKey.startsWith('P Core') ? 'P Core' :
- coreKey.startsWith('E Core') ? 'E Core' :
- cpuTempCaption;
- tempStr = `${coreType} ${tempIndex}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
- } else {
- // fallback for CPUs which do not have a core index
- let coreType = coreKey.startsWith('P Core') ? 'P Core' :
- coreKey.startsWith('E Core') ? 'E Core' :
- cpuTempCaption;
- tempStr = `${coreType}: ${Ext.util.Format.number(tempVal, formatTemp)}${cpuTempHelper.getUnit()} `;
- }
- }
-
- cpuTemps.push(tempStr);
- }
- } catch (e) { /*_*/ }
- });
-
- if(cpuTemps.length > 0) {
- temps.push({ model: cpuModel, temps: cpuTemps });
- }
- });
-
- let html = '';
- temps.forEach((cpuData, cpuIndex) => {
- const strCoreTemps = cpuData.temps.map((strTemp, index, arr) => {
- return strTemp + (index + 1 < arr.length ? (itemsPerRow > 0 && (index + 1) % itemsPerRow === 0 ? ' ' : ' | ') : '');
- });
- if(strCoreTemps.length > 0) {
- let cpuLabel = cpuCount > 1 ? `Socket ${cpuIndex + 1}` : 'Socket 1';
- let cpuModelStr = cpuData.model || 'Unknown CPU';
-
- html += '';
- html += `${cpuModelStr} `;
- html += `${strCoreTemps.join('')} `;
- html += ' ';
- }
- });
- html += '
';
-
- return html.indexOf('') > 0
- ? '' + html + '
'
- : 'N/A';
- }
- },
- {
- itemId: 'gpu_details',
- colspan: 2,
- iconCls: 'fa fa-fw fa-desktop',
- title: gettext('GPU Details'),
- printBar: false,
- textField: 'gpuStats',
- renderer: function(gpuStats) {
- if (!gpuStats || !gpuStats.Graphics) {
- return '';
- }
-
- let html = '';
-
- // Intel GPUs - Secondary details
- if (gpuStats.Graphics.Intel) {
- Object.keys(gpuStats.Graphics.Intel).sort().forEach(key => {
- const gpuData = gpuStats.Graphics.Intel[key];
-
- let details = [];
-
- // All engine details
- if (gpuData.stats.engines) {
- if (gpuData.stats.engines['Render/3D']) {
- details.push(`Render/3D: ${gpuData.stats.engines['Render/3D'].busy}%`);
- }
- if (gpuData.stats.engines['Video']) {
- details.push(`Video: ${gpuData.stats.engines['Video'].busy}%`);
- }
- if (gpuData.stats.engines['Blitter']) {
- details.push(`Blitter: ${gpuData.stats.engines['Blitter'].busy}%`);
- }
- if (gpuData.stats.engines['VideoEnhance']) {
- details.push(`VideoEnhance: ${gpuData.stats.engines['VideoEnhance'].busy}%`);
- }
- }
-
- // Power
- if (gpuData.stats.power) {
- details.push(`Power: ${gpuData.stats.power?.GPU ?? 'N/A'} / ${gpuData.stats.power?.Package ?? 'N/A'} ${gpuData.stats.power?.unit || 'W'}`);
- }
-
- // Frequency
- if (gpuData.stats.frequency) {
- details.push(`Freq: ${gpuData.stats.frequency?.actual ?? 'N/A'}/${gpuData.stats.frequency?.requested ?? 'N/A'} ${gpuData.stats.frequency?.unit || 'MHz'}`);
- }
-
- html += '';
- html += `${gpuData.name} `;
- html += `${details.join(' | ')} `;
- html += ' ';
- });
- }
-
- // NVIDIA GPUs - Secondary details
- if (gpuStats.Graphics.NVIDIA) {
- Object.keys(gpuStats.Graphics.NVIDIA).sort().forEach(key => {
- const gpuData = gpuStats.Graphics.NVIDIA[key];
- const stats = gpuData.stats;
-
- let details = [];
-
- // Memory Utilization
- if (stats.utilization && stats.utilization.memory) {
- const memUsage = parseInt(stats.utilization.memory);
- let memStyle = '';
- if (memUsage >= 90) memStyle = 'color: #d9534f; font-weight: bold;';
- else if (memUsage >= 70) memStyle = 'color: #f0ad4e; font-weight: bold;';
- details.push(`MEM: ${stats.utilization.memory}% `);
- }
-
- // VRAM Usage
- if (stats.memory) {
- const vramUsedGB = parseInt(stats.memory.used);
- const vramTotalGB = parseInt(stats.memory.total);
- const vramPercent = (vramUsedGB / vramTotalGB) * 100;
- let vramStyle = '';
- if (vramPercent >= 90) vramStyle = 'color: #d9534f; font-weight: bold;';
- else if (vramPercent >= 70) vramStyle = 'color: #f0ad4e; font-weight: bold;';
- details.push(`VRAM: ${stats.memory.used}/${stats.memory.total} ${stats.memory.unit} `);
- }
-
- // Temperature
- if (stats.temperature) {
- let tempStyle = '';
- if (stats.temperature.gpu >= 80) {
- tempStyle = 'color: red; font-weight: bold;';
- } else if (stats.temperature.gpu >= 70) {
- tempStyle = 'color: #FFC300; font-weight: bold;';
- }
- details.push(`Temp: ${stats.temperature.gpu}${stats.temperature.unit} `);
- }
-
- // Power
- if (stats.power) {
- details.push(`Power: ${stats.power.draw}/${stats.power.limit} ${stats.power.unit}`);
- }
-
- html += '';
- html += `${stats.name} `;
- html += `${details.join(' | ')} `;
- html += ' ';
- });
- }
-
- html += '
';
- return html.indexOf(' ') > 0
- ? '' + html + '
'
- : '';
- },
- },
- {
- itemId: 'thermalNvme',
- colspan: 2,
- printBar: false,
- title: gettext('NVMe Temperatures'),
- iconCls: 'fa fa-fw fa-thermometer-half',
- textField: 'sensorsOutput',
- renderer: function(value) {
- // sensors configuration
- const addressPrefix = "nvme-pci-";
- const sensorName = "Composite";
- const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: PVE.mod.TempHelper.CELSIUS});
- // display configuration
- const itemsPerRow = 0;
- // ---
- let objValue;
- try {
- objValue = JSON.parse(value) || {};
- objValue = objValue[Object.keys(objValue)[0]] || {};
- } catch(e) {
- objValue = {};
- }
- const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();
- let nvmeData = [];
- nvmeKeys.forEach((nvmeKey, index) => {
- try {
- let tempVal = NaN, tempMax = NaN, tempCrit = NaN, model = '', serial = '';
- Object.keys(objValue[nvmeKey][sensorName]).forEach((secondLevelKey) => {
- if (secondLevelKey.endsWith('_input')) {
- tempVal = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
- } else if (secondLevelKey.endsWith('_max')) {
- tempMax = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
- } else if (secondLevelKey.endsWith('_crit')) {
- tempCrit = tempHelper.getTemp(parseFloat(objValue[nvmeKey][sensorName][secondLevelKey]));
- }
- });
- model = objValue[nvmeKey]['model'] || 'Unknown';
- serial = objValue[nvmeKey]['serial'] || '';
-
- if (!isNaN(tempVal)) {
- let tempStyle = '';
- if (!isNaN(tempMax) && tempVal >= tempMax) {
- tempStyle = 'color: #FFC300; font-weight: bold;';
- }
- if (!isNaN(tempCrit) && tempVal >= tempCrit) {
- tempStyle = 'color: red; font-weight: bold;';
- }
- nvmeData.push({
- model: model,
- serial: serial,
- temp: tempVal,
- tempStyle: tempStyle,
- unit: tempHelper.getUnit()
- });
- }
- } catch(e) { /*_*/ }
- });
-
- if (nvmeData.length === 0) {
- return 'N/A';
- }
-
- let html = '';
- nvmeData.forEach((data) => {
- let deviceName = data.model;
- if (data.serial) {
- deviceName += ` (${data.serial})`;
- }
- html += '';
- html += `${deviceName} `;
- html += `${Ext.util.Format.number(data.temp, '0.0')}${data.unit} `;
- html += ' ';
- });
- html += '
';
- return '' + html + '
';
- }
- },
-
- // ========== TERTIARY DIAGNOSTICS (Tier 3) ==========
- {
- xtype: 'box',
- colspan: 2,
- padding: '15 0 5 0',
- html: 'Diagnostics
',
- },
- {
- itemId: 'speedFan',
- colspan: 2,
- printBar: false,
- title: gettext('System Fans'),
- iconCls: 'fa fa-fw fa-snowflake-o',
- textField: 'sensorsOutput',
- renderer: function(value) {
- // ---
- let objValue;
- try {
- objValue = JSON.parse(value) || {};
- objValue = objValue[Object.keys(objValue)[0]] || {};
- } catch(e) {
- objValue = {};
- }
-
- // Recursive function to find fan keys and values
- function findFanKeys(obj, fanKeys, parentKey = null) {
- Object.keys(obj).forEach(key => {
- const value = obj[key];
- if (typeof value === 'object' && value !== null) {
- // If the value is an object, recursively call the function
- findFanKeys(value, fanKeys, key);
- } else if (/^fan[0-9]+(_input)?$/.test(key)) {
- if (true != true && value === 0) {
- // Skip this fan if DISPLAY_ZERO_SPEED_FANS is false and value is 0
- return;
- }
- // If the key matches the pattern, add the parent key and value to the fanKeys array
- fanKeys.push({ key: parentKey, value: value });
- }
- });
- }
-
- let speeds = [];
- // Loop through the parent keys
- Object.keys(objValue).forEach(parentKey => {
- const parentObj = objValue[parentKey];
- // Array to store fan keys and values
- const fanKeys = [];
- // Call the recursive function to find fan keys and values
- findFanKeys(parentObj, fanKeys);
- // Sort the fan keys
- fanKeys.sort((a, b) => {
- if (a.key < b.key) return -1;
- if (a.key > b.key) return 1;
- return 0;
- });
- // Process each fan key and value
- fanKeys.forEach(({ key: fanKey, value: fanSpeed }) => {
- try {
- const fan = fanKey.charAt(0).toUpperCase() + fanKey.slice(1); // Capitalize the first letter of fanKey
- speeds.push(`${fan}: ${fanSpeed} RPM`);
- } catch(e) {
- console.error(`Error retrieving fan speed for ${fanKey} in ${parentKey}:`, e); // Debug: Log specific error
- }
- });
- });
- return '' + (speeds.length > 0 ? speeds.join(' | ') : 'N/A') + '
';
- }
- },
- {
- itemId: 'gpuFans',
- colspan: 2,
- printBar: false,
- title: gettext('GPU Fans'),
- iconCls: 'fa fa-fw fa-snowflake-o',
- textField: 'gpuStats',
- renderer: function(gpuStats) {
- if (!gpuStats || !gpuStats.Graphics || !gpuStats.Graphics.NVIDIA) {
- return 'N/A';
- }
-
- let rows = [];
-
- Object.keys(gpuStats.Graphics.NVIDIA).sort().forEach(key => {
- const gpuData = gpuStats.Graphics.NVIDIA[key];
- const stats = gpuData?.stats;
- const fan = stats?.fan;
-
- if (!fan || fan.speed === undefined || fan.speed === null) {
- return;
- }
-
- const gpuName = stats?.name || key;
- const unit = fan.unit || '%';
- rows.push(
- ' ' +
- `${gpuName} ` +
- `Fan: ${fan.speed}${unit} ` +
- ' ',
- );
- });
-
- if (rows.length === 0) {
- return 'N/A';
- }
-
- return '';
- },
- },
- {
- itemId: 'upsc',
- colspan: 2,
- printBar: false,
- title: gettext('UPS Status'),
- iconCls: 'fa fa-fw fa-battery-three-quarters',
- textField: 'upsStats',
- renderer: function(value) {
- let objValue = {};
- try {
- // Parse the UPS data
- if (typeof value === 'string') {
- objValue = JSON.parse(value) || {};
- } else if (typeof value === 'object') {
- objValue = value || {};
- }
- } catch(e) {
- objValue = {};
- }
-
- // If objValue is null or empty, return N/A
- if (!objValue || Object.keys(objValue).length === 0) {
- return 'N/A';
- }
-
- // Helper function to get status color
- function getStatusColor(status) {
- if (!status) return '#999';
- const statusUpper = status.toUpperCase();
- if (statusUpper.includes('OL')) return null;
- if (statusUpper.includes('OB')) return '#d9534f';
- if (statusUpper.includes('LB')) return '#d9534f';
- return '#f0ad4e';
- }
-
- // Helper function to get load/charge color
- function getPercentageColor(value, isLoad = false) {
- if (!value || isNaN(value)) return '#999';
- const num = parseFloat(value);
- if (isLoad) {
- if (num >= 80) return '#d9534f';
- if (num >= 60) return '#f0ad4e';
- return null;
- } else {
- if (num <= 20) return '#d9534f';
- if (num <= 50) return '#f0ad4e';
- return null;
- }
- }
-
- // Helper function to format runtime
- function formatRuntime(seconds) {
- if (!seconds || isNaN(seconds)) return 'N/A';
- const mins = Math.floor(seconds / 60);
- const secs = seconds % 60;
- return `${mins}m ${secs}s`;
- }
-
- // Process each UPS in the data
- let allDisplayItems = [];
-
- Object.keys(objValue).forEach(upsKey => {
- const upsData = objValue[upsKey];
-
- // Extract key UPS information
- const batteryCharge = upsData['battery.charge'];
- const batteryRuntime = upsData['battery.runtime'];
- const inputVoltage = upsData['input.voltage'];
- const upsLoad = upsData['ups.load'];
- const upsStatus = upsData['ups.status'];
- const upsModel = upsData['ups.model'] || upsData['device.model'];
- const testResult = upsData['ups.test.result'];
- const batteryChargeLow = upsData['battery.charge.low'];
- const batteryRuntimeLow = upsData['battery.runtime.low'];
- const upsRealPowerNominal = upsData['ups.realpower.nominal'];
- const batteryMfrDate = upsData['battery.mfr.date'];
-
- // Main status line with all metrics
- let statusLine = '';
-
- // Status
- if (upsStatus) {
- const statusUpper = upsStatus.toUpperCase();
- let statusText = 'Unknown';
- let statusColor = '#f0ad4e';
-
- if (statusUpper.includes('OL')) {
- statusText = 'Online';
- statusColor = null;
- } else if (statusUpper.includes('OB')) {
- statusText = 'On Battery';
- statusColor = '#d9534f';
- } else if (statusUpper.includes('LB')) {
- statusText = 'Low Battery';
- statusColor = '#d9534f';
- } else {
- statusText = upsStatus;
- statusColor = '#f0ad4e';
- }
-
- let statusStyle = statusColor ? ('color: ' + statusColor + ';') : '';
- statusLine += 'Status: ' + statusText + ' ';
- } else {
- statusLine += 'Status: N/A ';
- }
-
- // Battery charge
- if (statusLine) statusLine += ' | ';
- if (batteryCharge) {
- const chargeColor = getPercentageColor(batteryCharge, false);
- let chargeStyle = chargeColor ? ('color: ' + chargeColor + ';') : '';
- statusLine += 'Battery: ' + batteryCharge + '% ';
- } else {
- statusLine += 'Battery: N/A ';
- }
-
- // Load percentage
- if (statusLine) statusLine += ' | ';
- if (upsLoad) {
- const loadColor = getPercentageColor(upsLoad, true);
- let loadStyle = loadColor ? ('color: ' + loadColor + ';') : '';
- statusLine += 'Load: ' + upsLoad + '% ';
- } else {
- statusLine += 'Load: N/A ';
- }
-
- // Runtime
- if (statusLine) statusLine += ' | ';
- if (batteryRuntime) {
- const runtime = parseInt(batteryRuntime);
- const runtimeLowThreshold = batteryRuntimeLow ? parseInt(batteryRuntimeLow) : 600;
- let runtimeColor = null;
- if (runtime <= runtimeLowThreshold / 2) runtimeColor = '#d9534f';
- else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e';
- let runtimeStyle = runtimeColor ? ('color: ' + runtimeColor + ';') : '';
- statusLine += 'Runtime: ' + formatRuntime(runtime) + ' ';
- } else {
- statusLine += 'Runtime: N/A ';
- }
-
- // Input voltage
- if (statusLine) statusLine += ' | ';
- if (inputVoltage) {
- statusLine += 'Input: ' + parseFloat(inputVoltage).toFixed(0) + 'V ';
- } else {
- statusLine += 'Input: N/A ';
- }
-
- // Calculate actual watt usage
- if (statusLine) statusLine += ' | ';
- let actualWattage = null;
- if (upsLoad && upsRealPowerNominal) {
- const load = parseFloat(upsLoad);
- const nominal = parseFloat(upsRealPowerNominal);
- if (!isNaN(load) && !isNaN(nominal)) {
- actualWattage = Math.round((load / 100) * nominal);
- }
- }
-
- // Real power (calculated watt usage)
- if (actualWattage !== null) {
- statusLine += 'Output: ' + actualWattage + 'W ';
- } else {
- statusLine += 'Output: N/A ';
- }
-
- // Append battery MFD + last test to the same line (single-line UPS summary)
- statusLine += ' | Battery MFD: ' + (batteryMfrDate || 'N/A');
- if (testResult && !testResult.toLowerCase().includes('no test')) {
- const testColor = testResult.toLowerCase().includes('passed') ? null : '#d9534f';
- let testStyle = testColor ? ('color: ' + testColor + ';') : '';
- statusLine += ' | Test: ' + testResult + ' ';
- } else {
- statusLine += ' | Test: N/A';
- }
-
- // Build UPS display with model on left, details on right
- let upsHtml = '';
- upsHtml += '' + (upsModel || upsKey) + ' ';
- upsHtml += '' + statusLine + ' ';
- upsHtml += ' ';
-
- allDisplayItems.push(upsHtml);
- });
-
- // Format the final output for all UPS devices
- return '' + allDisplayItems.join('') + '
';
- }
- },
- {
- xtype: 'box',
- colspan: 2,
- padding: '15 0 5 0',
- html: 'System
',
- },
- {
- colspan: 2,
- title: gettext('Kernel Version'),
- printBar: false,
- // TODO: remove with next major and only use newish current-kernel textfield
- multiField: true,
- //textField: 'current-kernel',
- renderer: ({ data }) => {
- if (!data['current-kernel']) {
- return data.kversion;
- }
- let kernel = data['current-kernel'];
- let buildDate = kernel.version.match(/\((.+)\)\s*$/)?.[1] ?? 'unknown';
- return `${kernel.sysname} ${kernel.release} (${buildDate})`;
- },
- value: '',
- },
- {
- colspan: 2,
- title: gettext('Boot Mode'),
- printBar: false,
- textField: 'boot-info',
- renderer: (boot) => {
- if (boot.mode === 'legacy-bios') {
- return 'Legacy BIOS';
- } else if (boot.mode === 'efi') {
- return `EFI${boot.secureboot ? ' (Secure Boot)' : ''}`;
- }
- return Proxmox.Utils.unknownText;
- },
- value: '',
- },
- {
- itemId: 'version',
- colspan: 2,
- printBar: false,
- title: gettext('Manager Version'),
- textField: 'pveversion',
- value: '',
- },
- {
- itemId: 'pve_mod_version',
- colspan: 2,
- printBar: false,
- title: gettext('Sensor Mod Version'),
- textField: 'pveModVersion',
- value: '',
- },
- ],
-
- updateTitle: function () {
- var me = this;
- var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime'));
- me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')');
- },
-
- initComponent: function () {
- let me = this;
-
- let stateProvider = Ext.state.Manager.getProvider();
- let repoLink = stateProvider.encodeHToken({
- view: 'server',
- rid: `node/${me.pveSelNode.data.node}`,
- ltab: 'tasks',
- nodetab: 'aptrepositories',
- });
-
- me.items.push({
- xtype: 'pmxNodeInfoRepoStatus',
- itemId: 'repositoryStatus',
- product: 'Proxmox VE',
- repoLink: `#${repoLink}`,
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.node.SubscriptionKeyEdit', {
- extend: 'Proxmox.window.Edit',
-
- title: gettext('Upload Subscription Key'),
- width: 350,
-
- items: {
- xtype: 'textfield',
- name: 'key',
- value: '',
- fieldLabel: gettext('Subscription Key'),
- labelWidth: 120,
- getSubmitValue: function () {
- return this.processRawValue(this.getRawValue())?.trim();
- },
- },
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
-
- me.load();
- },
-});
-
-Ext.define('PVE.node.Subscription', {
- extend: 'Proxmox.grid.ObjectGrid',
-
- alias: ['widget.pveNodeSubscription'],
-
- onlineHelp: 'getting_help',
-
- viewConfig: {
- enableTextSelection: true,
- },
-
- showReport: function () {
- var me = this;
-
- var getReportFileName = function () {
- var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i');
- return `${me.nodename}-pve-report-${now}.txt`;
- };
-
- var view = Ext.createWidget('component', {
- itemId: 'system-report-view',
- scrollable: true,
- style: {
- 'white-space': 'pre',
- 'font-family': 'monospace',
- padding: '5px',
- },
- });
-
- var reportWindow = Ext.create('Ext.window.Window', {
- title: gettext('System Report'),
- width: 1024,
- height: 600,
- layout: 'fit',
- modal: true,
- buttons: [
- '->',
- {
- text: gettext('Download'),
- handler: function () {
- var fileContent = Ext.String.htmlDecode(
- reportWindow.getComponent('system-report-view').html,
- );
- var fileName = getReportFileName();
-
- // Internet Explorer
- if (window.navigator.msSaveOrOpenBlob) {
- navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName);
- } else {
- let element = document.createElement('a');
- element.setAttribute(
- 'href',
- 'data:text/plain;charset=utf-8,' + encodeURIComponent(fileContent),
- );
- element.setAttribute('download', fileName);
- element.style.display = 'none';
- document.body.appendChild(element);
- element.click();
- document.body.removeChild(element);
- }
- },
- },
- ],
- items: view,
- });
-
- Proxmox.Utils.API2Request({
- url: '/api2/extjs/nodes/' + me.nodename + '/report',
- method: 'GET',
- waitMsgTarget: me,
- failure: function (response) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response) {
- var report = Ext.htmlEncode(response.result.data);
- reportWindow.show();
- view.update(report);
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- let rows = {
- productname: {
- header: gettext('Type'),
- },
- key: {
- header: gettext('Subscription Key'),
- },
- status: {
- header: gettext('Status'),
- renderer: (v) => {
- let message = me.getObjectValue('message');
- return message ? `${v}: ${message}` : v;
- },
- },
- message: {
- visible: false,
- },
- serverid: {
- header: gettext('Server ID'),
- },
- sockets: {
- header: gettext('Sockets'),
- },
- checktime: {
- header: gettext('Last checked'),
- renderer: Proxmox.Utils.render_timestamp,
- },
- nextduedate: {
- header: gettext('Next due date'),
- },
- signature: {
- header: gettext('Signed/Offline'),
- renderer: (v) => (v ? gettext('Yes') : gettext('No')),
- },
- };
-
- Ext.apply(me, {
- url: `/api2/json/nodes/${me.nodename}/subscription`,
- cwidth1: 170,
- tbar: [
- {
- text: gettext('Upload Subscription Key'),
- handler: () =>
- Ext.create('PVE.node.SubscriptionKeyEdit', {
- autoShow: true,
- url: `/api2/extjs/nodes/${me.nodename}/subscription`,
- listeners: {
- destroy: () => me.rstore.load(),
- },
- }),
- },
- {
- text: gettext('Check'),
- handler: () =>
- Proxmox.Utils.API2Request({
- params: { force: 1 },
- url: `/nodes/${me.nodename}/subscription`,
- method: 'POST',
- waitMsgTarget: me,
- failure: (response) =>
- Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- callback: () => me.rstore.load(),
- }),
- },
- {
- text: gettext('Remove Subscription'),
- xtype: 'proxmoxStdRemoveButton',
- confirmMsg: gettext('Are you sure you want to remove the subscription key?'),
- baseurl: `/nodes/${me.nodename}/subscription`,
- dangerous: true,
- selModel: false,
- callback: () => me.rstore.load(),
- },
- '-',
- {
- text: gettext('System Report'),
- handler: function () {
- Proxmox.Utils.checked_command(function () {
- me.showReport();
- });
- },
- },
- ],
- rows: rows,
- listeners: {
- activate: () => me.rstore.load(),
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.node.Summary', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveNodeSummary',
-
- scrollable: true,
- bodyPadding: 5,
-
- showVersions: function () {
- var me = this;
-
- // Note: we use simply text/html here, because ExtJS grid has problems
- // with cut&paste
-
- var nodename = me.pveSelNode.data.node;
-
- var view = Ext.createWidget('component', {
- autoScroll: true,
- id: 'pkgversions',
- padding: 5,
- style: {
- 'white-space': 'pre',
- 'font-family': 'monospace',
- },
- });
-
- var win = Ext.create('Ext.window.Window', {
- title: gettext('Package versions'),
- width: 600,
- height: 600,
- layout: 'fit',
- modal: true,
- items: [view],
- buttons: [
- {
- xtype: 'button',
- iconCls: 'fa fa-clipboard',
- handler: function (button) {
- window
- .getSelection()
- .selectAllChildren(document.getElementById('pkgversions'));
- document.execCommand('copy');
- },
- text: gettext('Copy'),
- },
- {
- text: gettext('Ok'),
- handler: function () {
- this.up('window').close();
- },
- },
- ],
- });
-
- Proxmox.Utils.API2Request({
- waitMsgTarget: me,
- url: `/nodes/${nodename}/apt/versions`,
- method: 'GET',
- failure: function (response, opts) {
- win.close();
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response, opts) {
- win.show();
- let text = '';
- Ext.Array.each(response.result.data, function (rec) {
- let version = 'not correctly installed';
- let pkg = rec.Package;
- if (rec.OldVersion && rec.CurrentState === 'Installed') {
- version = rec.OldVersion;
- }
- if (rec.RunningKernel) {
- text += `${pkg}: ${version} (running kernel: ${rec.RunningKernel})\n`;
- } else if (rec.ManagerVersion) {
- text += `${pkg}: ${version} (running version: ${rec.ManagerVersion})\n`;
- } else {
- text += `${pkg}: ${version}\n`;
- }
- });
-
- view.update(Ext.htmlEncode(text));
- },
- });
- },
-
- updateRepositoryStatus: function () {
- let me = this;
- let repoStatus = me.nodeStatus.down('#repositoryStatus');
-
- let nodename = me.pveSelNode.data.node;
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/apt/repositories`,
- method: 'GET',
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: (response) =>
- repoStatus.setRepositoryInfo(response.result.data['standard-repos']),
- });
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/subscription`,
- method: 'GET',
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- success: function (response, opts) {
- const res = response.result;
- const subscription = res?.data?.status.toLowerCase() === 'active';
- repoStatus.setSubscriptionStatus(subscription);
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- if (!me.statusStore) {
- throw 'no status storage specified';
- }
-
- var rstore = me.statusStore;
-
- var version_btn = new Ext.Button({
- text: gettext('Package versions'),
- handler: function () {
- Proxmox.Utils.checked_command(function () {
- me.showVersions();
- });
- },
- });
-
- var rrdstore = Ext.create('Proxmox.data.RRDStore', {
- rrdurl: '/api2/json/nodes/' + nodename + '/rrddata',
- model: 'pve-rrd-node',
- });
-
- let nodeStatus = Ext.create('PVE.node.StatusView', {
- xtype: 'pveNodeStatus',
- rstore: rstore,
- width: 770,
- pveSelNode: me.pveSelNode,
- });
-
- Ext.apply(me, {
- tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' }],
- nodeStatus: nodeStatus,
- items: [
-{
- xtype: 'container',
- itemId: 'summarycontainer',
- layout: 'column',
- minWidth: 700,
- defaults: {
- minHeight: 350,
- padding: 5,
- columnWidth: 1,
- },
- items: [
- nodeStatus,
- ]
-},
- {
- xtype: 'container',
- itemId: 'itemcontainer',
- layout: 'column',
- minWidth: 700,
- defaults: {
- minHeight: 360,
- padding: 5,
- columnWidth: 1,
- },
- items: [
- //nodeStatus,
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('CPU Usage'),
- fields: ['cpu', 'iowait'],
- fieldTitles: [gettext('CPU usage'), gettext('IO delay')],
- unit: 'percent',
- store: rrdstore,
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Server Load'),
- fields: ['loadavg'],
- fieldTitles: [gettext('Load average')],
- store: rrdstore,
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Memory usage'),
- fields: [
- {
- yField: 'memtotal',
- title: gettext('Total'),
- tooltip: {
- trackMouse: true,
- renderer: function (toolTip, record, item) {
- let value = record.get('memtotal');
-
- if (value === null) {
- toolTip.setHtml(gettext('No Data'));
- } else {
- let total = Proxmox.Utils.format_size(value);
- let time = new Date(record.get('time'));
-
- let avail = record.get('memavailable');
- let availText = '';
- if (Ext.isNumeric(avail)) {
- let v = Proxmox.Utils.format_size(avail);
- availText = ` (${gettext('Available')}: ${v})`;
- }
-
- toolTip.setHtml(
- `${gettext('Total')}: ${total}${availText} ${time}`,
- );
- }
- },
- },
- },
- {
- yField: 'memused',
- title: gettext('Used'),
- tooltip: {
- trackMouse: true,
- renderer: function (toolTip, record, item) {
- let value = record.get('memused');
-
- if (value === null) {
- toolTip.setHtml(gettext('No Data'));
- } else {
- let total = Proxmox.Utils.format_size(value);
- let time = new Date(record.get('time'));
-
- let arc = record.get('arcsize');
- let arcText = '';
- if (Ext.isNumeric(arc) && arc > 1024 * 1024) {
- let v = Proxmox.Utils.format_size(value - arc);
- arcText = ` (${gettext('Without ZFS ARC')}: ${v})`;
- }
-
- toolTip.setHtml(
- `${gettext('Used')}: ${total}${arcText} ${time}`,
- );
- }
- },
- },
- },
- 'arcsize',
- {
- type: 'line',
- fill: false,
- yField: 'memavailable',
- title: gettext('Available'),
- style: {
- lineWidth: 2.5,
- opacity: 1,
- },
- },
- ],
- fieldTitles: [
- gettext('Total'),
- gettext('Used'),
- gettext('ZFS ARC'),
- gettext('Available'),
- ],
- colors: ['#94ae0a', '#115fa6', '#24AD9A', '#bbde0d'],
- unit: 'bytes',
- powerOfTwo: true,
- store: rrdstore,
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Network Traffic'),
- fields: ['netin', 'netout'],
- fieldTitles: [gettext('Incoming'), gettext('Outgoing')],
- store: rrdstore,
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('CPU Pressure Stall'),
- fieldTitles: ['Some'],
- fields: ['pressurecpusome'],
- colors: ['#FFD13E', '#A61120'],
- store: rrdstore,
- unit: 'percent',
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('IO Pressure Stall'),
- fieldTitles: ['Some', 'Full'],
- fields: ['pressureiosome', 'pressureiofull'],
- colors: ['#FFD13E', '#A61120'],
- store: rrdstore,
- unit: 'percent',
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Memory Pressure Stall'),
- fieldTitles: ['Some', 'Full'],
- fields: ['pressurememorysome', 'pressurememoryfull'],
- colors: ['#FFD13E', '#A61120'],
- store: rrdstore,
- unit: 'percent',
- },
- {
- // todo find me
- xtype: 'proxmoxRRDChart',
- title: gettext('Graphics Intel'),
- fields: ['netin', 'netout'],
- fieldTitles: [gettext('Incoming'), gettext('Outgoing')],
- store: rrdstore,
- },
- ],
- listeners: {
- resize: function (panel) {
- Proxmox.Utils.updateColumns(panel);
- },
- },
- },
- ],
- listeners: {
- activate: function () {
- rstore.setInterval(1000);
- rstore.startUpdate(); // just to be sure
- rrdstore.startUpdate();
- },
- destroy: function () {
- rstore.setInterval(5000); // don't stop it, it's not ours!
- rrdstore.stopUpdate();
- },
- },
- });
-
- me.updateRepositoryStatus();
-
- me.callParent();
-
- let sp = Ext.state.Manager.getProvider();
- me.mon(sp, 'statechange', function (provider, key, value) {
- if (key !== 'summarycolumns') {
- return;
- }
- Proxmox.Utils.updateColumns(me.getComponent('itemcontainer'));
- });
- },
-});
-Ext.define('PVE.node.CreateZFS', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveCreateZFS',
-
- onlineHelp: 'chapter_zfs',
- subject: 'ZFS',
-
- showProgress: true,
- isCreate: true,
- width: 800,
-
- viewModel: {
- data: {
- raidLevel: 'single',
- },
- formulas: {
- isDraid: (get) => get('raidLevel')?.startsWith('draid'),
- },
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- Ext.apply(me, {
- url: `/nodes/${me.nodename}/disks/zfs`,
- method: 'POST',
- items: [
- {
- xtype: 'inputpanel',
- onGetValues: function (values) {
- if (values.draidData || values.draidSpares) {
- let opt = { data: values.draidData, spares: values.draidSpares };
- values['draid-config'] = PVE.Parser.printPropertyString(opt);
- }
- delete values.draidData;
- delete values.draidSpares;
- return values;
- },
- column1: [
- {
- xtype: 'proxmoxtextfield',
- name: 'name',
- fieldLabel: gettext('Name'),
- allowBlank: false,
- maxLength: 128, // ZFS_MAX_DATASET_NAME_LEN is (256 - some edge case)
- validator: (v) => {
- // see zpool_name_valid function in libzfs_zpool.c
- if (v.match(/^(mirror|raidz|draid|spare)/) || v === 'log') {
- return gettext('Cannot use reserved pool name');
- } else if (!v.match(/^[a-zA-Z][a-zA-Z0-9\-_.]*$/)) {
- // note: zfs would support also : and whitespace, but we don't
- return gettext('Invalid characters in pool name');
- }
- return true;
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'add_storage',
- fieldLabel: gettext('Add Storage'),
- value: '1',
- },
- ],
- column2: [
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('RAID Level'),
- name: 'raidlevel',
- value: 'single',
- comboItems: [
- ['single', gettext('Single Disk')],
- ['mirror', 'Mirror'],
- ['raid10', 'RAID10'],
- ['raidz', 'RAIDZ'],
- ['raidz2', 'RAIDZ2'],
- ['raidz3', 'RAIDZ3'],
- ['draid', 'dRAID'],
- ['draid2', 'dRAID2'],
- ['draid3', 'dRAID3'],
- ],
- bind: {
- value: '{raidLevel}',
- },
- },
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('Compression'),
- name: 'compression',
- value: 'on',
- comboItems: [
- ['on', 'on'],
- ['off', 'off'],
- ['gzip', 'gzip'],
- ['lz4', 'lz4'],
- ['lzjb', 'lzjb'],
- ['zle', 'zle'],
- ['zstd', 'zstd'],
- ],
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: gettext('ashift'),
- minValue: 9,
- maxValue: 16,
- value: '12',
- name: 'ashift',
- },
- ],
- columnB: [
- {
- xtype: 'fieldset',
- title: gettext('dRAID Config'),
- collapsible: false,
- bind: {
- hidden: '{!isDraid}',
- },
- layout: 'hbox',
- padding: '5px 10px',
- defaults: {
- flex: 1,
- layout: 'anchor',
- },
- items: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'draidData',
- fieldLabel: gettext('Data Devs'),
- minValue: 1,
- allowBlank: false,
- disabled: true,
- hidden: true,
- bind: {
- disabled: '{!isDraid}',
- hidden: '{!isDraid}',
- },
- padding: '0 10 0 0',
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'draidSpares',
- fieldLabel: gettext('Spares'),
- minValue: 0,
- allowBlank: false,
- disabled: true,
- hidden: true,
- bind: {
- disabled: '{!isDraid}',
- hidden: '{!isDraid}',
- },
- padding: '0 0 0 10',
- },
- ],
- },
- {
- xtype: 'pmxMultiDiskSelector',
- name: 'devices',
- nodename: me.nodename,
- diskType: 'unused',
- includePartitions: true,
- height: 200,
- emptyText: gettext('No Disks unused'),
- itemId: 'disklist',
- },
- ],
- },
- {
- xtype: 'displayfield',
- padding: '5 0 0 0',
- userCls: 'pmx-hint',
- value:
- 'Note: ZFS is not compatible with disks backed by a hardware ' +
- 'RAID controller. For details see the reference documentation .',
- },
- ],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.node.ZFSList', {
- extend: 'Ext.grid.Panel',
- xtype: 'pveZFSList',
-
- viewModel: {
- data: {
- pool: '',
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- destroyPool: function () {
- let me = this;
- let vm = me.getViewModel();
- let view = me.getView();
-
- const pool = vm.get('pool');
-
- if (!view.nodename) {
- throw 'no node name specified';
- }
-
- if (!pool) {
- throw 'no pool specified';
- }
-
- Ext.create('PVE.window.SafeDestroyStorage', {
- url: `/nodes/${view.nodename}/disks/zfs/${pool}`,
- item: { id: pool },
- taskName: 'zfsremove',
- taskDone: () => {
- view.reload();
- },
- }).show();
- },
- },
-
- stateful: true,
- stateId: 'grid-node-zfs',
- columns: [
- {
- text: gettext('Name'),
- dataIndex: 'name',
- flex: 1,
- },
- {
- header: gettext('Size'),
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'size',
- },
- {
- header: gettext('Free'),
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'free',
- },
- {
- header: gettext('Allocated'),
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'alloc',
- },
- {
- header: gettext('Fragmentation'),
- renderer: function (value) {
- return value.toString() + '%';
- },
- dataIndex: 'frag',
- },
- {
- header: gettext('Health'),
- renderer: PVE.Utils.render_zfs_health,
- dataIndex: 'health',
- },
- {
- header: gettext('Deduplication'),
- hidden: true,
- renderer: function (value) {
- return value.toFixed(2).toString() + 'x';
- },
- dataIndex: 'dedup',
- },
- ],
-
- rootVisible: false,
- useArrows: true,
-
- tbar: [
- {
- text: gettext('Reload'),
- iconCls: 'fa fa-refresh',
- handler: function () {
- this.up('panel').reload();
- },
- },
- {
- text: gettext('Create') + ': ZFS',
- handler: function () {
- let view = this.up('panel');
- Ext.create('PVE.node.CreateZFS', {
- nodename: view.nodename,
- listeners: {
- destroy: () => view.reload(),
- },
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Detail'),
- itemId: 'detailbtn',
- disabled: true,
- handler: function () {
- let view = this.up('panel');
- let selection = view.getSelection();
- if (selection.length) {
- view.show_detail(selection[0].get('name'));
- }
- },
- },
- '->',
- {
- xtype: 'tbtext',
- data: {
- pool: undefined,
- },
- bind: {
- data: {
- pool: '{pool}',
- },
- },
- tpl: [
- '',
- 'Pool {pool}:',
- '',
- Ext.String.format(gettext('No {0} selected'), 'pool'),
- ' ',
- ],
- },
- {
- text: gettext('More'),
- iconCls: 'fa fa-bars',
- disabled: true,
- bind: {
- disabled: '{!pool}',
- },
- menu: [
- {
- text: gettext('Destroy'),
- itemId: 'remove',
- iconCls: 'fa fa-fw fa-trash-o',
- handler: 'destroyPool',
- disabled: true,
- bind: {
- disabled: '{!pool}',
- },
- },
- ],
- },
- ],
-
- show_detail: function (zpool) {
- let me = this;
-
- Ext.create('Proxmox.window.ZFSDetail', {
- zpool,
- nodename: me.nodename,
- }).show();
- },
-
- set_button_status: function () {
- var _me = this;
- },
-
- reload: function () {
- var me = this;
- me.store.load();
- me.store.sort();
- },
-
- listeners: {
- activate: function () {
- this.reload();
- },
- selectionchange: function (model, selected) {
- let me = this;
- let vm = me.getViewModel();
-
- me.down('#detailbtn').setDisabled(selected.length === 0);
- vm.set('pool', selected[0]?.data.name || '');
- },
- itemdblclick: function (grid, record) {
- this.show_detail(record.get('name'));
- },
- },
-
- initComponent: function () {
- let me = this;
-
- me.nodename = me.pveSelNode.data.node;
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- Ext.apply(me, {
- store: {
- fields: ['name', 'size', 'free', 'alloc', 'dedup', 'frag', 'health'],
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/disks/zfs`,
- },
- sorters: 'name',
- },
- });
-
- me.callParent();
-
- Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
- me.reload();
- },
-});
-Ext.define('Proxmox.node.NodeOptionsView', {
- extend: 'Proxmox.grid.ObjectGrid',
- alias: ['widget.proxmoxNodeOptionsView'],
- mixins: ['Proxmox.Mixin.CBind'],
-
- cwidth1: 250,
-
- cbindData: function (_initialconfig) {
- let me = this;
-
- let baseUrl = `/nodes/${me.nodename}/config`;
- me.url = `/api2/json${baseUrl}`;
- me.editorConfig = {
- url: `/api2/extjs/${baseUrl}`,
- };
-
- return {};
- },
-
- listeners: {
- itemdblclick: function () {
- this.run_editor();
- },
- activate: function () {
- this.rstore.startUpdate();
- },
- destroy: function () {
- this.rstore.stopUpdate();
- },
- deactivate: function () {
- this.rstore.stopUpdate();
- },
- },
-
- tbar: [
- {
- text: gettext('Edit'),
- xtype: 'proxmoxButton',
- disabled: true,
- handler: (btn) => btn.up('grid').run_editor(),
- },
- ],
-
- gridRows: [
- {
- xtype: 'integer',
- name: 'startall-onboot-delay',
- text: gettext('Start on boot delay'),
- minValue: 0,
- maxValue: 300,
- labelWidth: 130,
- deleteEmpty: true,
- renderer: function (value) {
- if (value === undefined) {
- return Proxmox.Utils.defaultText;
- }
-
- // TODO: simplify once we can use ngetext
- let secString = value === '1' ? gettext('Second') : gettext('Seconds');
- return `${value} ${secString}`;
- },
- },
- {
- xtype: 'text',
- name: 'wakeonlan',
- text: gettext('MAC address for Wake on LAN'),
- vtype: 'MacAddress',
- labelWidth: 150,
- deleteEmpty: true,
- renderer: (value) => (value !== undefined ? value : Proxmox.Utils.NoneText),
- },
- {
- xtype: 'integer',
- name: 'ballooning-target',
- text: gettext('RAM usage target for ballooning'),
- minValue: 0,
- maxValue: 100,
- deleteEmpty: true,
- onlineHelp: 'qm_memory',
- renderer: (value) => (value !== undefined ? `${value}%` : gettext('Default (80%)')),
- },
- ],
-});
-Ext.define('PVE.pool.Config', {
- extend: 'PVE.panel.Config',
- alias: 'widget.pvePoolConfig',
-
- onlineHelp: 'pveum_pools',
-
- initComponent: function () {
- var me = this;
-
- var pool = me.pveSelNode.data.pool;
- if (!pool) {
- throw 'no pool specified';
- }
-
- Ext.apply(me, {
- title: Ext.String.format(gettext('Resource Pool') + ': ' + pool),
- hstateid: 'pooltab',
- items: [
- {
- title: gettext('Summary'),
- iconCls: 'fa fa-book',
- xtype: 'pvePoolSummary',
- itemId: 'summary',
- },
- {
- title: gettext('Members'),
- xtype: 'pvePoolMembers',
- iconCls: 'fa fa-th',
- pool: pool,
- itemId: 'members',
- },
- {
- xtype: 'pveACLView',
- title: gettext('Permissions'),
- iconCls: 'fa fa-unlock',
- itemId: 'permissions',
- path: '/pool/' + pool,
- },
- ],
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.pool.StatusView', {
- extend: 'Proxmox.grid.ObjectGrid',
- alias: ['widget.pvePoolStatusView'],
- disabled: true,
-
- title: gettext('Status'),
- cwidth1: 150,
- interval: 30000,
- //height: 195,
- initComponent: function () {
- var me = this;
-
- var pool = me.pveSelNode.data.pool;
- if (!pool) {
- throw 'no pool specified';
- }
-
- var rows = {
- comment: {
- header: gettext('Comment'),
- renderer: Ext.String.htmlEncode,
- required: true,
- },
- };
-
- Ext.apply(me, {
- url: '/api2/json/pools/?poolid=' + pool,
- rows: rows,
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.pool.Summary', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pvePoolSummary',
-
- initComponent: function () {
- var me = this;
-
- var pool = me.pveSelNode.data.pool;
- if (!pool) {
- throw 'no pool specified';
- }
-
- var statusview = Ext.create('PVE.pool.StatusView', {
- pveSelNode: me.pveSelNode,
- style: 'padding-top:0px',
- });
-
- var rstore = statusview.rstore;
-
- Ext.apply(me, {
- autoScroll: true,
- bodyStyle: 'padding:10px',
- defaults: {
- style: 'padding-top:10px',
- width: 800,
- },
- items: [statusview],
- });
-
- me.on('activate', rstore.startUpdate);
- me.on('destroy', rstore.stopUpdate);
-
- me.callParent();
- },
-});
-Ext.define('PVE.qemu.AudioInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveAudioInputPanel',
-
- onlineHelp: 'qm_audio_device',
-
- onGetValues: function (values) {
- var ret = PVE.Parser.printPropertyString(values);
- if (ret === '') {
- return {
- delete: 'audio0',
- };
- }
- return {
- audio0: ret,
- };
- },
-
- items: [
- {
- name: 'device',
- xtype: 'proxmoxKVComboBox',
- value: 'ich9-intel-hda',
- fieldLabel: gettext('Audio Device'),
- comboItems: [
- ['ich9-intel-hda', 'ich9-intel-hda'],
- ['intel-hda', 'intel-hda'],
- ['AC97', 'AC97'],
- ],
- },
- {
- name: 'driver',
- xtype: 'proxmoxKVComboBox',
- value: 'spice',
- fieldLabel: gettext('Backend Driver'),
- comboItems: [
- ['spice', 'SPICE'],
- ['none', `${Proxmox.Utils.NoneText} (${gettext('Dummy Device')})`],
- ],
- },
- ],
-});
-
-Ext.define('PVE.qemu.AudioEdit', {
- extend: 'Proxmox.window.Edit',
-
- vmconfig: undefined,
-
- subject: gettext('Audio Device'),
-
- items: [
- {
- xtype: 'pveAudioInputPanel',
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
-
- me.load({
- success: function (response) {
- me.vmconfig = response.result.data;
-
- var audio0 = me.vmconfig.audio0;
- if (audio0) {
- me.setValues(PVE.Parser.parsePropertyString(audio0));
- }
- },
- });
- },
-});
-Ext.define('pve-boot-order-entry', {
- extend: 'Ext.data.Model',
- fields: [
- { name: 'name', type: 'string' },
- { name: 'enabled', type: 'bool' },
- { name: 'desc', type: 'string' },
- ],
-});
-
-Ext.define('PVE.qemu.BootOrderPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveQemuBootOrderPanel',
-
- onlineHelp: 'qm_bootorder',
-
- vmconfig: {}, // store loaded vm config
- store: undefined,
-
- inUpdate: false,
- controller: {
- xclass: 'Ext.app.ViewController',
-
- init: function (view) {
- let me = this;
-
- let grid = me.lookup('grid');
- let marker = me.lookup('marker');
- let emptyWarning = me.lookup('emptyWarning');
-
- marker.originalValue = undefined;
-
- view.store = Ext.create('Ext.data.Store', {
- model: 'pve-boot-order-entry',
- listeners: {
- update: function () {
- this.commitChanges();
- let val = view.calculateValue();
- if (marker.originalValue === undefined) {
- marker.originalValue = val;
- }
- view.inUpdate = true;
- marker.setValue(val);
- view.inUpdate = false;
- marker.checkDirty();
- emptyWarning.setHidden(val !== '');
- grid.getView().refresh();
- },
- },
- });
- grid.setStore(view.store);
- },
- },
-
- isCloudinit: (v) => v.match(/media=cdrom/) && v.match(/[:/]vm-\d+-cloudinit/),
-
- isDisk: function (value) {
- return PVE.Utils.bus_match.test(value);
- },
-
- isBootdev: function (dev, value) {
- return (
- (this.isDisk(dev) && !this.isCloudinit(value)) ||
- /^net\d+/.test(dev) ||
- /^hostpci\d+/.test(dev) ||
- (/^usb\d+/.test(dev) && !/spice/.test(value))
- );
- },
-
- setVMConfig: function (vmconfig) {
- let me = this;
- me.vmconfig = vmconfig;
-
- me.store.removeAll();
-
- let boot = PVE.Parser.parsePropertyString(me.vmconfig.boot, 'legacy');
-
- let bootorder = [];
- if (boot.order) {
- bootorder = boot.order.split(';').map((dev) => ({ name: dev, enabled: true }));
- } else if (!/^\s*$/.test(me.vmconfig.boot)) {
- // legacy style, transform to new bootorder
- let order = boot.legacy || 'cdn';
- let bootdisk = me.vmconfig.bootdisk || undefined;
-
- // get the first 4 characters (acdn)
- // ignore the rest (there should never be more than 4)
- let orderList = order.split('').slice(0, 4);
-
- // build bootdev list
- for (let i = 0; i < orderList.length; i++) {
- let list = [];
- if (orderList[i] === 'c') {
- if (bootdisk !== undefined && me.vmconfig[bootdisk]) {
- list.push(bootdisk);
- }
- } else if (orderList[i] === 'd') {
- Ext.Object.each(me.vmconfig, function (key, value) {
- if (
- me.isDisk(key) &&
- value.match(/media=cdrom/) &&
- !me.isCloudinit(value)
- ) {
- list.push(key);
- }
- });
- } else if (orderList[i] === 'n') {
- Ext.Object.each(me.vmconfig, function (key, value) {
- if (/^net\d+/.test(key)) {
- list.push(key);
- }
- });
- }
-
- // Object.each iterates in random order, sort alphabetically
- list.sort();
- list.forEach((dev) => bootorder.push({ name: dev, enabled: true }));
- }
- }
-
- // add disabled devices as well
- let disabled = [];
- Ext.Object.each(me.vmconfig, function (key, value) {
- if (me.isBootdev(key, value) && !Ext.Array.some(bootorder, (x) => x.name === key)) {
- disabled.push(key);
- }
- });
- disabled.sort();
- disabled.forEach((dev) => bootorder.push({ name: dev, enabled: false }));
-
- // add descriptions
- bootorder.forEach((entry) => {
- entry.desc = me.vmconfig[entry.name];
- });
-
- me.store.insert(0, bootorder);
- me.store.fireEvent('update');
- },
-
- calculateValue: function () {
- let me = this;
- return me.store
- .getData()
- .items.filter((x) => x.data.enabled)
- .map((x) => x.data.name)
- .join(';');
- },
-
- onGetValues: function () {
- let me = this;
- // Note: we allow an empty value, so no 'delete' option
- let val = { order: me.calculateValue() };
- let res = { boot: PVE.Parser.printPropertyString(val) };
- return res;
- },
-
- items: [
- {
- xtype: 'grid',
- reference: 'grid',
- margin: '0 0 5 0',
- minHeight: 150,
- defaults: {
- sortable: false,
- hideable: false,
- draggable: false,
- },
- columns: [
- {
- header: '#',
- flex: 4,
- renderer: (value, metaData, record, rowIndex) => {
- let dragHandle =
- " ";
- let idx = (rowIndex + 1).toString();
- if (record.get('enabled')) {
- return dragHandle + idx;
- } else {
- return dragHandle + "" + idx + ' ';
- }
- },
- },
- {
- xtype: 'checkcolumn',
- header: gettext('Enabled'),
- dataIndex: 'enabled',
- flex: 4,
- },
- {
- header: gettext('Device'),
- dataIndex: 'name',
- flex: 6,
- renderer: (value, metaData, record, rowIndex) => {
- let desc = record.get('desc');
-
- let icon = '',
- iconCls;
- if (value.match(/^net\d+$/)) {
- iconCls = 'exchange';
- } else if (desc.match(/media=cdrom/)) {
- metaData.tdCls = 'pve-itype-icon-cdrom';
- } else {
- iconCls = 'hdd-o';
- }
- if (iconCls !== undefined) {
- metaData.tdCls += 'pve-itype-fa';
- icon = ` `;
- }
-
- return icon + value;
- },
- },
- {
- header: gettext('Description'),
- dataIndex: 'desc',
- flex: 20,
- },
- ],
- viewConfig: {
- plugins: {
- ptype: 'gridviewdragdrop',
- dragText: gettext('Drag and drop to reorder'),
- },
- },
- listeners: {
- drop: function () {
- // doesn't fire automatically on reorder
- this.getStore().fireEvent('update');
- },
- },
- },
- {
- xtype: 'component',
- html: gettext('Drag and drop to reorder'),
- },
- {
- xtype: 'displayfield',
- reference: 'emptyWarning',
- userCls: 'pmx-hint',
- value: gettext('Warning: No devices selected, the VM will probably not boot!'),
- },
- {
- // for dirty marking and 'reset' function
- xtype: 'field',
- reference: 'marker',
- hidden: true,
- setValue: function (val) {
- let me = this;
- let panel = me.up('pveQemuBootOrderPanel');
-
- // on form reset, go back to original state
- if (!panel.inUpdate) {
- panel.setVMConfig(panel.vmconfig);
- }
-
- // not a subclass, so no callParent; just do it manually
- me.setRawValue(me.valueToRaw(val));
- return me.mixins.field.setValue.call(me, val);
- },
- },
- ],
-});
-
-Ext.define('PVE.qemu.BootOrderEdit', {
- extend: 'Proxmox.window.Edit',
-
- items: [
- {
- xtype: 'pveQemuBootOrderPanel',
- itemId: 'inputpanel',
- },
- ],
-
- subject: gettext('Boot Order'),
- width: 640,
-
- initComponent: function () {
- let me = this;
- me.callParent();
- me.load({
- success: ({ result }) => me.down('#inputpanel').setVMConfig(result.data),
- });
- },
-});
-Ext.define('PVE.qemu.CDInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveQemuCDInputPanel',
-
- insideWizard: false,
-
- onGetValues: function (values) {
- var me = this;
-
- var confid = me.confid || values.controller + values.deviceid;
-
- me.drive.media = 'cdrom';
- if (values.mediaType === 'iso') {
- me.drive.file = values.cdimage;
- } else if (values.mediaType === 'cdrom') {
- me.drive.file = 'cdrom';
- } else {
- me.drive.file = 'none';
- }
-
- var params = {};
-
- params[confid] = PVE.Parser.printQemuDrive(me.drive);
-
- return params;
- },
-
- setVMConfig: function (vmconfig) {
- var me = this;
-
- if (me.bussel) {
- me.bussel.setVMConfig(vmconfig, 'cdrom');
- }
- },
-
- setDrive: function (drive) {
- var me = this;
-
- var values = {};
- if (drive.file === 'cdrom') {
- values.mediaType = 'cdrom';
- } else if (drive.file === 'none') {
- values.mediaType = 'none';
- } else {
- values.mediaType = 'iso';
- values.cdimage = drive.file;
- }
-
- me.drive = drive;
-
- me.setValues(values);
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- me.isosel.setNodename(nodename);
- },
-
- initComponent: function () {
- var me = this;
-
- me.drive = {};
-
- var items = [];
-
- if (!me.confid) {
- me.bussel = Ext.create('PVE.form.ControllerSelector', {
- withVirtIO: false,
- });
- items.push(me.bussel);
- }
-
- items.push({
- xtype: 'radiofield',
- name: 'mediaType',
- inputValue: 'iso',
- boxLabel: gettext('Use CD/DVD disc image file (iso)'),
- checked: true,
- listeners: {
- change: function (f, value) {
- if (!me.rendered) {
- return;
- }
- var cdImageField = me.down('pveIsoSelector');
- cdImageField.setDisabled(!value);
- if (value) {
- cdImageField.validate();
- } else {
- cdImageField.reset();
- }
- },
- },
- });
-
- me.isosel = Ext.create('PVE.form.IsoSelector', {
- nodename: me.nodename,
- insideWizard: me.insideWizard,
- name: 'cdimage',
- });
-
- items.push(me.isosel);
-
- items.push({
- xtype: 'radiofield',
- name: 'mediaType',
- inputValue: 'cdrom',
- boxLabel: gettext('Use physical CD/DVD Drive'),
- });
-
- items.push({
- xtype: 'radiofield',
- name: 'mediaType',
- inputValue: 'none',
- boxLabel: gettext('Do not use any media'),
- });
-
- me.items = items;
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.CDEdit', {
- extend: 'Proxmox.window.Edit',
-
- width: 400,
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- me.isCreate = !me.confid;
-
- var ipanel = Ext.create('PVE.qemu.CDInputPanel', {
- confid: me.confid,
- nodename: nodename,
- });
-
- Ext.applyIf(me, {
- subject: 'CD/DVD Drive',
- items: [ipanel],
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- ipanel.setVMConfig(response.result.data);
- if (me.confid) {
- let value = response.result.data[me.confid];
- let drive = PVE.Parser.parseQemuDrive(me.confid, value);
- if (!drive) {
- Ext.Msg.alert('Error', 'Unable to parse drive options');
- me.close();
- return;
- }
- ipanel.setDrive(drive);
- }
- },
- });
- },
-});
-Ext.define('PVE.qemu.CIDriveInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveCIDriveInputPanel',
-
- insideWizard: false,
-
- vmconfig: {}, // used to select usused disks
-
- onGetValues: function (values) {
- var _me = this;
-
- var drive = {};
- var params = {};
- drive.file = values.hdstorage + ':cloudinit';
- drive.format = values.diskformat;
- params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive);
- return params;
- },
-
- setNodename: function (nodename) {
- var me = this;
- me.down('#hdstorage').setNodename(nodename);
- me.down('#hdimage').setStorage(undefined, nodename);
- },
-
- setVMConfig: function (config) {
- var me = this;
- me.down('#drive').setVMConfig(config, 'cdrom');
- },
-
- initComponent: function () {
- var me = this;
-
- me.drive = {};
-
- me.items = [
- {
- xtype: 'pveControllerSelector',
- withVirtIO: false,
- itemId: 'drive',
- fieldLabel: gettext('CloudInit Drive'),
- name: 'drive',
- },
- {
- xtype: 'pveDiskStorageSelector',
- itemId: 'storselector',
- storageContent: 'images',
- nodename: me.nodename,
- hideSize: true,
- },
- ];
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.CIDriveEdit', {
- extend: 'Proxmox.window.Edit',
- xtype: 'pveCIDriveEdit',
-
- isCreate: true,
- subject: gettext('CloudInit Drive'),
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- me.items = [
- {
- xtype: 'pveCIDriveInputPanel',
- itemId: 'cipanel',
- nodename: nodename,
- },
- ];
-
- me.callParent();
-
- me.load({
- success: function (response, opts) {
- me.down('#cipanel').setVMConfig(response.result.data);
- },
- });
- },
-});
-Ext.define('PVE.qemu.CloudInit', {
- extend: 'Proxmox.grid.PendingObjectGrid',
- xtype: 'pveCiPanel',
-
- onlineHelp: 'qm_cloud_init',
-
- tbar: [
- {
- xtype: 'proxmoxButton',
- disabled: true,
- dangerous: true,
- confirmMsg: function (rec) {
- let view = this.up('grid');
- var warn = gettext('Are you sure you want to remove entry {0}');
-
- var entry = rec.data.key;
- var msg = Ext.String.format(warn, "'" + view.renderKey(entry, {}, rec) + "'");
-
- return msg;
- },
- enableFn: function (record) {
- let view = this.up('grid');
- var caps = Ext.state.Manager.get('GuiCap');
- let caps_ci = caps.vms['VM.Config.Network'] || caps.vms['VM.Config.Cloudinit'];
- if (view.rows[record.data.key].never_delete || !caps_ci) {
- return false;
- }
-
- if (record.data.key === 'cipassword' && !record.data.value) {
- return false;
- }
- return true;
- },
- handler: function () {
- let view = this.up('grid');
- let records = view.getSelection();
- if (!records || !records.length) {
- return;
- }
-
- var id = records[0].data.key;
- var match = id.match(/^net(\d+)$/);
- if (match) {
- id = 'ipconfig' + match[1];
- }
-
- var params = {};
- params.delete = id;
- Proxmox.Utils.API2Request({
- url: view.baseurl + '/config',
- waitMsgTarget: view,
- method: 'PUT',
- params: params,
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- callback: function () {
- view.reload();
- },
- });
- },
- text: gettext('Remove'),
- },
- {
- xtype: 'proxmoxButton',
- disabled: true,
- enableFn: function (rec) {
- let view = this.up('pveCiPanel');
- return !!view.rows[rec.data.key].editor;
- },
- handler: function () {
- let view = this.up('grid');
- view.run_editor();
- },
- text: gettext('Edit'),
- },
- '-',
- {
- xtype: 'button',
- itemId: 'savebtn',
- text: gettext('Regenerate Image'),
- handler: function () {
- let view = this.up('grid');
-
- Proxmox.Utils.API2Request({
- url: view.baseurl + '/cloudinit',
- waitMsgTarget: view,
- method: 'PUT',
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- callback: function () {
- view.reload();
- },
- });
- },
- },
- ],
-
- border: false,
-
- set_button_status: function (rstore, records, success) {
- if (!success || records.length < 1) {
- return;
- }
- var me = this;
- var found;
- records.forEach(function (record) {
- if (found) {
- return;
- }
- var id = record.data.key;
- var value = record.data.value;
- var ciregex = new RegExp('vm-' + me.pveSelNode.data.vmid + '-cloudinit');
- if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) {
- found = id;
- me.ciDriveId = found;
- me.ciDrive = value;
- }
- });
-
- let caps = Ext.state.Manager.get('GuiCap');
- let canRegenerateImage = !!caps.vms['VM.Config.Cloudinit'];
- me.down('#savebtn').setDisabled(!found || !canRegenerateImage);
-
- me.setDisabled(!found);
- if (!found) {
- me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']);
- } else {
- me.getView().unmask();
- }
- },
-
- renderKey: function (key, metaData, rec, rowIndex, colIndex, store) {
- var me = this;
- var rows = me.rows;
- var rowdef = rows[key] || {};
-
- var icon = '';
- if (rowdef.iconCls) {
- icon = ' ';
- }
- return icon + (rowdef.header || key);
- },
-
- listeners: {
- activate: function () {
- var me = this;
- me.rstore.startUpdate();
- },
- itemdblclick: function () {
- var me = this;
- me.run_editor();
- },
- },
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
- var caps = Ext.state.Manager.get('GuiCap');
- me.baseurl = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid;
- me.url = me.baseurl + '/pending';
- me.editorConfig.url = me.baseurl + '/config';
- me.editorConfig.pveSelNode = me.pveSelNode;
-
- let caps_ci = caps.vms['VM.Config.Cloudinit'] || caps.vms['VM.Config.Network'];
- /* editor is string and object */
- me.rows = {
- ciuser: {
- header: gettext('User'),
- iconCls: 'fa fa-user',
- never_delete: true,
- defaultValue: '',
- editor: caps_ci
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('User'),
- items: [
- {
- xtype: 'proxmoxtextfield',
- deleteEmpty: true,
- emptyText: Proxmox.Utils.defaultText,
- fieldLabel: gettext('User'),
- name: 'ciuser',
- },
- ],
- }
- : undefined,
- renderer: function (value) {
- return Ext.String.htmlEncode(value || Proxmox.Utils.defaultText);
- },
- },
- cipassword: {
- header: gettext('Password'),
- iconCls: 'fa fa-unlock',
- defaultValue: '',
- editor: caps_ci
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Password'),
- items: [
- {
- xtype: 'proxmoxtextfield',
- inputType: 'password',
- deleteEmpty: true,
- emptyText: Proxmox.Utils.noneText,
- fieldLabel: gettext('Password'),
- name: 'cipassword',
- },
- ],
- }
- : undefined,
- renderer: function (value) {
- return Ext.String.htmlEncode(value || Proxmox.Utils.noneText);
- },
- },
- searchdomain: {
- header: gettext('DNS domain'),
- iconCls: 'fa fa-globe',
- editor: caps_ci ? 'PVE.lxc.DNSEdit' : undefined,
- never_delete: true,
- defaultValue: gettext('use host settings'),
- },
- nameserver: {
- header: gettext('DNS servers'),
- iconCls: 'fa fa-globe',
- editor: caps_ci ? 'PVE.lxc.DNSEdit' : undefined,
- never_delete: true,
- defaultValue: gettext('use host settings'),
- },
- sshkeys: {
- header: gettext('SSH public key'),
- iconCls: 'fa fa-key',
- editor: caps_ci ? 'PVE.qemu.SSHKeyEdit' : undefined,
- never_delete: true,
- renderer: function (value) {
- value = decodeURIComponent(value);
- var keys = value.split('\n');
- var text = [];
- keys.forEach(function (key) {
- if (key.length) {
- let res = PVE.Parser.parseSSHKey(key);
- if (res) {
- key = Ext.String.htmlEncode(res.comment);
- if (res.options) {
- key +=
- ' (' +
- gettext('with options') +
- ') ';
- }
- text.push(key);
- return;
- }
- // Most likely invalid at this point, so just stick to
- // the old value.
- text.push(Ext.String.htmlEncode(key));
- }
- });
- if (text.length) {
- return text.join(' ');
- } else {
- return Proxmox.Utils.noneText;
- }
- },
- defaultValue: '',
- },
- ciupgrade: {
- header: gettext('Upgrade packages'),
- iconCls: 'fa fa-archive',
- renderer: Proxmox.Utils.format_boolean,
- defaultValue: 1,
- editor: {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Upgrade packages on boot'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'ciupgrade',
- uncheckedValue: 0,
- value: 1, // serves as default value, using defaultValue is not enough
- fieldLabel: gettext('Upgrade packages'),
- labelWidth: 140,
- },
- },
- },
- };
- var i;
- var ipconfig_renderer = function (value, md, record, ri, ci, store, pending) {
- var id = record.data.key;
- var match = id.match(/^net(\d+)$/);
- var val = '';
- if (match) {
- val = me.getObjectValue('ipconfig' + match[1], '', pending);
- }
- return val;
- };
- for (i = 0; i < 32; i++) {
- // we want to show an entry for every network device
- // even if it is empty
- me.rows['net' + i.toString()] = {
- multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()],
- header: gettext('IP Config') + ' (net' + i.toString() + ')',
- editor: caps_ci ? 'PVE.qemu.IPConfigEdit' : undefined,
- iconCls: 'fa fa-exchange',
- renderer: ipconfig_renderer,
- };
- me.rows['ipconfig' + i.toString()] = {
- visible: false,
- };
- }
-
- PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function (type, id) {
- me.rows[type + id] = {
- visible: false,
- };
- });
- me.callParent();
- me.mon(me.rstore, 'load', me.set_button_status, me);
- },
-});
-Ext.define('PVE.qemu.CmdMenu', {
- extend: 'Ext.menu.Menu',
-
- showSeparator: false,
- initComponent: function () {
- let me = this;
-
- let info = me.pveSelNode.data;
- if (!info.node) {
- throw 'no node name specified';
- }
- if (!info.vmid) {
- throw 'no VM ID specified';
- }
-
- let vm_command = function (cmd, params) {
- Proxmox.Utils.API2Request({
- params: params,
- url: `/nodes/${info.node}/${info.type}/${info.vmid}/status/${cmd}`,
- method: 'POST',
- failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- };
- let confirmedVMCommand = (cmd, params, confirmTask) => {
- let task = confirmTask || `qm${cmd}`;
- let msg = PVE.Utils.formatGuestTaskConfirmation(task, info.vmid, info.name);
- Ext.Msg.confirm(gettext('Confirm'), msg, (btn) => {
- if (btn === 'yes') {
- vm_command(cmd, params);
- }
- });
- };
-
- let caps = Ext.state.Manager.get('GuiCap');
- let standalone = PVE.Utils.isStandaloneNode();
-
- let running = false,
- stopped = true,
- suspended = false;
- switch (info.status) {
- case 'running':
- running = true;
- stopped = false;
- break;
- case 'suspended':
- stopped = false;
- suspended = true;
- break;
- case 'paused':
- stopped = false;
- suspended = true;
- break;
- default:
- break;
- }
-
- me.title = 'VM ' + info.vmid;
-
- me.items = [
- {
- text: gettext('Start'),
- iconCls: 'fa fa-fw fa-play',
- hidden: running || suspended,
- disabled: running || suspended,
- handler: () => vm_command('start'),
- },
- {
- text: gettext('Pause'),
- iconCls: 'fa fa-fw fa-pause',
- hidden: stopped || suspended,
- disabled: stopped || suspended,
- handler: () => confirmedVMCommand('suspend', undefined, 'qmpause'),
- },
- {
- text: gettext('Hibernate'),
- iconCls: 'fa fa-fw fa-download',
- hidden: stopped || suspended,
- disabled: stopped || suspended,
- tooltip: gettext('Suspend to disk'),
- handler: () => confirmedVMCommand('suspend', { todisk: 1 }),
- },
- {
- text: gettext('Resume'),
- iconCls: 'fa fa-fw fa-play',
- hidden: !suspended,
- handler: () => vm_command('resume'),
- },
- {
- text: gettext('Shutdown'),
- iconCls: 'fa fa-fw fa-power-off',
- disabled: stopped || suspended,
- handler: () => confirmedVMCommand('shutdown'),
- },
- {
- text: gettext('Stop'),
- iconCls: 'fa fa-fw fa-stop',
- disabled: stopped,
- tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
- handler: () => {
- Ext.create('PVE.GuestStop', {
- nodename: info.node,
- vm: info,
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Reboot'),
- iconCls: 'fa fa-fw fa-refresh',
- disabled: stopped,
- tooltip: Ext.String.format(gettext('Reboot {0}'), 'VM'),
- handler: () => confirmedVMCommand('reboot'),
- },
- {
- text: gettext('Reset'),
- iconCls: 'fa fa-fw fa-bolt',
- disabled: stopped,
- tooltip: Ext.String.format(gettext('Reset {0}'), 'VM'),
- handler: () => confirmedVMCommand('reset'),
- },
- {
- xtype: 'menuseparator',
- hidden:
- (standalone || !caps.vms['VM.Migrate']) &&
- !caps.vms['VM.Allocate'] &&
- !caps.vms['VM.Clone'],
- },
- {
- text: gettext('Migrate'),
- iconCls: 'fa fa-fw fa-send-o',
- hidden: standalone || !caps.vms['VM.Migrate'],
- handler: function () {
- Ext.create('PVE.window.Migrate', {
- vmtype: 'qemu',
- nodename: info.node,
- vmid: info.vmid,
- vmname: info.name,
- autoShow: true,
- });
- },
- },
- {
- text: gettext('Clone'),
- iconCls: 'fa fa-fw fa-clone',
- hidden: !caps.vms['VM.Clone'],
- handler: () =>
- PVE.window.Clone.wrap(info.node, info.vmid, info.name, me.isTemplate, 'qemu'),
- },
- {
- text: gettext('Convert to template'),
- iconCls: 'fa fa-fw fa-file-o',
- hidden: !caps.vms['VM.Allocate'],
- handler: function () {
- let msg = PVE.Utils.formatGuestTaskConfirmation(
- 'qmtemplate',
- info.vmid,
- info.name,
- );
- Ext.Msg.confirm(gettext('Confirm'), msg, (btn) => {
- if (btn === 'yes') {
- Proxmox.Utils.API2Request({
- url: `/nodes/${info.node}/qemu/${info.vmid}/template`,
- method: 'POST',
- failure: (response, opts) =>
- Ext.Msg.alert('Error', response.htmlStatus),
- });
- }
- });
- },
- },
- { xtype: 'menuseparator' },
- {
- text: gettext('Take Snapshot'),
- iconCls: 'fa fa-fw fa-history',
- itemId: 'takeSnapshotBtn',
- disabled: true,
- handler: () => {
- Ext.create('PVE.window.Snapshot', {
- nodename: info.node,
- vmid: info.vmid,
- vmname: info.name,
- viewonly: false,
- type: info.type,
- isCreate: true,
- submitText: gettext('Take Snapshot'),
- autoShow: true,
- running: running,
- });
- },
- },
- {
- text: gettext('Backup now'),
- iconCls: 'fa fa-fw fa-floppy-o',
- disabled: !caps.vms['VM.Backup'],
- handler: () => {
- Ext.create('PVE.window.Backup', {
- nodename: info.node,
- vmid: info.vmid,
- vmtype: info.type,
- vmname: info.name,
- autoShow: true,
- });
- },
- },
- { xtype: 'menuseparator' },
- {
- text: gettext('Console'),
- iconCls: 'fa fa-fw fa-terminal',
- handler: function () {
- Proxmox.Utils.API2Request({
- url: `/nodes/${info.node}/qemu/${info.vmid}/status/current`,
- failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus),
- success: function ({ result: { data } }, opts) {
- PVE.Utils.openDefaultConsoleWindow(
- {
- spice: data.spice,
- xtermjs: data.serial,
- },
- 'kvm',
- info.vmid,
- info.node,
- info.name,
- );
- },
- });
- },
- },
- ];
-
- me.callParent();
-
- if (caps.vms['VM.Snapshot']) {
- Proxmox.Utils.API2Request({
- url: `/nodes/${info.node}/${info.type}/${info.vmid}/feature`,
- params: { feature: 'snapshot' },
- method: 'GET',
- success: (response) => {
- let hasFeature = response.result.data.hasFeature;
- let btn = me.down('#takeSnapshotBtn');
- if (btn) {
- btn.setDisabled(!hasFeature);
- }
- },
- });
- }
- },
-});
-Ext.define('PVE.qemu.Config', {
- extend: 'PVE.panel.Config',
- alias: 'widget.PVE.qemu.Config',
-
- onlineHelp: 'chapter_virtual_machines',
- userCls: 'proxmox-tags-full',
-
- initComponent: function () {
- var me = this;
- var vm = me.pveSelNode.data;
-
- var nodename = vm.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = vm.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- var template = !!vm.template;
-
- var running = !!vm.uptime;
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- var base_url = '/nodes/' + nodename + '/qemu/' + vmid;
-
- me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
- url: '/api2/json' + base_url + '/status/current',
- interval: 1000,
- });
-
- var vm_command = function (cmd, params) {
- Proxmox.Utils.API2Request({
- params: params,
- url: base_url + '/status/' + cmd,
- waitMsgTarget: me,
- method: 'POST',
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- });
- };
-
- var resumeBtn = Ext.create('Ext.Button', {
- text: gettext('Resume'),
- disabled: !caps.vms['VM.PowerMgmt'],
- hidden: true,
- handler: function () {
- vm_command('resume');
- },
- iconCls: 'fa fa-play',
- });
-
- var startBtn = Ext.create('Ext.Button', {
- text: gettext('Start'),
- disabled: !caps.vms['VM.PowerMgmt'] || running,
- hidden: template,
- handler: function () {
- vm_command('start');
- },
- iconCls: 'fa fa-play',
- });
-
- var migrateBtn = Ext.create('Ext.Button', {
- text: gettext('Migrate'),
- disabled: !caps.vms['VM.Migrate'],
- hidden: PVE.Utils.isStandaloneNode(),
- handler: function () {
- var win = Ext.create('PVE.window.Migrate', {
- vmtype: 'qemu',
- nodename: nodename,
- vmid: vmid,
- vmname: vm.name,
- });
- win.show();
- },
- iconCls: 'fa fa-send-o',
- });
-
- var moreBtn = Ext.create('Proxmox.button.Button', {
- text: gettext('More'),
- menu: {
- items: [
- {
- text: gettext('Clone'),
- iconCls: 'fa fa-fw fa-clone',
- hidden: !caps.vms['VM.Clone'],
- handler: function () {
- PVE.window.Clone.wrap(nodename, vmid, vm.name, template, 'qemu');
- },
- },
- {
- text: gettext('Convert to template'),
- disabled: template,
- xtype: 'pveMenuItem',
- iconCls: 'fa fa-fw fa-file-o',
- hidden: !caps.vms['VM.Allocate'],
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation(
- 'qmtemplate',
- vmid,
- vm.name,
- ),
- handler: function () {
- Proxmox.Utils.API2Request({
- url: base_url + '/template',
- waitMsgTarget: me,
- method: 'POST',
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- });
- },
- },
- {
- iconCls: 'fa fa-heartbeat ',
- hidden: !caps.nodes['Sys.Console'],
- text: gettext('Manage HA'),
- handler: function () {
- var ha = vm.hastate;
- Ext.create('PVE.ha.VMResourceEdit', {
- vmid: vmid,
- isCreate: !ha || ha === 'unmanaged',
- }).show();
- },
- },
- {
- text: gettext('Remove'),
- itemId: 'removeBtn',
- disabled: !caps.vms['VM.Allocate'],
- handler: function () {
- Ext.create('PVE.window.SafeDestroyGuest', {
- url: base_url,
- item: {
- type: 'VM',
- id: vmid,
- formattedIdentifier: PVE.Utils.getFormattedGuestIdentifier(
- vmid,
- vm.name,
- ),
- },
- taskName: 'qmdestroy',
- }).show();
- },
- iconCls: 'fa fa-trash-o',
- },
- ],
- },
- });
-
- var shutdownBtn = Ext.create('PVE.button.Split', {
- text: gettext('Shutdown'),
- disabled: !caps.vms['VM.PowerMgmt'] || !running,
- hidden: template,
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation('qmshutdown', vmid, vm.name),
- handler: function () {
- vm_command('shutdown');
- },
- menu: {
- items: [
- {
- text: gettext('Reboot'),
- disabled: !caps.vms['VM.PowerMgmt'],
- tooltip: Ext.String.format(
- gettext('Shutdown, apply pending changes and reboot {0}'),
- 'VM',
- ),
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation(
- 'qmreboot',
- vmid,
- vm.name,
- ),
- handler: function () {
- vm_command('reboot');
- },
- iconCls: 'fa fa-refresh',
- },
- {
- text: gettext('Pause'),
- disabled: !caps.vms['VM.PowerMgmt'],
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation('qmpause', vmid, vm.name),
- handler: function () {
- vm_command('suspend');
- },
- iconCls: 'fa fa-pause',
- },
- {
- text: gettext('Hibernate'),
- disabled: !caps.vms['VM.PowerMgmt'],
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation(
- 'qmsuspend',
- vmid,
- vm.name,
- ),
- tooltip: gettext('Suspend to disk'),
- handler: function () {
- vm_command('suspend', { todisk: 1 });
- },
- iconCls: 'fa fa-download',
- },
- {
- text: gettext('Stop'),
- disabled: !caps.vms['VM.PowerMgmt'],
- tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
- handler: function () {
- Ext.create('PVE.GuestStop', {
- nodename: nodename,
- vm: vm,
- autoShow: true,
- });
- },
- iconCls: 'fa fa-stop',
- },
- {
- text: gettext('Reset'),
- disabled: !caps.vms['VM.PowerMgmt'],
- tooltip: Ext.String.format(gettext('Reset {0} immediately'), 'VM'),
- confirmMsg: PVE.Utils.formatGuestTaskConfirmation('qmreset', vmid, vm.name),
- handler: function () {
- vm_command('reset');
- },
- iconCls: 'fa fa-bolt',
- },
- ],
- },
- iconCls: 'fa fa-power-off',
- });
-
- var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
- disabled: !caps.vms['VM.Console'],
- hidden: template,
- consoleType: 'kvm',
- // disable spice/xterm for default action until status api call succeeded
- enableSpice: false,
- enableXtermjs: false,
- consoleName: vm.name,
- nodename: nodename,
- vmid: vmid,
- });
-
- var statusTxt = Ext.create('Ext.toolbar.TextItem', {
- data: {
- lock: undefined,
- },
- tpl: ['', ' ({lock})', ' '],
- });
-
- let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
- tags: vm.tags,
- canEdit: !!caps.vms['VM.Config.Options'],
- listeners: {
- change: function (tags) {
- Proxmox.Utils.API2Request({
- url: base_url + '/config',
- method: 'PUT',
- params: {
- tags,
- },
- success: function () {
- me.statusStore.load();
- },
- failure: function (response) {
- Ext.Msg.alert('Error', response.htmlStatus);
- me.statusStore.load();
- },
- });
- },
- },
- });
-
- let vm_text = `${vm.vmid} (${vm.name})`;
-
- Ext.apply(me, {
- title: Ext.String.format(
- gettext("Virtual Machine {0} on node '{1}'"),
- vm_text,
- nodename,
- ),
- hstateid: 'kvmtab',
- tbarSpacing: false,
- tbar: [
- statusTxt,
- tagsContainer,
- '->',
- resumeBtn,
- startBtn,
- shutdownBtn,
- migrateBtn,
- consoleBtn,
- moreBtn,
- ],
- defaults: { statusStore: me.statusStore },
- items: [
- {
- title: gettext('Summary'),
- xtype: 'pveGuestSummary',
- iconCls: 'fa fa-book',
- itemId: 'summary',
- },
- ],
- });
-
- if (caps.vms['VM.Console'] && !template) {
- me.items.push({
- title: gettext('Console'),
- itemId: 'console',
- iconCls: 'fa fa-terminal',
- xtype: 'pveNoVncConsole',
- vmid: vmid,
- consoleType: 'kvm',
- nodename: nodename,
- });
- }
-
- me.items.push(
- {
- title: gettext('Hardware'),
- itemId: 'hardware',
- iconCls: 'fa fa-desktop',
- xtype: 'PVE.qemu.HardwareView',
- },
- {
- title: 'Cloud-Init',
- itemId: 'cloudinit',
- iconCls: 'fa fa-cloud',
- xtype: 'pveCiPanel',
- },
- {
- title: gettext('Options'),
- iconCls: 'fa fa-gear',
- itemId: 'options',
- xtype: 'PVE.qemu.Options',
- },
- {
- title: gettext('Task History'),
- itemId: 'tasks',
- xtype: 'proxmoxNodeTasks',
- iconCls: 'fa fa-list-alt',
- nodename: nodename,
- preFilter: {
- vmid,
- },
- },
- );
-
- if (caps.nodes['Sys.Audit'] && !template) {
- me.items.push({
- title: gettext('Monitor'),
- iconCls: 'fa fa-eye',
- itemId: 'monitor',
- xtype: 'pveQemuMonitor',
- });
- }
-
- if (caps.vms['VM.Backup']) {
- me.items.push(
- {
- title: gettext('Backup'),
- iconCls: 'fa fa-floppy-o',
- xtype: 'pveBackupView',
- itemId: 'backup',
- },
- {
- title: gettext('Replication'),
- iconCls: 'fa fa-retweet',
- xtype: 'pveReplicaView',
- itemId: 'replication',
- },
- );
- }
-
- if (
- (caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback'] || caps.vms['VM.Audit']) &&
- !template
- ) {
- me.items.push({
- title: gettext('Snapshots'),
- iconCls: 'fa fa-history',
- type: 'qemu',
- xtype: 'pveGuestSnapshotTree',
- itemId: 'snapshot',
- });
- }
-
- if (caps.vms['VM.Audit']) {
- me.items.push(
- {
- xtype: 'pveFirewallRules',
- title: gettext('Firewall'),
- iconCls: 'fa fa-shield',
- allow_iface: true,
- base_url: base_url + '/firewall/rules',
- list_refs_url: base_url + '/firewall/refs',
- itemId: 'firewall',
- firewall_type: 'vm',
- },
- {
- xtype: 'pveFirewallOptions',
- groups: ['firewall'],
- iconCls: 'fa fa-gear',
- onlineHelp: 'pve_firewall_vm_container_configuration',
- title: gettext('Options'),
- base_url: base_url + '/firewall/options',
- fwtype: 'vm',
- itemId: 'firewall-options',
- },
- {
- xtype: 'pveFirewallAliases',
- title: gettext('Alias'),
- groups: ['firewall'],
- iconCls: 'fa fa-external-link',
- base_url: base_url + '/firewall/aliases',
- itemId: 'firewall-aliases',
- },
- {
- xtype: 'pveIPSet',
- title: gettext('IPSet'),
- groups: ['firewall'],
- iconCls: 'fa fa-list-ol',
- base_url: base_url + '/firewall/ipset',
- list_refs_url: base_url + '/firewall/refs',
- itemId: 'firewall-ipset',
- },
- );
- }
-
- if (caps.vms['VM.Console']) {
- me.items.push({
- title: gettext('Log'),
- groups: ['firewall'],
- iconCls: 'fa fa-list',
- onlineHelp: 'chapter_pve_firewall',
- itemId: 'firewall-fwlog',
- xtype: 'proxmoxLogView',
- url: '/api2/extjs' + base_url + '/firewall/log',
- log_select_timespan: true,
- submitFormat: 'U',
- });
- }
-
- if (caps.vms['Permissions.Modify']) {
- me.items.push({
- xtype: 'pveACLView',
- title: gettext('Permissions'),
- iconCls: 'fa fa-unlock',
- itemId: 'permissions',
- path: '/vms/' + vmid,
- });
- }
-
- me.callParent();
-
- var prevQMPStatus = 'unknown';
- me.mon(me.statusStore, 'load', function (s, records, success) {
- var status;
- var qmpstatus;
- var spice = false;
- var xtermjs = false;
- var lock;
- var rec;
-
- if (!success) {
- status = qmpstatus = 'unknown';
- } else {
- rec = s.data.get('status');
- status = rec ? rec.data.value : 'unknown';
- rec = s.data.get('qmpstatus');
- qmpstatus = rec ? rec.data.value : 'unknown';
- rec = s.data.get('template');
- template = rec ? rec.data.value : false;
- rec = s.data.get('lock');
- lock = rec ? rec.data.value : undefined;
-
- spice = !!s.data.get('spice');
- xtermjs = !!s.data.get('serial');
- }
-
- rec = s.data.get('tags');
- tagsContainer.loadTags(rec?.data?.value);
-
- if (template) {
- return;
- }
-
- var resume = ['prelaunch', 'paused', 'suspended'].indexOf(qmpstatus) !== -1;
-
- if (resume || lock === 'suspended') {
- startBtn.setVisible(false);
- resumeBtn.setVisible(true);
- } else {
- startBtn.setVisible(true);
- resumeBtn.setVisible(false);
- }
-
- consoleBtn.setEnableSpice(spice);
- consoleBtn.setEnableXtermJS(xtermjs);
-
- statusTxt.update({ lock: lock });
-
- let guest_running =
- status === 'running' && !(qmpstatus === 'shutdown' || qmpstatus === 'prelaunch');
- startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || template || guest_running);
-
- shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
- me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
- consoleBtn.setDisabled(template);
-
- let wasStopped = ['prelaunch', 'stopped', 'suspended'].indexOf(prevQMPStatus) !== -1;
- if (wasStopped && qmpstatus === 'running') {
- let con = me.down('#console');
- if (con) {
- con.reload();
- }
- }
-
- prevQMPStatus = qmpstatus;
- });
-
- me.on('afterrender', function () {
- me.statusStore.startUpdate();
- });
-
- me.on('destroy', function () {
- me.statusStore.stopUpdate();
- });
- },
-});
-Ext.define('PVE.qemu.CreateWizard', {
- extend: 'PVE.window.Wizard',
- alias: 'widget.pveQemuCreateWizard',
- mixins: ['Proxmox.Mixin.CBind'],
-
- viewModel: {
- data: {
- nodename: '',
- current: {
- scsihw: '',
- },
- },
- formulas: {
- cgroupMode: function (get) {
- const nodeInfo = PVE.data.ResourceStore.getNodes().find(
- (node) => node.node === get('nodename'),
- );
- return nodeInfo ? nodeInfo['cgroup-mode'] : 2;
- },
- },
- },
-
- cbindData: {
- nodename: undefined,
- },
-
- subject: gettext('Virtual Machine'),
-
- // fot the special case that we have 2 cdrom drives
- //
- // emulates part of the backend bootorder logic, but includes all cdrom drives since the backend
- // cannot know which one is a bootable iso and hardcodes the known values (ide0/2, net0)
- calculateBootOrder: function (values) {
- // user selected windows + second cdrom
- if (values.ide0 && values.ide0.match(/media=cdrom/)) {
- let disk;
- PVE.Utils.forEachBus(['ide', 'scsi', 'virtio', 'sata'], (type, id) => {
- let confId = type + id;
- if (!values[confId]) {
- return undefined;
- }
- if (values[confId].match(/media=cdrom/)) {
- return undefined;
- }
- disk = confId;
- return false; // abort loop
- });
-
- let order = [];
- if (disk) {
- order.push(disk);
- }
- order.push('ide2', 'ide0'); // ide2 is the install ISO and should be first
- if (values.net0) {
- order.push('net0');
- }
-
- return `order=${order.join(';')}`;
- }
- return undefined;
- },
-
- items: [
- {
- xtype: 'inputpanel',
- title: gettext('General'),
- onlineHelp: 'qm_general_settings',
- column1: [
- {
- xtype: 'pveNodeSelector',
- name: 'nodename',
- cbind: {
- selectCurNode: '{!nodename}',
- preferredValue: '{nodename}',
- },
- bind: {
- value: '{nodename}',
- },
- fieldLabel: gettext('Node'),
- allowBlank: false,
- onlineValidator: true,
- },
- {
- xtype: 'pveGuestIDSelector',
- name: 'vmid',
- guestType: 'qemu',
- value: '',
- loadNextFreeID: true,
- validateExists: false,
- },
- {
- xtype: 'textfield',
- name: 'name',
- vtype: 'DnsName',
- value: '',
- fieldLabel: gettext('Name'),
- allowBlank: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'ha-managed',
- // only submit value of checkbox if checked
- uncheckedValue: undefined,
- fieldLabel: gettext('Add to HA'),
- },
- ],
- column2: [
- {
- xtype: 'pvePoolSelector',
- fieldLabel: gettext('Resource Pool'),
- name: 'pool',
- value: '',
- allowBlank: true,
- },
- ],
- advancedColumn1: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'onboot',
- uncheckedValue: 0,
- defaultValue: 0,
- deleteDefaultValue: true,
- fieldLabel: gettext('Start at boot'),
- },
- ],
- advancedColumn2: [
- {
- xtype: 'textfield',
- name: 'order',
- defaultValue: '',
- emptyText: 'any',
- labelWidth: 120,
- fieldLabel: gettext('Start/Shutdown order'),
- },
- {
- xtype: 'textfield',
- name: 'up',
- defaultValue: '',
- emptyText: 'default',
- labelWidth: 120,
- fieldLabel: gettext('Startup delay'),
- },
- {
- xtype: 'textfield',
- name: 'down',
- defaultValue: '',
- emptyText: 'default',
- labelWidth: 120,
- fieldLabel: gettext('Shutdown timeout'),
- },
- ],
-
- advancedColumnB: [
- {
- xtype: 'pveTagFieldSet',
- name: 'tags',
- maxHeight: 150,
- },
- ],
-
- onGetValues: function (values) {
- ['name', 'pool', 'onboot', 'agent'].forEach(function (field) {
- if (!values[field]) {
- delete values[field];
- }
- });
-
- var res = PVE.Parser.printStartup({
- order: values.order,
- up: values.up,
- down: values.down,
- });
-
- if (res) {
- values.startup = res;
- }
-
- delete values.order;
- delete values.up;
- delete values.down;
-
- return values;
- },
- },
- {
- xtype: 'container',
- layout: 'hbox',
- defaults: {
- flex: 1,
- padding: '0 10',
- },
- title: gettext('OS'),
- items: [
- {
- xtype: 'pveQemuCDInputPanel',
- bind: {
- nodename: '{nodename}',
- },
- confid: 'ide2',
- insideWizard: true,
- },
- {
- xtype: 'pveQemuOSTypePanel',
- insideWizard: true,
- bind: {
- nodename: '{nodename}',
- },
- },
- ],
- },
- {
- xtype: 'pveQemuSystemPanel',
- title: gettext('System'),
- isCreate: true,
- insideWizard: true,
- },
- {
- xtype: 'pveMultiHDPanel',
- bind: {
- nodename: '{nodename}',
- },
- title: gettext('Disks'),
- },
- {
- xtype: 'pveQemuProcessorPanel',
- insideWizard: true,
- title: gettext('CPU'),
- },
- {
- xtype: 'pveQemuMemoryPanel',
- insideWizard: true,
- title: gettext('Memory'),
- },
- {
- xtype: 'pveQemuNetworkInputPanel',
- bind: {
- nodename: '{nodename}',
- },
- title: gettext('Network'),
- insideWizard: true,
- },
- {
- title: gettext('Confirm'),
- layout: 'fit',
- items: [
- {
- xtype: 'grid',
- store: {
- model: 'KeyValue',
- sorters: [
- {
- property: 'key',
- direction: 'ASC',
- },
- ],
- },
- columns: [
- { header: 'Key', width: 150, dataIndex: 'key' },
- { header: 'Value', flex: 1, dataIndex: 'value', renderer: Ext.htmlEncode },
- ],
- },
- ],
- dockedItems: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'start',
- dock: 'bottom',
- margin: '5 0 0 0',
- boxLabel: gettext('Start after created'),
- },
- ],
- listeners: {
- show: function (panel) {
- let wizard = this.up('window');
- var kv = wizard.getValues();
- var data = [];
-
- let boot = wizard.calculateBootOrder(kv);
- if (boot) {
- kv.boot = boot;
- }
-
- Ext.Object.each(kv, function (key, value) {
- if (key === 'delete') {
- // ignore
- return;
- }
- data.push({ key: key, value: value });
- });
-
- var summarystore = panel.down('grid').getStore();
- summarystore.suspendEvents();
- summarystore.removeAll();
- summarystore.add(data);
- summarystore.sort();
- summarystore.resumeEvents();
- summarystore.fireEvent('refresh');
- },
- },
- onSubmit: function () {
- var wizard = this.up('window');
- var kv = wizard.getValues();
- delete kv.delete;
-
- var nodename = kv.nodename;
- delete kv.nodename;
-
- let boot = wizard.calculateBootOrder(kv);
- if (boot) {
- kv.boot = boot;
- }
-
- Proxmox.Utils.API2Request({
- url: '/nodes/' + nodename + '/qemu',
- waitMsgTarget: wizard,
- method: 'POST',
- params: kv,
- success: function (response) {
- wizard.close();
- },
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- });
- },
- },
- ],
-});
-Ext.define('PVE.qemu.DisplayInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveDisplayInputPanel',
- onlineHelp: 'qm_display',
-
- onGetValues: function (values) {
- let ret = PVE.Parser.printPropertyString(values, 'type');
- if (ret === '') {
- return { delete: 'vga' };
- }
- return { vga: ret };
- },
-
- viewModel: {
- data: {
- type: '__default__',
- clipboard: '__default__',
- },
- formulas: {
- matchNonGUIOption: function (get) {
- return get('type').match(/^(serial\d|none)$/);
- },
- memoryEmptyText: function (get) {
- let val = get('type');
- if (val === 'cirrus') {
- return '4';
- } else if (val === 'std' || val.match(/^qxl\d?$/) || val === 'vmware') {
- return '16';
- } else if (val.match(/^virtio/)) {
- return '256';
- } else if (get('matchNonGUIOption')) {
- return 'N/A';
- } else {
- console.debug('unexpected display type', val);
- return Proxmox.Utils.defaultText;
- }
- },
- isVNC: (get) => get('clipboard') === 'vnc',
- hideDefaultHint: (get) => get('isVNC') || get('matchNonGUIOption'),
- hideVNCHint: (get) => !get('isVNC') || get('matchNonGUIOption'),
- },
- },
-
- items: [
- {
- name: 'type',
- xtype: 'proxmoxKVComboBox',
- value: '__default__',
- deleteEmpty: false,
- fieldLabel: gettext('Graphic card'),
- comboItems: Object.entries(PVE.Utils.kvm_vga_drivers),
- validator: function (v) {
- let cfg = this.up('proxmoxWindowEdit').vmconfig || {};
-
- if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) {
- let fmt = gettext("Serial interface '{0}' is not correctly configured.");
- return Ext.String.format(fmt, v);
- }
- return true;
- },
- bind: {
- value: '{type}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- emptyText: Proxmox.Utils.defaultText,
- fieldLabel: gettext('Memory') + ' (MiB)',
- minValue: 4,
- maxValue: 512,
- step: 4,
- name: 'memory',
- bind: {
- emptyText: '{memoryEmptyText}',
- disabled: '{matchNonGUIOption}',
- },
- },
- ],
-
- advancedItems: [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'clipboard',
- deleteEmpty: false,
- value: '__default__',
- fieldLabel: gettext('Clipboard'),
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText],
- ['vnc', 'VNC'],
- ],
- bind: {
- value: '{clipboard}',
- disabled: '{matchNonGUIOption}',
- },
- },
- {
- xtype: 'displayfield',
- name: 'vncHint',
- userCls: 'pmx-hint',
- value:
- gettext(
- 'You cannot use the default SPICE clipboard if the VNC clipboard is selected.',
- ) +
- ' ' +
- gettext('VNC clipboard requires spice-tools installed in the Guest-VM.'),
- bind: {
- hidden: '{hideVNCHint}',
- },
- },
- {
- xtype: 'displayfield',
- name: 'vncMigration',
- userCls: 'pmx-hint',
- value: gettext('You cannot live-migrate while using the VNC clipboard.'),
- bind: {
- hidden: '{hideVNCHint}',
- },
- },
- {
- xtype: 'displayfield',
- name: 'defaultHint',
- userCls: 'pmx-hint',
- value:
- gettext('This option depends on your display type.') +
- ' ' +
- gettext(
- 'If the display type uses SPICE you are able to use the default SPICE clipboard.',
- ),
- bind: {
- hidden: '{hideDefaultHint}',
- },
- },
- ],
-});
-
-Ext.define('PVE.qemu.DisplayEdit', {
- extend: 'Proxmox.window.Edit',
-
- vmconfig: undefined,
-
- subject: gettext('Display'),
- width: 350,
-
- items: [
- {
- xtype: 'pveDisplayInputPanel',
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- me.load({
- success: function (response) {
- me.vmconfig = response.result.data;
- let vga = me.vmconfig.vga || '__default__';
- me.setValues(PVE.Parser.parsePropertyString(vga, 'type'));
- },
- });
- },
-});
-/* 'change' property is assigned a string and then a function */
-Ext.define('PVE.qemu.HDInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveQemuHDInputPanel',
- onlineHelp: 'qm_hard_disk',
-
- insideWizard: false,
-
- unused: false, // ADD usused disk imaged
-
- importDisk: false, // use import options
- importSelection: undefined, // preselect a disk to import
-
- vmconfig: {}, // used to select usused disks
-
- viewModel: {
- data: {
- isSCSI: false,
- isVirtIO: false,
- isSCSISingle: false,
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- onControllerChange: function (field) {
- let me = this;
- let vm = this.getViewModel();
-
- let value = field.getValue();
- vm.set('isSCSI', value.match(/^scsi/));
- vm.set('isVirtIO', value.match(/^virtio/));
-
- me.fireIdChange();
- },
-
- fireIdChange: function () {
- let view = this.getView();
- view.fireEvent('diskidchange', view, view.bussel.getConfId());
- },
-
- control: {
- 'field[name=controller]': {
- change: 'onControllerChange',
- afterrender: 'onControllerChange',
- },
- 'field[name=deviceid]': {
- change: 'fireIdChange',
- },
- 'field[name=scsiController]': {
- change: function (f, value) {
- let vm = this.getViewModel();
- vm.set('isSCSISingle', value === 'virtio-scsi-single');
- },
- },
- },
-
- init: function (view) {
- var vm = this.getViewModel();
- if (view.isCreate) {
- vm.set('isIncludedInBackup', true);
- }
- if (view.confid) {
- vm.set('isSCSI', view.confid.match(/^scsi/));
- vm.set('isVirtIO', view.confid.match(/^virtio/));
- }
- },
- },
-
- onGetValues: function (values) {
- var me = this;
-
- var params = {};
- var confid = me.confid || values.controller + values.deviceid;
-
- if (me.unused) {
- me.drive.file = me.vmconfig[values.unusedId];
- confid = values.controller + values.deviceid;
- } else if (me.isCreate) {
- if (values.hdimage) {
- me.drive.file = values.hdimage;
- } else {
- let disksize = values['import-from'] ? 0 : values.disksize;
- me.drive.file = `${values.hdstorage}:${disksize}`;
- PVE.Utils.propertyStringSet(me.drive, values['import-from'], 'import-from');
- }
- me.drive.format = values.diskformat;
- }
-
- PVE.Utils.propertyStringSet(me.drive, !values.backup, 'backup', '0');
- PVE.Utils.propertyStringSet(me.drive, values.noreplicate, 'replicate', 'no');
- PVE.Utils.propertyStringSet(me.drive, values.discard, 'discard', 'on');
- PVE.Utils.propertyStringSet(me.drive, values.ssd, 'ssd', 'on');
- PVE.Utils.propertyStringSet(me.drive, values.iothread, 'iothread', 'on');
- PVE.Utils.propertyStringSet(me.drive, values.readOnly, 'ro', 'on');
- PVE.Utils.propertyStringSet(me.drive, values.cache, 'cache');
- PVE.Utils.propertyStringSet(me.drive, values.aio, 'aio');
-
- ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'].forEach((name) => {
- let burst_name = `${name}_max`;
- PVE.Utils.propertyStringSet(me.drive, values[name], name);
- PVE.Utils.propertyStringSet(me.drive, values[burst_name], burst_name);
- });
-
- params[confid] = PVE.Parser.printQemuDrive(me.drive);
-
- return params;
- },
-
- updateVMConfig: function (vmconfig) {
- var me = this;
- me.vmconfig = vmconfig;
- me.bussel?.updateVMConfig(vmconfig);
- },
-
- setVMConfig: function (vmconfig) {
- var me = this;
-
- me.vmconfig = vmconfig;
-
- if (me.bussel) {
- me.bussel.setVMConfig(vmconfig);
- me.scsiController.setValue(vmconfig.scsihw);
- }
- if (me.unusedDisks) {
- let disklist = [];
- Ext.Object.each(vmconfig, function (key, value) {
- if (key.match(/^unused\d+$/)) {
- disklist.push([key, value]);
- }
- });
- me.unusedDisks.store.loadData(disklist);
- me.unusedDisks.setValue(me.confid);
- }
- },
-
- setDrive: function (drive) {
- var me = this;
-
- me.drive = drive;
-
- var values = {};
- var match = drive.file.match(/^([^:]+):/);
- if (match) {
- values.hdstorage = match[1];
- }
-
- values.hdimage = drive.file;
- values.backup = PVE.Parser.parseBoolean(drive.backup, 1);
- values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1);
- values.diskformat = drive.format || 'raw';
- values.cache = drive.cache || '__default__';
- values.discard = drive.discard === 'on';
- values.ssd = PVE.Parser.parseBoolean(drive.ssd);
- values.iothread = PVE.Parser.parseBoolean(drive.iothread);
- values.readOnly = PVE.Parser.parseBoolean(drive.ro);
- values.aio = drive.aio || '__default__';
-
- values.mbps_rd = drive.mbps_rd;
- values.mbps_wr = drive.mbps_wr;
- values.iops_rd = drive.iops_rd;
- values.iops_wr = drive.iops_wr;
- values.mbps_rd_max = drive.mbps_rd_max;
- values.mbps_wr_max = drive.mbps_wr_max;
- values.iops_rd_max = drive.iops_rd_max;
- values.iops_wr_max = drive.iops_wr_max;
-
- me.setValues(values);
- },
-
- setNodename: function (nodename) {
- var me = this;
- me.down('#hdstorage').setNodename(nodename);
- me.down('#hdimage').setStorage(undefined, nodename);
-
- me.lookup('new-disk')?.setNodename(nodename);
- me.lookup('import-source')?.setNodename(nodename);
- me.lookup('import-source-file')?.setNodename(nodename);
- me.lookup('import-target')?.setNodename(nodename);
- },
-
- hasAdvanced: true,
-
- initComponent: function () {
- var me = this;
-
- me.drive = {};
-
- let column1 = [];
- let column2 = [];
-
- let advancedColumn1 = [];
- let advancedColumn2 = [];
-
- if (!me.confid || me.unused) {
- me.bussel = Ext.create('PVE.form.ControllerSelector', {
- vmconfig: me.vmconfig,
- selectFree: true,
- });
- column1.push(me.bussel);
-
- me.scsiController = Ext.create('Ext.form.field.Display', {
- fieldLabel: gettext('SCSI Controller'),
- reference: 'scsiController',
- name: 'scsiController',
- bind: me.insideWizard
- ? {
- value: '{current.scsihw}',
- visible: '{isSCSI}',
- }
- : {
- visible: '{isSCSI}',
- },
- renderer: PVE.Utils.render_scsihw,
- submitValue: false,
- hidden: true,
- });
- column1.push(me.scsiController);
- }
-
- if (me.unused) {
- me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', {
- name: 'unusedId',
- fieldLabel: gettext('Disk image'),
- matchFieldWidth: false,
- listConfig: {
- width: 350,
- },
- data: [],
- allowBlank: false,
- });
- column1.push(me.unusedDisks);
- } else if (me.isCreate) {
- if (!me.importDisk) {
- column1.push({
- reference: 'new-disk',
- xtype: 'pveDiskStorageSelector',
- storageContent: 'images',
- name: 'disk',
- nodename: me.nodename,
- autoSelect: me.insideWizard,
- });
- } else {
- if (me.importSelection) {
- column1.push({
- xtype: 'displayfield',
- fieldLabel: gettext('Selected Image'),
- value: me.importSelection,
- });
- column1.push({
- xtype: 'hiddenfield',
- name: 'import-from',
- value: me.importSelection,
- });
- } else {
- column1.push({
- xtype: 'pveStorageSelector',
- reference: 'import-source',
- fieldLabel: gettext('Import Storage'),
- name: 'import-source-storage',
- storageContent: 'import',
- nodename: me.nodename,
- autoSelect: me.insideWizard,
- disabled: false,
- listeners: {
- change: function (_selector, storage) {
- me.lookup('import-source-file').setStorage(storage);
- me.lookup('import-source-file').setDisabled(!storage);
- },
- },
- });
- column1.push({
- xtype: 'pveFileSelector',
- reference: 'import-source-file',
- fieldLabel: gettext('Select Image'),
- storageContent: 'import',
- name: 'import-from',
- filter: (rec) => ['qcow2', 'vmdk', 'raw'].indexOf(rec?.data?.format) !== -1,
- nodename: me.nodename,
- });
- }
- column1.push({
- xtype: 'pveDiskStorageSelector',
- reference: 'import-target',
- storageLabel: gettext('Target Storage'),
- hideSize: true,
- storageContent: 'images',
- name: 'disk',
- nodename: me.nodename,
- autoSelect: me.insideWizard,
- });
- }
- } else {
- column1.push({
- xtype: 'textfield',
- disabled: true,
- submitValue: false,
- fieldLabel: gettext('Disk image'),
- name: 'hdimage',
- });
- }
-
- column2.push(
- {
- xtype: 'CacheTypeSelector',
- name: 'cache',
- value: '__default__',
- fieldLabel: gettext('Cache'),
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Discard'),
- reference: 'discard',
- name: 'discard',
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'iothread',
- fieldLabel: 'IO thread',
- clearOnDisable: true,
- bind:
- me.insideWizard || me.isCreate
- ? {
- disabled: '{!isVirtIO && !isSCSI}',
- // Checkbox.setValue handles Arrays in a different way, therefore cast to bool
- value: '{!!isVirtIO || (isSCSI && isSCSISingle)}',
- }
- : {
- disabled: '{!isVirtIO && !isSCSI}',
- },
- },
- );
-
- advancedColumn1.push(
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('SSD emulation'),
- name: 'ssd',
- clearOnDisable: true,
- bind: {
- disabled: '{isVirtIO}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'readOnly', // `ro` in the config, we map in get/set values
- defaultValue: 0,
- fieldLabel: gettext('Read-only'),
- clearOnDisable: true,
- bind: {
- disabled: '{!isVirtIO && !isSCSI}',
- },
- },
- );
-
- advancedColumn2.push(
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Backup'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Include volume in backup job'),
- },
- name: 'backup',
- bind: {
- value: '{isIncludedInBackup}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Skip replication'),
- name: 'noreplicate',
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'aio',
- fieldLabel: gettext('Async IO'),
- allowBlank: false,
- value: '__default__',
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (io_uring)'],
- ['io_uring', 'io_uring'],
- ['native', 'native'],
- ['threads', 'threads'],
- ],
- },
- );
-
- let labelWidth = 140;
-
- let bwColumn1 = [
- {
- xtype: 'numberfield',
- name: 'mbps_rd',
- minValue: 1,
- step: 1,
- fieldLabel: gettext('Read limit') + ' (MB/s)',
- labelWidth: labelWidth,
- emptyText: gettext('unlimited'),
- },
- {
- xtype: 'numberfield',
- name: 'mbps_wr',
- minValue: 1,
- step: 1,
- fieldLabel: gettext('Write limit') + ' (MB/s)',
- labelWidth: labelWidth,
- emptyText: gettext('unlimited'),
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'iops_rd',
- minValue: 10,
- step: 10,
- fieldLabel: gettext('Read limit') + ' (ops/s)',
- labelWidth: labelWidth,
- emptyText: gettext('unlimited'),
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'iops_wr',
- minValue: 10,
- step: 10,
- fieldLabel: gettext('Write limit') + ' (ops/s)',
- labelWidth: labelWidth,
- emptyText: gettext('unlimited'),
- },
- ];
-
- let bwColumn2 = [
- {
- xtype: 'numberfield',
- name: 'mbps_rd_max',
- minValue: 1,
- step: 1,
- fieldLabel: gettext('Read max burst') + ' (MB)',
- labelWidth: labelWidth,
- emptyText: gettext('default'),
- },
- {
- xtype: 'numberfield',
- name: 'mbps_wr_max',
- minValue: 1,
- step: 1,
- fieldLabel: gettext('Write max burst') + ' (MB)',
- labelWidth: labelWidth,
- emptyText: gettext('default'),
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'iops_rd_max',
- minValue: 10,
- step: 10,
- fieldLabel: gettext('Read max burst') + ' (ops)',
- labelWidth: labelWidth,
- emptyText: gettext('default'),
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'iops_wr_max',
- minValue: 10,
- step: 10,
- fieldLabel: gettext('Write max burst') + ' (ops)',
- labelWidth: labelWidth,
- emptyText: gettext('default'),
- },
- ];
-
- me.items = [
- {
- xtype: 'tabpanel',
- plain: true,
- bodyPadding: 10,
- border: 0,
- items: [
- {
- title: gettext('Disk'),
- xtype: 'inputpanel',
- reference: 'diskpanel',
- column1,
- column2,
- advancedColumn1,
- advancedColumn2,
- showAdvanced: me.showAdvanced,
- getValues: () => ({}),
- },
- {
- title: gettext('Bandwidth'),
- xtype: 'inputpanel',
- reference: 'bwpanel',
- column1: bwColumn1,
- column2: bwColumn2,
- showAdvanced: me.showAdvanced,
- getValues: () => ({}),
- },
- ],
- },
- ];
-
- me.callParent();
- },
-
- setAdvancedVisible: function (visible) {
- this.lookup('diskpanel').setAdvancedVisible(visible);
- this.lookup('bwpanel').setAdvancedVisible(visible);
- },
-});
-
-Ext.define('PVE.qemu.HDEdit', {
- extend: 'Proxmox.window.Edit',
-
- isAdd: true,
-
- backgroundDelay: 5,
-
- width: 600,
- bodyPadding: 0,
-
- importDisk: false,
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var unused = me.confid && me.confid.match(/^unused\d+$/);
-
- me.isCreate = me.confid ? unused : true;
-
- var ipanel = Ext.create('PVE.qemu.HDInputPanel', {
- confid: me.confid,
- nodename: nodename,
- unused: unused,
- isCreate: me.isCreate,
- importDisk: me.importDisk,
- });
-
- if (unused) {
- me.subject = gettext('Unused Disk');
- } else if (me.isCreate) {
- me.subject = gettext('Hard Disk');
- } else {
- me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
- }
-
- me.items = [ipanel];
-
- me.callParent();
- /* 'data' is assigned an empty array in same file, and here we
- * use it like an object
- */
- me.load({
- success: function (response, options) {
- ipanel.setVMConfig(response.result.data);
- if (me.confid) {
- let value = response.result.data[me.confid];
- let drive = PVE.Parser.parseQemuDrive(me.confid, value);
- if (!drive) {
- Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options');
- me.close();
- return;
- }
- ipanel.setDrive(drive);
- me.isValid(); // trigger validation
- }
- },
- });
- },
-});
-
-Ext.define('PVE.qemu.HDImportEdit', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- isAdd: true,
- isCreate: true,
-
- backgroundDelay: 5,
-
- width: 600,
- bodyPadding: 0,
-
- title: gettext('Import Hard Disk'),
-
- url: 'dummy', // will be set on vmid change
-
- cbindData: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no nodename given';
- }
-
- if (!me.selection) {
- throw 'no image preselected';
- }
-
- return {
- nodename: me.nodename,
- selection: me.selection,
- };
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- onVmidChange: function (_selector, value) {
- let me = this;
- let view = me.getView();
- let ipanel = me.lookup('ipanel');
- ipanel.setDisabled(true);
- ipanel.setVisible(!!value);
- let validation = me.lookup('validationProxy');
- validation.setValue(false);
- view.url = `/api2/extjs/nodes/${view.nodename}/qemu/${value}/config`;
- Proxmox.Utils.setErrorMask(ipanel, true);
-
- Proxmox.Utils.API2Request({
- url: view.url,
- method: 'GET',
- success: function (response, opts) {
- ipanel.setVMConfig(response.result.data);
-
- validation.setValue(true);
-
- ipanel.setDisabled(false);
- Proxmox.Utils.setErrorMask(ipanel, false);
- },
- failure: function (response, _opts) {
- Proxmox.Utils.setErrorMask(ipanel, response.htmlStatus);
- },
- });
- },
- },
-
- items: [
- {
- xtype: 'vmComboSelector',
- padding: 10,
- allowBlank: false,
- fieldLabel: gettext('Target Guest'),
- submitValue: false,
- cbind: {}, // for nested cbinds
- store: {
- model: 'PVEResources',
- autoLoad: true,
- sorters: 'vmid',
- cbind: {}, // for nested cbinds
- filters: [
- {
- property: 'type',
- value: 'qemu',
- },
- {
- property: 'node',
- cbind: {
- value: '{nodename}',
- },
- },
- ],
- },
- listeners: {
- change: 'onVmidChange',
- },
- },
- {
- // used to prevent submitting while vm config is being loaded or that returns an error
- xtype: 'textfield',
- reference: 'validationProxy',
- submitValue: false,
- hidden: true,
- validator: (val) => !!val,
- },
- {
- xtype: 'pveQemuHDInputPanel',
- reference: 'ipanel',
- hidden: true,
- disabled: true,
- isCreate: true,
- importDisk: true,
- cbind: {
- importSelection: '{selection}',
- nodename: '{nodename}',
- },
- },
- ],
-});
-Ext.define('PVE.qemu.EFIDiskInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveEFIDiskInputPanel',
-
- insideWizard: false,
-
- unused: false, // ADD usused disk imaged
-
- vmconfig: {}, // used to select usused disks
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.disabled) {
- return {};
- }
-
- var confid = 'efidisk0';
-
- if (values.hdimage) {
- me.drive.file = values.hdimage;
- } else {
- // we use 1 here, because for efi the size gets overridden from the backend
- me.drive.file = values.hdstorage + ':1';
- }
-
- // always default to newer 4m type with secure boot support, if we're
- // adding a new EFI disk there can't be any old state anyway
- me.drive.efitype = '4m';
- me.drive['pre-enrolled-keys'] = values.preEnrolledKeys;
- delete values.preEnrolledKeys;
-
- me.drive.format = values.diskformat;
- let params = {};
- params[confid] = PVE.Parser.printQemuDrive(me.drive);
- return params;
- },
-
- setNodename: function (nodename) {
- var me = this;
- me.down('#hdstorage').setNodename(nodename);
- me.down('#hdimage').setStorage(undefined, nodename);
- },
-
- setDisabled: function (disabled) {
- let me = this;
- me.down('pveDiskStorageSelector').setDisabled(disabled);
- me.down('proxmoxcheckbox[name=preEnrolledKeys]').setDisabled(disabled);
- me.callParent(arguments);
- },
-
- initComponent: function () {
- var me = this;
-
- me.drive = {};
-
- me.items = [
- {
- xtype: 'pveDiskStorageSelector',
- name: 'efidisk0',
- storageLabel: gettext('EFI Storage'),
- storageContent: 'images',
- nodename: me.nodename,
- disabled: me.disabled,
- hideSize: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'preEnrolledKeys',
- checked: true,
- fieldLabel: gettext('Pre-Enroll keys'),
- disabled: me.disabled,
- //boxLabel: '(e.g., Microsoft secure-boot keys')',
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Use EFIvars image with standard distribution and Microsoft secure boot keys enrolled.',
- ),
- },
- },
- {
- xtype: 'label',
- text: gettext("Warning: The VM currently does not uses 'OVMF (UEFI)' as BIOS."),
- userCls: 'pmx-hint',
- hidden: me.usesEFI,
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.EFIDiskEdit', {
- extend: 'Proxmox.window.Edit',
-
- isAdd: true,
- subject: gettext('EFI Disk'),
-
- width: 450,
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- me.items = [
- {
- xtype: 'pveEFIDiskInputPanel',
- onlineHelp: 'qm_bios_and_uefi',
- confid: me.confid,
- nodename: nodename,
- usesEFI: me.usesEFI,
- isCreate: true,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.qemu.TPMDiskInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveTPMDiskInputPanel',
-
- unused: false,
- vmconfig: {},
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.disabled) {
- return {};
- }
-
- var confid = 'tpmstate0';
-
- if (values.hdimage) {
- me.drive.file = values.hdimage;
- } else {
- // size is constant, so just use 1
- me.drive.file = values.hdstorage + ':1';
- }
-
- me.drive.format = values.diskformat;
- me.drive.version = values.version;
- var params = {};
- params[confid] = PVE.Parser.printQemuDrive(me.drive);
- return params;
- },
-
- setNodename: function (nodename) {
- var me = this;
- me.down('#hdstorage').setNodename(nodename);
- me.down('#hdimage').setStorage(undefined, nodename);
- },
-
- setDisabled: function (disabled) {
- let me = this;
- me.down('pveDiskStorageSelector').setDisabled(disabled);
- me.down('proxmoxKVComboBox[name=version]').setDisabled(disabled);
- me.callParent(arguments);
- },
-
- initComponent: function () {
- var me = this;
-
- me.drive = {};
-
- me.items = [
- {
- xtype: 'pveDiskStorageSelector',
- name: me.disktype + '0',
- storageLabel: gettext('TPM Storage'),
- storageContent: 'images',
- nodename: me.nodename,
- disabled: me.disabled,
- hideSize: true,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'version',
- value: 'v2.0',
- fieldLabel: gettext('Version'),
- deleteEmpty: false,
- disabled: me.disabled,
- comboItems: [
- ['v1.2', 'v1.2'],
- ['v2.0', 'v2.0'],
- ],
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.TPMDiskEdit', {
- extend: 'Proxmox.window.Edit',
-
- isAdd: true,
- subject: gettext('TPM State'),
-
- width: 450,
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- me.items = [
- {
- xtype: 'pveTPMDiskInputPanel',
- onlineHelp: 'qm_tpm',
- confid: me.confid,
- nodename: nodename,
- isCreate: true,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.window.HDMove', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- resizable: false,
- modal: true,
- width: 350,
- border: false,
- layout: 'fit',
- showReset: false,
- showTaskViewer: true,
- method: 'POST',
-
- cbindData: function () {
- let me = this;
- return {
- disk: me.disk,
- isQemu: me.type === 'qemu',
- nodename: me.nodename,
- url: () => {
- let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume';
- return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`;
- },
- };
- },
-
- cbind: {
- title: (get) => (get('isQemu') ? gettext('Move disk') : gettext('Move Volume')),
- submitText: (get) => get('title'),
- qemu: '{isQemu}',
- url: '{url}',
- },
-
- getValues: function () {
- let me = this;
- let values = me.formPanel.getForm().getValues();
-
- let params = {
- storage: values.hdstorage,
- };
- params[me.qemu ? 'disk' : 'volume'] = me.disk;
-
- if (values.diskformat && me.qemu) {
- params.format = values.diskformat;
- }
-
- if (values.deleteDisk) {
- params.delete = 1;
- }
- return params;
- },
-
- items: [
- {
- xtype: 'form',
- reference: 'moveFormPanel',
- border: false,
- fieldDefaults: {
- labelWidth: 100,
- anchor: '100%',
- },
- items: [
- {
- xtype: 'displayfield',
- cbind: {
- name: (get) => (get('isQemu') ? 'disk' : 'volume'),
- fieldLabel: (get) =>
- get('isQemu') ? gettext('Disk') : gettext('Mount Point'),
- value: '{disk}',
- },
- allowBlank: false,
- },
- {
- xtype: 'pveDiskStorageSelector',
- storageLabel: gettext('Target Storage'),
- cbind: {
- nodename: '{nodename}',
- storageContent: (get) => (get('isQemu') ? 'images' : 'rootdir'),
- },
- hideSize: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Delete source'),
- name: 'deleteDisk',
- uncheckedValue: 0,
- checked: false,
- },
- ],
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.vmid) {
- throw 'no VM ID specified';
- }
-
- if (!me.type) {
- throw 'no type specified';
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.window.HDResize', {
- extend: 'Ext.window.Window',
-
- resizable: false,
-
- resize_disk: function (disk, size) {
- var me = this;
- var params = { disk: disk, size: '+' + size + 'G' };
-
- Proxmox.Utils.API2Request({
- params: params,
- url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize',
- waitMsgTarget: me,
- method: 'PUT',
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response, options) {
- Ext.create('Proxmox.window.TaskProgress', {
- autoShow: true,
- upid: response.result.data,
- });
- me.close();
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.vmid) {
- throw 'no VM ID specified';
- }
-
- var items = [
- {
- xtype: 'displayfield',
- name: 'disk',
- value: me.disk,
- fieldLabel: gettext('Disk'),
- vtype: 'StorageId',
- allowBlank: false,
- },
- ];
-
- me.hdsizesel = Ext.createWidget('numberfield', {
- name: 'size',
- minValue: 0,
- maxValue: 128 * 1024,
- decimalPrecision: 3,
- value: '0',
- fieldLabel: `${gettext('Size Increment')} (${gettext('GiB')})`,
- allowBlank: false,
- });
-
- items.push(me.hdsizesel);
-
- me.formPanel = Ext.create('Ext.form.Panel', {
- bodyPadding: 10,
- border: false,
- fieldDefaults: {
- labelWidth: 140,
- anchor: '100%',
- },
- items: items,
- });
-
- var form = me.formPanel.getForm();
-
- var submitBtn;
-
- me.title = gettext('Resize disk');
- submitBtn = Ext.create('Ext.Button', {
- text: gettext('Resize disk'),
- handler: function () {
- if (form.isValid()) {
- let values = form.getValues();
- me.resize_disk(me.disk, values.size);
- }
- },
- });
-
- Ext.apply(me, {
- modal: true,
- width: 250,
- height: 150,
- border: false,
- layout: 'fit',
- buttons: [submitBtn],
- items: [me.formPanel],
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.qemu.HardwareView', {
- extend: 'Proxmox.grid.PendingObjectGrid',
- alias: ['widget.PVE.qemu.HardwareView'],
-
- onlineHelp: 'qm_virtual_machines_settings',
-
- renderKey: function (key, metaData, rec, rowIndex, colIndex, store) {
- var me = this;
- var rows = me.rows;
- var rowdef = rows[key] || {};
- var iconCls = rowdef.iconCls;
- var icon = '';
- var txt = rowdef.header || key;
-
- metaData.tdAttr = 'valign=middle';
-
- if (rowdef.isOnStorageBus) {
- let value = me.getObjectValue(key, '', false);
- if (value === '') {
- value = me.getObjectValue(key, '', true);
- }
- if (value.match(/vm-.*-cloudinit/)) {
- iconCls = 'cloud';
- txt = rowdef.cloudheader;
- } else if (value.match(/media=cdrom/)) {
- metaData.tdCls = 'pve-itype-icon-cdrom';
- return rowdef.cdheader;
- }
- }
-
- if (rowdef.tdCls) {
- metaData.tdCls = rowdef.tdCls;
- } else if (iconCls) {
- icon = " ";
- metaData.tdCls += ' pve-itype-fa';
- }
-
- // only return icons in grid but not remove dialog
- if (rowIndex !== undefined) {
- return icon + txt;
- } else {
- return txt;
- }
- },
-
- initComponent: function () {
- var me = this;
-
- const { node: nodename, vmid } = me.pveSelNode.data;
- if (!nodename) {
- throw 'no node name specified';
- } else if (!vmid) {
- throw 'no VM ID specified';
- }
-
- const caps = Ext.state.Manager.get('GuiCap');
- const diskCap = caps.vms['VM.Config.Disk'];
- const cdromCap = caps.vms['VM.Config.CDROM'];
-
- let isCloudInitKey = (v) => v && v.toString().match(/vm-.*-cloudinit/);
-
- const nodeInfo = PVE.data.ResourceStore.getNodes().find((node) => node.node === nodename);
- let processorEditor = {
- xtype: 'pveQemuProcessorEdit',
- cgroupMode: nodeInfo['cgroup-mode'],
- };
-
- let rows = {
- memory: {
- header: gettext('Memory'),
- editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
- never_delete: true,
- defaultValue: '512',
- tdCls: 'pve-itype-icon-memory',
- group: 2,
- multiKey: ['memory', 'balloon', 'shares', 'allow-ksm'],
- renderer: function (value, metaData, record, ri, ci, store, pending) {
- var res = '';
-
- var max = me.getObjectValue('memory', 512, pending);
- var balloon = me.getObjectValue('balloon', undefined, pending);
- var shares = me.getObjectValue('shares', undefined, pending);
-
- res = Proxmox.Utils.format_size(max * 1024 * 1024);
-
- if (balloon !== undefined && balloon > 0) {
- res = Proxmox.Utils.format_size(balloon * 1024 * 1024) + '/' + res;
-
- if (shares) {
- res += ' [shares=' + shares + ']';
- }
- } else if (balloon === 0) {
- res += ' [balloon=0]';
- }
-
- let allowKsm = me.getObjectValue('allow-ksm', undefined, pending);
- if (allowKsm !== undefined) {
- res += ' [allow-ksm=' + allowKsm + ']';
- }
-
- return res;
- },
- },
- sockets: {
- header: gettext('Processors'),
- never_delete: true,
- editor:
- caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']
- ? processorEditor
- : undefined,
- tdCls: 'pve-itype-icon-cpu',
- group: 3,
- defaultValue: '1',
- multiKey: [
- 'sockets',
- 'cpu',
- 'cores',
- 'numa',
- 'vcpus',
- 'cpulimit',
- 'cpuunits',
- 'affinity',
- ],
- renderer: function (value, metaData, record, rowIndex, colIndex, store, pending) {
- var sockets = me.getObjectValue('sockets', 1, pending);
- var model = me.getObjectValue('cpu', undefined, pending);
- var cores = me.getObjectValue('cores', 1, pending);
- var numa = me.getObjectValue('numa', undefined, pending);
- var vcpus = me.getObjectValue('vcpus', undefined, pending);
- var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
- var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
- var cpuaffinity = me.getObjectValue('affinity', undefined, pending);
-
- let res = Ext.String.format(
- '{0} ({1} sockets, {2} cores)',
- sockets * cores,
- sockets,
- cores,
- );
-
- if (model) {
- res += ' [' + model + ']';
- }
- if (numa) {
- res += ' [numa=' + numa + ']';
- }
- if (vcpus) {
- res += ' [vcpus=' + vcpus + ']';
- }
- if (cpulimit) {
- res += ' [cpulimit=' + cpulimit + ']';
- }
- if (cpuunits) {
- res += ' [cpuunits=' + cpuunits + ']';
- }
- if (cpuaffinity) {
- res += ' [cpuaffinity=' + cpuaffinity + ']';
- }
-
- return res;
- },
- },
- bios: {
- header: 'BIOS',
- group: 4,
- never_delete: true,
- editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
- defaultValue: '',
- iconCls: 'microchip',
- renderer: PVE.Utils.render_qemu_bios,
- },
- vga: {
- header: gettext('Display'),
- editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
- never_delete: true,
- iconCls: 'desktop',
- group: 5,
- defaultValue: '',
- renderer: PVE.Utils.render_kvm_vga_driver,
- },
- machine: {
- header: gettext('Machine'),
- editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined,
- iconCls: 'cogs',
- never_delete: true,
- group: 6,
- defaultValue: '',
- renderer: function (value, metaData, record, rowIndex, colIndex, store, pending) {
- let ostype = me.getObjectValue('ostype', undefined, pending);
- if (
- PVE.Utils.is_windows(ostype) &&
- (!value || value === 'pc' || value === 'q35')
- ) {
- return value === 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1';
- }
- return PVE.Utils.render_qemu_machine(value);
- },
- },
- scsihw: {
- header: gettext('SCSI Controller'),
- iconCls: 'database',
- editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
- renderer: PVE.Utils.render_scsihw,
- group: 7,
- never_delete: true,
- defaultValue: '',
- },
- vmstate: {
- header: gettext('Hibernation VM State'),
- iconCls: 'download',
- del_extra_msg: gettext('The saved VM state will be permanently lost.'),
- group: 100,
- },
- cores: {
- visible: false,
- },
- cpu: {
- visible: false,
- },
- numa: {
- visible: false,
- },
- 'allow-ksm': {
- visible: false,
- },
- balloon: {
- visible: false,
- },
- hotplug: {
- visible: false,
- },
- vcpus: {
- visible: false,
- },
- cpuunits: {
- visible: false,
- },
- cpulimit: {
- visible: false,
- },
- shares: {
- visible: false,
- },
- ostype: {
- visible: false,
- },
- affinity: {
- visible: false,
- },
- };
-
- PVE.Utils.forEachBus(undefined, function (type, id) {
- let confid = type + id;
- rows[confid] = {
- group: 10,
- iconCls: 'hdd-o',
- editor: 'PVE.qemu.HDEdit',
- isOnStorageBus: true,
- header: gettext('Hard Disk') + ' (' + confid + ')',
- cdheader: gettext('CD/DVD Drive') + ' (' + confid + ')',
- cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')',
- renderer: Ext.htmlEncode,
- };
- });
- for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) {
- let confid = 'net' + i.toString();
- rows[confid] = {
- group: 15,
- order: i,
- iconCls: 'exchange',
- editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
- never_delete: !caps.vms['VM.Config.Network'],
- header: gettext('Network Device') + ' (' + confid + ')',
- };
- }
- rows.efidisk0 = {
- group: 20,
- iconCls: 'hdd-o',
- editor: null,
- never_delete: !caps.vms['VM.Config.Disk'],
- header: gettext('EFI Disk'),
- renderer: Ext.htmlEncode,
- };
- rows.tpmstate0 = {
- group: 22,
- iconCls: 'hdd-o',
- editor: null,
- never_delete: !caps.vms['VM.Config.Disk'],
- header: gettext('TPM State'),
- renderer: Ext.htmlEncode,
- };
- for (let i = 0; i < PVE.Utils.hardware_counts.usb; i++) {
- let confid = 'usb' + i.toString();
- rows[confid] = {
- group: 25,
- order: i,
- iconCls: 'usb',
- editor:
- caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use']
- ? 'PVE.qemu.USBEdit'
- : undefined,
- never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
- header: gettext('USB Device') + ' (' + confid + ')',
- };
- }
- for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
- let confid = 'hostpci' + i.toString();
- rows[confid] = {
- group: 30,
- order: i,
- tdCls: 'pve-itype-icon-pci',
- never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
- editor:
- caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use']
- ? 'PVE.qemu.PCIEdit'
- : undefined,
- header: gettext('PCI Device') + ' (' + confid + ')',
- };
- }
- for (let i = 0; i < PVE.Utils.hardware_counts.serial; i++) {
- let confid = 'serial' + i.toString();
- rows[confid] = {
- group: 35,
- order: i,
- tdCls: 'pve-itype-icon-serial',
- never_delete: !caps.nodes['Sys.Console'],
- header: gettext('Serial Port') + ' (' + confid + ')',
- };
- }
- rows.audio0 = {
- group: 40,
- iconCls: 'volume-up',
- editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
- never_delete: !caps.vms['VM.Config.HWType'],
- header: gettext('Audio Device'),
- };
- for (let i = 0; i < 256; i++) {
- rows['unused' + i.toString()] = {
- group: 99,
- order: i,
- iconCls: 'hdd-o',
- del_extra_msg: gettext('This will permanently erase all data.'),
- editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
- header: gettext('Unused Disk') + ' ' + i.toString(),
- renderer: Ext.htmlEncode,
- };
- }
- rows.rng0 = {
- group: 45,
- tdCls: 'pve-itype-icon-die',
- editor:
- caps.vms['VM.Config.HWType'] || caps.mapping['Mapping.Use']
- ? 'PVE.qemu.RNGEdit'
- : undefined,
- never_delete: !caps.vms['VM.Config.HWType'] && !caps.mapping['Mapping.Use'],
- header: gettext('VirtIO RNG'),
- };
- for (let i = 0; i < PVE.Utils.hardware_counts.virtiofs; i++) {
- let confid = 'virtiofs' + i.toString();
- rows[confid] = {
- group: 50,
- order: i,
- iconCls: 'folder',
- editor: 'PVE.qemu.VirtiofsEdit',
- header: gettext('Virtiofs') + ' (' + confid + ')',
- };
- }
-
- var sorterFn = function (rec1, rec2) {
- var v1 = rec1.data.key;
- var v2 = rec2.data.key;
- var g1 = rows[v1].group || 0;
- var g2 = rows[v2].group || 0;
- var order1 = rows[v1].order || 0;
- var order2 = rows[v2].order || 0;
-
- if (g1 - g2 !== 0) {
- return g1 - g2;
- }
-
- if (order1 - order2 !== 0) {
- return order1 - order2;
- }
-
- if (v1 > v2) {
- return 1;
- } else if (v1 < v2) {
- return -1;
- } else {
- return 0;
- }
- };
-
- let baseurl = `nodes/${nodename}/qemu/${vmid}/config`;
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec || !rows[rec.data.key]?.editor) {
- return;
- }
- let rowdef = rows[rec.data.key];
- let editor = rowdef.editor;
-
- if (rowdef.isOnStorageBus) {
- let value = me.getObjectValue(rec.data.key, '', true);
- if (isCloudInitKey(value)) {
- return;
- } else if (value.match(/media=cdrom/)) {
- editor = 'PVE.qemu.CDEdit';
- } else if (!diskCap) {
- return;
- }
- }
-
- let commonOpts = {
- autoShow: true,
- pveSelNode: me.pveSelNode,
- confid: rec.data.key,
- url: `/api2/extjs/${baseurl}`,
- listeners: {
- destroy: () => me.reload(),
- },
- };
-
- if (Ext.isString(editor)) {
- Ext.create(editor, commonOpts);
- } else {
- let win = Ext.createWidget(
- rowdef.editor.xtype,
- Ext.apply(commonOpts, rowdef.editor),
- );
- win.load();
- }
- };
-
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- selModel: sm,
- disabled: true,
- handler: run_editor,
- });
-
- let move_menuitem = new Ext.menu.Item({
- text: gettext('Move Storage'),
- tooltip: gettext('Move disk to another storage'),
- iconCls: 'fa fa-database',
- selModel: sm,
- handler: () => {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- Ext.create('PVE.window.HDMove', {
- autoShow: true,
- disk: rec.data.key,
- nodename: nodename,
- vmid: vmid,
- type: 'qemu',
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
- });
-
- let reassign_menuitem = new Ext.menu.Item({
- text: gettext('Reassign Owner'),
- tooltip: gettext('Reassign disk to another VM'),
- iconCls: 'fa fa-desktop',
- selModel: sm,
- handler: () => {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
-
- Ext.create('PVE.window.GuestDiskReassign', {
- autoShow: true,
- disk: rec.data.key,
- nodename: nodename,
- vmid: vmid,
- type: 'qemu',
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
- });
-
- let resize_menuitem = new Ext.menu.Item({
- text: gettext('Resize'),
- iconCls: 'fa fa-plus',
- selModel: sm,
- handler: () => {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- Ext.create('PVE.window.HDResize', {
- autoShow: true,
- disk: rec.data.key,
- nodename: nodename,
- vmid: vmid,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
- });
-
- let diskaction_btn = new Proxmox.button.Button({
- text: gettext('Disk Action'),
- disabled: true,
- menu: {
- items: [move_menuitem, reassign_menuitem, resize_menuitem],
- },
- });
-
- let remove_btn = new Proxmox.button.Button({
- text: gettext('Remove'),
- defaultText: gettext('Remove'),
- altText: gettext('Detach'),
- selModel: sm,
- disabled: true,
- dangerous: true,
- RESTMethod: 'PUT',
- confirmMsg: function (rec) {
- let warn = gettext('Are you sure you want to remove entry {0}');
- if (this.text === this.altText) {
- warn = gettext('Are you sure you want to detach entry {0}');
- }
- let rendered = me.renderKey(rec.data.key, {}, rec);
- let msg = Ext.String.format(warn, `'${rendered}'`);
-
- if (rows[rec.data.key].del_extra_msg) {
- msg += ' ' + rows[rec.data.key].del_extra_msg;
- }
- return msg;
- },
- handler: function (btn, e, rec) {
- let params = { delete: rec.data.key };
- if (btn.RESTMethod === 'POST') {
- params.background_delay = 5;
- }
- Proxmox.Utils.API2Request({
- url: '/api2/extjs/' + baseurl,
- waitMsgTarget: me,
- method: btn.RESTMethod,
- params: params,
- callback: () => me.reload(),
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- success: function (response, options) {
- if (btn.RESTMethod === 'POST' && response.result.data !== null) {
- Ext.create('Proxmox.window.TaskProgress', {
- autoShow: true,
- upid: response.result.data,
- listeners: {
- destroy: () => me.reload(),
- },
- });
- }
- },
- });
- },
- listeners: {
- render: function (btn) {
- // hack: calculate the max button width on first display to prevent the whole
- // toolbar to move when we switch between the "Remove" and "Detach" labels
- var def = btn.getSize().width;
-
- btn.setText(btn.altText);
- var alt = btn.getSize().width;
-
- btn.setText(btn.defaultText);
-
- var optimal = alt > def ? alt : def;
- btn.setSize({ width: optimal });
- },
- },
- });
-
- let revert_btn = new PVE.button.PendingRevert({
- apiurl: '/api2/extjs/' + baseurl,
- });
-
- let efidisk_menuitem = Ext.create('Ext.menu.Item', {
- text: gettext('EFI Disk'),
- iconCls: 'fa fa-fw fa-hdd-o black',
- disabled: !caps.vms['VM.Config.Disk'],
- handler: function () {
- let { data: bios } = me.rstore.getData().map.bios || {};
-
- Ext.create('PVE.qemu.EFIDiskEdit', {
- autoShow: true,
- url: '/api2/extjs/' + baseurl,
- pveSelNode: me.pveSelNode,
- usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf',
- listeners: {
- destroy: () => me.reload(),
- },
- });
- },
- });
-
- let counts = {};
- let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type];
- let isAtUsbLimit = () => {
- let ostype = me.getObjectValue('ostype');
- let machine = me.getObjectValue('machine');
- return counts.usb >= PVE.Utils.get_max_usb_count(ostype, machine);
- };
-
- let set_button_status = function () {
- let selection_model = me.getSelectionModel();
- let rec = selection_model.getSelection()[0];
-
- counts = {}; // en/disable hardwarebuttons
- let hasCloudInit = false;
- me.rstore.getData().items.forEach(function ({ id, data }) {
- if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) {
- hasCloudInit = true;
- return;
- }
-
- let match = id.match(/^([^\d]+)\d+$/);
- if (match && PVE.Utils.hardware_counts[match[1]] !== undefined) {
- let type = match[1];
- counts[type] = (counts[type] || 0) + 1;
- }
- });
-
- // heuristic only for disabling some stuff, the backend has the final word.
- const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
- const noVMConfigNetPerm = !caps.vms['VM.Config.Network'];
- const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk'];
- const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM'];
- const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit'];
- const noVMConfigOptionsPerm = !caps.vms['VM.Config.Options'];
-
- me.down('#addUsb').setDisabled(noVMConfigHWTypePerm || isAtUsbLimit());
- me.down('#addPci').setDisabled(noVMConfigHWTypePerm || isAtLimit('hostpci'));
- me.down('#addAudio').setDisabled(noVMConfigHWTypePerm || isAtLimit('audio'));
- me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial'));
- me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net'));
- me.down('#addRng').setDisabled(noVMConfigHWTypePerm || isAtLimit('rng'));
- efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk'));
- me.down('#addTpmState').setDisabled(noVMConfigDiskPerm || isAtLimit('tpmstate'));
- me.down('#addVirtiofs').setDisabled(noVMConfigOptionsPerm || isAtLimit('virtiofs'));
- me.down('#addCloudinitDrive').setDisabled(
- noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit,
- );
-
- if (!rec) {
- remove_btn.disable();
- edit_btn.disable();
- diskaction_btn.disable();
- revert_btn.disable();
- return;
- }
- const { key, value } = rec.data;
- const row = rows[key];
-
- const deleted = !!rec.data.delete;
- const pending = deleted || me.hasPendingChanges(key);
- const isRunning = me.pveSelNode.data.running;
-
- const isCloudInit = isCloudInitKey(value);
- const isCDRom = value && !!value.toString().match(/media=cdrom/);
-
- const isUnusedDisk = key.match(/^unused\d+/);
- const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
- const isDisk = isUnusedDisk || isUsedDisk;
- const isEfi = key === 'efidisk0';
- const tpmMoveable = key === 'tpmstate0' && !isRunning;
-
- let cannotDelete = deleted || row.never_delete;
- cannotDelete ||= isCDRom && !cdromCap;
- cannotDelete ||= isDisk && !diskCap;
- cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm;
- remove_btn.setDisabled(cannotDelete);
-
- remove_btn.setText(
- isUsedDisk && !isCloudInit ? remove_btn.altText : remove_btn.defaultText,
- );
- remove_btn.RESTMethod = isUnusedDisk || (isDisk && isRunning) ? 'POST' : 'PUT';
-
- edit_btn.setDisabled(
- deleted ||
- !row.editor ||
- isCloudInit ||
- (isCDRom && !cdromCap) ||
- (isDisk && !diskCap),
- );
-
- diskaction_btn.setDisabled(
- pending || !diskCap || isCloudInit || !(isDisk || isEfi || tpmMoveable),
- );
- reassign_menuitem.setDisabled(pending || isEfi || tpmMoveable);
- resize_menuitem.setDisabled(pending || !isUsedDisk);
-
- revert_btn.setDisabled(!pending);
- };
-
- let editorFactory = (classPath, extraOptions) => {
- extraOptions = extraOptions || {};
- return () =>
- Ext.create(`PVE.qemu.${classPath}`, {
- autoShow: true,
- url: `/api2/extjs/${baseurl}`,
- pveSelNode: me.pveSelNode,
- listeners: {
- destroy: () => me.reload(),
- },
- isAdd: true,
- isCreate: true,
- ...extraOptions,
- });
- };
-
- Ext.apply(me, {
- url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
- interval: 5000,
- selModel: sm,
- run_editor: run_editor,
- tbar: [
- {
- text: gettext('Add'),
- menu: new Ext.menu.Menu({
- cls: 'pve-add-hw-menu',
- items: [
- {
- text: gettext('Hard Disk'),
- iconCls: 'fa fa-fw fa-hdd-o black',
- disabled: !caps.vms['VM.Config.Disk'],
- handler: editorFactory('HDEdit'),
- },
- {
- text: gettext('Import Hard Disk'),
- iconCls: 'fa fa-fw fa-cloud-download',
- disabled: !caps.vms['VM.Config.Disk'],
- handler: editorFactory('HDEdit', { importDisk: true }),
- },
- {
- text: gettext('CD/DVD Drive'),
- iconCls: 'pve-itype-icon-cdrom',
- disabled: !caps.vms['VM.Config.CDROM'],
- handler: editorFactory('CDEdit'),
- },
- {
- text: gettext('Network Device'),
- itemId: 'addNet',
- iconCls: 'fa fa-fw fa-exchange black',
- disabled: !caps.vms['VM.Config.Network'],
- handler: editorFactory('NetworkEdit'),
- },
- efidisk_menuitem,
- {
- text: gettext('TPM State'),
- itemId: 'addTpmState',
- iconCls: 'fa fa-fw fa-hdd-o black',
- disabled: !caps.vms['VM.Config.Disk'],
- handler: editorFactory('TPMDiskEdit'),
- },
- {
- text: gettext('USB Device'),
- itemId: 'addUsb',
- iconCls: 'fa fa-fw fa-usb black',
- disabled:
- !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
- handler: editorFactory('USBEdit'),
- },
- {
- text: gettext('PCI Device'),
- itemId: 'addPci',
- iconCls: 'pve-itype-icon-pci',
- disabled:
- !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
- handler: editorFactory('PCIEdit'),
- },
- {
- text: gettext('Serial Port'),
- itemId: 'addSerial',
- iconCls: 'pve-itype-icon-serial',
- disabled: !caps.vms['VM.Config.Options'],
- handler: editorFactory('SerialEdit'),
- },
- {
- text: gettext('CloudInit Drive'),
- itemId: 'addCloudinitDrive',
- iconCls: 'fa fa-fw fa-cloud black',
- disabled:
- !caps.vms['VM.Config.CDROM'] ||
- !caps.vms['VM.Config.Cloudinit'],
- handler: editorFactory('CIDriveEdit'),
- },
- {
- text: gettext('Audio Device'),
- itemId: 'addAudio',
- iconCls: 'fa fa-fw fa-volume-up black',
- disabled: !caps.vms['VM.Config.HWType'],
- handler: editorFactory('AudioEdit'),
- },
- {
- text: gettext('VirtIO RNG'),
- itemId: 'addRng',
- iconCls: 'pve-itype-icon-die',
- disabled:
- !caps.vms['VM.Config.HWType'] && !caps.mapping['Mapping.Use'],
- handler: editorFactory('RNGEdit'),
- },
- {
- text: gettext('Virtiofs'),
- itemId: 'addVirtiofs',
- iconCls: 'fa fa-folder',
- disabled: !caps.nodes['Sys.Console'],
- handler: editorFactory('VirtiofsEdit'),
- },
- ],
- }),
- },
- remove_btn,
- edit_btn,
- diskaction_btn,
- revert_btn,
- ],
- rows: rows,
- sorterFn: sorterFn,
- listeners: {
- itemdblclick: run_editor,
- selectionchange: set_button_status,
- },
- });
-
- me.callParent();
-
- me.on('activate', me.rstore.startUpdate, me.rstore);
- me.on('destroy', me.rstore.stopUpdate, me.rstore);
-
- me.mon(me.getStore(), 'datachanged', set_button_status, me);
- },
-});
-Ext.define('PVE.qemu.IPConfigPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveIPConfigPanel',
-
- insideWizard: false,
-
- vmconfig: {},
-
- onGetValues: function (values) {
- var me = this;
-
- if (values.ipv4mode !== 'static') {
- values.ip = values.ipv4mode;
- }
-
- if (values.ipv6mode !== 'static') {
- values.ip6 = values.ipv6mode;
- }
-
- var params = {};
-
- var cfg = PVE.Parser.printIPConfig(values);
- if (cfg === '') {
- params.delete = [me.confid];
- } else {
- params[me.confid] = cfg;
- }
- return params;
- },
-
- setVMConfig: function (config) {
- var me = this;
- me.vmconfig = config;
- },
-
- setIPConfig: function (confid, data) {
- var me = this;
-
- me.confid = confid;
-
- if (data.ip === 'dhcp') {
- data.ipv4mode = data.ip;
- data.ip = '';
- } else {
- data.ipv4mode = 'static';
- }
- if (data.ip6 === 'dhcp' || data.ip6 === 'auto') {
- data.ipv6mode = data.ip6;
- data.ip6 = '';
- } else {
- data.ipv6mode = 'static';
- }
-
- me.ipconfig = data;
- me.setValues(me.ipconfig);
- },
-
- initComponent: function () {
- var me = this;
-
- me.ipconfig = {};
-
- me.column1 = [
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Network Device'),
- value: me.netid,
- },
- {
- layout: {
- type: 'hbox',
- align: 'middle',
- },
- border: false,
- margin: '0 0 5 0',
- items: [
- {
- xtype: 'label',
- text: gettext('IPv4') + ':',
- },
- {
- xtype: 'radiofield',
- boxLabel: gettext('Static'),
- name: 'ipv4mode',
- inputValue: 'static',
- checked: false,
- margin: '0 0 0 10',
- listeners: {
- change: function (cb, value) {
- me.down('field[name=ip]').setDisabled(!value);
- me.down('field[name=gw]').setDisabled(!value);
- },
- },
- },
- {
- xtype: 'radiofield',
- boxLabel: gettext('DHCP'),
- name: 'ipv4mode',
- inputValue: 'dhcp',
- checked: false,
- margin: '0 0 0 10',
- },
- ],
- },
- {
- xtype: 'textfield',
- name: 'ip',
- vtype: 'IPCIDRAddress',
- value: '',
- disabled: true,
- fieldLabel: gettext('IPv4/CIDR'),
- },
- {
- xtype: 'textfield',
- name: 'gw',
- value: '',
- vtype: 'IPAddress',
- disabled: true,
- fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') + ')',
- },
- ];
-
- me.column2 = [
- {
- xtype: 'displayfield',
- },
- {
- layout: {
- type: 'hbox',
- align: 'middle',
- },
- border: false,
- margin: '0 0 5 0',
- items: [
- {
- xtype: 'label',
- text: gettext('IPv6') + ':',
- },
- {
- xtype: 'radiofield',
- boxLabel: gettext('Static'),
- name: 'ipv6mode',
- inputValue: 'static',
- checked: false,
- margin: '0 0 0 10',
- listeners: {
- change: function (cb, value) {
- me.down('field[name=ip6]').setDisabled(!value);
- me.down('field[name=gw6]').setDisabled(!value);
- },
- },
- },
- {
- xtype: 'radiofield',
- boxLabel: gettext('DHCP'),
- name: 'ipv6mode',
- inputValue: 'dhcp',
- checked: false,
- margin: '0 0 0 10',
- },
- {
- xtype: 'radiofield',
- boxLabel: gettext('SLAAC'),
- name: 'ipv6mode',
- inputValue: 'auto',
- checked: false,
- margin: '0 0 0 10',
- },
- ],
- },
- {
- xtype: 'textfield',
- name: 'ip6',
- value: '',
- vtype: 'IP6CIDRAddress',
- disabled: true,
- fieldLabel: gettext('IPv6/CIDR'),
- },
- {
- xtype: 'textfield',
- name: 'gw6',
- vtype: 'IP6Address',
- value: '',
- disabled: true,
- fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') + ')',
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.IPConfigEdit', {
- extend: 'Proxmox.window.Edit',
-
- isAdd: true,
-
- initComponent: function () {
- var me = this;
-
- // convert confid from netX to ipconfigX
- var match = me.confid.match(/^net(\d+)$/);
- if (match) {
- me.netid = me.confid;
- me.confid = 'ipconfig' + match[1];
- }
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- me.isCreate = !me.confid;
-
- var ipanel = Ext.create('PVE.qemu.IPConfigPanel', {
- confid: me.confid,
- netid: me.netid,
- nodename: nodename,
- });
-
- Ext.applyIf(me, {
- subject: gettext('Network Config'),
- items: ipanel,
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- me.vmconfig = response.result.data;
- var ipconfig = {};
- var value = me.vmconfig[me.confid];
- if (value) {
- ipconfig = PVE.Parser.parseIPConfig(me.confid, value);
- if (!ipconfig) {
- Ext.Msg.alert(
- gettext('Error'),
- gettext('Unable to parse network configuration'),
- );
- me.close();
- return;
- }
- }
- ipanel.setIPConfig(me.confid, ipconfig);
- ipanel.setVMConfig(me.vmconfig);
- },
- });
- },
-});
-Ext.define('PVE.qemu.KeyboardEdit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- Ext.applyIf(me, {
- subject: gettext('Keyboard Layout'),
- items: {
- xtype: 'VNCKeyboardSelector',
- name: 'keyboard',
- value: '__default__',
- fieldLabel: gettext('Keyboard Layout'),
- },
- });
-
- me.callParent();
-
- me.load();
- },
-});
-Ext.define('PVE.qemu.MachineInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveMachineInputPanel',
- onlineHelp: 'qm_machine_type',
-
- viewModel: {
- data: {
- type: '__default__',
- },
- formulas: {
- q35: (get) => get('type') === 'q35',
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
- control: {
- 'combobox[name=machine]': {
- change: 'onMachineChange',
- },
- },
- onMachineChange: function (field, value) {
- let me = this;
- let version = me.lookup('version');
- let store = version.getStore();
- let oldRec = store.findRecord('id', version.getValue(), 0, false, false, true);
- let type = value === 'q35' ? 'q35' : 'i440fx';
- store.clearFilter();
- store.addFilter((val) => val.data.id === 'latest' || val.data.type === type);
- if (!me.getView().isWindows) {
- version.setValue('latest');
- } else {
- store.isWindows = true;
- if (!oldRec) {
- return;
- }
- let oldVers = oldRec.data.version;
- // we already filtered by correct type, so just check version property
- let rec = store.findRecord('version', oldVers, 0, false, false, true);
- if (rec) {
- version.select(rec);
- }
- }
- },
- },
-
- onGetValues: function (values) {
- if (values.delete === 'machine' && values.viommu) {
- delete values.delete;
- values.machine = 'pc';
- }
- if (values.version && values.version !== 'latest') {
- values.machine = values.version;
- delete values.delete;
- }
- delete values.version;
- if (values.delete === 'machine' && !values.viommu) {
- return values;
- }
- let ret = {};
- ret.machine = PVE.Parser.printPropertyString(values, 'machine');
- return ret;
- },
-
- setValues: function (values) {
- let me = this;
-
- let machineConf = PVE.Parser.parsePropertyString(values.machine, 'type');
- values.machine = machineConf.type;
-
- me.isWindows = values.isWindows;
- if (values.machine === 'pc') {
- values.machine = '__default__';
- }
-
- if (me.isWindows) {
- if (values.machine === '__default__') {
- values.version = 'pc-i440fx-5.1';
- } else if (values.machine === 'q35') {
- values.version = 'pc-q35-5.1';
- }
- }
-
- values.viommu = machineConf.viommu || '__default__';
-
- if (values.machine !== '__default__' && values.machine !== 'q35') {
- values.version = values.machine;
- values.machine = values.version.match(/q35/) ? 'q35' : '__default__';
-
- // avoid hiding a pinned version
- me.setAdvancedVisible(true);
- }
-
- this.callParent(arguments);
- },
-
- items: {
- xtype: 'proxmoxKVComboBox',
- name: 'machine',
- reference: 'machine',
- fieldLabel: gettext('Machine'),
- comboItems: [
- ['__default__', PVE.Utils.render_qemu_machine('')],
- ['q35', 'q35'],
- ],
- bind: {
- value: '{type}',
- },
- },
-
- advancedItems: [
- {
- xtype: 'combobox',
- name: 'version',
- reference: 'version',
- fieldLabel: gettext('Version'),
- emptyText: gettext('Latest'),
- value: 'latest',
- editable: false,
- valueField: 'id',
- displayField: 'version',
- queryParam: false,
- store: {
- autoLoad: true,
- fields: ['id', 'type', 'version'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/localhost/capabilities/qemu/machines',
- },
- listeners: {
- load: function (records) {
- if (!this.isWindows) {
- this.insert(0, {
- id: 'latest',
- type: 'any',
- version: gettext('Latest'),
- });
- }
- },
- },
- },
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Note'),
- value: gettext(
- 'Machine version change may affect hardware layout and settings in the guest OS.',
- ),
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'viommu',
- fieldLabel: gettext('vIOMMU'),
- reference: 'viommu-q35',
- deleteEmpty: false,
- value: '__default__',
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (None)'],
- ['intel', gettext('Intel (AMD Compatible)')],
- ['virtio', 'VirtIO'],
- ],
- bind: {
- hidden: '{!q35}',
- disabled: '{!q35}',
- },
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'viommu',
- fieldLabel: gettext('vIOMMU'),
- reference: 'viommu-i440fx',
- deleteEmpty: false,
- value: '__default__',
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (None)'],
- ['virtio', 'VirtIO'],
- ],
- bind: {
- hidden: '{q35}',
- disabled: '{q35}',
- },
- },
- ],
-});
-
-Ext.define('PVE.qemu.MachineEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: gettext('Machine'),
-
- items: {
- xtype: 'pveMachineInputPanel',
- },
-
- width: 400,
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- me.load({
- success: function (response) {
- let conf = response.result.data;
- let values = {
- machine: conf.machine || '__default__',
- };
- values.isWindows = PVE.Utils.is_windows(conf.ostype);
- me.setValues(values);
- },
- });
- },
-});
-Ext.define('PVE.qemu.MemoryInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveQemuMemoryPanel',
- onlineHelp: 'qm_memory',
-
- insideWizard: false,
-
- viewModel: {}, // inherit data from createWizard if insideWizard
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- control: {
- '#': {
- afterrender: 'setMemory',
- },
- },
-
- setMemory: function () {
- let me = this;
- let view = me.getView(),
- viewModel = me.getViewModel();
- if (view.insideWizard) {
- let memory = view.down('pveMemoryField[name=memory]');
- // NOTE: we only set memory but that then sets balloon in its change handler
- if (viewModel.get('current.ostype') === 'win11') {
- memory.setValue('4096');
- } else {
- memory.setValue('2048');
- }
- }
- },
- },
-
- onGetValues: function (values) {
- let res = {};
-
- let deleteSet = new Set([]);
-
- // properties that can be passed as-is
- let propagate = ['allow-ksm', 'memory'];
-
- propagate.forEach(function (prop) {
- if (values.delete?.split(',').includes(prop)) {
- deleteSet.add(prop);
- }
- if (prop in values) {
- res[prop] = values[prop];
- }
- });
-
- res.balloon = values.balloon;
-
- if (!values.ballooning) {
- res.balloon = 0;
- deleteSet.add('shares');
- } else if (values.memory === values.balloon) {
- delete res.balloon;
- deleteSet.add('balloon');
- deleteSet.add('shares');
- } else if (Ext.isDefined(values.shares) && values.shares !== '') {
- res.shares = values.shares;
- } else {
- deleteSet.add('shares');
- }
-
- if (deleteSet.size > 0) {
- res.delete = deleteSet.keys().toArray().join(',');
- }
-
- return res;
- },
-
- initComponent: function () {
- var me = this;
- var labelWidth = 160;
-
- me.items = [
- {
- xtype: 'pveMemoryField',
- labelWidth: labelWidth,
- fieldLabel: gettext('Memory') + ' (MiB)',
- name: 'memory',
- value: '512', // better defaults get set via the view controllers afterrender
- minValue: 1,
- step: 32,
- hotplug: me.hotplug,
- listeners: {
- change: function (f, value, old) {
- var bf = me.down('field[name=balloon]');
- var balloon = bf.getValue();
- bf.setMaxValue(value);
- if (balloon === old) {
- bf.setValue(value);
- }
- bf.validate();
- },
- },
- },
- ];
-
- me.advancedItems = [
- {
- xtype: 'pveMemoryField',
- name: 'balloon',
- minValue: 1,
- maxValue: me.insideWizard ? 2048 : 512,
- value: '512', // better defaults get set (indirectly) via the view controllers afterrender
- step: 32,
- fieldLabel: gettext('Minimum memory') + ' (MiB)',
- hotplug: me.hotplug,
- labelWidth: labelWidth,
- allowBlank: false,
- listeners: {
- change: function (f, value) {
- var memory = me.down('field[name=memory]').getValue();
- var shares = me.down('field[name=shares]');
- shares.setDisabled(value === memory);
- },
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'shares',
- disabled: true,
- minValue: 0,
- maxValue: 50000,
- value: '',
- step: 10,
- fieldLabel: gettext('Shares'),
- labelWidth: labelWidth,
- allowBlank: true,
- emptyText: Proxmox.Utils.defaultText + ' (1000)',
- submitEmptyText: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- labelWidth: labelWidth,
- value: '1',
- name: 'ballooning',
- fieldLabel: gettext('Ballooning Device'),
- listeners: {
- change: function (f, value) {
- var bf = me.down('field[name=balloon]');
- var shares = me.down('field[name=shares]');
- var memory = me.down('field[name=memory]');
- bf.setDisabled(!value);
- shares.setDisabled(!value || bf.getValue() === memory.getValue());
- },
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'allow-ksm',
- labelWidth: labelWidth,
- fieldLabel: gettext('Allow KSM'),
- checked: true,
- uncheckedValue: '0',
- defaultValue: '1',
- deleteDefaultValue: true,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Allow the Kernel Samepage Merging daemon to merge memory pages of this VM.',
- ),
- },
- },
- ];
-
- if (me.insideWizard) {
- me.column1 = me.items;
- me.items = undefined;
- me.advancedColumn1 = me.advancedItems;
- me.advancedItems = undefined;
- }
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.MemoryEdit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- var memoryhotplug;
- if (me.hotplug) {
- Ext.each(me.hotplug.split(','), function (el) {
- if (el === 'memory') {
- memoryhotplug = 1;
- }
- });
- }
-
- var ipanel = Ext.create('PVE.qemu.MemoryInputPanel', {
- hotplug: memoryhotplug,
- });
-
- Ext.apply(me, {
- subject: gettext('Memory'),
- items: [ipanel],
- // uncomment the following to use the async configiguration API
- // backgroundDelay: 5,
- width: 400,
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- var data = response.result.data;
-
- var values = {
- ballooning: data.balloon === 0 ? '0' : '1',
- shares: data.shares,
- memory: data.memory || '512',
- balloon: data.balloon > 0 ? data.balloon : data.memory || '512',
- 'allow-ksm': data['allow-ksm'] ?? true,
- };
-
- ipanel.setValues(values);
- },
- });
- },
-});
-Ext.define('PVE.qemu.Monitor', {
- extend: 'Ext.panel.Panel',
-
- alias: 'widget.pveQemuMonitor',
-
- // start to trim saved command output once there are *both*, more than `commandLimit` commands
- // executed and the total of saved in+output is over `lineLimit` lines; repeat by dropping one
- // full command output until either condition is false again
- commandLimit: 10,
- lineLimit: 5000,
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- var history = [];
- var histNum = -1;
- let commands = [];
-
- var textbox = Ext.createWidget('panel', {
- region: 'center',
- xtype: 'panel',
- autoScroll: true,
- border: true,
- margins: '5 5 5 5',
- bodyStyle: 'font-family: monospace;',
- });
-
- var scrollToEnd = function () {
- var el = textbox.getTargetEl();
- var dom = Ext.getDom(el);
-
- var clientHeight = dom.clientHeight;
- // BrowserBug: clientHeight reports 0 in IE9 StrictMode
- // Instead we are using offsetHeight and hardcoding borders
- if (Ext.isIE9 && Ext.isStrict) {
- clientHeight = dom.offsetHeight + 2;
- }
- dom.scrollTop = dom.scrollHeight - clientHeight;
- };
-
- var refresh = function () {
- textbox.update(`${commands.flat(2).join('\n')} `);
- scrollToEnd();
- };
-
- let recordInput = (line) => {
- commands.push([line]);
-
- // drop oldest commands and their output until we're not over both limits anymore
- while (commands.length > me.commandLimit && commands.flat(2).length > me.lineLimit) {
- commands.shift();
- }
- };
-
- let addResponse = (lines) => commands[commands.length - 1].push(lines);
-
- var executeCmd = function (cmd) {
- recordInput('# ' + Ext.htmlEncode(cmd), true);
- if (cmd) {
- history.unshift(cmd);
- if (history.length > 20) {
- history.splice(20);
- }
- }
- histNum = -1;
-
- refresh();
- Proxmox.Utils.API2Request({
- params: { command: cmd },
- url: '/nodes/' + nodename + '/qemu/' + vmid + '/monitor',
- method: 'POST',
- waitMsgTarget: me,
- success: function (response, opts) {
- var res = response.result.data;
- addResponse(res.split('\n').map((line) => Ext.htmlEncode(line)));
- refresh();
- },
- failure: function (response, opts) {
- Ext.Msg.alert('Error', response.htmlStatus);
- },
- });
- };
-
- Ext.apply(me, {
- layout: { type: 'border' },
- border: false,
- items: [
- textbox,
- {
- region: 'south',
- margins: '0 5 5 5',
- border: false,
- xtype: 'textfield',
- name: 'cmd',
- value: '',
- fieldStyle: 'font-family: monospace;',
- allowBlank: true,
- listeners: {
- afterrender: function (f) {
- f.focus(false);
- recordInput("Type 'help' for help.");
- refresh();
- },
- specialkey: function (f, e) {
- var key = e.getKey();
- switch (key) {
- case e.ENTER: {
- let cmd = f.getValue();
- f.setValue('');
- executeCmd(cmd);
- break;
- }
- case e.PAGE_UP:
- textbox.scrollBy(0, -0.9 * textbox.getHeight(), false);
- break;
- case e.PAGE_DOWN:
- textbox.scrollBy(0, 0.9 * textbox.getHeight(), false);
- break;
- case e.UP:
- if (histNum + 1 < history.length) {
- f.setValue(history[++histNum]);
- }
- e.preventDefault();
- break;
- case e.DOWN:
- if (histNum > 0) {
- f.setValue(history[--histNum]);
- }
- e.preventDefault();
- break;
- default:
- break;
- }
- },
- },
- },
- ],
- listeners: {
- show: function () {
- var field = me.query('textfield[name="cmd"]')[0];
- field.focus(false, true);
- },
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.qemu.MultiHDPanel', {
- extend: 'PVE.panel.MultiDiskPanel',
- alias: 'widget.pveMultiHDPanel',
-
- onlineHelp: 'qm_hard_disk',
-
- importDisk: true,
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- // maxCount is the sum of all controller ids - 1 (ide2 is fixed in the wizard)
- maxCount:
- Object.values(PVE.Utils.diskControllerMaxIDs).reduce(
- (previous, current) => previous + current,
- 0,
- ) - 1,
-
- getNextFreeDisk: function (vmconfig) {
- let clist = PVE.Utils.sortByPreviousUsage(vmconfig);
- return PVE.Utils.nextFreeDisk(clist, vmconfig);
- },
-
- addPanel: function (itemId, vmconfig, nextFreeDisk, importDisk) {
- let me = this;
- return me.getView().add({
- vmconfig,
- border: false,
- showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'),
- xtype: 'pveQemuHDInputPanel',
- bind: {
- nodename: '{nodename}',
- },
- padding: '0 0 0 5',
- itemId,
- isCreate: true,
- insideWizard: true,
- importDisk,
- });
- },
-
- getBaseVMConfig: function () {
- let me = this;
- let vm = me.getViewModel();
-
- let res = {
- ide2: 'media=cdrom',
- scsihw: vm.get('current.scsihw'),
- ostype: vm.get('current.ostype'),
- };
-
- if (vm.get('current.ide0') === 'some') {
- res.ide0 = 'media=cdrom';
- }
-
- return res;
- },
-
- diskSorter: {
- sorterFn: function (rec1, rec2) {
- let [, name1, id1] = PVE.Utils.bus_match.exec(rec1.data.name);
- let [, name2, id2] = PVE.Utils.bus_match.exec(rec2.data.name);
-
- if (name1 === name2) {
- return parseInt(id1, 10) - parseInt(id2, 10);
- }
-
- return name1 < name2 ? -1 : 1;
- },
- },
-
- deleteDisabled: () => false,
- },
-});
-Ext.define('PVE.qemu.NetworkInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveQemuNetworkInputPanel',
- onlineHelp: 'qm_network_device',
-
- insideWizard: false,
-
- onGetValues: function (values) {
- var me = this;
-
- me.network.model = values.model;
- if (values.nonetwork) {
- return {};
- } else {
- me.network.bridge = values.bridge;
- me.network.tag = values.tag;
- me.network.firewall = values.firewall;
- }
- me.network.macaddr = values.macaddr;
- me.network.disconnect = values.disconnect;
- me.network.queues = values.queues;
- me.network.mtu = values.mtu;
-
- if (values.rate) {
- me.network.rate = values.rate;
- } else {
- delete me.network.rate;
- }
-
- var params = {};
-
- params[me.confid] = PVE.Parser.printQemuNetwork(me.network);
-
- return params;
- },
-
- viewModel: {
- data: {
- networkModel: undefined,
- mtu: '',
- },
- formulas: {
- isVirtio: (get) => get('networkModel') === 'virtio',
- showMtuHint: (get) => get('mtu') === 1,
- },
- },
-
- setNetwork: function (confid, data) {
- var me = this;
-
- me.confid = confid;
-
- if (data) {
- data.networkmode = data.bridge ? 'bridge' : 'nat';
- } else {
- data = {};
- data.networkmode = 'bridge';
- }
- me.network = data;
-
- me.setValues(me.network);
- },
-
- setNodename: function (nodename) {
- var me = this;
-
- me.bridgesel.setNodename(nodename);
- },
-
- initComponent: function () {
- var me = this;
-
- me.network = {};
- me.confid = 'net0';
-
- me.column1 = [];
- me.column2 = [];
-
- me.bridgesel = Ext.create('PVE.form.BridgeSelector', {
- name: 'bridge',
- fieldLabel: gettext('Bridge'),
- nodename: me.nodename,
- autoSelect: true,
- allowBlank: false,
- });
-
- me.column1 = [
- me.bridgesel,
- {
- xtype: 'pveVlanField',
- name: 'tag',
- value: '',
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Firewall'),
- name: 'firewall',
- checked: me.insideWizard || me.isCreate,
- },
- ];
-
- me.advancedColumn1 = [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Disconnect'),
- name: 'disconnect',
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'mtu',
- fieldLabel: 'MTU',
- bind: {
- disabled: '{!isVirtio}',
- value: '{mtu}',
- },
- emptyText: gettext('Same as bridge'),
- minValue: 1,
- maxValue: 65520,
- allowBlank: true,
- validator: (val) =>
- val === '' || val >= 576 || val === '1'
- ? true
- : gettext(
- 'MTU needs to be >= 576 or 1 to inherit the MTU from the underlying bridge.',
- ),
- },
- ];
-
- if (me.insideWizard) {
- me.column1.unshift({
- xtype: 'checkbox',
- name: 'nonetwork',
- inputValue: 'none',
- boxLabel: gettext('No network device'),
- listeners: {
- change: function (cb, value) {
- var fields = [
- 'disconnect',
- 'bridge',
- 'tag',
- 'firewall',
- 'model',
- 'macaddr',
- 'rate',
- 'queues',
- 'mtu',
- ];
- fields.forEach(function (fieldname) {
- me.down('field[name=' + fieldname + ']').setDisabled(value);
- });
- me.down('field[name=bridge]').validate();
- },
- },
- });
- me.column2.unshift({
- xtype: 'displayfield',
- });
- }
-
- me.column2.push(
- {
- xtype: 'pveNetworkCardSelector',
- name: 'model',
- fieldLabel: gettext('Model'),
- bind: '{networkModel}',
- value: PVE.qemu.OSDefaults.generic.networkCard,
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'macaddr',
- fieldLabel: gettext('MAC address'),
- vtype: 'MacAddress',
- allowBlank: true,
- emptyText: 'auto',
- },
- );
- me.advancedColumn2 = [
- {
- xtype: 'numberfield',
- name: 'rate',
- fieldLabel: gettext('Rate limit') + ' (MB/s)',
- minValue: 0,
- maxValue: 10 * 1024,
- value: '',
- emptyText: 'unlimited',
- allowBlank: true,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'queues',
- fieldLabel: 'Multiqueue',
- minValue: 1,
- maxValue: 64,
- value: '',
- allowBlank: true,
- },
- ];
- me.advancedColumnB = [
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- "Use the special value '1' to inherit the MTU value from the underlying bridge",
- ),
- bind: {
- hidden: '{!showMtuHint}',
- },
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.NetworkEdit', {
- extend: 'Proxmox.window.Edit',
-
- isAdd: true,
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- me.isCreate = !me.confid;
-
- var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', {
- confid: me.confid,
- nodename: nodename,
- isCreate: me.isCreate,
- });
-
- Ext.applyIf(me, {
- subject: gettext('Network Device'),
- items: ipanel,
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- var i, confid;
- me.vmconfig = response.result.data;
- if (!me.isCreate) {
- let value = me.vmconfig[me.confid];
- let network = PVE.Parser.parseQemuNetwork(me.confid, value);
- if (!network) {
- Ext.Msg.alert(gettext('Error'), 'Unable to parse network options');
- me.close();
- return;
- }
- ipanel.setNetwork(me.confid, network);
- } else {
- for (i = 0; i < 100; i++) {
- confid = 'net' + i.toString();
- if (!Ext.isDefined(me.vmconfig[confid])) {
- me.confid = confid;
- break;
- }
- }
-
- let ostype = me.vmconfig.ostype;
- let defaults = PVE.qemu.OSDefaults.getDefaults(ostype);
- let data = {
- model: defaults.networkCard,
- };
-
- ipanel.setNetwork(me.confid, data);
- }
- },
- });
- },
-});
-/*
- * This class holds performance *recommended* settings for the PVE Qemu wizards
- * the *mandatory* settings are set in the PVE::QemuServer
- * config_to_command sub
- * We store this here until we get the data from the API server
- */
-
-// this is how you would add an hypothetic FreeBSD > 10 entry
-//
-//virtio-blk is stable but virtIO net still
-// problematic as of 10.3
-// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059
-// addOS({
-// parent: 'generic', // inherits defaults
-// pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js
-// busType: 'virtio' // must match a pveBusController value
-// // networkCard muss match a pveNetworkCardSelector
-
-Ext.define('PVE.qemu.OSDefaults', {
- singleton: true, // will also force creation when loaded
-
- constructor: function () {
- let me = this;
-
- let addOS = function (settings) {
- if (Object.hasOwn(settings, 'parent')) {
- let child = Ext.clone(me[settings.parent]);
- me[settings.pveOS] = Ext.apply(child, settings);
- } else {
- throw 'Could not find your genitor';
- }
- };
-
- // default values
- me.generic = {
- busType: 'ide',
- networkCard: 'e1000',
- busPriority: {
- ide: 4,
- sata: 3,
- scsi: 2,
- virtio: 1,
- },
- scsihw: 'virtio-scsi-single',
- cputype: 'x86-64-v2-AES',
- };
-
- // virtio-net is in kernel since 2.6.25
- // virtio-scsi since 3.2 but backported in RHEL with 2.6 kernel
- addOS({
- pveOS: 'l26',
- parent: 'generic',
- busType: 'scsi',
- busPriority: {
- scsi: 4,
- virtio: 3,
- sata: 2,
- ide: 1,
- },
- networkCard: 'virtio',
- });
-
- // recommendation from http://wiki.qemu.org/Windows2000
- addOS({
- pveOS: 'w2k',
- parent: 'generic',
- networkCard: 'rtl8139',
- scsihw: '',
- });
- // https://pve.proxmox.com/wiki/Windows_XP_Guest_Notes
- addOS({
- pveOS: 'wxp',
- parent: 'w2k',
- });
-
- me.getDefaults = function (ostype) {
- if (PVE.qemu.OSDefaults[ostype]) {
- return PVE.qemu.OSDefaults[ostype];
- } else {
- return PVE.qemu.OSDefaults.generic;
- }
- };
- },
-});
-Ext.define('PVE.qemu.OSTypeInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveQemuOSTypePanel',
- onlineHelp: 'qm_os_settings',
- insideWizard: false,
-
- controller: {
- xclass: 'Ext.app.ViewController',
- control: {
- 'combobox[name=osbase]': {
- change: 'onOSBaseChange',
- },
- 'combobox[name=ostype]': {
- afterrender: 'onOSTypeChange',
- change: 'onOSTypeChange',
- },
- 'checkbox[reference=enableSecondCD]': {
- change: 'onSecondCDChange',
- },
- },
- onOSBaseChange: function (field, value) {
- let me = this;
- me.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]);
- if (me.getView().insideWizard) {
- let isWindows = value === 'Microsoft Windows';
- let enableSecondCD = me.lookup('enableSecondCD');
- enableSecondCD.setVisible(isWindows);
- if (!isWindows) {
- enableSecondCD.setValue(false);
- }
- }
- },
- onOSTypeChange: function (field) {
- var me = this,
- ostype = field.getValue();
- if (!me.getView().insideWizard) {
- return;
- }
- var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype);
-
- me.setWidget('pveBusSelector', targetValues.busType);
- me.setWidget('pveNetworkCardSelector', targetValues.networkCard);
- me.setWidget('CPUModelSelector', targetValues.cputype);
- var scsihw = targetValues.scsihw || '__default__';
- this.getViewModel().set('current.scsihw', scsihw);
- this.getViewModel().set('current.ostype', ostype);
- },
- setWidget: function (widget, newValue) {
- // changing a widget is safe only if ComponentQuery.query returns us
- // a single value array
- var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget);
- if (widgets.length === 1) {
- widgets[0].setValue(newValue);
- } else {
- // ignore multiple disks, we only want to set the type if there is a single disk
- }
- },
- onSecondCDChange: function (widget, value, lastValue) {
- let me = this;
- let vm = me.getViewModel();
- let updateVMConfig = function () {
- let widgets = Ext.ComponentQuery.query('pveMultiHDPanel');
- if (widgets.length === 1) {
- widgets[0].getController().updateVMConfig();
- }
- };
- if (value) {
- // only for windows
- vm.set('current.ide0', 'some');
- vm.notify();
- updateVMConfig();
- me.setWidget('pveBusSelector', 'scsi');
- me.setWidget('pveNetworkCardSelector', 'virtio');
- } else {
- vm.set('current.ide0', '');
- vm.notify();
- updateVMConfig();
- me.setWidget('pveBusSelector', 'scsi');
- let ostype = me.lookup('ostype').getValue();
- let targetValues = PVE.qemu.OSDefaults.getDefaults(ostype);
- me.setWidget('pveBusSelector', targetValues.busType);
- }
- },
- },
-
- setNodename: function (nodename) {
- var me = this;
- me.lookup('isoSelector').setNodename(nodename);
- },
-
- onGetValues: function (values) {
- if (values.ide0) {
- let drive = {
- media: 'cdrom',
- file: values.ide0,
- };
- values.ide0 = PVE.Parser.printQemuDrive(drive);
- }
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: 'displayfield',
- value: gettext('Guest OS') + ':',
- hidden: !me.insideWizard,
- },
- {
- xtype: 'combobox',
- submitValue: false,
- name: 'osbase',
- fieldLabel: gettext('Type'),
- editable: false,
- queryMode: 'local',
- value: 'Linux',
- store: Object.keys(PVE.Utils.kvm_ostypes),
- },
- {
- xtype: 'combobox',
- name: 'ostype',
- reference: 'ostype',
- fieldLabel: gettext('Version'),
- value: 'l26',
- allowBlank: false,
- editable: false,
- queryMode: 'local',
- valueField: 'val',
- displayField: 'desc',
- store: {
- fields: ['desc', 'val'],
- data: PVE.Utils.kvm_ostypes.Linux,
- listeners: {
- datachanged: function (store) {
- var ostype = me.lookup('ostype');
- var old_val = ostype.getValue();
- if (!me.insideWizard && old_val && store.find('val', old_val) !== -1) {
- ostype.setValue(old_val);
- } else {
- ostype.setValue(store.getAt(0));
- }
- },
- },
- },
- },
- ];
-
- if (me.insideWizard) {
- me.items.push(
- {
- xtype: 'proxmoxcheckbox',
- reference: 'enableSecondCD',
- isFormField: false,
- hidden: true,
- checked: false,
- boxLabel: gettext('Add additional drive for VirtIO drivers'),
- listeners: {
- change: function (cb, value) {
- me.lookup('isoSelector').setDisabled(!value);
- me.lookup('isoSelector').setHidden(!value);
- },
- },
- },
- {
- xtype: 'pveIsoSelector',
- reference: 'isoSelector',
- name: 'ide0',
- nodename: me.nodename,
- insideWizard: true,
- hidden: true,
- disabled: true,
- },
- );
- }
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.OSTypeEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: 'OS Type',
-
- items: [{ xtype: 'pveQemuOSTypePanel' }],
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- var value = response.result.data.ostype || 'other';
- var osinfo = PVE.Utils.get_kvm_osinfo(value);
- me.setValues({ ostype: value, osbase: osinfo.base });
- },
- });
- },
-});
-Ext.define('PVE.qemu.Options', {
- extend: 'Proxmox.grid.PendingObjectGrid',
- alias: ['widget.PVE.qemu.Options'],
-
- onlineHelp: 'qm_options',
-
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var vmid = me.pveSelNode.data.vmid;
- if (!vmid) {
- throw 'no VM ID specified';
- }
-
- var caps = Ext.state.Manager.get('GuiCap');
-
- var rows = {
- name: {
- required: true,
- defaultValue: me.pveSelNode.data.name,
- header: gettext('Name'),
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Name'),
- items: {
- xtype: 'inputpanel',
- items: {
- xtype: 'textfield',
- name: 'name',
- vtype: 'DnsName',
- value: '',
- fieldLabel: gettext('Name'),
- allowBlank: true,
- },
- onGetValues: function (values) {
- var params = values;
- if (
- values.name === undefined ||
- values.name === null ||
- values.name === ''
- ) {
- params = { delete: 'name' };
- }
- return params;
- },
- },
- }
- : undefined,
- },
- onboot: {
- header: gettext('Start at boot'),
- defaultValue: '',
- renderer: Proxmox.Utils.format_boolean,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Start at boot'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'onboot',
- uncheckedValue: 0,
- defaultValue: 0,
- deleteDefaultValue: true,
- fieldLabel: gettext('Start at boot'),
- },
- }
- : undefined,
- },
- startup: {
- header: gettext('Start/Shutdown order'),
- defaultValue: '',
- renderer: PVE.Utils.render_kvm_startup,
- editor:
- caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify']
- ? {
- xtype: 'pveWindowStartupEdit',
- onlineHelp: 'qm_startup_and_shutdown',
- }
- : undefined,
- },
- ostype: {
- header: gettext('OS Type'),
- editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined,
- renderer: PVE.Utils.render_kvm_ostype,
- defaultValue: 'other',
- },
- bootdisk: {
- visible: false,
- },
- boot: {
- header: gettext('Boot Order'),
- defaultValue: 'cdn',
- editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined,
- multiKey: ['boot', 'bootdisk'],
- renderer: function (order, metaData, record, rowIndex, colIndex, store, pending) {
- if (/^\s*$/.test(order)) {
- return gettext('(No boot device selected)');
- }
- let boot = PVE.Parser.parsePropertyString(order, 'legacy');
- if (boot.order) {
- let list = boot.order.split(';');
- let ret = '';
- list.forEach((dev) => {
- if (ret) {
- ret += ', ';
- }
- ret += dev;
- });
- return ret;
- }
-
- // legacy style and fallback
- let i;
- var text = '';
- var bootdisk = me.getObjectValue('bootdisk', undefined, pending);
- order = boot.legacy || 'cdn';
- for (i = 0; i < order.length; i++) {
- if (text) {
- text += ', ';
- }
- let sel = order.substring(i, i + 1);
- if (sel === 'c') {
- if (bootdisk) {
- text += bootdisk;
- } else {
- text += gettext('first disk');
- }
- } else if (sel === 'n') {
- text += gettext('any net');
- } else if (sel === 'a') {
- text += gettext('Floppy');
- } else if (sel === 'd') {
- text += gettext('any CD-ROM');
- } else {
- text += sel;
- }
- }
- return text;
- },
- },
- tablet: {
- header: gettext('Use tablet for pointer'),
- defaultValue: true,
- renderer: Proxmox.Utils.format_boolean,
- editor: caps.vms['VM.Config.HWType']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Use tablet for pointer'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'tablet',
- checked: true,
- uncheckedValue: 0,
- defaultValue: 1,
- deleteDefaultValue: true,
- fieldLabel: gettext('Enabled'),
- },
- }
- : undefined,
- },
- hotplug: {
- header: gettext('Hotplug'),
- defaultValue: 'disk,network,usb',
- renderer: PVE.Utils.render_hotplug_features,
- editor: caps.vms['VM.Config.HWType']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Hotplug'),
- items: {
- xtype: 'pveHotplugFeatureSelector',
- name: 'hotplug',
- value: '',
- multiSelect: true,
- fieldLabel: gettext('Hotplug'),
- allowBlank: true,
- },
- }
- : undefined,
- },
- acpi: {
- header: gettext('ACPI support'),
- defaultValue: true,
- renderer: Proxmox.Utils.format_boolean,
- editor: caps.vms['VM.Config.HWType']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('ACPI support'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'acpi',
- checked: true,
- uncheckedValue: 0,
- defaultValue: 1,
- deleteDefaultValue: true,
- fieldLabel: gettext('Enabled'),
- },
- }
- : undefined,
- },
- kvm: {
- header: gettext('KVM hardware virtualization'),
- defaultValue: true,
- renderer: Proxmox.Utils.format_boolean,
- editor: caps.vms['VM.Config.HWType']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('KVM hardware virtualization'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'kvm',
- checked: true,
- uncheckedValue: 0,
- defaultValue: 1,
- deleteDefaultValue: true,
- fieldLabel: gettext('Enabled'),
- },
- }
- : undefined,
- },
- freeze: {
- header: gettext('Freeze CPU at startup'),
- defaultValue: false,
- renderer: Proxmox.Utils.format_boolean,
- editor: caps.vms['VM.PowerMgmt']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Freeze CPU at startup'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'freeze',
- uncheckedValue: 0,
- defaultValue: 0,
- deleteDefaultValue: true,
- labelWidth: 140,
- fieldLabel: gettext('Freeze CPU at startup'),
- },
- }
- : undefined,
- },
- localtime: {
- header: gettext('Use local time for RTC'),
- defaultValue: '__default__',
- renderer: PVE.Utils.render_localtime,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Use local time for RTC'),
- width: 400,
- items: {
- xtype: 'proxmoxKVComboBox',
- name: 'localtime',
- value: '__default__',
- comboItems: [
- ['__default__', PVE.Utils.render_localtime('__default__')],
- [1, PVE.Utils.render_localtime(1)],
- [0, PVE.Utils.render_localtime(0)],
- ],
- labelWidth: 140,
- fieldLabel: gettext('Use local time for RTC'),
- },
- }
- : undefined,
- },
- startdate: {
- header: gettext('RTC start date'),
- defaultValue: 'now',
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('RTC start date'),
- items: {
- xtype: 'proxmoxtextfield',
- name: 'startdate',
- deleteEmpty: true,
- value: 'now',
- fieldLabel: gettext('RTC start date'),
- vtype: 'QemuStartDate',
- allowBlank: true,
- },
- }
- : undefined,
- },
- smbios1: {
- header: gettext('SMBIOS settings (type1)'),
- defaultValue: '',
- renderer: Ext.String.htmlEncode,
- editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined,
- },
- agent: {
- header: 'QEMU Guest Agent',
- defaultValue: false,
- renderer: PVE.Utils.render_qga_features,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Qemu Agent'),
- width: 350,
- onlineHelp: 'qm_qemu_agent',
- items: {
- xtype: 'pveAgentFeatureSelector',
- name: 'agent',
- },
- }
- : undefined,
- },
- protection: {
- header: gettext('Protection'),
- defaultValue: false,
- renderer: Proxmox.Utils.format_boolean,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Protection'),
- items: {
- xtype: 'proxmoxcheckbox',
- name: 'protection',
- uncheckedValue: 0,
- defaultValue: 0,
- deleteDefaultValue: true,
- fieldLabel: gettext('Enabled'),
- },
- }
- : undefined,
- },
- spice_enhancements: {
- header: gettext('Spice Enhancements'),
- defaultValue: false,
- renderer: PVE.Utils.render_spice_enhancements,
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('Spice Enhancements'),
- onlineHelp: 'qm_spice_enhancements',
- items: {
- xtype: 'pveSpiceEnhancementSelector',
- name: 'spice_enhancements',
- },
- }
- : undefined,
- },
- vmstatestorage: {
- header: gettext('VM State storage'),
- defaultValue: '',
- renderer: (val) => val || gettext('Automatic'),
- editor: caps.vms['VM.Config.Options']
- ? {
- xtype: 'proxmoxWindowEdit',
- subject: gettext('VM State storage'),
- onlineHelp: 'qm_vmstatestorage',
- width: 350,
- items: {
- xtype: 'pveStorageSelector',
- storageContent: 'images',
- allowBlank: true,
- emptyText: gettext("Automatic (Storage used by the VM, or 'local')"),
- autoSelect: false,
- deleteEmpty: true,
- skipEmptyText: true,
- nodename: nodename,
- name: 'vmstatestorage',
- },
- }
- : undefined,
- },
- 'amd-sev': {
- header: gettext('AMD SEV'),
- editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.SevEdit' : undefined,
- defaultValue: Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')',
- renderer: function (value, metaData, record, ri, ci, store, pending) {
- let amd_sev = PVE.Parser.parsePropertyString(value, 'type');
- if (amd_sev.type === 'std') {
- return 'AMD SEV (' + value + ')';
- }
- if (amd_sev.type === 'es') {
- return 'AMD SEV-ES (' + value + ')';
- }
- if (amd_sev.type === 'snp') {
- return 'AMD SEV-SNP (' + value + ')';
- }
- return value;
- },
- },
- 'intel-tdx': {
- header: gettext('Intel TDX'),
- editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.TdxEdit' : undefined,
- defaultValue: Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')',
- renderer: function (value, metaData, record, ri, ci, store, pending) {
- let intel_tdx = PVE.Parser.parsePropertyString(value, 'type');
- if (intel_tdx.type === 'tdx') {
- return 'Intel (' + value + ')';
- }
- return value;
- },
- },
- hookscript: {
- header: gettext('Hookscript'),
- },
- };
-
- var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
-
- var edit_btn = new Ext.Button({
- text: gettext('Edit'),
- disabled: true,
- handler: function () {
- me.run_editor();
- },
- });
-
- var revert_btn = new PVE.button.PendingRevert();
-
- var set_button_status = function () {
- var sm = me.getSelectionModel();
- var rec = sm.getSelection()[0];
-
- if (!rec) {
- edit_btn.disable();
- return;
- }
-
- var key = rec.data.key;
- var pending = rec.data.delete || me.hasPendingChanges(key);
- var rowdef = rows[key];
-
- edit_btn.setDisabled(!rowdef.editor);
- revert_btn.setDisabled(!pending);
- };
-
- Ext.apply(me, {
- url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/pending',
- interval: 5000,
- cwidth1: 250,
- tbar: [edit_btn, revert_btn],
- rows: rows,
- editorConfig: {
- url: '/api2/extjs/' + baseurl,
- },
- listeners: {
- itemdblclick: me.run_editor,
- selectionchange: set_button_status,
- },
- });
-
- me.callParent();
-
- me.on('activate', () => me.rstore.startUpdate());
- me.on('destroy', () => me.rstore.stopUpdate());
- me.on('deactivate', () => me.rstore.stopUpdate());
-
- me.mon(me.getStore(), 'datachanged', function () {
- set_button_status();
- });
- },
-});
-Ext.define('PVE.qemu.PCIInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
-
- onlineHelp: 'qm_pci_passthrough_vm_config',
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- setVMConfig: function (vmconfig) {
- let me = this;
- let view = me.getView();
- let vm = me.getViewModel();
- me.vmconfig = vmconfig;
-
- let hostpci = me.vmconfig[view.confid] || '';
-
- let values = PVE.Parser.parsePropertyString(hostpci, 'host');
- if (values.host) {
- if (!values.host.match(/^[0-9a-f]{4}:/i)) {
- // add optional domain
- values.host = '0000:' + values.host;
- }
- if (values.host.length < 11) {
- // 0000:00:00 format not 0000:00:00.0
- values.host += '.0';
- values.multifunction = true;
- }
- values.type = 'raw';
- } else if (values.mapping) {
- values.type = 'mapped';
- }
- vm.set('isMapped', values.type !== 'raw');
-
- values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0);
- values.pcie = PVE.Parser.parseBoolean(values.pcie, 0);
- values.rombar = PVE.Parser.parseBoolean(values.rombar, 1);
-
- view.setValues(values);
- if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) {
- // machine is not set to some variant of q35, so we disable pcie
- let pcie = me.lookup('pcie');
- pcie.setDisabled(true);
- pcie.setBoxLabel(gettext('Q35 only'));
- }
-
- if (values.romfile) {
- me.lookup('romfile').setVisible(true);
- }
- },
-
- selectorEnable: function (selector) {
- let me = this;
- me.pciDevChange(selector, selector.getValue());
- },
-
- pciDevChange: function (pcisel, value) {
- let me = this;
- let mdevfield = me.lookup('mdev');
- if (!value) {
- if (!pcisel.isDisabled()) {
- mdevfield.setDisabled(true);
- }
- return;
- }
- let pciDev = pcisel.getStore().getById(value);
-
- mdevfield.setDisabled(!pciDev || !pciDev.data.mdev);
- if (!pciDev) {
- return;
- }
-
- let path = value;
- if (pciDev.data.map) {
- path = pciDev.data.id;
- }
-
- if (pciDev.data.mdev) {
- mdevfield.setPciIdOrMapping(path);
- }
- if (pcisel.reference === 'selector') {
- let iommu = pciDev.data.iommugroup;
- if (iommu === -1) {
- return;
- }
- // try to find out if there are more devices in that iommu group
- let id = path.substring(0, 5); // 00:00
- let count = 0;
- pcisel.getStore().each(({ data }) => {
- if (data.iommugroup === iommu && data.id.substring(0, 5) !== id) {
- count++;
- return false;
- }
- return true;
- });
- me.lookup('group_warning').setVisible(count > 0);
- }
- },
-
- onGetValues: function (values) {
- let me = this;
- let view = me.getView();
- if (!view.confid) {
- for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
- if (!me.vmconfig['hostpci' + i.toString()]) {
- view.confid = 'hostpci' + i.toString();
- break;
- }
- }
- // FIXME: what if no confid was found??
- }
-
- values.host?.replace(/^0000:/, ''); // remove optional '0000' domain
-
- if (values.multifunction && values.host) {
- values.host = values.host.substring(0, values.host.indexOf('.')); // skip the '.X'
- delete values.multifunction;
- }
-
- if (values.rombar) {
- delete values.rombar;
- } else {
- values.rombar = 0;
- }
-
- if (!values.romfile) {
- delete values.romfile;
- }
-
- delete values.type;
-
- let ret = {};
- ret[view.confid] = PVE.Parser.printPropertyString(values, 'host');
- return ret;
- },
- },
-
- viewModel: {
- data: {
- isMapped: true,
- },
- },
-
- setVMConfig: function (vmconfig) {
- return this.getController().setVMConfig(vmconfig);
- },
-
- onGetValues: function (values) {
- return this.getController().onGetValues(values);
- },
-
- initComponent: function () {
- let me = this;
-
- me.nodename = me.pveSelNode.data.node;
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- me.columnT = [
- {
- xtype: 'displayfield',
- reference: 'iommu_warning',
- hidden: true,
- columnWidth: 1,
- padding: '0 0 10 0',
- value:
- 'No IOMMU detected, please activate it.' +
- 'See Documentation for further information.',
- userCls: 'pmx-hint',
- },
- {
- xtype: 'displayfield',
- reference: 'group_warning',
- hidden: true,
- columnWidth: 1,
- padding: '0 0 10 0',
- itemId: 'iommuwarning',
- value: 'The selected Device is not in a separate IOMMU group, make sure this is intended.',
- userCls: 'pmx-hint',
- },
- ];
-
- me.column1 = [
- {
- xtype: 'radiofield',
- name: 'type',
- inputValue: 'mapped',
- checked: true,
- boxLabel: gettext('Mapped Device'),
- bind: {
- value: '{isMapped}',
- },
- },
- {
- xtype: 'pvePCIMapSelector',
- fieldLabel: gettext('Device'),
- reference: 'mapped_selector',
- name: 'mapping',
- labelAlign: 'right',
- nodename: me.nodename,
- allowBlank: false,
- bind: {
- disabled: '{!isMapped}',
- },
- listeners: {
- change: 'pciDevChange',
- enable: 'selectorEnable',
- },
- },
- {
- xtype: 'radiofield',
- name: 'type',
- inputValue: 'raw',
- boxLabel: gettext('Raw Device'),
- },
- {
- xtype: 'pvePCISelector',
- fieldLabel: gettext('Device'),
- name: 'host',
- reference: 'selector',
- nodename: me.nodename,
- labelAlign: 'right',
- allowBlank: false,
- disabled: true,
- bind: {
- disabled: '{isMapped}',
- },
- onLoadCallBack: function (store, records, success) {
- if (!success || !records.length) {
- return;
- }
- me.lookup('iommu_warning').setVisible(
- records.every((val) => val.data.iommugroup === -1),
- );
- },
- listeners: {
- change: 'pciDevChange',
- enable: 'selectorEnable',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('All Functions'),
- reference: 'all_functions',
- disabled: true,
- labelAlign: 'right',
- name: 'multifunction',
- bind: {
- disabled: '{isMapped}',
- },
- },
- ];
-
- me.column2 = [
- {
- xtype: 'pveMDevSelector',
- name: 'mdev',
- reference: 'mdev',
- disabled: true,
- fieldLabel: gettext('MDev Type'),
- nodename: me.nodename,
- listeners: {
- change: function (field, value) {
- let multiFunction = me.down('field[name=multifunction]');
- if (value) {
- multiFunction.setValue(false);
- }
- multiFunction.setDisabled(!!value);
- },
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Primary GPU'),
- name: 'x-vga',
- },
- ];
-
- me.advancedColumn1 = [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: 'ROM-Bar',
- name: 'rombar',
- },
- {
- xtype: 'displayfield',
- submitValue: true,
- hidden: true,
- fieldLabel: 'ROM-File',
- reference: 'romfile',
- name: 'romfile',
- },
- {
- xtype: 'textfield',
- name: 'vendor-id',
- fieldLabel: gettext('Vendor ID'),
- emptyText: gettext('From Device'),
- vtype: 'PciId',
- allowBlank: true,
- submitEmpty: false,
- },
- {
- xtype: 'textfield',
- name: 'device-id',
- fieldLabel: gettext('Device ID'),
- emptyText: gettext('From Device'),
- vtype: 'PciId',
- allowBlank: true,
- submitEmpty: false,
- },
- ];
-
- me.advancedColumn2 = [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: 'PCI-Express',
- reference: 'pcie',
- name: 'pcie',
- },
- {
- xtype: 'textfield',
- name: 'sub-vendor-id',
- fieldLabel: gettext('Sub-Vendor ID'),
- emptyText: gettext('From Device'),
- vtype: 'PciId',
- allowBlank: true,
- submitEmpty: false,
- },
- {
- xtype: 'textfield',
- name: 'sub-device-id',
- fieldLabel: gettext('Sub-Device ID'),
- emptyText: gettext('From Device'),
- vtype: 'PciId',
- allowBlank: true,
- submitEmpty: false,
- },
- ];
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.PCIEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: gettext('PCI Device'),
-
- vmconfig: undefined,
- isAdd: true,
-
- initComponent: function () {
- let me = this;
-
- me.isCreate = !me.confid;
-
- let ipanel = Ext.create('PVE.qemu.PCIInputPanel', {
- confid: me.confid,
- pveSelNode: me.pveSelNode,
- });
-
- Ext.apply(me, {
- items: [ipanel],
- });
-
- me.callParent();
-
- me.load({
- success: ({ result }) => ipanel.setVMConfig(result.data),
- });
- },
-});
-// The view model of the parent should contain a 'cgroupMode' variable (or params for v2 are used).
-Ext.define('PVE.qemu.ProcessorInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.pveQemuProcessorPanel',
- onlineHelp: 'qm_cpu',
-
- insideWizard: false,
-
- viewModel: {
- data: {
- socketCount: 1,
- coreCount: 1,
- showCustomModelPermWarning: false,
- userIsRoot: false,
- },
- formulas: {
- totalCoreCount: (get) => get('socketCount') * get('coreCount'),
- cpuunitsDefault: (get) => (get('cgroupMode') === 1 ? 1024 : 100),
- cpuunitsMin: (get) => (get('cgroupMode') === 1 ? 2 : 1),
- cpuunitsMax: (get) => (get('cgroupMode') === 1 ? 262144 : 10000),
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
- init: function () {
- let me = this;
- let viewModel = me.getViewModel();
-
- viewModel.set('userIsRoot', Proxmox.UserName === 'root@pam');
- },
- },
-
- onGetValues: function (values) {
- let me = this;
- let cpuunitsDefault = me.getViewModel().get('cpuunitsDefault');
-
- if (Array.isArray(values.delete)) {
- values.delete = values.delete.join(',');
- }
-
- PVE.Utils.delete_if_default(values, 'cpulimit', '0', me.insideWizard);
- PVE.Utils.delete_if_default(values, 'cpuunits', `${cpuunitsDefault}`, me.insideWizard);
-
- // build the cpu options:
- me.cpu.cputype = values.cputype;
-
- if (values.flags) {
- me.cpu.flags = values.flags;
- } else {
- delete me.cpu.flags;
- }
-
- delete values.cputype;
- delete values.flags;
- var cpustring = PVE.Parser.printQemuCpu(me.cpu);
-
- // remove cputype delete request:
- var del = values.delete;
- delete values.delete;
- if (del) {
- del = del.split(',');
- Ext.Array.remove(del, 'cputype');
- } else {
- del = [];
- }
-
- if (cpustring) {
- values.cpu = cpustring;
- } else {
- del.push('cpu');
- }
-
- var delarr = del.join(',');
- if (delarr) {
- values.delete = delarr;
- }
-
- return values;
- },
-
- setValues: function (values) {
- let me = this;
-
- let type = values.cputype;
- let typeSelector = me.lookupReference('cputype');
- let typeStore = typeSelector.getStore();
- typeStore.on('load', (store, records, success) => {
- if (!success || !type || records.some((x) => x.data.name === type)) {
- return;
- }
-
- // if we get here, a custom CPU model is selected for the VM but we
- // don't have permission to configure it - it will not be in the
- // list retrieved from the API, so add it manually to allow changing
- // other processor options
- typeStore.add({
- name: type,
- displayname: type.replace(/^custom-/, ''),
- custom: 1,
- vendor: gettext('Unknown'),
- });
- typeSelector.select(type);
- });
-
- me.callParent([values]);
- },
-
- cpu: {},
-
- column1: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'sockets',
- minValue: 1,
- maxValue: 4,
- value: '1',
- fieldLabel: gettext('Sockets'),
- allowBlank: false,
- bind: {
- value: '{socketCount}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'cores',
- minValue: 1,
- maxValue: 256,
- value: '1',
- fieldLabel: gettext('Cores'),
- allowBlank: false,
- bind: {
- value: '{coreCount}',
- },
- },
- ],
-
- column2: [
- {
- xtype: 'CPUModelSelector',
- name: 'cputype',
- reference: 'cputype',
- fieldLabel: gettext('Type'),
- },
- {
- xtype: 'displayfield',
- fieldLabel: gettext('Total cores'),
- name: 'totalcores',
- isFormField: false,
- bind: {
- value: '{totalCoreCount}',
- },
- },
- ],
-
- columnB: [
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- 'WARNING: You do not have permission to configure custom CPU types, if you change the type you will not be able to go back!',
- ),
- hidden: true,
- bind: {
- hidden: '{!showCustomModelPermWarning}',
- },
- },
- ],
-
- advancedColumn1: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'vcpus',
- minValue: 1,
- maxValue: 1,
- value: '',
- fieldLabel: gettext('VCPUs'),
- deleteEmpty: true,
- allowBlank: true,
- emptyText: '1',
- bind: {
- emptyText: '{totalCoreCount}',
- maxValue: '{totalCoreCount}',
- },
- },
- {
- xtype: 'numberfield',
- name: 'cpulimit',
- minValue: 0,
- maxValue: 128, // api maximum
- value: '',
- step: 1,
- fieldLabel: gettext('CPU limit'),
- allowBlank: true,
- emptyText: gettext('unlimited'),
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'affinity',
- vtype: 'CpuSet',
- value: '',
- fieldLabel: gettext('CPU Affinity'),
- allowBlank: true,
- emptyText: gettext('All Cores'),
- deleteEmpty: true,
- bind: {
- disabled: '{!userIsRoot}',
- },
- },
- ],
-
- advancedColumn2: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'cpuunits',
- fieldLabel: gettext('CPU units'),
- minValue: '1',
- maxValue: '10000',
- value: '',
- emptyText: '100',
- bind: {
- minValue: '{cpuunitsMin}',
- maxValue: '{cpuunitsMax}',
- emptyText: '{cpuunitsDefault}',
- },
- deleteEmpty: true,
- allowBlank: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Enable NUMA'),
- name: 'numa',
- uncheckedValue: 0,
- },
- ],
- advancedColumnB: [
- {
- xtype: 'label',
- text: 'Extra CPU Flags:',
- },
- {
- xtype: 'vmcpuflagselector',
- name: 'flags',
- },
- ],
-});
-
-Ext.define('PVE.qemu.ProcessorEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveQemuProcessorEdit',
-
- width: 700,
-
- viewModel: {
- data: {
- cgroupMode: 2,
- },
- },
-
- initComponent: function () {
- let me = this;
- me.getViewModel().set('cgroupMode', me.cgroupMode);
-
- var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel');
-
- Ext.apply(me, {
- subject: gettext('Processors'),
- items: ipanel,
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- var data = response.result.data;
- var value = data.cpu;
- if (value) {
- let cpu = PVE.Parser.parseQemuCpu(value);
- ipanel.cpu = cpu;
- data.cputype = cpu.cputype;
- if (cpu.flags) {
- data.flags = cpu.flags;
- }
-
- let caps = Ext.state.Manager.get('GuiCap');
- if (data.cputype.indexOf('custom-') === 0 && !caps.nodes['Sys.Audit']) {
- let vm = ipanel.getViewModel();
- vm.set('showCustomModelPermWarning', true);
- }
- }
- me.setValues(data);
- },
- });
- },
-});
-Ext.define('PVE.qemu.BiosEdit', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveQemuBiosEdit',
-
- onlineHelp: 'qm_bios_and_uefi',
- subject: 'BIOS',
- autoLoad: true,
-
- viewModel: {
- data: {
- bios: '__default__',
- efidisk0: false,
- },
- formulas: {
- showEFIDiskHint: (get) => get('bios') === 'ovmf' && !get('efidisk0'),
- },
- },
-
- items: [
- {
- xtype: 'pveQemuBiosSelector',
- onlineHelp: 'qm_bios_and_uefi',
- name: 'bios',
- value: '__default__',
- bind: '{bios}',
- fieldLabel: 'BIOS',
- },
- {
- xtype: 'displayfield',
- name: 'efidisk0',
- bind: '{efidisk0}',
- hidden: true,
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- 'You need to add an EFI disk for storing the EFI settings. See the online help for details.',
- ),
- bind: {
- hidden: '{!showEFIDiskHint}',
- },
- },
- ],
-});
-Ext.define('PVE.qemu.RNGInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveRNGInputPanel',
-
- onlineHelp: 'qm_virtio_rng',
-
- onGetValues: function (values) {
- if (values.max_bytes === '') {
- values.max_bytes = '0';
- } else if (values.max_bytes === '1024' && values.period === '') {
- delete values.max_bytes;
- }
-
- var ret = PVE.Parser.printPropertyString(values);
-
- return {
- rng0: ret,
- };
- },
-
- setValues: function (values) {
- if (values.max_bytes === 0) {
- values.max_bytes = null;
- }
-
- this.callParent(arguments);
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
- control: {
- '#max_bytes': {
- change: function (el, newVal) {
- let limitWarning = this.lookupReference('limitWarning');
- limitWarning.setHidden(!!newVal);
- },
- },
- },
- },
-
- items: [
- {
- itemId: 'source',
- name: 'source',
- xtype: 'proxmoxKVComboBox',
- value: '/dev/urandom',
- fieldLabel: gettext('Entropy source'),
- labelWidth: 130,
- comboItems: [
- ['/dev/urandom', '/dev/urandom'],
- ['/dev/random', '/dev/random'],
- ['/dev/hwrng', '/dev/hwrng'],
- ],
- },
- {
- xtype: 'numberfield',
- itemId: 'max_bytes',
- name: 'max_bytes',
- minValue: 0,
- step: 1,
- value: 1024,
- fieldLabel: gettext('Limit (Bytes/Period)'),
- labelWidth: 130,
- emptyText: gettext('unlimited'),
- },
- {
- xtype: 'numberfield',
- name: 'period',
- minValue: 1,
- step: 1,
- fieldLabel: gettext('Period') + ' (ms)',
- labelWidth: 130,
- emptyText: '1000',
- },
- {
- xtype: 'displayfield',
- reference: 'limitWarning',
- value: gettext(
- 'Disabling the limiter can potentially allow a guest to overload the host. Proceed with caution.',
- ),
- userCls: 'pmx-hint',
- hidden: true,
- },
- ],
-});
-
-Ext.define('PVE.qemu.RNGEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: gettext('VirtIO RNG'),
-
- items: [
- {
- xtype: 'pveRNGInputPanel',
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response) {
- me.vmconfig = response.result.data;
-
- var rng0 = me.vmconfig.rng0;
- if (rng0) {
- me.setValues(PVE.Parser.parsePropertyString(rng0));
- }
- },
- });
- }
- },
-});
-Ext.define('PVE.qemu.SSHKeyInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveQemuSSHKeyInputPanel',
-
- insideWizard: false,
-
- onGetValues: function (values) {
- var _me = this;
- if (values.sshkeys) {
- values.sshkeys.trim();
- }
- if (!values.sshkeys.length) {
- values = {};
- values.delete = 'sshkeys';
- return values;
- } else {
- values.sshkeys = encodeURIComponent(values.sshkeys);
- }
- return values;
- },
-
- items: [
- {
- xtype: 'textarea',
- itemId: 'sshkeys',
- name: 'sshkeys',
- height: 250,
- },
- {
- xtype: 'filebutton',
- itemId: 'filebutton',
- name: 'file',
- text: gettext('Load SSH Key File'),
- fieldLabel: 'test',
- listeners: {
- change: function (btn, e, value) {
- let view = this.up('inputpanel');
- e = e.event;
- Ext.Array.each(e.target.files, function (file) {
- PVE.Utils.loadSSHKeyFromFile(file, function (res) {
- let keysField = view.down('#sshkeys');
- var old = keysField.getValue();
- keysField.setValue(old + res);
- });
- });
- btn.reset();
- },
- },
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
- if (!window.FileReader) {
- me.down('#filebutton').setVisible(false);
- }
- },
-});
-
-Ext.define('PVE.qemu.SSHKeyEdit', {
- extend: 'Proxmox.window.Edit',
-
- width: 800,
-
- initComponent: function () {
- var me = this;
-
- var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel');
-
- Ext.apply(me, {
- subject: gettext('SSH Keys'),
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.create) {
- me.load({
- success: function (response, options) {
- var data = response.result.data;
- if (data.sshkeys) {
- data.sshkeys = decodeURIComponent(data.sshkeys);
- ipanel.setValues(data);
- }
- },
- });
- }
- },
-});
-Ext.define('PVE.qemu.ScsiHwEdit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- Ext.applyIf(me, {
- subject: gettext('SCSI Controller Type'),
- items: {
- xtype: 'pveScsiHwSelector',
- name: 'scsihw',
- value: '__default__',
- fieldLabel: gettext('Type'),
- },
- });
-
- me.callParent();
-
- me.load();
- },
-});
-Ext.define('PVE.qemu.SerialnputPanel', {
- extend: 'Proxmox.panel.InputPanel',
-
- autoComplete: false,
-
- setVMConfig: function (vmconfig) {
- var me = this,
- i;
- me.vmconfig = vmconfig;
-
- for (i = 0; i < 4; i++) {
- let port = 'serial' + i.toString();
- if (!me.vmconfig[port]) {
- me.down('field[name=serialid]').setValue(i);
- break;
- }
- }
- },
-
- onGetValues: function (values) {
- var _me = this;
-
- var id = 'serial' + values.serialid;
- delete values.serialid;
- values[id] = 'socket';
- return values;
- },
-
- items: [
- {
- xtype: 'proxmoxintegerfield',
- name: 'serialid',
- fieldLabel: gettext('Serial Port'),
- minValue: 0,
- maxValue: 3,
- allowBlank: false,
- validator: function (id) {
- if (!this.rendered) {
- return true;
- }
- let view = this.up('panel');
- if (view.vmconfig !== undefined && Ext.isDefined(view.vmconfig['serial' + id])) {
- return 'This device is already in use.';
- }
- return true;
- },
- },
- ],
-});
-
-Ext.define('PVE.qemu.SerialEdit', {
- extend: 'Proxmox.window.Edit',
-
- vmconfig: undefined,
-
- isAdd: true,
-
- subject: gettext('Serial Port'),
-
- initComponent: function () {
- var me = this;
-
- // for now create of (socket) serial port only
- me.isCreate = true;
-
- var ipanel = Ext.create('PVE.qemu.SerialnputPanel', {});
-
- Ext.apply(me, {
- items: [ipanel],
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- ipanel.setVMConfig(response.result.data);
- },
- });
- },
-});
-Ext.define('PVE.qemu.SevInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveSevInputPanel',
-
- onlineHelp: 'qm_memory', // TODO: change to 'qm_memory_encryption' one available
-
- viewModel: {
- data: {
- type: '__default__',
- },
- formulas: {
- sevEnabled: (get) =>
- get('type') === 'std' || get('type') === 'es' || get('type') === 'snp',
- snpEnabled: (get) => get('type') === 'snp',
- },
- },
-
- onGetValues: function (values) {
- if (values.delete === 'type') {
- values.delete = 'amd-sev';
- return values;
- }
- if (!values.debug) {
- values['no-debug'] = 1;
- }
- if (!values.smt && values.type === 'snp') {
- values['allow-smt'] = 0;
- }
- if (!values['key-sharing'] && values.type !== 'snp') {
- values['no-key-sharing'] = 1;
- }
- delete values.debug;
- delete values.smt;
- delete values['key-sharing'];
- let ret = {};
- ret['amd-sev'] = PVE.Parser.printPropertyString(values, 'type');
- return ret;
- },
-
- setValues: function (values) {
- if (PVE.Parser.parseBoolean(values['no-debug'])) {
- values.debug = 0;
- }
- values.smt = PVE.Parser.parseBoolean(values['allow-smt'], 1);
- if (PVE.Parser.parseBoolean(values['no-key-sharing'])) {
- values['key-sharing'] = 0;
- }
- this.callParent(arguments);
- },
-
- items: [
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('AMD SEV Type'),
- labelWidth: 150,
- name: 'type',
- value: '__default__',
- comboItems: [
- [
- '__default__',
- Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')',
- ],
- ['std', 'AMD SEV'],
- ['es', 'AMD SEV-ES'],
- ['snp', 'AMD SEV-SNP'],
- ],
- bind: {
- value: '{type}',
- },
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext('WARNING: When using SEV-SNP no EFI disk is loaded as pflash.'),
- bind: {
- hidden: '{!snpEnabled}',
- },
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext('Note: SEV-SNP requires host kernel version 6.11 or higher.'),
- bind: {
- hidden: '{!snpEnabled}',
- },
- },
- ],
-
- advancedItems: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Allow Debugging'),
- labelWidth: 150,
- name: 'debug',
- value: 1,
- bind: {
- hidden: '{!sevEnabled}',
- disabled: '{!sevEnabled}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Allow Key-Sharing'),
- labelWidth: 150,
- name: 'key-sharing',
- value: 1,
- bind: {
- hidden: '{!sevEnabled || snpEnabled}',
- disabled: '{!sevEnabled || snpEnabled}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Allow SMT'),
- labelWidth: 150,
- name: 'smt',
- value: 1,
- bind: {
- hidden: '{!snpEnabled}',
- disabled: '{!snpEnabled}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Enable Kernel Hashes'),
- labelWidth: 150,
- name: 'kernel-hashes',
- deleteDefaultValue: false,
- bind: {
- hidden: '{!sevEnabled}',
- disabled: '{!sevEnabled}',
- },
- },
- ],
-});
-
-Ext.define('PVE.qemu.SevEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: 'AMD Secure Encrypted Virtualization (SEV)',
-
- items: {
- xtype: 'pveSevInputPanel',
- },
-
- width: 400,
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- me.load({
- success: function (response) {
- let conf = response.result.data;
- let amd_sev = conf['amd-sev'] || '__default__';
- me.setValues(PVE.Parser.parsePropertyString(amd_sev, 'type'));
- },
- });
- },
-});
-Ext.define('PVE.qemu.Smbios1InputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- alias: 'widget.PVE.qemu.Smbios1InputPanel',
-
- insideWizard: false,
-
- smbios1: {},
-
- onGetValues: function (values) {
- var _me = this;
-
- var params = {
- smbios1: PVE.Parser.printQemuSmbios1(values),
- };
-
- return params;
- },
-
- setSmbios1: function (data) {
- var me = this;
-
- me.smbios1 = data;
-
- me.setValues(me.smbios1);
- },
-
- items: [
- {
- xtype: 'textfield',
- fieldLabel: 'UUID',
- regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/,
- name: 'uuid',
- },
- {
- xtype: 'textareafield',
- fieldLabel: gettext('Manufacturer'),
- fieldStyle: {
- height: '2em',
- minHeight: '2em',
- },
- name: 'manufacturer',
- },
- {
- xtype: 'textareafield',
- fieldLabel: gettext('Product'),
- fieldStyle: {
- height: '2em',
- minHeight: '2em',
- },
- name: 'product',
- },
- {
- xtype: 'textareafield',
- fieldLabel: gettext('Version'),
- fieldStyle: {
- height: '2em',
- minHeight: '2em',
- },
- name: 'version',
- },
- {
- xtype: 'textareafield',
- fieldLabel: gettext('Serial'),
- fieldStyle: {
- height: '2em',
- minHeight: '2em',
- },
- name: 'serial',
- },
- {
- xtype: 'textareafield',
- fieldLabel: 'SKU',
- fieldStyle: {
- height: '2em',
- minHeight: '2em',
- },
- name: 'sku',
- },
- {
- xtype: 'textareafield',
- fieldLabel: gettext('Family'),
- fieldStyle: {
- height: '2em',
- minHeight: '2em',
- },
- name: 'family',
- },
- ],
-});
-
-Ext.define('PVE.qemu.Smbios1Edit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {});
-
- Ext.applyIf(me, {
- subject: gettext('SMBIOS settings (type1)'),
- width: 450,
- items: ipanel,
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- me.vmconfig = response.result.data;
- var value = me.vmconfig.smbios1;
- if (value) {
- let data = PVE.Parser.parseQemuSmbios1(value);
- if (!data) {
- Ext.Msg.alert(gettext('Error'), 'Unable to parse smbios options');
- me.close();
- return;
- }
- ipanel.setSmbios1(data);
- }
- },
- });
- },
-});
-Ext.define('PVE.qemu.SystemInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveQemuSystemPanel',
-
- onlineHelp: 'qm_system_settings',
-
- viewModel: {
- data: {
- efi: false,
- addefi: true,
- },
-
- formulas: {
- efidisk: function (get) {
- return get('efi') && get('addefi');
- },
- },
- },
-
- onGetValues: function (values) {
- if (values.vga && values.vga.substr(0, 6) === 'serial') {
- values['serial' + values.vga.substr(6, 1)] = 'socket';
- }
-
- delete values.hdimage;
- delete values.hdstorage;
- delete values.diskformat;
-
- delete values.preEnrolledKeys; // efidisk
- delete values.version; // tpmstate
-
- return values;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- scsihwChange: function (field, value) {
- var me = this;
- if (me.getView().insideWizard) {
- me.getViewModel().set('current.scsihw', value);
- }
- },
-
- biosChange: function (field, value) {
- var me = this;
- if (me.getView().insideWizard) {
- me.getViewModel().set('efi', value === 'ovmf');
- }
- },
-
- control: {
- pveScsiHwSelector: {
- change: 'scsihwChange',
- },
- pveQemuBiosSelector: {
- change: 'biosChange',
- },
- '#': {
- afterrender: 'setMachine',
- },
- },
-
- setMachine: function () {
- let me = this;
- let vm = this.getViewModel();
- let ostype = vm.get('current.ostype');
- if (ostype === 'win11') {
- me.lookup('machine').setValue('q35');
- me.lookup('bios').setValue('ovmf');
- me.lookup('addtpmbox').setValue(true);
- }
- },
- },
-
- column1: [
- {
- xtype: 'proxmoxKVComboBox',
- value: '__default__',
- deleteEmpty: false,
- fieldLabel: gettext('Graphic card'),
- name: 'vga',
- comboItems: Object.entries(PVE.Utils.kvm_vga_drivers),
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'machine',
- reference: 'machine',
- value: '__default__',
- fieldLabel: gettext('Machine'),
- comboItems: [
- ['__default__', PVE.Utils.render_qemu_machine('')],
- ['q35', 'q35'],
- ],
- },
- {
- xtype: 'displayfield',
- value: gettext('Firmware'),
- },
- {
- xtype: 'pveQemuBiosSelector',
- name: 'bios',
- reference: 'bios',
- value: '__default__',
- fieldLabel: 'BIOS',
- },
- {
- xtype: 'proxmoxcheckbox',
- bind: {
- value: '{addefi}',
- hidden: '{!efi}',
- disabled: '{!efi}',
- },
- hidden: true,
- submitValue: false,
- disabled: true,
- fieldLabel: gettext('Add EFI Disk'),
- },
- {
- xtype: 'pveEFIDiskInputPanel',
- name: 'efidisk0',
- storageContent: 'images',
- bind: {
- nodename: '{nodename}',
- hidden: '{!efi}',
- disabled: '{!efidisk}',
- },
- autoSelect: false,
- disabled: true,
- hidden: true,
- hideSize: true,
- usesEFI: true,
- },
- ],
-
- column2: [
- {
- xtype: 'pveScsiHwSelector',
- name: 'scsihw',
- value: '__default__',
- bind: {
- value: '{current.scsihw}',
- },
- fieldLabel: gettext('SCSI Controller'),
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'agent',
- uncheckedValue: 0,
- defaultValue: 0,
- deleteDefaultValue: true,
- fieldLabel: gettext('Qemu Agent'),
- },
- {
- // fake for spacing
- xtype: 'displayfield',
- value: ' ',
- },
- {
- xtype: 'proxmoxcheckbox',
- reference: 'addtpmbox',
- bind: {
- value: '{addtpm}',
- },
- submitValue: false,
- fieldLabel: gettext('Add TPM'),
- },
- {
- xtype: 'pveTPMDiskInputPanel',
- name: 'tpmstate0',
- storageContent: 'images',
- bind: {
- nodename: '{nodename}',
- hidden: '{!addtpm}',
- disabled: '{!addtpm}',
- },
- disabled: true,
- hidden: true,
- },
- ],
-});
-Ext.define('PVE.qemu.TdxInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveTdxInputPanel',
-
- onlineHelp: 'qm_memory', // TODO: change to 'qm_memory_encryption' one available
-
- viewModel: {
- data: {
- type: '__default__',
- attestation: 1,
- },
- formulas: {
- tdxEnabled: (get) => get('type') === 'tdx',
- attestationEnabled: (get) => Number(get('attestation')) === 1,
- },
- },
-
- onGetValues: function (values) {
- if (values.delete === 'type') {
- values.delete = 'intel-tdx';
- return values;
- }
- let ret = {};
- ret['intel-tdx'] = PVE.Parser.printPropertyString(values, 'type');
- return ret;
- },
-
- setValues: function (values) {
- this.callParent(arguments);
- },
-
- items: [
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('Intel TDX Type'),
- labelWidth: 150,
- name: 'type',
- value: '__default__',
- comboItems: [
- [
- '__default__',
- Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')',
- ],
- ['tdx', 'Intel TDX'],
- ],
- bind: {
- value: '{type}',
- },
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext('WARNING: When using Intel TDX no EFI disk is loaded as pflash.'),
- bind: {
- hidden: '{!tdxEnabled}',
- },
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- 'Note: Intel TDX is only supported by specific recent CPU models and requires host kernel version 6.16 or higher.',
- ),
- bind: {
- hidden: '{!tdxEnabled}',
- },
- },
- ],
-
- advancedItems: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Enable Attestation'),
- labelWidth: 150,
- name: 'attestation',
- value: 1,
- uncheckedValue: 0,
- bind: {
- value: '{attestation}',
- hidden: '{!tdxEnabled}',
- disabled: '{!tdxEnabled}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: gettext('CID'),
- labelWidth: 150,
- name: 'vsock-cid',
- minValue: 2,
- value: '2',
- allowBlank: false,
- bind: {
- hidden: '{!tdxEnabled}',
- disabled: '{!attestationEnabled || !tdxEnabled}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- fieldLabel: gettext('Port'),
- labelWidth: 150,
- name: 'vsock-port',
- minValue: 0,
- value: '4050',
- allowBlank: false,
- bind: {
- hidden: '{!tdxEnabled}',
- disabled: '{!attestationEnabled || !tdxEnabled}',
- },
- },
- ],
-});
-
-Ext.define('PVE.qemu.TdxEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: 'Intel Trust Domain Extension (TDX)',
-
- items: {
- xtype: 'pveTdxInputPanel',
- },
-
- width: 400,
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
-
- me.load({
- success: function (response) {
- let conf = response.result.data;
- let intel_tdx = conf['intel-tdx'] || '__default__';
- me.setValues(PVE.Parser.parsePropertyString(intel_tdx, 'type'));
- },
- });
- },
-});
-Ext.define('PVE.qemu.USBInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- autoComplete: false,
- onlineHelp: 'qm_usb_passthrough',
-
- cbindData: function (initialConfig) {
- let me = this;
- if (!me.pveSelNode) {
- throw 'no pveSelNode given';
- }
-
- return { nodename: me.pveSelNode.data.node };
- },
-
- viewModel: {
- data: {},
- },
-
- setVMConfig: function (vmconfig) {
- var me = this;
- me.vmconfig = vmconfig;
- let max_usb = PVE.Utils.get_max_usb_count(me.vmconfig.ostype, me.vmconfig.machine);
- if (max_usb > PVE.Utils.hardware_counts.usb_old) {
- me.down('field[name=usb3]').setDisabled(true);
- }
- },
-
- onGetValues: function (values) {
- var me = this;
- if (!me.confid) {
- let max_usb = PVE.Utils.get_max_usb_count(me.vmconfig.ostype, me.vmconfig.machine);
- for (let i = 0; i < max_usb; i++) {
- let id = 'usb' + i.toString();
- if (!me.vmconfig[id]) {
- me.confid = id;
- break;
- }
- }
- }
- var val = '';
- var type = me.down('radiofield').getGroupValue();
- switch (type) {
- case 'spice':
- val = 'spice';
- break;
- case 'mapped':
- val = `mapping=${values[type]}`;
- delete values.mapped;
- break;
- case 'hostdevice':
- case 'port':
- val = 'host=' + values[type];
- delete values[type];
- break;
- default:
- throw 'invalid type selected';
- }
-
- if (values.usb3) {
- delete values.usb3;
- val += ',usb3=1';
- }
- values[me.confid] = val;
- return values;
- },
-
- items: [
- {
- xtype: 'fieldcontainer',
- defaultType: 'radiofield',
- layout: 'fit',
- items: [
- {
- name: 'usb',
- inputValue: 'spice',
- boxLabel: gettext('Spice Port'),
- submitValue: false,
- checked: true,
- },
- {
- name: 'usb',
- inputValue: 'mapped',
- boxLabel: gettext('Use mapped Device'),
- reference: 'mapped',
- submitValue: false,
- },
- {
- xtype: 'pveUSBMapSelector',
- disabled: true,
- name: 'mapped',
- cbind: { nodename: '{nodename}' },
- bind: { disabled: '{!mapped.checked}' },
- allowBlank: false,
- fieldLabel: gettext('Choose Device'),
- labelAlign: 'right',
- },
- {
- name: 'usb',
- inputValue: 'hostdevice',
- boxLabel: gettext('Use USB Vendor/Device ID'),
- reference: 'hostdevice',
- submitValue: false,
- },
- {
- xtype: 'pveUSBSelector',
- disabled: true,
- type: 'device',
- name: 'hostdevice',
- cbind: { pveSelNode: '{pveSelNode}' },
- bind: { disabled: '{!hostdevice.checked}' },
- editable: true,
- allowBlank: false,
- fieldLabel: gettext('Choose Device'),
- labelAlign: 'right',
- },
- {
- name: 'usb',
- inputValue: 'port',
- boxLabel: gettext('Use USB Port'),
- reference: 'port',
- submitValue: false,
- },
- {
- xtype: 'pveUSBSelector',
- disabled: true,
- name: 'port',
- cbind: { pveSelNode: '{pveSelNode}' },
- bind: { disabled: '{!port.checked}' },
- editable: true,
- type: 'port',
- allowBlank: false,
- fieldLabel: gettext('Choose Port'),
- labelAlign: 'right',
- },
- {
- xtype: 'checkbox',
- name: 'usb3',
- inputValue: true,
- checked: true,
- reference: 'usb3',
- fieldLabel: gettext('Use USB3'),
- },
- ],
- },
- ],
-});
-
-Ext.define('PVE.qemu.USBEdit', {
- extend: 'Proxmox.window.Edit',
-
- vmconfig: undefined,
-
- isAdd: true,
- width: 400,
- subject: gettext('USB Device'),
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.confid;
-
- var ipanel = Ext.create('PVE.qemu.USBInputPanel', {
- confid: me.confid,
- pveSelNode: me.pveSelNode,
- });
-
- Ext.apply(me, {
- items: [ipanel],
- });
-
- me.callParent();
-
- me.load({
- success: function (response, options) {
- ipanel.setVMConfig(response.result.data);
- if (me.isCreate) {
- return;
- }
-
- let data = PVE.Parser.parsePropertyString(response.result.data[me.confid], 'host');
- let port,
- hostdevice,
- mapped,
- usb3 = false;
- let usb;
-
- if (data.host) {
- if (/^(0x)?[a-zA-Z0-9]{4}:(0x)?[a-zA-Z0-9]{4}$/.test(data.host)) {
- hostdevice = data.host.replace('0x', '');
- usb = 'hostdevice';
- } else if (/^(\d+)-(\d+(\.\d+)*)$/.test(data.host)) {
- port = data.host;
- usb = 'port';
- } else if (/^spice$/i.test(data.host)) {
- usb = 'spice';
- }
- } else if (data.mapping) {
- mapped = data.mapping;
- usb = 'mapped';
- }
-
- usb3 = data.usb3 ?? false;
-
- var values = {
- usb,
- hostdevice,
- port,
- usb3,
- mapped,
- };
-
- ipanel.setValues(values);
- },
- });
- },
-});
-Ext.define('PVE.qemu.VirtiofsInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pveVirtiofsInputPanel',
- onlineHelp: 'qm_virtiofs',
-
- insideWizard: false,
-
- onGetValues: function (values) {
- var me = this;
- var confid = me.confid;
- var params = {};
- delete values.delete;
- params[confid] = PVE.Parser.printPropertyString(values, 'dirid');
- return params;
- },
-
- setSharedfiles: function (confid, data) {
- var me = this;
- me.confid = confid;
- me.virtiofs = data;
- me.setValues(me.virtiofs);
- },
- initComponent: function () {
- let me = this;
-
- me.nodename = me.pveSelNode.data.node;
- if (!me.nodename) {
- throw 'no node name specified';
- }
- me.items = [
- {
- xtype: 'pveDirMapSelector',
- name: 'dirid',
- fieldLabel: gettext('Directory ID'),
- emptyText: gettext('Mapping ID'),
- nodename: me.nodename,
- allowBlank: false,
- },
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: gettext(
- 'Directory Mappings can be managed under Datacenter -> Directory Mappings',
- ),
- },
- ];
- me.advancedItems = [
- {
- xtype: 'proxmoxKVComboBox',
- name: 'cache',
- fieldLabel: gettext('Cache'),
- value: '__default__',
- deleteDefaultValue: false,
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText + ' (auto)'],
- ['auto', 'auto'],
- ['always', 'always'],
- ['metadata', 'metadata'],
- ['never', 'never'],
- ],
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'expose-xattr',
- fieldLabel: gettext('xattr Support'),
- boxLabel: gettext('Enable support for extended attributes.'),
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'expose-acl',
- fieldLabel: gettext('POSIX ACLs'),
- boxLabel: gettext('Implies xattr support.'),
- listeners: {
- change: function (f, value) {
- let xattr = me.down('field[name=expose-xattr]');
- xattr.setDisabled(value);
- xattr.setValue(value);
- },
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'direct-io',
- fieldLabel: gettext('Allow Direct IO'),
- },
- ];
-
- me.virtiofs = {};
- me.confid = 'virtiofs0';
- me.callParent();
- },
-});
-
-Ext.define('PVE.qemu.VirtiofsEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: gettext('Virtiofs Filesystem Passthrough'),
- width: 450,
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.confid;
-
- var ipanel = Ext.create('PVE.qemu.VirtiofsInputPanel', {
- confid: me.confid,
- pveSelNode: me.pveSelNode,
- isCreate: me.isCreate,
- });
-
- Ext.applyIf(me, {
- items: ipanel,
- });
-
- me.callParent();
-
- me.load({
- success: function (response) {
- me.conf = response.result.data;
- var i, confid;
- if (!me.isCreate) {
- let value = me.conf[me.confid];
- let virtiofs = PVE.Parser.parsePropertyString(value, 'dirid');
- if (!virtiofs) {
- Ext.Msg.alert(gettext('Error'), 'Unable to parse virtiofs options');
- me.close();
- return;
- }
- ipanel.setSharedfiles(me.confid, virtiofs);
- } else {
- for (i = 0; i < PVE.Utils.hardware_counts.virtiofs; i++) {
- confid = 'virtiofs' + i.toString();
- if (!Ext.isDefined(me.conf[confid])) {
- me.confid = confid;
- break;
- }
- }
- ipanel.setSharedfiles(me.confid, {});
- }
- },
- });
- },
-});
-Ext.define('PVE.sdn.Browser', {
- extend: 'PVE.panel.Config',
- alias: 'widget.PVE.sdn.Browser',
-
- onlineHelp: 'chapter_pvesdn',
-
- initComponent: function () {
- let me = this;
-
- let nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
- let sdnId = me.pveSelNode.data.sdn;
- if (!sdnId) {
- throw 'no sdn ID specified';
- }
-
- me.items = [];
-
- Ext.apply(me, {
- title: Ext.String.format(
- gettext('Zone {0} on node {1}'),
- `'${sdnId}'`,
- `'${nodename}'`,
- ),
- hstateid: 'sdntab',
- });
-
- const caps = Ext.state.Manager.get('GuiCap');
-
- me.items.push({
- nodename: nodename,
- zone: sdnId,
- xtype: 'pveSDNZoneContentPanel',
- title: gettext('Content'),
- iconCls: 'fa fa-th',
- itemId: 'content',
- });
-
- if (caps.sdn['Permissions.Modify']) {
- me.items.push({
- xtype: 'pveACLView',
- title: gettext('Permissions'),
- iconCls: 'fa fa-unlock',
- itemId: 'permissions',
- path: `/sdn/zones/${sdnId}`,
- });
- }
-
- if (me.pveSelNode.data['zone-type'] && me.pveSelNode.data['zone-type'] === 'evpn') {
- me.items.push({
- nodename: nodename,
- zone: sdnId,
- xtype: 'pveSDNEvpnZoneIpVrfPanel',
- title: gettext('IP-VRF'),
- iconCls: 'fa fa-th-list',
- itemId: 'ip-vrf',
- });
-
- me.items.push({
- nodename: nodename,
- zone: sdnId,
- xtype: 'pveSDNEvpnZoneMacVrfPanel',
- title: gettext('MAC-VRFs'),
- iconCls: 'fa fa-th-list',
- itemId: 'mac-vrfs',
- });
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.network.Browser', {
- extend: 'PVE.panel.Config',
- alias: 'widget.PVE.network.Browser',
-
- initComponent: function () {
- let me = this;
- let data = me.pveSelNode.data;
-
- let node = data.node;
- if (!node) {
- throw 'no node name specified';
- }
-
- let name = data.network;
- if (!name) {
- throw 'no name specified';
- }
-
- let networkType = data['network-type'];
- if (!networkType) {
- throw 'no type specified';
- }
-
- me.items = [];
-
- if (networkType === 'fabric') {
- me.onlineHelp = 'pvesdn_config_fabrics';
-
- me.items.push({
- nodename: node,
- fabricId: name,
- protocol: me.pveSelNode.data.protocol,
- xtype: 'pveSDNFabricRoutesContentView',
- title: gettext('Routes'),
- iconCls: 'fa fa-exchange',
- itemId: 'routes',
- width: '100%',
- store: {
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${node}/sdn/fabrics/${name}/routes`,
- reader: {
- type: 'json',
- rootProperty: 'data',
- },
- },
- autoLoad: true,
- },
- });
-
- me.items.push({
- nodename: node,
- fabricId: name,
- protocol: me.pveSelNode.data.protocol,
- xtype: 'pveSDNFabricNeighborsContentView',
- title: gettext('Neighbors'),
- iconCls: 'fa fa-handshake-o',
- itemId: 'neighbors',
- width: '100%',
- store: {
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${node}/sdn/fabrics/${name}/neighbors`,
- reader: {
- type: 'json',
- rootProperty: 'data',
- },
- },
- autoLoad: true,
- },
- });
-
- me.items.push({
- nodename: node,
- fabricId: name,
- protocol: me.pveSelNode.data.protocol,
- xtype: 'pveSDNFabricInterfacesContentView',
- title: gettext('Interfaces'),
- iconCls: 'fa fa-upload',
- itemId: 'interfaces',
- width: '100%',
- store: {
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${node}/sdn/fabrics/${name}/interfaces`,
- reader: {
- type: 'json',
- rootProperty: 'data',
- },
- },
- autoLoad: true,
- },
- });
- } else if (networkType === 'zone') {
- const caps = Ext.state.Manager.get('GuiCap');
-
- me.items.push({
- nodename: node,
- zone: name,
- xtype: 'pveSDNZoneContentPanel',
- title: gettext('Content'),
- iconCls: 'fa fa-th',
- itemId: 'content',
- });
-
- if (caps.sdn['Permissions.Modify']) {
- me.items.push({
- xtype: 'pveACLView',
- title: gettext('Permissions'),
- iconCls: 'fa fa-unlock',
- itemId: 'permissions',
- path: `/sdn/zones/${name}`,
- });
- }
-
- me.items.push({
- nodename: node,
- zone: name,
- xtype: 'pveSDNZoneBridgePanel',
- title: gettext('Bridges'),
- iconCls: 'fa fa-network-wired x-fa-sdn-treelist',
- itemId: 'bridges',
- });
-
- if (data['zone-type'] && data['zone-type'] === 'evpn') {
- me.items.push({
- nodename: node,
- zone: name,
- xtype: 'pveSDNEvpnZoneIpVrfPanel',
- title: gettext('IP-VRF'),
- iconCls: 'fa fa-th-list',
- itemId: 'ip-vrf',
- });
-
- me.items.push({
- nodename: node,
- zone: name,
- xtype: 'pveSDNEvpnZoneMacVrfPanel',
- title: gettext('MAC-VRFs'),
- iconCls: 'fa fa-th-list',
- itemId: 'mac-vrfs',
- });
- }
- } else {
- me.items.push({
- xtype: 'container',
- title: gettext('Content'),
- iconCls: 'fa fa-th',
- itemId: 'content',
- html: `unknown network type: ${networkType}`,
- width: '100%',
- });
- }
-
- Ext.apply(me, {
- title: Ext.String.format(
- gettext('{0} {1} on node {2}'),
- `${networkType}`,
- `'${name}'`,
- `'${node}'`,
- ),
- hstateid: 'networktab',
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.ControllerView', {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveSDNControllerView'],
-
- onlineHelp: 'pvesdn_config_controllers',
-
- stateful: true,
- stateId: 'grid-sdn-controller',
-
- createSDNControllerEditWindow: function (type, sid) {
- var schema = PVE.Utils.sdncontrollerSchema[type];
- if (!schema || !schema.ipanel) {
- throw 'no editor registered for controller type: ' + type;
- }
-
- Ext.create('PVE.sdn.controllers.BaseEdit', {
- paneltype: 'PVE.sdn.controllers.' + schema.ipanel,
- type: type,
- controllerid: sid,
- autoShow: true,
- listeners: {
- destroy: this.reloadStore,
- },
- });
- },
-
- initComponent: function () {
- var me = this;
-
- var store = new Ext.data.Store({
- model: 'pve-sdn-controller',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/controllers?pending=1',
- },
- sorters: {
- property: 'controller',
- direction: 'ASC',
- },
- });
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- let type = rec.data.type,
- controller = rec.data.controller;
- me.createSDNControllerEditWindow(type, controller);
- };
-
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/cluster/sdn/controllers/',
- callback: () => store.load(),
- });
-
- // else we cannot dynamically generate the add menu handlers
- let addHandleGenerator = function (type) {
- return function () {
- me.createSDNControllerEditWindow(type);
- };
- };
- let addMenuItems = [];
- for (const [type, controller] of Object.entries(PVE.Utils.sdncontrollerSchema)) {
- if (controller.hideAdd) {
- continue;
- }
- addMenuItems.push({
- text: PVE.Utils.format_sdncontroller_type(type),
- iconCls: 'fa fa-fw fa-' + controller.faIcon,
- handler: addHandleGenerator(type),
- });
- }
-
- Ext.apply(me, {
- store: store,
- reloadStore: () => store.load(),
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- tbar: [
- {
- text: gettext('Add'),
- menu: new Ext.menu.Menu({
- items: addMenuItems,
- }),
- },
- remove_btn,
- edit_btn,
- ],
- columns: [
- {
- header: 'ID',
- flex: 2,
- sortable: true,
- dataIndex: 'controller',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'controller', 1);
- },
- },
- {
- header: gettext('Type'),
- flex: 1,
- sortable: true,
- dataIndex: 'type',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'type', 1);
- },
- },
- {
- header: gettext('Node'),
- flex: 1,
- sortable: true,
- dataIndex: 'node',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'node', 1);
- },
- },
- {
- header: gettext('State'),
- width: 100,
- dataIndex: 'state',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending_state(rec, value);
- },
- },
- ],
- listeners: {
- activate: () => store.load(),
- itemdblclick: run_editor,
- },
- });
- store.load();
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.Status', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveSDNStatus',
-
- onlineHelp: 'chapter_pvesdn',
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- initComponent: function () {
- var me = this;
-
- me.rstore = Ext.create('Proxmox.data.ObjectStore', {
- interval: me.interval,
- model: 'pve-sdn-status',
- storeid: 'pve-store-' + ++Ext.idSeed,
- groupField: 'type',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/resources',
- },
- });
-
- me.items = [
- {
- xtype: 'pveSDNStatusView',
- title: gettext('Status'),
- rstore: me.rstore,
- border: 0,
- collapsible: true,
- padding: '0 0 20 0',
- },
- ];
-
- me.callParent();
- me.on('activate', me.rstore.startUpdate);
- },
-});
-Ext.define(
- 'PVE.sdn.StatusView',
- {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNStatusView',
-
- sortPriority: {
- sdn: 1,
- node: 2,
- status: 3,
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.rstore) {
- throw 'no rstore given';
- }
-
- Proxmox.Utils.monStoreErrors(me, me.rstore);
-
- var store = Ext.create('Proxmox.data.DiffStore', {
- rstore: me.rstore,
- sortAfterUpdate: true,
- sorters: [
- {
- sorterFn: function (rec1, rec2) {
- var p1 = me.sortPriority[rec1.data.type];
- var p2 = me.sortPriority[rec2.data.type];
- return p1 !== p2 ? (p1 > p2 ? 1 : -1) : 0;
- },
- },
- ],
- filters: {
- property: 'type',
- value: 'network',
- operator: '==',
- },
- });
-
- Ext.apply(me, {
- store: store,
- stateful: false,
- tbar: [
- {
- text: gettext('Apply'),
- handler: function () {
- Ext.Msg.show({
- title: gettext('Confirm'),
- icon: Ext.Msg.QUESTION,
- msg: gettext(
- 'Applying pending SDN changes will also apply any pending local node network changes. Proceed?',
- ),
- buttons: Ext.Msg.YESNO,
- callback: function (btn) {
- if (btn === 'yes') {
- Proxmox.Utils.API2Request({
- url: '/cluster/sdn/',
- method: 'PUT',
- waitMsgTarget: me,
- failure: (response) =>
- Ext.Msg.alert(
- gettext('Error'),
- response.htmlStatus,
- ),
- });
- }
- },
- });
- },
- },
- ],
- viewConfig: {
- trackOver: false,
- },
- columns: [
- {
- header: 'SDN',
- width: 80,
- dataIndex: 'network',
- },
- {
- header: gettext('Node'),
- width: 80,
- dataIndex: 'node',
- },
- {
- header: gettext('Type'),
- width: 80,
- dataIndex: 'network-type',
- },
- {
- header: gettext('Status'),
- width: 80,
- flex: 1,
- dataIndex: 'status',
- },
- ],
- });
-
- me.callParent();
-
- me.on('activate', me.rstore.startUpdate);
- me.on('destroy', me.rstore.stopUpdate);
- },
- },
- function () {
- Ext.define('pve-sdn-status', {
- extend: 'Ext.data.Model',
- fields: ['id', 'type', 'node', 'status', 'network'],
- idProperty: 'id',
- });
- },
-);
-Ext.define('PVE.sdn.VnetInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onGetValues: function (values) {
- let me = this;
-
- if (me.isCreate) {
- values.type = 'vnet';
- }
-
- return values;
- },
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
- me.setZoneType(undefined);
- },
-
- items: [
- {
- xtype: 'pmxDisplayEditField',
- name: 'vnet',
- cbind: {
- editable: '{isCreate}',
- },
- maxLength: 8,
- flex: 1,
- allowBlank: false,
- fieldLabel: gettext('Name'),
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'alias',
- fieldLabel: gettext('Alias'),
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'pveSDNZoneSelector',
- fieldLabel: gettext('Zone'),
- name: 'zone',
- value: '',
- allowBlank: false,
- listeners: {
- change: function () {
- let me = this;
-
- let record = me.findRecordByValue(me.value);
- let zoneType = record?.data?.type;
-
- let panel = me.up('panel');
- panel.setZoneType(zoneType);
- },
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- itemId: 'sdnVnetTagField',
- name: 'tag',
- minValue: 1,
- maxValue: 16777216,
- fieldLabel: gettext('Tag'),
- allowBlank: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
- advancedItems: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'isolate-ports',
- uncheckedValue: null,
- checked: false,
- fieldLabel: gettext('Isolate Ports'),
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- itemId: 'sdnVnetVlanAwareField',
- name: 'vlanaware',
- uncheckedValue: null,
- checked: false,
- fieldLabel: gettext('VLAN Aware'),
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-
- setZoneType: function (zoneType) {
- let me = this;
-
- let tagField = me.down('#sdnVnetTagField');
- if (!zoneType || zoneType === 'simple') {
- tagField.setVisible(false);
- tagField.setValue('');
- } else {
- tagField.setVisible(true);
- }
-
- let vlanField = me.down('#sdnVnetVlanAwareField');
- if (!zoneType || zoneType === 'evpn') {
- vlanField.setVisible(false);
- vlanField.setValue('');
- } else {
- vlanField.setVisible(true);
- }
- },
-});
-
-Ext.define('PVE.sdn.VnetEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: gettext('VNet'),
-
- vnet: undefined,
-
- width: 350,
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = me.vnet === undefined;
-
- if (me.isCreate) {
- me.url = '/api2/extjs/cluster/sdn/vnets';
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs/cluster/sdn/vnets/' + me.vnet;
- me.method = 'PUT';
- }
-
- let ipanel = Ext.create('PVE.sdn.VnetInputPanel', {
- isCreate: me.isCreate,
- });
-
- Ext.apply(me, {
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- let values = response.result.data;
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-Ext.define('PVE.sdn.VnetView', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNVnetView',
-
- onlineHelp: 'pvesdn_config_vnet',
- emptyText: gettext('No VNet configured.'),
-
- stateful: true,
- stateId: 'grid-sdn-vnet',
-
- subnetview_panel: undefined,
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pve-sdn-vnet',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/vnets?pending=1',
- },
- sorters: {
- property: 'vnet',
- direction: 'ASC',
- },
- });
-
- let reload = () => store.load();
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
-
- let win = Ext.create('PVE.sdn.VnetEdit', {
- autoShow: true,
- onlineHelp: 'pvesdn_config_vnet',
- vnet: rec.data.vnet,
- });
- win.on('destroy', reload);
- };
-
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/cluster/sdn/vnets/',
- callback: reload,
- });
-
- let set_button_status = function () {
- var rec = me.selModel.getSelection()[0];
-
- if (!rec || rec.data.state === 'deleted') {
- edit_btn.disable();
- remove_btn.disable();
- }
- };
-
- Ext.apply(me, {
- store: store,
- reloadStore: reload,
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- tbar: [
- {
- text: gettext('Create'),
- handler: function () {
- let win = Ext.create('PVE.sdn.VnetEdit', {
- autoShow: true,
- onlineHelp: 'pvesdn_config_vnet',
- type: 'vnet',
- });
- win.on('destroy', reload);
- },
- },
- remove_btn,
- edit_btn,
- ],
- columns: [
- {
- header: 'ID',
- flex: 2,
- dataIndex: 'vnet',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'vnet', 1);
- },
- },
- {
- header: gettext('Alias'),
- flex: 1,
- dataIndex: 'alias',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'alias');
- },
- },
- {
- header: gettext('Zone'),
- flex: 1,
- dataIndex: 'zone',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'zone');
- },
- },
- {
- header: gettext('Tag'),
- flex: 1,
- dataIndex: 'tag',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'tag');
- },
- },
- {
- header: gettext('VLAN Aware'),
- flex: 1,
- dataIndex: 'vlanaware',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'vlanaware');
- },
- },
- {
- header: gettext('State'),
- width: 100,
- dataIndex: 'state',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending_state(rec, value);
- },
- },
- ],
- listeners: {
- activate: reload,
- itemdblclick: run_editor,
- selectionchange: set_button_status,
- show: reload,
- select: function (_sm, rec) {
- let url = `/cluster/sdn/vnets/${rec.data.vnet}/subnets`;
- me.subnetview_panel.setBaseUrl(url);
- },
- deselect: function () {
- me.subnetview_panel.setBaseUrl(undefined);
- },
- },
- });
- store.load();
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.VnetACLAdd', {
- extend: 'Proxmox.window.Edit',
- alias: ['widget.pveSDNVnetACLAdd'],
-
- url: '/access/acl',
- method: 'PUT',
- isAdd: true,
- isCreate: true,
-
- width: 400,
- initComponent: function () {
- let me = this;
-
- let items = [
- {
- xtype: 'hiddenfield',
- name: 'path',
- value: me.path,
- allowBlank: false,
- fieldLabel: gettext('Path'),
- },
- ];
-
- if (me.aclType === 'group') {
- me.subject = gettext('Group Permission');
- items.push({
- xtype: 'pveGroupSelector',
- name: 'groups',
- fieldLabel: gettext('Group'),
- });
- } else if (me.aclType === 'user') {
- me.subject = gettext('User Permission');
- items.push({
- xtype: 'pmxUserSelector',
- name: 'users',
- fieldLabel: gettext('User'),
- });
- } else if (me.aclType === 'token') {
- me.subject = gettext('API Token Permission');
- items.push({
- xtype: 'pveTokenSelector',
- name: 'tokens',
- fieldLabel: gettext('API Token'),
- });
- } else {
- throw 'unknown ACL type';
- }
-
- items.push({
- xtype: 'pmxRoleSelector',
- name: 'roles',
- value: 'NoAccess',
- fieldLabel: gettext('Role'),
- });
-
- items.push({
- xtype: 'proxmoxintegerfield',
- name: 'vlan',
- minValue: 1,
- maxValue: 4096,
- allowBlank: true,
- fieldLabel: 'VLAN',
- emptyText: gettext('All'),
- });
-
- let ipanel = Ext.create('Proxmox.panel.InputPanel', {
- items: items,
- onlineHelp: 'pveum_permission_management',
- onGetValues: function (values) {
- if (values.vlan) {
- values.path = values.path + '/' + values.vlan;
- delete values.vlan;
- }
- return values;
- },
- });
-
- Ext.apply(me, {
- items: [ipanel],
- });
-
- me.callParent();
- },
-});
-
-Ext.define(
- 'PVE.sdn.VnetACLView',
- {
- extend: 'Ext.grid.GridPanel',
-
- alias: ['widget.pveSDNVnetACLView'],
-
- onlineHelp: 'chapter_user_management',
-
- stateful: true,
- stateId: 'grid-acls',
-
- // use fixed path
- path: undefined,
-
- setPath: function (path) {
- let me = this;
-
- me.path = path;
-
- if (path === undefined) {
- me.down('#groupmenu').setDisabled(true);
- me.down('#usermenu').setDisabled(true);
- me.down('#tokenmenu').setDisabled(true);
- } else {
- me.down('#groupmenu').setDisabled(false);
- me.down('#usermenu').setDisabled(false);
- me.down('#tokenmenu').setDisabled(false);
- me.store.load();
- }
- },
- initComponent: function () {
- let me = this;
-
- let store = Ext.create('Ext.data.Store', {
- model: 'pve-acl',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/access/acl',
- },
- sorters: {
- property: 'path',
- direction: 'ASC',
- },
- });
-
- store.addFilter(
- Ext.create('Ext.util.Filter', {
- filterFn: (item) =>
- item.data.path.replace(/(\/sdn\/zones\/(.*)\/(.*))\/[0-9]*$/, '$1') ===
- me.path,
- }),
- );
-
- let render_ugid = function (ugid, metaData, record) {
- if (record.data.type === 'group') {
- return '@' + ugid;
- }
-
- return Ext.String.htmlEncode(ugid);
- };
-
- let render_vlan = function (path, metaData, record) {
- let vlan = 'any';
- const match = path.match(/(\/sdn\/zones\/)(.*)\/(.*)\/([0-9]*)$/);
- if (match) {
- vlan = match[4];
- }
-
- return Ext.String.htmlEncode(vlan);
- };
-
- let columns = [
- {
- header: gettext('User') + '/' + gettext('Group') + '/' + gettext('API Token'),
- flex: 1,
- sortable: true,
- renderer: render_ugid,
- dataIndex: 'ugid',
- },
- {
- header: gettext('Role'),
- flex: 1,
- sortable: true,
- dataIndex: 'roleid',
- },
- {
- header: gettext('VLAN'),
- flex: 1,
- sortable: true,
- renderer: render_vlan,
- dataIndex: 'path',
- },
- ];
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let remove_btn = new Proxmox.button.Button({
- text: gettext('Remove'),
- disabled: true,
- selModel: sm,
- confirmMsg: gettext('Are you sure you want to remove this entry'),
- handler: function (btn, event, rec) {
- var params = {
- delete: 1,
- path: rec.data.path,
- roles: rec.data.roleid,
- };
- if (rec.data.type === 'group') {
- params.groups = rec.data.ugid;
- } else if (rec.data.type === 'user') {
- params.users = rec.data.ugid;
- } else if (rec.data.type === 'token') {
- params.tokens = rec.data.ugid;
- } else {
- throw 'unknown data type';
- }
-
- Proxmox.Utils.API2Request({
- url: '/access/acl',
- params: params,
- method: 'PUT',
- waitMsgTarget: me,
- callback: () => store.load(),
- failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
- });
- },
- });
-
- Proxmox.Utils.monStoreErrors(me, store);
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: [
- {
- text: gettext('Add'),
- menu: {
- xtype: 'menu',
- items: [
- {
- text: gettext('Group Permission'),
- disabled: !me.path,
- itemId: 'groupmenu',
- iconCls: 'fa fa-fw fa-group',
- handler: function () {
- var win = Ext.create('PVE.sdn.VnetACLAdd', {
- aclType: 'group',
- path: me.path,
- });
- win.on('destroy', () => store.load());
- win.show();
- },
- },
- {
- text: gettext('User Permission'),
- disabled: !me.path,
- itemId: 'usermenu',
- iconCls: 'fa fa-fw fa-user',
- handler: function () {
- var win = Ext.create('PVE.sdn.VnetACLAdd', {
- aclType: 'user',
- path: me.path,
- });
- win.on('destroy', () => store.load());
- win.show();
- },
- },
- {
- text: gettext('API Token Permission'),
- disabled: !me.path,
- itemId: 'tokenmenu',
- iconCls: 'fa fa-fw fa-user-o',
- handler: function () {
- let win = Ext.create('PVE.sdn.VnetACLAdd', {
- aclType: 'token',
- path: me.path,
- });
- win.on('destroy', () => store.load());
- win.show();
- },
- },
- ],
- },
- },
- remove_btn,
- ],
- viewConfig: {
- trackOver: false,
- },
- columns: columns,
- listeners: {},
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-acl-vnet', {
- extend: 'Ext.data.Model',
- fields: [
- 'path',
- 'type',
- 'ugid',
- 'roleid',
- {
- name: 'propagate',
- type: 'boolean',
- },
- ],
- });
- },
-);
-Ext.define('PVE.sdn.Vnet', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveSDNVnet',
-
- title: 'VNet',
-
- onlineHelp: 'pvesdn_config_vnet',
-
- initComponent: function () {
- var me = this;
-
- var subnetview_panel = Ext.createWidget('pveSDNSubnetView', {
- title: gettext('Subnets'),
- region: 'center',
- border: false,
- });
-
- var vnetview_panel = Ext.createWidget('pveSDNVnetView', {
- title: 'VNets',
- region: 'west',
- subnetview_panel: subnetview_panel,
- width: '50%',
- border: false,
- split: true,
- });
-
- Ext.apply(me, {
- layout: 'border',
- items: [vnetview_panel, subnetview_panel],
- listeners: {
- show: function () {
- subnetview_panel.fireEvent('show', subnetview_panel);
- },
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.SubnetInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onGetValues: function (values) {
- let me = this;
-
- if (me.isCreate) {
- values.type = 'subnet';
- values.subnet = values.cidr;
- delete values.cidr;
- }
-
- return values;
- },
-
- items: [
- {
- xtype: 'pmxDisplayEditField',
- name: 'cidr',
- cbind: {
- editable: '{isCreate}',
- },
- flex: 1,
- allowBlank: false,
- fieldLabel: gettext('Subnet'),
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'gateway',
- vtype: 'IP64Address',
- fieldLabel: gettext('Gateway'),
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'snat',
- uncheckedValue: null,
- checked: false,
- fieldLabel: 'SNAT',
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'dnszoneprefix',
- skipEmptyText: true,
- fieldLabel: gettext('DNS Zone Prefix'),
- allowBlank: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-});
-
-Ext.define('PVE.sdn.SubnetDhcpRangePanel', {
- extend: 'Ext.form.FieldContainer',
- mixins: ['Ext.form.field.Field'],
-
- initComponent: function () {
- let me = this;
-
- me.callParent();
- me.initField();
- },
-
- // since value is an array of objects we need to override isEquals here
- isEqual: function (value1, value2) {
- return JSON.stringify(value1) === JSON.stringify(value2);
- },
-
- getValue: function () {
- let me = this;
- let store = me.lookup('grid').getStore();
-
- let value = [];
-
- store.getData().each((item) => {
- // needs a deep copy otherwise we run in to ExtJS reference
- // shenaningans
- value.push({
- 'start-address': item.data['start-address'],
- 'end-address': item.data['end-address'],
- });
- });
-
- return value;
- },
-
- getSubmitData: function () {
- let me = this;
-
- let data = {};
-
- let value = me
- .getValue()
- .map(
- (item) =>
- `start-address=${item['start-address']},end-address=${item['end-address']}`,
- );
-
- if (value.length) {
- data[me.getName()] = value;
- } else if (!me.isCreate) {
- data.delete = me.getName();
- }
-
- return data;
- },
-
- setValue: function (dhcpRanges) {
- let me = this;
- let store = me.lookup('grid').getStore();
-
- let data = [];
-
- dhcpRanges.forEach((item) => {
- // needs a deep copy otherwise we run in to ExtJS reference
- // shenaningans
- data.push({
- 'start-address': item['start-address'],
- 'end-address': item['end-address'],
- });
- });
-
- store.setData(data);
- },
-
- getErrors: function () {
- let _me = this;
- let errors = [];
-
- return errors;
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- addRange: function () {
- let me = this;
- me.lookup('grid').getStore().add({});
-
- me.getView().checkChange();
- },
-
- removeRange: function (field) {
- let me = this;
- let record = field.getWidgetRecord();
-
- me.lookup('grid').getStore().remove(record);
-
- me.getView().checkChange();
- },
-
- onValueChange: function (field, value) {
- let me = this;
- let record = field.getWidgetRecord();
- let column = field.getWidgetColumn();
-
- record.set(column.dataIndex, value);
- record.commit();
-
- me.getView().checkChange();
- },
-
- control: {
- 'grid button': {
- click: 'removeRange',
- },
- field: {
- change: 'onValueChange',
- },
- },
- },
-
- items: [
- {
- xtype: 'grid',
- reference: 'grid',
- scrollable: true,
- store: {
- fields: ['start-address', 'end-address'],
- },
- columns: [
- {
- text: gettext('Start Address'),
- xtype: 'widgetcolumn',
- dataIndex: 'start-address',
- flex: 1,
- widget: {
- xtype: 'textfield',
- vtype: 'IP64Address',
- },
- },
- {
- text: gettext('End Address'),
- xtype: 'widgetcolumn',
- dataIndex: 'end-address',
- flex: 1,
- widget: {
- xtype: 'textfield',
- vtype: 'IP64Address',
- },
- },
- {
- xtype: 'widgetcolumn',
- width: 40,
- widget: {
- xtype: 'button',
- iconCls: 'fa fa-trash-o',
- },
- },
- ],
- },
- {
- xtype: 'container',
- layout: {
- type: 'hbox',
- },
- items: [
- {
- xtype: 'button',
- text: gettext('Add'),
- iconCls: 'fa fa-plus-circle',
- handler: 'addRange',
- },
- ],
- },
- ],
-});
-
-Ext.define('PVE.sdn.SubnetEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: gettext('Subnet'),
-
- subnet: undefined,
-
- width: 350,
-
- base_url: undefined,
-
- bodyPadding: 0,
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = me.subnet === undefined;
-
- if (me.isCreate) {
- me.url = me.base_url;
- me.method = 'POST';
- } else {
- me.url = me.base_url + '/' + me.subnet;
- me.method = 'PUT';
- }
-
- let ipanel = Ext.create('PVE.sdn.SubnetInputPanel', {
- isCreate: me.isCreate,
- title: gettext('General'),
- });
-
- let dhcpPanel = Ext.create('PVE.sdn.SubnetDhcpRangePanel', {
- isCreate: me.isCreate,
- title: gettext('DHCP Ranges'),
- name: 'dhcp-range',
- });
-
- Ext.apply(me, {
- items: [
- {
- xtype: 'tabpanel',
- bodyPadding: 10,
- items: [ipanel, dhcpPanel],
- },
- ],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- me.setValues(response.result.data);
- },
- });
- }
- },
-});
-Ext.define(
- 'PVE.sdn.SubnetView',
- {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNSubnetView',
-
- stateful: true,
- stateId: 'grid-sdn-subnet',
-
- base_url: undefined,
-
- remove_btn: undefined,
-
- setBaseUrl: function (url) {
- let me = this;
-
- me.base_url = url;
-
- if (url === undefined) {
- me.store.removeAll();
- me.create_btn.disable();
- } else {
- me.remove_btn.baseurl = url + '/';
- me.store.setProxy({
- type: 'proxmox',
- url: '/api2/json/' + url + '?pending=1',
- });
- me.create_btn.enable();
- me.store.load();
- }
- },
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pve-sdn-subnet',
- });
-
- let reload = function () {
- store.load();
- };
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
-
- let win = Ext.create('PVE.sdn.SubnetEdit', {
- autoShow: true,
- subnet: rec.data.subnet,
- base_url: me.base_url,
- });
- win.on('destroy', reload);
- };
-
- me.create_btn = new Proxmox.button.Button({
- text: gettext('Create'),
- disabled: true,
- handler: function () {
- let win = Ext.create('PVE.sdn.SubnetEdit', {
- autoShow: true,
- base_url: me.base_url,
- type: 'subnet',
- });
- win.on('destroy', reload);
- },
- });
-
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- me.remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: me.base_url + '/',
- callback: () => store.load(),
- });
-
- let set_button_status = function () {
- var rec = me.selModel.getSelection()[0];
-
- if (!rec || rec.data.state === 'deleted') {
- edit_btn.disable();
- me.remove_btn.disable();
- }
- };
-
- Ext.apply(me, {
- store: store,
- reloadStore: reload,
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- tbar: [me.create_btn, me.remove_btn, edit_btn],
- columns: [
- {
- header: gettext('Subnet'),
- flex: 2,
- dataIndex: 'cidr',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'cidr', 1);
- },
- },
- {
- header: gettext('Gateway'),
- flex: 1,
- dataIndex: 'gateway',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'gateway');
- },
- },
- {
- header: 'SNAT',
- flex: 1,
- dataIndex: 'snat',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'snat');
- },
- },
- {
- header: gettext('DNS Prefix'),
- flex: 1,
- dataIndex: 'dnszoneprefix',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'dnszoneprefix');
- },
- },
- {
- header: gettext('State'),
- width: 100,
- dataIndex: 'state',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending_state(rec, value);
- },
- },
- ],
- listeners: {
- activate: reload,
- itemdblclick: run_editor,
- selectionchange: set_button_status,
- },
- });
-
- me.callParent();
-
- if (me.base_url) {
- me.setBaseUrl(me.base_url); // load
- }
- },
- },
- function () {
- Ext.define('pve-sdn-subnet', {
- extend: 'Ext.data.Model',
- fields: ['cidr', 'gateway', 'snat'],
- idProperty: 'subnet',
- });
- },
-);
-Ext.define(
- 'PVE.sdn.ZoneContentView',
- {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNZoneContentView',
-
- stateful: true,
- stateId: 'grid-sdnzone-content',
- viewConfig: {
- trackOver: false,
- loadMask: false,
- },
- features: [
- {
- ftype: 'grouping',
- groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
- },
- ],
-
- sub_panel: null,
-
- columns: [
- {
- header: gettext('VNet'),
- width: 100,
- sortable: true,
- dataIndex: 'vnet',
- },
- {
- header: gettext('Alias'),
- width: 300,
- sortable: true,
- dataIndex: 'alias',
- },
- {
- header: gettext('Status'),
- width: 100,
- sortable: true,
- dataIndex: 'status',
- },
- {
- header: gettext('Details'),
- flex: 1,
- dataIndex: 'statusmsg',
- },
- ],
-
- on_select: function (selectionModel, record) {
- // do nothing by default
- },
-
- on_deselect: function () {
- // do nothing by default
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.zone) {
- throw 'no zone ID specified';
- }
-
- var baseurl = '/nodes/' + me.nodename + '/sdn/zones/' + me.zone + '/content';
- if (me.zone === 'localnetwork') {
- baseurl = '/nodes/' + me.nodename + '/network?type=any_local_bridge';
- }
- var store = Ext.create('Ext.data.Store', {
- model: 'pve-sdnzone-content',
- groupField: 'content',
- proxy: {
- type: 'proxmox',
- url: '/api2/json' + baseurl,
- },
- sorters: {
- property: 'vnet',
- direction: 'ASC',
- },
- });
-
- var reload = function () {
- store.load();
- };
-
- Proxmox.Utils.monStoreErrors(me, store);
- Ext.apply(me, {
- store: store,
- listeners: {
- activate: reload,
- show: reload,
- select: me.on_select,
- deselect: me.on_deselect,
- },
- });
- store.load();
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-sdnzone-content', {
- extend: 'Ext.data.Model',
- fields: [
- {
- name: 'iface',
- convert: function (value, record) {
- //map local vmbr to vnet
- if (record.data.iface) {
- record.data.vnet = record.data.iface;
- }
- return value;
- },
- },
- {
- name: 'comments',
- convert: function (value, record) {
- //map local vmbr comments to vnet alias
- if (record.data.comments) {
- record.data.alias = record.data.comments;
- }
- return value;
- },
- },
- 'vnet',
- 'status',
- 'statusmsg',
- {
- name: 'text',
- convert: function (value, record) {
- // check for volid, because if you click on a grouping header,
- // it calls convert (but with an empty volid)
- if (value || record.data.vnet === null) {
- return value;
- }
- return PVE.Utils.format_sdnvnet_type(value, {}, record);
- },
- },
- ],
- idProperty: 'vnet',
- });
- },
-);
-Ext.define('PVE.sdn.ZoneContentPanel', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveSDNZoneContentPanel',
-
- title: 'VNet',
-
- onlineHelp: 'pvesdn_config_vnet',
-
- initComponent: function () {
- var me = this;
-
- var permissions_panel = Ext.createWidget('pveSDNVnetACLView', {
- title: gettext('VNet Permissions'),
- region: 'center',
- border: false,
- });
-
- var vnetview_panel = Ext.createWidget('pveSDNZoneContentView', {
- title: 'VNets',
- region: 'west',
- sub_panel: permissions_panel,
- nodename: me.nodename,
- zone: me.zone,
- width: '50%',
- border: false,
- split: true,
-
- on_select: function (_sm, rec) {
- let path = `/sdn/zones/${me.zone}/${rec.data.vnet}`;
- permissions_panel.setPath(path);
- },
-
- on_deselect: function () {
- permissions_panel.setPath(undefined);
- },
- });
-
- Ext.apply(me, {
- layout: 'border',
- items: [vnetview_panel, permissions_panel],
- listeners: {
- show: function () {
- permissions_panel.fireEvent('show', permissions_panel);
- },
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.ZoneBridgePanel', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveSDNZoneBridgePanel',
-
- title: gettext('Bridges'),
- onlineHelp: 'pvesdn_zone_plugin_evpn',
-
- stateful: true,
- stateId: 'grid-sdn-zone-bridges',
-
- initComponent: function () {
- var me = this;
- let nodename = me.nodename;
-
- var bridge_ports_panel = Ext.createWidget('pveSDNZoneBridgePortsPanel', {
- title: gettext('Bridge Ports'),
- region: 'center',
- border: false,
- });
-
- var vnetview_panel = Ext.createWidget('pveSDNZoneBridgeView', {
- title: gettext('VNets'),
- region: 'west',
- nodename: me.nodename,
- zone: me.zone,
-
- width: '50%',
- border: false,
- split: true,
-
- on_select: function (_sm, rec) {
- let deepCopy = structuredClone(rec.data.ports);
- bridge_ports_panel.setPorts(deepCopy, nodename);
- },
-
- on_deselect: function () {
- bridge_ports_panel.clearPorts();
- },
- });
-
- Ext.apply(me, {
- layout: 'border',
- items: [vnetview_panel, bridge_ports_panel],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('ZoneBridgePort', {
- extend: 'Ext.data.Model',
- fields: ['index', 'name', 'primary_vlan', 'vlans', 'vmid'],
-});
-
-Ext.define('PVE.sdn.ZoneBridgePortsPanel', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNZoneBridgePortsPanel',
-
- title: gettext('IP-VRF'),
- onlineHelp: 'pvesdn_zone_plugin_evpn',
-
- stateful: true,
- stateId: 'grid-sdn-zone-ports',
-
- columns: [
- {
- text: gettext('Name'),
- flex: 2,
- sortable: true,
- dataIndex: 'name',
- },
- {
- text: gettext('VMID'),
- flex: 1,
- sortable: true,
- dataIndex: 'vmid',
- },
- {
- text: gettext('Guest Network Device'),
- flex: 1,
- sortable: true,
- dataIndex: 'index',
- },
- {
- text: gettext('Primary VLAN'),
- flex: 1,
- sortable: true,
- dataIndex: 'primary_vlan',
- },
- {
- text: gettext('VLANs'),
- flex: 1,
- sortable: true,
- dataIndex: 'vlans',
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'ZoneBridge',
- sorters: [
- {
- property: 'vmid',
- direction: 'ASC',
- },
- {
- property: 'index',
- direction: 'ASC',
- },
- ],
- });
-
- Ext.apply(me, {
- store,
- });
-
- me.callParent();
- },
-
- setPorts: function (ports) {
- let me = this;
- me.getStore().setData(ports);
- },
-
- clearPorts: function (ports) {
- let me = this;
- me.getStore().removeAll();
- },
-});
-Ext.define('ZoneBridge', {
- extend: 'Ext.data.Model',
- fields: ['name', 'vlan_filtering', 'ports'],
-});
-
-Ext.define('PVE.sdn.ZoneBridgeView', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNZoneBridgeView',
-
- stateful: true,
- stateId: 'grid-sdnzone-bridges',
-
- viewConfig: {
- trackOver: false,
- loadMask: false,
- },
-
- columns: [
- {
- header: gettext('Bridge'),
- width: 100,
- sortable: true,
- dataIndex: 'name',
- flex: 1,
- },
- {
- header: gettext('VLAN-aware'),
- width: 300,
- sortable: true,
- dataIndex: 'vlan_filtering',
- flex: 1,
- renderer: function (value) {
- return value === 1 ? gettext('Yes') : gettext('No');
- },
- },
- ],
-
- on_select: function (selectionModel, record) {
- // do nothing by default
- },
-
- on_deselect: function () {
- // do nothing by default
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- if (!me.zone) {
- throw 'no zone ID specified';
- }
-
- let baseUrl = `/nodes/${me.nodename}/sdn/zones/${me.zone}/bridges`;
-
- let store = Ext.create('Ext.data.Store', {
- model: 'ZoneBridge',
- proxy: {
- type: 'proxmox',
- url: '/api2/json' + baseUrl,
- },
- sorters: {
- property: 'name',
- direction: 'ASC',
- },
- });
-
- let reload = function () {
- store.load();
- };
-
- Proxmox.Utils.monStoreErrors(me, store);
- Ext.apply(me, {
- store: store,
- listeners: {
- activate: reload,
- show: reload,
- select: me.on_select,
- deselect: me.on_deselect,
- },
- });
- store.load();
- me.callParent();
- },
-});
-Ext.define('IpVrfRoute', {
- extend: 'Ext.data.Model',
- fields: ['ip', 'metric', 'nexthops', 'protocol'],
-});
-
-Ext.define('PVE.sdn.EvpnZoneIpVrfPanel', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNEvpnZoneIpVrfPanel',
-
- title: gettext('IP-VRF'),
- onlineHelp: 'pvesdn_zone_plugin_evpn',
-
- stateful: true,
- stateId: 'grid-sdn-ip-vrf',
-
- columns: [
- {
- text: gettext('CIDR'),
- flex: 2,
- sortable: true,
- dataIndex: 'ip',
- },
- {
- text: gettext('Nexthop'),
- flex: 3,
- dataIndex: 'nexthops',
- renderer: (value) => {
- if (Ext.isArray(value)) {
- return value.join(' ');
- }
- return value || '';
- },
- },
- {
- text: gettext('Protocol'),
- flex: 1,
- sortable: true,
- dataIndex: 'protocol',
- },
- {
- text: gettext('Metric'),
- flex: 1,
- sortable: true,
- dataIndex: 'metric',
- },
- ],
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'IpVrfRoute',
- proxy: {
- type: 'proxmox',
- url: `/api2/json/nodes/${me.nodename}/sdn/zones/${me.zone}/ip-vrf`,
- reader: {
- type: 'json',
- rootProperty: 'data',
- },
- },
- sorters: [
- {
- property: 'ip',
- direction: 'ASC',
- },
- {
- property: 'nexthop',
- direction: 'ASC',
- },
- {
- property: 'metric',
- direction: 'ASC',
- },
- ],
- autoLoad: true,
- });
-
- Ext.apply(me, {
- store,
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.EvpnZoneMacVrfPanel', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveSDNEvpnZoneMacVrfPanel',
-
- title: 'MAC-VRFs',
- onlineHelp: 'pvesdn_zone_plugin_evpn',
-
- initComponent: function () {
- var me = this;
- let nodename = me.nodename;
-
- var mac_vrf_panel = Ext.createWidget('pveSDNEvpnZoneMacVrfGridPanel', {
- title: gettext('VNet MAC-VRF'),
- region: 'center',
- border: false,
- });
-
- var vnetview_panel = Ext.createWidget('pveSDNZoneContentView', {
- title: gettext('VNets'),
- region: 'west',
- sub_panel: mac_vrf_panel,
- nodename: me.nodename,
- zone: me.zone,
-
- width: '50%',
- border: false,
- split: true,
-
- on_select: function (_sm, rec) {
- mac_vrf_panel.setVnet(rec.data.vnet, nodename);
- },
-
- on_deselect: function () {
- mac_vrf_panel.clearVnet();
- },
- });
-
- Ext.apply(me, {
- layout: 'border',
- items: [vnetview_panel, mac_vrf_panel],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('MacVrfRoute', {
- extend: 'Ext.data.Model',
- fields: ['ip', 'metric', 'nexthops', 'protocol'],
-});
-
-Ext.define('PVE.sdn.EvpnZoneMacVrfGridPanel', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNEvpnZoneMacVrfGridPanel',
-
- title: gettext('MAC-VRF'),
-
- stateful: true,
- stateId: 'grid-sdn-mac-vrf',
-
- columns: [
- {
- text: gettext('IP'),
- flex: 1,
- sortable: true,
- dataIndex: 'ip',
- },
- {
- text: gettext('MAC-Address'),
- flex: 1,
- sortable: true,
- dataIndex: 'mac',
- },
- {
- text: gettext('Nexthop'),
- flex: 1,
- dataIndex: 'nexthop',
- },
- ],
-
- clearVnet: function () {
- let me = this;
-
- me.getStore().removeAll();
- },
-
- setVnet: function (vnet, node) {
- let me = this;
-
- let store = me.getStore();
-
- store.getProxy().setUrl(`/api2/json/nodes/${node}/sdn/vnets/${vnet}/mac-vrf`);
- store.load();
- },
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'MacVrfRoute',
- proxy: {
- type: 'proxmox',
- reader: {
- type: 'json',
- rootProperty: 'data',
- },
- },
- sorters: [
- {
- property: 'ip',
- direction: 'ASC',
- },
- {
- property: 'mac',
- direction: 'ASC',
- },
- {
- property: 'nexthop',
- direction: 'ASC',
- },
- ],
- });
-
- Ext.apply(me, {
- store,
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.FirewallPanel', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveSDNFirewall',
-
- title: 'VNet',
-
- onlineHelp: 'pvesdn_firewall_integration',
-
- initComponent: function () {
- let me = this;
-
- let tabPanel = Ext.create('Ext.TabPanel', {
- fullscreen: true,
- region: 'center',
- border: false,
- split: true,
- disabled: true,
- flex: 2,
- items: [
- {
- xtype: 'pveFirewallRules',
- title: gettext('Rules'),
- list_refs_url: '/cluster/firewall/refs',
- firewall_type: 'vnet',
- },
- {
- xtype: 'pveFirewallOptions',
- title: gettext('Options'),
- fwtype: 'vnet',
- },
- ],
- });
-
- let vnetPanel = Ext.createWidget('pveSDNFirewallVnetView', {
- title: 'VNets',
- region: 'west',
- border: false,
- split: true,
- forceFit: true,
- flex: 1,
- tabPanel,
- });
-
- Ext.apply(me, {
- layout: 'border',
- items: [vnetPanel, tabPanel],
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.FirewallVnetView', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNFirewallVnetView',
-
- stateful: true,
- stateId: 'grid-sdn-vnet-firewall',
-
- tabPanel: undefined,
-
- emptyText: gettext('No VNet configured.'),
-
- getRulesPanel: function () {
- let me = this;
- return me.tabPanel.items.getAt(0);
- },
-
- getOptionsPanel: function () {
- let me = this;
- return me.tabPanel.items.getAt(1);
- },
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pve-sdn-vnet',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/vnets',
- },
- sorters: {
- property: ['zone', 'vnet'],
- direction: 'ASC',
- },
- });
-
- let reload = () => store.load();
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- Ext.apply(me, {
- store: store,
- reloadStore: reload,
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- columns: [
- {
- header: 'ID',
- flex: 1,
- dataIndex: 'vnet',
- },
- {
- header: gettext('Zone'),
- flex: 1,
- dataIndex: 'zone',
- renderer: Ext.htmlEncode,
- },
- {
- header: gettext('Alias'),
- flex: 1,
- dataIndex: 'alias',
- renderer: Ext.htmlEncode,
- },
- ],
- listeners: {
- activate: reload,
- show: reload,
- select: function (_sm, rec) {
- me.tabPanel.setDisabled(false);
-
- me.getRulesPanel().setBaseUrl(`/cluster/sdn/vnets/${rec.id}/firewall/rules`);
- me.getOptionsPanel().setBaseUrl(
- `/cluster/sdn/vnets/${rec.id}/firewall/options`,
- );
- },
- },
- });
- store.load();
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.ZoneView', {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveSDNZoneView'],
-
- onlineHelp: 'pvesdn_config_zone',
- emptyText: gettext('No zone configured.'),
-
- stateful: true,
- stateId: 'grid-sdn-zone',
-
- createSDNEditWindow: function (type, sid) {
- let schema = PVE.Utils.sdnzoneSchema[type];
- if (!schema || !schema.ipanel) {
- throw 'no editor registered for zone type: ' + type;
- }
-
- Ext.create('PVE.sdn.zones.BaseEdit', {
- paneltype: 'PVE.sdn.zones.' + schema.ipanel,
- type: type,
- zone: sid,
- autoShow: true,
- listeners: {
- destroy: this.reloadStore,
- },
- });
- },
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pve-sdn-zone',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/zones?pending=1',
- },
- sorters: {
- property: 'zone',
- direction: 'ASC',
- },
- });
-
- let reload = function () {
- store.load();
- };
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- let type = rec.data.type,
- zone = rec.data.zone;
-
- me.createSDNEditWindow(type, zone);
- };
-
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/cluster/sdn/zones/',
- callback: reload,
- });
-
- let set_button_status = function () {
- var rec = me.selModel.getSelection()[0];
-
- if (!rec || rec.data.state === 'deleted') {
- edit_btn.disable();
- remove_btn.disable();
- }
- };
-
- // else we cannot dynamically generate the add menu handlers
- let addHandleGenerator = function (type) {
- return function () {
- me.createSDNEditWindow(type);
- };
- };
- let addMenuItems = [];
- for (const [type, zone] of Object.entries(PVE.Utils.sdnzoneSchema)) {
- if (zone.hideAdd) {
- continue;
- }
- addMenuItems.push({
- text: PVE.Utils.format_sdnzone_type(type),
- iconCls: 'fa fa-fw fa-' + zone.faIcon,
- handler: addHandleGenerator(type),
- });
- }
-
- Ext.apply(me, {
- store: store,
- reloadStore: reload,
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- tbar: [
- {
- text: gettext('Add'),
- menu: new Ext.menu.Menu({
- items: addMenuItems,
- }),
- },
- remove_btn,
- edit_btn,
- ],
- columns: [
- {
- header: 'ID',
- width: 100,
- dataIndex: 'zone',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'zone', 1);
- },
- },
- {
- header: gettext('Type'),
- width: 100,
- dataIndex: 'type',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'type', 1);
- },
- },
- {
- header: 'MTU',
- width: 50,
- dataIndex: 'mtu',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'mtu');
- },
- },
- {
- header: 'IPAM',
- flex: 3,
- dataIndex: 'ipam',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'ipam');
- },
- },
- {
- header: gettext('Domain'),
- flex: 3,
- dataIndex: 'dnszone',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'dnszone');
- },
- },
- {
- header: gettext('DNS'),
- flex: 3,
- dataIndex: 'dns',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'dns');
- },
- },
- {
- header: gettext('Reverse DNS'),
- flex: 3,
- dataIndex: 'reversedns',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'reversedns');
- },
- },
- {
- header: gettext('Nodes'),
- flex: 3,
- dataIndex: 'nodes',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending(rec, value, 'nodes');
- },
- },
- {
- header: gettext('State'),
- width: 100,
- dataIndex: 'state',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending_state(rec, value);
- },
- },
- ],
- listeners: {
- activate: reload,
- itemdblclick: run_editor,
- selectionchange: set_button_status,
- },
- });
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.IpamEditInputPanel', {
- extend: 'Proxmox.panel.InputPanel',
- mixins: ['Proxmox.Mixin.CBind'],
-
- isCreate: false,
-
- onGetValues: function (values) {
- let _me = this;
-
- if (!values.vmid) {
- delete values.vmid;
- }
-
- return values;
- },
-
- items: [
- {
- xtype: 'pmxDisplayEditField',
- name: 'vmid',
- fieldLabel: 'VMID',
- allowBlank: false,
- editable: false,
- cbind: {
- hidden: '{isCreate}',
- },
- },
- {
- xtype: 'pmxDisplayEditField',
- name: 'mac',
- fieldLabel: 'MAC',
- allowBlank: false,
- cbind: {
- editable: '{isCreate}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'ip',
- fieldLabel: gettext('IP Address'),
- allowBlank: false,
- },
- ],
-});
-
-Ext.define('PVE.sdn.IpamEdit', {
- extend: 'Proxmox.window.Edit',
-
- subject: gettext('DHCP Mapping'),
- width: 350,
-
- isCreate: false,
- mapping: {},
-
- url: '/cluster/sdn/vnets',
-
- submitUrl: function (url, values) {
- return `${url}/${values.vnet}/ips`;
- },
-
- initComponent: function () {
- var me = this;
-
- me.method = me.isCreate ? 'POST' : 'PUT';
-
- let ipanel = Ext.create('PVE.sdn.IpamEditInputPanel', {
- isCreate: me.isCreate,
- });
-
- Ext.apply(me, {
- items: [ipanel],
- });
-
- me.callParent();
-
- ipanel.setValues(me.mapping);
- },
-});
-Ext.define('PVE.sdn.Options', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveSDNOptions',
-
- title: 'Options',
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- onlineHelp: 'pvesdn_config_controllers',
-
- items: [
- {
- xtype: 'pveSDNControllerView',
- title: gettext('Controllers'),
- flex: 1,
- padding: '0 0 20 0',
- border: 0,
- },
- {
- xtype: 'pveSDNIpamView',
- title: 'IPAM',
- flex: 1,
- padding: '0 0 20 0',
- border: 0,
- },
- {
- xtype: 'pveSDNDnsView',
- title: 'DNS',
- flex: 1,
- border: 0,
- },
- ],
-});
-Ext.define('PVE.panel.SDNControllerBase', {
- extend: 'Proxmox.panel.InputPanel',
-
- type: '',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- delete values.delete;
- } else {
- delete values.controller;
-
- for (const [key, value] of Object.entries(values)) {
- if (value === null || value === undefined || value === '') {
- delete values[key];
-
- if (values.delete) {
- if (Array.isArray(values.delete)) {
- values.delete.push(key);
- } else {
- values.delete = [values.delete, key];
- }
- } else {
- values.delete = [key];
- }
- }
- }
- }
-
- return values;
- },
-});
-
-Ext.define('PVE.sdn.controllers.BaseEdit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.controllerid;
-
- if (me.isCreate) {
- me.url = '/api2/extjs/cluster/sdn/controllers';
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs/cluster/sdn/controllers/' + me.controllerid;
- me.method = 'PUT';
- }
-
- var ipanel = Ext.create(me.paneltype, {
- type: me.type,
- isCreate: me.isCreate,
- controllerid: me.controllerid,
- });
-
- Ext.apply(me, {
- subject: PVE.Utils.format_sdncontroller_type(me.type),
- isAdd: true,
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
- var ctypes = values.content || '';
-
- values.content = ctypes.split(',');
-
- if (values.nodes) {
- values.nodes = values.nodes.split(',');
- }
- values.enable = values.disable ? 0 : 1;
-
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-Ext.define('PVE.sdn.controllers.EvpnInputPanel', {
- extend: 'PVE.panel.SDNControllerBase',
-
- onlineHelp: 'pvesdn_controller_plugin_evpn',
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'controller',
- maxLength: 8,
- value: me.controllerid || '',
- fieldLabel: 'ID',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'asn',
- minValue: 1,
- maxValue: 4294967295,
- value: 65000,
- fieldLabel: 'ASN #',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxNetworkSelector',
- name: 'fabric',
- type: 'fabric',
- valueField: 'iface',
- displayField: 'iface',
- fieldLabel: 'SDN Fabric',
- allowBlank: true,
- deleteEmpty: true,
- skipEmptyText: true,
- autoSelect: false,
- emptyText: gettext('used as underlay network'),
- nodename: 'localhost',
- listConfig: {
- width: 600,
- columns: [
- {
- header: gettext('Fabric'),
- width: 90,
- dataIndex: 'iface',
- },
- {
- header: gettext('CIDR'),
- dataIndex: 'cidr',
- hideable: false,
- flex: 1,
- },
- ],
- },
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'peers',
- fieldLabel: gettext('Peers'),
- allowBlank: true,
- deleteEmpty: true,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.controllers.BgpInputPanel', {
- extend: 'PVE.panel.SDNControllerBase',
-
- onlineHelp: 'pvesdn_controller_plugin_evpn',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- values.controller = 'bgp' + values.node;
- } else {
- delete values.controller;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: 'pveNodeSelector',
- name: 'node',
- fieldLabel: gettext('Node'),
- multiSelect: false,
- autoSelect: false,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'asn',
- minValue: 1,
- maxValue: 4294967295,
- value: 65000,
- fieldLabel: 'ASN #',
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'peers',
- fieldLabel: gettext('Peers'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'ebgp',
- uncheckedValue: 0,
- checked: false,
- fieldLabel: 'EBGP',
- },
- ];
-
- me.advancedItems = [
- {
- xtype: 'textfield',
- name: 'loopback',
- fieldLabel: gettext('Loopback Interface'),
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'ebgp-multihop',
- minValue: 1,
- maxValue: 100,
- fieldLabel: 'ebgp-multihop',
- allowBlank: true,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'bgp-multipath-as-path-relax',
- uncheckedValue: 0,
- checked: false,
- fieldLabel: 'bgp-multipath-as-path-relax',
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.controllers.IsisInputPanel', {
- extend: 'PVE.panel.SDNControllerBase',
-
- onlineHelp: 'pvesdn_controller_plugin_evpn',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- values.controller = 'isis' + values.node;
- } else {
- delete values.controller;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: 'pveNodeSelector',
- name: 'node',
- fieldLabel: gettext('Node'),
- multiSelect: false,
- autoSelect: false,
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'isis-domain',
- fieldLabel: 'Domain',
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'isis-net',
- fieldLabel: 'Network entity title',
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'isis-ifaces',
- fieldLabel: gettext('Interfaces'),
- allowBlank: false,
- },
- ];
-
- me.advancedItems = [
- {
- xtype: 'textfield',
- name: 'loopback',
- fieldLabel: gettext('Loopback Interface'),
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.IpamView', {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveSDNIpamView'],
-
- stateful: true,
- stateId: 'grid-sdn-ipam',
-
- createSDNEditWindow: function (type, sid) {
- let schema = PVE.Utils.sdnipamSchema[type];
- if (!schema || !schema.ipanel) {
- throw 'no editor registered for ipam type: ' + type;
- }
-
- Ext.create('PVE.sdn.ipams.BaseEdit', {
- paneltype: 'PVE.sdn.ipams.' + schema.ipanel,
- type: type,
- ipam: sid,
- autoShow: true,
- listeners: {
- destroy: this.reloadStore,
- },
- });
- },
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pve-sdn-ipam',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/ipams',
- },
- sorters: {
- property: 'ipam',
- direction: 'ASC',
- },
- });
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- let type = rec.data.type,
- ipam = rec.data.ipam;
- me.createSDNEditWindow(type, ipam);
- };
-
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/cluster/sdn/ipams/',
- callback: () => store.load(),
- });
-
- // else we cannot dynamically generate the add menu handlers
- let addHandleGenerator = function (type) {
- return function () {
- me.createSDNEditWindow(type);
- };
- };
- let addMenuItems = [];
- for (const [type, ipam] of Object.entries(PVE.Utils.sdnipamSchema)) {
- if (ipam.hideAdd) {
- continue;
- }
- addMenuItems.push({
- text: PVE.Utils.format_sdnipam_type(type),
- iconCls: 'fa fa-fw fa-' + ipam.faIcon,
- handler: addHandleGenerator(type),
- });
- }
-
- Ext.apply(me, {
- store: store,
- reloadStore: () => store.load(),
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- tbar: [
- {
- text: gettext('Add'),
- menu: new Ext.menu.Menu({
- items: addMenuItems,
- }),
- },
- remove_btn,
- edit_btn,
- ],
- columns: [
- {
- header: 'ID',
- flex: 2,
- dataIndex: 'ipam',
- renderer: Ext.htmlEncode,
- },
- {
- header: gettext('Type'),
- flex: 1,
- dataIndex: 'type',
- renderer: PVE.Utils.format_sdnipam_type,
- },
- {
- header: 'url',
- flex: 1,
- dataIndex: 'url',
- renderer: Ext.htmlEncode,
- },
- ],
- listeners: {
- activate: () => store.load(),
- itemdblclick: run_editor,
- },
- });
-
- store.load();
- me.callParent();
- },
-});
-Ext.define('PVE.panel.SDNIpamBase', {
- extend: 'Proxmox.panel.InputPanel',
-
- type: '',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.ipam;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.sdn.ipams.BaseEdit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.ipam;
-
- if (me.isCreate) {
- me.url = '/api2/extjs/cluster/sdn/ipams';
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs/cluster/sdn/ipams/' + me.ipam;
- me.method = 'PUT';
- }
-
- var ipanel = Ext.create(me.paneltype, {
- type: me.type,
- isCreate: me.isCreate,
- ipam: me.ipam,
- });
-
- Ext.apply(me, {
- subject: PVE.Utils.format_sdnipam_type(me.type),
- isAdd: true,
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
- var ctypes = values.content || '';
-
- values.content = ctypes.split(',');
-
- if (values.nodes) {
- values.nodes = values.nodes.split(',');
- }
- values.enable = values.disable ? 0 : 1;
-
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-Ext.define('PVE.sdn.ipams.NetboxInputPanel', {
- extend: 'PVE.panel.SDNIpamBase',
-
- onlineHelp: 'pvesdn_ipam_plugin_netbox',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.ipam;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'ipam',
- maxLength: 10,
- value: me.zone || '',
- fieldLabel: 'ID',
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'token',
- fieldLabel: gettext('Token'),
- allowBlank: false,
- },
- ];
-
- me.column2 = [
- {
- xtype: 'textfield',
- name: 'url',
- fieldLabel: gettext('URL'),
- allowBlank: false,
- },
- ];
-
- me.columnB = [
- {
- xtype: 'pmxFingerprintField',
- name: 'fingerprint',
- value: me.isCreate ? null : undefined,
- deleteEmpty: !me.isCreate,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.ipams.PVEIpamInputPanel', {
- extend: 'PVE.panel.SDNIpamBase',
-
- onlineHelp: 'pvesdn_ipam_plugin_pveipam',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.ipam;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'ipam',
- maxLength: 10,
- value: me.zone || '',
- fieldLabel: 'ID',
- allowBlank: false,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.ipams.PhpIpamInputPanel', {
- extend: 'PVE.panel.SDNIpamBase',
-
- onlineHelp: 'pvesdn_ipam_plugin_phpipam',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.ipam;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'ipam',
- maxLength: 10,
- value: me.zone || '',
- fieldLabel: 'ID',
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'token',
- fieldLabel: gettext('Token'),
- allowBlank: false,
- },
- ];
- me.column2 = [
- {
- xtype: 'textfield',
- name: 'url',
- fieldLabel: gettext('URL'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'section',
- fieldLabel: gettext('Section'),
- allowBlank: false,
- },
- ];
-
- me.columnB = [
- {
- xtype: 'pmxFingerprintField',
- name: 'fingerprint',
- value: me.isCreate ? null : undefined,
- deleteEmpty: !me.isCreate,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.DnsView', {
- extend: 'Ext.grid.GridPanel',
- alias: ['widget.pveSDNDnsView'],
-
- stateful: true,
- stateId: 'grid-sdn-dns',
-
- createSDNEditWindow: function (type, sid) {
- let schema = PVE.Utils.sdndnsSchema[type];
- if (!schema || !schema.ipanel) {
- throw 'no editor registered for dns type: ' + type;
- }
-
- Ext.create('PVE.sdn.dns.BaseEdit', {
- paneltype: 'PVE.sdn.dns.' + schema.ipanel,
- type: type,
- dns: sid,
- autoShow: true,
- listeners: {
- destroy: this.reloadStore,
- },
- });
- },
-
- initComponent: function () {
- let me = this;
-
- let store = new Ext.data.Store({
- model: 'pve-sdn-dns',
- proxy: {
- type: 'proxmox',
- url: '/api2/json/cluster/sdn/dns',
- },
- sorters: {
- property: 'dns',
- direction: 'ASC',
- },
- });
-
- let sm = Ext.create('Ext.selection.RowModel', {});
-
- let run_editor = function () {
- let rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
- let type = rec.data.type,
- dns = rec.data.dns;
-
- me.createSDNEditWindow(type, dns);
- };
-
- let edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor,
- });
-
- let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
- selModel: sm,
- baseurl: '/cluster/sdn/dns/',
- callback: () => store.load(),
- });
-
- // else we cannot dynamically generate the add menu handlers
- let addHandleGenerator = function (type) {
- return function () {
- me.createSDNEditWindow(type);
- };
- };
- let addMenuItems = [];
- for (const [type, dns] of Object.entries(PVE.Utils.sdndnsSchema)) {
- if (dns.hideAdd) {
- continue;
- }
- addMenuItems.push({
- text: PVE.Utils.format_sdndns_type(type),
- iconCls: 'fa fa-fw fa-' + dns.faIcon,
- handler: addHandleGenerator(type),
- });
- }
-
- Ext.apply(me, {
- store: store,
- reloadStore: () => store.load(),
- selModel: sm,
- viewConfig: {
- trackOver: false,
- },
- tbar: [
- {
- text: gettext('Add'),
- menu: new Ext.menu.Menu({
- items: addMenuItems,
- }),
- },
- remove_btn,
- edit_btn,
- ],
- columns: [
- {
- header: 'ID',
- flex: 2,
- dataIndex: 'dns',
- renderer: Ext.htmlEncode,
- },
- {
- header: gettext('Type'),
- flex: 1,
- dataIndex: 'type',
- renderer: PVE.Utils.format_sdndns_type,
- },
- {
- header: 'url',
- flex: 1,
- dataIndex: 'url',
- renderer: Ext.htmlEncode,
- },
- ],
- listeners: {
- activate: () => store.load(),
- itemdblclick: run_editor,
- },
- });
-
- store.load();
- me.callParent();
- },
-});
-Ext.define('PVE.panel.SDNDnsBase', {
- extend: 'Proxmox.panel.InputPanel',
-
- type: '',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.dns;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.sdn.dns.BaseEdit', {
- extend: 'Proxmox.window.Edit',
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.dns;
-
- if (me.isCreate) {
- me.url = '/api2/extjs/cluster/sdn/dns';
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs/cluster/sdn/dns/' + me.dns;
- me.method = 'PUT';
- }
-
- var ipanel = Ext.create(me.paneltype, {
- type: me.type,
- isCreate: me.isCreate,
- dns: me.dns,
- });
-
- Ext.apply(me, {
- subject: PVE.Utils.format_sdndns_type(me.type),
- isAdd: true,
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
- var ctypes = values.content || '';
-
- values.content = ctypes.split(',');
-
- if (values.nodes) {
- values.nodes = values.nodes.split(',');
- }
- values.enable = values.disable ? 0 : 1;
-
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-Ext.define('PVE.sdn.dns.PowerdnsInputPanel', {
- extend: 'PVE.panel.SDNDnsBase',
-
- onlineHelp: 'pvesdn_dns_plugin_powerdns',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.dns;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'dns',
- maxLength: 10,
- value: me.dns || '',
- fieldLabel: 'ID',
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'key',
- fieldLabel: gettext('API Key'),
- allowBlank: false,
- },
- ];
- me.column2 = [
- {
- xtype: 'textfield',
- name: 'url',
- fieldLabel: 'URL',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'ttl',
- fieldLabel: 'TTL',
- allowBlank: true,
- },
- ];
- me.columnB = [
- {
- xtype: 'pmxFingerprintField',
- name: 'fingerprint',
- value: me.isCreate ? null : undefined,
- deleteEmpty: !me.isCreate,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.panel.SDNZoneBase', {
- extend: 'Proxmox.panel.InputPanel',
-
- type: '',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.zone;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items.unshift({
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'zone',
- maxLength: 8,
- value: me.zone || '',
- fieldLabel: 'ID',
- allowBlank: false,
- });
-
- me.items.push(
- {
- xtype: 'proxmoxintegerfield',
- name: 'mtu',
- minValue: 100,
- maxValue: 65000,
- fieldLabel: 'MTU',
- allowBlank: true,
- emptyText: 'auto',
- deleteEmpty: !me.isCreate,
- },
- {
- xtype: 'pveNodeSelector',
- name: 'nodes',
- fieldLabel: gettext('Nodes'),
- emptyText: gettext('All') + ' (' + gettext('No restrictions') + ')',
- multiSelect: true,
- autoSelect: false,
- },
- {
- xtype: 'pveSDNIpamSelector',
- fieldLabel: gettext('IPAM'),
- name: 'ipam',
- value: me.ipam || 'pve',
- allowBlank: false,
- },
- );
-
- me.advancedItems = me.advancedItems ?? [];
-
- me.advancedItems.unshift(
- {
- xtype: 'pveSDNDnsSelector',
- fieldLabel: gettext('DNS Server'),
- name: 'dns',
- value: '',
- allowBlank: true,
- },
- {
- xtype: 'pveSDNDnsSelector',
- fieldLabel: gettext('Reverse DNS Server'),
- name: 'reversedns',
- value: '',
- allowBlank: true,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'dnszone',
- skipEmptyText: true,
- fieldLabel: gettext('DNS Zone'),
- allowBlank: true,
- deleteEmpty: !me.isCreate,
- },
- );
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.sdn.zones.BaseEdit', {
- extend: 'Proxmox.window.Edit',
-
- width: 400,
-
- initComponent: function () {
- var me = this;
-
- me.isCreate = !me.zone;
-
- if (me.isCreate) {
- me.url = '/api2/extjs/cluster/sdn/zones';
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs/cluster/sdn/zones/' + me.zone;
- me.method = 'PUT';
- }
-
- var ipanel = Ext.create(me.paneltype, {
- type: me.type,
- isCreate: me.isCreate,
- zone: me.zone,
- });
-
- Ext.apply(me, {
- subject: PVE.Utils.format_sdnzone_type(me.type),
- isAdd: true,
- items: [ipanel],
- });
-
- me.callParent();
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- var values = response.result.data;
- var ctypes = values.content || '';
-
- values.content = ctypes.split(',');
-
- if (values.nodes) {
- values.nodes = values.nodes.split(',');
- }
-
- if (values.exitnodes) {
- values.exitnodes = values.exitnodes.split(',');
- }
-
- values.enable = values.disable ? 0 : 1;
-
- ipanel.setValues(values);
- },
- });
- }
- },
-});
-Ext.define('PVE.sdn.zones.EvpnInputPanel', {
- extend: 'PVE.panel.SDNZoneBase',
-
- onlineHelp: 'pvesdn_zone_plugin_evpn',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: 'pveSDNControllerSelector',
- fieldLabel: gettext('Controller'),
- name: 'controller',
- value: '',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'vrf-vxlan',
- minValue: 1,
- maxValue: 16000000,
- fieldLabel: 'VRF-VXLAN Tag',
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'mac',
- fieldLabel: gettext('VNet MAC Address'),
- vtype: 'MacAddress',
- allowBlank: true,
- emptyText: 'auto',
- deleteEmpty: !me.isCreate,
- },
- {
- xtype: 'pveNodeSelector',
- name: 'exitnodes',
- fieldLabel: gettext('Exit Nodes'),
- multiSelect: true,
- autoSelect: false,
- },
- {
- xtype: 'pveNodeSelector',
- name: 'exitnodes-primary',
- fieldLabel: gettext('Primary Exit Node'),
- multiSelect: false,
- autoSelect: false,
- skipEmptyText: true,
- deleteEmpty: !me.isCreate,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'exitnodes-local-routing',
- uncheckedValue: null,
- checked: false,
- fieldLabel: gettext('Exit Nodes Local Routing'),
- deleteEmpty: !me.isCreate,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'advertise-subnets',
- uncheckedValue: null,
- checked: false,
- fieldLabel: gettext('Advertise Subnets'),
- deleteEmpty: !me.isCreate,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'disable-arp-nd-suppression',
- uncheckedValue: null,
- checked: false,
- fieldLabel: gettext('Disable ARP-nd Suppression'),
- deleteEmpty: !me.isCreate,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'rt-import',
- fieldLabel: gettext('Route Target Import'),
- allowBlank: true,
- deleteEmpty: !me.isCreate,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.zones.QinQInputPanel', {
- extend: 'PVE.panel.SDNZoneBase',
-
- onlineHelp: 'pvesdn_zone_plugin_qinq',
-
- onGetValues: function (values) {
- let me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.sdn;
- }
-
- return values;
- },
-
- initComponent: function () {
- let me = this;
-
- me.items = [
- {
- xtype: 'textfield',
- name: 'bridge',
- fieldLabel: 'Bridge',
- allowBlank: false,
- vtype: 'BridgeName',
- minLength: 1,
- maxLength: 10,
- },
- {
- xtype: 'proxmoxintegerfield',
- name: 'tag',
- minValue: 0,
- maxValue: 4096,
- fieldLabel: gettext('Service VLAN'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'vlan-protocol',
- fieldLabel: gettext('Service VLAN Protocol'),
- allowBlank: true,
- value: '802.1q',
- comboItems: [
- ['802.1q', '802.1q'],
- ['802.1ad', '802.1ad'],
- ],
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.zones.SimpleInputPanel', {
- extend: 'PVE.panel.SDNZoneBase',
-
- onlineHelp: 'pvesdn_zone_plugin_simple',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.zone;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [];
- me.advancedItems = [
- {
- xtype: 'proxmoxcheckbox',
- name: 'dhcp',
- inputValue: 'dnsmasq',
- uncheckedValue: null,
- checked: false,
- fieldLabel: gettext('automatic DHCP'),
- deleteEmpty: !me.isCreate,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.zones.VlanInputPanel', {
- extend: 'PVE.panel.SDNZoneBase',
-
- onlineHelp: 'pvesdn_zone_plugin_vlan',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.zone;
- }
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: 'textfield',
- name: 'bridge',
- fieldLabel: 'Bridge',
- allowBlank: false,
- vtype: 'BridgeName',
- minLength: 1,
- maxLength: 10,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.zones.VxlanInputPanel', {
- extend: 'PVE.panel.SDNZoneBase',
-
- onlineHelp: 'pvesdn_zone_plugin_vxlan',
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- delete values.delete;
- } else {
- delete values.zone;
-
- for (const [key, value] of Object.entries(values)) {
- if (value === null || value === undefined || value === '') {
- delete values[key];
-
- if (values.delete) {
- if (Array.isArray(values.delete)) {
- values.delete.push(key);
- } else {
- values.delete = [values.delete, key];
- }
- } else {
- values.delete = [key];
- }
- }
- }
- }
-
- delete values.mode;
-
- return values;
- },
-
- initComponent: function () {
- var me = this;
-
- me.items = [
- {
- xtype: 'proxmoxtextfield',
- name: 'peers',
- fieldLabel: gettext('Peer Address List'),
- allowBlank: true,
- deleteEmpty: true,
- },
- {
- xtype: 'proxmoxNetworkSelector',
- name: 'fabric',
- type: 'fabric',
- valueField: 'iface',
- displayField: 'iface',
- fieldLabel: 'SDN Fabric',
- skipEmptyText: true,
- allowBlank: true,
- deleteEmpty: true,
- autoSelect: false,
- emptyText: gettext('used as underlay network'),
- nodename: 'localhost',
- listConfig: {
- width: 600,
- columns: [
- {
- // TRANSLATORS: As in "Network Fabric": https://en.wikipedia.org/wiki/Switched_fabric
- header: gettext('Fabric'),
- width: 90,
- dataIndex: 'iface',
- },
- {
- header: gettext('CIDR'),
- dataIndex: 'cidr',
- hideable: false,
- flex: 1,
- },
- ],
- },
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.Fabric.TreeModel', {
- extend: 'Ext.data.TreeModel',
- idProperty: 'tree_id',
-});
-
-Ext.define('PVE.sdn.Fabric.View', {
- extend: 'Ext.tree.Panel',
-
- xtype: 'pveSDNFabricView',
-
- onlineHelp: 'pvesdn_config_fabrics',
-
- columns: [
- {
- xtype: 'treecolumn',
- text: gettext('Name'),
- dataIndex: 'node_id',
- width: 200,
- renderer: function (value, metaData, rec) {
- if (rec.data.type === 'fabric') {
- return PVE.Utils.render_sdn_pending(rec, rec.data.id, 'id');
- }
-
- return PVE.Utils.render_sdn_pending(rec, value, 'node_id');
- },
- },
- {
- text: gettext('Protocol'),
- dataIndex: 'protocol',
- width: 100,
- renderer: function (value, metaData, rec) {
- if (rec.data.type === 'fabric') {
- const PROTOCOL_DISPLAY_NAMES = {
- openfabric: 'OpenFabric',
- ospf: 'OSPF',
- };
- const displayValue = PROTOCOL_DISPLAY_NAMES[value];
- if (rec.data.state === undefined || rec.data.state === null) {
- return Ext.htmlEncode(displayValue);
- }
- if (rec.data.state === 'deleted') {
- if (value === undefined) {
- return ' ';
- } else {
- let encoded = Ext.htmlEncode(displayValue);
- return `${encoded} `;
- }
- }
- return Ext.htmlEncode(displayValue);
- }
-
- return '';
- },
- },
- {
- text: gettext('IPv4'),
- dataIndex: 'ip',
- width: 150,
- renderer: function (value, metaData, rec) {
- if (rec.data.type === 'fabric') {
- return PVE.Utils.render_sdn_pending(rec, rec.data.ip_prefix, 'ip_prefix');
- }
-
- return PVE.Utils.render_sdn_pending(rec, value, 'ip');
- },
- },
- {
- text: gettext('IPv6'),
- dataIndex: 'ip6',
- width: 150,
- renderer: function (value, metaData, rec) {
- if (rec.data.type === 'fabric') {
- return PVE.Utils.render_sdn_pending(rec, rec.data.ip6_prefix, 'ip6_prefix');
- }
-
- return PVE.Utils.render_sdn_pending(rec, value, 'ip6');
- },
- },
- {
- header: gettext('Interfaces'),
- width: 200,
- dataIndex: 'interface',
- renderer: function (value, metaData, rec) {
- const interfaces = rec.data.pending?.interfaces || rec.data.interfaces || [];
-
- let names = interfaces.map((iface) => {
- const properties = Proxmox.Utils.parsePropertyString(iface);
- return properties.name;
- });
-
- names.sort();
- const displayValue = Ext.htmlEncode(names.join(', '));
- if (rec.data.state === 'deleted') {
- return `${displayValue} `;
- }
- return displayValue;
- },
- },
- {
- text: gettext('Action'),
- xtype: 'actioncolumn',
- dataIndex: 'text',
- width: 100,
- items: [
- {
- handler: 'addActionTreeColumn',
- getTip: (_v, _m, _rec) => gettext('Add Node'),
- getClass: (_v, _m, { data }) => {
- if (data.type === 'fabric') {
- return 'fa fa-plus-circle';
- }
-
- return 'pmx-hidden';
- },
- isActionDisabled: (_v, _r, _c, _i, { data }) => data.type !== 'fabric',
- },
- {
- tooltip: gettext('Edit'),
- handler: 'editAction',
- getClass: (_v, _m, { data }) => {
- // the fabric type (openfabric, ospf, etc.) cannot be edited
- if (data.type && data.state !== 'deleted') {
- return 'fa fa-pencil fa-fw';
- }
-
- return 'pmx-hidden';
- },
- isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type,
- },
- {
- tooltip: gettext('Delete'),
- handler: 'deleteAction',
- getClass: (_v, _m, { data }) => {
- // the fabric type (openfabric, ospf, etc.) cannot be deleted
- if (data.type && data.state !== 'deleted') {
- return 'fa critical fa-trash-o';
- }
-
- return 'pmx-hidden';
- },
- isActionDisabled: (_v, _r, _c, _i, { data }) => !data.type,
- },
- ],
- },
- {
- header: gettext('State'),
- width: 100,
- dataIndex: 'state',
- renderer: function (value, metaData, rec) {
- return PVE.Utils.render_sdn_pending_state(rec, value);
- },
- },
- ],
-
- store: {
- sorters: ['tree_id'],
- model: 'PVE.sdn.Fabric.TreeModel',
- },
-
- layout: 'fit',
- rootVisible: false,
- animate: false,
-
- initComponent: function () {
- let me = this;
-
- let addNodeButton = new Proxmox.button.Button({
- text: gettext('Add Node'),
- handler: 'addActionTbar',
- disabled: true,
- });
-
- let setAddNodeButtonStatus = function () {
- let selection = me.view.getSelection();
-
- if (selection.length === 0) {
- return;
- }
-
- let enabled = selection[0].data.type === 'fabric';
- addNodeButton.setDisabled(!enabled);
- };
-
- Ext.apply(me, {
- tbar: [
- {
- text: gettext('Add Fabric'),
- menu: [
- {
- text: 'OpenFabric',
- handler: 'addOpenfabric',
- },
- {
- text: 'OSPF',
- handler: 'addOspf',
- },
- ],
- },
- addNodeButton,
- {
- xtype: 'proxmoxButton',
- text: gettext('Reload'),
- handler: function () {
- const view = this.up('pveSDNFabricView');
- view.getController().reload();
- },
- },
- ],
- listeners: {
- selectionchange: setAddNodeButtonStatus,
- },
- });
-
- me.callParent();
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- reload: function (successCallback) {
- let me = this;
-
- Proxmox.Utils.API2Request({
- url: `/cluster/sdn/fabrics/all?pending=1`,
- method: 'GET',
- success: function (response, opts) {
- let fabrics = {};
-
- for (const fabric of response.result.data.fabrics) {
- let mergedFabric = {
- expanded: true,
- type: 'fabric',
- iconCls: 'fa fa-road x-fa-treepanel',
- children: [],
- ...fabric,
- ...fabric.pending,
- };
-
- mergedFabric.tree_id = mergedFabric.id;
-
- fabrics[mergedFabric.id] = mergedFabric;
- }
-
- for (const node of response.result.data.nodes) {
- let mergedNode = {
- type: 'node',
- iconCls: 'fa fa-desktop x-fa-treepanel',
- leaf: true,
- ...node,
- ...node.pending,
- };
-
- mergedNode.tree_id = `${mergedNode.fabric_id}_${mergedNode.node_id}`;
-
- fabrics[mergedNode.fabric_id].children.push(mergedNode);
- }
-
- me.getView().setRootNode({
- name: '__root',
- expanded: true,
- children: Object.values(fabrics),
- });
-
- if (successCallback) {
- successCallback();
- }
- },
- });
- },
-
- getFabricEditPanel: function (protocol) {
- const FABRIC_PANELS = {
- openfabric: 'PVE.sdn.Fabric.OpenFabric.Fabric.Edit',
- ospf: 'PVE.sdn.Fabric.Ospf.Fabric.Edit',
- };
-
- return FABRIC_PANELS[protocol];
- },
-
- getNodeEditPanel: function (protocol) {
- const NODE_PANELS = {
- openfabric: 'PVE.sdn.Fabric.OpenFabric.Node.Edit',
- ospf: 'PVE.sdn.Fabric.Ospf.Node.Edit',
- };
-
- return NODE_PANELS[protocol];
- },
-
- addOpenfabric: function () {
- let me = this;
- me.openFabricAddWindow('openfabric');
- },
-
- addOspf: function () {
- let me = this;
- me.openFabricAddWindow('ospf');
- },
-
- openFabricAddWindow: function (protocol) {
- let me = this;
-
- let component = me.getFabricEditPanel(protocol);
-
- let window = Ext.create(component, {
- autoShow: true,
- autoLoad: false,
- isCreate: true,
- });
-
- window.on('destroy', () => me.reload());
- },
-
- addActionTreeColumn: function (_grid, _rI, _cI, _item, _e, rec) {
- this.openNodeAddWindow(rec.data);
- },
-
- addActionTbar: function () {
- let me = this;
-
- let selection = me.view.getSelection();
-
- if (selection.length === 0) {
- return;
- }
-
- if (selection[0].data.type === 'fabric') {
- me.openNodeAddWindow(selection[0].data);
- }
- },
-
- openNodeAddWindow: function (fabric) {
- let me = this;
-
- let component = me.getNodeEditPanel(fabric.protocol);
-
- let disallowedNodes = fabric.children
- .filter((node) => !node.state || node.state !== 'deleted')
- .map((node) => node.node_id);
-
- Ext.create(component, {
- autoShow: true,
- fabricId: fabric.id,
- protocol: fabric.protocol,
- disallowedNodes,
- addAnotherCallback: () => {
- let successCallback = () => {
- let new_fabric = me
- .getView()
- .getStore()
- .findRecord('tree_id', fabric.tree_id);
-
- me.openNodeAddWindow(new_fabric.data);
- };
-
- me.reload(successCallback);
- },
- apiCallDone: (success, _response, _options) => {
- if (success) {
- me.reload();
- }
- },
- });
- },
-
- openFabricEditWindow: function (fabric) {
- let me = this;
-
- let component = me.getFabricEditPanel(fabric.protocol);
-
- let window = Ext.create(component, {
- autoShow: true,
- fabricId: fabric.id,
- });
-
- window.on('destroy', () => me.reload());
- },
-
- openNodeEditWindow: function (node) {
- let me = this;
-
- let component = me.getNodeEditPanel(node.protocol);
-
- let window = Ext.create(component, {
- autoShow: true,
- fabricId: node.fabric_id,
- nodeId: node.node_id,
- protocol: node.protocol,
- });
-
- window.on('destroy', () => me.reload());
- },
-
- editAction: function (_grid, _rI, _cI, _item, _e, rec) {
- let me = this;
-
- if (rec.data.type === 'fabric') {
- me.openFabricEditWindow(rec.data);
- } else if (rec.data.type === 'node') {
- me.openNodeEditWindow(rec.data);
- } else {
- console.warn(`unknown type ${rec.data.type}`);
- }
- },
-
- handleDeleteAction: function (url, message) {
- let me = this;
- let view = me.getView();
-
- Ext.Msg.show({
- title: gettext('Confirm'),
- icon: Ext.Msg.WARNING,
- message: Ext.htmlEncode(message),
- buttons: Ext.Msg.YESNO,
- defaultFocus: 'no',
- callback: function (btn) {
- if (btn !== 'yes') {
- return;
- }
-
- Proxmox.Utils.API2Request({
- url,
- method: 'DELETE',
- waitMsgTarget: view,
- failure: function (response, opts) {
- Ext.Msg.alert(Proxmox.Utils.errorText, response.htmlStatus);
- },
- callback: () => me.reload(),
- });
- },
- });
- },
-
- deleteAction: function (table, rI, cI, item, e, rec) {
- let me = this;
-
- if (rec.data.type === 'fabric') {
- let message = Ext.String.format(
- gettext('Are you sure you want to remove the fabric "{0}"?'),
- rec.data.id,
- );
-
- let url = `/cluster/sdn/fabrics/fabric/${rec.data.id}`;
-
- me.handleDeleteAction(url, message);
- } else if (rec.data.type === 'node') {
- let message = Ext.String.format(
- gettext(
- 'Are you sure you want to remove the node "{0}" from the fabric "{1}"?',
- ),
- rec.data.node_id,
- rec.data.fabric_id,
- );
-
- let url = `/cluster/sdn/fabrics/node/${rec.data.fabric_id}/${rec.data.node_id}`;
-
- me.handleDeleteAction(url, message);
- } else {
- console.warn(`unknown type: ${rec.data.type}`);
- }
- },
-
- init: function (view) {
- let me = this;
- me.reload();
- },
- },
-});
-Ext.define('PVE.sdn.FabricRoutesContentView', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNFabricRoutesContentView',
-
- columns: [
- {
- header: gettext('Route'),
- sortable: true,
- dataIndex: 'route',
- flex: 1,
- },
- {
- header: gettext('Via'),
- sortable: true,
- dataIndex: 'via',
- renderer: (value) => {
- if (Ext.isArray(value)) {
- return value.join(' ');
- }
- return value || '';
- },
- flex: 1,
- },
- ],
-});
-
-Ext.define('PVE.sdn.FabricNeighborsContentView', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNFabricNeighborsContentView',
-
- columns: [
- {
- header: gettext('Neighbor'),
- sortable: true,
- dataIndex: 'neighbor',
- flex: 1,
- },
- {
- header: gettext('Status'),
- sortable: true,
- dataIndex: 'status',
- flex: 0.5,
- },
- {
- header: gettext('Uptime'),
- sortable: true,
- dataIndex: 'uptime',
- flex: 0.5,
- },
- ],
-});
-
-Ext.define('PVE.sdn.FabricInterfacesContentView', {
- extend: 'Ext.grid.GridPanel',
- alias: 'widget.pveSDNFabricInterfacesContentView',
-
- columns: [
- {
- header: gettext('Name'),
- sortable: true,
- dataIndex: 'name',
- flex: 1,
- },
- {
- header: gettext('Type'),
- sortable: true,
- dataIndex: 'type',
- flex: 1,
- },
- {
- header: gettext('State'),
- sortable: true,
- dataIndex: 'state',
- flex: 1,
- },
- ],
-});
-Ext.define('Pve.sdn.Fabric', {
- extend: 'Ext.data.Model',
- idProperty: 'name',
- fields: ['id', 'protocol', 'ip_prefix', 'ip6_prefix'],
-});
-
-Ext.define('Pve.sdn.Node', {
- extend: 'Ext.data.Model',
- idProperty: 'name',
- fields: ['fabric_id', 'node_id', 'protocol', 'ip', 'ip6', 'area'],
-});
-
-Ext.define('Pve.sdn.Interface', {
- extend: 'Ext.data.Model',
- idProperty: 'name',
- fields: ['name', 'ip', 'ip6', 'hello_interval', 'hello_multiplier', 'csnp_interval'],
-});
-Ext.define('PVE.sdn.Fabric.InterfacePanel', {
- extend: 'Ext.grid.Panel',
- mixins: ['Ext.form.field.Field'],
-
- xtype: 'pveSDNFabricsInterfacePanel',
-
- nodeInterfaces: {},
-
- selModel: {
- mode: 'SIMPLE',
- type: 'checkboxmodel',
- },
-
- commonColumns: [
- {
- text: gettext('Status'),
- dataIndex: 'status',
- width: 30,
- renderer: function (value, metaData, record) {
- let me = this;
-
- let warning;
- let nodeInterface = me.nodeInterfaces[record.data.name];
-
- if (!nodeInterface) {
- warning = gettext('Interface does not exist on node');
- } else if (
- (nodeInterface.ip && record.data.ip) ||
- (nodeInterface.ip6 && record.data.ip6)
- ) {
- warning = gettext(
- 'Interface already has an address configured in /etc/network/interfaces',
- );
- } else if (nodeInterface.ip || nodeInterface.ip6) {
- warning = gettext(
- 'Configure the IP in the fabric, instead of /etc/network/interfaces',
- );
- }
-
- if (warning) {
- metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(warning))}"`;
- return ` `;
- }
-
- return '';
- },
- },
- {
- text: gettext('Name'),
- dataIndex: 'name',
- flex: 2,
- },
- {
- text: gettext('Type'),
- dataIndex: 'type',
- flex: 1,
- },
- {
- text: gettext('IP'),
- xtype: 'widgetcolumn',
- dataIndex: 'ip',
- flex: 1,
- widget: {
- xtype: 'proxmoxtextfield',
- isFormField: false,
- bind: {
- disabled: '{record.isDisabled}',
- },
- },
- },
- ],
-
- additionalColumns: [],
-
- controller: {
- onValueChange: function (field, value) {
- let me = this;
-
- let record = field.getWidgetRecord();
-
- if (!record) {
- return;
- }
-
- let column = field.getWidgetColumn();
-
- record.set(column.dataIndex, value);
- record.commit();
-
- me.getView().checkChange();
- },
-
- control: {
- field: {
- change: 'onValueChange',
- },
- },
- },
-
- listeners: {
- selectionchange: function () {
- this.checkChange();
- },
- },
-
- initComponent: function () {
- let me = this;
-
- Ext.apply(me, {
- store: Ext.create('Ext.data.Store', {
- model: 'Pve.sdn.Interface',
- sorters: {
- property: 'name',
- direction: 'ASC',
- },
- }),
- columns: me.commonColumns.concat(me.additionalColumns),
- });
-
- me.callParent();
-
- Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
- me.initField();
- },
-
- setNodeInterfaces: function (interfaces) {
- let me = this;
-
- let nodeInterfaces = {};
- for (const iface of interfaces) {
- nodeInterfaces[iface.name] = iface;
- }
-
- me.nodeInterfaces = nodeInterfaces;
-
- // reset value when setting new available interfaces
- me.setValue([]);
- },
-
- getValue: function () {
- let me = this;
-
- return me.getSelection().map((rec) => {
- let data = {};
-
- for (const [key, value] of Object.entries(rec.data)) {
- if (value === '' || value === undefined || value === null) {
- continue;
- }
-
- if (['type', 'isDisabled'].includes(key)) {
- continue;
- }
-
- data[key] = value;
- }
-
- return PVE.Parser.printPropertyString(data);
- });
- },
-
- setValue: function (value) {
- let me = this;
-
- let store = me.getStore();
-
- let selection = me.getSelectionModel();
- selection.deselectAll();
-
- let data = structuredClone(me.nodeInterfaces);
-
- for (const iface of Object.values(data)) {
- iface.isDisabled = iface.ip || iface.ip6;
- }
-
- let selected = [];
- let fabricInterfaces = structuredClone(value);
-
- for (let iface of fabricInterfaces) {
- iface = PVE.Parser.parsePropertyString(iface);
-
- selected.push(iface.name);
-
- // if the fabric configuration defines an interface that was
- // previously disabled, re-enable the field to allow editing of the
- // value set in the fabric - we show a warning as well if there is
- // already an IP configured in /e/n/i
- iface.isDisabled = false;
-
- if (Object.hasOwn(data, iface.name)) {
- data[iface.name] = {
- ...data[iface.name],
- // fabric properties have precedence
- ...iface,
- };
- } else {
- data[iface.name] = iface;
- }
- }
-
- store.setData(Object.values(data));
-
- let selected_records = selected.map((name) => store.findRecord('name', name));
- selection.select(selected_records);
-
- me.resetOriginalValue();
- },
-
- getSubmitData: function () {
- let me = this;
-
- let name = me.getName();
- let value = me.getValue();
-
- if (value.length === 0 && !me.isCreate) {
- return {
- delete: name,
- };
- }
-
- return {
- [name]: value,
- };
- },
-});
-Ext.define('PVE.sdn.Fabric.Node.Edit', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- width: 800,
- subject: gettext('Node'),
-
- isCreate: undefined,
-
- fabricId: undefined,
- nodeId: undefined,
- protocol: undefined,
-
- disallowedNodes: [],
-
- baseUrl: '/cluster/sdn/fabrics/node',
-
- items: [
- {
- xtype: 'textfield',
- name: 'digest',
- hidden: true,
- allowBlank: true,
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('IPv4'),
- labelWidth: 120,
- name: 'ip',
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-
- additionalItems: [],
-
- addAnotherCallback: undefined,
-
- initComponent: function () {
- let me = this;
-
- me.isCreate = me.nodeId === undefined;
- me.autoLoad = !me.isCreate;
- me.method = me.isCreate ? 'POST' : 'PUT';
-
- if (!me.isCreate) {
- me.url = `${me.baseUrl}/${me.fabricId}/${me.nodeId}`;
- } else {
- me.url = `${me.baseUrl}/${me.fabricId}`;
- }
-
- me.nodeSelector = me.getNodeSelector();
- me.interfaceSelector = me.getInterfaceSelector();
-
- me.items = [me.nodeSelector, ...me.items, ...me.additionalItems, me.interfaceSelector];
-
- me.callParent();
-
- if (me.isCreate && me.addAnotherCallback) {
- let addAnotherBtn = Ext.create('Ext.Button', {
- text: gettext('Create another'),
- disabled: !me.isCreate,
- handler: function () {
- me.apiCallDone = (success, _response, _options) => {
- if (success) {
- me.addAnotherCallback();
- }
- };
-
- me.submit();
- },
- });
-
- let form = me.formPanel.getForm();
-
- let set_button_status = function () {
- let valid = form.isValid();
- let dirty = form.isDirty();
- addAnotherBtn.setDisabled(!valid || !(dirty || me.isCreate));
- };
-
- form.on('dirtychange', set_button_status);
- form.on('validitychange', set_button_status);
-
- me.getDockedItems()[0].add(addAnotherBtn);
- }
- },
-
- loadNode: async function () {
- let me = this;
-
- if (me.isCreate) {
- return {};
- }
-
- let req = await Proxmox.Async.api2({
- url: `/cluster/sdn/fabrics/node/${me.fabricId}/${me.nodeId}`,
- method: 'GET',
- });
-
- return req.result.data;
- },
-
- loadNodeInterfaces: async function () {
- let me = this;
-
- let req = await Proxmox.Async.api2({
- url: `/api2/extjs/nodes/${me.nodeId}/network`,
- method: 'GET',
- });
-
- return req.result.data.map((iface) => ({
- name: iface.iface,
- type: iface.type,
- ip: iface.cidr,
- ipv6: iface.cidr6,
- }));
- },
-
- load: function () {
- let me = this;
-
- me.setLoading('fetching node information');
-
- Promise.all([me.loadNode(me.fabricId, me.nodeId), me.loadNodeInterfaces(me.nodeId)])
- .catch(Proxmox.Utils.alertResponseFailure)
- .then(([node, nodeInterfaces]) => {
- me.interfaceSelector.setNodeInterfaces(nodeInterfaces);
- me.setValues(node);
- })
- .finally(() => {
- me.setLoading(false);
- });
- },
-
- getNodeSelector: function () {
- let me = this;
-
- return Ext.create('PVE.form.NodeSelector', {
- xtype: 'pveNodeSelector',
- reference: 'nodeselector',
- fieldLabel: gettext('Node'),
- labelWidth: 120,
- name: 'node_id',
- allowBlank: false,
- disabled: !me.isCreate,
- disallowedNodes: me.disallowedNodes,
- onlineValidator: me.isCreate,
- autoSelect: me.isCreate,
- listeners: {
- change: function (f, value) {
- if (me.isCreate) {
- me.nodeId = value;
- me.load();
- }
- },
- },
- listConfig: {
- columns: [
- {
- header: gettext('Node'),
- dataIndex: 'node',
- sortable: true,
- hideable: false,
- flex: 1,
- },
- ],
- },
- store: {
- fields: ['node'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes',
- },
- sorters: [
- {
- property: 'node',
- direction: 'ASC',
- },
- ],
- listeners: {
- load: function (store) {
- if (store.count() === 0) {
- Ext.Msg.alert(
- gettext('Add Node'),
- gettext('All available nodes are already part of the fabric'),
- () => me.destroy(),
- );
- }
- },
- },
- },
- });
- },
-
- getInterfacePanel: function (protocol) {
- const INTERFACE_PANELS = {
- openfabric: 'PVE.sdn.Fabric.OpenFabric.InterfacePanel',
- ospf: 'PVE.sdn.Fabric.Ospf.InterfacePanel',
- };
-
- return INTERFACE_PANELS[protocol];
- },
-
- getInterfaceSelector: function () {
- let me = this;
-
- return Ext.create(me.getInterfacePanel(me.protocol), {
- name: 'interfaces',
- });
- },
-});
-Ext.define('PVE.sdn.Fabric.Fabric.Edit', {
- extend: 'Proxmox.window.Edit',
- mixins: ['Proxmox.Mixin.CBind'],
-
- width: 400,
-
- fabricId: undefined,
- baseUrl: '/cluster/sdn/fabrics/fabric',
-
- items: [
- {
- xtype: 'textfield',
- name: 'digest',
- hidden: true,
- allowBlank: true,
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('Name'),
- labelWidth: 120,
- maxLength: 8,
- name: 'id',
- cbind: {
- disabled: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('IPv4 Prefix'),
- labelWidth: 120,
- name: 'ip_prefix',
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- disabled: '{!isCreate}',
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-
- additionalItems: [],
-
- initComponent: function () {
- let me = this;
-
- me.isCreate = me.fabricId === undefined;
- me.autoLoad = !me.isCreate;
- me.method = me.isCreate ? 'POST' : 'PUT';
-
- if (!me.isCreate) {
- me.url = `${me.baseUrl}/${me.fabricId}`;
- } else {
- me.url = me.baseUrl;
- }
-
- me.items.push(...me.additionalItems);
-
- me.callParent();
- },
-});
-Ext.define('PVE.sdn.Fabric.OpenFabric.InterfacePanel', {
- extend: 'PVE.sdn.Fabric.InterfacePanel',
-
- additionalColumns: [
- {
- text: gettext('IPv6'),
- xtype: 'widgetcolumn',
- dataIndex: 'ip6',
- flex: 1,
- widget: {
- xtype: 'proxmoxtextfield',
- isFormField: false,
- bind: {
- disabled: '{record.isDisabled}',
- },
- },
- },
- {
- text: gettext('Hello Multiplier'),
- xtype: 'widgetcolumn',
- dataIndex: 'hello_multiplier',
- flex: 1,
- hidden: true,
- widget: {
- xtype: 'proxmoxintegerfield',
- isFormField: false,
- emptyText: '10',
- bind: {
- disabled: '{record.isDisabled}',
- },
- },
- },
- ],
-});
-Ext.define('PVE.sdn.Fabric.OpenFabric.Node.Edit', {
- extend: 'PVE.sdn.Fabric.Node.Edit',
- protocol: 'openfabric',
-
- extraRequestParams: {
- protocol: 'openfabric',
- },
-
- additionalItems: [
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('IPv6'),
- labelWidth: 120,
- name: 'ip6',
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-});
-Ext.define('PVE.sdn.Fabric.OpenFabric.Fabric.Edit', {
- extend: 'PVE.sdn.Fabric.Fabric.Edit',
-
- subject: 'OpenFabric',
- onlineHelp: 'pvesdn_openfabric_fabric',
-
- viewModel: {
- data: {
- showIpv6ForwardingHint: false,
- },
- },
-
- extraRequestParams: {
- protocol: 'openfabric',
- },
-
- additionalItems: [
- {
- xtype: 'displayfield',
- value: 'To make IPv6 fabrics work, enable global IPv6 forwarding on all nodes. Click on the Help button for more details.',
- bind: {
- hidden: '{!showIpv6ForwardingHint}',
- },
- userCls: 'pmx-hint',
- },
- {
- xtype: 'proxmoxtextfield',
- fieldLabel: gettext('IPv6 Prefix'),
- labelWidth: 120,
- name: 'ip6_prefix',
- allowBlank: true,
- skipEmptyText: true,
- cbind: {
- disabled: '{!isCreate}',
- deleteEmpty: '{!isCreate}',
- },
- listeners: {
- change: function (textbox, value) {
- let vm = textbox.up('window').getViewModel();
- vm.set('showIpv6ForwardingHint', !!value);
- },
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- // TRANSLATORS: See https://en.wikipedia.org/wiki/IS-IS#Packet_types
- fieldLabel: gettext('Hello Interval'),
- labelWidth: 120,
- name: 'hello_interval',
- allowBlank: true,
- emptyText: '3',
- skipEmptyText: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- {
- xtype: 'proxmoxintegerfield',
- // TRANSLATORS: Stands for Complete Sequence Number Packet, see
- // https://datatracker.ietf.org/doc/html/draft-ietf-lsr-distoptflood#name-flooding-failures
- fieldLabel: gettext('CSNP Interval'),
- labelWidth: 120,
- name: 'csnp_interval',
- allowBlank: true,
- emptyText: '10',
- skipEmptyText: true,
- cbind: {
- deleteEmpty: '{!isCreate}',
- },
- },
- ],
-});
-Ext.define('PVE.sdn.Fabric.Ospf.InterfacePanel', {
- extend: 'PVE.sdn.Fabric.InterfacePanel',
-});
-Ext.define('PVE.sdn.Fabric.Ospf.Node.Edit', {
- extend: 'PVE.sdn.Fabric.Node.Edit',
- protocol: 'ospf',
-
- extraRequestParams: {
- protocol: 'ospf',
- },
-});
-Ext.define('PVE.sdn.Fabric.Ospf.Fabric.Edit', {
- extend: 'PVE.sdn.Fabric.Fabric.Edit',
-
- subject: 'OSPF',
- onlineHelp: 'pvesdn_ospf_fabric',
-
- extraRequestParams: {
- protocol: 'ospf',
- },
-
- additionalItems: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Area'),
- labelWidth: 120,
- name: 'area',
- emptyText: '0',
- allowBlank: false,
- },
- ],
-});
-Ext.define(
- 'PVE.storage.ContentView',
- {
- extend: 'Ext.grid.GridPanel',
-
- alias: 'widget.pveStorageContentView',
-
- itemdblclick: Ext.emptyFn,
-
- viewConfig: {
- trackOver: false,
- loadMask: false,
- },
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- me.nodename = me.pveSelNode.data.node;
- if (!me.nodename) {
- throw 'no node name specified';
- }
- }
- const nodename = me.nodename;
-
- if (!me.storage) {
- me.storage = me.pveSelNode.data.storage;
- if (!me.storage) {
- throw 'no storage ID specified';
- }
- }
- const storage = me.storage;
-
- var content = me.content;
- if (!content) {
- throw 'no content type specified';
- }
-
- const baseurl = `/nodes/${nodename}/storage/${storage}/content`;
- let store = (me.store = Ext.create('Ext.data.Store', {
- model: 'pve-storage-content',
- proxy: {
- type: 'proxmox',
- url: '/api2/json' + baseurl,
- extraParams: {
- content: content,
- },
- },
- sorters: [
- (a, b) =>
- a.data.text
- .toString()
- .localeCompare(b.data.text.toString(), undefined, { numeric: true }),
- ],
- }));
-
- if (!me.sm) {
- me.sm = Ext.create('Ext.selection.RowModel', {});
- }
- let sm = me.sm;
-
- let reload = () => store.load();
-
- Proxmox.Utils.monStoreErrors(me, store);
-
- let tbar = me.tbar ? [...me.tbar] : [];
- if (me.useUploadButton) {
- tbar.unshift(
- {
- xtype: 'button',
- text: gettext('Upload'),
- disabled: !me.enableUploadButton,
- handler: function () {
- Ext.create('PVE.window.UploadToStorage', {
- nodename: nodename,
- storage: storage,
- content: content,
- autoShow: true,
- taskDone: () => reload(),
- });
- },
- },
- {
- xtype: 'button',
- text: gettext('Download from URL'),
- disabled: !me.enableDownloadUrlButton,
- handler: function () {
- Ext.create('PVE.window.DownloadUrlToStorage', {
- nodename: nodename,
- storage: storage,
- content: content,
- autoShow: true,
- taskDone: () => reload(),
- });
- },
- },
- '-',
- );
- }
- if (!me.useCustomRemoveButton) {
- tbar.push({
- xtype: 'proxmoxStdRemoveButton',
- selModel: sm,
- enableFn: (rec) => !rec?.data?.protected,
- delay: 5,
- callback: () => reload(),
- baseurl: baseurl + '/',
- });
- }
- tbar.push('->', gettext('Search') + ':', ' ', {
- xtype: 'textfield',
- width: 200,
- enableKeyEvents: true,
- emptyText:
- content === 'backup' ? gettext('Name, Format, Notes') : gettext('Name, Format'),
- listeners: {
- keyup: {
- buffer: 500,
- fn: function (field) {
- let needle = field.getValue().toLocaleLowerCase();
- store.clearFilter(true);
- store.filter([
- {
- filterFn: ({ data }) =>
- data.text?.toLocaleLowerCase().includes(needle) ||
- data.notes?.toLocaleLowerCase().includes(needle),
- },
- ]);
- },
- },
- change: function (field, newValue, oldValue) {
- if (newValue !== this.originalValue) {
- this.triggers.clear.setVisible(true);
- }
- },
- },
- triggers: {
- clear: {
- cls: 'pmx-clear-trigger',
- weight: -1,
- hidden: true,
- handler: function () {
- this.triggers.clear.setVisible(false);
- this.setValue(this.originalValue);
- store.clearFilter();
- },
- },
- },
- });
-
- let availableColumns = {
- name: {
- header: gettext('Name'),
- flex: 2,
- sortable: true,
- renderer: PVE.Utils.render_storage_content,
- sorter: (a, b) =>
- a.data.text
- .toString()
- .localeCompare(b.data.text.toString(), undefined, { numeric: true }),
- dataIndex: 'text',
- },
- notes: {
- header: gettext('Notes'),
- flex: 1,
- renderer: Ext.htmlEncode,
- dataIndex: 'notes',
- },
- protected: {
- header: ` `,
- tooltip: gettext('Protected'),
- width: 30,
- renderer: (v) =>
- v ? ` ` : '',
- sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0),
- dataIndex: 'protected',
- },
- date: {
- header: gettext('Date'),
- width: 150,
- dataIndex: 'vdate',
- },
- format: {
- header: gettext('Format'),
- width: 100,
- dataIndex: 'format',
- },
- size: {
- header: gettext('Size'),
- width: 100,
- renderer: Proxmox.Utils.format_size,
- dataIndex: 'size',
- },
- };
-
- let showColumns = me.showColumns || ['name', 'date', 'format', 'size'];
-
- Object.keys(availableColumns).forEach(function (key) {
- if (!showColumns.includes(key)) {
- delete availableColumns[key];
- }
- });
-
- if (me.extraColumns && typeof me.extraColumns === 'object') {
- Object.assign(availableColumns, me.extraColumns);
- }
- const columns = Object.values(availableColumns);
-
- Ext.apply(me, {
- store,
- selModel: sm,
- tbar,
- columns,
- listeners: {
- activate: reload,
- itemdblclick: (view, record) => me.itemdblclick(view, record),
- },
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-storage-content', {
- extend: 'Ext.data.Model',
- fields: [
- 'volid',
- 'content',
- 'format',
- 'size',
- 'used',
- 'vmid',
- 'channel',
- 'id',
- 'lun',
- 'notes',
- 'verification',
- {
- name: 'text',
- convert: function (value, record) {
- // check for volid, because if you click on a grouping header,
- // it calls convert (but with an empty volid)
- if (value || record.data.volid === null) {
- return value;
- }
- return PVE.Utils.render_storage_content(value, {}, record);
- },
- },
- {
- name: 'vdate',
- convert: function (value, record) {
- // check for volid, because if you click on a grouping header,
- // it calls convert (but with an empty volid)
- if (value || record.data.volid === null) {
- return value;
- }
- let t = record.data.content;
- if (t === 'backup') {
- let v = record.data.volid;
- let match = v.match(/(\d{4}_\d{2}_\d{2})-(\d{2}_\d{2}_\d{2})/);
- if (match) {
- let date = match[1].replace(/_/g, '-');
- let time = match[2].replace(/_/g, ':');
- return date + ' ' + time;
- }
- }
- if (record.data.ctime) {
- let ctime = new Date(record.data.ctime * 1000);
- return Ext.Date.format(ctime, 'Y-m-d H:i:s');
- }
- return '';
- },
- },
- ],
- idProperty: 'volid',
- });
- },
-);
-Ext.define('PVE.storage.BackupView', {
- extend: 'PVE.storage.ContentView',
-
- onlineHelp: 'chapter_vzdump',
-
- alias: 'widget.pveStorageBackupView',
-
- showColumns: ['name', 'notes', 'protected', 'date', 'format', 'size'],
-
- initComponent: function () {
- let me = this;
-
- let nodename = (me.nodename = me.pveSelNode.data.node);
- if (!nodename) {
- throw 'no node name specified';
- }
-
- let storage = (me.storage = me.pveSelNode.data.storage);
- if (!storage) {
- throw 'no storage ID specified';
- }
-
- me.content = 'backup';
-
- let sm = (me.sm = Ext.create('Ext.selection.RowModel', {}));
-
- let pruneButton = Ext.create('Proxmox.button.Button', {
- text: gettext('Prune group'),
- disabled: true,
- selModel: sm,
- setBackupGroup: function (backup) {
- if (backup) {
- let name = backup.text;
- let vmid = backup.vmid;
- let format = backup.format;
-
- let vmtype;
- if (name.startsWith('vzdump-lxc-') || format === 'pbs-ct') {
- vmtype = 'lxc';
- } else if (name.startsWith('vzdump-qemu-') || format === 'pbs-vm') {
- vmtype = 'qemu';
- }
-
- if (vmid && vmtype) {
- this.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`);
- this.vmid = vmid;
- this.vmtype = vmtype;
- this.setDisabled(false);
- return;
- }
- }
- this.setText(gettext('Prune group'));
- this.vmid = null;
- this.vmtype = null;
- this.setDisabled(true);
- },
- handler: function (b, e, rec) {
- Ext.create('PVE.window.Prune', {
- autoShow: true,
- nodename,
- storage,
- backup_id: this.vmid,
- backup_type: this.vmtype,
- listeners: {
- destroy: () => me.store.load(),
- },
- });
- },
- });
-
- me.on('selectionchange', function (model, srecords, eOpts) {
- if (srecords.length === 1) {
- pruneButton.setBackupGroup(srecords[0].data);
- } else {
- pruneButton.setBackupGroup(null);
- }
- });
-
- let isPBS = me.pluginType === 'pbs';
-
- me.tbar = [
- {
- xtype: 'proxmoxButton',
- text: gettext('Restore'),
- selModel: sm,
- disabled: true,
- handler: function (b, e, rec) {
- let vmtype;
- if (PVE.Utils.volume_is_qemu_backup(rec.data)) {
- vmtype = 'qemu';
- } else if (PVE.Utils.volume_is_lxc_backup(rec.data)) {
- vmtype = 'lxc';
- } else {
- return;
- }
-
- Ext.create('PVE.window.Restore', {
- autoShow: true,
- nodename,
- volid: rec.data.volid,
- volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
- vmtype,
- isPBS,
- listeners: {
- destroy: () => me.store.load(),
- },
- });
- },
- },
- ];
- if (isPBS) {
- me.tbar.push({
- xtype: 'proxmoxButton',
- text: gettext('File Restore'),
- disabled: true,
- selModel: sm,
- handler: function (b, e, rec) {
- let isVMArchive = PVE.Utils.volume_is_qemu_backup(
- rec.data.volid,
- rec.data.format,
- );
- Ext.create('Proxmox.window.FileBrowser', {
- title: gettext('File Restore') + ' - ' + rec.data.text,
- listURL: `/api2/json/nodes/localhost/storage/${me.storage}/file-restore/list`,
- downloadURL: `/api2/json/nodes/localhost/storage/${me.storage}/file-restore/download`,
- extraParams: {
- volume: rec.data.volid,
- },
- archive: isVMArchive ? 'all' : undefined,
- autoShow: true,
- });
- },
- });
- }
- me.tbar.push(
- {
- xtype: 'proxmoxButton',
- text: gettext('Show Configuration'),
- disabled: true,
- selModel: sm,
- handler: function (b, e, rec) {
- Ext.create('PVE.window.BackupConfig', {
- autoShow: true,
- volume: rec.data.volid,
- pveSelNode: me.pveSelNode,
- });
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Edit Notes'),
- disabled: true,
- selModel: sm,
- handler: function (b, e, rec) {
- let volid = rec.data.volid;
- Ext.create('Proxmox.window.Edit', {
- autoShow: true,
- autoLoad: true,
- width: 600,
- height: 400,
- resizable: true,
- title: gettext('Notes'),
- url: `/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`,
- layout: 'fit',
- items: [
- {
- xtype: 'textarea',
- layout: 'fit',
- name: 'notes',
- height: '100%',
- },
- ],
- listeners: {
- destroy: () => me.store.load(),
- },
- });
- },
- },
- {
- xtype: 'proxmoxButton',
- text: gettext('Change Protection'),
- disabled: true,
- handler: function (button, event, record) {
- const volid = record.data.volid;
- Proxmox.Utils.API2Request({
- url: `/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`,
- method: 'PUT',
- waitMsgTarget: me,
- params: { protected: record.data.protected ? 0 : 1 },
- failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
- success: () => {
- me.store.load({
- callback: () => sm.fireEvent('selectionchange', sm, [record]),
- });
- },
- });
- },
- },
- '-',
- pruneButton,
- );
-
- me.extraColumns = {};
-
- if (isPBS) {
- me.extraColumns.encrypted = {
- header: gettext('Encrypted'),
- dataIndex: 'encrypted',
- renderer: PVE.Utils.render_backup_encryption,
- sorter: {
- property: 'encrypted',
- transform: (encrypted) => (encrypted ? 1 : 0),
- },
- };
- me.extraColumns.verification = {
- // TRANSLATORS: The state of the verification task
- header: gettext('Verify State'),
- dataIndex: 'verification',
- renderer: PVE.Utils.render_backup_verification,
- sorter: {
- property: 'verification',
- transform: (value) => {
- let state = value?.state ?? 'none';
- let order = PVE.Utils.verificationStateOrder;
- return order[state] ?? order.__default__;
- },
- },
- };
- }
-
- me.extraColumns.vmid = {
- header: 'VMID',
- dataIndex: 'vmid',
- hidden: true,
- sorter: (a, b) => (a.data.vmid ?? 0) - (b.data.vmid ?? 0),
- };
-
- me.callParent();
-
- me.store.getSorters().clear();
- me.store.setSorters([
- {
- property: 'vdate',
- direction: 'DESC',
- },
- ]);
- },
-});
-Ext.define('PVE.panel.StorageBase', {
- extend: 'Proxmox.panel.InputPanel',
- controller: 'storageEdit',
-
- type: '',
-
- onGetValues: function (values) {
- let me = this;
-
- if (me.isCreate) {
- values.type = me.type;
- } else {
- delete values.storage;
- }
-
- values.disable = values.enable ? 0 : 1;
- delete values.enable;
-
- return values;
- },
-
- initComponent: function () {
- let me = this;
-
- me.column1.unshift({
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'storage',
- value: me.storageId || '',
- fieldLabel: 'ID',
- vtype: 'StorageId',
- allowBlank: false,
- });
-
- me.column2 = me.column2 || [];
- me.column2.unshift(
- {
- xtype: 'pveNodeSelector',
- name: 'nodes',
- reference: 'storageNodeRestriction',
- disabled: me.storageId === 'local',
- fieldLabel: gettext('Nodes'),
- emptyText: gettext('All') + ' (' + gettext('No restrictions') + ')',
- multiSelect: true,
- autoSelect: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'enable',
- checked: true,
- uncheckedValue: 0,
- fieldLabel: gettext('Enable'),
- },
- );
-
- const addAdvancedWidget = (widget) => {
- me.advancedColumn1 = me.advancedColumn1 || [];
- me.advancedColumn2 = me.advancedColumn2 || [];
- if (me.advancedColumn2.length < me.advancedColumn1.length) {
- me.advancedColumn2.unshift(widget);
- } else {
- me.advancedColumn1.unshift(widget);
- }
- };
-
- const qemuImgStorageTypes = ['dir', 'btrfs', 'nfs', 'cifs'];
-
- if (qemuImgStorageTypes.includes(me.type)) {
- addAdvancedWidget({
- xtype: 'pvePreallocationSelector',
- name: 'preallocation',
- fieldLabel: gettext('Preallocation'),
- allowBlank: false,
- deleteEmpty: !me.isCreate,
- value: '__default__',
- });
- }
-
- const externalStorageManagedSnapshotSupport = ['dir', 'nfs', 'cifs', 'lvm'];
-
- if (externalStorageManagedSnapshotSupport.includes(me.type)) {
- addAdvancedWidget({
- xtype: 'proxmoxcheckbox',
- name: 'snapshot-as-volume-chain',
- // TRANSLATORS: As in "a chain of volumes, each referencing the next one".
- boxLabel: gettext('Allow Snapshots as Volume-Chain'),
- deleteEmpty: !me.isCreate,
- // can only allow to enable this on creation for storages that previously already
- // supported qcow2 to avoid ambiguity with existing volumes.
- disabled: !me.isCreate && me.type !== 'lvm',
- checked: false,
- });
-
- me.advancedColumnB = me.advancedColumnB || [];
- if (me.type === 'lvm') {
- me.advancedColumnB.unshift({
- xtype: 'displayfield',
- name: 'external-snapshot-hint-lvm',
- userCls: 'pmx-hint',
- value: gettext('Keep Snapshots as Volume-Chain enabled if qcow2 images exist!'),
- });
- }
- me.advancedColumnB.unshift({
- xtype: 'displayfield',
- name: 'external-snapshot-hint',
- userCls: 'pmx-hint',
- value: gettext('Snapshots as Volume-Chain are a technology preview.'),
- });
- }
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.BaseEdit', {
- extend: 'Proxmox.window.Edit',
-
- apiCallDone: function (success, response, options) {
- let me = this;
- if (typeof me.ipanel.apiCallDone === 'function') {
- me.ipanel.apiCallDone(success, response, options);
- }
- },
-
- initComponent: function () {
- let me = this;
-
- me.isCreate = !me.storageId;
-
- if (me.isCreate) {
- me.url = '/api2/extjs/storage';
- me.method = 'POST';
- } else {
- me.url = '/api2/extjs/storage/' + me.storageId;
- me.method = 'PUT';
- }
-
- me.ipanel = Ext.create(me.paneltype, {
- title: gettext('General'),
- type: me.type,
- isCreate: me.isCreate,
- storageId: me.storageId,
- });
-
- Ext.apply(me, {
- subject: PVE.Utils.format_storage_type(me.type),
- isAdd: true,
- bodyPadding: 0,
- items: {
- xtype: 'tabpanel',
- region: 'center',
- layout: 'fit',
- bodyPadding: 10,
- items: [
- me.ipanel,
- {
- xtype: 'pveBackupJobPrunePanel',
- title: gettext('Backup Retention'),
- hasMaxProtected: true,
- isCreate: me.isCreate,
- keepAllDefaultForCreate: true,
- showPBSHint: me.ipanel.isPBS,
- fallbackHintHtml: gettext(
- "Without any keep option, the node's vzdump.conf or `keep-all` is used as fallback for backup jobs",
- ),
- },
- ],
- },
- });
-
- if (me.ipanel.extraTabs) {
- me.ipanel.extraTabs.forEach((panel) => {
- panel.isCreate = me.isCreate;
- me.items.items.push(panel);
- });
- }
-
- me.callParent();
-
- if (!me.canDoBackups) {
- // cannot mask now, not fully rendered until activated
- me.down('pmxPruneInputPanel').needMask = true;
- }
-
- if (!me.isCreate) {
- me.load({
- success: function (response, options) {
- let values = response.result.data;
- let ctypes = values.content || '';
-
- values.content = ctypes.split(',');
-
- if (values.nodes) {
- values.nodes = values.nodes.split(',');
- }
- values.enable = values.disable ? 0 : 1;
- if (values['prune-backups']) {
- let retention = PVE.Parser.parsePropertyString(values['prune-backups']);
- delete values['prune-backups'];
- Object.assign(values, retention);
- }
-
- me.query('inputpanel').forEach((panel) => {
- panel.setValues(values);
- });
- },
- });
- }
- },
-});
-Ext.define('PVE.storage.Browser', {
- extend: 'PVE.panel.Config',
- alias: 'widget.PVE.storage.Browser',
-
- onlineHelp: 'chapter_storage',
-
- initComponent: function () {
- let me = this;
-
- let nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- let storeid = me.pveSelNode.data.storage;
- if (!storeid) {
- throw 'no storage ID specified';
- }
-
- let storageInfo = PVE.data.ResourceStore.findRecord(
- 'id',
- `storage/${nodename}/${storeid}`,
- 0, // startIndex
- false, // anyMatch
- true, // caseSensitive
- true, // exactMatch
- );
- let res = storageInfo.data;
- let plugin = res.plugintype;
-
- let isEsxi = plugin === 'esxi';
-
- me.items = !isEsxi
- ? [
- {
- title: gettext('Summary'),
- xtype: 'pveStorageSummary',
- iconCls: 'fa fa-book',
- itemId: 'summary',
- },
- ]
- : [];
-
- let caps = Ext.state.Manager.get('GuiCap');
-
- Ext.apply(me, {
- title: Ext.String.format(
- gettext('Storage {0} on node {1}'),
- `'${storeid}'`,
- `'${nodename}'`,
- ),
- hstateid: 'storagetab',
- });
-
- if (
- caps.storage['Datastore.Allocate'] ||
- caps.storage['Datastore.AllocateSpace'] ||
- caps.storage['Datastore.Audit']
- ) {
- let contents = res.content.split(',');
-
- let enableUpload = !!caps.storage['Datastore.AllocateTemplate'];
- let enableDownloadUrl =
- enableUpload &&
- (!!(caps.nodes['Sys.Audit'] && caps.nodes['Sys.Modify']) || // for backward compat
- !!caps.nodes['Sys.AccessNetwork']); // new explicit priv for querying (local) networks
-
- if (contents.includes('backup')) {
- me.items.push({
- xtype: 'pveStorageBackupView',
- title: gettext('Backups'),
- iconCls: 'fa fa-floppy-o',
- itemId: 'contentBackup',
- pluginType: plugin,
- });
- }
- if (contents.includes('images')) {
- me.items.push({
- xtype: 'pveStorageImageView',
- title: gettext('VM Disks'),
- iconCls: 'fa fa-hdd-o',
- itemId: 'contentImages',
- content: 'images',
- pluginType: plugin,
- });
- }
- if (contents.includes('rootdir')) {
- me.items.push({
- xtype: 'pveStorageImageView',
- title: gettext('CT Volumes'),
- iconCls: 'fa fa-hdd-o lxc',
- itemId: 'contentRootdir',
- content: 'rootdir',
- pluginType: plugin,
- });
- }
- if (contents.includes('iso')) {
- me.items.push({
- xtype: 'pveStorageContentView',
- title: gettext('ISO Images'),
- iconCls: 'pve-itype-treelist-item-icon-cdrom',
- itemId: 'contentIso',
- content: 'iso',
- pluginType: plugin,
- enableUploadButton: enableUpload,
- enableDownloadUrlButton: enableDownloadUrl,
- useUploadButton: true,
- });
- }
- if (contents.includes('vztmpl')) {
- me.items.push({
- xtype: 'pveStorageTemplateView',
- title: gettext('CT Templates'),
- iconCls: 'fa fa-file-o lxc',
- itemId: 'contentVztmpl',
- pluginType: plugin,
- enableUploadButton: enableUpload,
- enableDownloadUrlButton: enableDownloadUrl,
- useUploadButton: true,
- });
- }
- if (contents.includes('snippets')) {
- me.items.push({
- xtype: 'pveStorageContentView',
- title: gettext('Snippets'),
- iconCls: 'fa fa-file-code-o',
- itemId: 'contentSnippets',
- content: 'snippets',
- pluginType: plugin,
- });
- }
- if (contents.includes('import')) {
- let isImportable = (format) =>
- ['ova', 'ovf', 'vmx', 'raw', 'qcow2', 'vmdk'].indexOf(format) !== -1;
- let createGuestImportWindow = (selection) => {
- if (!selection) {
- return;
- }
-
- let volumeName = selection.data.volid.replace(/^.*?:/, '');
-
- if (['raw', 'vmdk', 'qcow2'].indexOf(selection.data.format) !== -1) {
- Ext.create('PVE.qemu.HDImportEdit', {
- selection: selection.data.volid,
- nodename,
- autoShow: true,
- });
- } else {
- Ext.create('PVE.window.GuestImport', {
- storage: storeid,
- volumeName,
- nodename,
- autoShow: true,
- });
- }
- };
- me.items.push({
- xtype: 'pveStorageContentView',
- // each gettext needs to be in a separate line
- title: isEsxi ? gettext('Virtual Guests') : gettext('Import'),
- iconCls: isEsxi ? 'fa fa-desktop' : 'fa fa-cloud-download',
- itemId: 'contentImport',
- content: 'import',
- useCustomRemoveButton: isEsxi, // hide default remove button for esxi
- showColumns: isEsxi ? ['name', 'format'] : ['name', 'size', 'format'],
- enableUploadButton: enableUpload && !isEsxi,
- enableDownloadUrlButton: enableDownloadUrl && !isEsxi,
- useUploadButton: !isEsxi,
- itemdblclick: (view, record) => {
- if (isImportable(record.data.format)) {
- createGuestImportWindow(record);
- }
- },
- tbar: [
- {
- xtype: 'proxmoxButton',
- disabled: true,
- text: gettext('Import'),
- iconCls: 'fa fa-cloud-download',
- enableFn: (rec) => isImportable(rec.data.format),
- handler: function () {
- let grid = this.up('pveStorageContentView');
- let selection = grid.getSelection()?.[0];
-
- createGuestImportWindow(selection);
- },
- },
- ],
- pluginType: plugin,
- });
- }
- }
-
- if (caps.storage['Permissions.Modify']) {
- me.items.push({
- xtype: 'pveACLView',
- title: gettext('Permissions'),
- iconCls: 'fa fa-unlock',
- itemId: 'permissions',
- path: `/storage/${storeid}`,
- });
- }
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.CIFSScan', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveCIFSScan',
-
- queryParam: 'server',
-
- valueField: 'share',
- displayField: 'share',
- matchFieldWidth: false,
- listConfig: {
- loadingText: gettext('Scanning...'),
- width: 350,
- },
- doRawQuery: Ext.emptyFn,
-
- onTriggerClick: function () {
- var me = this;
-
- if (!me.queryCaching || me.lastQuery !== me.cifsServer) {
- me.store.removeAll();
- }
-
- var params = {};
- if (me.cifsUsername) {
- params.username = me.cifsUsername;
- }
- if (me.cifsPassword) {
- params.password = me.cifsPassword;
- }
- if (me.cifsDomain) {
- params.domain = me.cifsDomain;
- }
-
- me.store.getProxy().setExtraParams(params);
- me.allQuery = me.cifsServer;
-
- me.callParent();
- },
-
- resetProxy: function () {
- let me = this;
- me.lastQuery = null;
- if (!me.readOnly && !me.disabled) {
- if (me.isExpanded) {
- me.collapse();
- }
- }
- },
-
- setServer: function (server) {
- if (this.cifsServer !== server) {
- this.cifsServer = server;
- this.resetProxy();
- }
- },
- setUsername: function (username) {
- if (this.cifsUsername !== username) {
- this.cifsUsername = username;
- this.resetProxy();
- }
- },
- setPassword: function (password) {
- if (this.cifsPassword !== password) {
- this.cifsPassword = password;
- this.resetProxy();
- }
- },
- setDomain: function (domain) {
- if (this.cifsDomain !== domain) {
- this.cifsDomain = domain;
- this.resetProxy();
- }
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
-
- let store = Ext.create('Ext.data.Store', {
- fields: ['description', 'share'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/' + me.nodename + '/scan/cifs',
- },
- });
- store.sort('share', 'ASC');
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
-
- let picker = me.getPicker();
- // don't use monStoreErrors directly, it doesn't copes well with comboboxes
- picker.mon(store, 'beforeload', function (s, operation, eOpts) {
- picker.unmask();
- delete picker.minHeight;
- });
- picker.mon(store.proxy, 'afterload', function (proxy, request, success) {
- if (success) {
- Proxmox.Utils.setErrorMask(picker, false);
- return;
- }
- let error = request._operation.getError();
- let msg = Proxmox.Utils.getResponseErrorMessage(error);
- if (msg) {
- picker.minHeight = 100;
- }
- Proxmox.Utils.setErrorMask(picker, msg);
- });
- },
-});
-
-Ext.define('PVE.storage.CIFSInputPanel', {
- extend: 'PVE.panel.StorageBase',
-
- onlineHelp: 'storage_cifs',
-
- onGetValues: function (values) {
- let me = this;
-
- if (values.password?.length === 0) {
- delete values.password;
- }
- if (values.username?.length === 0) {
- delete values.username;
- }
- if (values.subdir?.length === 0) {
- delete values.subdir;
- }
-
- return me.callParent([values]);
- },
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'server',
- value: '',
- fieldLabel: gettext('Server'),
- allowBlank: false,
- listeners: {
- change: function (f, value) {
- if (me.isCreate) {
- let exportField = me.down('field[name=share]');
- exportField.setServer(value);
- }
- },
- },
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'username',
- value: '',
- fieldLabel: gettext('Username'),
- emptyText: gettext('Guest user'),
- listeners: {
- change: function (f, value) {
- if (!me.isCreate) {
- return;
- }
- var exportField = me.down('field[name=share]');
- exportField.setUsername(value);
- },
- },
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- inputType: 'password',
- name: 'password',
- value: me.isCreate ? '' : '********',
- emptyText: me.isCreate ? gettext('None') : '',
- fieldLabel: gettext('Password'),
- minLength: 1,
- listeners: {
- change: function (f, value) {
- let exportField = me.down('field[name=share]');
- exportField.setPassword(value);
- },
- },
- },
- {
- xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield',
- name: 'share',
- value: '',
- fieldLabel: 'Share',
- allowBlank: false,
- },
- ];
-
- me.column2 = [
- {
- xtype: 'pveContentTypeSelector',
- name: 'content',
- value: 'images',
- multiSelect: true,
- fieldLabel: gettext('Content'),
- allowBlank: false,
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'domain',
- value: me.isCreate ? '' : undefined,
- fieldLabel: gettext('Domain'),
- allowBlank: true,
- listeners: {
- change: function (f, value) {
- if (me.isCreate) {
- let exportField = me.down('field[name=share]');
- exportField.setDomain(value);
- }
- },
- },
- },
- {
- xtype: 'pmxDisplayEditField',
- editable: me.isCreate,
- name: 'subdir',
- fieldLabel: gettext('Subdirectory'),
- allowBlank: true,
- emptyText: gettext('/some/path'),
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.CephFSInputPanel', {
- extend: 'PVE.panel.StorageBase',
- controller: 'cephstorage',
-
- onlineHelp: 'storage_cephfs',
-
- viewModel: {
- type: 'cephstorage',
- },
-
- setValues: function (values) {
- if (values.monhost) {
- this.viewModel.set('pveceph', false);
- this.lookupReference('pvecephRef').setValue(false);
- this.lookupReference('pvecephRef').resetOriginalValue();
- }
- this.callParent([values]);
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
- me.type = 'cephfs';
-
- me.column1 = [];
-
- me.column1.push(
- {
- xtype: 'textfield',
- name: 'monhost',
- vtype: 'HostList',
- value: '',
- bind: {
- disabled: '{pveceph}',
- submitValue: '{!pveceph}',
- hidden: '{pveceph}',
- },
- fieldLabel: 'Monitor(s)',
- allowBlank: false,
- },
- {
- xtype: 'displayfield',
- reference: 'monhost',
- bind: {
- disabled: '{!pveceph}',
- hidden: '{!pveceph}',
- },
- value: '',
- fieldLabel: 'Monitor(s)',
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'username',
- value: 'admin',
- bind: {
- disabled: '{pveceph}',
- submitValue: '{!pveceph}',
- },
- fieldLabel: gettext('User name'),
- allowBlank: true,
- },
- );
-
- if (me.isCreate) {
- me.column1.push(
- {
- xtype: 'pveCephFSSelector',
- nodename: me.nodename,
- name: 'fs-name',
- bind: {
- disabled: '{!pveceph}',
- submitValue: '{pveceph}',
- hidden: '{!pveceph}',
- },
- fieldLabel: gettext('FS Name'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- nodename: me.nodename,
- name: 'fs-name',
- bind: {
- disabled: '{pveceph}',
- submitValue: '{!pveceph}',
- hidden: '{pveceph}',
- },
- fieldLabel: gettext('FS Name'),
- },
- );
- }
-
- me.column2 = [
- {
- xtype: 'pveContentTypeSelector',
- cts: ['backup', 'iso', 'vztmpl', 'snippets', 'import'],
- fieldLabel: gettext('Content'),
- name: 'content',
- value: 'backup',
- multiSelect: true,
- allowBlank: false,
- },
- ];
-
- me.columnB = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'keyring',
- fieldLabel: gettext('Secret Key'),
- value: me.isCreate ? '' : '***********',
- allowBlank: false,
- bind: {
- hidden: '{pveceph}',
- disabled: '{pveceph}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'pveceph',
- reference: 'pvecephRef',
- bind: {
- disabled: '{!pvecephPossible}',
- value: '{pveceph}',
- },
- checked: true,
- uncheckedValue: 0,
- submitValue: false,
- hidden: !me.isCreate,
- boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS'),
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.DirInputPanel', {
- extend: 'PVE.panel.StorageBase',
-
- onlineHelp: 'storage_directory',
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'path',
- value: '',
- fieldLabel: gettext('Directory'),
- allowBlank: false,
- },
- {
- xtype: 'pveContentTypeSelector',
- name: 'content',
- value: 'images',
- multiSelect: true,
- fieldLabel: gettext('Content'),
- allowBlank: false,
- },
- ];
-
- me.column2 = [
- {
- xtype: 'proxmoxcheckbox',
- name: 'shared',
- uncheckedValue: 0,
- fieldLabel: gettext('Shared'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext(
- 'Enable if the underlying file system is already shared between nodes.',
- ),
- },
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.ImageView', {
- extend: 'PVE.storage.ContentView',
-
- alias: 'widget.pveStorageImageView',
-
- initComponent: function () {
- var me = this;
-
- var nodename = (me.nodename = me.pveSelNode.data.node);
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- var storage = (me.storage = me.pveSelNode.data.storage);
- if (!me.storage) {
- throw 'no storage ID specified';
- }
-
- if (!me.content || (me.content !== 'images' && me.content !== 'rootdir')) {
- throw "content needs to be either 'images' or 'rootdir'";
- }
-
- var sm = (me.sm = Ext.create('Ext.selection.RowModel', {}));
-
- var reload = function () {
- me.store.load();
- };
-
- me.tbar = [
- {
- xtype: 'proxmoxButton',
- selModel: sm,
- text: gettext('Remove'),
- disabled: true,
- handler: function (btn, event, rec) {
- let url = `/nodes/${nodename}/storage/${storage}/content/${rec.data.volid}`;
- var vmid = rec.data.vmid;
-
- var store = PVE.data.ResourceStore;
-
- if (vmid && store.findVMID(vmid)) {
- let guest_node = store.guestNode(vmid);
- let storage_path = 'storage/' + nodename + '/' + storage;
-
- // allow to delete local backed images if a VMID exists on another node.
- if (store.storageIsShared(storage_path) || guest_node === nodename) {
- let msg = Ext.String.format(
- gettext("Cannot remove image, a guest with VMID '{0}' exists!"),
- vmid,
- );
- msg +=
- ' ' +
- gettext("You can delete the image from the guest's hardware pane");
-
- Ext.Msg.show({
- title: gettext('Cannot remove disk image.'),
- icon: Ext.Msg.ERROR,
- msg: msg,
- });
- return;
- }
- }
- var win = Ext.create('Proxmox.window.SafeDestroy', {
- title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid),
- showProgress: true,
- url: url,
- item: { type: 'Image', id: vmid },
- taskName: 'unknownimgdel',
- }).show();
- win.on('destroy', reload);
- },
- },
- ];
- me.useCustomRemoveButton = true;
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.IScsiScan', {
- extend: 'PVE.form.ComboBoxSetStoreNode',
- alias: 'widget.pveIScsiScan',
-
- queryParam: 'portal',
- valueField: 'target',
- displayField: 'target',
- matchFieldWidth: false,
- allowBlank: false,
-
- listConfig: {
- width: 350,
- columns: [
- {
- dataIndex: 'target',
- flex: 1,
- },
- ],
- emptyText: PVE.Utils.renderNotFound(gettext('iSCSI Target')),
- },
-
- config: {
- apiSuffix: '/scan/iscsi',
- },
-
- showNodeSelector: true,
-
- reload: function () {
- let me = this;
- if (!me.isDisabled()) {
- me.getStore().load();
- }
- },
-
- setPortal: function (portal) {
- let me = this;
- me.portal = portal;
- me.getStore().getProxy().setExtraParams({ portal });
- me.reload();
- },
-
- setNodeName: function (value) {
- let me = this;
- me.callParent([value]);
- me.reload();
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
-
- let store = Ext.create('Ext.data.Store', {
- fields: ['target', 'portal'],
- proxy: {
- type: 'proxmox',
- url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
- },
- });
- store.sort('target', 'ASC');
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.IScsiInputPanel', {
- extend: 'PVE.panel.StorageBase',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'storage_open_iscsi',
-
- onGetValues: function (values) {
- let me = this;
-
- values.content = values.luns ? 'images' : 'none';
- delete values.luns;
-
- return me.callParent([values]);
- },
-
- setValues: function (values) {
- values.luns = values.content.indexOf('images') !== -1;
- this.callParent([values]);
- },
-
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: '{isCreate}',
- },
-
- name: 'portal',
- value: '',
- fieldLabel: 'Portal',
- allowBlank: false,
-
- editConfig: {
- listeners: {
- change: {
- fn: function (f, value) {
- let panel = this.up('inputpanel');
- let exportField = panel.lookup('iScsiTargetScan');
- if (exportField) {
- exportField.setDisabled(!value);
- exportField.setPortal(value);
- exportField.setValue('');
- }
- },
- buffer: 500,
- },
- },
- },
- },
- {
- cbind: {
- xtype: (get) => (get('isCreate') ? 'pveIScsiScan' : 'displayfield'),
- readOnly: '{!isCreate}',
- disabled: '{isCreate}',
- },
-
- name: 'target',
- value: '',
- fieldLabel: gettext('Target'),
- allowBlank: false,
- reference: 'iScsiTargetScan',
- listeners: {
- nodechanged: function (value) {
- this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
- },
- },
- },
- ],
-
- column2: [
- {
- xtype: 'checkbox',
- name: 'luns',
- checked: true,
- fieldLabel: gettext('Use LUNs directly'),
- },
- ],
-});
-Ext.define('PVE.storage.VgSelector', {
- extend: 'PVE.form.ComboBoxSetStoreNode',
- alias: 'widget.pveVgSelector',
- valueField: 'vg',
- displayField: 'vg',
- queryMode: 'local',
- editable: false,
-
- listConfig: {
- columns: [
- {
- dataIndex: 'vg',
- flex: 1,
- },
- ],
- emptyText: PVE.Utils.renderNotFound('VGs'),
- },
-
- config: {
- apiSuffix: '/scan/lvm',
- },
-
- showNodeSelector: true,
-
- setNodeName: function (value) {
- let me = this;
- me.callParent([value]);
- me.getStore().load();
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
-
- let store = Ext.create('Ext.data.Store', {
- autoLoad: {}, // true,
- fields: ['vg', 'size', 'free'],
- proxy: {
- type: 'proxmox',
- url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
- },
- });
-
- store.sort('vg', 'ASC');
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.BaseStorageSelector', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveBaseStorageSelector',
-
- existingGroupsText: gettext('Existing volume groups'),
- queryMode: 'local',
- editable: false,
- value: '',
- valueField: 'storage',
- displayField: 'text',
- initComponent: function () {
- let me = this;
-
- let store = Ext.create('Ext.data.Store', {
- autoLoad: {
- addRecords: true,
- params: {
- type: 'iscsi',
- },
- },
- fields: [
- 'storage',
- 'type',
- 'content',
- {
- name: 'text',
- convert: function (value, record) {
- if (record.data.storage) {
- return record.data.storage + ' (iSCSI)';
- } else {
- return me.existingGroupsText;
- }
- },
- },
- ],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/storage/',
- },
- });
-
- store.loadData([{ storage: '' }], true);
-
- store.sort('storage', 'ASC');
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.LunSelector', {
- extend: 'PVE.form.FileSelector',
- alias: 'widget.pveStorageLunSelector',
-
- nodename: 'localhost',
- storageContent: 'images',
- allowBlank: false,
-
- initComponent: function () {
- let me = this;
-
- if (!PVE.Utils.isStandaloneNode()) {
- me.errorHeight = 140;
- Ext.apply(me.listConfig ?? {}, {
- tbar: {
- xtype: 'toolbar',
- items: [
- {
- xtype: 'pveStorageScanNodeSelector',
- autoSelect: false,
- fieldLabel: gettext('Node to scan'),
- listeners: {
- change: (_field, value) => me.setNodename(value),
- },
- },
- ],
- },
- emptyText: me.listConfig?.emptyText ?? PVE.Utils.renderNotFound(gettext('Volume')),
- });
- }
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.LVMInputPanel', {
- extend: 'PVE.panel.StorageBase',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'storage_lvm',
-
- column1: [
- {
- xtype: 'pveBaseStorageSelector',
- name: 'basesel',
- fieldLabel: gettext('Base storage'),
- cbind: {
- disabled: '{!isCreate}',
- hidden: '{!isCreate}',
- },
- submitValue: false,
- listeners: {
- change: function (f, value) {
- let me = this;
- let vgField = me.up('inputpanel').lookup('volumeGroupSelector');
- let vgNameField = me.up('inputpanel').lookup('vgName');
- let baseField = me.up('inputpanel').lookup('lunSelector');
-
- vgField.setVisible(!value);
- vgField.setDisabled(!!value);
-
- baseField.setVisible(!!value);
- baseField.setDisabled(!value);
- baseField.setStorage(value);
-
- vgNameField.setVisible(!!value);
- vgNameField.setDisabled(!value);
- },
- },
- },
- {
- xtype: 'pveStorageLunSelector',
- name: 'base',
- fieldLabel: gettext('Base volume'),
- reference: 'lunSelector',
- hidden: true,
- disabled: true,
- },
- {
- xtype: 'pveVgSelector',
- name: 'vgname',
- fieldLabel: gettext('Volume group'),
- reference: 'volumeGroupSelector',
- cbind: {
- disabled: '{!isCreate}',
- hidden: '{!isCreate}',
- },
- allowBlank: false,
- listeners: {
- nodechanged: function (value) {
- this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
- },
- },
- },
- {
- name: 'vgname',
- fieldLabel: gettext('Volume group'),
- reference: 'vgName',
- cbind: {
- xtype: (get) => (get('isCreate') ? 'textfield' : 'displayfield'),
- hidden: '{isCreate}',
- disabled: '{isCreate}',
- },
- value: '',
- allowBlank: false,
- },
- {
- xtype: 'pveContentTypeSelector',
- cts: ['images', 'rootdir'],
- fieldLabel: gettext('Content'),
- name: 'content',
- value: ['images', 'rootdir'],
- multiSelect: true,
- allowBlank: false,
- },
- ],
-
- column2: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'shared',
- uncheckedValue: 0,
- fieldLabel: gettext('Shared'),
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Enable if the LVM is located on a shared LUN.'),
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'saferemove',
- uncheckedValue: 0,
- fieldLabel: gettext('Wipe Removed Volumes'),
- },
- ],
-});
-Ext.define('PVE.storage.TPoolSelector', {
- extend: 'PVE.form.ComboBoxSetStoreNode',
- alias: 'widget.pveTPSelector',
-
- queryParam: 'vg',
- valueField: 'lv',
- displayField: 'lv',
- editable: false,
- allowBlank: false,
-
- listConfig: {
- emptyText: PVE.Utils.renderNotFound('Thin-Pool'),
- columns: [
- {
- dataIndex: 'lv',
- flex: 1,
- },
- ],
- },
-
- config: {
- apiSuffix: '/scan/lvmthin',
- },
-
- reload: function () {
- let me = this;
- if (!me.isDisabled()) {
- me.getStore().load();
- }
- },
-
- setVG: function (myvg) {
- let me = this;
- me.vg = myvg;
- me.getStore().getProxy().setExtraParams({ vg: myvg });
- me.reload();
- },
-
- setNodeName: function (value) {
- let me = this;
- me.callParent([value]);
- me.reload();
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
-
- let store = Ext.create('Ext.data.Store', {
- fields: ['lv'],
- proxy: {
- type: 'proxmox',
- url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
- },
- });
-
- store.sort('lv', 'ASC');
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.BaseVGSelector', {
- extend: 'PVE.form.ComboBoxSetStoreNode',
- alias: 'widget.pveBaseVGSelector',
-
- valueField: 'vg',
- displayField: 'vg',
- queryMode: 'local',
- editable: false,
- allowBlank: false,
-
- listConfig: {
- columns: [
- {
- dataIndex: 'vg',
- flex: 1,
- },
- ],
- },
-
- showNodeSelector: true,
-
- config: {
- apiSuffix: '/scan/lvm',
- },
-
- setNodeName: function (value) {
- let me = this;
- me.callParent([value]);
- me.getStore().load();
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
-
- let store = Ext.create('Ext.data.Store', {
- autoLoad: {},
- fields: ['vg', 'size', 'free'],
- proxy: {
- type: 'proxmox',
- url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
- },
- });
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.LvmThinInputPanel', {
- extend: 'PVE.panel.StorageBase',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'storage_lvmthin',
-
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: '{isCreate}',
- },
-
- name: 'vgname',
- fieldLabel: gettext('Volume group'),
-
- editConfig: {
- xtype: 'pveBaseVGSelector',
- listeners: {
- nodechanged: function (value) {
- let panel = this.up('inputpanel');
- panel.lookup('thinPoolSelector').setNodeName(value);
- panel.lookup('storageNodeRestriction').setValue(value);
- },
- change: function (f, value) {
- let vgField = this.up('inputpanel').lookup('thinPoolSelector');
- if (vgField && !f.isDisabled()) {
- vgField.setDisabled(!value);
- vgField.setVG(value);
- vgField.setValue('');
- }
- },
- },
- },
- },
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: '{isCreate}',
- },
-
- name: 'thinpool',
- fieldLabel: gettext('Thin Pool'),
- allowBlank: false,
-
- editConfig: {
- xtype: 'pveTPSelector',
- reference: 'thinPoolSelector',
- disabled: true,
- },
- },
- {
- xtype: 'pveContentTypeSelector',
- cts: ['images', 'rootdir'],
- fieldLabel: gettext('Content'),
- name: 'content',
- value: ['images', 'rootdir'],
- multiSelect: true,
- allowBlank: false,
- },
- ],
-});
-Ext.define('PVE.storage.BTRFSInputPanel', {
- extend: 'PVE.panel.StorageBase',
-
- onlineHelp: 'storage_btrfs',
-
- initComponent: function () {
- let me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'path',
- value: '',
- fieldLabel: gettext('Path'),
- allowBlank: false,
- },
- {
- xtype: 'pveContentTypeSelector',
- name: 'content',
- value: ['images', 'rootdir'],
- multiSelect: true,
- fieldLabel: gettext('Content'),
- allowBlank: false,
- },
- ];
-
- me.columnB = [
- {
- xtype: 'displayfield',
- userCls: 'pmx-hint',
- value: `BTRFS integration is currently a technology preview.`,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.NFSScan', {
- extend: 'Ext.form.field.ComboBox',
- alias: 'widget.pveNFSScan',
-
- queryParam: 'server',
-
- valueField: 'path',
- displayField: 'path',
- matchFieldWidth: false,
- listConfig: {
- loadingText: gettext('Scanning...'),
- width: 350,
- },
- doRawQuery: function () {
- // do nothing
- },
-
- onTriggerClick: function () {
- var me = this;
-
- if (!me.queryCaching || me.lastQuery !== me.nfsServer) {
- me.store.removeAll();
- }
-
- me.allQuery = me.nfsServer;
-
- me.callParent();
- },
-
- setServer: function (server) {
- var me = this;
-
- me.nfsServer = server;
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
-
- var store = Ext.create('Ext.data.Store', {
- fields: ['path', 'options'],
- proxy: {
- type: 'proxmox',
- url: '/api2/json/nodes/' + me.nodename + '/scan/nfs',
- },
- });
-
- store.sort('path', 'ASC');
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.NFSInputPanel', {
- extend: 'PVE.panel.StorageBase',
-
- onlineHelp: 'storage_nfs',
-
- options: [],
-
- onGetValues: function (values) {
- var me = this;
-
- var i;
- var res = [];
- for (i = 0; i < me.options.length; i++) {
- let item = me.options[i];
- if (!item.match(/^vers=(.*)$/)) {
- res.push(item);
- }
- }
- if (values.nfsversion && values.nfsversion !== '__default__') {
- res.push('vers=' + values.nfsversion);
- }
- delete values.nfsversion;
- values.options = res.join(',');
- if (values.options === '') {
- delete values.options;
- if (!me.isCreate) {
- values.delete = 'options';
- }
- }
-
- return me.callParent([values]);
- },
-
- setValues: function (values) {
- var me = this;
- if (values.options) {
- me.options = values.options.split(',');
- me.options.forEach(function (item) {
- var match = item.match(/^vers=(.*)$/);
- if (match) {
- values.nfsversion = match[1];
- }
- });
- }
- return me.callParent([values]);
- },
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'server',
- value: '',
- fieldLabel: gettext('Server'),
- allowBlank: false,
- listeners: {
- change: function (f, value) {
- if (me.isCreate) {
- let exportField = me.down('field[name=export]');
- exportField.setServer(value);
- exportField.setValue('');
- }
- },
- },
- },
- {
- xtype: me.isCreate ? 'pveNFSScan' : 'displayfield',
- name: 'export',
- value: '',
- fieldLabel: 'Export',
- allowBlank: false,
- },
- {
- xtype: 'pveContentTypeSelector',
- name: 'content',
- value: 'images',
- multiSelect: true,
- fieldLabel: gettext('Content'),
- allowBlank: false,
- },
- ];
-
- me.advancedColumn2 = [
- {
- xtype: 'proxmoxKVComboBox',
- fieldLabel: gettext('NFS Version'),
- name: 'nfsversion',
- value: '__default__',
- deleteEmpty: false,
- comboItems: [
- ['__default__', Proxmox.Utils.defaultText],
- ['3', '3'],
- ['4', '4'],
- ['4.1', '4.1'],
- ['4.2', '4.2'],
- ],
- },
- ];
-
- me.callParent();
- },
-});
-/*global QRCode*/
-Ext.define('PVE.Storage.PBSKeyShow', {
- extend: 'Ext.window.Window',
- xtype: 'pvePBSKeyShow',
- mixins: ['Proxmox.Mixin.CBind'],
-
- width: 600,
- modal: true,
- resizable: false,
- title: gettext('Important: Save your Encryption Key'),
-
- // avoid that esc closes this by mistake, force user to more manual action
- onEsc: Ext.emptyFn,
- closable: false,
-
- items: [
- {
- xtype: 'form',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- bodyPadding: 10,
- border: false,
- defaults: {
- anchor: '100%',
- border: false,
- padding: '10 0 0 0',
- },
- items: [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Key'),
- labelWidth: 80,
- inputId: 'encryption-key-value',
- cbind: {
- value: '{key}',
- },
- editable: false,
- },
- {
- xtype: 'component',
- html:
- gettext(
- 'Keep your encryption key safe, but easily accessible for disaster recovery.',
- ) +
- ' ' +
- gettext('We recommend the following safe-keeping strategy:'),
- },
- {
- xtyp: 'container',
- layout: 'hbox',
- items: [
- {
- xtype: 'component',
- html: '1. ' + gettext('Save the key in your password manager.'),
- flex: 1,
- },
- {
- xtype: 'button',
- text: gettext('Copy Key'),
- iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small',
- cls: 'x-btn-default-toolbar-small proxmox-inline-button',
- width: 110,
- handler: function (b) {
- document.getElementById('encryption-key-value').select();
- document.execCommand('copy');
- },
- },
- ],
- },
- {
- xtype: 'container',
- layout: 'hbox',
- items: [
- {
- xtype: 'component',
- html:
- '2. ' +
- gettext(
- 'Download the key to a USB (pen) drive, placed in secure vault.',
- ),
- flex: 1,
- },
- {
- xtype: 'button',
- text: gettext('Download'),
- iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small',
- cls: 'x-btn-default-toolbar-small proxmox-inline-button',
- width: 110,
- handler: function (b) {
- let win = this.up('window');
-
- let pveID = PVE.ClusterName || window.location.hostname;
- let name = `pve-${pveID}-storage-${win.sid}.enc`;
-
- let hiddenElement = document.createElement('a');
- hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key);
- hiddenElement.target = '_blank';
- hiddenElement.download = name;
- hiddenElement.click();
- },
- },
- ],
- },
- {
- xtype: 'container',
- layout: 'hbox',
- items: [
- {
- xtype: 'component',
- html:
- '3. ' +
- gettext('Print as paperkey, laminated and placed in secure vault.'),
- flex: 1,
- },
- {
- xtype: 'button',
- text: gettext('Print Key'),
- iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small',
- cls: 'x-btn-default-toolbar-small proxmox-inline-button',
- width: 110,
- handler: function (b) {
- let win = this.up('window');
- win.paperkey(win.key);
- },
- },
- ],
- },
- ],
- },
- {
- xtype: 'component',
- border: false,
- padding: '10 10 10 10',
- userCls: 'pmx-hint',
- html: gettext(
- 'Please save the encryption key - losing it will render any backup created with it unusable',
- ),
- },
- ],
- buttons: [
- {
- text: gettext('Close'),
- handler: function (b) {
- let win = this.up('window');
- win.close();
- },
- },
- ],
- paperkey: function (keyString) {
- let me = this;
-
- const key = JSON.parse(keyString);
-
- const qrwidth = 500;
- let qrdiv = document.createElement('div');
- let qrcode = new QRCode(qrdiv, {
- width: qrwidth,
- height: qrwidth,
- correctLevel: QRCode.CorrectLevel.H,
- });
- qrcode.makeCode(keyString);
-
- let shortKeyFP = '';
- if (key.fingerprint) {
- shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint);
- }
-
- let printFrame = document.createElement('iframe');
- Object.assign(printFrame.style, {
- position: 'fixed',
- right: '0',
- bottom: '0',
- width: '0',
- height: '0',
- border: '0',
- });
- const prettifiedKey = JSON.stringify(key, null, 2);
- const keyQrBase64 = qrdiv.children[0].toDataURL('image/png');
- const html = `
- Encryption Key - Storage '${me.sid}' (${shortKeyFP})
-
------BEGIN PROXMOX BACKUP KEY-----
-${prettifiedKey}
------END PROXMOX BACKUP KEY-----
-
- `;
-
- printFrame.src = 'data:text/html;base64,' + btoa(html);
- document.body.appendChild(printFrame);
- me.on('destroy', () => document.body.removeChild(printFrame));
- },
-});
-
-Ext.define('PVE.panel.PBSEncryptionKeyTab', {
- extend: 'Proxmox.panel.InputPanel',
- xtype: 'pvePBSEncryptionKeyTab',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'storage_pbs_encryption',
-
- onGetValues: function (form) {
- let values = {};
- if (form.cryptMode === 'upload') {
- values['encryption-key'] = form['crypt-key-upload'];
- } else if (form.cryptMode === 'autogenerate') {
- values['encryption-key'] = 'autogen';
- } else if (form.cryptMode === 'none') {
- if (!this.isCreate) {
- values.delete = ['encryption-key'];
- }
- }
- return values;
- },
-
- setValues: function (values) {
- let me = this;
- let vm = me.getViewModel();
-
- let cryptKeyInfo = values['encryption-key'];
- if (cryptKeyInfo) {
- let icon = ' ';
- if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) {
- // new style fingerprint
- let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo);
- values['crypt-key-fp'] =
- icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
- } else {
- // old key without FP
- values['crypt-key-fp'] = icon + gettext('Active');
- }
- values.cryptMode = 'keep';
- values['crypt-allow-edit'] = false;
- } else {
- values['crypt-key-fp'] = gettext('None');
- let cryptModeNone = me.down('radiofield[inputValue=none]');
- cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
- values.cryptMode = 'none';
- values['crypt-allow-edit'] = true;
- }
- vm.set('keepCryptVisible', !!cryptKeyInfo);
- vm.set('allowEdit', !cryptKeyInfo);
-
- me.callParent([values]);
- },
-
- viewModel: {
- data: {
- allowEdit: true,
- keepCryptVisible: false,
- },
- formulas: {
- showDangerousHint: (get) => {
- let allowEdit = get('allowEdit');
- return get('keepCryptVisible') && allowEdit;
- },
- },
- },
-
- items: [
- {
- xtype: 'displayfield',
- name: 'crypt-key-fp',
- fieldLabel: gettext('Encryption Key'),
- padding: '2 0',
- },
- {
- xtype: 'checkbox',
- name: 'crypt-allow-edit',
- boxLabel: gettext('Edit existing encryption key (dangerous!)'),
- hidden: true,
- submitValue: false,
- isDirty: () => false,
- bind: {
- hidden: '{!keepCryptVisible}',
- value: '{allowEdit}',
- },
- },
- {
- xtype: 'radiofield',
- name: 'cryptMode',
- inputValue: 'keep',
- boxLabel: gettext('Keep encryption key'),
- padding: '0 0 0 25',
- cbind: {
- hidden: '{isCreate}',
- },
- bind: {
- hidden: '{!keepCryptVisible}',
- disabled: '{!allowEdit}',
- },
- },
- {
- xtype: 'radiofield',
- name: 'cryptMode',
- inputValue: 'none',
- checked: true,
- padding: '0 0 0 25',
- cbind: {
- disabled: '{!isCreate}',
- checked: '{isCreate}',
- boxLabel: (get) =>
- get('isCreate')
- ? gettext('Do not encrypt backups')
- : gettext('Delete existing encryption key'),
- },
- bind: {
- disabled: '{!allowEdit}',
- },
- },
- {
- xtype: 'radiofield',
- name: 'cryptMode',
- inputValue: 'autogenerate',
- boxLabel: gettext('Auto-generate a client encryption key'),
- padding: '0 0 0 25',
- cbind: {
- disabled: '{!isCreate}',
- },
- bind: {
- disabled: '{!allowEdit}',
- },
- },
- {
- xtype: 'radiofield',
- name: 'cryptMode',
- inputValue: 'upload',
- boxLabel: gettext('Upload an existing client encryption key'),
- padding: '0 0 0 25',
- cbind: {
- disabled: '{!isCreate}',
- },
- bind: {
- disabled: '{!allowEdit}',
- },
- listeners: {
- change: function (f, value) {
- let panel = this.up('inputpanel');
- if (!panel.rendered) {
- return;
- }
- let uploadKeyField = panel.down('field[name=crypt-key-upload]');
- uploadKeyField.setDisabled(!value);
- uploadKeyField.setHidden(!value);
-
- let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
- uploadKeyButton.setDisabled(!value);
- uploadKeyButton.setHidden(!value);
-
- if (value) {
- uploadKeyField.validate();
- } else {
- uploadKeyField.reset();
- }
- },
- },
- },
- {
- xtype: 'fieldcontainer',
- layout: 'hbox',
- items: [
- {
- xtype: 'proxmoxtextfield',
- name: 'crypt-key-upload',
- fieldLabel: gettext('Key'),
- value: '',
- disabled: true,
- hidden: true,
- allowBlank: false,
- labelAlign: 'right',
- flex: 1,
- emptyText: gettext('You can drag-and-drop a key file here.'),
- validator: function (value) {
- if (value.length) {
- let key;
- try {
- key = JSON.parse(value);
- } catch (e) {
- return 'Failed to parse key - ' + e;
- }
- if (key.data === undefined) {
- return 'Does not seems like a valid Proxmox Backup key!';
- }
- }
- return true;
- },
- afterRender: function () {
- if (!window.FileReader) {
- // No FileReader support in this browser
- return;
- }
- let cancel = function (ev) {
- ev = ev.event;
- if (ev.preventDefault) {
- ev.preventDefault();
- }
- };
- this.inputEl.on('dragover', cancel);
- this.inputEl.on('dragenter', cancel);
- this.inputEl.on('drop', (ev) => {
- cancel(ev);
- let files = ev.event.dataTransfer.files;
- PVE.Utils.loadTextFromFile(files[0], (v) => this.setValue(v));
- });
- },
- },
- {
- xtype: 'filebutton',
- name: 'crypt-upload-button',
- iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
- cls: 'x-btn-default-toolbar-small proxmox-inline-button',
- margin: '0 0 0 4',
- disabled: true,
- hidden: true,
- listeners: {
- change: function (btn, e, value) {
- let ev = e.event;
- let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
- PVE.Utils.loadTextFromFile(ev.target.files[0], (v) =>
- field.setValue(v),
- );
- btn.reset();
- },
- },
- },
- ],
- },
- {
- xtype: 'component',
- border: false,
- padding: '5 2',
- userCls: 'pmx-hint',
- html: // `${ngettext('Warning', 'Warnings', 1)} : ` +
- ` ` +
- gettext(
- 'Deleting or replacing the encryption key will break restoring backups created with it!',
- ),
- hidden: true,
- bind: {
- hidden: '{!showDangerousHint}',
- },
- },
- ],
-});
-
-Ext.define('PVE.storage.PBSInputPanel', {
- extend: 'PVE.panel.StorageBase',
-
- onlineHelp: 'storage_pbs',
-
- apiCallDone: function (success, response, options) {
- let res = response.result.data;
- if (!(res && res.config && res.config['encryption-key'])) {
- return;
- }
- let key = res.config['encryption-key'];
- Ext.create('PVE.Storage.PBSKeyShow', {
- autoShow: true,
- sid: res.storage,
- key: key,
- });
- },
-
- isPBS: true, // HACK
-
- extraTabs: [
- {
- xtype: 'pvePBSEncryptionKeyTab',
- title: gettext('Encryption'),
- },
- ],
-
- setValues: function (values) {
- let me = this;
-
- let server = values.server;
- if (values.port !== undefined) {
- if (Proxmox.Utils.IP6_match.test(server)) {
- server = `[${server}]`;
- }
- server += `:${values.port}`;
- }
- values.hostport = server;
-
- return me.callParent([values]);
- },
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
- fieldLabel: gettext('Server'),
- allowBlank: false,
- name: 'hostport',
- submitValue: false,
- vtype: 'HostPort',
- listeners: {
- change: function (field, newvalue) {
- let server = newvalue;
- let port;
-
- let match = Proxmox.Utils.HostPort_match.exec(newvalue);
- if (match === null) {
- match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue);
- if (match === null) {
- match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue);
- }
- }
-
- if (match !== null) {
- server = match[1];
- if (match[2] !== undefined) {
- port = match[2];
- }
- }
-
- field.up('inputpanel').down('field[name=server]').setValue(server);
- field.up('inputpanel').down('field[name=port]').setValue(port);
- },
- },
- },
- {
- xtype: 'proxmoxtextfield',
- hidden: true,
- name: 'server',
- submitValue: me.isCreate, // it is fixed
- },
- {
- xtype: 'proxmoxtextfield',
- hidden: true,
- deleteEmpty: !me.isCreate,
- name: 'port',
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'username',
- value: '',
- emptyText: gettext('Example') + ': admin@pbs',
- fieldLabel: gettext('Username'),
- regex: /\S+@\w+/,
- regexText: gettext('Example') + ': admin@pbs',
- allowBlank: false,
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- inputType: 'password',
- name: 'password',
- value: me.isCreate ? '' : '********',
- emptyText: me.isCreate ? gettext('None') : '',
- fieldLabel: gettext('Password'),
- allowBlank: false,
- },
- ];
-
- me.column2 = [
- {
- xtype: 'displayfield',
- name: 'content',
- value: 'backup',
- submitValue: true,
- fieldLabel: gettext('Content'),
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'datastore',
- value: '',
- fieldLabel: 'Datastore',
- allowBlank: false,
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'namespace',
- value: '',
- emptyText: gettext('Root'),
- fieldLabel: gettext('Namespace'),
- allowBlank: true,
- },
- ];
-
- me.columnB = [
- {
- xtype: 'pmxFingerprintField',
- name: 'fingerprint',
- value: me.isCreate ? null : undefined,
- deleteEmpty: !me.isCreate,
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.Ceph.Model', {
- extend: 'Ext.app.ViewModel',
- alias: 'viewmodel.cephstorage',
-
- data: {
- pveceph: true,
- pvecephPossible: true,
- namespacePresent: false,
- },
-});
-
-Ext.define('PVE.storage.Ceph.Controller', {
- extend: 'PVE.controller.StorageEdit',
- alias: 'controller.cephstorage',
-
- control: {
- '#': {
- afterrender: 'queryMonitors',
- },
- 'textfield[name=username]': {
- disable: 'resetField',
- },
- 'displayfield[name=monhost]': {
- enable: 'queryMonitors',
- },
- 'textfield[name=monhost]': {
- disable: 'resetField',
- enable: 'resetField',
- },
- 'textfield[name=namespace]': {
- change: 'updateNamespaceHint',
- },
- },
- resetField: function (field) {
- field.reset();
- },
- updateNamespaceHint: function (field, newVal, oldVal) {
- this.getViewModel().set('namespacePresent', newVal);
- },
- queryMonitors: function (field, newVal, oldVal) {
- // we get called with two signatures, the above one for a field
- // change event and the afterrender from the view, this check only
- // can be true for the field change one and omit the API request if
- // pveceph got unchecked - as it's not needed there.
- if (field && !newVal && oldVal) {
- return;
- }
- var view = this.getView();
- var vm = this.getViewModel();
- if (!(view.isCreate || vm.get('pveceph'))) {
- return; // only query on create or if editing a pveceph store
- }
-
- var monhostField = this.lookupReference('monhost');
-
- Proxmox.Utils.API2Request({
- url: '/api2/json/nodes/localhost/ceph/mon',
- method: 'GET',
- scope: this,
- callback: function (options, success, response) {
- var data = response.result.data;
- if (response.status === 200) {
- if (data.length > 0) {
- let monhost = Ext.Array.pluck(data, 'name').sort().join(',');
- monhostField.setValue(monhost);
- monhostField.resetOriginalValue();
- if (view.isCreate) {
- vm.set('pvecephPossible', true);
- }
- } else {
- vm.set('pveceph', false);
- }
- } else {
- vm.set('pveceph', false);
- vm.set('pvecephPossible', false);
- }
- },
- });
- },
-});
-
-Ext.define('PVE.storage.RBDInputPanel', {
- extend: 'PVE.panel.StorageBase',
- controller: 'cephstorage',
-
- onlineHelp: 'ceph_rados_block_devices',
-
- viewModel: {
- type: 'cephstorage',
- },
-
- setValues: function (values) {
- if (values.monhost) {
- this.viewModel.set('pveceph', false);
- this.lookupReference('pvecephRef').setValue(false);
- this.lookupReference('pvecephRef').resetOriginalValue();
- }
- if (values.namespace) {
- this.getViewModel().set('namespacePresent', true);
- }
- this.callParent([values]);
- },
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
- me.type = 'rbd';
-
- me.column1 = [];
-
- if (me.isCreate) {
- me.column1.push(
- {
- xtype: 'pveCephPoolSelector',
- nodename: me.nodename,
- name: 'pool',
- bind: {
- disabled: '{!pveceph}',
- submitValue: '{pveceph}',
- hidden: '{!pveceph}',
- },
- fieldLabel: gettext('Pool'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'pool',
- value: 'rbd',
- bind: {
- disabled: '{pveceph}',
- submitValue: '{!pveceph}',
- hidden: '{pveceph}',
- },
- fieldLabel: gettext('Pool'),
- allowBlank: false,
- },
- );
- } else {
- me.column1.push({
- xtype: 'displayfield',
- nodename: me.nodename,
- name: 'pool',
- fieldLabel: gettext('Pool'),
- renderer: Ext.htmlEncode,
- allowBlank: false,
- });
- }
-
- me.column1.push(
- {
- xtype: 'textfield',
- name: 'monhost',
- vtype: 'HostList',
- bind: {
- disabled: '{pveceph}',
- submitValue: '{!pveceph}',
- hidden: '{pveceph}',
- },
- value: '',
- fieldLabel: 'Monitor(s)',
- allowBlank: false,
- },
- {
- xtype: 'displayfield',
- reference: 'monhost',
- bind: {
- disabled: '{!pveceph}',
- hidden: '{!pveceph}',
- },
- value: '',
- fieldLabel: 'Monitor(s)',
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'username',
- bind: {
- disabled: '{pveceph}',
- submitValue: '{!pveceph}',
- },
- value: 'admin',
- fieldLabel: gettext('User name'),
- allowBlank: true,
- },
- );
-
- me.column2 = [
- {
- xtype: 'pveContentTypeSelector',
- cts: ['images', 'rootdir'],
- fieldLabel: gettext('Content'),
- name: 'content',
- value: ['images'],
- multiSelect: true,
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'krbd',
- uncheckedValue: 0,
- fieldLabel: 'KRBD',
- },
- ];
-
- me.columnB = [
- {
- xtype: me.isCreate ? 'textarea' : 'displayfield',
- name: 'keyring',
- fieldLabel: 'Keyring',
- value: me.isCreate ? '' : '***********',
- allowBlank: false,
- bind: {
- hidden: '{pveceph}',
- disabled: '{pveceph}',
- },
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'pveceph',
- reference: 'pvecephRef',
- bind: {
- disabled: '{!pvecephPossible}',
- value: '{pveceph}',
- },
- checked: true,
- uncheckedValue: 0,
- submitValue: false,
- hidden: !me.isCreate,
- boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool'),
- },
- ];
-
- me.advancedColumn1 = [
- {
- xtype: 'pmxDisplayEditField',
- editable: me.isCreate,
- name: 'namespace',
- value: '',
- fieldLabel: gettext('Namespace'),
- allowBlank: true,
- },
- ];
- me.advancedColumn2 = [
- {
- xtype: 'displayfield',
- name: 'namespace-hint',
- userCls: 'pmx-hint',
- value: gettext('RBD namespaces must be created manually!'),
- bind: {
- hidden: '{!namespacePresent}',
- },
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.StatusView', {
- extend: 'Proxmox.panel.StatusView',
- alias: 'widget.pveStorageStatusView',
-
- height: 230,
- title: gettext('Status'),
-
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
-
- defaults: {
- xtype: 'pmxInfoWidget',
- padding: '0 30 5 30',
- },
- items: [
- {
- xtype: 'box',
- height: 30,
- },
- {
- itemId: 'enabled',
- title: gettext('Enabled'),
- printBar: false,
- textField: 'disabled',
- renderer: Proxmox.Utils.format_neg_boolean,
- },
- {
- itemId: 'active',
- title: gettext('Active'),
- printBar: false,
- textField: 'active',
- renderer: Proxmox.Utils.format_boolean,
- },
- {
- itemId: 'content',
- title: gettext('Content'),
- printBar: false,
- textField: 'content',
- renderer: PVE.Utils.format_content_types,
- },
- {
- itemId: 'type',
- title: gettext('Type'),
- printBar: false,
- textField: 'type',
- renderer: PVE.Utils.format_storage_type,
- },
- {
- xtype: 'box',
- height: 10,
- },
- {
- itemId: 'usage',
- title: gettext('Usage'),
- valueField: 'used',
- maxField: 'total',
- renderer: (val, max) => {
- if (max === undefined) {
- return val;
- }
- return Proxmox.Utils.render_size_usage(val, max, true);
- },
- },
- ],
-
- updateTitle: function () {
- // nothing
- },
-});
-Ext.define('PVE.storage.Summary', {
- extend: 'Ext.panel.Panel',
- alias: 'widget.pveStorageSummary',
- scrollable: true,
- bodyPadding: 5,
- tbar: [
- '->',
- {
- xtype: 'proxmoxRRDTypeSelector',
- },
- ],
- layout: {
- type: 'column',
- },
- defaults: {
- padding: 5,
- columnWidth: 1,
- },
- initComponent: function () {
- var me = this;
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var storage = me.pveSelNode.data.storage;
- if (!storage) {
- throw 'no storage ID specified';
- }
-
- var rstore = Ext.create('Proxmox.data.ObjectStore', {
- url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status',
- interval: 1000,
- });
-
- var rrdstore = Ext.create('Proxmox.data.RRDStore', {
- rrdurl: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/rrddata',
- model: 'pve-rrd-storage',
- });
-
- Ext.apply(me, {
- items: [
- {
- xtype: 'pveStorageStatusView',
- pveSelNode: me.pveSelNode,
- rstore: rstore,
- },
- {
- xtype: 'proxmoxRRDChart',
- title: gettext('Usage'),
- fields: ['total', 'used'],
- fieldTitles: ['Total Size', 'Used Size'],
- store: rrdstore,
- unit: 'bytes',
- },
- ],
- listeners: {
- activate: function () {
- rstore.startUpdate();
- rrdstore.startUpdate();
- },
- destroy: function () {
- rstore.stopUpdate();
- rrdstore.stopUpdate();
- },
- },
- });
-
- me.callParent();
- },
-});
-Ext.define(
- 'PVE.grid.TemplateSelector',
- {
- extend: 'Ext.grid.GridPanel',
-
- alias: 'widget.pveTemplateSelector',
-
- stateful: true,
- stateId: 'grid-template-selector',
- viewConfig: {
- trackOver: false,
- },
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- var baseurl = '/nodes/' + me.nodename + '/aplinfo';
- var store = new Ext.data.Store({
- model: 'pve-aplinfo',
- groupField: 'section',
- proxy: {
- type: 'proxmox',
- url: '/api2/json' + baseurl,
- },
- });
-
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
- groupHeaderTpl:
- '{[ "Section: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
- });
-
- var reload = function () {
- store.load();
- };
-
- Proxmox.Utils.monStoreErrors(me, store);
-
- Ext.apply(me, {
- store: store,
- selModel: sm,
- tbar: [
- '->',
- gettext('Search'),
- {
- xtype: 'textfield',
- width: 200,
- enableKeyEvents: true,
- listeners: {
- buffer: 500,
- keyup: function (field) {
- var value = field.getValue().toLowerCase();
- store.clearFilter(true);
- store.filterBy(function (rec) {
- return (
- rec.data.package.toLowerCase().indexOf(value) !== -1 ||
- rec.data.headline.toLowerCase().indexOf(value) !== -1
- );
- });
- },
- },
- },
- ],
- features: [groupingFeature],
- columns: [
- {
- header: gettext('Type'),
- width: 80,
- dataIndex: 'type',
- },
- {
- header: gettext('Package'),
- flex: 1,
- dataIndex: 'package',
- },
- {
- header: gettext('Version'),
- width: 80,
- dataIndex: 'version',
- },
- {
- header: gettext('Description'),
- flex: 1.5,
- renderer: Ext.String.htmlEncode,
- dataIndex: 'headline',
- },
- ],
- listeners: {
- afterRender: reload,
- },
- });
-
- me.callParent();
- },
- },
- function () {
- Ext.define('pve-aplinfo', {
- extend: 'Ext.data.Model',
- fields: [
- 'template',
- 'type',
- 'package',
- 'version',
- 'headline',
- 'infopage',
- 'description',
- 'os',
- 'section',
- ],
- idProperty: 'template',
- });
- },
-);
-
-Ext.define('PVE.storage.TemplateDownload', {
- extend: 'Ext.window.Window',
- alias: 'widget.pveTemplateDownload',
-
- modal: true,
- title: gettext('Templates'),
- layout: 'fit',
- width: 900,
- height: 600,
- initComponent: function () {
- var me = this;
-
- var grid = Ext.create('PVE.grid.TemplateSelector', {
- border: false,
- scrollable: true,
- nodename: me.nodename,
- });
-
- var sm = grid.getSelectionModel();
-
- var submitBtn = Ext.create('Proxmox.button.Button', {
- text: gettext('Download'),
- disabled: true,
- selModel: sm,
- handler: function (button, event, rec) {
- Proxmox.Utils.API2Request({
- url: '/nodes/' + me.nodename + '/aplinfo',
- params: {
- storage: me.storage,
- template: rec.data.template,
- },
- method: 'POST',
- failure: function (response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: function (response, options) {
- var upid = response.result.data;
-
- Ext.create('Proxmox.window.TaskViewer', {
- upid: upid,
- listeners: {
- destroy: me.reloadGrid,
- },
- }).show();
-
- me.close();
- },
- });
- },
- });
-
- Ext.apply(me, {
- items: grid,
- buttons: [submitBtn],
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.OciRegistryPull', {
- extend: 'Proxmox.window.Edit',
- alias: 'widget.pveOciRegistryPull',
- mixins: ['Proxmox.Mixin.CBind'],
-
- method: 'POST',
-
- showTaskViewer: true,
-
- title: gettext('Pull from OCI Registry'),
- submitText: gettext('Download'),
- width: 450,
-
- cbind: {
- url: '/nodes/{nodename}/storage/{storage}/oci-registry-pull',
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
-
- onReferenceChange: function (field, value) {
- let me = this;
- let view = me.getView();
- let tagField = view.down('[name=tag]');
- tagField.setComboItems([]);
- let matches = me.parseReference(value);
- if (matches) {
- let ref = matches[0];
- let tag = matches[1];
-
- if (tag) {
- field.setValue(ref);
- tagField.setValue(tag);
- tagField.focus();
- } else {
- tagField.clearValue();
- }
- }
- },
-
- parseReference: function (value) {
- const re = new RegExp(
- '^((?:[a-zA-Z\\d](?:[a-zA-Z\\d-]*[a-zA-Z\\d])?(?:\\.(?:[a-zA-Z\\d]' +
- '(?:[a-zA-Z\\d-]*[a-zA-Z\\d])?))*(?::\\d+)?/)?[a-z\\d]+(?:(?:[._]|__|-+)' +
- '[a-z\\d]+)*(?:/[a-z\\d]+(?:(?:[._]|__|-+)[a-z\\d]+)*)*)' +
- '(?::(\\w[\\w.-]{0,127}))?$',
- );
- let matches = value.match(re);
- if (matches) {
- let ref = matches[1];
- let tag = matches[2];
- return [ref, tag];
- }
- return undefined;
- },
-
- queryTags: function (field) {
- let me = this;
- let view = me.getView();
- let refField = view.down('[name=reference]');
- let reference = refField.value.trim();
- let tagField = view.down('[name=tag]');
-
- Proxmox.Utils.API2Request({
- url: `/nodes/${view.nodename}/query-oci-repo-tags`,
- method: 'GET',
- params: {
- reference,
- },
- waitMsgTarget: view,
- failure: (res) => {
- Ext.MessageBox.alert(gettext('Error'), res.htmlStatus);
- },
- success: function (res, opt) {
- let tags = res.result.data;
- tagField.clearValue();
- tagField.setComboItems(tags.map((tag) => [tag, Ext.htmlEncode(tag)]));
- },
- });
- },
- },
-
- items: [
- {
- xtype: 'inputpanel',
- border: false,
- onGetValues: function (values) {
- values.reference = values.reference + ':' + values.tag;
- delete values.tag;
- if (!values.filename) {
- delete values.filename;
- }
- return values;
- },
- items: [
- {
- xtype: 'fieldcontainer',
- layout: 'hbox',
- fieldLabel: gettext('Reference'),
- items: [
- {
- xtype: 'textfield',
- name: 'reference',
- allowBlank: false,
- emptyText: 'registry.example.org/name',
- flex: 1,
- listeners: {
- change: 'onReferenceChange',
- },
- validator: function (value) {
- let me = this;
- let controller = me.up('pveOciRegistryPull').getController();
- if (controller.parseReference(value)) {
- return true;
- }
- return gettext('Invalid OCI Registry Reference');
- },
- },
- {
- xtype: 'button',
- name: 'check',
- text: gettext('Query Tags'),
- margin: '0 0 0 5',
- listeners: {
- click: 'queryTags',
- },
- },
- ],
- },
- {
- xtype: 'proxmoxKVComboBox',
- name: 'tag',
- allowBlank: false,
- // TRANSLATORS: As in a version of an OCI container, e.g. debian:latest
- emptyText: gettext("for example 'latest'"),
- fieldLabel: gettext('Tag'),
- forceSelection: false,
- editable: true,
- typeAhead: true,
- comboItems: [],
- },
- {
- xtype: 'textfield',
- name: 'filename',
- emptyText: '_',
- fieldLabel: gettext('File name'),
- },
- ],
- },
- ],
-
- initComponent: function () {
- var me = this;
-
- if (!me.nodename) {
- throw 'no node name specified';
- }
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.TemplateView', {
- extend: 'PVE.storage.ContentView',
-
- alias: 'widget.pveStorageTemplateView',
-
- initComponent: function () {
- var me = this;
-
- var nodename = (me.nodename = me.pveSelNode.data.node);
- if (!nodename) {
- throw 'no node name specified';
- }
-
- var storage = (me.storage = me.pveSelNode.data.storage);
- if (!storage) {
- throw 'no storage ID specified';
- }
-
- me.content = 'vztmpl';
-
- var reload = function () {
- me.store.load();
- };
-
- var templateButton = Ext.create('Proxmox.button.Button', {
- itemId: 'tmpl-btn',
- text: gettext('Templates'),
- handler: function () {
- var win = Ext.create('PVE.storage.TemplateDownload', {
- nodename: nodename,
- storage: storage,
- reloadGrid: reload,
- });
- win.show();
- },
- });
-
- var pullOciImageButton = Ext.create('Proxmox.button.Button', {
- itemId: 'pull-oci-img-btn',
- text: gettext('Pull from OCI Registry'),
- handler: function () {
- var win = Ext.create('PVE.storage.OciRegistryPull', {
- nodename: nodename,
- storage: storage,
- taskDone: () => reload(),
- });
- win.show();
- },
- });
-
- me.tbar = [templateButton, pullOciImageButton];
- me.useUploadButton = true;
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.ZFSInputPanel', {
- extend: 'PVE.panel.StorageBase',
-
- viewModel: {
- parent: null,
- data: {
- isLIO: false,
- isComstar: true,
- hasWriteCacheOption: true,
- },
- },
-
- controller: {
- xclass: 'Ext.app.ViewController',
- control: {
- 'field[name=iscsiprovider]': {
- change: 'changeISCSIProvider',
- },
- },
- changeISCSIProvider: function (f, newVal, oldVal) {
- var vm = this.getViewModel();
- vm.set('isLIO', newVal === 'LIO');
- vm.set('isComstar', newVal === 'comstar');
- vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
- },
- },
-
- onGetValues: function (values) {
- var me = this;
-
- if (me.isCreate) {
- values.content = 'images';
- }
-
- values.nowritecache = values.writecache ? 0 : 1;
- delete values.writecache;
-
- return me.callParent([values]);
- },
-
- setValues: function (values) {
- values.writecache = values.nowritecache ? 0 : 1;
- this.callParent([values]);
- },
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'portal',
- value: '',
- fieldLabel: gettext('Portal'),
- allowBlank: false,
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'pool',
- value: '',
- fieldLabel: gettext('Pool'),
- allowBlank: false,
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'blocksize',
- value: '4k',
- fieldLabel: gettext('Block Size'),
- allowBlank: false,
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'target',
- value: '',
- fieldLabel: gettext('Target'),
- allowBlank: false,
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'comstar_tg',
- value: '',
- fieldLabel: gettext('Target group'),
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
- allowBlank: true,
- },
- ];
-
- me.column2 = [
- {
- xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield',
- name: 'iscsiprovider',
- value: 'comstar',
- fieldLabel: gettext('iSCSI Provider'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'sparse',
- checked: false,
- uncheckedValue: 0,
- fieldLabel: gettext('Thin provision'),
- },
- {
- xtype: 'proxmoxcheckbox',
- name: 'writecache',
- checked: true,
- bind: me.isCreate
- ? { disabled: '{!hasWriteCacheOption}' }
- : { hidden: '{!hasWriteCacheOption}' },
- uncheckedValue: 0,
- fieldLabel: gettext('Write cache'),
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'comstar_hg',
- value: '',
- bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
- fieldLabel: gettext('Host group'),
- allowBlank: true,
- },
- {
- xtype: me.isCreate ? 'textfield' : 'displayfield',
- name: 'lio_tpg',
- value: '',
- bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
- allowBlank: false,
- fieldLabel: gettext('Target portal group'),
- },
- ];
-
- me.callParent();
- },
-});
-Ext.define('PVE.storage.ZFSPoolSelector', {
- extend: 'PVE.form.ComboBoxSetStoreNode',
- alias: 'widget.pveZFSPoolSelector',
- valueField: 'pool',
- displayField: 'pool',
- queryMode: 'local',
- editable: false,
- allowBlank: false,
-
- listConfig: {
- columns: [
- {
- dataIndex: 'pool',
- flex: 1,
- },
- ],
- emptyText: PVE.Utils.renderNotFound(gettext('ZFS Pool')),
- },
-
- config: {
- apiSuffix: '/scan/zfs',
- },
-
- showNodeSelector: true,
-
- setNodeName: function (value) {
- let me = this;
- me.callParent([value]);
- me.getStore().load();
- },
-
- initComponent: function () {
- let me = this;
-
- if (!me.nodename) {
- me.nodename = 'localhost';
- }
-
- let store = Ext.create('Ext.data.Store', {
- autoLoad: {}, // true,
- fields: ['pool', 'size', 'free'],
- proxy: {
- type: 'proxmox',
- url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
- },
- });
- store.sort('pool', 'ASC');
-
- Ext.apply(me, {
- store: store,
- });
-
- me.callParent();
- },
-});
-
-Ext.define('PVE.storage.ZFSPoolInputPanel', {
- extend: 'PVE.panel.StorageBase',
- mixins: ['Proxmox.Mixin.CBind'],
-
- onlineHelp: 'storage_zfspool',
-
- column1: [
- {
- xtype: 'pmxDisplayEditField',
- cbind: {
- editable: '{isCreate}',
- },
-
- name: 'pool',
- fieldLabel: gettext('ZFS Pool'),
- allowBlank: false,
-
- editConfig: {
- xtype: 'pveZFSPoolSelector',
- reference: 'zfsPoolSelector',
- listeners: {
- nodechanged: function (value) {
- this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
- },
- },
- },
- },
- {
- xtype: 'pveContentTypeSelector',
- cts: ['images', 'rootdir'],
- fieldLabel: gettext('Content'),
- name: 'content',
- value: ['images', 'rootdir'],
- multiSelect: true,
- allowBlank: false,
- },
- ],
-
- column2: [
- {
- xtype: 'proxmoxcheckbox',
- name: 'sparse',
- checked: false,
- uncheckedValue: 0,
- fieldLabel: gettext('Thin provision'),
- },
- {
- xtype: 'textfield',
- name: 'blocksize',
- emptyText: '16k',
- fieldLabel: gettext('Block Size'),
- allowBlank: true,
- },
- ],
-});
-Ext.define('PVE.storage.ESXIInputPanel', {
- extend: 'PVE.panel.StorageBase',
-
- setValues: function (values) {
- let me = this;
-
- let server = values.server;
- if (values.port !== undefined) {
- if (Proxmox.Utils.IP6_match.test(server)) {
- server = `[${server}]`;
- }
- server += `:${values.port}`;
- }
- values.server = server;
-
- return me.callParent([values]);
- },
-
- onGetValues: function (values) {
- let me = this;
-
- if (values.password?.length === 0) {
- delete values.password;
- }
- if (values.username?.length === 0) {
- delete values.username;
- }
-
- if (me.isCreate) {
- let serverPortMatch = Proxmox.Utils.HostPort_match.exec(values.server);
- if (serverPortMatch === null) {
- serverPortMatch = Proxmox.Utils.HostPortBrackets_match.exec(values.server);
- if (serverPortMatch === null) {
- serverPortMatch = Proxmox.Utils.IP6_dotnotation_match.exec(values.server);
- }
- }
-
- if (serverPortMatch !== null) {
- values.server = serverPortMatch[1];
- if (serverPortMatch[2] !== undefined) {
- values.port = serverPortMatch[2];
- }
- }
- }
-
- return me.callParent([values]);
- },
-
- initComponent: function () {
- var me = this;
-
- me.column1 = [
- {
- xtype: 'pmxDisplayEditField',
- name: 'server',
- fieldLabel: gettext('Server'),
- editable: me.isCreate,
- emptyText: gettext('IP address or hostname'),
- allowBlank: false,
- },
- {
- xtype: 'textfield',
- name: 'username',
- fieldLabel: gettext('Username'),
- allowBlank: false,
- },
- {
- xtype: 'proxmoxtextfield',
- name: 'password',
- fieldLabel: gettext('Password'),
- inputType: 'password',
- emptyText: gettext('Unchanged'),
- minLength: 1,
- allowBlank: !me.isCreate,
- },
- ];
-
- me.column2 = [
- {
- xtype: 'proxmoxcheckbox',
- name: 'skip-cert-verification',
- fieldLabel: gettext('Skip Certificate Verification'),
- value: false,
- uncheckedValue: 0,
- defaultValue: 0,
- deleteDefaultValue: !me.isCreate,
- },
- ];
-
- me.callParent();
- },
-});
-/*
- * Workspace base class
- *
- * popup login window when auth fails (call onLogin handler)
- * update (re-login) ticket every 15 minutes
- *
- */
-
-Ext.define('PVE.Workspace', {
- extend: 'Ext.container.Viewport',
-
- title: 'Proxmox Virtual Environment',
-
- loginData: null, // Data from last login call
-
- response401count: 0,
-
- onLogin: function (loginData) {
- // override me
- },
-
- // private
- updateLoginData: function (loginData) {
- let me = this;
- me.loginData = loginData;
- Proxmox.Utils.setAuthData(loginData);
-
- PVE.ClusterName = loginData.clustername;
-
- if (loginData.cap) {
- Ext.state.Manager.set('GuiCap', loginData.cap);
- }
- me.response401count = 0;
-
- me.onLogin(loginData);
- },
-
- // private
- showLogin: function () {
- let me = this;
-
- Proxmox.Utils.authClear();
- Ext.state.Manager.clear('GuiCap');
- Proxmox.UserName = null;
- me.loginData = null;
-
- if (!me.login) {
- me.login = Ext.create('PVE.window.LoginWindow', {
- handler: function (data) {
- me.login = null;
- me.updateLoginData(data);
- Proxmox.Utils.checked_command(Ext.emptyFn); // display subscription status
- },
- });
- }
- me.onLogin(null);
- me.login.show();
- },
-
- initComponent: function () {
- let me = this;
-
- Ext.tip.QuickTipManager.init();
-
- // fixme: what about other errors
- Ext.Ajax.on('requestexception', function (conn, response, options) {
- if (
- (response.status === 401 || response.status === '401') &&
- !PVE.Utils.silenceAuthFailures
- ) {
- // auth failure
- // don't immediately show as logged out to cope better with some big
- // upgrades, which may temporarily produce a false positive 401 err
- me.response401count++;
- if (me.response401count > 5) {
- me.showLogin();
- }
- }
- });
-
- me.callParent();
-
- if (!Proxmox.Utils.authOK()) {
- me.showLogin();
- } else if (me.loginData) {
- me.onLogin(me.loginData);
- }
-
- Ext.TaskManager.start({
- run: function () {
- let ticket = Proxmox.Utils.authOK();
- if (!ticket || !Proxmox.UserName) {
- return;
- }
-
- Ext.Ajax.request({
- params: {
- username: Proxmox.UserName,
- password: ticket,
- },
- url: '/api2/json/access/ticket',
- method: 'POST',
- success: function (response, opts) {
- let obj = Ext.decode(response.responseText);
- me.updateLoginData(obj.data);
- },
- });
- },
- interval: 15 * 60 * 1000,
- });
- },
-});
-
-Ext.define('PVE.StdWorkspace', {
- extend: 'PVE.Workspace',
-
- alias: ['widget.pveStdWorkspace'],
-
- // private
- setContent: function (comp) {
- let me = this;
-
- let view = me.child('#content');
- let layout = view.getLayout();
- let current = layout.getActiveItem();
-
- if (comp) {
- Proxmox.Utils.setErrorMask(view, false);
- comp.border = false;
- view.add(comp);
- if (current !== null && layout.getNext()) {
- layout.next();
- let task = Ext.create('Ext.util.DelayedTask', function () {
- view.remove(current);
- });
- task.delay(10);
- }
- } else {
- view.removeAll(); // helper for cleaning the content when logging out
- }
- },
-
- selectById: function (nodeid) {
- let me = this;
- me.down('pveResourceTree').selectById(nodeid);
- },
-
- onLogin: function (loginData) {
- let me = this;
-
- me.updateUserInfo();
-
- if (loginData) {
- PVE.data.ResourceStore.startUpdate();
-
- Proxmox.Utils.API2Request({
- url: '/version',
- method: 'GET',
- success: function (response) {
- PVE.VersionInfo = response.result.data;
- me.updateVersionInfo();
- },
- });
-
- PVE.UIOptions.update();
-
- Proxmox.Utils.API2Request({
- url: '/cluster/sdn',
- method: 'GET',
- success: function (response) {
- PVE.SDNInfo = response.result.data;
- },
- failure: function (response) {
- PVE.SDNInfo = null;
- let ui = Ext.ComponentQuery.query('treelistitem[text="SDN"]')[0];
- if (ui) {
- ui.addCls('x-hidden-display');
- }
- },
- });
-
- Proxmox.Utils.API2Request({
- url: '/access/domains',
- method: 'GET',
- success: function (response) {
- let [_username, realm] = Proxmox.Utils.parse_userid(Proxmox.UserName);
- response.result.data.forEach((domain) => {
- if (domain.realm === realm) {
- let schema = PVE.Utils.authSchema[domain.type];
- if (schema) {
- me.query('#tfaitem')[0].setHidden(!schema.tfa);
- me.query('#passworditem')[0].setHidden(!schema.pwchange);
- }
- }
- });
- },
- });
- }
- },
-
- updateUserInfo: function () {
- let me = this;
- let ui = me.query('#userinfo')[0];
- ui.setText(Ext.String.htmlEncode(Proxmox.UserName || ''));
- ui.updateLayout();
- },
-
- updateVersionInfo: function () {
- let me = this;
-
- let ui = me.query('#versioninfo')[0];
-
- if (PVE.VersionInfo) {
- let version = PVE.VersionInfo.version;
- ui.update('Virtual Environment ' + version);
- } else {
- ui.update('Virtual Environment');
- }
- ui.updateLayout();
- },
-
- initComponent: function () {
- let me = this;
-
- Ext.History.init();
-
- let appState = Ext.create('PVE.StateProvider');
- Ext.state.Manager.setProvider(appState);
-
- let selview = Ext.create('PVE.form.ViewSelector', {
- flex: 1,
- padding: '0 5 0 0',
- });
-
- let rtree = Ext.createWidget('pveResourceTree', {
- viewFilter: selview.getViewFilter(),
- flex: 1,
- selModel: {
- selType: 'treemodel',
- listeners: {
- selectionchange: function (sm, selected) {
- if (selected.length <= 0) {
- return;
- }
- let treeNode = selected[0];
- let treeTypeToClass = {
- root: 'PVE.dc.Config',
- node: 'PVE.node.Config',
- qemu: 'PVE.qemu.Config',
- lxc: 'pveLXCConfig',
- storage: 'PVE.storage.Browser',
- sdn: 'PVE.sdn.Browser',
- network: 'PVE.network.Browser',
- pool: 'pvePoolConfig',
- tag: 'pveTagConfig',
- };
- PVE.curSelectedNode = treeNode;
- me.setContent({
- xtype:
- treeTypeToClass[treeNode.data.type || 'root'] || 'pvePanelConfig',
- showSearch:
- treeNode.data.id === 'root' ||
- Ext.isDefined(treeNode.data.groupbyid),
- pveSelNode: treeNode,
- workspace: me,
- viewFilter: selview.getViewFilter(),
- });
- },
- },
- },
- });
-
- selview.on('select', function (combo, records) {
- if (records) {
- let view = combo.getViewFilter();
- rtree.setViewFilter(view);
- }
- });
-
- let caps = appState.get('GuiCap');
-
- let createVM = Ext.createWidget('button', {
- pack: 'end',
- margin: '3 5 0 0',
- baseCls: 'x-btn',
- iconCls: 'fa fa-desktop',
- text: gettext('Create VM'),
- disabled: !caps.vms['VM.Allocate'],
- handler: function () {
- let wiz = Ext.create('PVE.qemu.CreateWizard', {});
- wiz.show();
- },
- });
-
- let createCT = Ext.createWidget('button', {
- pack: 'end',
- margin: '3 5 0 0',
- baseCls: 'x-btn',
- iconCls: 'fa fa-cube',
- text: gettext('Create CT'),
- disabled: !caps.vms['VM.Allocate'],
- handler: function () {
- let wiz = Ext.create('PVE.lxc.CreateWizard', {});
- wiz.show();
- },
- });
-
- appState.on('statechange', function (sp, key, value) {
- if (key === 'GuiCap' && value) {
- caps = value;
- createVM.setDisabled(!caps.vms['VM.Allocate']);
- createCT.setDisabled(!caps.vms['VM.Allocate']);
- } else if (key === '_stripBeta') {
- let betaLink = me.query('#betalink')?.[0];
- if (betaLink) {
- betaLink.setHidden(true);
- }
-
- const indexOfTilde = PVE.VersionInfo?.version.indexOf('~') ?? '';
- if (indexOfTilde !== -1) {
- PVE.VersionInfo.version = PVE.VersionInfo.version.substring(0, indexOfTilde);
- me.updateVersionInfo();
- }
- }
- });
-
- Ext.apply(me, {
- layout: { type: 'border' },
- border: false,
- items: [
- {
- region: 'north',
- title: gettext('Header'), // for ARIA
- header: false, // avoid rendering the title
- layout: {
- type: 'hbox',
- align: 'middle',
- },
- baseCls: 'x-plain',
- defaults: {
- baseCls: 'x-plain',
- },
- border: false,
- margin: '2 0 2 5',
- items: [
- {
- xtype: 'proxmoxLogoSvg',
- prefix: 'pwt',
- },
- {
- minWidth: 150,
- id: 'versioninfo',
- html: 'Virtual Environment',
- padding: '0 5',
- style: {
- 'font-size': '16px',
- 'line-height': '20px',
- },
- },
- {
- flex: 2,
- },
- {
- xtype: 'pveGlobalSearchField',
- tree: rtree,
- minWidth: 150,
- maxWidth: 600,
- flex: 3,
- },
- {
- flex: 2,
- },
- {
- xtype: 'proxmoxHelpButton',
- hidden: false,
- baseCls: 'x-btn',
- iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ',
- listenToGlobalEvent: false,
- onlineHelp: 'pve_documentation_index',
- text: gettext('Documentation'),
- margin: '0 5 0 0',
- },
- createVM,
- createCT,
- {
- pack: 'end',
- margin: '0 5 0 0',
- id: 'userinfo',
- xtype: 'button',
- baseCls: 'x-btn',
- style: {
- // proxmox dark grey p light grey as border
- backgroundColor: '#464d4d',
- borderColor: '#ABBABA',
- },
- iconCls: 'fa fa-user',
- menu: [
- {
- iconCls: 'fa fa-gear',
- text: gettext('My Settings'),
- handler: function () {
- var win = Ext.create('PVE.window.Settings');
- win.show();
- },
- },
- {
- text: gettext('Password'),
- itemId: 'passworditem',
- iconCls: 'fa fa-fw fa-key',
- handler: function () {
- var win = Ext.create('Proxmox.window.PasswordEdit', {
- userid: Proxmox.UserName,
- confirmCurrentPassword: Proxmox.UserName !== 'root@pam',
- minLength: 8,
- });
- win.show();
- },
- },
- {
- text: 'TFA',
- itemId: 'tfaitem',
- iconCls: 'fa fa-fw fa-lock',
- handler: function (btn, event, rec) {
- Ext.state.Manager.getProvider().set(
- 'dctab',
- { value: 'tfa' },
- true,
- );
- me.selectById('root');
- },
- },
- {
- iconCls: 'fa fa-paint-brush',
- text: gettext('Color Theme'),
- handler: function () {
- Ext.create('Proxmox.window.ThemeEditWindow').show();
- },
- },
- {
- iconCls: 'fa fa-language',
- text: gettext('Language'),
- handler: function () {
- Ext.create('Proxmox.window.LanguageEditWindow').show();
- },
- },
- '-',
- {
- iconCls: 'fa fa-fw fa-sign-out',
- text: gettext('Logout'),
- handler: function () {
- PVE.data.ResourceStore.loadData([], false);
- me.showLogin();
- me.setContent(null);
- var rt = me.down('pveResourceTree');
- PVE.ClusterName = undefined;
- rt.clearTree();
-
- // empty the stores of the StatusPanel child items
- var statusPanels =
- Ext.ComponentQuery.query('pveStatusPanel grid');
- Ext.Array.forEach(statusPanels, function (comp) {
- if (comp.getStore()) {
- comp.getStore().loadData([], false);
- }
- });
- },
- },
- ],
- },
- ],
- },
- {
- region: 'center',
- stateful: true,
- stateId: 'pvecenter',
- minWidth: 100,
- minHeight: 100,
- id: 'content',
- xtype: 'container',
- layout: { type: 'card' },
- border: false,
- margin: '0 5 0 0',
- items: [],
- },
- {
- region: 'west',
- stateful: true,
- stateId: 'pvewest',
- itemId: 'west',
- xtype: 'container',
- border: false,
- layout: { type: 'vbox', align: 'stretch' },
- margin: '0 0 0 5',
- split: true,
- width: 300,
- items: [
- {
- xtype: 'container',
- layout: 'hbox',
- padding: '0 0 5 0',
- items: [
- selview,
- {
- xtype: 'button',
- cls: 'x-btn-default-toolbar-small',
- iconCls: 'fa fa-fw fa-gear x-btn-icon-el-default-toolbar-small',
- handler: () => {
- Ext.create('PVE.window.TreeSettingsEdit', {
- autoShow: true,
- apiCallDone: () => PVE.UIOptions.fireUIConfigChanged(),
- });
- },
- },
- ],
- },
- rtree,
- ],
- listeners: {
- resize: function (panel, width, height) {
- var viewWidth = me.getSize().width;
- if (width > viewWidth - 100 && viewWidth > 150) {
- panel.setWidth(viewWidth - 100);
- }
- },
- },
- },
- {
- xtype: 'pveStatusPanel',
- stateful: true,
- stateId: 'pvesouth',
- itemId: 'south',
- region: 'south',
- margin: '0 5 5 5',
- title: gettext('Logs'),
- collapsible: true,
- header: false,
- height: 200,
- split: true,
- listeners: {
- resize: function (panel, width, height) {
- var viewHeight = me.getSize().height;
- if (height > viewHeight - 150 && viewHeight > 200) {
- panel.setHeight(viewHeight - 150);
- }
- },
- },
- },
- ],
- });
-
- me.callParent();
-
- me.updateUserInfo();
-
- // on resize, center all modal windows
- Ext.on('resize', function () {
- let modalWindows = Ext.ComponentQuery.query('window[modal]');
- if (modalWindows.length > 0) {
- modalWindows.forEach((win) => win.alignTo(me, 'c-c'));
- }
- });
-
- let tagSelectors = [];
- ['circle', 'dense'].forEach((style) => {
- ['dark', 'light'].forEach((variant) => {
- let selector = `.proxmox-tags-${style} :not(.proxmox-tags-full) > .proxmox-tag-${variant}`;
- tagSelectors.push(selector);
- });
- });
-
- Ext.create('Ext.tip.ToolTip', {
- target: me.el,
- delegate: tagSelectors.join(', '),
- trackMouse: true,
- renderTo: Ext.getBody(),
- border: 0,
- minWidth: 0,
- padding: 0,
- bodyBorder: 0,
- bodyPadding: 0,
- dismissDelay: 0,
- userCls: 'pmx-tag-tooltip',
- shadow: false,
- listeners: {
- beforeshow: function (tip) {
- let tag = Ext.htmlEncode(tip.triggerElement.innerHTML);
- let tagEl = Proxmox.Utils.getTagElement(tag, PVE.UIOptions.tagOverrides);
- tip.update(`${tagEl} `);
- },
- },
- });
- },
-});