var 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_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" : "Manage Ceph Services on Proxmox VE Nodes"
},
"chapter_pvecm" : {
"link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm",
"title" : "Cluster Manager"
},
"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"
},
"getting_help" : {
"link" : "/pve-docs/pve-admin-guide.html#getting_help",
"title" : "Getting Help"
},
"gui_my_settings" : {
"link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings",
"subtitle" : "My Settings",
"title" : "Graphical User Interface"
},
"ha_manager_fencing" : {
"link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing",
"subtitle" : "Fencing",
"title" : "High Availability"
},
"ha_manager_groups" : {
"link" : "/pve-docs/chapter-ha-manager.html#ha_manager_groups",
"subtitle" : "Groups",
"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"
},
"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_snapshots" : {
"link" : "/pve-docs/chapter-pct.html#pct_snapshots",
"subtitle" : "Snapshots",
"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"
},
"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" : "Installation of Ceph Packages",
"title" : "Manage Ceph Services on Proxmox VE Nodes"
},
"pve_ceph_monitors" : {
"link" : "/pve-docs/chapter-pveceph.html#pve_ceph_monitors",
"subtitle" : "Creating Ceph Monitors",
"title" : "Manage Ceph Services on Proxmox VE Nodes"
},
"pve_ceph_osds" : {
"link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds",
"subtitle" : "Creating Ceph OSDs",
"title" : "Manage Ceph Services on Proxmox VE Nodes"
},
"pve_ceph_pools" : {
"link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools",
"subtitle" : "Creating Ceph Pools",
"title" : "Manage Ceph Services on Proxmox VE Nodes"
},
"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_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" : "Manage Ceph Services on Proxmox VE Nodes"
},
"pveceph_fs_create" : {
"link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create",
"subtitle" : "Create a CephFS",
"title" : "Manage Ceph Services on Proxmox VE Nodes"
},
"pveceph_fs_mds" : {
"link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_mds",
"subtitle" : "Metadata Server (MDS)",
"title" : "Manage Ceph Services on Proxmox VE Nodes"
},
"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_groups" : {
"link" : "/pve-docs/chapter-pveum.html#pveum_groups",
"subtitle" : "Groups",
"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_tfa_auth" : {
"link" : "/pve-docs/chapter-pveum.html#pveum_tfa_auth",
"subtitle" : "Two factor authentication",
"title" : "User Management"
},
"pveum_users" : {
"link" : "/pve-docs/chapter-pveum.html#pveum_users",
"subtitle" : "Users",
"title" : "User Management"
},
"qm_bios_and_uefi" : {
"link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi",
"subtitle" : "BIOS and UEFI",
"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_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_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" : {
"link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough",
"title" : "PCI(e) Passthrough"
},
"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_usb_passthrough" : {
"link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough",
"subtitle" : "USB Passthrough",
"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"
},
"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_glusterfs" : {
"link" : "/pve-docs/chapter-pvesm.html#storage_glusterfs",
"title" : "GlusterFS 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_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_network_configuration" : {
"link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration",
"title" : "Network Configuration"
}
};
Ext.ns('PVE');
// avoid errors related to Accessible Rich Internet Applications
// (access for people with disabilities)
// TODO reenable after all components are upgraded
Ext.enableAria = false;
Ext.enableAriaButtons = false;
Ext.enableAriaPanels = false;
// avoid errors when running without development tools
if (!Ext.isDefined(Ext.global.console)) {
var console = {
log: function() {}
};
}
console.log("Starting PVE Manager");
Ext.Ajax.defaultHeaders = {
'Accept': 'application/json'
};
/*jslint confusion: true */
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.',
kvm_ostypes: {
'Linux': [
{ desc: '4.X/3.X/2.6 Kernel', val: 'l26' },
{ desc: '2.4 Kernel', val: 'l24' }
],
'Microsoft Windows': [
{ desc: '10/2016', 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'}
]
},
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 'warning':
icon = 'warning fa-exclamation';
break;
case 'critical':
icon = 'critical fa-times';
break;
default: break;
}
if (circle) {
icon += '-circle';
}
return icon;
},
map_ceph_health: {
'HEALTH_OK':'good',
'HEALTH_WARN':'warning',
'HEALTH_ERR':'critical'
},
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;
},
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_qga_features: function(value) {
if (!value) {
return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')';
}
var props = PVE.Parser.parsePropertyString(value, 'enabled');
if (!PVE.Parser.parseBoolean(props.enabled)) {
return Proxmox.Utils.disabledText;
}
delete props.enabled;
var agentstring = Proxmox.Utils.enabledText;
Ext.Object.each(props, function(key, value) {
var keystring = '' ;
agentstring += ', ' + key + ': ';
if (PVE.Parser.parseBoolean(value)) {
agentstring += Proxmox.Utils.enabledText;
} else {
agentstring += Proxmox.Utils.disabledText;
}
});
return agentstring;
},
render_qemu_machine: function(value) {
return value || (Proxmox.Utils.defaultText + ' (i440fx)');
},
render_qemu_bios: function(value) {
if (!value) {
return Proxmox.Utils.defaultText + ' (SeaBIOS)';
} else if (value === 'seabios') {
return "SeaBIOS";
} else if (value === 'ovmf') {
return "OVMF (UEFI)";
} else {
return value;
}
},
render_dc_ha_opts: function(value) {
if (!value) {
return Proxmox.Utils.defaultText;
} else {
return PVE.Parser.printPropertyString(value);
}
},
render_as_property_string: function(value) {
return (!value) ? Proxmox.Utils.defaultText
: PVE.Parser.printPropertyString(value);
},
render_scsihw: function(value) {
if (!value) {
return Proxmox.Utils.defaultText + ' (LSI 53C895A)';
} else if (value === 'lsi') {
return 'LSI 53C895A';
} else if (value === 'lsi53c810') {
return 'LSI 53C810';
} else if (value === 'megasas') {
return 'MegaRAID SAS 8708EM2';
} else if (value === 'virtio-scsi-pci') {
return 'VirtIO SCSI';
} else if (value === 'virtio-scsi-single') {
return 'VirtIO SCSI single';
} else if (value === 'pvscsi') {
return 'VMware PVSCSI';
} else {
return value;
}
},
// fixme: auto-generate this
// for now, please keep in sync with PVE::Tools::kvmkeymaps
kvm_keymaps: {
//ar: 'Arabic',
da: 'Danish',
de: 'German',
'de-ch': 'German (Swiss)',
'en-gb': 'English (UK)',
'en-us': 'English (USA)',
es: 'Spanish',
//et: 'Estonia',
fi: 'Finnish',
//fo: 'Faroe Islands',
fr: 'French',
'fr-be': 'French (Belgium)',
'fr-ca': 'French (Canada)',
'fr-ch': 'French (Swiss)',
//hr: 'Croatia',
hu: 'Hungarian',
is: 'Icelandic',
it: 'Italian',
ja: 'Japanese',
lt: 'Lithuanian',
//lv: 'Latvian',
mk: 'Macedonian',
nl: 'Dutch',
//'nl-be': 'Dutch (Belgium)',
no: 'Norwegian',
pl: 'Polish',
pt: 'Portuguese',
'pt-br': 'Portuguese (Brazil)',
//ru: 'Russian',
sl: 'Slovenian',
sv: 'Swedish',
//th: 'Thai',
tr: 'Turkish'
},
kvm_vga_drivers: {
std: gettext('Standard VGA'),
vmware: gettext('VMware compatible'),
qxl: 'SPICE',
qxl2: 'SPICE dual monitor',
qxl3: 'SPICE three monitors',
qxl4: 'SPICE four monitors',
serial0: gettext('Serial terminal') + ' 0',
serial1: gettext('Serial terminal') + ' 1',
serial2: gettext('Serial terminal') + ' 2',
serial3: gettext('Serial terminal') + ' 3',
virtio: 'VirtIO-GPU',
none: Proxmox.Utils.noneText
},
render_kvm_language: function (value) {
if (!value || value === '__default__') {
return Proxmox.Utils.defaultText;
}
var text = PVE.Utils.kvm_keymaps[value];
if (text) {
return text + ' (' + value + ')';
}
return value;
},
kvm_keymap_array: function() {
var data = [['__default__', PVE.Utils.render_kvm_language('')]];
Ext.Object.each(PVE.Utils.kvm_keymaps, function(key, value) {
data.push([key, PVE.Utils.render_kvm_language(value)]);
});
return data;
},
console_map: {
'__default__': Proxmox.Utils.defaultText + ' (HTML5)',
'vv': 'SPICE (remote-viewer)',
'html5': 'HTML5 (noVNC)',
'xtermjs': 'xterm.js'
},
render_console_viewer: function(value) {
value = value || '__default__';
if (PVE.Utils.console_map[value]) {
return PVE.Utils.console_map[value];
}
return value;
},
console_viewer_array: function() {
return Ext.Array.map(Object.keys(PVE.Utils.console_map), function(v) {
return [v, PVE.Utils.render_console_viewer(v)];
});
},
render_kvm_vga_driver: function (value) {
if (!value) {
return Proxmox.Utils.defaultText;
}
var vga = PVE.Parser.parsePropertyString(value, 'type');
var text = PVE.Utils.kvm_vga_drivers[vga.type];
if (!vga.type) {
text = Proxmox.Utils.defaultText;
}
if (text) {
return text + ' (' + value + ')';
}
return value;
},
kvm_vga_driver_array: function() {
var data = [['__default__', PVE.Utils.render_kvm_vga_driver('')]];
Ext.Object.each(PVE.Utils.kvm_vga_drivers, function(key, value) {
data.push([key, PVE.Utils.render_kvm_vga_driver(value)]);
});
return data;
},
render_kvm_startup: function(value) {
var startup = PVE.Parser.parseStartup(value);
var res = 'order=';
if (startup.order === undefined) {
res += 'any';
} else {
res += startup.order;
}
if (startup.up !== undefined) {
res += ',up=' + startup.up;
}
if (startup.down !== undefined) {
res += ',down=' + startup.down;
}
return res;
},
extractFormActionError: function(action) {
var msg;
switch (action.failureType) {
case Ext.form.action.Action.CLIENT_INVALID:
msg = gettext('Form fields may not be submitted with invalid values');
break;
case Ext.form.action.Action.CONNECT_FAILURE:
msg = gettext('Connection error');
var resp = action.response;
if (resp.status && resp.statusText) {
msg += " " + resp.status + ": " + resp.statusText;
}
break;
case Ext.form.action.Action.LOAD_FAILURE:
case Ext.form.action.Action.SERVER_INVALID:
msg = Proxmox.Utils.extractRequestError(action.result, true);
break;
}
return msg;
},
format_duration_short: function(ut) {
if (ut < 60) {
return ut.toFixed(1) + 's';
}
if (ut < 3600) {
var mins = ut / 60;
return mins.toFixed(1) + 'm';
}
if (ut < 86400) {
var hours = ut / 3600;
return hours.toFixed(1) + 'h';
}
var days = ut / 86400;
return days.toFixed(1) + 'd';
},
contentTypes: {
'images': gettext('Disk image'),
'backup': gettext('VZDump backup file'),
'vztmpl': gettext('Container template'),
'iso': gettext('ISO image'),
'rootdir': gettext('Container'),
'snippets': gettext('Snippets')
},
storageSchema: {
dir: {
name: Proxmox.Utils.directoryText,
ipanel: 'DirInputPanel',
faIcon: 'folder'
},
lvm: {
name: 'LVM',
ipanel: 'LVMInputPanel',
faIcon: 'folder'
},
lvmthin: {
name: 'LVM-Thin',
ipanel: 'LvmThinInputPanel',
faIcon: 'folder'
},
nfs: {
name: 'NFS',
ipanel: 'NFSInputPanel',
faIcon: 'building'
},
cifs: {
name: 'CIFS',
ipanel: 'CIFSInputPanel',
faIcon: 'building'
},
glusterfs: {
name: 'GlusterFS',
ipanel: 'GlusterFsInputPanel',
faIcon: 'building'
},
iscsi: {
name: 'iSCSI',
ipanel: 'IScsiInputPanel',
faIcon: 'building'
},
sheepdog: {
name: 'Sheepdog',
ipanel: 'SheepdogInputPanel',
hideAdd: true,
faIcon: 'building'
},
cephfs: {
name: 'CephFS',
ipanel: 'CephFSInputPanel',
faIcon: 'building'
},
pvecephfs: {
name: 'CephFS (PVE)',
ipanel: 'CephFSInputPanel',
hideAdd: true,
faIcon: 'building'
},
rbd: {
name: 'RBD',
ipanel: 'RBDInputPanel',
faIcon: 'building'
},
pveceph: {
name: 'RBD (PVE)',
ipanel: 'RBDInputPanel',
hideAdd: true,
faIcon: 'building'
},
zfs: {
name: 'ZFS over iSCSI',
ipanel: 'ZFSInputPanel',
faIcon: 'building'
},
zfspool: {
name: 'ZFS',
ipanel: 'ZFSPoolInputPanel',
faIcon: 'folder'
},
drbd: {
name: 'DRBD',
hideAdd: true
}
},
format_storage_type: function(value, md, record) {
if (value === 'rbd') {
value = (!record || record.get('monhost') ? 'rbd' : 'pveceph');
} else if (value === 'cephfs') {
value = (!record || record.get('monhost') ? 'cephfs' : 'pvecephfs');
}
var schema = PVE.Utils.storageSchema[value];
if (schema) {
return schema.name;
}
return Proxmox.Utils.unknownText;
},
format_ha: function(value) {
var text = Proxmox.Utils.noneText;
if (value.managed) {
text = value.state || Proxmox.Utils.noneText;
text += ', ' + Proxmox.Utils.groupText + ': ';
text += value.group || Proxmox.Utils.noneText;
}
return text;
},
format_content_types: function(value) {
return value.split(',').sort().map(function(ct) {
return PVE.Utils.contentTypes[ct] || ct;
}).join(', ');
},
render_storage_content: function(value, metaData, record) {
var data = record.data;
if (Ext.isNumber(data.channel) &&
Ext.isNumber(data.id) &&
Ext.isNumber(data.lun)) {
return "CH " +
Ext.String.leftPad(data.channel,2, '0') +
" ID " + data.id + " LUN " + data.lun;
}
return data.volid.replace(/^.*:(.*\/)?/,'');
},
render_serverity: function (value) {
return PVE.Utils.log_severity_hash[value] || value;
},
render_cpu: function(value, metaData, record, rowIndex, colIndex, store) {
if (!(record.data.uptime && Ext.isNumeric(value))) {
return '';
}
var maxcpu = record.data.maxcpu || 1;
if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
return '';
}
var per = value * 100;
return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU');
},
render_size: function(value, metaData, record, rowIndex, colIndex, store) {
/*jslint confusion: true */
if (!Ext.isNumeric(value)) {
return '';
}
return Proxmox.Utils.format_size(value);
},
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_duration: function(value) {
if (value === undefined) {
return '-';
}
return PVE.Utils.format_duration_short(value);
},
calculate_mem_usage: function(data) {
if (!Ext.isNumeric(data.mem) ||
data.maxmem === 0 ||
data.uptime < 1) {
return -1;
}
return (data.mem / data.maxmem);
},
render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
if (!Ext.isNumeric(value) || value === -1) {
return '';
}
if (value > 1 ) {
// we got no percentage but bytes
var mem = value;
var maxmem = record.data.maxmem;
if (!record.data.uptime ||
maxmem === 0 ||
!Ext.isNumeric(mem)) {
return '';
}
return ((mem*100)/maxmem).toFixed(1) + " %";
}
return (value*100).toFixed(1) + " %";
},
render_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 PVE.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) ||
type === 'qemu' ||
maxdisk === 0 ||
(type === 'lxc' && record.data.uptime === 0)) {
return '';
}
return PVE.Utils.render_size(value);
},
get_object_icon_class: function(type, record) {
var status = '';
var objType = type;
if (type === 'type') {
// for folder view
objType = record.groupbyid;
} else if (record.template) {
// templates
objType = 'template';
status = type;
} else {
// everything else
status = record.status + ' ha-' + record.hastate;
}
var defaults = PVE.tree.ResourceTree.typeDefaults[objType];
if (defaults && defaults.iconCls) {
var retVal = defaults.iconCls + ' ' + status;
return retVal;
}
return '';
},
render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) {
var cls = PVE.Utils.get_object_icon_class(value,record.data);
var fa = ' ';
return fa + value;
},
render_support_level: function(value, metaData, record) {
return PVE.Utils.support_level_hash[value] || '-';
},
render_upid: function(value, metaData, record) {
var type = record.data.type;
var id = record.data.id;
return Proxmox.Utils.format_task_description(type, id);
},
/* render functions for new status panel */
render_usage: function(val) {
return (val*100).toFixed(2) + '%';
},
render_cpu_usage: function(val, max) {
return Ext.String.format(gettext('{0}% of {1}') +
' ' + gettext('CPU(s)'), (val*100).toFixed(2), max);
},
render_size_usage: function(val, max) {
if (max === 0) {
return gettext('N/A');
}
return (val*100/max).toFixed(2) + '% '+ '(' +
Ext.String.format(gettext('{0} of {1}'),
PVE.Utils.render_size(val), PVE.Utils.render_size(max)) + ')';
},
/* this is different for nodes */
render_node_cpu_usage: function(value, record) {
return PVE.Utils.render_cpu_usage(value, record.cpus);
},
/* this is different for nodes */
render_node_size_usage: function(record) {
return PVE.Utils.render_size_usage(record.used, record.total);
},
render_optional_url: function(value) {
var match;
if (value && (match = value.match(/^https?:\/\//)) !== null) {
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);
},
render_u2f_error: function(error) {
var ErrorNames = {
'1': gettext('Other Error'),
'2': gettext('Bad Request'),
'3': gettext('Configuration Unsupported'),
'4': gettext('Device Ineligible'),
'5': gettext('Timeout')
};
return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
},
windowHostname: function() {
return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match,
function(m, addr, offset, original) { return addr; });
},
openDefaultConsoleWindow: function(consoles, vmtype, vmid, nodename, vmname, cmd) {
var dv = PVE.Utils.defaultViewer(consoles);
PVE.Utils.openConsoleWindow(dv, vmtype, vmid, nodename, vmname, cmd);
},
openConsoleWindow: function(viewer, vmtype, vmid, nodename, vmname, cmd) {
// kvm, lxc, shell, upgrade
if (vmid == undefined && (vmtype === 'kvm' || vmtype === 'lxc')) {
throw "missing vmid";
}
if (!nodename) {
throw "no nodename specified";
}
if (viewer === 'html5') {
PVE.Utils.openVNCViewer(vmtype, vmid, nodename, vmname, cmd);
} else if (viewer === 'xtermjs') {
Proxmox.Utils.openXtermJsViewer(vmtype, vmid, nodename, vmname, cmd);
} else if (viewer === 'vv') {
var url;
var params = { proxy: PVE.Utils.windowHostname() };
if (vmtype === 'kvm') {
url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy';
PVE.Utils.openSpiceViewer(url, params);
} else if (vmtype === 'lxc') {
url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy';
PVE.Utils.openSpiceViewer(url, params);
} else if (vmtype === 'shell') {
url = '/nodes/' + nodename + '/spiceshell';
PVE.Utils.openSpiceViewer(url, params);
} else if (vmtype === 'upgrade') {
url = '/nodes/' + nodename + '/spiceshell';
params.upgrade = 1;
PVE.Utils.openSpiceViewer(url, params);
} else if (vmtype === 'cmd') {
url = '/nodes/' + nodename + '/spiceshell';
params.cmd = cmd;
PVE.Utils.openSpiceViewer(url, params);
}
} else {
throw "unknown viewer type";
}
},
defaultViewer: function(consoles) {
var allowSpice, allowXtermjs;
if (consoles === true) {
allowSpice = true;
allowXtermjs = true;
} else if (typeof consoles === 'object') {
allowSpice = consoles.spice;
allowXtermjs = !!consoles.xtermjs;
}
var vncdefault = 'html5';
var dv = PVE.VersionInfo.console || vncdefault;
if ((dv === 'vv' && !allowSpice) || (dv === 'xtermjs' && !allowXtermjs)) {
dv = vncdefault;
}
return dv;
},
openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) {
var url = Ext.Object.toQueryString({
console: vmtype, // kvm, lxc, upgrade or shell
novnc: 1,
vmid: vmid,
vmname: vmname,
node: nodename,
resize: 'off',
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 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)
var andriod = navigator.userAgent.match(/Android/i) ? true : false;
if (andriod) {
link.download = name;
}
if (link.fireEvent) {
link.fireEvent('onclick');
} else {
var 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){
var raw = "[virt-viewer]\n";
Ext.Object.each(response.result.data, function(k, v) {
raw += k + "=" + v + "\n";
});
var url = 'data:application/x-virt-viewer;charset=UTF-8,' +
encodeURIComponent(raw);
downloadWithName(url, "pve-spice.vv");
}
});
},
openTreeConsole: function(tree, record, item, index, e) {
e.stopEvent();
var nodename = record.data.node;
var vmid = record.data.vmid;
var vmname = record.data.name;
if (record.data.type === 'qemu' && !record.data.template) {
Proxmox.Utils.API2Request({
url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current',
failure: function(response, opts) {
Ext.Msg.alert('Error', response.htmlStatus);
},
success: function(response, opts) {
var allowSpice = !!response.result.data.spice;
PVE.Utils.openDefaultConsoleWindow(allowSpice, '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) {
var list = menu.query('menuitem');
Ext.Array.each(list, function(item) {
if (item.text === text) {
if (item.handler) {
item.handler();
return 1;
} else {
return undefined;
}
}
});
},
createCmdMenu: function(v, record, item, index, event) {
event.stopEvent();
if (!(v instanceof Ext.tree.View)) {
v.select(record);
}
var menu;
var template = !!record.data.template;
var type = record.data.type;
if (template) {
if (type === 'qemu' || type == 'lxc') {
menu = Ext.create('PVE.menu.TemplateMenu', {
pveSelNode: record
});
}
} else if (type === 'qemu' ||
type === 'lxc' ||
type === 'node') {
menu = Ext.create('PVE.' + type + '.CmdMenu', {
pveSelNode: record,
nodename: record.data.node
});
} else {
return;
}
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']) {
values['delete'] += ',' + fieldname;
} else {
values['delete'] = fieldname;
}
}
delete values[fieldname];
}
},
loadSSHKeyFromFile: function(file, callback) {
// ssh-keygen produces 740 bytes for an average 4096 bit rsa key, with
// a user@host comment, 1420 for 8192 bits; current max is 16kbit
// assume: 740*8 for max. 32kbit (5920 byte file)
// round upwards to nearest nice number => 8192 bytes, leaves lots of comment space
if (file.size > 8192) {
Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
return;
}
/*global
FileReader
*/
var reader = new FileReader();
reader.onload = function(evt) {
callback(evt.target.result);
};
reader.readAsText(file);
},
bus_counts: { ide: 4, sata: 6, scsi: 16, virtio: 16 },
// types is either undefined (all busses), an array of busses, or a single bus
forEachBus: function(types, func) {
var busses = Object.keys(PVE.Utils.bus_counts);
var i, j, count, cont;
if (Ext.isArray(types)) {
busses = types;
} else if (Ext.isDefined(types)) {
busses = [ types ];
}
// check if we only have valid busses
for (i = 0; i < busses.length; i++) {
if (!PVE.Utils.bus_counts[busses[i]]) {
throw "invalid bus: '" + busses[i] + "'";
}
}
for (i = 0; i < busses.length; i++) {
count = PVE.Utils.bus_counts[busses[i]];
for (j = 0; j < count; j++) {
cont = func(busses[i], j);
if (!cont && cont !== undefined) {
return;
}
}
}
},
mp_counts: { mps: 256, unused: 256 },
forEachMP: function(func, includeUnused) {
var i, cont;
for (i = 0; i < PVE.Utils.mp_counts.mps; i++) {
cont = func('mp', i);
if (!cont && cont !== undefined) {
return;
}
}
if (!includeUnused) {
return;
}
for (i = 0; i < PVE.Utils.mp_counts.unused; i++) {
cont = func('unused', i);
if (!cont && cont !== undefined) {
return;
}
}
},
cleanEmptyObjectKeys: function (obj) {
var propName;
for (propName in obj) {
if (obj.hasOwnProperty(propName)) {
if (obj[propName] === null || obj[propName] === undefined) {
delete obj[propName];
}
}
}
},
handleStoreErrorOrMask: function(me, store, regex, callback) {
me.mon(store, 'load', function (proxy, response, success, operation) {
if (success) {
Proxmox.Utils.setErrorMask(me, false);
return;
}
var msg;
if (operation.error.statusText) {
if (operation.error.statusText.match(regex)) {
callback(me, operation.error);
return;
} else {
msg = operation.error.statusText + ' (' + operation.error.status + ')';
}
} else {
msg = gettext('Connection error');
}
Proxmox.Utils.setErrorMask(me, msg);
});
},
showCephInstallOrMask: function(container, msg, nodename, callback){
var regex = new RegExp("not (installed|initialized)", "i");
if (msg.match(regex)) {
if (Proxmox.UserName === 'root@pam') {
container.el.mask();
if (!container.down('pveCephInstallWindow')){
var isInstalled = msg.match(/not initialized/i) ? true : false;
var win = Ext.create('PVE.ceph.Install', {
nodename: nodename
});
win.getViewModel().set('isInstalled', isInstalled);
container.add(win);
win.show();
callback(win);
}
} else {
container.mask(Ext.String.format(gettext('{0} not installed.') +
' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']);
}
return true;
} else {
return false;
}
}
},
singleton: true,
constructor: function() {
var me = this;
Ext.apply(me, me.utilities);
}
});
// 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: function(v) {
var list = v.split(/[\ \,\;]+/);
var i;
for (i = 0; i < list.length; i++) {
if (list[i] == '') {
continue;
}
if (!Proxmox.Utils.IP64_match.test(list[i])) {
return false;
}
}
return true;
},
IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2',
IP64AddressListMask: /[A-Fa-f0-9\,\:\.\;\ ]/
});
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.
}
});
// Some configuration values are complex strings -
// so we need parsers/generators for them.
Ext.define('PVE.Parser', { statics: {
// this class only contains static functions
parseACME: function(value) {
if (!value) {
return;
}
var res = {};
var errors = false;
Ext.Array.each(value.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; //continue
}
var match_res;
if ((match_res = p.match(/^(?:domains=)?((?:[a-zA-Z0-9\-\.]+[;, ]?)+)$/)) !== null) {
res.domains = match_res[1].split(/[;, ]/);
} else {
errors = true;
return false;
}
});
if (errors || !res) {
return;
}
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) {
var res = {},
error;
Ext.Array.each(value.split(','), function(p) {
var kv = p.split('=', 2);
if (Ext.isDefined(kv[1])) {
res[kv[0]] = kv[1];
} else if (Ext.isDefined(defaultKey)) {
if (Ext.isDefined(res[defaultKey])) {
error = 'defaultKey may be only defined once in propertyString';
return false; // break
}
res[defaultKey] = kv[0];
} else {
error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
return false; // break
}
});
if (error !== undefined) {
console.error(error);
return;
}
return res;
},
printPropertyString: function(data, defaultKey) {
var stringparts = [];
Ext.Object.each(data, function(key, value) {
if (defaultKey !== undefined && key === defaultKey) {
stringparts.unshift(value);
} else {
stringparts.push(key + '=' + value);
}
});
return stringparts.join(',');
},
parseQemuNetwork: function(key, value) {
if (!(key && value)) {
return;
}
var res = {};
var errors = false;
Ext.Array.each(value.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; // continue
}
var match_res;
if ((match_res = p.match(/^(ne2k_pci|e1000|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+)?)$/)) !== 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 {
errors = true;
return false; // break
}
});
if (errors || !res.model) {
return;
}
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;
}
return netstr;
},
parseQemuDrive: function(key, value) {
if (!(key && value)) {
return;
}
var res = {};
var match_res = key.match(/^([a-z]+)(\d+)$/);
if (!match_res) {
return;
}
res['interface'] = match_res[1];
res.index = match_res[2];
var errors = false;
Ext.Array.each(value.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; // continue
}
var match_res = p.match(/^([a-z_]+)=(\S+)$/);
if (!match_res) {
if (!p.match(/\=/)) {
res.file = p;
return; // continue
}
errors = true;
return false; // break
}
var k = match_res[1];
if (k === 'volume') {
k = 'file';
}
if (Ext.isDefined(res[k])) {
errors = true;
return false; // break
}
var v = match_res[2];
if (k === 'cache' && v === 'off') {
v = 'none';
}
res[k] = v;
});
if (errors || !res.file) {
return;
}
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;
}
var res = {};
var errors = false;
Ext.Array.each(value.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; // continue
}
var match_res;
if ((match_res = p.match(/^ip=(\S+)$/)) !== null) {
res.ip = match_res[1];
} else if ((match_res = p.match(/^gw=(\S+)$/)) !== null) {
res.gw = match_res[1];
} else if ((match_res = p.match(/^ip6=(\S+)$/)) !== null) {
res.ip6 = match_res[1];
} else if ((match_res = p.match(/^gw6=(\S+)$/)) !== null) {
res.gw6 = match_res[1];
} else {
errors = true;
return false; // break
}
});
if (errors) {
return;
}
return res;
},
printIPConfig: function(cfg) {
var c = "";
var str = "";
if (cfg.ip) {
str += "ip=" + cfg.ip;
c = ",";
}
if (cfg.gw) {
str += c + "gw=" + cfg.gw;
c = ",";
}
if (cfg.ip6) {
str += c + "ip6=" + cfg.ip6;
c = ",";
}
if (cfg.gw6) {
str += c + "gw6=" + cfg.gw6;
c = ",";
}
return str;
},
parseOpenVZNetIf: function(value) {
if (!value) {
return;
}
var res = {};
var errors = false;
Ext.Array.each(value.split(';'), function(item) {
if (!item || item.match(/^\s*$/)) {
return; // continue
}
var data = {};
Ext.Array.each(item.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; // continue
}
var match_res = p.match(/^(ifname|mac|bridge|host_ifname|host_mac|mac_filter)=(\S+)$/);
if (!match_res) {
errors = true;
return false; // break
}
if (match_res[1] === 'bridge'){
var bridgevlanf = match_res[2];
var bridge_res = bridgevlanf.match(/^(vmbr(\d+))(v(\d+))?(f)?$/);
if (!bridge_res) {
errors = true;
return false; // break
}
data.bridge = bridge_res[1];
data.tag = bridge_res[4];
/*jslint confusion: true*/
data.firewall = bridge_res[5] ? 1 : 0;
/*jslint confusion: false*/
} else {
data[match_res[1]] = match_res[2];
}
});
if (errors || !data.ifname) {
errors = true;
return false; // break
}
data.raw = item;
res[data.ifname] = data;
});
return errors ? undefined: res;
},
printOpenVZNetIf: function(netif) {
var netarray = [];
Ext.Object.each(netif, function(iface, data) {
var tmparray = [];
Ext.Array.each(['ifname', 'mac', 'bridge', 'host_ifname' , 'host_mac', 'mac_filter', 'tag', 'firewall'], function(key) {
var value = data[key];
if (key === 'bridge'){
if(data.tag){
value = value + 'v' + data.tag;
}
if (data.firewall){
value = value + 'f';
}
}
if (value) {
tmparray.push(key + '=' + value);
}
});
netarray.push(tmparray.join(','));
});
return netarray.join(';');
},
parseLxcNetwork: function(value) {
if (!value) {
return;
}
var data = {};
Ext.Array.each(value.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; // continue
}
var 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 {
// todo: simply ignore errors ?
return; // continue
}
});
return data;
},
printLxcNetwork: function(data) {
var tmparray = [];
Ext.Array.each(['bridge', 'hwaddr', 'mtu', 'name', 'ip',
'gw', 'ip6', 'gw6', 'firewall', 'tag'], function(key) {
var value = data[key];
if (value) {
tmparray.push(key + '=' + value);
}
});
/*jslint confusion: true*/
if (data.rate > 0) {
tmparray.push('rate=' + data.rate);
}
/*jslint confusion: false*/
return tmparray.join(',');
},
parseLxcMountPoint: function(value) {
if (!value) {
return;
}
var res = {};
var errors = false;
Ext.Array.each(value.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; // continue
}
var match_res = p.match(/^([a-z_]+)=(.+)$/);
if (!match_res) {
if (!p.match(/\=/)) {
res.file = p;
return; // continue
}
errors = true;
return false; // break
}
var k = match_res[1];
if (k === 'volume') {
k = 'file';
}
if (Ext.isDefined(res[k])) {
errors = true;
return false; // break
}
var v = match_res[2];
res[k] = v;
});
if (errors || !res.file) {
return;
}
var m = res.file.match(/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):/i);
if (m) {
res.storage = m[1];
res.type = 'volume';
} else if (res.file.match(/^\/dev\//)) {
res.type = 'device';
} else {
res.type = 'bind';
}
return res;
},
printLxcMountPoint: function(mp) {
var drivestr = mp.file;
Ext.Object.each(mp, function(key, value) {
if (!Ext.isDefined(value) || key === 'file' ||
key === 'type' || key === 'storage') {
return; // continue
}
drivestr += ',' + key + '=' + value;
});
return drivestr;
},
parseStartup: function(value) {
if (value === undefined) {
return;
}
var res = {};
var errors = false;
Ext.Array.each(value.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; // continue
}
var 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 {
errors = true;
return false; // break
}
});
if (errors) {
return;
}
return res;
},
printStartup: function(startup) {
var 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) {
var res = {};
Ext.Array.each(value.split(','), function(p) {
var kva = p.split('=', 2);
res[kva[0]] = kva[1];
});
return res;
},
printQemuSmbios1: function(data) {
var datastr = '';
Ext.Object.each(data, function(key, value) {
if (value === '') { return; }
datastr += (datastr !== '' ? ',' : '') + key + '=' + value;
});
return datastr;
},
parseTfaConfig: function(value) {
var res = {};
Ext.Array.each(value.split(','), function(p) {
var kva = p.split('=', 2);
res[kva[0]] = kva[1];
});
return res;
},
parseQemuCpu: function(value) {
if (!value) {
return {};
}
var res = {};
var errors = false;
Ext.Array.each(value.split(','), function(p) {
if (!p || p.match(/^\s*$/)) {
return; // continue
}
if (!p.match(/\=/)) {
if (Ext.isDefined(res.cpu)) {
errors = true;
return false; // break
}
res.cputype = p;
return; // continue
}
var match_res = p.match(/^([a-z_]+)=(\S+)$/);
if (!match_res) {
errors = true;
return false; // break
}
var k = match_res[1];
if (Ext.isDefined(res[k])) {
errors = true;
return false; // break
}
res[k] = match_res[2];
});
if (errors || !res.cputype) {
return;
}
return res;
},
printQemuCpu: function(cpu) {
var cpustr = cpu.cputype;
var 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;
}
return;
}
return cpustr + optstr;
},
parseSSHKey: function(key) {
// |--- options can have quotes--| type key comment
var keyre = /^(?:((?:[^\s"]|\"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
var typere = /^(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)$/;
var m = key.match(keyre);
if (!m) {
return null;
}
if (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;
}
}});
/* This state provider keeps part of the state inside
* the browser history.
*
* We compress (shorten) url using dictionary based compression
* i.e. 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) {
var me = this;
var changes = false;
var oldtext = Ext.encode(me.UIState[name]);
var newtext = Ext.encode(newvalue);
if (newtext != oldtext) {
changes = true;
me.UIState[name] = newvalue;
//console.log("changed old " + name + " " + oldtext);
//console.log("changed new " + name + " " + newtext);
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', ''],
['pooltab', ''],
['kvmtab', ''],
['lxctab', ''],
['dctab', '']
],
hprefix: 'v1',
compDict: {
cloudinit: 52,
replication: 51,
system: 50,
monitor: 49,
'ha-fencing': 48,
'ha-groups': 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) {
var me = this;
var state = {};
if (!token) {
Ext.Array.each(me.hslist, function(rec) {
state[rec[0]] = rec[1];
});
return state;
}
// return Ext.urlDecode(token);
var items = token.split(':');
var prefix = items.shift();
if (prefix != me.hprefix) {
return me.decodeHToken();
}
Ext.Array.each(me.hslist, function(rec) {
var value = items.shift();
if (value) {
if (value[0] === '=') {
value = decodeURIComponent(value.slice(1));
} else {
Ext.Object.each(me.compDict, function(key, cv) {
if (value == cv) {
value = key;
return false;
}
});
}
}
state[rec[0]] = value;
});
return state;
},
encodeHToken: function(state) {
var me = this;
// return Ext.urlEncode(state);
var ctoken = me.hprefix;
Ext.Array.each(me.hslist, function(rec) {
var value = state[rec[0]];
if (!Ext.isDefined(value)) {
value = rec[1];
}
value = encodeURIComponent(value);
if (!value) {
ctoken += ':';
} else {
var comp = me.compDict[value];
if (Ext.isDefined(comp)) {
ctoken += ":" + comp;
} else {
ctoken += ":=" + value;
}
}
});
return ctoken;
},
constructor: function(config){
var me = this;
me.callParent([config]);
me.UIState = me.decodeHToken(); // set default
var history_change_cb = function(token) {
//console.log("HC " + token);
if (!token) {
var res = window.confirm(gettext('Are you sure you want to navigate away from this page?'));
if (res){
// process text value and close...
Ext.History.back();
} else {
Ext.History.forward();
}
return;
}
var 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);
});
};
var start_token = Ext.History.getToken();
if (start_token) {
history_change_cb(start_token);
} else {
var htext = me.encodeHToken(me.UIState);
Ext.History.add(htext);
}
Ext.History.on('change', history_change_cb);
},
get: function(name, defaultValue){
/*jslint confusion: true */
var me = this;
var 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: {} };
}
}
//console.log("GET " + name + " " + Ext.encode(data));
return data;
},
clear: function(name){
var me = this;
if (typeof me.UIState[name] != "undefined") {
me.UIState[name] = null;
}
me.callParent(arguments);
},
set: function(name, value){
var me = this;
//console.log("SET " + name + " " + Ext.encode(value));
if (typeof me.UIState[name] != "undefined") {
var newvalue = value ? value.value : null;
if (me.setHV(name, newvalue, false)) {
var htext = me.encodeHToken(me.UIState);
Ext.History.add(htext);
}
} else {
me.callParent(arguments);
}
}
});
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() {
var me = this;
if (me.handler) {
me.setHandler(me.handler, me.scope);
}
me.callParent();
},
setHandler: function(fn, scope) {
var me = this;
me.scope = scope;
me.handler = function(button, e) {
var rec, msg;
if (me.confirmMsg) {
msg = 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: msg,
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() {
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 guestType = me.pveSelNode.data.type;
if (guestType !== 'qemu' && guestType != 'lxc') {
throw "invalid guest type";
}
var vmname = me.pveSelNode.data.name;
var template = me.pveSelNode.data.template;
var vm_command = function(cmd, params) {
Proxmox.Utils.API2Request({
params: params,
url: '/nodes/' + nodename + '/' + guestType + '/' + vmid + "/status/" + cmd,
method: 'POST',
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
};
me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + vmid;
me.items = [
{
text: gettext('Migrate'),
iconCls: 'fa fa-fw fa-send-o',
handler: function() {
var win = Ext.create('PVE.window.Migrate', {
vmtype: guestType,
nodename: nodename,
vmid: vmid
});
win.show();
}
},
{
text: gettext('Clone'),
iconCls: 'fa fa-fw fa-clone',
handler: function() {
var win = Ext.create('PVE.window.Clone', {
nodename: nodename,
guestType: guestType,
vmid: vmid,
isTemplate: template
});
win.show();
}
}
];
me.callParent();
}
});
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() {
var me = this;
var consoles = {
spice: me.enableSpice,
xtermjs: me.enableXtermjs
};
PVE.Utils.openDefaultConsoleWindow(consoles, 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) {
var me = this.up('button');
PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
}
},
{
xterm: 'menuitem',
itemId: 'spicemenu',
text: 'SPICE',
type: 'vv',
iconCls: 'pve-itype-icon-virt-viewer',
handler: function(button) {
var me = this.up('button');
PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
}
},
{
text: 'xterm.js',
itemId: 'xtermjs',
iconCls: 'pve-itype-icon-xtermjs',
type: 'xtermjs',
handler: function(button) {
var me = this.up('button');
PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
}
}
],
initComponent: function() {
var me = this;
if (!me.nodename) {
throw "no node name specified";
}
me.callParent();
}
});
/* 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) { },
// 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
/*jslint confusion: true*/
if (Ext.isFunction(me.confirmMsg)) {
msg = me.confirmMsg(rec);
}
/*jslint confusion: false*/
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() {
/*jslint confusion: true */
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) {
var hasBackups = Ext.Array.contains(value, 'backup');
var maxfiles = this.lookupReference('maxfiles');
if (!maxfiles) {
return;
}
if (!hasBackups) {
// clear values which will never be submitted
maxfiles.reset();
}
maxfiles.setDisabled(!hasBackups);
}
}
}
});
Ext.define('PVE.qemu.CmdMenu', {
extend: 'Ext.menu.Menu',
showSeparator: false,
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 vmname = me.pveSelNode.data.name;
var vm_command = function(cmd, params) {
Proxmox.Utils.API2Request({
params: params,
url: '/nodes/' + nodename + '/qemu/' + vmid + "/status/" + cmd,
method: 'POST',
failure: function(response, opts) {
Ext.Msg.alert('Error', response.htmlStatus);
}
});
};
var caps = Ext.state.Manager.get('GuiCap');
var running = false;
var stopped = true;
var suspended = false;
var standalone = PVE.data.ResourceStore.getNodes().length < 2;
switch (me.pveSelNode.data.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 " + vmid;
me.items = [
{
text: gettext('Start'),
iconCls: 'fa fa-fw fa-play',
hidden: running || suspended,
disabled: running || suspended,
handler: function() {
vm_command('start');
}
},
{
text: gettext('Pause'),
iconCls: 'fa fa-fw fa-pause',
hidden: stopped || suspended,
disabled: stopped || suspended,
handler: function() {
var msg = Proxmox.Utils.format_task_description('qmpause', vmid);
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
if (btn !== 'yes') {
return;
}
vm_command('suspend');
});
}
},
{
text: gettext('Hibernate'),
iconCls: 'fa fa-fw fa-download',
hidden: stopped || suspended,
disabled: stopped || suspended,
tooltip: gettext('Suspend to disk'),
handler: function() {
var msg = Proxmox.Utils.format_task_description('qmsuspend', vmid);
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
if (btn !== 'yes') {
return;
}
vm_command('suspend', { todisk: 1 });
});
}
},
{
text: gettext('Resume'),
iconCls: 'fa fa-fw fa-play',
hidden: !suspended,
handler: function() {
vm_command('resume');
}
},
{
text: gettext('Shutdown'),
iconCls: 'fa fa-fw fa-power-off',
disabled: stopped || suspended,
handler: function() {
var msg = Proxmox.Utils.format_task_description('qmshutdown', vmid);
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
if (btn !== 'yes') {
return;
}
vm_command('shutdown');
});
}
},
{
text: gettext('Stop'),
iconCls: 'fa fa-fw fa-stop',
disabled: stopped,
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
handler: function() {
var msg = Proxmox.Utils.format_task_description('qmstop', vmid);
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
if (btn !== 'yes') {
return;
}
vm_command("stop");
});
}
},
{
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() {
var win = Ext.create('PVE.window.Migrate', {
vmtype: 'qemu',
nodename: nodename,
vmid: vmid
});
win.show();
}
},
{
text: gettext('Clone'),
iconCls: 'fa fa-fw fa-clone',
hidden: !caps.vms['VM.Clone'],
handler: function() {
PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'qemu');
}
},
{
text: gettext('Convert to template'),
iconCls: 'fa fa-fw fa-file-o',
hidden: !caps.vms['VM.Allocate'],
handler: function() {
var msg = Proxmox.Utils.format_task_description('qmtemplate', vmid);
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
if (btn !== 'yes') {
return;
}
Proxmox.Utils.API2Request({
url: '/nodes/' + nodename + '/qemu/' + vmid + '/template',
method: 'POST',
failure: function(response, opts) {
Ext.Msg.alert('Error', response.htmlStatus);
}
});
});
}
},
{ xtype: 'menuseparator' },
{
text: gettext('Console'),
iconCls: 'fa fa-fw fa-terminal',
handler: function() {
Proxmox.Utils.API2Request({
url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current',
failure: function(response, opts) {
Ext.Msg.alert('Error', response.htmlStatus);
},
success: function(response, opts) {
var allowSpice = response.result.data.spice;
var allowXtermjs = response.result.data.serial;
var consoles = {
spice: allowSpice,
xtermjs: allowXtermjs
};
PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
}
});
}
}
];
me.callParent();
}
});
Ext.define('PVE.lxc.CmdMenu', {
extend: 'Ext.menu.Menu',
showSeparator: false,
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 CT ID specified";
}
var vmname = me.pveSelNode.data.name;
var vm_command = function(cmd, params) {
Proxmox.Utils.API2Request({
params: params,
url: '/nodes/' + nodename + '/lxc/' + vmid + "/status/" + cmd,
method: 'POST',
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
};
var caps = Ext.state.Manager.get('GuiCap');
var running = false;
var stopped = true;
var suspended = false;
var standalone = PVE.data.ResourceStore.getNodes().length < 2;
switch (me.pveSelNode.data.status) {
case 'running':
running = true;
stopped = false;
break;
case 'paused':
stopped = false;
suspended = true;
break;
default: break;
}
me.title = 'CT ' + vmid;
me.items = [
{
text: gettext('Start'),
iconCls: 'fa fa-fw fa-play',
disabled: running,
handler: function() {
vm_command('start');
}
},
// {
// text: gettext('Suspend'),
// iconCls: 'fa fa-fw fa-pause',
// hidde: suspended,
// disabled: stopped || suspended,
// handler: function() {
// var msg = Proxmox.Utils.format_task_description('vzsuspend', vmid);
// Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
// if (btn !== 'yes') {
// return;
// }
//
// vm_command('suspend');
// });
// }
// },
// {
// text: gettext('Resume'),
// iconCls: 'fa fa-fw fa-play',
// hidden: !suspended,
// handler: function() {
// vm_command('resume');
// }
// },
{
text: gettext('Shutdown'),
iconCls: 'fa fa-fw fa-power-off',
disabled: stopped || suspended,
handler: function() {
var msg = Proxmox.Utils.format_task_description('vzshutdown', vmid);
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
if (btn !== 'yes') {
return;
}
vm_command('shutdown');
});
}
},
{
text: gettext('Stop'),
iconCls: 'fa fa-fw fa-stop',
disabled: stopped,
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
handler: function() {
var msg = Proxmox.Utils.format_task_description('vzstop', vmid);
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
if (btn !== 'yes') {
return;
}
vm_command("stop");
});
}
},
{
xtype: 'menuseparator',
hidden: standalone || !caps.vms['VM.Migrate']
},
{
text: gettext('Clone'),
iconCls: 'fa fa-fw fa-clone',
hidden: !caps.vms['VM.Clone'],
handler: function() {
PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'lxc');
}
},
{
text: gettext('Migrate'),
iconCls: 'fa fa-fw fa-send-o',
hidden: standalone || !caps.vms['VM.Migrate'],
handler: function() {
var win = Ext.create('PVE.window.Migrate', {
vmtype: 'lxc',
nodename: nodename,
vmid: vmid
});
win.show();
}
},
{
text: gettext('Convert to template'),
iconCls: 'fa fa-fw fa-file-o',
handler: function() {
var msg = Proxmox.Utils.format_task_description('vztemplate', vmid);
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
if (btn !== 'yes') {
return;
}
Proxmox.Utils.API2Request({
url: '/nodes/' + nodename + '/lxc/' + vmid + '/template',
method: 'POST',
failure: function(response, opts) {
Ext.Msg.alert('Error', response.htmlStatus);
}
});
});
}
},
{ xtype: 'menuseparator' },
{
text: gettext('Console'),
iconCls: 'fa fa-fw fa-terminal',
handler: function() {
PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
}
}
];
me.callParent();
}
});
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() {
var me = this.up('menu');
var wiz = Ext.create('PVE.qemu.CreateWizard', {
nodename: me.nodename
});
wiz.show();
}
},
{
text: gettext('Create CT'),
itemId: 'createct',
iconCls: 'fa fa-cube',
handler: function() {
var me = this.up('menu');
var wiz = Ext.create('PVE.lxc.CreateWizard', {
nodename: me.nodename
});
wiz.show();
}
},
{ xtype: 'menuseparator' },
{
text: gettext('Bulk Start'),
itemId: 'bulkstart',
iconCls: 'fa fa-fw fa-play',
handler: function() {
var me = this.up('menu');
var win = Ext.create('PVE.window.BulkAction', {
nodename: me.nodename,
title: gettext('Bulk Start'),
btnText: gettext('Start'),
action: 'startall'
});
win.show();
}
},
{
text: gettext('Bulk Stop'),
itemId: 'bulkstop',
iconCls: 'fa fa-fw fa-stop',
handler: function() {
var me = this.up('menu');
var win = Ext.create('PVE.window.BulkAction', {
nodename: me.nodename,
title: gettext('Bulk Stop'),
btnText: gettext('Stop'),
action: 'stopall'
});
win.show();
}
},
{
text: gettext('Bulk Migrate'),
itemId: 'bulkmigrate',
iconCls: 'fa fa-fw fa-send-o',
handler: function() {
var me = this.up('menu');
var win = Ext.create('PVE.window.BulkAction', {
nodename: me.nodename,
title: gettext('Bulk Migrate'),
btnText: gettext('Migrate'),
action: 'migrateall'
});
win.show();
}
},
{ xtype: 'menuseparator' },
{
text: gettext('Shell'),
itemId: 'shell',
iconCls: 'fa fa-fw fa-terminal',
handler: function() {
var me = this.up('menu');
PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, me.nodename, undefined);
}
},
{ xtype: 'menuseparator' },
{
text: gettext('Wake-on-LAN'),
itemId: 'wakeonlan',
iconCls: 'fa fa-fw fa-power-off',
handler: function() {
var me = this.up('menu');
Proxmox.Utils.API2Request({
param: {},
url: '/nodes/' + me.nodename + '/wakeonlan',
method: 'POST',
failure: function(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}'"), me.nodename, response.result.data)
});
}
});
}
}
],
initComponent: function() {
var me = this;
if (!me.nodename) {
throw 'no nodename specified';
}
me.title = gettext('Node') + " '" + me.nodename + "'";
me.callParent();
var caps = Ext.state.Manager.get('GuiCap');
// disable not allowed options
if (!caps.vms['VM.Allocate']) {
me.getComponent('createct').setDisabled(true);
me.getComponent('createvm').setDisabled(true);
}
if (!caps.nodes['Sys.PowerMgmt']) {
me.getComponent('bulkstart').setDisabled(true);
me.getComponent('bulkstop').setDisabled(true);
me.getComponent('bulkmigrate').setDisabled(true);
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);
}
}
});
Ext.define('PVE.noVncConsole', {
extend: 'Ext.panel.Panel',
alias: 'widget.pveNoVncConsole',
nodename: undefined,
vmid: undefined,
cmd: undefined,
consoleType: undefined, // lxc, kvm, shell, cmd
layout: 'fit',
xtermjs: false,
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" });
var type = me.xtermjs ? 'xtermjs' : 'novnc';
Ext.apply(me, {
items: box,
listeners: {
activate: function() {
var queryDict = {
console: me.consoleType, // kvm, lxc, upgrade or shell
vmid: me.vmid,
node: me.nodename,
cmd: me.cmd,
resize: 'scale'
};
queryDict[type] = 1;
PVE.Utils.cleanEmptyObjectKeys(queryDict);
var url = '/?' + Ext.Object.toQueryString(queryDict);
box.load(url);
}
}
});
me.callParent();
me.on('afterrender', function() {
me.focus();
});
}
});
Ext.define('PVE.data.PermPathStore', {
extend: 'Ext.data.Store',
alias: 'store.pvePermPath',
fields: [ 'value' ],
autoLoad: false,
data: [
{'value': '/'},
{'value': '/access'},
{'value': '/nodes'},
{'value': '/pool'},
{'value': '/storage'},
{'value': '/vms'}
],
constructor: function(config) {
var me = this;
config = config || {};
me.callParent([config]);
me.suspendEvents();
PVE.data.ResourceStore.each(function(record) {
switch (record.get('type')) {
case 'node':
me.add({value: '/nodes/' + record.get('text')});
break;
case 'qemu':
me.add({value: '/vms/' + record.get('vmid')});
break;
case 'lxc':
me.add({value: '/vms/' + record.get('vmid')});
break;
case 'storage':
me.add({value: '/storage/' + record.get('storage')});
break;
case 'pool':
me.add({value: '/pool/' + record.get('pool')});
break;
}
});
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,
findVMID: function(vmid) {
var me = this, i;
return (me.findExact('vmid', parseInt(vmid, 10)) >= 0);
},
// returns the cached data from all nodes
getNodes: function() {
var me = this;
var nodes = [];
me.each(function(record) {
if (record.get('type') == "node") {
nodes.push( record.getData() );
}
});
return nodes;
},
storageIsShared: function(storage_path) {
var me = this;
var index = me.findExact('id', storage_path);
return me.getAt(index).data.shared;
},
guestNode: function(vmid) {
var me = this;
var index = me.findExact('vmid', parseInt(vmid, 10));
return me.getAt(index).data.node;
},
constructor: function(config) {
// fixme: how to avoid those warnings
/*jslint confusion: true */
var me = this;
config = config || {};
var 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) {
var info = record.data;
var text;
if (value) {
return value;
}
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: PVE.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
},
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: PVE.Utils.render_size,
hidden: true,
sortable: true,
width: 100
},
cpu: {
header: gettext('CPU usage'),
type: 'float',
renderer: PVE.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
}
};
var fields = [];
var 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() {
var res = [];
Ext.Object.each(field_defaults, function(field, info) {
var fi = Ext.apply({ dataIndex: field }, info);
res.push(fi);
});
return res;
},
fieldNames: fieldNames
});
me.callParent([config]);
}
});
Ext.define('pve-domains', {
extend: "Ext.data.Model",
fields: [
'realm', 'type', 'comment', 'default', 'tfa',
{
name: 'descr',
// Note: We use this in the RealmComboBox.js (see Bug #125)
convert: function(value, record) {
if (value) {
return value;
}
var info = record.data;
// return realm if there is no comment
var text = info.comment || info.realm;
if (info.tfa) {
text += " (+ " + info.tfa + ")";
}
return Ext.String.htmlEncode(text);
}
}
],
idProperty: 'realm',
proxy: {
type: 'proxmox',
url: "/api2/json/access/domains"
}
});
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',
{ 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',
'mem',
'maxmem',
'disk',
'maxdisk',
'diskread',
'diskwrite',
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
]
});
Ext.define('pve-rrd-storage', {
extend: 'Ext.data.Model',
fields: [
'used',
'total',
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
]
});
Ext.define('PVE.form.VlanField', {
extend: 'Ext.form.field.Number',
alias: ['widget.pveVlanField'],
deleteEmpty: false,
emptyText: '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();
}
});
// boolean type including 'Default' (delete property from file)
Ext.define('PVE.form.Boolean', {
extend: 'Proxmox.form.KVComboBox',
alias: ['widget.booleanfield'],
comboItems: [
['__default__', gettext('Default')],
[1, gettext('Yes')],
[0, gettext('No')]
]
});
Ext.define('PVE.form.CompressionSelector', {
extend: 'Proxmox.form.KVComboBox',
alias: ['widget.pveCompressionSelector'],
comboItems: [
['0', Proxmox.Utils.noneText],
['lzo', 'LZO (' + gettext('fast') + ')'],
['gzip', 'GZIP (' + gettext('good') + ')']
]
});
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.PrivilegesSelector', {
extend: 'Proxmox.form.KVComboBox',
xtype: 'pvePrivilegesSelector',
multiSelect: true,
initComponent: function() {
var me = this;
// So me.store is available.
me.callParent();
Proxmox.Utils.API2Request({
url: '/access/roles/Administrator',
method: 'GET',
success: function(response, options) {
var data = [], key;
/*jslint forin: true */
for (key in response.result.data) {
data.push([key, key]);
}
/*jslint forin: false */
me.store.setData(data);
me.store.sort({
property: 'key',
direction: 'ASC'
});
},
failure: function (response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
});
}
});
Ext.define('pve-groups', {
extend: 'Ext.data.Model',
fields: [ 'groupid', 'comment' ],
proxy: {
type: 'proxmox',
url: "/api2/json/access/groups"
},
idProperty: 'groupid'
});
Ext.define('PVE.form.GroupSelector', {
extend: 'Proxmox.form.ComboGrid',
xtype: 'pveGroupSelector',
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
}
]
},
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.UserSelector', {
extend: 'Proxmox.form.ComboGrid',
alias: ['widget.pveUserSelector'],
allowBlank: false,
autoSelect: false,
valueField: 'userid',
displayField: 'userid',
editable: true,
anyMatch: true,
forceSelection: true,
initComponent: function() {
var me = this;
var store = new Ext.data.Store({
model: 'pve-users',
sorters: [{
property: 'userid'
}]
});
Ext.apply(me, {
store: store,
listConfig: {
columns: [
{
header: gettext('User'),
sortable: true,
dataIndex: 'userid',
flex: 1
},
{
header: gettext('Name'),
sortable: true,
renderer: PVE.Utils.render_full_name,
dataIndex: 'firstname',
flex: 1
},
{
header: gettext('Comment'),
sortable: false,
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 1
}
]
}
});
me.callParent();
store.load({ params: { enabled: 1 }});
}
}, function() {
Ext.define('pve-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"
},
idProperty: 'userid'
});
});
Ext.define('PVE.form.RoleSelector', {
extend: 'Proxmox.form.ComboGrid',
alias: ['widget.pveRoleSelector'],
allowBlank: false,
autoSelect: false,
valueField: 'roleid',
displayField: 'roleid',
initComponent: function() {
var me = this;
var store = new Ext.data.Store({
model: 'pve-roles',
sorters: [{
property: 'roleid'
}]
});
Ext.apply(me, {
store: store,
listConfig: {
columns: [
{
header: gettext('Role'),
sortable: true,
dataIndex: 'roleid',
flex: 1
}
]
}
});
me.callParent();
store.load();
}
}, function() {
Ext.define('pve-roles', {
extend: 'Ext.data.Model',
fields: [ 'roleid', 'privs' ],
proxy: {
type: 'proxmox',
url: "/api2/json/access/roles"
},
idProperty: 'roleid'
});
});
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 exists');
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.MemoryField', {
extend: 'Ext.form.field.Number',
alias: 'widget.pveMemoryField',
allowBlank: false,
hotplug: false,
minValue: 32,
maxValue: 4178944,
step: 32,
value: '512', // qm 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) {
var res = me.computeUpDown(me.getValue());
me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue));
}
},
onSpinDown: function() {
var me = this;
if (!me.readOnly) {
var 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.NetworkCardSelector', {
extend: 'Proxmox.form.KVComboBox',
alias: 'widget.pveNetworkCardSelector',
comboItems: [
['e1000', 'Intel E1000'],
['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'],
['rtl8139', 'Realtek RTL8139'],
['vmxnet3', 'VMware vmxnet3']
]
});
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.DiskSelector', {
extend: 'Proxmox.form.ComboGrid',
xtype: 'pveDiskSelector',
// can be
// undefined: all
// unused: only unused
// journal_disk: all disks with gpt
diskType: undefined,
valueField: 'devpath',
displayField: 'devpath',
emptyText: gettext('No Disks unused'),
listConfig: {
columns: [
{
header: gettext('Device'),
width: 80,
sortable: true,
dataIndex: 'devpath'
},
{
header: gettext('Size'),
width: 60,
sortable: false,
renderer: Proxmox.Utils.format_size,
dataIndex: 'size'
},
{
header: gettext('Serial'),
flex: 1,
sortable: true,
dataIndex: 'serial'
}
]
},
initComponent: function() {
var me = this;
var nodename = me.nodename;
if (!nodename) {
throw "no node name specified";
}
var store = Ext.create('Ext.data.Store', {
filterOnLoad: true,
model: 'pve-disk-list',
proxy: {
type: 'proxmox',
url: "/api2/json/nodes/" + nodename + "/disks/list",
extraParams: { type: me.diskType }
},
sorters: [
{
property : 'devpath',
direction: 'ASC'
}
]
});
Ext.apply(me, {
store: store
});
me.callParent();
store.load();
}
}, function() {
Ext.define('pve-disk-list', {
extend: 'Ext.data.Model',
fields: [ 'devpath', 'used', { name: 'size', type: 'number'},
{name: 'osdid', type: 'number'},
'vendor', 'model', 'serial'],
idProperty: 'devpath'
});
});
Ext.define('PVE.form.BusTypeSelector', {
extend: 'Proxmox.form.KVComboBox',
alias: 'widget.pveBusSelector',
noVirtIO: false,
initComponent: function() {
var me = this;
me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']];
if (!me.noVirtIO) {
me.comboItems.push(['virtio', 'VirtIO Block']);
}
me.comboItems.push(['scsi', 'SCSI']);
me.callParent();
}
});
Ext.define('PVE.form.ControllerSelector', {
extend: 'Ext.form.FieldContainer',
alias: 'widget.pveControllerSelector',
statics: {
maxIds: {
ide: 3,
sata: 5,
virtio: 15,
scsi: 13
}
},
noVirtIO: false,
vmconfig: {}, // used to check for existing devices
sortByPreviousUsage: function(vmconfig, controllerList) {
var usedControllers = Ext.clone(PVE.form.ControllerSelector.maxIds);
var type;
for (type in usedControllers) {
if(usedControllers.hasOwnProperty(type)) {
usedControllers[type] = 0;
}
}
var property;
for (property in vmconfig) {
if (vmconfig.hasOwnProperty(property)) {
if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) {
var foundController = property.match(PVE.Utils.bus_match)[1];
usedControllers[foundController]++;
}
}
}
var vmDefaults = PVE.qemu.OSDefaults[vmconfig.ostype];
var sortPriority = vmDefaults && vmDefaults.busPriority
? vmDefaults.busPriority : PVE.qemu.OSDefaults.generic;
var 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;
},
setVMConfig: function(vmconfig, autoSelect) {
var me = this;
me.vmconfig = Ext.apply({}, vmconfig);
var clist = ['ide', 'virtio', 'scsi', 'sata'];
var bussel = me.down('field[name=controller]');
var deviceid = me.down('field[name=deviceid]');
if (autoSelect === 'cdrom') {
clist = ['ide', 'scsi', 'sata'];
if (!Ext.isDefined(me.vmconfig.ide2)) {
bussel.setValue('ide');
deviceid.setValue(2);
return;
}
} else {
// in most cases we want to add a disk to the same controller
// we previously used
clist = me.sortByPreviousUsage(me.vmconfig, clist);
}
Ext.Array.each(clist, function(controller) {
var confid, i;
bussel.setValue(controller);
for (i = 0; i <= PVE.form.ControllerSelector.maxIds[controller]; i++) {
confid = controller + i.toString();
if (!Ext.isDefined(me.vmconfig[confid])) {
deviceid.setValue(i);
return false; // break
}
}
});
deviceid.validate();
},
initComponent: function() {
var me = this;
Ext.apply(me, {
fieldLabel: gettext('Bus/Device'),
layout: 'hbox',
defaults: {
hideLabel: true
},
items: [
{
xtype: 'pveBusSelector',
name: 'controller',
value: PVE.qemu.OSDefaults.generic.busType,
noVirtIO: me.noVirtIO,
allowBlank: false,
flex: 2,
listeners: {
change: function(t, value) {
if (!value) {
return;
}
var field = me.down('field[name=deviceid]');
field.setMaxValue(PVE.form.ControllerSelector.maxIds[value]);
field.validate();
}
}
},
{
xtype: 'proxmoxintegerfield',
name: 'deviceid',
minValue: 0,
maxValue: PVE.form.ControllerSelector.maxIds.ide,
value: '0',
flex: 1,
allowBlank: false,
validator: function(value) {
/*jslint confusion: true */
if (!me.rendered) {
return;
}
var field = me.down('field[name=controller]');
var controller = field.getValue();
var confid = controller + value;
if (Ext.isDefined(me.vmconfig[confid])) {
return "This device is already in use.";
}
return true;
}
}
]
});
me.callParent();
}
});
Ext.define('PVE.form.EmailNotificationSelector', {
extend: 'Proxmox.form.KVComboBox',
alias: ['widget.pveEmailNotificationSelector'],
comboItems: [
['always', gettext('Always')],
['failure', gettext('On failure only')]
]
});
/*global Proxmox*/
Ext.define('PVE.form.RealmComboBox', {
extend: 'Ext.form.field.ComboBox',
alias: ['widget.pveRealmComboBox'],
controller: {
xclass: 'Ext.app.ViewController',
init: function(view) {
view.store.on('load', this.onLoad, view);
},
onLoad: function(store, records, success) {
if (!success) {
return;
}
var me = this;
var val = me.getValue();
if (!val || !me.store.findRecord('realm', val)) {
var def = 'pam';
Ext.each(records, function(rec) {
if (rec.data && rec.data['default']) {
def = rec.data.realm;
}
});
me.setValue(def);
}
}
},
fieldLabel: gettext('Realm'),
name: 'realm',
queryMode: 'local',
allowBlank: false,
editable: false,
forceSelection: true,
autoSelect: false,
triggerAction: 'all',
valueField: 'realm',
displayField: 'descr',
getState: function() {
return { value: this.getValue() };
},
applyState : function(state) {
if (state && state.value) {
this.setValue(state.value);
}
},
stateEvents: [ 'select' ],
stateful: true, // last chosen auth realm is saved between page reloads
id: 'pveloginrealm', // We need stable ids when using stateful, not autogenerated
stateID: 'pveloginrealm',
needOTP: function(realm) {
var me = this;
// use exact match
var rec = me.store.findRecord('realm', realm, 0, false, false, true);
return rec && rec.data && rec.data.tfa ? rec.data.tfa : undefined;
},
store: {
model: 'pve-domains',
autoLoad: true
}
});
/*
* 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() {
var me = this;
var default_views = {
server: {
text: gettext('Server View'),
groups: ['node']
},
folder: {
text: gettext('Folder View'),
groups: ['type']
},
storage: {
text: gettext('Storage View'),
groups: ['node'],
filterfn: function(node) {
return node.data.type === 'storage' || node.data.type === 'node';
}
},
pool: {
text: gettext('Pool View'),
groups: ['pool'],
// Pool View only lists VMs and Containers
filterfn: function(node) {
return node.data.type === 'qemu' || node.data.type === 'lxc' || node.data.type === 'openvz' ||
node.data.type === 'pool';
}
}
};
var groupdef = [];
Ext.Object.each(default_views, function(viewname, value) {
groupdef.push([viewname, value.text]);
});
var 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() {
var view = me.getValue();
return Ext.apply({ id: view }, default_views[view] || default_views.server);
},
getState: function() {
return { value: me.getValue() };
},
applyState : function(state, doSelect) {
var view = me.getValue();
if (state && state.value && (view != state.value)) {
var record = store.findRecord('key', state.value);
if (record) {
me.setValue(state.value, true);
if (doSelect) {
me.fireEvent('select', me, [record]);
}
}
}
},
stateEvents: [ 'select' ],
stateful: true,
stateId: 'pveview',
id: 'view'
});
me.callParent();
var statechange = function(sp, key, value) {
if (key === me.id) {
me.applyState(value, true);
}
};
var sp = Ext.state.Manager.getProvider();
me.mon(sp, 'statechange', statechange, me);
}
});
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,
// 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: '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: PVE.Utils.render_cpu,
sortable: true,
width: 100,
dataIndex: 'cpu'
}
]
},
validator: function(value) {
/*jslint confusion: true */
var me = this;
if (!me.onlineValidator || (me.allowBlank && !value)) {
return true;
}
var offline = [];
var notAllowed = [];
Ext.Array.each(value.split(/\s*,\s*/), function(node) {
var rec = me.store.findRecord(me.valueField, node);
if (!(rec && rec.data) || !Ext.isNumeric(rec.data.mem)) {
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();
// filter out disallowed nodes
me.getStore().addFilter(new Ext.util.Filter({
filterFn: function(item) {
if (Ext.isArray(me.disallowedNodes)) {
return !Ext.Array.contains(me.disallowedNodes, item.data.node);
} else {
return true;
}
}
}));
me.mon(me.getStore(), 'load', function(){
me.isValid();
});
}
});
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
});
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',
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.StorageSelector', {
extend: 'Proxmox.form.ComboGrid',
alias: 'widget.pveStorageSelector',
allowBlank: false,
valueField: 'storage',
displayField: 'storage',
listConfig: {
columns: [
{
header: gettext('Name'),
dataIndex: 'storage',
hideable: false,
flex: 1
},
{
header: gettext('Type'),
width: 60,
dataIndex: 'type'
},
{
header: gettext('Avail'),
width: 80,
dataIndex: 'avail',
renderer: Proxmox.Utils.format_size
},
{
header: gettext('Capacity'),
width: 80,
dataIndex: 'total',
renderer: Proxmox.Utils.format_size
}
]
},
reloadStorageList: function() {
var me = this;
if (!me.nodename) {
return;
}
var params = {
format: 1
};
var url = '/api2/json/nodes/' + me.nodename + '/storage';
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: url,
extraParams: params
});
me.store.load();
},
setTargetNode: function(targetNode) {
var me = this;
if (!targetNode || (me.targetNode === targetNode)) {
return;
}
me.targetNode = targetNode;
me.reloadStorageList();
},
setNodename: function(nodename) {
var me = this;
if (!nodename || (me.nodename === nodename)) {
return;
}
me.nodename = nodename;
me.reloadStorageList();
},
initComponent: function() {
var me = this;
var nodename = me.nodename;
me.nodename = undefined;
var store = Ext.create('Ext.data.Store', {
model: 'pve-storage-status',
sorters: {
property: 'storage',
order: 'DESC'
}
});
Ext.apply(me, {
store: store
});
me.callParent();
if (nodename) {
me.setNodename(nodename);
}
}
}, function() {
Ext.define('pve-storage-status', {
extend: 'Ext.data.Model',
fields: [ 'storage', 'active', 'type', 'avail', 'total' ],
idProperty: 'storage'
});
});
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,
// sets the intial 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;
}
var selectformat = false;
if (rec.data.format) {
var format = rec.data.format[0]; // 0 is the formats, 1 the default in the backend
delete format.subvol; // we never need subvol in the gui
selectformat = (Ext.Object.getSize(format) > 1);
}
var select = !!rec.data.select_existing && !me.hideSelection;
formatsel.setDisabled(!selectformat);
formatsel.setValue(selectformat ? 'qcow2' : 'raw');
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',
reference: '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',
reference: 'hdimage',
itemId: 'hdimage',
fieldLabel: gettext('Disk image'),
nodename: me.nodename,
disabled: true,
hidden: true
},
{
xtype: 'numberfield',
itemId: 'disksize',
reference: 'disksize',
name: 'disksize',
fieldLabel: gettext('Disk size') + ' (GiB)',
hidden: me.hideSize,
disabled: me.hideSize,
minValue: 0.001,
maxValue: 128*1024,
decimalPrecision: 3,
value: me.defaultSize,
allowBlank: false
},
{
xtype: 'pveDiskFormatSelector',
itemId: 'diskformat',
reference: 'diskformat',
name: 'diskformat',
fieldLabel: gettext('Format'),
nodename: me.nodename,
disabled: true,
hidden: 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.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.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: {
width: 800,
columns: [
{
header: 'ID',
dataIndex: 'id',
width: 80
},
{
header: gettext('IOMMU Group'),
dataIndex: 'iommugroup',
width: 50
},
{
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.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: {
columns: [
{
header: gettext('Type'),
dataIndex: 'type',
flex: 1
},
{
header: gettext('Available'),
dataIndex: 'available',
width: 80
},
{
header: gettext('Description'),
dataIndex: 'description',
flex: 1,
renderer: function(value) {
if (!value) {
return '';
}
return value.split('\n').join('
');
}
}
]
},
setPciID: function(pciid, force) {
var me = this;
if (!force && (!pciid || (me.pciid === pciid))) {
return;
}
me.pciid = pciid;
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.pciid + '/mdev'
});
me.store.load();
},
initComponent: function() {
var me = this;
if (!me.nodename) {
throw 'no node name specified';
}
me.callParent();
if (me.pciid) {
me.setPciID(me.pciid, true);
}
}
});
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',
order: 'DESC'
}
});
Ext.apply(me, {
store: store,
listConfig: {
columns: [
{
header: gettext('Security Group'),
dataIndex: 'group',
hideable: false,
width: 100
},
{
header: gettext('Comment'),
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 1
}
]
}
});
me.callParent();
}
});
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: 'ref',
displayField: 'ref',
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' ],
idProperty: 'ref',
proxy: {
type: 'proxmox',
url: url
},
sorters: {
property: 'ref',
order: 'DESC'
}
});
var disable_query_for_ips = function(f, value) {
if (value === null ||
value.match(/^\d/)) { // IP address starts with \d
f.queryDelay = 9999999999; // hack: disbale with long delay
} else {
f.queryDelay = 10;
}
};
var columns = [];
if (!me.ref_type) {
columns.push({
header: gettext('Type'),
dataIndex: 'type',
hideable: false,
width: 60
});
}
columns.push(
{
header: gettext('Name'),
dataIndex: 'ref',
hideable: false,
width: 140
},
{
header: gettext('Comment'),
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 1
}
);
Ext.apply(me, {
store: store,
listConfig: { columns: columns }
});
me.on('change', disable_query_for_ips);
me.callParent();
}
});
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.CPUModelSelector', {
extend: 'Proxmox.form.KVComboBox',
alias: ['widget.CPUModelSelector'],
comboItems: [
['__default__', Proxmox.Utils.defaultText + ' (kvm64)'],
['486', '486'],
['athlon', 'athlon'],
['core2duo', 'core2duo'],
['coreduo', 'coreduo'],
['kvm32', 'kvm32'],
['kvm64', 'kvm64'],
['pentium', 'pentium'],
['pentium2', 'pentium2'],
['pentium3', 'pentium3'],
['phenom', 'phenom'],
['qemu32', 'qemu32'],
['qemu64', 'qemu64'],
['Conroe', 'Conroe'],
['Penryn', 'Penryn'],
['Nehalem', 'Nehalem'],
['Westmere', 'Westmere'],
['SandyBridge', 'SandyBridge'],
['IvyBridge', 'IvyBridge'],
['Haswell', 'Haswell'],
['Haswell-noTSX','Haswell-noTSX'],
['Broadwell', 'Broadwell'],
['Broadwell-noTSX','Broadwell-noTSX'],
['Skylake-Client','Skylake-Client'],
['Opteron_G1', 'Opteron_G1'],
['Opteron_G2', 'Opteron_G2'],
['Opteron_G3', 'Opteron_G3'],
['Opteron_G4', 'Opteron_G4'],
['Opteron_G5', 'Opteron_G5'],
['EPYC', 'EPYC'],
['host', 'host']
]
});
Ext.define('PVE.form.VNCKeyboardSelector', {
extend: 'Proxmox.form.KVComboBox',
alias: ['widget.VNCKeyboardSelector'],
comboItems: PVE.Utils.kvm_keymap_array()
});
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.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.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'];
}
Ext.Array.each(me.cts, function(ct) {
me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]);
});
me.callParent();
}
});
Ext.define('PVE.form.HotplugFeatureSelector', {
extend: 'Ext.form.CheckboxGroup',
alias: 'widget.pveHotplugFeatureSelector',
columns: 1,
vertical: true,
defaults: {
name: 'hotplug',
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([{ hotplug: newVal }]);
},
// overide 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 */
/*jslint confusion: true*/
if (data.length === 0) {
return { 'hotplug':'0' };
} else {
return { 'hotplug': data.join(',') };
}
}
});
Ext.define('PVE.form.AgentFeatureSelector', {
extend: 'Proxmox.panel.InputPanel',
alias: ['widget.pveAgentFeatureSelector'],
initComponent: function() {
var me = this;
me.items= [
{
xtype: 'proxmoxcheckbox',
boxLabel: gettext('Qemu Agent'),
name: 'enabled',
uncheckedValue: 0,
listeners: {
change: function(f, value, old) {
var gtcb = me.down('proxmoxcheckbox[name=fstrim_cloned_disks]');
if (value) {
gtcb.setDisabled(false);
} else {
gtcb.setDisabled(true);
}
}
}
},
{
xtype: 'proxmoxcheckbox',
boxLabel: gettext('Run guest-trim after clone disk'),
name: 'fstrim_cloned_disks',
disabled: true
}
];
me.callParent();
},
onGetValues: function(values) {
var agentstr = PVE.Parser.printPropertyString(values, 'enabled');
return { agent: agentstr };
},
setValues: function(values) {
var agent = values.agent || '';
var res = PVE.Parser.parsePropertyString(agent, 'enabled');
this.callParent([res]);
}
});
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.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.BackupModeSelector', {
extend: 'Proxmox.form.KVComboBox',
alias: ['widget.pveBackupModeSelector'],
comboItems: [
['snapshot', gettext('Snapshot')],
['suspend', gettext('Suspend')],
['stop', gettext('Stop')]
]
});
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.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
*
* it filters and sorts the objects by the algorithm in
* the customFilter function
*
* also it does accept 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',
focusOnToFront: false,
floating: true,
emptyText: Proxmox.Utils.noneText,
width: 600,
height: 400,
scrollable: {
xtype: 'scroller',
y: true,
x:false
},
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);
},
/* because of lint */
focusleave: {
fn: 'hideMe'
},
focusenter: 'setFocus'
},
columns: [
{
text: gettext('Type'),
dataIndex: 'type',
width: 100,
renderer: PVE.Utils.render_resource_type
},
{
text: gettext('Description'),
flex: 1,
dataIndex: 'text'
},
{
text: gettext('Node'),
dataIndex: 'node'
},
{
text: gettext('Pool'),
dataIndex: 'pool'
}
]
},
customFilter: function(item) {
var me = this;
var match = 0;
var fieldArr = [];
var i,j, fields;
// different types of objects have different fields to search
// for example, a node will never have a pool and vice versa
switch (item.data.type) {
case 'pool': fieldArr = ['type', 'pool', 'text']; break;
case 'node': fieldArr = ['type', 'node', 'text']; break;
case 'storage': fieldArr = ['type', 'pool', 'node', 'storage']; break;
default: fieldArr = ['name', 'type', 'node', 'pool', 'vmid'];
}
if (me.filterVal === '') {
item.data.relevance = 0;
return true;
}
// all text is case insensitive and each word is
// searched alone
// for every partial match, the row gets
// 1 match point, for every exact match
// it gets 2 points
//
// results gets sorted by points (descending)
fields = me.filterVal.split(/\s+/);
for(i = 0; i < fieldArr.length; i++) {
var v = item.data[fieldArr[i]];
if (v !== undefined) {
v = v.toString().toLowerCase();
for(j = 0; j < fields.length; j++) {
if (v.indexOf(fields[j]) !== -1) {
match++;
if(v === fields[j]) {
match++;
}
}
}
}
}
// give the row the 'relevance' value
item.data.relevance = match;
return (match > 0);
},
updateFilter: function(field, newValue, oldValue) {
var 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) {
var me = this;
var records = [];
me.hasFocus = true;
me.grid.textfield = me;
me.grid.store.load();
me.grid.showBy(me, 'tl-bl');
},
hideGrid: function() {
var 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() {
var me = this;
if (!me.hasFocus) {
me.focus();
} else {
me.blur();
}
},
initComponent: function() {
var me = this;
if (!me.tree) {
throw "no tree given";
}
me.grid = Ext.create(me.grid);
me.callParent();
/*jslint confusion: true*/
/*because shift is also a function*/
// 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.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();
}
});
/*jslint confusion: true*/
/* 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',
autoLoad: true,
sorters: 'vmid',
filters: [{
property: 'type',
value: /lxc|qemu/
}]
},
columns: [
{
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')}
],
// due to EXTJS-18711
// we have to do a static list via a store
// but to avoid creating an object,
// we have to have a pseudo un function
un: function(){}
}
}
},
{
header: 'HA ' + gettext('Status'),
dataIndex: 'hastate',
flex: 1,
filter: {
type: 'list'
}
}
],
selModel: {
selType: 'checkboxmodel',
mode: 'SIMPLE'
},
checkChangeEvents: [
'selectionchange',
'change'
],
listeners: {
selectionchange: function() {
// to trigger validity and error checks
this.checkChange();
}
},
getValue: function() {
var me = this;
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;
},
setValue: function(value) {
console.log(value);
var me = this;
var sm = me.getSelectionModel();
if (!Ext.isArray(value)) {
value = value.split(',');
}
var selection = [];
var store = me.getStore();
value.forEach(function(item) {
var rec = store.findRecord('vmid',item, 0, false, true, true);
console.log(store);
if (rec) {
console.log(rec);
selection.push(rec);
}
});
sm.select(selection);
return me.mixins.field.setValue.call(me, value);
},
getErrors: function(value) {
var me = this;
if (me.allowBlank === false &&
me.getSelectionModel().getCount() === 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 [];
},
initComponent: function() {
var me = this;
me.callParent();
if (me.nodename) {
me.store.filters.add({
property: 'node',
exactMatch: true,
value: me.nodename
});
}
// only show the relevant guests by default
if (me.action) {
var statusfilter = '';
switch (me.action) {
case 'startall':
statusfilter = 'stopped';
break;
case 'stopall':
statusfilter = 'running';
break;
}
if (statusfilter !== '') {
me.store.filters.add({
property: 'template',
value: 0
},{
id: 'x-gridfilter-status',
operator: 'in',
property: 'status',
value: [statusfilter]
});
}
}
var store = me.getStore();
var sm = me.getSelectionModel();
if (me.selectAll) {
me.mon(store,'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.USBSelector', {
extend: 'Proxmox.form.ComboGrid',
alias: ['widget.pveUSBSelector'],
allowBlank: false,
autoSelect: false,
displayField: 'usbid',
valueField: 'usbid',
editable: true,
getUSBValue: function() {
var me = this;
var rec = me.store.findRecord('usbid', me.value);
var val = 'host='+ me.value;
if (rec && rec.data.speed === "5000") {
val = 'host=' + me.value + ",usb3=1";
}
return val;
},
validator: function(value) {
var me = this;
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 false;
},
initComponent: function() {
var me = this;
var nodename = me.pveSelNode.data.node;
if (!nodename) {
throw "no nodename specified";
}
if (me.type !== 'device' && me.type !== 'port') {
throw "no valid type specified";
}
var store = new Ext.data.Store({
model: 'pve-usb-' + me.type,
proxy: {
type: 'proxmox',
url: "/api2/json/nodes/" + nodename + "/scan/usb"
},
filters: [
function (item) {
return !!item.data.usbpath && !!item.data.prodid && item.data['class'] != 9;
}
]
});
Ext.apply(me, {
store: store,
listConfig: {
columns: [
{
header: (me.type === 'device')?gettext('Device'):gettext('Port'),
sortable: true,
dataIndex: 'usbid',
width: 80
},
{
header: gettext('Manufacturer'),
sortable: true,
dataIndex: 'manufacturer',
width: 100
},
{
header: gettext('Product'),
sortable: true,
dataIndex: 'product',
flex: 1
},
{
header: gettext('Speed'),
width: 70,
sortable: true,
dataIndex: 'speed',
renderer: function(value) {
if (value === "5000") {
return "USB 3.0";
} else if (value === "480") {
return "USB 2.0";
} else {
return "USB 1.x";
}
}
}
]
}
});
me.callParent();
store.load();
}
}, 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' }
]
});
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' }
]
});
});
Ext.define('PVE.form.CalendarEvent', {
extend: 'Ext.form.field.ComboBox',
xtype: 'pveCalendarEvent',
editable: true,
valueField: 'value',
displayField: 'text',
queryMode: 'local',
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: '2,22:30', text: gettext("Every day") + " 02:30, 22:30"},
{ value: 'mon..fri', text: gettext("Monday to Friday") + " 00:00"},
{ value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly")},
{ value: 'sun 01:00', text: gettext("Sunday") + " 01:00"}
]
},
tpl: [
'
' + Ext.String.format(gettext('{0} is not initialized.'), 'Ceph') + ' '+ gettext('You need to create a initial config once.') + '
'; } else { return '' +
Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + '
' +
gettext('Would you like to install it now?') + '
' + Ext.htmlEncode(errors[name]) + '
'; metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html.replace(/\"/g,'"') + '"'; } return value; }; var columns = [ { // similar to xtype: 'rownumberer', dataIndex: 'pos', resizable: false, width: 23, sortable: false, align: 'right', hideable: false, menuDisabled: true, renderer: function(value, metaData, record, rowIdx, colIdx, store) { metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; if (value >= 0) { return value; } return ''; } }, { xtype: 'checkcolumn', header: gettext('Enable'), 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: 50 }, { header: gettext('Type'), dataIndex: 'type', renderer: function(value, metaData, record) { return render_errors('type', value, metaData, record); }, width: 50 }, { header: gettext('Action'), dataIndex: 'action', renderer: function(value, metaData, record) { return render_errors('action', value, metaData, record); }, width: 80 }, { header: gettext('Macro'), dataIndex: 'macro', renderer: function(value, metaData, record) { return render_errors('macro', value, metaData, record); }, width: 80 } ]; if (me.allow_iface) { columns.push({ header: gettext('Interface'), dataIndex: 'iface', renderer: function(value, metaData, record) { return render_errors('iface', value, metaData, record); }, width: 80 }); } columns.push( { header: gettext('Source'), dataIndex: 'source', renderer: function(value, metaData, record) { return render_errors('source', value, metaData, record); }, width: 100 }, { header: gettext('Destination'), dataIndex: 'dest', renderer: function(value, metaData, record) { return render_errors('dest', value, metaData, record); }, width: 100 }, { header: gettext('Protocol'), dataIndex: 'proto', renderer: function(value, metaData, record) { return render_errors('proto', value, metaData, record); }, width: 100 }, { header: gettext('Dest. port'), dataIndex: 'dport', renderer: function(value, metaData, record) { return render_errors('dport', value, metaData, record); }, width: 100 }, { header: gettext('Source port'), dataIndex: 'sport', renderer: function(value, metaData, record) { return render_errors('sport', value, metaData, record); }, width: 100 }, { 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: 1, renderer: function(value, metaData, record) { return render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record); } } ); 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 } var moveto = dropRec.get('pos'); if (dropPosition === 'after') { moveto++; } var 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.FirewallAliasEdit', { extend: 'Proxmox.window.Edit', base_url: undefined, alias_name: undefined, initComponent : function() { var 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'; } var 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') } ]; var ipanel = Ext.create('Proxmox.panel.InputPanel', { isCreate: me.isCreate, items: items }); Ext.apply(me, { subject: gettext('Alias'), isAdd: true, items: [ ipanel ] }); me.callParent(); if (!me.isCreate) { me.load({ success: function(response, options) { var 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() { var me = this; if (!me.base_url) { throw "missing base_url configuration"; } var store = new Ext.data.Store({ model: 'pve-fw-aliases', proxy: { type: 'proxmox', url: "/api2/json" + me.base_url }, sorters: { property: 'name', order: 'DESC' } }); var sm = Ext.create('Ext.selection.RowModel', {}); var reload = function() { var oldrec = sm.getSelection()[0]; store.load(function(records, operation, success) { if (oldrec) { var rec = store.findRecord('name', oldrec.data.name); if (rec) { sm.select(rec); } } }); }; var run_editor = function() { var sm = me.getSelectionModel(); var rec = sm.getSelection()[0]; if (!rec) { return; } var 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, handler: run_editor }); me.addBtn = Ext.create('Ext.Button', { text: gettext('Add'), 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', { selModel: sm, 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', width: 100 }, { header: gettext('IP/CIDR'), dataIndex: 'cidr', width: 100 }, { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } ], 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' or 'vm' base_url: undefined, initComponent : function() { /*jslint confusion: true */ var me = this; if (!me.base_url) { throw "missing base_url configuration"; } if (me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm') { if (me.fwtype === 'node') { me.cwidth1 = 250; } } else { throw "unknown firewall option type"; } 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('tcp_flags_log_level', 120); add_log_row('smurf_log_level'); } 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: 'enable=0', editor: { xtype: 'pveFirewallLograteEdit' } }; } 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') } } }; } 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]; edit_btn.setDisabled(!rowdef.editor); }; Ext.apply(me, { url: "/api2/json" + me.base_url, tbar: [ edit_btn ], editorConfig: { url: '/api2/extjs/' + me.base_url }, 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); } }); 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']] }); /* * Left Treepanel, containing all the ressources we manage in this datacenter: server nodes, server storages, VMs and Containers */ Ext.define('PVE.tree.ResourceTree', { extend: 'Ext.tree.TreePanel', alias: ['widget.pveResourceTree'], 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') }, qemu: { iconCls: 'fa fa-desktop', text: gettext('Virtual Machine') }, lxc: { //iconCls: 'x-tree-node-lxc', iconCls: 'fa fa-cube', text: gettext('LXC Container') }, template: { iconCls: 'fa fa-file-o' } } }, useArrows: true, // private nodeSortFn: function(node1, node2) { var n1 = node1.data; var n2 = node2.data; if ((n1.groupbyid && n2.groupbyid) || !(n1.groupbyid || n2.groupbyid)) { var tcmp; var v1 = n1.type; var v2 = n2.type; if ((tcmp = v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)) != 0) { return tcmp; } // numeric compare for VM IDs // sort templates after regular VMs if (v1 === 'qemu' || v1 === 'lxc') { if (n1.template && !n2.template) { return 1; } else if (n2.template && !n1.template) { return -1; } v1 = n1.vmid; v2 = n2.vmid; if ((tcmp = v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)) != 0) { return tcmp; } } return n1.id > n2.id ? 1 : (n1.id < n2.id ? -1 : 0); } else if (n1.groupbyid) { return -1; } else if (n2.groupbyid) { return 1; } }, // private: fast binary search findInsertIndex: function(node, child, start, end) { var me = this; var diff = end - start; var mid = start + (diff>>1); if (diff <= 0) { return start; } var 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) { var me = this; var cls = PVE.Utils.get_object_icon_class(info.type, info); if (cls !== '') { info.iconCls = cls; } }, // add additional elements to text // at the moment only the usage indicator for storages setText: function(info) { var me = this; var status = ''; if (info.type === 'storage') { var maxdisk = info.maxdisk; var disk = info.disk; var usage = disk/maxdisk; var cls = ''; if (usage <= 1.0 && usage >= 0.0) { var height = (usage*100).toFixed(0); var neg_height = (100-usage*100).toFixed(0); status = '' + Ext.htmlEncode(msg) + '
'; metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html.replace(/\"/g,'"') + '"'; } } return value; }; Ext.apply(me, { tbar: [ 'IP/CIDR:', me.addBtn, me.removeBtn, me.editBtn ], store: store, selModel: sm, listeners: { itemdblclick: run_editor }, columns: [ { xtype: 'rownumberer' }, { header: gettext('IP/CIDR'), dataIndex: 'cidr', width: 150, 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: 1, 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(); } }); /* * 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 ressource grid with a search button as first tab viewFilter: undefined, // a filter to pass to that ressource 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: { backgroundColor: '#f5f5f5', padding: 0, margin: 0 }, items: { xtype: 'treelist', itemId: 'menu', ui: 'nav', expanderOnly: true, expanderFirst: false, animation: false, singleExpand: false, listeners: { selectionchange: function(treeList, selection) { var me = this.up('panel'); me.suspendLayout = true; me.activateCard(selection.data.id); me.suspendLayout = false; me.updateLayout(); }, itemclick: function(treelist, info) { var olditem = treelist.getSelection(); var newitem = info.node; // when clicking on the expand arrow, // we dont select items, but still want // the original behaviour if (info.select === false) { return; } // if you click on a different item which is open, // 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(); var menu = me.down('#menu'); menu.setSelection(selection); return cardid; } }, activateCard: function(cardid) { var me = this; if (me.savedItems[cardid]) { var curcard = me.getLayout().getActiveItem(); var 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 var 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) { var state = me.sp.get(stateid); if (state && state.value) { // if this tab does not exists, 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) { switch(me.pveSelNode.data.id) { case 'type/storage':me.onlineHelp = 'chapter-pvesm.html'; break; case 'type/qemu':me.onlineHelp = 'chapter-qm.html'; break; case 'type/lxc':me.onlineHelp = 'chapter-pct.html'; break; case 'type/pool':me.onlineHelp = 'chapter-pveum.html#_pools'; break; case 'type/node':me.onlineHelp = 'chapter-sysadmin.html'; break; } } 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({ itemId: 'search', title: gettext('Search'), iconCls: 'fa fa-search', xtype: 'pveResourceGrid', pveSelNode: me.pveSelNode }); } me.savedItems = {}; /*jslint confusion:true*/ if (me.items[0]) { me.firstItem = me.items[0].itemId; } /*jslint confusion:false*/ me.store = Ext.create('Ext.data.TreeStore', { root: { expanded: true } }); var root = me.store.getRoot(); me.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(); var 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"; } }); 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 var acard = me.getLayout().getActiveItem().itemId; // get the itemid of the new value var 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); } } }); 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 === 'openvz') { vmtypeFilter = function(item) { return item.data.volid.match(':backup/vzdump-openvz-'); }; } else if (vmtype === 'lxc') { vmtypeFilter = function(item) { return item.data.volid.match(':backup/vzdump-lxc-'); }; } else if (vmtype === 'qemu') { vmtypeFilter = function(item) { return item.data.volid.match(':backup/vzdump-qemu-'); }; } else { throw "unsupported VM type '" + vmtype + "'"; } var searchFilter = { property: 'volid', // on initial store display only our vmid backups // surround with minus sign to prevent the 2016 VMID bug value: vmtype + '-' + vmid + '-', anyMatch: true, caseSensitive: false }; me.store = Ext.create('Ext.data.Store', { model: 'pve-storage-content', sorters: { property: 'volid', order: 'DESC' }, filters: [ vmtypeFilter, searchFilter ] }); var reload = Ext.Function.createBuffered(function() { if (me.store) { me.store.load(); } }, 100); var setStorage = function(storage) { var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content'; url += '?content=backup'; me.store.setProxy({ type: 'proxmox', url: url }); reload(); }; var storagesel = Ext.create('PVE.form.StorageSelector', { nodename: nodename, fieldLabel: gettext('Storage'), labelAlign: 'right', storageContent: 'backup', allowBlank: false, listeners: { change: function(f, value) { setStorage(value); } } }); 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(); me.store.filter([ vmtypeFilter, searchFilter ]); } } }); 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, 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) { var volid = rec.data.volid; var win = Ext.create('PVE.window.Restore', { nodename: nodename, vmid: vmid, volid: rec.data.volid, volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec), vmtype: vmtype }); win.show(); win.on('destroy', reload); } }); var delete_btn = Ext.create('Proxmox.button.StdRemoveButton', { selModel: sm, dangerous: true, confirmMsg: function(rec) { var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), "'" + rec.data.volid + "'"); msg += " " + gettext('This will permanently erase all data.'); return msg; }, getUrl: function(rec) { var storage = storagesel.getValue(); return '/nodes/' + nodename + '/storage/' + storage + '/content/' + rec.data.volid; }, callback: function() { reload(); } }); var config_btn = Ext.create('Proxmox.button.Button', { text: gettext('Show Configuration'), disabled: true, selModel: sm, enableFn: function(rec) { return !!rec; }, handler: function(b, e, rec) { var storage = storagesel.getValue(); if (!storage) { return; } var win = Ext.create('PVE.window.BackupConfig', { volume: rec.data.volid, pveSelNode: me.pveSelNode }); win.show(); } }); Ext.apply(me, { selModel: sm, tbar: [ backup_btn, restore_btn, delete_btn,config_btn, '->', storagesel, storagefilter ], columns: [ { header: gettext('Name'), flex: 1, sortable: true, renderer: PVE.Utils.render_storage_content, dataIndex: 'volid' }, { header: gettext('Format'), width: 100, dataIndex: 'format' }, { header: gettext('Size'), width: 100, renderer: Proxmox.Utils.format_size, dataIndex: 'size' } ] }); me.callParent(); } }); /*jslint confusion: true */ 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 Storage'), value: true, name: 'add-storage' } ], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.setFSName(); me.callParent(); } }); Ext.define('PVE.CephCreateMDS', { extend: 'Proxmox.window.Edit', alias: 'widget.pveCephCreateMDS', showProgress: true, onlineHelp: 'pveceph_fs_mds', subject: 'Ceph MDS', isCreate: true, method: 'POST', setNode: function(nodename) { var me = this; me.nodename = nodename; me.url = "/nodes/" + nodename + "/ceph/mds/" + nodename; }, items: [ { xtype: 'pveNodeSelector', fieldLabel: gettext('Node'), selectCurNode: true, submitValue: false, allowBlank: false, listeners: { change: function(f, value) { this.up('pveCephCreateMDS').setNode(value); } } } ], initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.setNode(me.nodename); 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: { cephfsConfigured: false, mdsCount: 0 }, formulas: { canCreateFS: function(get) { return (!get('cephfsConfigured') && 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', order: 'DESC' } })); var regex = new RegExp("not (installed|initialized)", "i"); PVE.Utils.handleStoreErrorOrMask(view, view.rstore, regex, function(me, error){ me.rstore.stopUpdate(); PVE.Utils.showCephInstallOrMask(me.ownerCt, error.statusText, view.nodename, function(win){ me.mon(win, 'cephInstallWindowClosed', function(){ me.rstore.startUpdate(); }); } ); }); view.rstore.on('load', this.onLoad, this); view.on('destroy', view.rstore.stopUpdate); }, onCreate: function() { var view = this.getView(); view.rstore.stopUpdate(); var win = Ext.create('PVE.CephCreateFS', { autoShow: true, nodename: view.nodename, listeners: { destroy: function() { view.rstore.startUpdate(); } } }); }, onLoad: function(store, records, success) { var vm = this.getViewModel(); if (!(success && records && records.length > 0)) { vm.set('cephfsConfigured', false); return; } vm.set('cephfsConfigured', true); } }, tbar: [ { text: gettext('Create CephFS'), reference: 'createButton', handler: 'onCreate', bind: { // only one CephFS per Ceph cluster makes sense for now disabled: '{!canCreateFS}' } } ], columns: [ { header: gettext('Name'), flex: 1, dataIndex: 'name' }, { header: 'Data Pool', flex: 1, dataIndex: 'data_pool' }, { header: 'Metadata Pool', flex: 1, dataIndex: 'metadata_pool' } ], cbind: { nodename: '{nodename}' } }, { xtype: 'grid', title: gettext('Metadata Servers'), emptyText: Ext.String.format(gettext('No {0} configured.'), 'MDS'), controller: { xclass: 'Ext.app.ViewController', init: function(view) { view.rstore = Ext.create('Proxmox.data.UpdateStore', { autoLoad: true, xtype: 'update', interval: 3 * 1000, autoStart: true, storeid: 'pve-ceph-mds', proxy: { type: 'proxmox', url: '/api2/json/nodes/'+ view.nodename +'/ceph/mds' }, model: 'pve-ceph-mds' }); view.setStore(Ext.create('Proxmox.data.DiffStore', { rstore: view.rstore, sorters: { property: 'id', order: 'DESC' } })); var regex = new RegExp("not (installed|initialized)", "i"); PVE.Utils.handleStoreErrorOrMask(view, view.rstore, regex, function(me, error){ me.rstore.stopUpdate(); PVE.Utils.showCephInstallOrMask(me.ownerCt, error.statusText, view.nodename, function(win){ me.mon(win, 'cephInstallWindowClosed', function(){ me.rstore.startUpdate(); }); } ); }); view.rstore.on('load', this.onLoad, this); view.on('destroy', view.rstore.stopUpdate); }, onLoad: function(store, records, success) { var vm = this.getViewModel(); if (!success || !records) { vm.set('mdsCount', 0); return; } vm.set('mdsCount', records.length); }, onCreateMDS: function() { var view = this.getView(); view.rstore.stopUpdate(); var win = Ext.create('PVE.CephCreateMDS', { autoShow: true, nodename: view.nodename, listeners: { destroy: function() { view.rstore.startUpdate(); } } }); } }, tbar: [ { text: gettext('Create MDS'), reference: 'createButton', handler: 'onCreateMDS' }, { text: gettext('Destroy MDS'), xtype: 'proxmoxStdRemoveButton', getUrl: function(rec) { if (!rec.data.host) { Ext.Msg.alert(gettext('Error'), "entry has no host"); return; } return "/nodes/" + rec.data.host + "/ceph/mds/" + rec.data.name; }, callback: function(options, success, response) { if (!success) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); return; } var upid = response.result.data; var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); win.show(); } } ], columns: [ { header: gettext('Name'), flex: 1, dataIndex: 'name' }, { header: gettext('Host'), flex: 1, dataIndex: 'host' }, { header: gettext('Address'), flex: 1, dataIndex: 'addr' }, { header: gettext('State'), flex: 1, dataIndex: 'state' } ], cbind: { nodename: '{nodename}' } } ] }, function() { Ext.define('pve-ceph-mds', { extend: 'Ext.data.Model', fields: [ 'name', 'host', 'addr', 'state' ], proxy: { type: 'proxmox', url: "/api2/json/nodes/localhost/ceph/mds" }, idProperty: 'name' }); 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.CephCreatePool', { extend: 'Proxmox.window.Edit', alias: 'widget.pveCephCreatePool', showProgress: true, onlineHelp: 'pve_ceph_pools', subject: 'Ceph Pool', isCreate: true, method: 'POST', items: [ { xtype: 'textfield', fieldLabel: gettext('Name'), name: 'name', allowBlank: false }, { xtype: 'proxmoxintegerfield', fieldLabel: gettext('Size'), name: 'size', value: 3, minValue: 1, maxValue: 7, allowBlank: false }, { xtype: 'proxmoxintegerfield', fieldLabel: gettext('Min. Size'), name: 'min_size', value: 2, minValue: 1, maxValue: 7, allowBlank: false }, { xtype: 'pveCephRuleSelector', fieldLabel: 'Crush Rule', // do not localize name: 'crush_rule', allowBlank: false }, { xtype: 'proxmoxintegerfield', fieldLabel: 'pg_num', name: 'pg_num', value: 128, minValue: 8, maxValue: 32768, allowBlank: false }, { xtype: 'proxmoxcheckbox', fieldLabel: gettext('Add Storage'), name: 'add_storages' } ], initComponent : function() { /*jslint confusion: true */ var me = this; if (!me.nodename) { throw "no node name specified"; } Ext.apply(me, { url: "/nodes/" + me.nodename + "/ceph/pools", defaults: { nodename: me.nodename } }); me.callParent(); } }); Ext.define('PVE.node.CephPoolList', { extend: 'Ext.grid.GridPanel', alias: 'widget.pveNodeCephPoolList', onlineHelp: 'chapter_pveceph', stateful: true, stateId: 'grid-ceph-pools', bufferedRenderer: false, features: [ { ftype: 'summary'} ], columns: [ { header: gettext('Name'), width: 100, sortable: true, dataIndex: 'pool_name' }, { header: gettext('Size') + '/min', width: 80, sortable: false, renderer: function(v, meta, rec) { return v + '/' + rec.data.min_size; }, dataIndex: 'size' }, { header: 'pg_num', width: 100, sortable: false, dataIndex: 'pg_num' }, { header: 'rule', width: 50, sortable: false, dataIndex: 'crush_rule' }, { header: 'rule_name', width: 50, sortable: false, dataIndex: 'crush_rule_name' }, { header: gettext('Used'), columns: [ { header: '%', width: 80, sortable: true, align: 'right', renderer: Ext.util.Format.numberRenderer('0.00'), dataIndex: 'percent_used', summaryType: 'sum', summaryRenderer: Ext.util.Format.numberRenderer('0.00') }, { header: gettext('Total'), width: 100, sortable: true, renderer: PVE.Utils.render_size, align: 'right', dataIndex: 'bytes_used', summaryType: 'sum', summaryRenderer: PVE.Utils.render_size } ] } ], 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/pools" } }); var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore }); var regex = new RegExp("not (installed|initialized)", "i"); PVE.Utils.handleStoreErrorOrMask(me, rstore, regex, function(me, error){ me.store.rstore.stopUpdate(); PVE.Utils.showCephInstallOrMask(me, error.statusText, nodename, function(win){ me.mon(win, 'cephInstallWindowClosed', function(){ me.store.rstore.startUpdate(); }); } ); }); var create_btn = new Ext.Button({ text: gettext('Create'), handler: function() { var win = Ext.create('PVE.CephCreatePool', { nodename: nodename }); win.show(); win.on('destroy', function() { rstore.load(); }); } }); var destroy_btn = Ext.create('Proxmox.button.Button', { text: gettext('Destroy'), selModel: sm, disabled: true, handler: function() { var rec = sm.getSelection()[0]; if (!rec.data.pool_name) { return; } var base_url = '/nodes/' + nodename + '/ceph/pools/' + rec.data.pool_name; var win = Ext.create('PVE.window.SafeDestroy', { showProgress: true, url: base_url, params: { remove_storages: 1 }, item: { type: 'CephPool', id: rec.data.pool_name } }).show(); win.on('destroy', function() { rstore.load(); }); } }); Ext.apply(me, { store: store, selModel: sm, tbar: [ create_btn, destroy_btn ], listeners: { activate: rstore.startUpdate, destroy: rstore.stopUpdate } }); 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: 'bytes_used', type: 'integer'}, { name: 'percent_used', type: 'number'}, { name: 'crush_rule', type: 'integer'}, { name: 'crush_rule_name', type: 'string'} ], 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() { 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/rules' } }); 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.CephCreateOsd', { extend: 'Proxmox.window.Edit', alias: ['widget.pveCephCreateOsd'], subject: 'Ceph OSD', showProgress: true, onlineHelp: 'pve_ceph_osds', initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.isCreate = true; Ext.applyIf(me, { url: "/nodes/" + me.nodename + "/ceph/osd", method: 'POST', items: [ { xtype: 'pveDiskSelector', name: 'dev', nodename: me.nodename, diskType: 'unused', fieldLabel: gettext('Disk'), allowBlank: false }, { xtype: 'pveDiskSelector', name: 'journal_dev', nodename: me.nodename, diskType: 'journal_disks', fieldLabel: gettext('Journal/DB Disk'), value: '', autoSelect: false, allowBlank: true, emptyText: 'use OSD disk' }, { xtype: 'proxmoxcheckbox', name: 'bluestore', fieldLabel: 'Bluestore', uncheckedValue: '0', value: '1' } ] }); 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('Remove Partitions') } ], initComponent : function() { var 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(); } }); Ext.define('PVE.node.CephOsdTree', { extend: 'Ext.tree.Panel', alias: ['widget.pveNodeCephOsdTree'], onlineHelp: 'chapter_pveceph', stateful: true, stateId: 'grid-ceph-osd', columns: [ { xtype: 'treecolumn', text: 'Name', dataIndex: 'name', width: 150 }, { text: 'Type', dataIndex: 'type', align: 'right', width: 60 }, { text: gettext("Class"), dataIndex: 'device_class', align: 'right', width: 40 }, { text: "OSD Type", dataIndex: 'osdtype', align: 'right', width: 40 }, { text: "Bluestore Device", dataIndex: 'blfsdev', align: 'right', width: 40, hidden: true }, { text: "DB Device", dataIndex: 'dbdev', align: 'right', width: 40, hidden: true }, { text: "WAL Device", dataIndex: 'waldev', align: 'right', renderer: function(value, metaData, rec) { if (!value && rec.data.osdtype === 'bluestore' && rec.data.type === 'osd') { return 'N/A'; } return value; }, width: 40, hidden: true }, { text: 'Status', dataIndex: 'status', align: 'right', renderer: function(value, metaData, rec) { if (!value) { return value; } var inout = rec.data['in'] ? 'in' : 'out'; var updownicon = value === 'up' ? 'good fa-arrow-circle-up' : 'critical fa-arrow-circle-down'; var inouticon = rec.data['in'] ? 'good fa-circle' : 'warning fa-circle-o'; var text = value + ' / ' + inout + ' '; return text; }, width: 80 }, { text: 'weight', dataIndex: 'crush_weight', align: 'right', renderer: function(value, metaData, rec) { if (rec.data.type !== 'osd') { return ''; } return value; }, width: 80 }, { text: 'reweight', dataIndex: 'reweight', align: 'right', renderer: function(value, metaData, rec) { if (rec.data.type !== 'osd') { return ''; } return value; }, width: 90 }, { header: gettext('Used'), columns: [ { text: '%', 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: 80 }, { text: gettext('Total'), dataIndex: 'total_space', align: 'right', renderer: function(value, metaData, rec) { if (rec.data.type !== 'osd') { return ''; } return PVE.Utils.render_size(value); }, width: 100 } ] }, { header: gettext('Latency (ms)'), columns: [ { text: 'Apply', dataIndex: 'apply_latency_ms', align: 'right', renderer: function(value, metaData, rec) { if (rec.data.type !== 'osd') { return ''; } return value; }, width: 60 }, { text: 'Commit', dataIndex: 'commit_latency_ms', align: 'right', renderer: function(value, metaData, rec) { if (rec.data.type !== 'osd') { return ''; } return value; }, width: 60 } ] } ], initComponent: function() { /*jslint confusion: true */ var me = this; // we expect noout to be not set by default var noout = false; var nodename = me.pveSelNode.data.node; if (!nodename) { throw "no node name specified"; } var sm = Ext.create('Ext.selection.TreeModel', {}); var set_button_status; // defined later var reload = function() { Proxmox.Utils.API2Request({ url: "/nodes/" + nodename + "/ceph/osd", waitMsgTarget: me, method: 'GET', failure: function(response, opts) { var msg = response.htmlStatus; PVE.Utils.showCephInstallOrMask(me, msg, me.pveSelNode.data.node, function(win){ me.mon(win, 'cephInstallWindowClosed', function(){ reload(); }); } ); }, success: function(response, opts) { sm.deselectAll(); me.setRootNode(response.result.data.root); me.expandAll(); // extract noout flag if (response.result.data.flags && response.result.data.flags.search(/noout/) !== -1) { noout = true; } else { noout = false; } set_button_status(); } }); }; var osd_cmd = function(cmd) { var rec = sm.getSelection()[0]; if (!(rec && (rec.data.id >= 0) && rec.data.host)) { return; } Proxmox.Utils.API2Request({ url: "/nodes/" + rec.data.host + "/ceph/osd/" + rec.data.id + '/' + cmd, waitMsgTarget: me, method: 'POST', success: reload, failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); }; var service_cmd = function(cmd) { var rec = sm.getSelection()[0]; if (!(rec && rec.data.name && rec.data.host)) { return; } Proxmox.Utils.API2Request({ url: "/nodes/" + rec.data.host + "/ceph/" + cmd, params: { service: rec.data.name }, waitMsgTarget: me, method: 'POST', success: function(response, options) { var upid = response.result.data; var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); win.show(); me.mon(win, 'close', reload, me); }, failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); }; var create_btn = new Proxmox.button.Button({ text: gettext('Create') + ': OSD', handler: function() { var rec = sm.getSelection()[0]; var win = Ext.create('PVE.CephCreateOsd', { nodename: nodename }); win.show(); me.mon(win, 'close', reload, me); } }); var start_btn = new Ext.Button({ text: gettext('Start'), disabled: true, handler: function(){ service_cmd('start'); } }); var stop_btn = new Ext.Button({ text: gettext('Stop'), disabled: true, handler: function(){ service_cmd('stop'); } }); var restart_btn = new Ext.Button({ text: gettext('Restart'), disabled: true, handler: function(){ service_cmd('restart'); } }); var osd_out_btn = new Ext.Button({ text: 'Out', disabled: true, handler: function(){ osd_cmd('out'); } }); var osd_in_btn = new Ext.Button({ text: 'In', disabled: true, handler: function(){ osd_cmd('in'); } }); var remove_btn = new Ext.Button({ text: gettext('Destroy'), disabled: true, handler: function(){ var rec = sm.getSelection()[0]; if (!(rec && (rec.data.id >= 0) && rec.data.host)) { return; } var win = Ext.create('PVE.CephRemoveOsd', { nodename: rec.data.host, osdid: rec.data.id }); win.show(); me.mon(win, 'close', reload, me); } }); var noout_btn = new Ext.Button({ text: gettext('Set noout'), handler: function() { Proxmox.Utils.API2Request({ url: "/nodes/" + nodename + "/ceph/flags/noout", waitMsgTarget: me, method: noout ? 'DELETE' : 'POST', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: reload }); } }); var osd_label = new Ext.toolbar.TextItem({ data: { osd: undefined }, tpl: [ '' + Ext.htmlEncode(record.data.detail) + '' ] }] }); win.show(); } } ] } ] } ] }, { xtype: 'pveCephStatusDetail', itemId: 'statusdetail', plugins: 'responsive', responsiveConfig: { 'width < 1900': { columnWidth: 1 }, 'width >= 1900': { columnWidth: 0.5 } }, title: gettext('Status') }, { xtype: 'panel', title: gettext('Performance'), columnWidth: 1, bodyPadding: 5, layout: { type: 'hbox', align: 'center' }, items: [ { flex: 1, xtype: 'proxmoxGauge', itemId: 'space', title: gettext('Usage') }, { flex: 2, xtype: 'container', defaults: { padding: 0, height: 100 }, items: [ { itemId: 'reads', xtype: 'pveRunningChart', title: gettext('Reads'), renderer: PVE.Utils.render_bandwidth }, { itemId: 'writes', xtype: 'pveRunningChart', title: gettext('Writes'), renderer: PVE.Utils.render_bandwidth }, { itemId: 'iops', xtype: 'pveRunningChart', hidden: true, title: 'IOPS', // do not localize renderer: Ext.util.Format.numberRenderer('0,000') }, { itemId: 'readiops', xtype: 'pveRunningChart', hidden: true, title: 'IOPS: ' + gettext('Reads'), renderer: Ext.util.Format.numberRenderer('0,000') }, { itemId: 'writeiops', xtype: 'pveRunningChart', hidden: true, title: 'IOPS: ' + gettext('Writes'), renderer: Ext.util.Format.numberRenderer('0,000') } ] } ] } ], generateCheckData: function(health) { var result = []; var checks = health.checks || {}; var keys = Ext.Object.getKeys(checks).sort(); Ext.Array.forEach(keys, function(key) { var details = checks[key].detail || []; result.push({ id: key, summary: checks[key].summary.message, detail: Ext.Array.reduce( checks[key].detail, function(first, second) { return first + '\n' + second.message; }, '' ), severity: checks[key].severity }); }); return result; }, updateAll: function(store, records, success) { if (!success || records.length === 0) { return; } var me = this; var rec = records[0]; // add health panel me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {})); // add errors to gridstore me.down('#warnings').getStore().loadRawData(me.generateCheckData(rec.data.health || {}), false); // update detailstatus panel me.getComponent('statusdetail').updateAll( rec.data.health || {}, rec.data.monmap || {}, rec.data.pgmap || {}, rec.data.osdmap || {}, rec.data.quorum_names || []); // add performance data var used = rec.data.pgmap.bytes_used; var total = rec.data.pgmap.bytes_total; var text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(used), PVE.Utils.render_size(total) ); // update the usage widget me.down('#space').updateValue(used/total, text); // TODO: logic for jewel (iops splitted in read/write) var iops = rec.data.pgmap.op_per_sec; var readiops = rec.data.pgmap.read_op_per_sec; var writeiops = rec.data.pgmap.write_op_per_sec; var reads = rec.data.pgmap.read_bytes_sec || 0; var writes = rec.data.pgmap.write_bytes_sec || 0; if (iops !== undefined && me.version !== 'hammer') { me.change_version('hammer'); } else if((readiops !== undefined || writeiops !== undefined) && me.version !== 'jewel') { me.change_version('jewel'); } // update the graphs me.reads.addDataPoint(reads); me.writes.addDataPoint(writes); me.iops.addDataPoint(iops); me.readiops.addDataPoint(readiops); me.writeiops.addDataPoint(writeiops); }, change_version: function(version) { var me = this; me.version = version; me.sp.set('ceph-version', version); me.iops.setVisible(version === 'hammer'); me.readiops.setVisible(version === 'jewel'); me.writeiops.setVisible(version === 'jewel'); }, initComponent: function() { var me = this; var nodename = me.pveSelNode.data.node; if (!nodename) { throw "no node name specified"; } me.callParent(); me.store = Ext.create('Proxmox.data.UpdateStore', { storeid: 'ceph-status-' + nodename, interval: 5000, proxy: { type: 'proxmox', url: '/api2/json/nodes/' + nodename + '/ceph/status' } }); // 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'); // get ceph version me.sp = Ext.state.Manager.getProvider(); me.version = me.sp.get('ceph-version'); me.change_version(me.version); var regex = new RegExp("not (installed|initialized)", "i"); PVE.Utils.handleStoreErrorOrMask(me, me.store, regex, function(me, error){ me.store.stopUpdate(); PVE.Utils.showCephInstallOrMask(me, error.statusText, nodename, function(win){ me.mon(win, 'cephInstallWindowClosed', function(){ me.store.startUpdate(); }); } ); }); me.mon(me.store, 'load', me.updateAll, me); me.on('destroy', me.store.stopUpdate); me.store.startUpdate(); } }); Ext.define('PVE.ceph.StatusDetail', { extend: 'Ext.panel.Panel', alias: 'widget.pveCephStatusDetail', layout: { type: 'hbox', align: 'stretch' }, bodyPadding: '0 5 20', defaults: { xtype: 'box', style: { 'text-align':'center' } }, items: [{ flex: 1, itemId: 'monitors', xtype: 'container', items: [ { xtype: 'box', width: '100%', html: '
', ' | ', gettext('In'), ' | ', '', gettext('Out'), ' | ', '
', gettext('Up'), ' | ', '{upin} | ', '{upout} | ', '
', gettext('Down'), ' | ', '{downin} | ', '{downout} | ', '
'+ '"Ceph is a unified, distributed storage system designed for excellent performance, reliability and scalability."
Ceph is currently not installed on this node, click on the next button below to start the installation.'+ ' This wizard will guide you through the necessary steps, after the initial installation you will be offered to create a initial configuration.'+ ' The configuration step is only needed once per cluster and will be skipped if a config is already present.
'+ 'Please take a look at our documentation, by clicking the help button below, before starting the installation, '+ 'if you want to gain deeper knowledge about Ceph visit ceph.com.
', listeners: { activate: function() { // notify owning container that it should display a help button if (this.onlineHelp) { Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); } this.up('pveCephInstallWizard').down('#back').hide(true); this.up('pveCephInstallWizard').down('#next').setText(gettext('Start installation')); }, 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() { var me = this; var 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")) { Proxmox.Utils.setErrorMask(me, operation.error.statusText); } } } }); me.updateStore.startUpdate(); }, destroy: function() { var me = this; if (me.updateStore) { me.updateStore.stopUpdate(); } } }, items: [ { itemId: 'jsconsole', consoleType: 'cmd', xtermjs: true, xtype: 'pveNoVncConsole', cbind:{ nodename: '{nodename}' }, cmd: 'ceph_install' }, { xtype: 'textfield', name: 'installSuccess', value: '', allowBlank: false, submitValue: false, hidden: true } ] }, { xtype: 'inputpanel', title: gettext('Configuration'), onlineHelp: 'chapter_pveceph', cbind: { nodename: '{nodename}' }, viewModel: { data: { replicas: undefined, minreplicas: undefined } }, listeners: { activate: function() { this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next')); }, beforeshow: function() { if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) { this.mask("Coniguration 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: 'textfield', name: 'network', vtype: 'IPCIDRAddress', value: '', fieldLabel: 'Public Network IP/CIDR', bind: { allowBlank: '{configuration}' }, setAllowBlank: function(allowBlank) { this.allowBlank = allowBlank; this.validate(); } }, { xtype: 'textfield', name: 'cluster-network', vtype: 'IPCIDRAddress', fieldLabel: 'Cluster Network IP/CIDR', allowBlank: true, emptyText: gettext('Same as Public Network') } // FIXME: add hint about cluster network and/or reference user to docs?? ], column2: [ { xtype: 'displayfield', value: gettext('First Ceph monitor') + ':' }, { xtype: 'pveNodeSelector', fieldLabel: gettext('Monitor node'), name: 'mon-node', selectCurNode: true, allowBlank: false }, { xtype: 'displayfield', value: gettext('Additional monitors are recommended. They can be created at any time in the Monitor tab.'), userCls: 'pve-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')) { var wizard = me.up('window'); var kv = wizard.getValues(); delete kv['delete']; var monNode = kv['mon-node']; delete kv['mon-node']; var 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/' + monNode + '/ceph/mon', 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: 'The basic installation and configuration is completed, depending on your setup some of the following steps are required to start using Ceph:
'+ '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--) { var 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.DiskList', { extend: 'Ext.grid.GridPanel', alias: 'widget.pveNodeDiskList', emptyText: gettext('No Disks found'), stateful: true, stateId: 'grid-node-disks', columns: [ { header: gettext('Device'), width: 100, sortable: true, dataIndex: 'devpath' }, { header: gettext('Type'), width: 80, sortable: true, dataIndex: 'type', renderer: function(v) { if (v === 'ssd') { return 'SSD'; } else if (v === 'hdd') { return 'Hard Disk'; } else if (v === 'usb'){ return 'USB'; } else { return gettext('Unknown'); } } }, { header: gettext('Usage'), width: 80, sortable: false, renderer: function(v, metaData, rec) { if (rec) { if (rec.data.osdid >= 0) { var bluestore = ''; if (rec.data.bluestore === 1) { bluestore = ' (Bluestore)'; } return "Ceph osd." + rec.data.osdid.toString() + bluestore; } var types = []; if (rec.data.journals > 0) { types.push('Journal'); } if (rec.data.db > 0) { types.push('DB'); } if (rec.data.wal > 0) { types.push('WAL'); } if (types.length > 0) { return 'Ceph (' + types.join(', ') + ')'; } } return v || Proxmox.Utils.noText; }, dataIndex: 'used' }, { header: gettext('Size'), width: 100, align: 'right', sortable: true, renderer: Proxmox.Utils.format_size, dataIndex: 'size' }, { header: 'GPT', width: 60, align: 'right', renderer: function(value) { if (value) { return Proxmox.Utils.yesText; } else { return Proxmox.Utils.noText; } }, dataIndex: 'gpt' }, { header: gettext('Vendor'), width: 100, sortable: true, renderer: Ext.String.htmlEncode, dataIndex: 'vendor' }, { header: gettext('Model'), width: 200, sortable: true, renderer: Ext.String.htmlEncode, dataIndex: 'model' }, { header: gettext('Serial'), width: 200, sortable: true, renderer: Ext.String.htmlEncode, dataIndex: 'serial' }, { header: 'S.M.A.R.T.', width: 100, sortable: true, renderer: Ext.String.htmlEncode, dataIndex: 'health' }, { header: 'Wearout', width: 100, sortable: true, dataIndex: 'wearout', renderer: function(value) { if (Ext.isNumeric(value)) { return (100 - value).toString() + '%'; } return 'N/A'; } } ], initComponent: function() { /*jslint confusion: true */ var me = this; var nodename = me.pveSelNode.data.node; if (!nodename) { throw "no node name specified"; } var sm = Ext.create('Ext.selection.RowModel', {}); var store = Ext.create('Ext.data.Store', { storeid: 'node-disk-list' + nodename, model: 'node-disk-list', proxy: { type: 'proxmox', url: "/api2/json/nodes/" + nodename + "/disks/list" }, sorters: [ { property : 'dev', direction: 'ASC' } ] }); var reloadButton = Ext.create('Proxmox.button.Button', { text: gettext('Reload'), handler: function() { me.store.load(); } }); var smartButton = Ext.create('Proxmox.button.Button', { text: gettext('Show S.M.A.R.T. values'), selModel: sm, enableFn: function() { return !!sm.getSelection().length; }, disabled: true, handler: function() { var rec = sm.getSelection()[0]; var win = Ext.create('PVE.DiskSmartWindow', { nodename: nodename, dev: rec.data.devpath }); win.show(); } }); var initButton = Ext.create('Proxmox.button.Button', { text: gettext('Initialize Disk with GPT'), selModel: sm, enableFn: function() { var selection = sm.getSelection(); if (!selection.length || selection[0].data.used) { return false; } else { return true; } }, disabled: true, handler: function() { var rec = sm.getSelection()[0]; Proxmox.Utils.API2Request({ url: '/api2/extjs/nodes/' + nodename + '/disks/initgpt', waitMsgTarget: me, method: 'POST', params: { disk: rec.data.devpath}, failure: function(response, options) { 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(); } }); } }); me.loadCount = 1; // avoid duplicate loadmask Proxmox.Utils.monStoreErrors(me, store); Ext.apply(me, { store: store, selModel: sm, tbar: [ reloadButton, smartButton, initButton ], listeners: { itemdblclick: function() { var rec = sm.getSelection()[0]; var win = Ext.create('PVE.DiskSmartWindow', { nodename: nodename, dev: rec.data.devpath }); win.show(); } } }); me.callParent(); me.store.load(); } }, function() { Ext.define('node-disk-list', { extend: 'Ext.data.Model', fields: [ 'devpath', 'used', { name: 'size', type: 'number'}, {name: 'osdid', type: 'number'}, 'vendor', 'model', 'serial', 'rpm', 'type', 'health', 'wearout' ], idProperty: 'devpath' }); }); Ext.define('PVE.DiskSmartWindow', { extend: 'Ext.window.Window', alias: 'widget.pveSmartWindow', modal: true, items: [ { xtype: 'gridpanel', layout: { type: 'fit' }, emptyText: gettext('No S.M.A.R.T. Values'), scrollable: true, flex: 1, itemId: 'smarts', reserveScrollbar: true, columns: [ { text: 'ID', dataIndex: 'id', width: 50 }, { text: gettext('Attribute'), flex: 1, dataIndex: 'name', renderer: Ext.String.htmlEncode }, { text: gettext('Value'), dataIndex: 'raw', renderer: Ext.String.htmlEncode }, { text: gettext('Normalized'), dataIndex: 'value', width: 60}, { text: gettext('Threshold'), dataIndex: 'threshold', width: 60}, { text: gettext('Worst'), dataIndex: 'worst', width: 60}, { text: gettext('Flags'), dataIndex: 'flags'}, { text: gettext('Failing'), dataIndex: 'fail', renderer: Ext.String.htmlEncode } ] }, { xtype: 'component', itemId: 'text', layout: { type: 'fit' }, hidden: true, style: { 'background-color': 'white', 'white-space': 'pre', 'font-family': 'monospace' } } ], buttons: [ { text: gettext('Reload'), name: 'reload', handler: function() { var me = this; me.up('window').store.reload(); } }, { text: gettext('Close'), name: 'close', handler: function() { var me = this; me.up('window').close(); } } ], layout: { type: 'vbox', align: 'stretch' }, width: 800, height: 500, minWidth: 600, minHeight: 400, bodyPadding: 5, title: gettext('S.M.A.R.T. Values'), initComponent: function() { var me = this; var nodename = me.nodename; if (!nodename) { throw "no node name specified"; } var dev = me.dev; if (!dev) { throw "no device specified"; } me.store = Ext.create('Ext.data.Store', { model: 'disk-smart', proxy: { type: 'proxmox', url: "/api2/json/nodes/" + nodename + "/disks/smart?disk=" + dev } }); me.callParent(); var grid = me.down('#smarts'); var text = me.down('#text'); Proxmox.Utils.monStoreErrors(grid, me.store); me.mon(me.store, 'load', function(s, records, success) { if (success && records.length > 0) { var rec = records[0]; switch (rec.data.type) { case 'text': grid.setVisible(false); text.setVisible(true); text.setHtml(Ext.String.htmlEncode(rec.data.text)); break; default: // includes 'ata' // cannot use empty case because // of jslint grid.setVisible(true); text.setVisible(false); grid.setStore(rec.attributes()); break; } } }); me.store.load(); } }, function() { Ext.define('disk-smart', { extend: 'Ext.data.Model', fields: [ { name:'health'}, { name:'type'}, { name:'text'} ], hasMany: {model: 'smart-attribute', name: 'attributes'} }); Ext.define('smart-attribute', { extend: 'Ext.data.Model', fields: [ { name:'id', type:'number' }, 'name', 'value', 'worst', 'threshold', 'flags', 'fail', 'raw' ] }); }); Ext.define('PVE.node.CreateLVM', { extend: 'Proxmox.window.Edit', xtype: 'pveCreateLVM', subject: 'LVM Volume Group', showProgress: true, onlineHelp: 'chapter_lvm', initComponent : function() { var 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: 'pveDiskSelector', name: 'device', nodename: me.nodename, diskType: 'unused', 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', emptyText: gettext('No Volume Groups found'), stateful: true, stateId: 'grid-node-lvm', columns: [ { xtype: 'treecolumn', text: gettext('Name'), dataIndex: 'name', flex: 1 }, { text: gettext('Number of LVs'), dataIndex: 'lvcount', width: 150, align: 'right' }, { 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: 'size' }, { header: gettext('Free'), width: 100, align: 'right', sortable: true, renderer: Proxmox.Utils.format_size, dataIndex: 'free' } ], rootVisible: false, useArrows: true, tbar: [ { text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { var me = this.up('panel'); me.reload(); } }, { text: gettext('Create') + ': Volume Group', handler: function() { var me = this.up('panel'); var win = Ext.create('PVE.node.CreateLVM', { nodename: me.nodename, taskDone: function() { me.reload(); } }).show(); } } ], reload: function() { var me = this; var sm = me.getSelectionModel(); Proxmox.Utils.API2Request({ url: "/nodes/" + me.nodename + "/disks/lvm", 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(); } }); }, listeners: { activate: function() { var me = this; me.reload(); } }, initComponent: function() { /*jslint confusion: true */ var me = this; me.nodename = me.pveSelNode.data.node; if (!me.nodename) { throw "no node name specified"; } var sm = Ext.create('Ext.selection.TreeModel', {}); Ext.apply(me, { selModel: sm, fields: ['name', 'size', 'free', { type: 'string', name: 'iconCls', calculate: function(data) { var txt = 'fa x-fa-tree fa-'; txt += (data.leaf) ? 'hdd-o' : 'object-group'; return txt; } }, { type: 'number', name: 'usage', calculate: function(data) { return ((data.size-data.free)/data.size); } } ], sorters: 'name' }); me.callParent(); me.reload(); } }); Ext.define('PVE.node.CreateLVMThin', { extend: 'Proxmox.window.Edit', xtype: 'pveCreateLVMThin', subject: 'LVM Thinpool', showProgress: true, onlineHelp: 'chapter_lvm', initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.isCreate = true; Ext.applyIf(me, { url: "/nodes/" + me.nodename + "/disks/lvmthin", method: 'POST', items: [ { xtype: 'pveDiskSelector', name: 'device', nodename: me.nodename, diskType: 'unused', 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', emptyText: gettext('No thinpools found'), stateful: true, stateId: 'grid-node-lvmthin', columns: [ { text: gettext('Name'), dataIndex: 'lv', flex: 1 }, { 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' } ], rootVisible: false, useArrows: true, tbar: [ { text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { var me = this.up('panel'); me.reload(); } }, { text: gettext('Create') + ': Thinpool', handler: function() { var me = this.up('panel'); var win = Ext.create('PVE.node.CreateLVMThin', { nodename: me.nodename, taskDone: function() { me.reload(); } }).show(); } } ], reload: function() { var me = this; me.store.load(); me.store.sort(); }, listeners: { activate: function() { var me = this; me.reload(); } }, initComponent: function() { /*jslint confusion: true */ var 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: function(data) { return data.used/data.lv_size; } }, { type: 'number', name: 'metadata_usage', calculate: function(data) { return 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.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: 'pveDiskSelector', name: 'device', nodename: me.nodename, diskType: 'unused', 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', 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() { var me = this.up('panel'); me.reload(); } }, { text: gettext('Create') + ': Directory', handler: function() { var me = this.up('panel'); var win = Ext.create('PVE.node.CreateDirectory', { nodename: me.nodename }).show(); win.on('destroy', function() { me.reload(); }); } } ], reload: function() { var me = this; me.store.load(); me.store.sort(); }, listeners: { activate: function() { var me = this; me.reload(); } }, initComponent: function() { /*jslint confusion: true */ var 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(); } }); /*jslint confusion: true*/ Ext.define('PVE.node.CreateZFS', { extend: 'Proxmox.window.Edit', xtype: 'pveCreateZFS', subject: 'ZFS', showProgress: true, onlineHelp: 'chapter_zfs', initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.isCreate = true; var update_disklist = function() { var grid = me.down('#disklist'); var disks = grid.getSelection(); var val = []; disks.sort(function(a,b) { var aorder = a.get('order') || 0; var border = b.get('order') || 0; return (aorder - border); }); disks.forEach(function(disk) { val.push(disk.get('devpath')); }); me.down('field[name=devices]').setValue(val.join(',')); }; Ext.apply(me, { url: '/nodes/' + me.nodename + '/disks/zfs', method: 'POST', items: [ { xtype: 'inputpanel', onGetValues: function(values) { return values; }, column1: [ { xtype: 'textfield', hidden: true, name: 'devices', allowBlank: false }, { xtype: 'proxmoxtextfield', name: 'name', fieldLabel: gettext('Name'), allowBlank: false }, { 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'] ] }, { xtype: 'proxmoxKVComboBox', fieldLabel: gettext('Compression'), name: 'compression', value: 'on', comboItems: [ ['on', 'on'], ['off', 'off'], ['gzip', 'gzip'], ['lz4', 'lz4'], ['lzjb', 'lzjb'], ['zle', 'zle'] ] }, { xtype: 'proxmoxintegerfield', fieldLabel: gettext('ashift'), minValue: 9, maxValue: 16, value: '12', name: 'ashift' } ], columnB: [ { xtype: 'grid', height: 200, emptyText: gettext('No Disks unused'), itemId: 'disklist', selModel: 'checkboxmodel', listeners: { selectionchange: update_disklist }, store: { proxy: { type: 'proxmox', url: '/api2/json/nodes/' + me.nodename + '/disks/list?type=unused' } }, columns: [ { text: gettext('Device'), dataIndex: 'devpath', flex: 1 }, { text: gettext('Serial'), dataIndex: 'serial' }, { text: gettext('Size'), dataIndex: 'size', renderer: PVE.Utils.render_size }, { header: gettext('Order'), xtype: 'widgetcolumn', dataIndex: 'order', sortable: true, widget: { xtype: 'proxmoxintegerfield', minValue: 1, isFormField: false, listeners: { change: function(numberfield, value, old_value) { var record = numberfield.getWidgetRecord(); record.set('order', value); update_disklist(record); } } } } ] } ] } ] }); me.callParent(); me.down('#disklist').getStore().load(); } }); Ext.define('PVE.node.ZFSDevices', { extend: 'Ext.tree.Panel', xtype: 'pveZFSDevices', stateful: true, stateId: 'grid-node-zfsstatus', columns: [ { xtype: 'treecolumn', text: gettext('Name'), dataIndex: 'name', flex: 1 }, { text: gettext('Health'), renderer: PVE.Utils.render_zfs_health, dataIndex: 'state' }, { text: 'READ', dataIndex: 'read' }, { text: 'WRITE', dataIndex: 'write' }, { text: 'CKSUM', dataIndex: 'cksum' }, { text: gettext('Message'), dataIndex: 'msg' } ], rootVisible: true, reload: function() { var me = this; var sm = me.getSelectionModel(); Proxmox.Utils.API2Request({ url: "/nodes/" + me.nodename + "/disks/zfs/" + me.zpool, 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() { /*jslint confusion: true */ var me = this; if (!me.nodename) { throw "no node name specified"; } if (!me.zpool) { throw "no zpool specified"; } var sm = Ext.create('Ext.selection.TreeModel', {}); Ext.apply(me, { selModel: sm, fields: ['name', 'status', { type: 'string', name: 'iconCls', calculate: function(data) { var txt = 'fa x-fa-tree fa-'; if (data.leaf) { return txt + 'hdd-o'; } } } ], sorters: 'name' }); me.callParent(); me.reload(); } }); Ext.define('PVE.node.ZFSStatus', { extend: 'Proxmox.grid.ObjectGrid', xtype: 'pveZFSStatus', layout: 'fit', border: false, initComponent: function() { /*jslint confusion: true */ var me = this; if (!me.nodename) { throw "no node name specified"; } if (!me.zpool) { throw "no zpool specified"; } me.url = "/api2/extjs/nodes/" + me.nodename + "/disks/zfs/" + me.zpool; me.rows = { scan: { header: gettext('Scan') }, status: { header: gettext('Status') }, action: { header: gettext('Action') }, errors: { header: gettext('Errors') } }; me.callParent(); me.reload(); } }); Ext.define('PVE.node.ZFSList', { extend: 'Ext.grid.Panel', xtype: 'pveZFSList', 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() { var me = this.up('panel'); me.reload(); } }, { text: gettext('Create') + ': ZFS', handler: function() { var me = this.up('panel'); var win = Ext.create('PVE.node.CreateZFS', { nodename: me.nodename }).show(); win.on('destroy', function() { me.reload(); }); } }, { text: gettext('Detail'), itemId: 'detailbtn', disabled: true, handler: function() { var me = this.up('panel'); var selection = me.getSelection(); if (selection.length < 1) { return; } me.show_detail(selection[0].get('name')); } } ], show_detail: function(zpool) { var me = this; var detailsgrid = Ext.create('PVE.node.ZFSStatus', { layout: 'fit', nodename: me.nodename, flex: 0, zpool: zpool }); var devicetree = Ext.create('PVE.node.ZFSDevices', { title: gettext('Devices'), nodename: me.nodename, flex: 1, zpool: zpool }); var win = Ext.create('Ext.window.Window', { modal: true, width: 800, height: 400, resizable: true, layout: 'fit', title: gettext('Status') + ': ' + zpool, items:[{ xtype: 'panel', region: 'center', layout: { type: 'vbox', align: 'stretch' }, items: [detailsgrid, devicetree], tbar: [{ text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { devicetree.reload(); detailsgrid.reload(); } }] }] }).show(); }, set_button_status: function() { var me = this; var selection = me.getSelection(); me.down('#detailbtn').setDisabled(selection.length === 0); }, reload: function() { var me = this; me.store.load(); me.store.sort(); }, listeners: { activate: function() { var me = this; me.reload(); }, selectionchange: function() { this.set_button_status(); }, itemdblclick: function(grid, record) { var me = this; me.show_detail(record.get('name')); } }, initComponent: function() { /*jslint confusion: true */ var 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('PVE.node.StatusView', { extend: 'PVE.panel.StatusView', alias: 'widget.pveNodeStatus', height: 300, bodyPadding: '20 15 20 15', layout: { type: 'table', columns: 2, tableAttrs: { style: { width: '100%' } } }, defaults: { xtype: 'pveInfoWidget', padding: '0 15 5 15' }, items: [ { itemId: 'cpu', iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', title: gettext('CPU usage'), valueField: 'cpu', maxField: 'cpuinfo', renderer: PVE.Utils.render_node_cpu_usage }, { itemId: 'wait', iconCls: 'fa fa-fw fa-clock-o', title: gettext('IO delay'), valueField: 'wait', rowspan: 2 }, { itemId: 'load', iconCls: 'fa fa-fw fa-tasks', title: gettext('Load average'), printBar: false, textField: 'loadavg' }, { xtype: 'box', colspan: 2, padding: '0 0 20 0' }, { iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', itemId: 'memory', title: gettext('RAM usage'), valueField: 'memory', maxField: 'memory', renderer: PVE.Utils.render_node_size_usage }, { itemId: 'ksm', printBar: false, title: gettext('KSM sharing'), textField: 'ksm', renderer: function(record) { return PVE.Utils.render_size(record.shared); }, padding: '0 15 10 15' }, { iconCls: 'fa fa-fw fa-hdd-o', itemId: 'rootfs', title: gettext('HD space') + '(root)', valueField: 'rootfs', maxField: 'rootfs', renderer: PVE.Utils.render_node_size_usage }, { iconCls: 'fa fa-fw fa-refresh', itemId: 'swap', printSize: true, title: gettext('SWAP usage'), valueField: 'swap', maxField: 'swap', renderer: PVE.Utils.render_node_size_usage }, { xtype: 'box', colspan: 2, padding: '0 0 20 0' }, { itemId: 'cpus', colspan: 2, printBar: false, title: gettext('CPU(s)'), textField: 'cpuinfo', renderer: function(cpuinfo) { return cpuinfo.cpus + " x " + cpuinfo.model + " (" + cpuinfo.sockets.toString() + " " + (cpuinfo.sockets > 1 ? gettext('Sockets') : gettext('Socket') ) + ")"; }, value: '' }, { itemId: 'kversion', colspan: 2, title: gettext('Kernel Version'), printBar: false, textField: 'kversion', value: '' }, { itemId: 'version', colspan: 2, printBar: false, title: gettext('PVE Manager Version'), textField: 'pveversion', value: '' } ], updateTitle: function() { var me = this; var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime')); me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')'); } }); 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, padding: 5, style: { 'background-color': 'white', 'white-space': 'pre', 'font-family': 'monospace' } }); var win = Ext.create('Ext.window.Window', { title: gettext('Package versions'), width: 600, height: 400, layout: 'fit', modal: true, items: [ view ] }); 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(); var text = ''; Ext.Array.each(response.result.data, function(rec) { var version = "not correctly installed"; var 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)); } }); }, 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' }); Ext.apply(me, { tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' } ], items: [ { xtype: 'container', layout: 'column', defaults: { minHeight: 320, padding: 5, plugins: 'responsive', responsiveConfig: { 'width < 1900': { columnWidth: 1 }, 'width >= 1900': { columnWidth: 0.5 } } }, items: [ { xtype: 'pveNodeStatus', rstore: rstore, width: 770, pveSelNode: me.pveSelNode }, { xtype: 'proxmoxRRDChart', title: gettext('CPU usage'), fields: ['cpu','iowait'], fieldTitles: [gettext('CPU usage'), gettext('IO delay')], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Server load'), fields: ['loadavg'], fieldTitles: [gettext('Load average')], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Memory usage'), fields: ['memtotal','memused'], fieldTitles: [gettext('Total'), gettext('RAM usage')], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Network traffic'), fields: ['netin','netout'], store: rrdstore } ] } ], listeners: { activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } } }); me.callParent(); } }); /*global Blob*/ Ext.define('PVE.node.SubscriptionKeyEdit', { extend: 'Proxmox.window.Edit', title: gettext('Upload Subscription Key'), width: 300, items: { xtype: 'textfield', name: 'key', value: '', fieldLabel: gettext('Subscription Key') }, 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 nodename = me.pveSelNode.data.node; var getReportFileName = function() { var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i'); return me.nodename + '-report-' + now + '.txt'; }; var view = Ext.createWidget('component', { itemId: 'system-report-view', scrollable: true, style: { 'background-color': 'white', '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 = reportWindow.getComponent('system-report-view').html; var fileName = getReportFileName(); // Internet Explorer if (window.navigator.msSaveOrOpenBlob) { navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName); } else { var 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"; } var reload = function() { me.rstore.load(); }; var baseurl = '/nodes/' + me.nodename + '/subscription'; var render_status = function(value) { var message = me.getObjectValue('message'); if (message) { return value + ": " + message; } return value; }; var rows = { productname: { header: gettext('Type') }, key: { header: gettext('Subscription Key') }, status: { header: gettext('Status'), renderer: render_status }, 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') } }; Ext.apply(me, { url: '/api2/json' + baseurl, cwidth1: 170, tbar: [ { text: gettext('Upload Subscription Key'), handler: function() { var win = Ext.create('PVE.node.SubscriptionKeyEdit', { url: '/api2/extjs/' + baseurl }); win.show(); win.on('destroy', reload); } }, { text: gettext('Check'), handler: function() { Proxmox.Utils.API2Request({ params: { force: 1 }, url: baseurl, method: 'POST', waitMsgTarget: me, failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, callback: reload }); } }, { text: gettext('System Report'), handler: function() { Proxmox.Utils.checked_command(function (){ me.showReport(); }); } } ], rows: rows, listeners: { activate: reload } }); me.callParent(); } }); Ext.define('PVE.node.CertificateView', { extend: 'Ext.container.Container', xtype: 'pveCertificatesView', onlineHelp: 'sysadmin_certificate_management', mixins: ['Proxmox.Mixin.CBind' ], 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, resizable: true, items: [ { 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' }, { 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' }, { xtype: 'displayfield', fieldLabel: gettext('Subject Alternative Names'), name: 'san', renderer: PVE.Utils.render_san }, { xtype: 'textarea', editable: false, grow: true, growMax: 200, fieldLabel: gettext('Certificate'), name: 'pem' } ], initComponent: function() { var 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)) { Ext.Array.each(response.result.data, function(item) { if (item.filename === me.cert) { me.setValues(item); return false; } }); } } }); } }); 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; } 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); }, 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) { var me = this.up('form'); e = e.event; Ext.Array.each(e.target.files, function(file) { PVE.Utils.loadSSHKeyFromFile(file, function(res) { me.down('field[name=key]').setValue(res); }); }); btn.reset(); } } }, { xtype: 'box', autoEl: 'hr' }, { fieldLabel: gettext('Certificate Chain'), labelAlign: 'top', allowBlank: false, name: 'certificates', xtype: 'textarea' }, { xtype: 'filebutton', text: gettext('From File'), listeners: { change: function(btn, e, value) { var me = this.up('form'); e = e.event; Ext.Array.each(e.target.files, function(file) { PVE.Utils.loadSSHKeyFromFile(file, function(res) { me.down('field[name=certificates]').setValue(res); }); }); btn.reset(); } } }, { xtype: 'hidden', name: 'restart', value: '1' }, { xtype: 'hidden', name: 'force', value: '1' } ], initComponent: function() { var 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' ], idProperty: 'filename' }); Ext.define('PVE.node.Certificates', { extend: 'Ext.grid.Panel', xtype: 'pveCertView', tbar: [ { xtype: 'button', text: gettext('Upload Custom Certificate'), handler: function() { var me = this.up('grid'); var win = Ext.create('PVE.node.CertUpload', { nodename: me.nodename }); win.show(); win.on('destroy', me.reload, me); } }, { xtype: 'button', itemId: 'deletebtn', text: gettext('Delete Custom Certificate'), handler: function() { var me = this.up('grid'); Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/certificates/custom?restart=1', method: 'DELETE', success: function(response, opt) { 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); }, failure: function(response, opt) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); } }, '-', { xtype: 'proxmoxButton', itemId: 'viewbtn', disabled: true, text: gettext('View Certificate'), handler: function() { var me = this.up('grid'); me.view_certificate(); } } ], columns: [ { header: gettext('File'), width: 150, dataIndex: 'filename' }, { header: gettext('Issuer'), flex: 1, dataIndex: 'issuer' }, { header: gettext('Subject'), flex: 1, dataIndex: 'subject' }, { 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() { var me = this; me.rstore.load(); }, set_button_status: function() { var me = this; var rec = me.rstore.getById('pveproxy-ssl.pem'); me.down('#deletebtn').setDisabled(!rec); }, view_certificate: function() { var me = this; var 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: 'view_certificate' }, 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', me.set_button_status, me); me.rstore.startUpdate(); } }); Ext.define('PVE.node.ACMEEditor', { extend: 'Proxmox.window.Edit', xtype: 'pveACMEEditor', subject: gettext('Domains'), items: [ { xtype: 'inputpanel', items: [ { xtype: 'textarea', fieldLabel: gettext('Domains'), emptyText: "domain1.example.com\ndomain2.example.com", name: 'domains' } ], onGetValues: function(values) { if (!values.domains) { return { 'delete': 'acme' }; } var domains = values.domains.split(/\n/).join(';'); return { 'acme': 'domains=' + domains }; } } ], initComponent: function() { var me = this; me.callParent(); me.load({ success: function(response, opts) { var res = PVE.Parser.parseACME(response.result.data.acme); if (res) { res.domains = res.domains.join(' '); me.setValues(res); } } }); } }); Ext.define('PVE.node.ACMEAccountCreate', { extend: 'Proxmox.window.Edit', width: 400, title: gettext('Register Account'), isCreate: true, method: 'POST', submitText: gettext('Register'), url: '/cluster/acme/account', showTaskViewer: true, items: [ { xtype: 'proxmoxComboGrid', name: 'directory', allowBlank: false, valueField: 'url', displayField: 'name', fieldLabel: gettext('ACME Directory'), store: { autoLoad: true, fields: ['name', 'url'], idProperty: ['name'], proxy: { type: 'proxmox', url: '/api2/json/cluster/acme/directories' }, sorters: { property: 'name', order: 'ASC' } }, listConfig: { columns: [ { header: gettext('Name'), dataIndex: 'name', flex: 1 }, { header: gettext('URL'), dataIndex: 'url', flex: 1 } ] }, listeners: { change: function(combogrid, value) { var me = this; if (!value) { return; } var disp = me.up('window').down('#tos_url_display'); var field = me.up('window').down('#tos_url'); var checkbox = me.up('window').down('#tos_checkbox'); disp.setValue(gettext('Loading')); field.setValue(undefined); checkbox.setValue(undefined); Proxmox.Utils.API2Request({ url: '/cluster/acme/tos', method: 'GET', params: { directory: value }, success: function(response, opt) { me.up('window').down('#tos_url').setValue(response.result.data); me.up('window').down('#tos_url_display').setValue(response.result.data); }, failure: function(response, opt) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); } } }, { xtype: 'displayfield', itemId: 'tos_url_display', fieldLabel: gettext('Terms of Service'), renderer: PVE.Utils.render_optional_url, name: 'tos_url_display' }, { xtype: 'hidden', itemId: 'tos_url', name: 'tos_url' }, { xtype: 'proxmoxcheckbox', itemId: 'tos_checkbox', fieldLabel: gettext('Accept TOS'), submitValue: false, validateValue: function(value) { if (value && this.checked) { return true; } return false; } }, { xtype: 'textfield', name: 'contact', vtype: 'email', allowBlank: false, fieldLabel: gettext('E-Mail') } ] }); Ext.define('PVE.node.ACMEAccountView', { extend: 'Proxmox.window.Edit', width: 600, fieldDefaults: { labelWidth: 140 }, title: gettext('Account'), items: [ { xtype: 'displayfield', fieldLabel: gettext('E-Mail'), name: 'email' }, { xtype: 'displayfield', fieldLabel: gettext('Created'), name: 'createdAt' }, { xtype: 'displayfield', fieldLabel: gettext('Status'), name: 'status' }, { xtype: 'displayfield', fieldLabel: gettext('Directory'), renderer: PVE.Utils.render_optional_url, name: 'directory' }, { xtype: 'displayfield', fieldLabel: gettext('Terms of Services'), renderer: PVE.Utils.render_optional_url, name: 'tos' } ], initComponent: function() { var me = this; if (!me.accountname) { throw "no account name defined"; } me.url = '/cluster/acme/account/' + me.accountname; 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) { var data = response.result.data; data.email = data.account.contact[0]; data.createdAt = data.account.createdAt; data.status = data.account.status; me.setValues(data); } }); } }); Ext.define('PVE.node.ACME', { extend: 'Proxmox.grid.ObjectGrid', xtype: 'pveACMEView', margin: '10 0 0 0', title: 'ACME', tbar: [ { xtype: 'button', itemId: 'edit', text: gettext('Edit Domains'), handler: function() { this.up('grid').run_editor(); } }, { xtype: 'button', itemId: 'createaccount', text: gettext('Register Account'), handler: function() { var me = this.up('grid'); var win = Ext.create('PVE.node.ACMEAccountCreate', { taskDone: function() { me.load_account(); me.reload(); } }); win.show(); } }, { xtype: 'button', itemId: 'viewaccount', text: gettext('View Account'), handler: function() { var me = this.up('grid'); var win = Ext.create('PVE.node.ACMEAccountView', { accountname: 'default' }); win.show(); } }, { xtype: 'button', itemId: 'order', text: gettext('Order Certificate'), handler: function() { var me = this.up('grid'); Proxmox.Utils.API2Request({ method: 'POST', params: { force: 1 }, url: '/nodes/' + me.nodename + '/certificates/acme/certificate', success: function(response, opt) { var win = Ext.create('Proxmox.window.TaskViewer', { upid: response.result.data, taskDone: function(success) { me.certificate_order_finished(success); } }); win.show(); }, failure: function(response, opt) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); } } ], certificate_order_finished: function(success) { if (!success) { 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); }, set_button_status: function() { var me = this; var account = !!me.account; var acmeObj = PVE.Parser.parseACME(me.getObjectValue('acme')); var domains = acmeObj ? acmeObj.domains.length : 0; var order = me.down('#order'); order.setVisible(account); order.setDisabled(!account || !domains); me.down('#createaccount').setVisible(!account); me.down('#viewaccount').setVisible(account); }, load_account: function() { var me = this; // for now we only use the 'default' account Proxmox.Utils.API2Request({ url: '/cluster/acme/account/default', success: function(response, opt) { me.account = response.result.data; me.set_button_status(); }, failure: function(response, opt) { me.account = undefined; me.set_button_status(); } }); }, run_editor: function() { var me = this; var win = Ext.create(me.rows.acme.editor, me.editorConfig); win.show(); win.on('destroy', me.reload, me); }, listeners: { itemdblclick: 'run_editor' }, // account data gets loaded here account: undefined, disableSelection: true, initComponent: function() { var me = this; if (!me.nodename) { throw "no nodename given"; } me.url = '/api2/json/nodes/' + me.nodename + '/config'; me.editorConfig = { url: '/api2/extjs/nodes/' + me.nodename + '/config' }; /*jslint confusion: true*/ /*acme is a string above*/ me.rows = { acme: { defaultValue: '', header: gettext('Domains'), editor: 'PVE.node.ACMEEditor', renderer: function(value) { var acmeObj = PVE.Parser.parseACME(value); if (acmeObj) { return acmeObj.domains.join('' + lines.join('\n') + ''); scrollToEnd(); }; var addLine = function(line) { lines.push(line); if (lines.length > me.maxLines) { lines.shift(); } }; var executeCmd = function(cmd) { addLine("# " + Ext.htmlEncode(cmd)); 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; Ext.Array.each(res.split('\n'), function(line) { addLine(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); addLine("Type 'help' for help."); refresh(); }, specialkey: function(f, e) { var key = e.getKey(); switch (key) { case e.ENTER: var 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.Summary', { extend: 'Ext.panel.Panel', alias: 'widget.pveQemuSummary', 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 template = !!me.pveSelNode.data.template; var rstore = me.statusStore; var width = template ? 1 : 0.5; var items = [ { xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView', responsiveConfig: { 'width < 1900': { columnWidth: width }, 'width >= 1900': { columnWidth: width / 2 } }, itemId: 'gueststatus', pveSelNode: me.pveSelNode, rstore: rstore }, { xtype: 'pveNotesView', maxHeight: 330, itemId: 'notesview', pveSelNode: me.pveSelNode, responsiveConfig: { 'width < 1900': { columnWidth: width }, 'width >= 1900': { columnWidth: width / 2 } } } ]; var rrdstore; if (!template) { rrdstore = Ext.create('Proxmox.data.RRDStore', { rrdurl: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/rrddata", model: 'pve-rrd-guest' }); items.push( { xtype: 'proxmoxRRDChart', title: gettext('CPU usage'), pveSelNode: me.pveSelNode, fields: ['cpu'], fieldTitles: [gettext('CPU usage')], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Memory usage'), pveSelNode: me.pveSelNode, fields: ['maxmem', 'mem'], fieldTitles: [gettext('Total'), gettext('RAM usage')], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Network traffic'), pveSelNode: me.pveSelNode, fields: ['netin','netout'], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Disk IO'), pveSelNode: me.pveSelNode, fields: ['diskread','diskwrite'], store: rrdstore } ); } Ext.apply(me, { tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ], items: [ { xtype: 'container', layout: { type: 'column' }, defaults: { minHeight: 330, padding: 5, plugins: 'responsive', responsiveConfig: { 'width < 1900': { columnWidth: 1 }, 'width >= 1900': { columnWidth: 0.5 } } }, items: items } ] }); me.callParent(); if (!template) { rrdstore.startUpdate(); me.on('destroy', rrdstore.stopUpdate); } } }); 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' } }, onOSBaseChange: function(field, value) { this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]); }, 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); var scsihw = targetValues.scsihw || '__default__'; this.getViewModel().set('current.scsihw', scsihw); }, 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 { throw 'non unique widget :' + widget + ' in Wizard'; } } }, initComponent : function() { var me = this; /*jslint confusion: true */ 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)); } } } } } ]; /*jslint confusion: false */ 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 }); } }); } }); /* * 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() { var me = this; var addOS = function(settings) { if (me.hasOwnProperty(settings.parent)) { var 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-pci' }; // 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' }); // recommandation 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.ProcessorInputPanel', { extend: 'Proxmox.panel.InputPanel', alias: 'widget.pveQemuProcessorPanel', onlineHelp: 'qm_cpu', insideWizard: false, controller: { xclass: 'Ext.app.ViewController', updateCores: function() { var me = this.getView(); var sockets = me.down('field[name=sockets]').getValue(); var cores = me.down('field[name=cores]').getValue(); me.down('field[name=totalcores]').setValue(sockets*cores); var vcpus = me.down('field[name=vcpus]'); vcpus.setMaxValue(sockets*cores); vcpus.setEmptyText(sockets*cores); vcpus.validate(); }, control: { 'field[name=sockets]': { change: 'updateCores' }, 'field[name=cores]': { change: 'updateCores' } } }, onGetValues: function(values) { var me = this; if (Array.isArray(values['delete'])) { values['delete'] = values['delete'].join(','); } PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0); PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0); // build the cpu options: me.cpu.cputype = values.cputype; var flags = []; ['pcid', 'spec-ctrl'].forEach(function(flag) { if (values[flag]) { flags.push('+' + flag.toString()); } delete values[flag]; }); me.cpu.flags = flags.length ? flags.join(';') : undefined; 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; }, cpu: {}, column1: [ { xtype: 'proxmoxintegerfield', name: 'sockets', minValue: 1, maxValue: 4, value: '1', fieldLabel: gettext('Sockets'), allowBlank: false }, { xtype: 'proxmoxintegerfield', name: 'cores', minValue: 1, maxValue: 128, value: '1', fieldLabel: gettext('Cores'), allowBlank: false } ], column2: [ { xtype: 'CPUModelSelector', name: 'cputype', value: '__default__', fieldLabel: gettext('Type') }, { xtype: 'displayfield', fieldLabel: gettext('Total cores'), name: 'totalcores', value: '1' } ], advancedColumn1: [ { xtype: 'proxmoxintegerfield', name: 'vcpus', minValue: 1, maxValue: 1, value: '', fieldLabel: gettext('VCPUs'), deleteEmpty: true, allowBlank: true, emptyText: '1' }, { xtype: 'numberfield', name: 'cpulimit', minValue: 0, maxValue: 128, // api maximum value: '', step: 1, fieldLabel: gettext('CPU limit'), allowBlank: true, emptyText: gettext('unlimited') }, { xtype: 'proxmoxintegerfield', name: 'cpuunits', fieldLabel: gettext('CPU units'), minValue: 8, maxValue: 500000, value: '1024', deleteEmpty: true, allowBlank: true } ], advancedColumn2: [ { xtype: 'proxmoxcheckbox', fieldLabel: gettext('Enable NUMA'), name: 'numa', uncheckedValue: 0 }, { xtype: 'proxmoxcheckbox', fieldLabel: 'PCID', name: 'pcid', uncheckedValue: 0 }, { xtype: 'proxmoxcheckbox', fieldLabel: 'SPEC-CTRL', name: 'spec-ctrl', uncheckedValue: 0 } ] }); Ext.define('PVE.qemu.ProcessorEdit', { extend: 'Proxmox.window.Edit', initComponent : function() { var me = this; 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) { var cpu = PVE.Parser.parseQemuCpu(value); ipanel.cpu = cpu; data.cputype = cpu.cputype; if (cpu.flags) { var flags = cpu.flags.split(';'); flags.forEach(function(flag) { var sign = flag.substr(0,1); flag = flag.substr(1); data[flag] = (sign === '+'); }); } } me.setValues(data); } }); } }); Ext.define('PVE.qemu.BootOrderPanel', { extend: 'Proxmox.panel.InputPanel', alias: 'widget.pveQemuBootOrderPanel', vmconfig: {}, // store loaded vm config bootdisk: undefined, selection: [], list: [], comboboxes: [], isBootDisk: function(value) { return PVE.Utils.bus_match.test(value); }, setVMConfig: function(vmconfig) { var me = this; me.vmconfig = vmconfig; var order = me.vmconfig.boot || 'cdn'; me.bootdisk = me.vmconfig.bootdisk || undefined; // get the first 3 characters // ignore the rest (there should never be more than 3) me.selection = order.split('').slice(0,3); // build bootdev list me.list = []; Ext.Object.each(me.vmconfig, function(key, value) { if (me.isBootDisk(key) && !(/media=cdrom/).test(value)) { me.list.push([key, "Disk '" + key + "'"]); } }); me.list.push(['d', 'CD-ROM']); me.list.push(['n', gettext('Network')]); me.list.push(['__none__', Proxmox.Utils.noneText]); me.recomputeList(); me.comboboxes.forEach(function(box) { box.resetOriginalValue(); }); }, onGetValues: function(values) { var me = this; var order = me.selection.join(''); var res = { boot: order }; if (me.bootdisk && order.indexOf('c') !== -1) { res.bootdisk = me.bootdisk; } else { res['delete'] = 'bootdisk'; } return res; }, recomputeSelection: function(combobox, newVal, oldVal) { var me = this.up('#inputpanel'); me.selection = []; me.comboboxes.forEach(function(item) { var val = item.getValue(); // when selecting an already selected item, // switch it around if ((val === newVal || (me.isBootDisk(val) && me.isBootDisk(newVal))) && item.name !== combobox.name && newVal !== '__none__') { // swap items val = oldVal; } // push 'c','d' or 'n' in the array if (me.isBootDisk(val)) { me.selection.push('c'); me.bootdisk = val; } else if (val === 'd' || val === 'n') { me.selection.push(val); } }); me.recomputeList(); }, recomputeList: function(){ var me = this; // set the correct values in the kvcomboboxes var cnt = 0; me.comboboxes.forEach(function(item) { if (cnt === 0) { // never show 'none' on first combobox item.store.loadData(me.list.slice(0, me.list.length-1)); } else { item.store.loadData(me.list); } item.suspendEvent('change'); if (cnt < me.selection.length) { item.setValue((me.selection[cnt] !== 'c')?me.selection[cnt]:me.bootdisk); } else if (cnt === 0){ item.setValue(''); } else { item.setValue('__none__'); } cnt++; item.resumeEvent('change'); item.validate(); }); }, initComponent : function() { var me = this; // this has to be done here, because of // the way our inputPanel class handles items me.comboboxes = [ Ext.createWidget('proxmoxKVComboBox', { fieldLabel: gettext('Boot device') + " 1", labelWidth: 120, name: 'bd1', allowBlank: false, listeners: { change: me.recomputeSelection } }), Ext.createWidget('proxmoxKVComboBox', { fieldLabel: gettext('Boot device') + " 2", labelWidth: 120, name: 'bd2', allowBlank: false, listeners: { change: me.recomputeSelection } }), Ext.createWidget('proxmoxKVComboBox', { fieldLabel: gettext('Boot device') + " 3", labelWidth: 120, name: 'bd3', allowBlank: false, listeners: { change: me.recomputeSelection } }) ]; Ext.apply(me, { items: me.comboboxes }); me.callParent(); } }); Ext.define('PVE.qemu.BootOrderEdit', { extend: 'Proxmox.window.Edit', items: [{ xtype: 'pveQemuBootOrderPanel', itemId: 'inputpanel' }], subject: gettext('Boot Order'), initComponent : function() { var me = this; me.callParent(); me.load({ success: function(response, options) { me.down('#inputpanel').setVMConfig(response.result.data); } }); } }); Ext.define('PVE.qemu.MemoryInputPanel', { extend: 'Proxmox.panel.InputPanel', alias: 'widget.pveQemuMemoryPanel', onlineHelp: 'qm_memory', insideWizard: false, onGetValues: function(values) { var me = this; var res = {}; res.memory = values.memory; res.balloon = values.balloon; if (!values.ballooning) { res.balloon = 0; res['delete'] = 'shares'; } else if (values.memory === values.balloon) { delete res.balloon; res['delete'] = 'balloon,shares'; } else if (Ext.isDefined(values.shares) && (values.shares !== "")) { res.shares = values.shares; } else { res['delete'] = "shares"; } return res; }, initComponent: function() { var me = this; var labelWidth = 160; me.items= [ { xtype: 'pveMemoryField', labelWidth: labelWidth, fieldLabel: gettext('Memory') + ' (MiB)', name: 'memory', 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, 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())); } } } ]; 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') }; ipanel.setValues(values); } }); } }); 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; 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; }, 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' } ]; 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' ]; 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'), 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: 8, value: '', allowBlank: true } ]; me.callParent(); } }); Ext.define('PVE.qemu.NetworkEdit', { extend: 'Proxmox.window.Edit', isAdd: true, initComponent : function() { /*jslint confusion: true */ var me = this; var nodename = me.pveSelNode.data.node; if (!nodename) { throw "no node name specified"; } me.isCreate = me.confid ? false : true; 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) { var value = me.vmconfig[me.confid]; var 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; } } ipanel.setNetwork(me.confid); } } }); } }); 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); }, initComponent : function() { var me = this; me.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: 'textfield', fieldLabel: gettext('Manufacturer'), regex: /^\S+$/, name: 'manufacturer' }, { xtype: 'textfield', fieldLabel: gettext('Product'), regex: /^\S+$/, name: 'product' }, { xtype: 'textfield', fieldLabel: gettext('Version'), regex: /^\S+$/, name: 'version' }, { xtype: 'textfield', fieldLabel: gettext('Serial'), regex: /^\S+$/, name: 'serial' }, { xtype: 'textfield', fieldLabel: 'SKU', regex: /^\S+$/, name: 'sku' }, { xtype: 'textfield', fieldLabel: gettext('Family'), regex: /^\S+$/, name: 'family' } ]; me.callParent(); } }); Ext.define('PVE.qemu.Smbios1Edit', { extend: 'Proxmox.window.Edit', initComponent : function() { /*jslint confusion: true */ 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) { var i, confid; me.vmconfig = response.result.data; var value = me.vmconfig.smbios1; if (value) { var 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.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'; var match = drive.file.match(/^([^:]+):/); if (match) { values.cdstorage = match[1]; values.cdimage = drive.file; } } me.drive = drive; me.setValues(values); }, setNodename: function(nodename) { var me = this; me.cdstoragesel.setNodename(nodename); me.cdfilesel.setStorage(undefined, nodename); }, initComponent : function() { var me = this; me.drive = {}; var items = []; if (!me.confid) { me.bussel = Ext.create('PVE.form.ControllerSelector', { noVirtIO: true }); 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; } me.down('field[name=cdstorage]').setDisabled(!value); me.down('field[name=cdimage]').setDisabled(!value); me.down('field[name=cdimage]').validate(); } } }); me.cdfilesel = Ext.create('PVE.form.FileSelector', { name: 'cdimage', nodename: me.nodename, storageContent: 'iso', fieldLabel: gettext('ISO image'), labelAlign: 'right', allowBlank: false }); me.cdstoragesel = Ext.create('PVE.form.StorageSelector', { name: 'cdstorage', nodename: me.nodename, fieldLabel: gettext('Storage'), labelAlign: 'right', storageContent: 'iso', allowBlank: false, autoSelect: me.insideWizard, listeners: { change: function(f, value) { me.cdfilesel.setStorage(value); } } }); items.push(me.cdstoragesel); items.push(me.cdfilesel); 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 ? false : true; 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) { var value = response.result.data[me.confid]; var drive = PVE.Parser.parseQemuDrive(me.confid, value); if (!drive) { Ext.Msg.alert('Error', 'Unable to parse drive options'); me.close(); return; } ipanel.setDrive(drive); } } }); } }); /*jslint confusion: true */ /* '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 vmconfig: {}, // used to select usused disks controller: { xclass: 'Ext.app.ViewController', onControllerChange: function(field) { var value = field.getValue(); var allowIOthread = value.match(/^(virtio|scsi)/); this.lookup('iothread').setDisabled(!allowIOthread); if (!allowIOthread) { this.lookup('iothread').setValue(false); } var virtio = value.match(/^virtio/); this.lookup('discard').setDisabled(virtio); this.lookup('ssd').setDisabled(virtio); if (virtio) { this.lookup('discard').setValue(false); this.lookup('ssd').setValue(false); } this.lookup('scsiController').setVisible(value.match(/^scsi/)); }, control: { 'field[name=controller]': { change: 'onControllerChange', afterrender: 'onControllerChange' }, 'field[name=iothread]' : { change: function(f, value) { if (!this.getView().insideWizard) { return; } var vmScsiType = value ? 'virtio-scsi-single': 'virtio-scsi-pci'; this.lookupReference('scsiController').setValue(vmScsiType); } } } }, 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 { me.drive.file = values.hdstorage + ":" + values.disksize; } me.drive.format = values.diskformat; } if (values.nobackup) { me.drive.backup = 'no'; } else { delete me.drive.backup; } if (values.noreplicate) { me.drive.replicate = 'no'; } else { delete me.drive.replicate; } if (values.discard) { me.drive.discard = 'on'; } else { delete me.drive.discard; } if (values.ssd) { me.drive.ssd = 'on'; } else { delete me.drive.ssd; } if (values.iothread) { me.drive.iothread = 'on'; } else { delete me.drive.iothread; } if (values.cache) { me.drive.cache = values.cache; } else { delete me.drive.cache; } var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr']; Ext.Array.each(names, function(name) { if (values[name]) { me.drive[name] = values[name]; } else { delete me.drive[name]; } var burst_name = name + '_max'; if (values[burst_name] && values[name]) { me.drive[burst_name] = values[burst_name]; } else { delete me.drive[burst_name]; } }); params[confid] = PVE.Parser.printQemuDrive(me.drive); return params; }, setVMConfig: function(vmconfig) { var me = this; me.vmconfig = vmconfig; if (me.bussel) { me.bussel.setVMConfig(vmconfig); me.scsiController.setValue(vmconfig.scsihw); } if (me.unusedDisks) { var 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.nobackup = !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.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); }, initComponent : function() { var me = this; var labelWidth = 140; me.drive = {}; me.column1 = []; me.column2 = []; me.advancedColumn1 = []; me.advancedColumn2 = []; if (!me.confid || me.unused) { me.bussel = Ext.create('PVE.form.ControllerSelector', { vmconfig: me.insideWizard ? {ide2: 'cdrom'} : {} }); me.column1.push(me.bussel); me.scsiController = Ext.create('Ext.form.field.Display', { fieldLabel: gettext('SCSI Controller'), reference: 'scsiController', bind: me.insideWizard ? { value: '{current.scsihw}' } : undefined, renderer: PVE.Utils.render_scsihw, submitValue: false, hidden: true }); me.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 }); me.column1.push(me.unusedDisks); } else if (me.isCreate) { me.column1.push({ xtype: 'pveDiskStorageSelector', storageContent: 'images', name: 'disk', nodename: me.nodename, autoSelect: me.insideWizard }); } else { me.column1.push({ xtype: 'textfield', disabled: true, submitValue: false, fieldLabel: gettext('Disk image'), name: 'hdimage' }); } me.column2.push( { xtype: 'CacheTypeSelector', name: 'cache', value: '__default__', fieldLabel: gettext('Cache') }, { xtype: 'proxmoxcheckbox', fieldLabel: gettext('Discard'), disabled: me.confid && me.confid.match(/^virtio/), reference: 'discard', name: 'discard' } ); me.advancedColumn1.push( { xtype: 'proxmoxcheckbox', disabled: me.confid && me.confid.match(/^virtio/), fieldLabel: gettext('SSD emulation'), labelWidth: labelWidth, name: 'ssd', reference: 'ssd' }, { xtype: 'proxmoxcheckbox', disabled: me.confid && !me.confid.match(/^(virtio|scsi)/), fieldLabel: 'IO thread', labelWidth: labelWidth, reference: 'iothread', name: 'iothread' }, { 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') } ); me.advancedColumn2.push( { xtype: 'proxmoxcheckbox', fieldLabel: gettext('No backup'), labelWidth: labelWidth, name: 'nobackup' }, { xtype: 'proxmoxcheckbox', fieldLabel: gettext('Skip replication'), labelWidth: labelWidth, name: 'noreplicate' }, { 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.callParent(); } }); /*jslint confusion: false */ Ext.define('PVE.qemu.HDEdit', { extend: 'Proxmox.window.Edit', isAdd: true, backgroundDelay: 5, 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 }); var subject; 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(); /*jslint confusion: true*/ /* '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) { var value = response.result.data[me.confid]; var 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 } } }); /*jslint confusion: false*/ } }); 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) { 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') + ' (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()) { var 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(); if (!me.disk) { return; } } }); Ext.define('PVE.window.HDMove', { extend: 'Ext.window.Window', resizable: false, move_disk: function(disk, storage, format, delete_disk) { var me = this; var qemu = (me.type === 'qemu'); var params = {}; params.storage = storage; params[qemu ? 'disk':'volume'] = disk; if (format && qemu) { params.format = format; } if (delete_disk) { params['delete'] = 1; } var url = '/nodes/' + me.nodename + '/' + me.type + '/' + me.vmid + '/'; url += qemu ? 'move_disk' : 'move_volume'; Proxmox.Utils.API2Request({ params: params, url: url, waitMsgTarget: me, method: 'POST', failure: function(response, opts) { Ext.Msg.alert('Error', response.htmlStatus); }, success: function(response, options) { var upid = response.result.data; var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid }); win.show(); win.on('destroy', function() { me.close(); }); } }); }, initComponent : function() { var me = this; var diskarray = []; if (!me.nodename) { throw "no node name specified"; } if (!me.vmid) { throw "no VM ID specified"; } if (!me.type) { me.type = 'qemu'; } var qemu = (me.type === 'qemu'); var items = [ { xtype: 'displayfield', name: qemu ? 'disk' : 'volume', value: me.disk, fieldLabel: qemu ? gettext('Disk') : gettext('Mount Point'), vtype: 'StorageId', allowBlank: false } ]; items.push({ xtype: 'pveDiskStorageSelector', storageLabel: gettext('Target Storage'), nodename: me.nodename, storageContent: qemu ? 'images' : 'rootdir', hideSize: true }); items.push({ xtype: 'proxmoxcheckbox', fieldLabel: gettext('Delete source'), name: 'deleteDisk', uncheckedValue: 0, checked: false }); me.formPanel = Ext.create('Ext.form.Panel', { bodyPadding: 10, border: false, fieldDefaults: { labelWidth: 100, anchor: '100%' }, items: items }); var form = me.formPanel.getForm(); var submitBtn; me.title = qemu ? gettext("Move disk") : gettext('Move Volume'); submitBtn = Ext.create('Ext.Button', { text: me.title, handler: function() { if (form.isValid()) { var values = form.getValues(); me.move_disk(me.disk, values.hdstorage, values.diskformat, values.deleteDisk); } } }); Ext.apply(me, { modal: true, width: 350, border: false, layout: 'fit', buttons: [ submitBtn ], items: [ me.formPanel ] }); me.callParent(); me.mon(me.formPanel, 'validitychange', function(fp, isValid) { submitBtn.setDisabled(!isValid); }); me.formPanel.isValid(); } }); 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; 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"; } me.drive.format = values.diskformat; 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); }, initComponent : function() { var me = this; me.drive = {}; me.items= []; me.items.push({ xtype: 'pveDiskStorageSelector', name: 'efidisk0', storageContent: 'images', nodename: me.nodename, hideSize: true }); me.callParent(); } }); Ext.define('PVE.qemu.EFIDiskEdit', { extend: 'Proxmox.window.Edit', isAdd: true, subject: gettext('EFI Disk'), 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, isCreate: true }]; me.callParent(); } }); Ext.define('PVE.qemu.DisplayInputPanel', { extend: 'Proxmox.panel.InputPanel', xtype: 'pveDisplayInputPanel', onGetValues: function(values) { var ret = PVE.Parser.printPropertyString(values, 'type'); if (ret === '') { return { 'delete': 'vga' }; } return { vga: ret }; }, items: [{ name: 'type', xtype: 'proxmoxKVComboBox', value: '__default__', deleteEmpty: false, fieldLabel: gettext('Graphic card'), comboItems: PVE.Utils.kvm_vga_driver_array(), validator: function() { var v = this.getValue(); var cfg = this.up('proxmoxWindowEdit').vmconfig || {}; if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) { var fmt = gettext("Serial interface '{0}' is not correctly configured."); return Ext.String.format(fmt, v); } return true; }, listeners: { change: function(cb, val) { var me = this.up('panel'); if (!val) { return; } var disable = false; var emptyText = Proxmox.Utils.defaultText; switch (val) { case "cirrus": emptyText = "4"; break; case "std": emptyText = "16"; break; case "qxl": case "qxl2": case "qxl3": case "qxl4": emptyText = "16"; break; case "vmware": emptyText = "16"; break; case "none": case "serial0": case "serial1": case "serial2": case "serial3": emptyText = 'N/A'; disable = true; break; case "virtio": emptyText = "256"; break; default: break; } var memoryfield = me.down('field[name=memory]'); memoryfield.setEmptyText(emptyText); memoryfield.setDisabled(disable); } } },{ xtype: 'proxmoxintegerfield', emptyText: Proxmox.Utils.defaultText, fieldLabel: gettext('Memory') + ' (MiB)', minValue: 4, maxValue: 512, step: 4, name: 'memory' }] }); Ext.define('PVE.qemu.DisplayEdit', { extend: 'Proxmox.window.Edit', vmconfig: undefined, subject: gettext('Display'), width: 350, items: [{ xtype: 'pveDisplayInputPanel' }], initComponent : function() { var me = this; me.callParent(); me.load({ success: function(response) { me.vmconfig = response.result.data; var vga = me.vmconfig.vga || '__default__'; me.setValues(PVE.Parser.parsePropertyString(vga, 'type')); } }); } }); 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.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.tdCls) { metaData.tdCls = rowdef.tdCls; if (rowdef.tdCls == 'pve-itype-icon-storage') { var value = me.getObjectValue(key, '', false); if (value === '') { value = me.getObjectValue(key, '', true); } if (value.match(/vm-.*-cloudinit/)) { metaData.tdCls = 'pve-itype-icon-cloud'; return rowdef.cloudheader; } else if (value.match(/media=cdrom/)) { metaData.tdCls = 'pve-itype-icon-cdrom'; return rowdef.cdheader; } } } else if (iconCls) { icon = ""; metaData.tdCls += " pve-itype-fa"; } return icon + txt; }, initComponent : function() { var me = this; var i, confid; 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']; /*jslint confusion: true */ var 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'], 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]'; } return res; } }, sockets: { header: gettext('Processors'), never_delete: true, editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ? 'PVE.qemu.ProcessorEdit' : undefined, tdCls: 'pve-itype-icon-processor', group: 3, defaultValue: '1', multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'], 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 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 +']'; } 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, tdCls: 'pve-itype-icon-display', group:5, defaultValue: '', renderer: PVE.Utils.render_kvm_vga_driver }, machine: { header: gettext('Machine'), editor: caps.vms['VM.Config.HWType'] ? { xtype: 'proxmoxWindowEdit', subject: gettext('Machine'), width: 350, items: [{ xtype: 'proxmoxKVComboBox', name: 'machine', value: '__default__', fieldLabel: gettext('Machine'), comboItems: [ ['__default__', PVE.Utils.render_qemu_machine('')], ['q35', 'q35'] ] }]} : undefined, iconCls: 'cogs', never_delete: true, group: 6, defaultValue: '', renderer: PVE.Utils.render_qemu_machine }, 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: '' }, cores: { visible: false }, cpu: { visible: false }, numa: { visible: false }, balloon: { visible: false }, hotplug: { visible: false }, vcpus: { visible: false }, cpuunits: { visible: false }, cpulimit: { visible: false }, shares: { visible: false } }; /*jslint confusion: false */ PVE.Utils.forEachBus(undefined, function(type, id) { var confid = type + id; rows[confid] = { group: 10, tdCls: 'pve-itype-icon-storage', editor: 'PVE.qemu.HDEdit', never_delete: caps.vms['VM.Config.Disk'] ? false : true, header: gettext('Hard Disk') + ' (' + confid +')', cdheader: gettext('CD/DVD Drive') + ' (' + confid +')', cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')' }; }); for (i = 0; i < 32; i++) { confid = "net" + i.toString(); rows[confid] = { group: 15, order: i, tdCls: 'pve-itype-icon-network', editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined, never_delete: caps.vms['VM.Config.Network'] ? false : true, header: gettext('Network Device') + ' (' + confid +')' }; } rows.efidisk0 = { group: 20, tdCls: 'pve-itype-icon-storage', editor: null, never_delete: caps.vms['VM.Config.Disk'] ? false : true, header: gettext('EFI Disk') }; for (i = 0; i < 5; i++) { confid = "usb" + i.toString(); rows[confid] = { group: 25, order: i, tdCls: 'pve-itype-icon-usb', editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined, never_delete: caps.nodes['Sys.Console'] ? false : true, header: gettext('USB Device') + ' (' + confid + ')' }; } for (i = 0; i < 4; i++) { confid = "hostpci" + i.toString(); rows[confid] = { group: 30, order: i, tdCls: 'pve-itype-icon-pci', never_delete: caps.nodes['Sys.Console'] ? false : true, editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined, header: gettext('PCI Device') + ' (' + confid + ')' }; } for (i = 0; i < 4; i++) { confid = "serial" + i.toString(); rows[confid] = { group: 35, order: i, tdCls: 'pve-itype-icon-serial', never_delete: caps.nodes['Sys.Console'] ? false : true, header: gettext('Serial Port') + ' (' + confid + ')' }; } for (i = 0; i < 256; i++) { rows["unused" + i.toString()] = { group: 99, order: i, tdCls: 'pve-itype-icon-storage', editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined, header: gettext('Unused Disk') + ' ' + i.toString() }; } 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; } }; var reload = function() { me.rstore.load(); }; var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; 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 editor = rowdef.editor; if (rowdef.tdCls == 'pve-itype-icon-storage') { if (!diskCap) { return; } var value = me.getObjectValue(rec.data.key, '', true); if (value.match(/vm-.*-cloudinit/)) { return; } else if (value.match(/media=cdrom/)) { editor = 'PVE.qemu.CDEdit'; } } var win; if (Ext.isString(editor)) { win = Ext.create(editor, { pveSelNode: me.pveSelNode, confid: rec.data.key, url: '/api2/extjs/' + baseurl }); } else { var config = Ext.apply({ pveSelNode: me.pveSelNode, confid: rec.data.key, url: '/api2/extjs/' + baseurl }, rowdef.editor); win = Ext.createWidget(rowdef.editor.xtype, config); win.load(); } win.show(); win.on('destroy', reload); }; var run_resize = function() { var rec = sm.getSelection()[0]; if (!rec) { return; } var win = Ext.create('PVE.window.HDResize', { disk: rec.data.key, nodename: nodename, vmid: vmid }); win.show(); win.on('destroy', reload); }; var run_move = function() { var rec = sm.getSelection()[0]; if (!rec) { return; } var win = Ext.create('PVE.window.HDMove', { disk: rec.data.key, nodename: nodename, vmid: vmid }); win.show(); win.on('destroy', reload); }; var edit_btn = new Proxmox.button.Button({ text: gettext('Edit'), selModel: sm, disabled: true, handler: run_editor }); var resize_btn = new Proxmox.button.Button({ text: gettext('Resize disk'), selModel: sm, disabled: true, handler: run_resize }); var move_btn = new Proxmox.button.Button({ text: gettext('Move disk'), selModel: sm, disabled: true, handler: run_move }); var 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) { var 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}'); } var entry = rec.data.key; var msg = Ext.String.format(warn, "'" + me.renderKey(entry, {}, rec) + "'"); if (entry.match(/^unused\d+$/)) { msg += " " + gettext('This will permanently erase all data.'); } return msg; }, handler: function(b, e, rec) { Proxmox.Utils.API2Request({ url: '/api2/extjs/' + baseurl, waitMsgTarget: me, method: b.RESTMethod, params: { 'delete': rec.data.key }, callback: function() { reload(); }, failure: function (response, opts) { Ext.Msg.alert('Error', response.htmlStatus); }, success: function(response, options) { if (b.RESTMethod === 'POST') { var upid = response.result.data; var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid, listeners: { destroy: function () { me.reload(); } } }); win.show(); } } }); }, listeners: { render: function(btn) { // hack: calculate an optimal 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 }); } } }); var revert_btn = new Proxmox.button.Button({ text: gettext('Revert'), selModel: sm, disabled: true, handler: function(b, e, rec) { var rowdef = me.rows[rec.data.key] || {}; var keys = rowdef.multiKey || [ rec.data.key ]; var revert = keys.join(','); Proxmox.Utils.API2Request({ url: '/api2/extjs/' + baseurl, waitMsgTarget: me, method: 'PUT', params: { 'revert': revert }, callback: function() { reload(); }, failure: function (response, opts) { Ext.Msg.alert('Error',response.htmlStatus); } }); } }); var efidisk_menuitem = Ext.create('Ext.menu.Item',{ text: gettext('EFI Disk'), iconCls: 'pve-itype-icon-storage', disabled: !caps.vms['VM.Config.Disk'], handler: function() { var rstoredata = me.rstore.getData().map; // check if ovmf is configured if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') { var win = Ext.create('PVE.qemu.EFIDiskEdit', { url: '/api2/extjs/' + baseurl, pveSelNode: me.pveSelNode }); win.on('destroy', reload); win.show(); } else { Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.')); } } }); var set_button_status = function() { var sm = me.getSelectionModel(); var rec = sm.getSelection()[0]; // disable button when we have an efidisk already // disable is ok in this case, because you can instantly // see that there is already one efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined); // en/disable usb add button var usbcount = 0; var pcicount = 0; var hasCloudInit = false; me.rstore.getData().items.forEach(function(item){ if (/^usb\d+/.test(item.id)) { usbcount++; } else if (/^hostpci\d+/.test(item.id)) { pcicount++; } if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) { hasCloudInit = true; } }); // heuristic only for disabling some stuff, the backend has the final word. var noSysConsolePerm = !caps.nodes['Sys.Console']; me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5)); me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4)); me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit); if (!rec) { remove_btn.disable(); edit_btn.disable(); resize_btn.disable(); move_btn.disable(); revert_btn.disable(); return; } var key = rec.data.key; var value = rec.data.value; var rowdef = rows[key]; var pending = rec.data['delete'] || me.hasPendingChanges(key); var isUnusedDisk = key.match(/^unused\d+/); var isUsedDisk = !isUnusedDisk && rowdef.tdCls == 'pve-itype-icon-storage' && (value && !value.match(/media=cdrom/)); var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/)); var isEfi = (key === 'efidisk0'); remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap)); remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText); remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT'; edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || !diskCap); resize_btn.setDisabled(pending || !isUsedDisk || !diskCap); move_btn.setDisabled(pending || !isUsedDisk || !diskCap); revert_btn.setDisabled(!pending); }; 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({ items: [ { text: gettext('Hard Disk'), iconCls: 'pve-itype-icon-storage', disabled: !caps.vms['VM.Config.Disk'], handler: function() { var win = Ext.create('PVE.qemu.HDEdit', { url: '/api2/extjs/' + baseurl, pveSelNode: me.pveSelNode }); win.on('destroy', reload); win.show(); } }, { text: gettext('CD/DVD Drive'), iconCls: 'pve-itype-icon-cdrom', disabled: !caps.vms['VM.Config.Disk'], handler: function() { var win = Ext.create('PVE.qemu.CDEdit', { url: '/api2/extjs/' + baseurl, pveSelNode: me.pveSelNode }); win.on('destroy', reload); win.show(); } }, { text: gettext('Network Device'), iconCls: 'pve-itype-icon-network', disabled: !caps.vms['VM.Config.Network'], handler: function() { var win = Ext.create('PVE.qemu.NetworkEdit', { url: '/api2/extjs/' + baseurl, pveSelNode: me.pveSelNode, isCreate: true }); win.on('destroy', reload); win.show(); } }, efidisk_menuitem, { text: gettext('USB Device'), itemId: 'addusb', iconCls: 'pve-itype-icon-usb', disabled: !caps.nodes['Sys.Console'], handler: function() { var win = Ext.create('PVE.qemu.USBEdit', { url: '/api2/extjs/' + baseurl, pveSelNode: me.pveSelNode }); win.on('destroy', reload); win.show(); } }, { text: gettext('PCI Device'), itemId: 'addpci', iconCls: 'pve-itype-icon-pci', disabled: !caps.nodes['Sys.Console'], handler: function() { var win = Ext.create('PVE.qemu.PCIEdit', { url: '/api2/extjs/' + baseurl, pveSelNode: me.pveSelNode }); win.on('destroy', reload); win.show(); } }, { text: gettext('Serial Port'), itemId: 'addserial', iconCls: 'pve-itype-icon-serial', disabled: !caps.vms['VM.Config.Options'], handler: function() { var win = Ext.create('PVE.qemu.SerialEdit', { url: '/api2/extjs/' + baseurl }); win.on('destroy', reload); win.show(); } }, { text: gettext('CloudInit Drive'), itemId: 'addci', iconCls: 'pve-itype-icon-cloud', disabled: !caps.nodes['Sys.Console'], handler: function() { var win = Ext.create('PVE.qemu.CIDriveEdit', { url: '/api2/extjs/' + baseurl, pveSelNode: me.pveSelNode }); win.on('destroy', reload); win.show(); } } ] }) }, remove_btn, edit_btn, resize_btn, move_btn, revert_btn ], rows: rows, sorterFn: sorterFn, listeners: { itemdblclick: run_editor, selectionchange: set_button_status } }); me.callParent(); me.on('activate', me.rstore.startUpdate); me.on('destroy', me.rstore.stopUpdate); me.mon(me.rstore, 'refresh', function() { set_button_status(); }); } }); 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.BiosEdit', { extend: 'Proxmox.window.Edit', alias: 'widget.pveQemuBiosEdit', initComponent : function() { var me = this; var EFIHint = Ext.createWidget({ xtype: 'displayfield', //submitValue is false, so we don't get submitted userCls: 'pve-hint', value: 'You need to add an EFI disk for storing the ' + 'EFI settings. See the online help for details.', hidden: true }); Ext.applyIf(me, { subject: 'BIOS', items: [ { xtype: 'pveQemuBiosSelector', onlineHelp: 'qm_bios_and_uefi', name: 'bios', value: '__default__', fieldLabel: 'BIOS', listeners: { 'change' : function(field, newValue) { if (newValue == 'ovmf') { Proxmox.Utils.API2Request({ url : me.url, method : 'GET', failure : function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success : function(response, opts) { var vmConfig = response.result.data; // there can be only one if (!vmConfig.efidisk0) { EFIHint.setVisible(true); } } }); } else { if (EFIHint.isVisible()) { EFIHint.setVisible(false); } } } } }, EFIHint ] }); me.callParent(); me.load(); } }); /*jslint confusion: true */ Ext.define('PVE.qemu.Options', { extend: 'Proxmox.grid.PendingObjectGrid', alias: ['widget.PVE.qemu.Options'], onlineHelp: 'qm_options', initComponent : function() { var me = this; var i; 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) { var i; var text = ''; var bootdisk = me.getObjectValue('bootdisk', undefined, pending); order = order || 'cdn'; for (i = 0; i < order.length; i++) { var sel = order.substring(i, i + 1); if (text) { text += ', '; } if (sel === 'c') { if (bootdisk) { text += "Disk '" + bootdisk + "'"; } else { text += "Disk"; } } else if (sel === 'n') { text += 'Network'; } else if (sel === 'a') { text += 'Floppy'; } else if (sel === 'd') { text += '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: false, renderer: Proxmox.Utils.format_boolean, editor: caps.vms['VM.Config.Options'] ? { xtype: 'proxmoxWindowEdit', subject: gettext('Use local time for RTC'), items: { xtype: 'proxmoxcheckbox', name: 'localtime', uncheckedValue: 0, defaultValue: 0, deleteDefaultValue: true, 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: gettext('Qemu Agent'), defaultValue: false, renderer: PVE.Utils.render_qga_features, editor: caps.vms['VM.Config.Options'] ? { xtype: 'proxmoxWindowEdit', subject: gettext('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 }, 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 Proxmox.button.Button({ text: gettext('Revert'), disabled: true, handler: function() { var sm = me.getSelectionModel(); var rec = sm.getSelection()[0]; if (!rec) { return; } var rowdef = me.rows[rec.data.key] || {}; var keys = rowdef.multiKey || [ rec.data.key ]; var revert = keys.join(','); Proxmox.Utils.API2Request({ url: '/api2/extjs/' + baseurl, waitMsgTarget: me, method: 'PUT', params: { 'revert': revert }, callback: function() { me.reload(); }, failure: function (response, opts) { Ext.Msg.alert('Error',response.htmlStatus); } }); } }); 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.rstore.on('datachanged', function() { set_button_status(); }); } }); Ext.define('PVE.window.Snapshot', { extend: 'Ext.window.Window', resizable: false, // needed for finding the reference to submitbutton // because we do not have a controller referenceHolder: true, defaultButton: 'submitbutton', defaultFocus: 'field', take_snapshot: function(snapname, descr, vmstate) { var me = this; var params = { snapname: snapname, vmstate: vmstate ? 1 : 0 }; if (descr) { params.description = descr; } Proxmox.Utils.API2Request({ params: params, url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot", waitMsgTarget: me, method: 'POST', 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(); me.close(); } }); }, update_snapshot: function(snapname, descr) { var me = this; Proxmox.Utils.API2Request({ params: { description: descr }, url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" + snapname + '/config', waitMsgTarget: me, method: 'PUT', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function(response, options) { me.close(); } }); }, initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } if (!me.vmid) { throw "no VM ID specified"; } var summarystore = Ext.create('Ext.data.Store', { model: 'KeyValue', sorters: [ { property : 'key', direction: 'ASC' } ] }); var items = [ { xtype: me.snapname ? 'displayfield' : 'textfield', name: 'snapname', value: me.snapname, fieldLabel: gettext('Name'), vtype: 'ConfigId', allowBlank: false } ]; if (me.snapname) { items.push({ xtype: 'displayfield', name: 'snaptime', renderer: PVE.Utils.render_timestamp_human_readable, fieldLabel: gettext('Timestamp') }); } else { items.push({ xtype: 'proxmoxcheckbox', name: 'vmstate', uncheckedValue: 0, defaultValue: 0, checked: 1, fieldLabel: gettext('Include RAM') }); } items.push({ xtype: 'textareafield', grow: true, name: 'description', fieldLabel: gettext('Description') }); if (me.snapname) { items.push({ title: gettext('Settings'), xtype: 'grid', height: 200, store: summarystore, columns: [ {header: gettext('Key'), width: 150, dataIndex: 'key'}, {header: gettext('Value'), flex: 1, dataIndex: 'value'} ] }); } me.formPanel = Ext.create('Ext.form.Panel', { bodyPadding: 10, border: false, fieldDefaults: { labelWidth: 100, anchor: '100%' }, items: items }); var form = me.formPanel.getForm(); var submitBtn; if (me.snapname) { me.title = gettext('Edit') + ': ' + gettext('Snapshot'); submitBtn = Ext.create('Ext.Button', { text: gettext('Update'), handler: function() { if (form.isValid()) { var values = form.getValues(); me.update_snapshot(me.snapname, values.description); } } }); } else { me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot'); submitBtn = Ext.create('Ext.Button', { text: gettext('Take Snapshot'), reference: 'submitbutton', handler: function() { if (form.isValid()) { var values = form.getValues(); me.take_snapshot(values.snapname, values.description, values.vmstate); } } }); } Ext.apply(me, { modal: true, width: 450, border: false, layout: 'fit', buttons: [ submitBtn ], items: [ me.formPanel ] }); if (me.snapname) { Ext.apply(me, { width: 620, height: 420 }); } me.callParent(); if (!me.snapname) { return; } // else load data Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" + me.snapname + '/config', waitMsgTarget: me, method: 'GET', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); me.close(); }, success: function(response, options) { var data = response.result.data; var kvarray = []; Ext.Object.each(data, function(key, value) { if (key === 'description' || key === 'snaptime') { return; } kvarray.push({ key: key, value: value }); }); summarystore.suspendEvents(); summarystore.add(kvarray); summarystore.sort(); summarystore.resumeEvents(); summarystore.fireEvent('refresh', summarystore); form.findField('snaptime').setValue(data.snaptime); form.findField('description').setValue(data.description); } }); } }); Ext.define('PVE.qemu.SnapshotTree', { extend: 'Ext.tree.Panel', alias: ['widget.pveQemuSnapshotTree'], load_delay: 3000, old_digest: 'invalid', stateful: true, stateId: 'grid-qemu-snapshots', sorterFn: function(rec1, rec2) { var v1 = rec1.data.snaptime; var v2 = rec2.data.snaptime; if (rec1.data.name === 'current') { return 1; } if (rec2.data.name === 'current') { return -1; } return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); }, reload: function(repeat) { var me = this; Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot', 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); 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') { digest = item.digest + item.running; if (item.running) { item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running'; } else { item.iconCls = 'fa fa-fw fa-desktop x-fa-tree'; } } 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]) { var 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.setRootNode(root); } me.load_task.delay(me.load_delay); } }); Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/feature', params: { feature: 'snapshot' }, method: 'GET', success: function(response, options) { var res = response.result.data; if (res.hasFeature) { var snpBtns = Ext.ComponentQuery.query('#snapshotBtn'); snpBtns.forEach(function(item){ item.enable(); }); } } }); }, listeners: { beforestatesave: function(grid, state, eopts) { // extjs cannot serialize functions, // so a the sorter with only the sorterFn will // not be a valid sorter when restoring the state delete state.storeState.sorters; } }, initComponent: function() { var me = this; me.nodename = me.pveSelNode.data.node; if (!me.nodename) { throw "no node name specified"; } me.vmid = me.pveSelNode.data.vmid; if (!me.vmid) { throw "no VM ID specified"; } me.load_task = new Ext.util.DelayedTask(me.reload, me); var sm = Ext.create('Ext.selection.RowModel', {}); var valid_snapshot = function(record) { return record && record.data && record.data.name && record.data.name !== 'current'; }; var valid_snapshot_rollback = function(record) { return record && record.data && record.data.name && record.data.name !== 'current' && !record.data.snapstate; }; var run_editor = function() { var rec = sm.getSelection()[0]; if (valid_snapshot(rec)) { var win = Ext.create('PVE.window.Snapshot', { snapname: rec.data.name, nodename: me.nodename, vmid: me.vmid }); win.show(); me.mon(win, 'close', me.reload, me); } }; var editBtn = new Proxmox.button.Button({ text: gettext('Edit'), disabled: true, selModel: sm, enableFn: valid_snapshot, handler: run_editor }); var rollbackBtn = new Proxmox.button.Button({ text: gettext('Rollback'), disabled: true, selModel: sm, enableFn: valid_snapshot_rollback, confirmMsg: function(rec) { return Proxmox.Utils.format_task_description('qmrollback', me.vmid) + " '" + rec.data.name + "'"; }, handler: function(btn, event) { var rec = sm.getSelection()[0]; if (!rec) { return; } var snapname = rec.data.name; Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname + '/rollback', method: 'POST', waitMsgTarget: me, 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(); } }); } }); var removeBtn = new Proxmox.button.Button({ text: gettext('Remove'), disabled: true, selModel: sm, confirmMsg: function(rec) { var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), "'" + rec.data.name + "'"); return msg; }, enableFn: valid_snapshot, handler: function(btn, event) { var rec = sm.getSelection()[0]; if (!rec) { return; } var snapname = rec.data.name; Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname, method: 'DELETE', waitMsgTarget: me, 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(); } }); } }); var snapshotBtn = Ext.create('Ext.Button', { itemId: 'snapshotBtn', text: gettext('Take Snapshot'), disabled: true, handler: function() { var win = Ext.create('PVE.window.Snapshot', { nodename: me.nodename, vmid: me.vmid }); win.show(); } }); Ext.apply(me, { layout: 'fit', rootVisible: false, animate: false, sortableColumns: false, selModel: sm, tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ], fields: [ 'name', 'description', 'snapstate', 'vmstate', 'running', { name: 'snaptime', type: 'date', dateFormat: 'timestamp' } ], columns: [ { xtype: 'treecolumn', text: gettext('Name'), dataIndex: 'name', width: 200, renderer: function(value, metaData, record) { if (value === 'current') { return "NOW"; } else { return value; } } }, { text: gettext('RAM'), align: 'center', resizable: false, dataIndex: 'vmstate', width: 50, renderer: function(value, metaData, record) { if (record.data.name !== 'current') { return 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; } if (value) { return Ext.Date.format(value,'Y-m-d H:i:s'); } } }, { 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); } } } ], columnLines: true, // will work in 4.1? listeners: { activate: me.reload, destroy: me.load_task.cancel, itemdblclick: run_editor } }); me.callParent(); me.store.sorters.add(new Ext.util.Sorter({ sorterFn: me.sorterFn })); } }); Ext.define('PVE.qemu.Config', { extend: 'PVE.panel.Config', alias: 'widget.PVE.qemu.Config', onlineHelp: 'chapter_virtual_machines', 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 template = !!me.pveSelNode.data.template; var running = !!me.pveSelNode.data.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.data.ResourceStore.getNodes().length < 2, handler: function() { var win = Ext.create('PVE.window.Migrate', { vmtype: 'qemu', nodename: nodename, vmid: vmid }); 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'] ? false : true, handler: function() { PVE.window.Clone.wrap(nodename, vmid, template, 'qemu'); } }, { text: gettext('Convert to template'), disabled: template, xtype: 'pveMenuItem', iconCls: 'fa fa-fw fa-file-o', hidden: caps.vms['VM.Allocate'] ? false : true, confirmMsg: Proxmox.Utils.format_task_description('qmtemplate', vmid), 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 = me.pveSelNode.data.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.SafeDestroy', { url: base_url, item: { type: 'VM', id: vmid } }).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: Proxmox.Utils.format_task_description('qmshutdown', vmid), handler: function() { vm_command('shutdown'); }, menu: { items: [{ text: gettext('Pause'), disabled: !caps.vms['VM.PowerMgmt'], confirmMsg: Proxmox.Utils.format_task_description('qmpause', vmid), handler: function() { vm_command("suspend"); }, iconCls: 'fa fa-pause' },{ text: gettext('Hibernate'), disabled: !caps.vms['VM.PowerMgmt'], confirmMsg: Proxmox.Utils.format_task_description('qmsuspend', vmid), tooltip: gettext('Suspend to disk'), handler: function() { vm_command("suspend", { todisk: 1 }); }, iconCls: 'fa fa-download' },{ text: gettext('Stop'), disabled: !caps.vms['VM.PowerMgmt'], dangerous: true, tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'), confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid), handler: function() { vm_command("stop", { timeout: 30 }); }, iconCls: 'fa fa-stop' },{ text: gettext('Reset'), disabled: !caps.vms['VM.PowerMgmt'], confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid), handler: function() { vm_command("reset"); }, iconCls: 'fa fa-bolt' }] }, iconCls: 'fa fa-power-off' }); var vm = me.pveSelNode.data; var consoleBtn = Ext.create('PVE.button.ConsoleButton', { disabled: !caps.vms['VM.Console'], hidden: template, consoleType: 'kvm', consoleName: vm.name, nodename: nodename, vmid: vmid }); var statusTxt = Ext.create('Ext.toolbar.TextItem', { data: { lock: undefined }, tpl: [ '
' + gettext('Note: Rollback stops CT') + '
'; return msg; }, handler: function(btn, event) { var rec = sm.getSelection()[0]; if (!rec) { return; } var snapname = rec.data.name; Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot/' + snapname + '/rollback', method: 'POST', waitMsgTarget: me, 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(); } }); } }); var removeBtn = new Proxmox.button.Button({ text: gettext('Remove'), disabled: true, selModel: sm, confirmMsg: function(rec) { var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), "'" + rec.data.name + "'"); return msg; }, enableFn: valid_snapshot, handler: function(btn, event) { var rec = sm.getSelection()[0]; if (!rec) { return; } var snapname = rec.data.name; Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot/' + snapname, method: 'DELETE', waitMsgTarget: me, 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(); } }); } }); var snapshotBtn = Ext.create('Ext.Button', { itemId: 'snapshotBtn', text: gettext('Take Snapshot'), disabled: true, handler: function() { var win = Ext.create('PVE.window.LxcSnapshot', { nodename: me.nodename, vmid: me.vmid }); win.show(); } }); Ext.apply(me, { layout: 'fit', rootVisible: false, animate: false, sortableColumns: false, selModel: sm, tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ], fields: [ 'name', 'description', 'snapstate', 'vmstate', 'running', { name: 'snaptime', type: 'date', dateFormat: 'timestamp' } ], columns: [ { xtype: 'treecolumn', text: gettext('Name'), dataIndex: 'name', width: 200, renderer: function(value, metaData, record) { if (value === 'current') { return "NOW"; } else { return value; } } }, // { // text: gettext('RAM'), // align: 'center', // resizable: false, // dataIndex: 'vmstate', // width: 50, // renderer: function(value, metaData, record) { // if (record.data.name !== 'current') { // return Proxmox.Utils.format_boolean(value); // } // } // }, { text: gettext('Date') + "/" + gettext("Status"), dataIndex: 'snaptime', resizable: false, width: 150, renderer: function(value, metaData, record) { if (record.data.snapstate) { return record.data.snapstate; } if (value) { return Ext.Date.format(value,'Y-m-d H:i:s'); } } }, { 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); } } } ], columnLines: true, listeners: { activate: me.reload, destroy: me.load_task.cancel, itemdblclick: run_editor } }); me.callParent(); me.store.sorters.add(new Ext.util.Sorter({ sorterFn: me.sorterFn })); } }); Ext.define('PVE.window.LxcSnapshot', { extend: 'Ext.window.Window', resizable: false, // needed for finding the reference to submitbutton // because we do not have a controller referenceHolder: true, defaultButton: 'submitbutton', defaultFocus: 'field', take_snapshot: function(snapname, descr, vmstate) { var me = this; var params = { snapname: snapname }; if (descr) { params.description = descr; } Proxmox.Utils.API2Request({ params: params, url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot", waitMsgTarget: me, method: 'POST', 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(); me.close(); } }); }, update_snapshot: function(snapname, descr) { var me = this; Proxmox.Utils.API2Request({ params: { description: descr }, url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot/" + snapname + '/config', waitMsgTarget: me, method: 'PUT', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function(response, options) { me.close(); } }); }, initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } if (!me.vmid) { throw "no VM ID specified"; } var summarystore = Ext.create('Ext.data.Store', { model: 'KeyValue', sorters: [ { property : 'key', direction: 'ASC' } ] }); var items = [ { xtype: me.snapname ? 'displayfield' : 'textfield', name: 'snapname', value: me.snapname, fieldLabel: gettext('Name'), vtype: 'ConfigId', allowBlank: false } ]; if (me.snapname) { items.push({ xtype: 'displayfield', name: 'snaptime', renderer: PVE.Utils.render_timestamp_human_readable, fieldLabel: gettext('Timestamp') }); } items.push({ xtype: 'textareafield', grow: true, name: 'description', fieldLabel: gettext('Description') }); if (me.snapname) { items.push({ title: gettext('Settings'), xtype: 'grid', height: 200, store: summarystore, columns: [ {header: gettext('Key'), width: 150, dataIndex: 'key'}, {header: gettext('Value'), flex: 1, dataIndex: 'value'} ] }); } me.formPanel = Ext.create('Ext.form.Panel', { bodyPadding: 10, border: false, fieldDefaults: { labelWidth: 100, anchor: '100%' }, items: items }); var form = me.formPanel.getForm(); var submitBtn; if (me.snapname) { me.title = gettext('Edit') + ': ' + gettext('Snapshot'); submitBtn = Ext.create('Ext.Button', { text: gettext('Update'), handler: function() { if (form.isValid()) { var values = form.getValues(); me.update_snapshot(me.snapname, values.description); } } }); } else { me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot'); submitBtn = Ext.create('Ext.Button', { text: gettext('Take Snapshot'), reference: 'submitbutton', handler: function() { if (form.isValid()) { var values = form.getValues(); me.take_snapshot(values.snapname, values.description); } } }); } Ext.apply(me, { modal: true, width: 450, border: false, layout: 'fit', buttons: [ submitBtn ], items: [ me.formPanel ] }); if (me.snapname) { Ext.apply(me, { width: 620, height: 420 }); } me.callParent(); if (!me.snapname) { return; } // else load data Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot/" + me.snapname + '/config', waitMsgTarget: me, method: 'GET', failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); me.close(); }, success: function(response, options) { var data = response.result.data; var kvarray = []; Ext.Object.each(data, function(key, value) { if (key === 'description' || key === 'snaptime') { return; } kvarray.push({ key: key, value: value }); }); summarystore.suspendEvents(); summarystore.add(kvarray); summarystore.sort(); summarystore.resumeEvents(); summarystore.fireEvent('refresh', summarystore); form.findField('snaptime').setValue(data.snaptime); form.findField('description').setValue(data.description); } }); } }); /*jslint confusion: true */ 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', initComponent : function() { var me = this; Ext.apply(me, { subject: gettext('CPU'), items: Ext.create('PVE.lxc.CPUInputPanel') }); me.callParent(); me.load(); } }); Ext.define('PVE.lxc.CPUInputPanel', { extend: 'Proxmox.panel.InputPanel', alias: 'widget.pveLxcCPUInputPanel', onlineHelp: 'pct_cpu', insideWizard: false, onGetValues: function(values) { var me = this; PVE.Utils.delete_if_default(values, 'cores', '', me.insideWizard); // cpu{limit,unit} aren't in the wizard so create is always false PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0); PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0); 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: 1024, minValue: 8, maxValue: 500000, labelWidth: labelWidth, allowBlank: false } ], initComponent: function() { var me = this; me.column1 = [ { xtype: 'proxmoxintegerfield', name: 'cores', minValue: 1, maxValue: 128, 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.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') + ' (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()) { var 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(); if (!me.disk) { return; } } }); /*jslint confusion: true*/ /* hidden: boolean and string * bind: function and object * disabled: boolean and string */ Ext.define('PVE.lxc.MountPointInputPanel', { extend: 'Proxmox.panel.InputPanel', xtype: 'pveLxcMountPointInputPanel', insideWizard: false, onlineHelp: 'pct_container_storage', 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; values.file = me.down('field[name=file]').getValue(); if (me.unused) { confid = "mp"+values.mpid; } else if (me.isCreate) { values.file = values.hdstorage + ':' + values.disksize; } // delete unnecessary fields delete values.mpid; delete values.hdstorage; delete values.disksize; delete values.diskformat; var res = {}; res[confid] = PVE.Parser.printLxcMountPoint(values); return res; }, setMountPoint: function(mp) { var me = this; var vm = this.getViewModel(); vm.set('mptype', mp.type); me.setValues(mp); }, setVMConfig: function(vmconfig) { var me = this; var vm = me.getViewModel(); me.vmconfig = vmconfig; vm.set('unpriv', vmconfig.unprivileged); vm.notify(); PVE.Utils.forEachMP(function(bus, i) { var name = "mp" + i.toString(); if (!Ext.isDefined(vmconfig[name])) { me.down('field[name=mpid]').setValue(i); return false; } }); }, setNodename: function(nodename) { var me = this; var vm = me.getViewModel(); vm.set('node', nodename); vm.notify(); me.down('#diskstorage').setNodename(nodename); }, controller: { xclass: 'Ext.app.ViewController', control: { 'field[name=mpid]': { change: function(field, value) { field.validate(); } }, '#hdstorage': { change: function(field, newValue) { var me = this; if (!newValue) { return; } var rec = field.store.getById(newValue); if (!rec) { return; } var vm = me.getViewModel(); vm.set('type', rec.data.type); vm.notify(); } } }, init: function(view) { var me = this; var vm = this.getViewModel(); 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); vm.notify(); } }, 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.mp_counts.mps - 1, hidden: true, allowBlank: false, disabled: true, bind: { hidden: '{hasMP}', disabled: '{hasMP}' }, validator: function(value) { var me = this.up('inputpanel'); if (!me.rendered) { return; } if (Ext.isDefined(me.vmconfig["mp"+value])) { return "Mount point is already in use."; } /*jslint confusion: true*/ /* returns a string above */ 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'), bind: { hidden: '{isRoot}', disabled: '{isBindOrRoot}' } } ], 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') } ], 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() { 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.lxc.MountPointInputPanel', { confid: me.confid, nodename: nodename, unused: unused, unprivileged: me.unprivileged, isCreate: me.isCreate }); var 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) { /*jslint confusion: true*/ /*data is defined as array above*/ var value = response.result.data[me.confid]; /*jslint confusion: false*/ var 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.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/" + 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.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.panel.StorageBase', { extend: 'Proxmox.panel.InputPanel', controller: 'storageEdit', type: '', onGetValues: function(values) { var 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() { var me = this; me.column1.unshift({ xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'storage', value: me.storageId || '', fieldLabel: 'ID', vtype: 'StorageId', allowBlank: false }); me.column2.unshift( { xtype: 'pveNodeSelector', name: 'nodes', 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') } ); me.callParent(); } }); Ext.define('PVE.storage.BaseEdit', { extend: 'Proxmox.window.Edit', initComponent : function() { var 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'; } var ipanel = Ext.create(me.paneltype, { type: me.type, isCreate: me.isCreate, storageId: me.storageId }); Ext.apply(me, { subject: PVE.Utils.format_storage_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.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() { /*jslint confusion: true */ 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.Upload', { extend: 'Ext.window.Window', alias: 'widget.pveStorageUpload', resizable: false, modal: true, initComponent : function() { /*jslint confusion: true */ var me = this; var xhr; if (!me.nodename) { throw "no node name specified"; } if (!me.storage) { throw "no storage ID specified"; } var baseurl = "/nodes/" + me.nodename + "/storage/" + me.storage + "/upload"; var pbar = Ext.create('Ext.ProgressBar', { text: 'Ready', hidden: true }); me.formPanel = Ext.create('Ext.form.Panel', { method: 'POST', waitMsgTarget: true, bodyPadding: 10, border: false, width: 300, fieldDefaults: { labelWidth: 100, anchor: '100%' }, items: [ { xtype: 'pveContentTypeSelector', cts: me.contents, fieldLabel: gettext('Content'), name: 'content', value: me.contents[0] || '', allowBlank: false }, { xtype: 'filefield', name: 'filename', buttonText: gettext('Select File...'), allowBlank: false }, pbar ] }); var form = me.formPanel.getForm(); var doStandardSubmit = function() { form.submit({ url: "/api2/htmljs" + baseurl, waitMsg: gettext('Uploading file...'), success: function(f, action) { me.close(); }, failure: function(f, action) { var msg = PVE.Utils.extractFormActionError(action); Ext.Msg.alert(gettext('Error'), msg); } }); }; var updateProgress = function(per, bytes) { var text = (per * 100).toFixed(2) + '%'; if (bytes) { text += " (" + Proxmox.Utils.format_size(bytes) + ')'; } pbar.updateProgress(per, text); }; var abortBtn = Ext.create('Ext.Button', { text: gettext('Abort'), disabled: true, handler: function() { me.close(); } }); var submitBtn = Ext.create('Ext.Button', { text: gettext('Upload'), disabled: true, handler: function(button) { var fd; try { fd = new FormData(); } catch (err) { doStandardSubmit(); return; } button.setDisabled(true); abortBtn.setDisabled(false); var field = form.findField('content'); fd.append("content", field.getValue()); field.setDisabled(true); field = form.findField('filename'); var file = field.fileInputEl.dom; fd.append("filename", file.files[0]); field.setDisabled(true); pbar.setVisible(true); updateProgress(0); xhr = new XMLHttpRequest(); xhr.addEventListener("load", function(e) { if (xhr.status == 200) { me.close(); } else { var msg = gettext('Error') + " " + xhr.status.toString() + ": " + Ext.htmlEncode(xhr.statusText); var result = Ext.decode(xhr.responseText); result.message = msg; var htmlStatus = Proxmox.Utils.extractRequestError(result, true); Ext.Msg.alert(gettext('Error'), htmlStatus, function(btn) { me.close(); }); } }, false); xhr.addEventListener("error", function(e) { var msg = "Error " + e.target.status.toString() + " occurred while receiving the document."; Ext.Msg.alert(gettext('Error'), msg, function(btn) { me.close(); }); }); xhr.upload.addEventListener("progress", function(evt) { if (evt.lengthComputable) { var percentComplete = evt.loaded / evt.total; updateProgress(percentComplete, evt.loaded); } }, false); xhr.open("POST", "/api2/json" + baseurl, true); xhr.send(fd); } }); form.on('validitychange', function(f, valid) { submitBtn.setDisabled(!valid); }); Ext.apply(me, { title: gettext('Upload'), items: me.formPanel, buttons: [ abortBtn, submitBtn ], listeners: { close: function() { if (xhr) { xhr.abort(); } } } }); me.callParent(); } }); Ext.define('PVE.storage.ContentView', { extend: 'Ext.grid.GridPanel', alias: 'widget.pveStorageContentView', stateful: true, stateId: 'grid-storage-content', viewConfig: { trackOver: false, loadMask: false }, features: [ { ftype: 'grouping', groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})' } ], 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 baseurl = "/nodes/" + nodename + "/storage/" + storage + "/content"; var store = Ext.create('Ext.data.Store',{ model: 'pve-storage-content', groupField: 'content', proxy: { type: 'proxmox', url: '/api2/json' + baseurl }, sorters: { property: 'volid', order: 'DESC' } }); var sm = Ext.create('Ext.selection.RowModel', {}); var reload = function() { store.load(); me.statusStore.load(); }; Proxmox.Utils.monStoreErrors(me, store); 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 uploadButton = Ext.create('Proxmox.button.Button', { contents : ['iso','vztmpl'], text: gettext('Upload'), handler: function() { var me = this; var win = Ext.create('PVE.storage.Upload', { nodename: nodename, storage: storage, contents: me.contents }); win.show(); win.on('destroy', reload); } }); var imageRemoveButton; var removeButton = Ext.create('Proxmox.button.StdRemoveButton',{ selModel: sm, enableFn: function(rec) { if (rec && rec.data.content !== 'images') { imageRemoveButton.setVisible(false); removeButton.setVisible(true); return true; } return false; }, callback: function() { reload(); }, baseurl: baseurl + '/' }); imageRemoveButton = Ext.create('Proxmox.button.Button',{ selModel: sm, hidden: true, text: gettext('Remove'), enableFn: function(rec) { if (rec && rec.data.content === 'images') { removeButton.setVisible(false); imageRemoveButton.setVisible(true); return true; } return false; }, handler: function(btn, event, rec) { me = this; var url = baseurl + '/' + rec.data.volid; var vmid = rec.data.vmid; var store = PVE.data.ResourceStore; if (vmid && store.findVMID(vmid)) { var guest_node = store.guestNode(vmid); var 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) { var msg = Ext.String.format( gettext("Cannot remove image, a guest with VMID '{0}' exists!"), vmid); msg += '' + Ext.htmlEncode(msg) + '
'; metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html.replace(/\"/g,'"') + '"'; } } return value; }; var sm = Ext.create('Ext.selection.RowModel', {}); var run_editor = function() { var rec = sm.getSelection()[0]; var sid = rec.data.sid; var regex = /^(\S+):(\S+)$/; var res = regex.exec(sid); if (res[1] !== 'vm' && res[1] !== 'ct') { return; } var guestType = res[1]; var vmid = res[2]; var win = Ext.create('PVE.ha.VMResourceEdit',{ guestType: guestType, vmid: vmid }); win.on('destroy', reload); win.show(); }; var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { selModel: sm, baseurl: '/cluster/ha/resources/', getUrl: function(rec) { var me = this; return me.baseurl + '/' + rec.get('sid'); }, callback: function() { reload(); } }); var edit_btn = new Proxmox.button.Button({ text: gettext('Edit'), disabled: true, selModel: sm, handler: run_editor }); Ext.apply(me, { store: store, selModel: sm, viewConfig: { trackOver: false }, tbar: [ { text: gettext('Add'), disabled: !caps.nodes['Sys.Console'], handler: function() { var win = Ext.create('PVE.ha.VMResourceEdit',{}); win.on('destroy', reload); win.show(); } }, edit_btn, remove_btn ], 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: function(v) { return v || 'started'; }, dataIndex: 'request_state' }, { header: gettext('CRM State'), width: 100, hidden: true, sortable: true, dataIndex: 'crm_state' }, { header: gettext('Max. Restart'), width: 100, sortable: true, renderer: function(v) { return v || '1'; }, dataIndex: 'max_restart' }, { header: gettext('Max. Relocate'), width: 100, sortable: true, renderer: function(v) { return v || '1'; }, dataIndex: 'max_relocate' }, { header: gettext('Group'), width: 200, sortable: true, renderer: function(value, metaData, record) { return render_error('group', value, metaData, record); }, dataIndex: 'group' }, { header: gettext('Description'), flex: 1, renderer: Ext.String.htmlEncode, dataIndex: 'comment' } ], listeners: { beforeselect: function(grid, record, index, eOpts) { if (!caps.nodes['Sys.Console']) { return false; } }, itemdblclick: run_editor } }); me.callParent(); } }, function() { Ext.define('pve-ha-resources', { extend: 'Ext.data.Model', fields: [ 'sid', 'state', 'digest', 'errors', 'group', 'comment', 'max_restart', 'max_relocate', 'type', 'status', 'node', 'crm_state', 'request_state' ], idProperty: 'sid' }); }); Ext.define('PVE.ha.GroupInputPanel', { extend: 'Proxmox.panel.InputPanel', onlineHelp: 'ha_manager_groups', groupId: undefined, onGetValues: function(values) { var me = this; if (me.isCreate) { values.type = 'group'; } return values; }, initComponent : function() { var me = this; var update_nodefield, update_node_selection; var sm = Ext.create('Ext.selection.CheckboxModel', { mode: 'SIMPLE', listeners: { selectionchange: function(model, selected) { update_nodefield(selected); } } }); // use already cached data to avoid an API call var data = PVE.data.ResourceStore.getNodes(); var store = Ext.create('Ext.data.Store', { fields: [ 'node', 'mem', 'cpu', 'priority' ], data: data, 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: PVE.Utils.render_cpu, sortable: true, width: 150, dataIndex: 'cpu' }, { header: '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) { var record = numberfield.getWidgetRecord(); record.set('priority', value); update_nodefield(sm.getSelection()); } } } } ] }); var nodefield = Ext.create('Ext.form.field.Hidden', { name: 'nodes', value: '', listeners: { change: function (nodefield, value) { update_node_selection(value); } }, isValid: function () { var value = nodefield.getValue(); return (value && 0 !== value.length); } }); update_node_selection = function(string) { sm.deselectAll(true); string.split(',').forEach(function (e, idx, array) { var res = e.split(':'); store.each(function(record) { var node = record.get('node'); if (node == res[0]) { sm.select(record, true); record.set('priority', res[1]); record.commit(); } }); }); nodegrid.reconfigure(store); }; update_nodefield = function(selected) { var nodes = ''; var first_iteration = true; Ext.Array.each(selected, function(record) { if (!first_iteration) { nodes += ','; } first_iteration = false; nodes += record.data.node; if (record.data.priority) { nodes += ':' + record.data.priority; } }); // 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.column1 = [ { xtype: me.isCreate ? 'textfield' : 'displayfield', name: 'group', value: me.groupId || '', fieldLabel: 'ID', vtype: 'StorageId', allowBlank: false }, nodefield ]; me.column2 = [ { xtype: 'proxmoxcheckbox', name: 'restricted', uncheckedValue: 0, fieldLabel: 'restricted' }, { xtype: 'proxmoxcheckbox', name: 'nofailback', uncheckedValue: 0, fieldLabel: 'nofailback' } ]; me.columnB = [ { xtype: 'textfield', name: 'comment', fieldLabel: gettext('Comment') }, nodegrid ]; me.callParent(); } }); Ext.define('PVE.ha.GroupEdit', { extend: 'Proxmox.window.Edit', groupId: undefined, initComponent : function() { var me = this; me.isCreate = !me.groupId; if (me.isCreate) { me.url = '/api2/extjs/cluster/ha/groups'; me.method = 'POST'; } else { me.url = '/api2/extjs/cluster/ha/groups/' + me.groupId; me.method = 'PUT'; } var ipanel = Ext.create('PVE.ha.GroupInputPanel', { isCreate: me.isCreate, groupId: me.groupId }); Ext.apply(me, { subject: gettext('HA Group'), items: [ ipanel ] }); me.callParent(); if (!me.isCreate) { me.load({ success: function(response, options) { var values = response.result.data; ipanel.setValues(values); } }); } } }); Ext.define('PVE.ha.GroupsView', { extend: 'Ext.grid.GridPanel', alias: ['widget.pveHAGroupsView'], onlineHelp: 'ha_manager_groups', stateful: true, stateId: 'grid-ha-groups', initComponent : function() { var me = this; var caps = Ext.state.Manager.get('GuiCap'); var store = new Ext.data.Store({ model: 'pve-ha-groups', sorters: { property: 'group', order: 'DESC' } }); var reload = function() { store.load(); }; var sm = Ext.create('Ext.selection.RowModel', {}); var run_editor = function() { var rec = sm.getSelection()[0]; var win = Ext.create('PVE.ha.GroupEdit',{ groupId: rec.data.group }); win.on('destroy', reload); win.show(); }; var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { selModel: sm, baseurl: '/cluster/ha/groups/', callback: function() { reload(); } }); var edit_btn = new Proxmox.button.Button({ text: gettext('Edit'), disabled: true, selModel: sm, handler: run_editor }); Ext.apply(me, { store: store, selModel: sm, viewConfig: { trackOver: false }, tbar: [ { text: gettext('Create'), disabled: !caps.nodes['Sys.Console'], handler: function() { var win = Ext.create('PVE.ha.GroupEdit',{}); win.on('destroy', reload); win.show(); } }, edit_btn, remove_btn ], columns: [ { header: gettext('Group'), width: 150, sortable: true, dataIndex: 'group' }, { header: 'restricted', width: 100, sortable: true, renderer: Proxmox.Utils.format_boolean, dataIndex: 'restricted' }, { header: 'nofailback', width: 100, sortable: true, renderer: Proxmox.Utils.format_boolean, dataIndex: 'nofailback' }, { header: gettext('Nodes'), flex: 1, sortable: false, dataIndex: 'nodes' }, { header: gettext('Comment'), flex: 1, renderer: Ext.String.htmlEncode, dataIndex: 'comment' } ], listeners: { activate: reload, beforeselect: function(grid, record, index, eOpts) { if (!caps.nodes['Sys.Console']) { return false; } }, itemdblclick: run_editor } }); me.callParent(); } }); 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: 'Use watchdog based fencing.' }, columns: [ { header: '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.dc.Summary', { extend: 'Ext.panel.Panel', alias: 'widget.pveDcSummary', scrollable: true, bodyPadding: 5, layout: 'column', defaults: { padding: 5, plugins: 'responsive', responsiveConfig: { 'width < 1900': { columnWidth: 1 }, 'width >= 1900': { columnWidth: 0.5 } } }, 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' }, { title: gettext('Storage'), itemId: 'storage' } ] }, { itemId: 'nodeview', xtype: 'pveDcNodeView', height: 250 }, { title: gettext('Subscriptions'), height: 220, items: [ { itemId: 'subscriptions', xtype: 'pveHealthWidget', userCls: 'pointer', listeners: { element: 'el', click: function() { if (this.component.userCls === 'pointer') { window.open('https://www.proxmox.com/en/proxmox-ve/pricing', '_blank'); } } } } ] } ], 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; var cpu = 0; var maxcpu = 0; var nodes = 0; var memory = 0; var maxmem = 0; var countedStorages = {}; var used = 0; var total = 0; var usableStorages = {}; var storages = sp.get('dash-storages') || ''; storages.split(',').forEach(function(storage){ if (storage !== '') { usableStorages[storage] = true; } }); var qemu = { running: 0, paused: 0, stopped: 0, template: 0 }; var lxc = { running: 0, paused: 0, stopped: 0, template: 0 }; var error = 0; var i; for (i = 0; i < results.length; i++) { var item = results[i]; switch(item.data.type) { case 'node': cpu += (item.data.cpu * item.data.maxcpu); maxcpu += item.data.maxcpu || 0; memory += item.data.mem || 0; maxmem += item.data.maxmem || 0; nodes++; // update grid also var griditem = gridstore.getById(item.data.id); if (griditem) { griditem.set('cpuusage', item.data.cpu); var max = item.data.maxmem || 1; var val = item.data.mem || 0; griditem.set('memoryusage', val/max); griditem.set('uptime', item.data.uptime); griditem.commit(); //else it marks the fields as dirty } break; case 'storage': if (!Ext.Object.isEmpty(usableStorages)) { if (usableStorages[item.data.id] === true) { used += item.data.disk; total += item.data.maxdisk; } break; } if (!countedStorages[item.data.storage] || (item.data.storage === 'local' && !countedStorages[item.data.id])) { used += item.data.disk; total += item.data.maxdisk; countedStorages[item.data.storage === 'local'?item.data.id:item.data.storage] = true; } break; case 'qemu': qemu[item.data.template ? 'template' : item.data.status]++; if (item.data.hastate === 'error') { error++; } break; case 'lxc': lxc[item.data.template ? 'template' : item.data.status]++; if (item.data.hastate === 'error') { error++; } break; default: break; } } var text = Ext.String.format(gettext('of {0} CPU(s)'), maxcpu); cpustat.updateValue((cpu/maxcpu), text); text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(memory), PVE.Utils.render_size(maxmem)); memorystat.updateValue((memory/maxmem), text); text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(used), PVE.Utils.render_size(total)); storagestat.updateValue((used/total), text); gueststatus.updateValues(qemu,lxc,error); me.suspendLayout = false; me.updateLayout(true); }); var dcHealth = me.getComponent('dcHealth'); me.mon(rstore, 'load', dcHealth.updateStatus, dcHealth); var subs = me.down('#subscriptions'); me.mon(rstore, 'load', function(store, records, success) { var i; var level; var curlevel; for (i = 0; i < records.length; i++) { if (records[i].get('type') !== 'node') { continue; } curlevel = records[i].get('level'); if (level === undefined || !curlevel) { level = curlevel; continue; } if (level !== curlevel) { break; } } if (level === '') { subs.setData({ 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 (level !== curlevel) { subs.setData({ 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 { subs.setData({ title: PVE.Utils.render_support_level(level), iconCls: PVE.Utils.get_health_icon('good', true), text: gettext('Your subscription status is valid.') }); subs.setUserCls(''); } }); me.on('destroy', function(){ rstore.stopUpdate(); }); rstore.startUpdate(); } }); 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) { var me = this.up('window'); values.disable = values.enabled ? 0 : 1; delete values.enabled; PVE.Utils.delete_if_default(values, 'rate', '', me.isCreate); PVE.Utils.delete_if_default(values, 'disable', 0, me.isCreate); PVE.Utils.delete_if_default(values, 'schedule', '*/15', me.isCreate); PVE.Utils.delete_if_default(values, 'comment', '', me.isCreate); if (me.isCreate) { values.type = 'local'; var vm = vmid || values.guest; var id = -1; if (me.highestids[vm] !== undefined) { id = me.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) { var vmid = parseInt(match[1],10); var id = parseInt(match[2],10); if (highestids[vmid] < id || highestids[vmid] === undefined) { highestids[vmid] = 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; } }); } } }); /*jslint confusion: true */ /* 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) { var me = this.getView(); var controller = this; var win = Ext.create('PVE.window.ReplicaEdit', { isCreate: true, method: 'POST', pveSelNode: me.pveSelNode }); win.on('destroy', function() { controller.reload(); }); win.show(); }, editJob: function(button,event,rec) { var me = this.getView(); var controller = this; var data = rec.data; var win = Ext.create('PVE.window.ReplicaEdit', { url: '/cluster/replication/' + data.id, method: 'PUT', pveSelNode: me.pveSelNode }); win.on('destroy', function() { controller.reload(); }); win.show(); }, scheduleJobNow: function(button,event,rec) { var me = this.getView(); var controller = this; Proxmox.Utils.API2Request({ url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/schedule_now", method: 'POST', waitMsgTarget: me, callback: function() { controller.reload(); }, failure: function (response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); }, showLog: function(button, event, rec) { var me = this.getView(); var controller = this; var logView = Ext.create('Proxmox.panel.LogView', { border: false, url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/log" }); var win = Ext.create('Ext.window.Window', { items: [ logView ], layout: 'fit', width: 800, height: 400, modal: true, title: gettext("Replication Log") }); var task = { run: function() { logView.requestUpdate(); }, interval: 1000 }; Ext.TaskManager.start(task); win.on('destroy', function() { Ext.TaskManager.stop(task); controller.reload(); }); win.show(); }, reload: function() { var me = this.getView(); me.rstore.load(); }, dblClick: function(grid, record, item) { var me = this; me.editJob(undefined, undefined, record); }, // check for cluster // currently replication is for cluster only, so we disable the whole // component checkPrerequisites: function() { var me = this.getView(); if (PVE.data.ResourceStore.getNodes().length < 2) { me.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 = [ { text: gettext('Enabled'), dataIndex: 'enabled', xtype: 'checkcolumn', sortable: true, disabled: 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 ''; } var icons = []; var 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: PVE.Utils.render_duration }, { text: gettext('Next Sync'), dataIndex: 'next_sync', width: 150, renderer: function(value) { if (!value) { return '-'; } var now = new Date(); var 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.data.ResourceStore.getNodes().length < 2) { 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.dc.Health', { extend: 'Ext.panel.Panel', alias: 'widget.pveDcHealth', title: gettext('Health'), bodyPadding: 10, height: 220, layout: { type: 'hbox', align: 'stretch' }, defaults: { flex: 1, xtype: 'box', style: { 'text-align':'center' } }, nodeList: [], nodeIndex: 0, updateStatus: function(store, records, success) { var me = this; if (!success) { return; } var cluster = { iconCls: PVE.Utils.get_health_icon('good', true), text: gettext("Standalone node - no cluster defined") }; var nodes = { online: 0, offline: 0 }; // by default we have one node var numNodes = 1; var i; for (i = 0; i < records.length; i++) { var item = records[i]; if (item.data.type === 'node') { nodes[item.data.online === 1 ? 'online':'offline']++; } else if(item.data.type === 'cluster') { cluster.text = gettext("Cluster") + ": "; cluster.text += item.data.name + ", "; cluster.text += gettext("Quorate") + ": "; cluster.text += Proxmox.Utils.format_boolean(item.data.quorate); if (item.data.quorate != 1) { cluster.iconCls = PVE.Utils.get_health_icon('critical', true); } numNodes = item.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) { var me = this; var cephstatus = me.getComponent('ceph'); if (!success || records.length < 1) { // if ceph status is already visible // dont stop to update if (cephstatus.isVisible()) { return; } // try all nodes until we either get a successfull 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; } var state = PVE.Utils.render_ceph_health(records[0].data.health || {}); cephstatus.updateHealth(state); cephstatus.setVisible(true); }, listeners: { destroy: function() { var me = this; me.cephstore.stopUpdate(); } }, items: [ { itemId: 'clusterstatus', xtype: 'pveHealthWidget', title: gettext('Status') }, { itemId: 'nodestatus', data: { online: 0, offline: 0 }, tpl: [ '