diff --git a/serverside/jsmod/5.4-3/pvemanagerlib.js b/serverside/jsmod/5.4-3/pvemanagerlib.js
new file mode 100644
index 0000000..e62acde
--- /dev/null
+++ b/serverside/jsmod/5.4-3/pvemanagerlib.js
@@ -0,0 +1,38347 @@
+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': '#23272a', + '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': '#23272a', + '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': '#23272a', + '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: [ + '