From 2ebe6e6235f0a9f3ec899868964ca953b6cc1cd4 Mon Sep 17 00:00:00 2001 From: Weilbyte Date: Sun, 26 May 2019 19:36:32 +0200 Subject: [PATCH] Modified pvemanagerlib.js Modified pvemanagerlib.js for PVE 5.4-3 --- serverside/jsmod/5.4-3/pvemanagerlib.js | 38347 ++++++++++++++++++++++ 1 file changed, 38347 insertions(+) create mode 100644 serverside/jsmod/5.4-3/pvemanagerlib.js 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: [ + '' + ], + + displayTpl: [ + '', + '{value}', + '' + ] + +}); +Ext.define('PVE.form.CephPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCephPoolSelector', + + allowBlank: false, + valueField: 'pool_name', + displayField: 'pool_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/pools' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load({ + callback: function(rec, op, success){ + if (success && rec.length > 0) { + me.select(rec[0]); + } + } + }); + } + +}); +Ext.define('PVE.form.PermPathSelector', { + extend: 'Ext.form.field.ComboBox', + xtype: 'pvePermPathSelector', + + valueField: 'value', + displayField: 'value', + typeAhead: true, + queryMode: 'local', + store: { + type: 'pvePermPath' + } +}); +/* This class defines the "Tasks" tab of the bottom status panel + * Tasks are jobs with a start, end and log output + */ + +Ext.define('PVE.dc.Tasks', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveClusterTasks'], + + initComponent : function() { + var me = this; + + var taskstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-cluster-tasks', + model: 'proxmox-tasks', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/tasks' + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: taskstore, + sortAfterUpdate: true, + appendAtStart: true, + sorters: [ + { + property : 'pid', + direction: 'DESC' + }, + { + property : 'starttime', + direction: 'DESC' + } + ] + + }); + + var run_task_viewer = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: rec.data.upid + }); + win.show(); + }; + + Ext.apply(me, { + store: store, + stateful: false, + + viewConfig: { + trackOver: false, + stripeRows: true, // does not work with getRowClass() + + getRowClass: function(record, index) { + var status = record.get('status'); + + if (status && status != 'OK') { + return "proxmox-invalid-row"; + } + } + }, + sortableColumns: false, + columns: [ + { + header: gettext("Start Time"), + dataIndex: 'starttime', + width: 150, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("End Time"), + dataIndex: 'endtime', + width: 150, + renderer: function(value, metaData, record) { + if (record.data.pid) { + if (record.data.type == "vncproxy" || + record.data.type == "vncshell" || + record.data.type == "spiceproxy") { + metaData.tdCls = "x-grid-row-console"; + } else { + metaData.tdCls = "x-grid-row-loading"; + } + return ""; + } + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 100 + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150 + }, + { + header: gettext("Description"), + dataIndex: 'upid', + flex: 1, + renderer: Proxmox.Utils.render_upid + }, + { + header: gettext("Status"), + dataIndex: 'status', + width: 200, + renderer: function(value, metaData, record) { + if (record.data.pid) { + if (record.data.type != "vncproxy") { + metaData.tdCls = "x-grid-row-loading"; + } + return ""; + } + if (value == 'OK') { + return 'OK'; + } + // metaData.attr = 'style="color:red;"'; + return Proxmox.Utils.errorText + ': ' + value; + } + } + ], + listeners: { + itemdblclick: run_task_viewer, + show: taskstore.startUpdate, + destroy: taskstore.stopUpdate + } + }); + + me.callParent(); + } +}); +/* This class defines the "Cluster log" tab of the bottom status panel + * A log entry is a timestamp associated with an action on a cluster + */ + +Ext.define('PVE.dc.Log', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveClusterLog'], + + initComponent : function() { + var me = this; + + var logstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-cluster-log', + model: 'proxmox-cluster-log', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/log' + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: logstore, + appendAtStart: true + }); + + Ext.apply(me, { + store: store, + stateful: false, + + viewConfig: { + trackOver: false, + stripeRows: true, + + getRowClass: function(record, index) { + var pri = record.get('pri'); + + if (pri && pri <= 3) { + return "proxmox-invalid-row"; + } + } + }, + sortableColumns: false, + columns: [ + { + header: gettext("Time"), + dataIndex: 'time', + width: 150, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 150 + }, + { + header: gettext("Service"), + dataIndex: 'tag', + width: 100 + }, + { + header: "PID", + dataIndex: 'pid', + width: 100 + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150 + }, + { + header: gettext("Severity"), + dataIndex: 'pri', + renderer: PVE.Utils.render_serverity, + width: 100 + }, + { + header: gettext("Message"), + dataIndex: 'msg', + flex: 1 + } + ], + listeners: { + activate: logstore.startUpdate, + deactivate: logstore.stopUpdate, + destroy: logstore.stopUpdate + } + }); + + me.callParent(); + } +}); +/* + * This class describes the bottom panel + */ +Ext.define('PVE.panel.StatusPanel', { + extend: 'Ext.tab.Panel', + alias: 'widget.pveStatusPanel', + + + //title: "Logs", + //tabPosition: 'bottom', + + initComponent: function() { + var me = this; + + var stateid = 'ltab'; + var sp = Ext.state.Manager.getProvider(); + + var state = sp.get(stateid); + if (state && state.value) { + me.activeTab = state.value; + } + + Ext.apply(me, { + listeners: { + tabchange: function() { + var atab = me.getActiveTab().itemId; + var state = { value: atab }; + sp.set(stateid, state); + } + }, + items: [ + { + itemId: 'tasks', + title: gettext('Tasks'), + xtype: 'pveClusterTasks' + }, + { + itemId: 'clog', + title: gettext('Cluster log'), + xtype: 'pveClusterLog' + } + ] + }); + + me.callParent(); + + me.items.get(0).fireEvent('show', me.items.get(0)); + + var statechange = function(sp, key, state) { + if (key === stateid) { + var atab = me.getActiveTab().itemId; + var ntab = state.value; + if (state && ntab && (atab != ntab)) { + me.setActiveTab(ntab); + } + } + }; + + sp.on('statechange', statechange); + me.on('destroy', function() { + sp.un('statechange', statechange); + }); + + } +}); +Ext.define('PVE.panel.StatusView', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveStatusView', + + layout: { + type: 'column' + }, + + title: gettext('Status'), + + getRecordValue: function(key, store) { + if (!key) { + throw "no key given"; + } + var me = this; + + if (store === undefined) { + store = me.getStore(); + } + + var rec = store.getById(key); + if (rec) { + return rec.data.value; + } + + return ''; + }, + + fieldRenderer: function(val,max) { + if (max === undefined) { + return val; + } + + if (!Ext.isNumeric(max) || max === 1) { + return PVE.Utils.render_usage(val); + } + return PVE.Utils.render_size_usage(val,max); + }, + + fieldCalculator: function(used, max) { + if (!Ext.isNumeric(max) && Ext.isNumeric(used)) { + return used; + } else if(!Ext.isNumeric(used)) { + /* we come here if the field is from a node + * where the records are not mem and maxmem + * but mem.used and mem.total + */ + if (used.used !== undefined && + used.total !== undefined) { + return used.used/used.total; + } + } + + return used/max; + }, + + updateField: function(field) { + var me = this; + var text = ''; + var renderer = me.fieldRenderer; + if (Ext.isFunction(field.renderer)) { + renderer = field.renderer; + } + if (field.multiField === true) { + field.updateValue(renderer.call(field, me.getStore().getRecord())); + } else if (field.textField !== undefined) { + field.updateValue(renderer.call(field, me.getRecordValue(field.textField))); + } else if(field.valueField !== undefined) { + var used = me.getRecordValue(field.valueField); + /*jslint confusion: true*/ + /* string and int */ + var max = field.maxField !== undefined ? me.getRecordValue(field.maxField) : 1; + + var calculate = me.fieldCalculator; + + if (Ext.isFunction(field.calculate)) { + calculate = field.calculate; + } + field.updateValue(renderer.call(field, used,max), calculate(used,max)); + } + }, + + getStore: function() { + var me = this; + if (!me.rstore) { + throw "there is no rstore"; + } + + return me.rstore; + }, + + updateTitle: function() { + var me = this; + me.setTitle(me.getRecordValue('name')); + }, + + updateValues: function(store, records, success) { + if (!success) { + return; // do not update if store load was not successful + } + var me = this; + var itemsToUpdate = me.query('pveInfoWidget'); + + itemsToUpdate.forEach(me.updateField, me); + + me.updateTitle(store); + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw "no rstore given"; + } + + if (!me.title) { + throw "no title given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + me.callParent(); + + me.mon(me.rstore, 'load', 'updateValues'); + } + +}); +Ext.define('PVE.panel.GuestStatusView', { + extend: 'PVE.panel.StatusView', + alias: 'widget.pveGuestStatusView', + mixins: ['Proxmox.Mixin.CBind'], + + height: 300, + + cbindData: function (initialConfig) { + var me = this; + return { + isQemu: me.pveSelNode.data.type === 'qemu', + isLxc: me.pveSelNode.data.type === 'lxc' + }; + }, + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + padding: '2 25' + }, + items: [ + { + xtype: 'box', + height: 20 + }, + { + itemId: 'status', + title: gettext('Status'), + iconCls: 'fa fa-info fa-fw', + printBar: false, + multiField: true, + renderer: function(record) { + var me = this; + var text = record.data.status; + var qmpstatus = record.data.qmpstatus; + if (qmpstatus && qmpstatus !== record.data.status) { + text += ' (' + qmpstatus + ')'; + } + return text; + } + }, + { + itemId: 'hamanaged', + iconCls: 'fa fa-heartbeat fa-fw', + title: gettext('HA State'), + printBar: false, + textField: 'ha', + renderer: PVE.Utils.format_ha + }, + { + xtype: 'pveInfoWidget', + itemId: 'node', + iconCls: 'fa fa-building fa-fw', + title: gettext('Node'), + cbind: { + text: '{pveSelNode.data.node}' + }, + printBar: false + }, + { + xtype: 'box', + height: 15 + }, + { + itemId: 'cpu', + iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', + title: gettext('CPU usage'), + valueField: 'cpu', + maxField: 'cpus', + renderer: PVE.Utils.render_cpu_usage, + // in this specific api call + // we already have the correct value for the usage + calculate: Ext.identityFn + }, + { + itemId: 'memory', + iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', + title: gettext('Memory usage'), + valueField: 'mem', + maxField: 'maxmem' + }, + { + itemId: 'swap', + xtype: 'pveInfoWidget', + iconCls: 'fa fa-refresh fa-fw', + title: gettext('SWAP usage'), + valueField: 'swap', + maxField: 'maxswap', + cbind: { + hidden: '{isQemu}', + disabled: '{isQemu}' + } + }, + { + itemId: 'rootfs', + iconCls: 'fa fa-hdd-o fa-fw', + title: gettext('Bootdisk size'), + valueField: 'disk', + maxField: 'maxdisk', + printBar: false, + renderer: function(used, max) { + var me = this; + me.setPrintBar(used > 0); + if (used === 0) { + return PVE.Utils.render_size(max); + } else { + return PVE.Utils.render_size_usage(used,max); + } + } + }, + { + xtype: 'box', + height: 15 + }, + { + itemId: 'ips', + xtype: 'pveAgentIPView', + cbind: { + rstore: '{rstore}', + pveSelNode: '{pveSelNode}', + hidden: '{isLxc}', + disabled: '{isLxc}' + } + } + ], + + updateTitle: function() { + var me = this; + var uptime = me.getRecordValue('uptime'); + + var text = ""; + if (Number(uptime) > 0) { + text = " (" + gettext('Uptime') + ': ' + Proxmox.Utils.format_duration_long(uptime) + + ')'; + } + + me.setTitle(me.getRecordValue('name') + text); + } +}); +/* + * This is a running chart widget + * you add time datapoints to it, + * and we only show the last x of it + * used for ceph performance charts + */ +Ext.define('PVE.widget.RunningChart', { + extend: 'Ext.container.Container', + alias: 'widget.pveRunningChart', + + layout: { + type: 'hbox', + align: 'center' + }, + items: [ + { + width: 80, + xtype: 'box', + itemId: 'title', + data: { + title: '' + }, + tpl: '

{title}:

' + }, + { + flex: 1, + xtype: 'cartesian', + height: '100%', + itemId: 'chart', + border: false, + axes: [ + { + type: 'numeric', + position: 'left', + hidden: true, + minimum: 0 + }, + { + type: 'numeric', + position: 'bottom', + hidden: true + } + ], + + store: { + data: {} + }, + + sprites: [{ + id: 'valueSprite', + type: 'text', + text: '0 B/s', + textAlign: 'end', + textBaseline: 'middle', + fontSize: 14 + }], + + series: [{ + type: 'line', + xField: 'time', + yField: 'val', + fill: 'true', + colors: ['#cfcfcf'], + tooltip: { + trackMouse: true, + renderer: function( tooltip, record, ctx) { + var me = this.getChart(); + var date = new Date(record.data.time); + var value = me.up().renderer(record.data.val); + tooltip.setHtml( + me.up().title + ': ' + value + '
' + + Ext.Date.format(date, 'H:i:s') + ); + } + }, + style: { + lineWidth: 1.5, + opacity: 0.60 + }, + marker: { + opacity: 0, + scaling: 0.01, + fx: { + duration: 200, + easing: 'easeOut' + } + }, + highlightCfg: { + opacity: 1, + scaling: 1.5 + } + }] + } + ], + + // the renderer for the tooltip and last value, + // default just the value + renderer: Ext.identityFn, + + // show the last x seconds + // default is 5 minutes + timeFrame: 5*60, + + addDataPoint: function(value, time) { + var me = this.chart; + var panel = me.up(); + var now = new Date(); + var begin = new Date(now.getTime() - (1000*panel.timeFrame)); + + me.store.add({ + time: time || now.getTime(), + val: value || 0 + }); + + // delete all old records when we have 20 times more datapoints + // than seconds in our timeframe (so even a subsecond graph does + // not trigger this often) + // + // records in the store do not take much space, but like this, + // we prevent a memory leak when someone has the site open for a long time + // with minimal graphical glitches + if (me.store.count() > panel.timeFrame * 20) { + var oldData = me.store.getData().createFiltered(function(item) { + return item.data.time < begin.getTime(); + }); + + me.store.remove(oldData.getRange()); + } + + me.timeaxis.setMinimum(begin.getTime()); + me.timeaxis.setMaximum(now.getTime()); + me.valuesprite.setText(panel.renderer(value || 0).toString()); + me.valuesprite.setAttributes({ + x: me.getWidth() - 15, + y: me.getHeight()/2 + }, true); + me.redraw(); + }, + + setTitle: function(title) { + this.title = title; + var me = this.getComponent('title'); + me.update({title: title}); + }, + + initComponent: function(){ + var me = this; + me.callParent(); + + if (me.title) { + me.getComponent('title').update({title: me.title}); + } + me.chart = me.getComponent('chart'); + me.chart.timeaxis = me.chart.getAxes()[1]; + me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite'); + if (me.color) { + me.chart.series[0].setStyle({ + fill: me.color, + stroke: me.color + }); + } + } +}); +Ext.define('PVE.widget.Info',{ + extend: 'Ext.container.Container', + alias: 'widget.pveInfoWidget', + + layout: { + type: 'vbox', + align: 'stretch' + }, + + value: 0, + maximum: 1, + printBar: true, + items: [ + { + xtype: 'component', + itemId: 'label', + data: { + title: '', + usage: '', + iconCls: undefined + }, + tpl: [ + '
', + '', + ' ', + '', + '{title}
 
{usage}
' + ] + }, + { + height: 2, + border: 0 + }, + { + xtype: 'progressbar', + itemId: 'progress', + height: 5, + value: 0, + animate: true + } + ], + + warningThreshold: 0.6, + criticalThreshold: 0.9, + + setPrintBar: function(enable) { + var me = this; + me.printBar = enable; + me.getComponent('progress').setVisible(enable); + }, + + setIconCls: function(iconCls) { + var me = this; + me.getComponent('label').data.iconCls = iconCls; + }, + + updateValue: function(text, usage) { + var me = this; + var label = me.getComponent('label'); + label.update(Ext.apply(label.data, {title: me.title, usage:text})); + + if (usage !== undefined && + me.printBar && + Ext.isNumeric(usage) && + usage >= 0) { + var progressBar = me.getComponent('progress'); + progressBar.updateProgress(usage, ''); + if (usage > me.criticalThreshold) { + progressBar.removeCls('warning'); + progressBar.addCls('critical'); + } else if (usage > me.warningThreshold) { + progressBar.removeCls('critical'); + progressBar.addCls('warning'); + } else { + progressBar.removeCls('warning'); + progressBar.removeCls('critical'); + } + } + }, + + initComponent: function() { + var me = this; + + if (!me.title) { + throw "no title defined"; + } + + me.callParent(); + + me.getComponent('progress').setVisible(me.printBar); + + me.updateValue(me.text, me.value); + me.setIconCls(me.iconCls); + } + +}); +Ext.define('PVE.panel.TemplateStatusView',{ + extend: 'PVE.panel.StatusView', + alias: 'widget.pveTemplateStatusView', + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + printBar: false, + padding: '2 25' + }, + items: [ + { + xtype: 'box', + height: 20 + }, + { + itemId: 'hamanaged', + iconCls: 'fa fa-heartbeat fa-fw', + title: gettext('HA State'), + printBar: false, + textField: 'ha', + renderer: PVE.Utils.format_ha + }, + { + itemId: 'node', + iconCls: 'fa fa-fw fa-building', + title: gettext('Node') + }, + { + xtype: 'box', + height: 20 + }, + { + itemId: 'cpus', + iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', + title: gettext('Processors'), + textField: 'cpus' + }, + { + itemId: 'memory', + iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', + title: gettext('Memory'), + textField: 'maxmem', + renderer: PVE.Utils.render_size + }, + { + itemId: 'swap', + iconCls: 'fa fa-refresh fa-fw', + title: gettext('Swap'), + textField: 'maxswap', + renderer: PVE.Utils.render_size + }, + { + itemId: 'disk', + iconCls: 'fa fa-hdd-o fa-fw', + title: gettext('Bootdisk size'), + textField: 'maxdisk', + renderer: PVE.Utils.render_size + }, + { + xtype: 'box', + height: 20 + } + ], + + initComponent: function() { + var me = this; + + var name = me.pveSelNode.data.name; + if (!name) { + throw "no name specified"; + } + + me.title = name; + + me.callParent(); + if (me.pveSelNode.data.type !== 'lxc') { + me.remove(me.getComponent('swap')); + } + me.getComponent('node').updateValue(me.pveSelNode.data.node); + } +}); +Ext.define('PVE.widget.HealthWidget', { + extend: 'Ext.Component', + alias: 'widget.pveHealthWidget', + + data: { + iconCls: PVE.Utils.get_health_icon(undefined, true), + text: '', + title: '' + }, + + style: { + 'text-align':'center' + }, + + tpl: [ + '

{title}

', + '', + '

', + '{text}' + ], + + updateHealth: function(data) { + var me = this; + me.update(Ext.apply(me.data, data)); + }, + + initComponent: function(){ + var me = this; + + if (me.title) { + me.config.data.title = me.title; + } + + me.callParent(); + } + +}); +/*global u2f*/ +Ext.define('PVE.window.LoginWindow', { + extend: 'Ext.window.Window', + + controller: { + + xclass: 'Ext.app.ViewController', + + onLogon: function() { + var me = this; + + var form = this.lookupReference('loginForm'); + var unField = this.lookupReference('usernameField'); + var saveunField = this.lookupReference('saveunField'); + var view = this.getView(); + + if (!form.isValid()) { + return; + } + + view.el.mask(gettext('Please wait...'), 'x-mask-loading'); + + // set or clear username + var sp = Ext.state.Manager.getProvider(); + if (saveunField.getValue() === true) { + sp.set(unField.getStateId(), unField.getValue()); + } else { + sp.clear(unField.getStateId()); + } + sp.set(saveunField.getStateId(), saveunField.getValue()); + + form.submit({ + failure: function(f, resp){ + me.failure(resp); + }, + success: function(f, resp){ + view.el.unmask(); + + var data = resp.result.data; + if (Ext.isDefined(data.NeedTFA)) { + // Store first factor login information first: + data.LoggedOut = true; + Proxmox.Utils.setAuthData(data); + + if (Ext.isDefined(data.U2FChallenge)) { + me.perform_u2f(data); + } else { + me.perform_otp(); + } + } else { + me.success(data); + } + } + }); + + }, + failure: function(resp) { + var me = this; + var view = me.getView(); + view.el.unmask(); + var handler = function() { + var uf = me.lookupReference('usernameField'); + uf.focus(true, true); + }; + + Ext.MessageBox.alert(gettext('Error'), + gettext("Login failed. Please try again"), + handler); + }, + success: function(data) { + var me = this; + var view = me.getView(); + var handler = view.handler || Ext.emptyFn; + handler.call(me, data); + view.close(); + }, + + perform_otp: function() { + var me = this; + var win = Ext.create('PVE.window.TFALoginWindow', { + onLogin: function(value) { + me.finish_tfa(value); + }, + onCancel: function() { + Proxmox.LoggedOut = false; + Proxmox.Utils.authClear(); + me.getView().show(); + } + }); + win.show(); + }, + + perform_u2f: function(data) { + var me = this; + // Show the message: + var msg = Ext.Msg.show({ + title: 'U2F: '+gettext('Verification'), + message: gettext('Please press the button on your U2F Device'), + buttons: [] + }); + var chlg = data.U2FChallenge; + var key = { + version: chlg.version, + keyHandle: chlg.keyHandle + }; + u2f.sign(chlg.appId, chlg.challenge, [key], function(res) { + msg.close(); + if (res.errorCode) { + Proxmox.Utils.authClear(); + Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode)); + return; + } + delete res.errorCode; + me.finish_tfa(JSON.stringify(res)); + }); + }, + finish_tfa: function(res) { + var me = this; + var view = me.getView(); + view.el.mask(gettext('Please wait...'), 'x-mask-loading'); + var params = { response: res }; + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'POST', + timeout: 5000, // it'll delay both success & failure + success: function(resp, opts) { + view.el.unmask(); + // Fill in what we copy over from the 1st factor: + var data = resp.result.data; + data.CSRFPreventionToken = Proxmox.CSRFPreventionToken; + data.username = Proxmox.UserName; + // Finish logging in: + me.success(data); + }, + failure: function(resp, opts) { + Proxmox.Utils.authClear(); + me.failure(resp); + } + }); + }, + + control: { + 'field[name=username]': { + specialkey: function(f, e) { + if (e.getKey() === e.ENTER) { + var pf = this.lookupReference('passwordField'); + if (!pf.getValue()) { + pf.focus(false); + } + } + } + }, + 'field[name=lang]': { + change: function(f, value) { + var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10); + Ext.util.Cookies.set('PVELangCookie', value, dt); + this.getView().mask(gettext('Please wait...'), 'x-mask-loading'); + window.location.reload(); + } + }, + 'button[reference=loginButton]': { + click: 'onLogon' + }, + '#': { + show: function() { + var sp = Ext.state.Manager.getProvider(); + var checkboxField = this.lookupReference('saveunField'); + var unField = this.lookupReference('usernameField'); + + var checked = sp.get(checkboxField.getStateId()); + checkboxField.setValue(checked); + + if(checked === true) { + var username = sp.get(unField.getStateId()); + unField.setValue(username); + var pwField = this.lookupReference('passwordField'); + pwField.focus(); + } + } + } + } + }, + + width: 400, + + modal: true, + + border: false, + + draggable: true, + + closable: false, + + resizable: false, + + layout: 'auto', + + title: gettext('Proxmox VE Login'), + + defaultFocus: 'usernameField', + + defaultButton: 'loginButton', + + items: [{ + xtype: 'form', + layout: 'form', + url: '/api2/extjs/access/ticket', + reference: 'loginForm', + + fieldDefaults: { + labelAlign: 'right', + allowBlank: false + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('User name'), + name: 'username', + itemId: 'usernameField', + reference: 'usernameField', + stateId: 'login-username' + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + name: 'password', + reference: 'passwordField' + }, + { + xtype: 'pveRealmComboBox', + name: 'realm' + }, + { + xtype: 'proxmoxLanguageSelector', + fieldLabel: gettext('Language'), + value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en', + name: 'lang', + reference: 'langField', + submitValue: false + } + ], + buttons: [ + { + xtype: 'checkbox', + fieldLabel: gettext('Save User name'), + name: 'saveusername', + reference: 'saveunField', + stateId: 'login-saveusername', + labelWidth: 'auto', + labelAlign: 'right', + submitValue: false + }, + { + text: gettext('Login'), + reference: 'loginButton' + } + ] + }] + }); +Ext.define('PVE.window.TFALoginWindow', { + extend: 'Ext.window.Window', + + modal: true, + resizable: false, + title: 'Two-Factor Authentication', + layout: 'form', + defaultButton: 'loginButton', + defaultFocus: 'otpField', + + controller: { + xclass: 'Ext.app.ViewController', + login: function() { + var me = this; + var view = me.getView(); + view.onLogin(me.lookup('otpField').value); + view.close(); + }, + cancel: function() { + var me = this; + var view = me.getView(); + view.onCancel(); + view.close(); + } + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Please enter your OTP verification code:'), + name: 'otp', + itemId: 'otpField', + reference: 'otpField', + allowBlank: false + } + ], + + buttons: [ + { + text: gettext('Login'), + reference: 'loginButton', + handler: 'login' + }, + { + text: gettext('Cancel'), + handler: 'cancel' + } + ] +}); +Ext.define('PVE.window.Wizard', { + extend: 'Ext.window.Window', + + activeTitle: '', // used for automated testing + + width: 700, + height: 510, + + modal: true, + border: false, + + draggable: true, + closable: true, + resizable: false, + + layout: 'border', + + getValues: function(dirtyOnly) { + var me = this; + + var values = {}; + + var form = me.down('form').getForm(); + + form.getFields().each(function(field) { + if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); + }); + + return values; + }, + + initComponent: function() { + var me = this; + + var tabs = me.items || []; + delete me.items; + + /* + * Items may have the following functions: + * validator(): per tab custom validation + * onSubmit(): submit handler + * onGetValues(): overwrite getValues results + */ + + Ext.Array.each(tabs, function(tab) { + tab.disabled = true; + }); + tabs[0].disabled = false; + + var maxidx = 0; + var curidx = 0; + + var check_card = function(card) { + var valid = true; + var fields = card.query('field, fieldcontainer'); + if (card.isXType('fieldcontainer')) { + fields.unshift(card); + } + Ext.Array.each(fields, function(field) { + // Note: not all fielcontainer have isValid() + if (Ext.isFunction(field.isValid) && !field.isValid()) { + valid = false; + } + }); + + if (Ext.isFunction(card.validator)) { + return card.validator(); + } + + return valid; + }; + + var disable_at = function(card) { + var tp = me.down('#wizcontent'); + var idx = tp.items.indexOf(card); + for(;idx < tp.items.getCount();idx++) { + var nc = tp.items.getAt(idx); + if (nc) { + nc.disable(); + } + } + }; + + var tabchange = function(tp, newcard, oldcard) { + if (newcard.onSubmit) { + me.down('#next').setVisible(false); + me.down('#submit').setVisible(true); + } else { + me.down('#next').setVisible(true); + me.down('#submit').setVisible(false); + } + var valid = check_card(newcard); + me.down('#next').setDisabled(!valid); + me.down('#submit').setDisabled(!valid); + me.down('#back').setDisabled(tp.items.indexOf(newcard) == 0); + + var idx = tp.items.indexOf(newcard); + if (idx > maxidx) { + maxidx = idx; + } + curidx = idx; + + var next = idx + 1; + var ntab = tp.items.getAt(next); + if (valid && ntab && !newcard.onSubmit) { + ntab.enable(); + } + }; + + if (me.subject && !me.title) { + me.title = Proxmox.Utils.dialog_title(me.subject, true, false); + } + + var sp = Ext.state.Manager.getProvider(); + var advchecked = sp.get('proxmox-advanced-cb'); + + Ext.apply(me, { + items: [ + { + xtype: 'form', + region: 'center', + layout: 'fit', + border: false, + margins: '5 5 0 5', + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [{ + itemId: 'wizcontent', + xtype: 'tabpanel', + activeItem: 0, + bodyPadding: 10, + listeners: { + afterrender: function(tp) { + var atab = this.getActiveTab(); + tabchange(tp, atab); + }, + tabchange: function(tp, newcard, oldcard) { + tabchange(tp, newcard, oldcard); + } + }, + items: tabs + }] + } + ], + fbar: [ + { + xtype: 'proxmoxHelpButton', + itemId: 'help' + }, + '->', + { + xtype: 'proxmoxcheckbox', + boxLabelAlign: 'before', + boxLabel: gettext('Advanced'), + value: advchecked, + listeners: { + change: function(cb, val) { + var tp = me.down('#wizcontent'); + tp.query('inputpanel').forEach(function(ip) { + ip.setAdvancedVisible(val); + }); + + sp.set('proxmox-advanced-cb', val); + } + } + }, + { + text: gettext('Back'), + disabled: true, + itemId: 'back', + minWidth: 60, + handler: function() { + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + var prev = tp.items.indexOf(atab) - 1; + if (prev < 0) { + return; + } + var ntab = tp.items.getAt(prev); + if (ntab) { + tp.setActiveTab(ntab); + } + } + }, + { + text: gettext('Next'), + disabled: true, + itemId: 'next', + minWidth: 60, + handler: function() { + + var form = me.down('form').getForm(); + + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + if (!check_card(atab)) { + return; + } + + var next = tp.items.indexOf(atab) + 1; + var ntab = tp.items.getAt(next); + if (ntab) { + ntab.enable(); + tp.setActiveTab(ntab); + } + + } + }, + { + text: gettext('Finish'), + minWidth: 60, + hidden: true, + itemId: 'submit', + handler: function() { + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + atab.onSubmit(); + } + } + ] + }); + me.callParent(); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + panel.setAdvancedVisible(advchecked); + }); + + Ext.Array.each(me.query('field'), function(field) { + var validcheck = function() { + var tp = me.down('#wizcontent'); + + // check tabs from current to the last enabled for validity + // since we might have changed a validity on a later one + var i; + for (i = curidx; i <= maxidx && i < tp.items.getCount(); i++) { + var tab = tp.items.getAt(i); + var valid = check_card(tab); + + // only set the buttons on the current panel + if (i === curidx) { + me.down('#next').setDisabled(!valid); + me.down('#submit').setDisabled(!valid); + } + + // if a panel is invalid, then disable it and all following, + // else enable it and go to the next + var ntab = tp.items.getAt(i + 1); + if (!valid) { + disable_at(ntab); + return; + } else if (ntab && !tab.onSubmit) { + ntab.enable(); + } + } + }; + field.on('change', validcheck); + field.on('validitychange', validcheck); + }); + } +}); +Ext.define('PVE.window.NotesEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.apply(me, { + title: gettext('Notes'), + width: 600, + height: '400px', + resizable: true, + layout: 'fit', + defaultButton: undefined, + items: { + xtype: 'textarea', + name: 'description', + height: '100%', + value: '', + hideLabel: true + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.window.Backup', { + extend: 'Ext.window.Window', + + resizable: false, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.vmtype) { + throw "no VM type specified"; + } + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: me.nodename, + name: 'storage', + value: me.storage, + fieldLabel: gettext('Storage'), + storageContent: 'backup', + allowBlank: false + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + storagesel, + { + xtype: 'pveBackupModeSelector', + fieldLabel: gettext('Mode'), + value: 'snapshot', + name: 'mode' + }, + { + xtype: 'pveCompressionSelector', + name: 'compress', + value: 'lzo', + fieldLabel: gettext('Compression') + }, + { + xtype: 'textfield', + fieldLabel: gettext('Send email to'), + name: 'mailto', + emptyText: Proxmox.Utils.noneText + } + ] + }); + + var form = me.formPanel.getForm(); + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Backup'), + handler: function(){ + var storage = storagesel.getValue(); + var values = form.getValues(); + var params = { + storage: storage, + vmid: me.vmid, + mode: values.mode, + remove: 0 + }; + + if ( values.mailto ) { + params.mailto = values.mailto; + } + + if (values.compress) { + params.compress = values.compress; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/vzdump', + params: params, + method: 'POST', + failure: function (response, opts) { + Ext.Msg.alert('Error',response.htmlStatus); + }, + success: function(response, options) { + // close later so we reload the grid + // after the task has completed + me.hide(); + + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + listeners: { + close: function() { + me.close(); + } + } + }); + win.show(); + } + }); + } + }); + + var helpBtn = Ext.create('Proxmox.button.Help', { + onlineHelp: 'chapter_vzdump', + listenToGlobalEvent: false, + hidden: false + }); + + var title = gettext('Backup') + " " + + ((me.vmtype === 'lxc') ? "CT" : "VM") + + " " + me.vmid; + + Ext.apply(me, { + title: title, + width: 350, + modal: true, + layout: 'auto', + border: false, + items: [ me.formPanel ], + buttons: [ helpBtn, '->', submitBtn ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.window.Restore', { + extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit? + + resizable: false, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.volid) { + throw "no volume ID specified"; + } + + if (!me.vmtype) { + throw "no vmtype specified"; + } + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: me.nodename, + name: 'storage', + value: '', + fieldLabel: gettext('Storage'), + storageContent: (me.vmtype === 'lxc') ? 'rootdir' : 'images', + allowBlank: true + }); + + var IDfield; + if (me.vmid) { + IDfield = Ext.create('Ext.form.field.Display', { + name: 'vmid', + value: me.vmid, + fieldLabel: (me.vmtype === 'lxc') ? 'CT' : 'VM' + }); + } else { + IDfield = Ext.create('PVE.form.GuestIDSelector', { + name: 'vmid', + guestType: me.vmtype, + loadNextFreeID: true, + validateExists: false + }); + } + + var items = [ + { + xtype: 'displayfield', + value: me.volidText || me.volid, + fieldLabel: gettext('Source') + }, + storagesel, + IDfield, + { + xtype: 'proxmoxintegerfield', + name: 'bwlimit', + fieldLabel: gettext('Read Limit (MiB/s)'), + minValue: 0, + emptyText: gettext('Defaults to target storage restore limit'), + autoEl: { + tag: 'div', + 'data-qtip': gettext("Use '0' to disable all bandwidth limits.") + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'unique', + fieldLabel: gettext('Unique'), + hidden: !!me.vmid, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses') + }, + checked: false + } + ]; + + /*jslint confusion: true*/ + if (me.vmtype === 'lxc') { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'unprivileged', + value: true, + fieldLabel: gettext('Unprivileged container') + }); + } + /*jslint confusion: 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 doRestore = function(url, params) { + Proxmox.Utils.API2Request({ + url: url, + params: params, + method: 'POST', + waitMsgTarget: me, + 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.TaskViewer', { + upid: upid + }); + win.show(); + me.close(); + } + }); + }; + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Restore'), + handler: function(){ + var storage = storagesel.getValue(); + var values = form.getValues(); + + var params = { + storage: storage, + vmid: me.vmid || values.vmid, + force: me.vmid ? 1 : 0 + }; + if (values.unique) { params.unique = 1; } + + if (values.bwlimit !== undefined) { + params.bwlimit = values.bwlimit * 1024; + } + + var url; + var msg; + if (me.vmtype === 'lxc') { + url = '/nodes/' + me.nodename + '/lxc'; + params.ostemplate = me.volid; + params.restore = 1; + if (values.unprivileged) { params.unprivileged = 1; } + msg = Proxmox.Utils.format_task_description('vzrestore', params.vmid); + } else if (me.vmtype === 'qemu') { + url = '/nodes/' + me.nodename + '/qemu'; + params.archive = me.volid; + msg = Proxmox.Utils.format_task_description('qmrestore', params.vmid); + } else { + throw 'unknown VM type'; + } + + if (me.vmid) { + msg += '. ' + gettext('This will permanently erase current VM data.'); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + doRestore(url, params); + }); + } else { + doRestore(url, params); + } + } + }); + + form.on('validitychange', function(f, valid) { + submitBtn.setDisabled(!valid); + }); + + var title = gettext('Restore') + ": " + ( + (me.vmtype === 'lxc') ? 'CT' : 'VM'); + + if (me.vmid) { + title += " " + me.vmid; + } + + Ext.apply(me, { + title: title, + width: 500, + modal: true, + layout: 'auto', + border: false, + items: [ me.formPanel ], + buttons: [ submitBtn ] + }); + + me.callParent(); + } +}); +/* Popup a message window + * where the user has to manually enter the ressource ID + * to enable the destroy button + */ +Ext.define('PVE.window.SafeDestroy', { + extend: 'Ext.window.Window', + alias: 'widget.pveSafeDestroy', + + title: gettext('Confirm'), + modal: true, + buttonAlign: 'center', + bodyPadding: 10, + width: 450, + layout: { type:'hbox' }, + defaultFocus: 'confirmField', + showProgress: false, + + config: { + item: { + id: undefined, + type: undefined + }, + url: undefined, + params: {} + }, + + getParams: function() { + var me = this; + if (Ext.Object.isEmpty(me.params)) { + return ''; + } + return '?' + Ext.Object.toQueryString(me.params); + }, + + controller: { + + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=confirm]': { + change: function(f, value) { + var view = this.getView(); + var removeButton = this.lookupReference('removeButton'); + if (value === view.getItem().id.toString()) { + removeButton.enable(); + } else { + removeButton.disable(); + } + }, + specialkey: function (field, event) { + var removeButton = this.lookupReference('removeButton'); + if (!removeButton.isDisabled() && event.getKey() == event.ENTER) { + removeButton.fireEvent('click', removeButton, event); + } + } + }, + 'button[reference=removeButton]': { + click: function() { + var view = this.getView(); + Proxmox.Utils.API2Request({ + url: view.getUrl() + view.getParams(), + method: 'DELETE', + waitMsgTarget: view, + failure: function(response, opts) { + view.close(); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var hasProgressBar = view.showProgress && + response.result.data ? true : false; + + if (hasProgressBar) { + // stay around so we can trigger our close events + // when background action is completed + view.hide(); + + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + listeners: { + destroy: function () { + view.close(); + } + } + }); + win.show(); + } else { + view.close(); + } + } + }); + } + } + } + }, + + items: [ + { + xtype: 'component', + cls: [ Ext.baseCSSPrefix + 'message-box-icon', + Ext.baseCSSPrefix + 'message-box-warning', + Ext.baseCSSPrefix + 'dlg-icon'] + }, + { + xtype: 'container', + flex: 1, + layout: { + type: 'vbox', + align: 'stretch' + }, + items: [ + { + xtype: 'component', + reference: 'messageCmp' + }, + { + itemId: 'confirmField', + reference: 'confirmField', + xtype: 'textfield', + name: 'confirm', + labelWidth: 300, + hideTrigger: true, + allowBlank: false + } + ] + } + ], + buttons: [ + { + reference: 'removeButton', + text: gettext('Remove'), + disabled: true + } + ], + + initComponent : function() { + var me = this; + + me.callParent(); + + var item = me.getItem(); + + if (!Ext.isDefined(item.id)) { + throw "no ID specified"; + } + + if (!Ext.isDefined(item.type)) { + throw "no VM type specified"; + } + + var messageCmp = me.lookupReference('messageCmp'); + var msg; + + if (item.type === 'VM') { + msg = Proxmox.Utils.format_task_description('qmdestroy', item.id); + } else if (item.type === 'CT') { + msg = Proxmox.Utils.format_task_description('vzdestroy', item.id); + } else if (item.type === 'CephPool') { + msg = Proxmox.Utils.format_task_description('cephdestroypool', item.id); + } else if (item.type === 'Image') { + msg = Proxmox.Utils.format_task_description('unknownimgdel', item.id); + } else { + throw "unknown item type specified"; + } + + messageCmp.setHtml(msg); + + var confirmField = me.lookupReference('confirmField'); + msg = gettext('Please enter the ID to confirm') + + ' (' + item.id + ')'; + confirmField.setFieldLabel(msg); + } +}); +Ext.define('PVE.window.BackupConfig', { + extend: 'Ext.window.Window', + title: gettext('Configuration'), + width: 600, + height: 400, + layout: 'fit', + modal: true, + items: { + xtype: 'component', + itemId: 'configtext', + autoScroll: true, + style: { + 'background-color': 'white', + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px' + } + }, + + initComponent: function() { + var me = this; + + if (!me.volume) { + throw "no volume specified"; + } + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.callParent(); + + Proxmox.Utils.API2Request({ + url: "/nodes/" + nodename + "/vzdump/extractconfig", + method: 'GET', + params: { + volume: me.volume + }, + failure: function(response, opts) { + me.close(); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response,options) { + me.show(); + me.down('#configtext').update(Ext.htmlEncode(response.result.data)); + } + }); + } +}); +Ext.define('PVE.window.Settings', { + extend: 'Ext.window.Window', + + width: '800px', + title: gettext('My Settings'), + iconCls: 'fa fa-gear', + modal: true, + bodyPadding: 10, + resizable: false, + + buttons: [ + { + xtype: 'proxmoxHelpButton', + onlineHelp: 'gui_my_settings', + hidden: false + }, + '->', + { + text: gettext('Close'), + handler: function() { + this.up('window').close(); + } + } + ], + + layout: { + type: 'hbox', + align: 'top' + }, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + var me = this; + var sp = Ext.state.Manager.getProvider(); + + var username = sp.get('login-username') || Proxmox.Utils.noneText; + me.lookupReference('savedUserName').setValue(username); + + var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight']; + settings.forEach(function(setting) { + var val = localStorage.getItem('pve-xterm-' + setting); + if (val !== undefined && val !== null) { + var field = me.lookup(setting); + field.setValue(val); + field.resetOriginalValue(); + } + }); + }, + + set_button_status: function() { + var me = this; + + var form = me.lookup('xtermform'); + var valid = form.isValid(); + var dirty = form.isDirty(); + + var hasvalues = false; + var values = form.getValues(); + Ext.Object.eachValue(values, function(value) { + if (value) { + hasvalues = true; + return false; + } + }); + + me.lookup('xtermsave').setDisabled(!dirty || !valid); + me.lookup('xtermreset').setDisabled(!hasvalues); + }, + + control: { + '#xtermjs form': { + dirtychange: 'set_button_status', + validitychange: 'set_button_status' + }, + '#xtermjs button': { + click: function(button) { + var me = this; + var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight']; + settings.forEach(function(setting) { + var field = me.lookup(setting); + if (button.reference === 'xtermsave') { + var value = field.getValue(); + if (value) { + localStorage.setItem('pve-xterm-' + setting, value); + } else { + localStorage.removeItem('pve-xterm-' + setting); + } + } else if (button.reference === 'xtermreset') { + field.setValue(undefined); + localStorage.removeItem('pve-xterm-' + setting); + } + field.resetOriginalValue(); + }); + me.set_button_status(); + } + }, + 'button[name=reset]': { + click: function () { + var blacklist = ['GuiCap', 'login-username', 'dash-storages']; + var sp = Ext.state.Manager.getProvider(); + var state; + for (state in sp.state) { + if (sp.state.hasOwnProperty(state)) { + if (blacklist.indexOf(state) !== -1) { + continue; + } + + sp.clear(state); + } + } + + window.location.reload(); + } + }, + 'button[name=clear-username]': { + click: function () { + var me = this; + var usernamefield = me.lookupReference('savedUserName'); + var sp = Ext.state.Manager.getProvider(); + + usernamefield.setValue(Proxmox.Utils.noneText); + sp.clear('login-username'); + } + }, + 'grid[reference=dashboard-storages]': { + selectionchange: function(grid, selected) { + var me = this; + var sp = Ext.state.Manager.getProvider(); + + // saves the selected storageids as + // "id1,id2,id3,..." + // or clears the variable + if (selected.length > 0) { + sp.set('dash-storages', + Ext.Array.pluck(selected, 'id').join(',')); + } else { + sp.clear('dash-storages'); + } + }, + afterrender: function(grid) { + var me = grid; + var sp = Ext.state.Manager.getProvider(); + var store = me.getStore(); + var items = []; + me.suspendEvent('selectionchange'); + var storages = sp.get('dash-storages') || ''; + storages.split(',').forEach(function(storage){ + // we have to get the records + // to be able to select them + if (storage !== '') { + var item = store.getById(storage); + if (item) { + items.push(item); + } + } + }); + me.getSelectionModel().select(items); + me.resumeEvent('selectionchange'); + } + } + } + }, + + items: [{ + xtype: 'fieldset', + width: '50%', + title: gettext('Webinterface Settings'), + margin: '5', + layout: { + type: 'vbox', + align: 'left' + }, + defaults: { + width: '100%', + margin: '0 0 10 0' + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Dashboard Storages'), + labelAlign: 'left', + labelWidth: '50%' + }, + { + xtype: 'grid', + maxHeight: 150, + reference: 'dashboard-storages', + selModel: { + selType: 'checkboxmodel' + }, + columns: [{ + header: gettext('Name'), + dataIndex: 'storage', + flex: 1 + },{ + header: gettext('Node'), + dataIndex: 'node', + flex: 1 + }], + store: { + type: 'diff', + field: ['type', 'storage', 'id', 'node'], + rstore: PVE.data.ResourceStore, + filters: [{ + property: 'type', + value: 'storage' + }], + sorters: [ 'node','storage'] + } + }, + { + xtype: 'box', + autoEl: { tag: 'hr'} + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Saved User name'), + labelAlign: 'left', + labelWidth: '50%', + stateId: 'login-username', + reference: 'savedUserName', + value: '' + }, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + text: gettext('Clear User name'), + width: 'auto', + name: 'clear-username' + }, + { + xtype: 'box', + autoEl: { tag: 'hr'} + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Layout'), + labelAlign: 'left', + labelWidth: '50%' + }, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + text: gettext('Reset Layout'), + width: 'auto', + name: 'reset' + } + ] + },{ + xtype: 'fieldset', + itemId: 'xtermjs', + width: '50%', + margin: '5', + title: gettext('xterm.js Settings'), + items: [{ + xtype: 'form', + reference: 'xtermform', + border: false, + layout: { + type: 'vbox', + algin: 'left' + }, + defaults: { + width: '100%', + margin: '0 0 10 0' + }, + items: [ + { + xtype: 'textfield', + name: 'fontFamily', + reference: 'fontFamily', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Font-Family') + }, + { + xtype: 'proxmoxintegerfield', + emptyText: Proxmox.Utils.defaultText, + name: 'fontSize', + reference: 'fontSize', + minValue: 1, + fieldLabel: gettext('Font-Size') + }, + { + xtype: 'numberfield', + name: 'letterSpacing', + reference: 'letterSpacing', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Letter Spacing') + }, + { + xtype: 'numberfield', + name: 'lineHeight', + minValue: 0.1, + reference: 'lineHeight', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Line Height') + }, + { + xtype: 'container', + layout: { + type: 'hbox', + pack: 'end' + }, + items: [ + { + xtype: 'button', + reference: 'xtermreset', + disabled: true, + text: gettext('Reset') + }, + { + xtype: 'button', + reference: 'xtermsave', + disabled: true, + text: gettext('Save') + } + ] + } + ] + }] + }], + + onShow: function() { + var me = this; + me.callParent(); + } +}); +Ext.define('PVE.panel.StartupInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'qm_startup_and_shutdown', + + onGetValues: function(values) { + var me = this; + + var res = PVE.Parser.printStartup(values); + + if (res === undefined || res === '') { + return { 'delete': 'startup' }; + } + + return { startup: res }; + }, + + setStartup: function(value) { + var me = this; + + var startup = PVE.Parser.parseStartup(value); + if (startup) { + me.setValues(startup); + } + }, + + initComponent : function() { + var me = this; + + me.items = [ + { + xtype: 'textfield', + name: 'order', + defaultValue: '', + emptyText: 'any', + fieldLabel: gettext('Start/Shutdown order') + }, + { + xtype: 'textfield', + name: 'up', + defaultValue: '', + emptyText: 'default', + fieldLabel: gettext('Startup delay') + }, + { + xtype: 'textfield', + name: 'down', + defaultValue: '', + emptyText: 'default', + fieldLabel: gettext('Shutdown timeout') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.window.StartupEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveWindowStartupEdit', + onlineHelp: undefined, + + initComponent : function() { + + var me = this; + var ipanelConfig = me.onlineHelp ? {onlineHelp: me.onlineHelp} : {}; + var ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig); + + Ext.applyIf(me, { + subject: gettext('Start/Shutdown order'), + fieldDefaults: { + labelWidth: 120 + }, + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var i, confid; + me.vmconfig = response.result.data; + ipanel.setStartup(me.vmconfig.startup); + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.ceph.Install', { + extend: 'Ext.window.Window', + xtype: 'pveCephInstallWindow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 220, + header: false, + resizable: false, + draggable: false, + modal: true, + nodename: undefined, + shadow: false, + border: false, + bodyBorder: false, + closable: false, + cls: 'install-mask', + bodyCls: 'install-mask', + layout: { + align: 'stretch', + pack: 'center', + type: 'vbox' + }, + viewModel: { + data: { + cephVersion: 'luminous', + isInstalled: false + }, + formulas: { + buttonText: function (get){ + if (get('isInstalled')) { + return gettext('Configure Ceph'); + } else { + return gettext('Install Ceph-') + get('cephVersion'); + } + }, + windowText: function (get) { + if (get('isInstalled')) { + return '

' + + 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?') + '

'; + } + } + } + }, + items: [ + { + bind: { + html: '{windowText}' + }, + border: false, + padding: 5, + bodyCls: 'install-mask' + + }, + { + xtype: 'button', + bind: { + text: '{buttonText}' + }, + viewModel: {}, + cbind: { + nodename: '{nodename}' + }, + handler: function() { + var me = this.up('pveCephInstallWindow'); + var win = Ext.create('PVE.ceph.CephInstallWizard',{ + nodename: me.nodename + }); + win.getViewModel().set('isInstalled', this.getViewModel().get('isInstalled')); + win.show(); + me.mon(win,'beforeClose', function(){ + me.fireEvent("cephInstallWindowClosed"); + me.close(); + }); + + } + } + ] +}); +/*jslint confusion: true*/ +Ext.define('PVE.FirewallEnableEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveFirewallEnableEdit'], + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Firewall'), + cbindData: { + defaultValue: 0 + }, + width: 350, + + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + uncheckedValue: 0, + cbind: { + defaultValue: '{defaultValue}', + checked: '{defaultValue}' + }, + deleteDefaultValue: false, + fieldLabel: gettext('Firewall') + }, + { + xtype: 'displayfield', + name: 'warning', + userCls: 'pve-hint', + value: gettext('Warning: Firewall still disabled at datacenter level!'), + hidden: true + } + ], + + beforeShow: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/cluster/firewall/options', + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + if (!response.result.data.enable) { + me.down('displayfield[name=warning]').setVisible(true); + } + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.FirewallLograteInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveFirewallLograteInputPanel', + + viewModel: {}, + + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + reference: 'enable', + fieldLabel: gettext('Enable'), + value: false + }, + { + layout: 'hbox', + border: false, + items: [ + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Log rate limit'), + minValue: 1, + maxValue: 99, + allowBlank: false, + flex: 2, + value: 1 + }, + { + html: '
/
' + }, + { + xtype: 'proxmoxKVComboBox', + name: 'unit', + comboItems: [['second', 'second'], ['minute', 'minute'], + ['hour', 'hour'], ['day', 'day']], + allowBlank: false, + flex: 1, + value: 'second' + } + ] + }, + { + xtype: 'numberfield', + name: 'burst', + fieldLabel: gettext('Log burst limit'), + minValue: 1, + maxValue: 99, + value: 5 + } + ], + + onGetValues: function(values) { + var me = this; + + var vals = {}; + vals.enable = values.enable !== undefined ? 1 : 0; + vals.rate = values.rate + '/' + values.unit; + vals.burst = values.burst; + var properties = PVE.Parser.printPropertyString(vals, undefined); + if (properties == '') { + return { 'delete': 'log_ratelimit' }; + } + return { log_ratelimit: properties }; + }, + + setValues: function(values) { + var me = this; + + var properties = {}; + if (values.log_ratelimit !== undefined) { + properties = PVE.Parser.parsePropertyString(values.log_ratelimit); + var matches = properties.rate.match(/^(\d+)\/(second|minute|hour|day)$/); + if (matches) { + properties.rate = matches[1]; + properties.unit = matches[2]; + } + } + me.callParent([properties]); + } +}); + +Ext.define('PVE.FirewallLograteEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveFirewallLograteEdit', + + subject: gettext('Log rate limit'), + + items: [{ + xtype: 'pveFirewallLograteInputPanel' + }], + autoLoad: true +}); +Ext.define('PVE.panel.NotesView', { + extend: 'Ext.panel.Panel', + xtype: 'pveNotesView', + + title: gettext("Notes"), + bodyStyle: 'white-space:pre', + bodyPadding: 10, + scrollable: true, + + tbar: { + itemId: 'tbar', + hidden: true, + items: [ + { + text: gettext('Edit'), + handler: function() { + var me = this.up('panel'); + me.run_editor(); + } + } + ] + }, + + run_editor: function() { + var me = this; + var win = Ext.create('PVE.window.NotesEdit', { + pveSelNode: me.pveSelNode, + url: me.url + }); + win.show(); + win.on('destroy', me.load, me); + }, + + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data.description || ''; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + listeners: { + render: function(c) { + var me = this; + me.getEl().on('dblclick', me.run_editor, me); + } + }, + + tools: [{ + type: 'gear', + handler: function() { + var me = this.up('panel'); + me.run_editor(); + } + }], + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var type = me.pveSelNode.data.type; + if (!Ext.Array.contains(['node', 'qemu', 'lxc'], type)) { + throw 'invalid type specified'; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid && type !== 'node') { + throw "no VM ID specified"; + } + + me.url = '/api2/extjs/nodes/' + nodename + '/'; + + // add the type specific path if qemu/lxc + if (type === 'qemu' || type === 'lxc') { + me.url += type + '/' + vmid + '/'; + } + + me.url += 'config'; + + me.callParent(); + if (type === 'node') { + me.down('#tbar').setVisible(true); + } + me.load(); + } +}); +Ext.define('PVE.grid.ResourceGrid', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveResourceGrid'], + + border: false, + defaultSorter: { + property: 'type', + direction: 'ASC' + }, + initComponent : function() { + var me = this; + + var rstore = PVE.data.ResourceStore; + var sp = Ext.state.Manager.getProvider(); + + var coldef = rstore.defaultColumns(); + + var store = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: me.defaultSorter, + proxy: { type: 'memory' } + }); + + var textfilter = ''; + + var textfilter_match = function(item) { + var match = false; + Ext.each(['name', 'storage', 'node', 'type', 'text'], function(field) { + var v = item.data[field]; + if (v !== undefined) { + v = v.toLowerCase(); + if (v.indexOf(textfilter) >= 0) { + match = true; + return false; + } + } + }); + return match; + }; + + var updateGrid = function() { + + var filterfn = me.viewFilter ? me.viewFilter.filterfn : null; + + //console.log("START GRID UPDATE " + me.viewFilter); + + store.suspendEvents(); + + var nodeidx = {}; + var gather_child_nodes = function(cn) { + if (!cn) { + return; + } + var cs = cn.childNodes; + if (!cs) { + return; + } + var len = cs.length, i = 0, n, res; + + for (; i < len; i++) { + var child = cs[i]; + var orgnode = rstore.data.get(child.data.id); + if (orgnode) { + if ((!filterfn || filterfn(child)) && + (!textfilter || textfilter_match(child))) { + nodeidx[child.data.id] = orgnode; + } + } + gather_child_nodes(child); + } + }; + gather_child_nodes(me.pveSelNode); + + // remove vanished items + var rmlist = []; + store.each(function(olditem) { + var item = nodeidx[olditem.data.id]; + if (!item) { + //console.log("GRID REM UID: " + olditem.data.id); + rmlist.push(olditem); + } + }); + + if (rmlist.length) { + store.remove(rmlist); + } + + // add new items + var addlist = []; + var key; + for (key in nodeidx) { + if (nodeidx.hasOwnProperty(key)) { + var item = nodeidx[key]; + + // getById() use find(), which is slow (ExtJS4 DP5) + //var olditem = store.getById(item.data.id); + var olditem = store.data.get(item.data.id); + + if (!olditem) { + //console.log("GRID ADD UID: " + item.data.id); + var info = Ext.apply({}, item.data); + var child = Ext.create(store.model, info); + addlist.push(item); + continue; + } + // try to detect changes + var changes = false; + var fieldkeys = PVE.data.ResourceStore.fieldNames; + var fieldcount = fieldkeys.length; + var fieldind; + for (fieldind = 0; fieldind < fieldcount; fieldind++) { + var field = fieldkeys[fieldind]; + if (field != 'id' && item.data[field] != olditem.data[field]) { + changes = true; + //console.log("changed item " + item.id + " " + field + " " + item.data[field] + " != " + olditem.data[field]); + olditem.beginEdit(); + olditem.set(field, item.data[field]); + } + } + if (changes) { + olditem.endEdit(true); + olditem.commit(true); + } + } + } + + if (addlist.length) { + store.add(addlist); + } + + store.sort(); + + store.resumeEvents(); + + store.fireEvent('refresh', store); + + //console.log("END GRID UPDATE"); + }; + + var filter_task = new Ext.util.DelayedTask(function(){ + updateGrid(); + }); + + var load_cb = function() { + updateGrid(); + }; + + Ext.apply(me, { + store: store, + stateful: true, + stateId: 'grid-resource', + tbar: [ + '->', + gettext('Search') + ':', ' ', + { + xtype: 'textfield', + width: 200, + value: textfilter, + enableKeyEvents: true, + listeners: { + keyup: function(field, e) { + var v = field.getValue(); + textfilter = v.toLowerCase(); + filter_task.delay(500); + } + } + } + ], + viewConfig: { + stripeRows: true + }, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + itemdblclick: function(v, record) { + var ws = me.up('pveStdWorkspace'); + ws.selectById(record.data.id); + }, + destroy: function() { + rstore.un("load", load_cb); + } + }, + columns: coldef + }); + me.callParent(); + updateGrid(); + rstore.on("load", load_cb); + } +}); +Ext.define('PVE.pool.AddVM', { + extend: 'Proxmox.window.Edit', + width: 600, + height: 400, + isAdd: true, + isCreate: true, + initComponent : function() { + + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + me.url = "/pools/" + me.pool; + me.method = 'PUT'; + + var vmsField = Ext.create('Ext.form.field.Text', { + name: 'vms', + hidden: true, + allowBlank: false + }); + + var vmStore = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: [ + { + property: 'vmid', + order: 'ASC' + } + ], + filters: [ + function(item) { + return ((item.data.type === 'lxc' || item.data.type === 'qemu') && item.data.pool === ''); + } + ] + }); + + var vmGrid = Ext.create('widget.grid',{ + store: vmStore, + border: true, + height: 300, + scrollable: true, + selModel: { + selType: 'checkboxmodel', + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected, opts) { + var selectedVms = []; + selected.forEach(function(vm) { + selectedVms.push(vm.data.vmid); + }); + vmsField.setValue(selectedVms); + } + } + }, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 60 + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'uptime', + renderer: function(value) { + if (value) { + return Proxmox.Utils.runningText; + } else { + return Proxmox.Utils.stoppedText; + } + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('Type'), + dataIndex: 'type' + } + ] + }); + Ext.apply(me, { + subject: gettext('Virtual Machine'), + items: [ vmsField, vmGrid ] + }); + + me.callParent(); + vmStore.load(); + } +}); + +Ext.define('PVE.pool.AddStorage', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + me.isCreate = true; + me.isAdd = true; + me.url = "/pools/" + me.pool; + me.method = 'PUT'; + + Ext.apply(me, { + subject: gettext('Storage'), + width: 350, + items: [ + { + xtype: 'pveStorageSelector', + name: 'storage', + nodename: 'localhost', + autoSelect: false, + value: '', + fieldLabel: gettext("Storage") + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.grid.PoolMembers', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pvePoolMembers'], + + // fixme: dynamic status update ? + + stateful: true, + stateId: 'grid-pool-members', + + initComponent : function() { + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + var store = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: [ + { + property : 'type', + direction: 'ASC' + } + ], + proxy: { + type: 'proxmox', + root: 'data.members', + url: "/api2/json/pools/" + me.pool + } + }); + + var coldef = PVE.data.ResourceStore.defaultColumns(); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: function (rec) { + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.id + "'"); + }, + handler: function(btn, event, rec) { + var params = { 'delete': 1 }; + if (rec.data.type === 'storage') { + params.storage = rec.data.storage; + } else if (rec.data.type === 'qemu' || rec.data.type === 'lxc' || rec.data.type === 'openvz') { + params.vms = rec.data.vmid; + } else { + throw "unknown resource type"; + } + + Proxmox.Utils.API2Request({ + url: '/pools/' + me.pool, + method: 'PUT', + params: params, + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Virtual Machine'), + iconCls: 'pve-itype-icon-qemu', + handler: function() { + var win = Ext.create('PVE.pool.AddVM', { pool: me.pool }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('Storage'), + iconCls: 'pve-itype-icon-storage', + handler: function() { + var win = Ext.create('PVE.pool.AddStorage', { pool: me.pool }); + win.on('destroy', reload); + win.show(); + } + } + ] + }) + }, + remove_btn + ], + viewConfig: { + stripeRows: true + }, + columns: coldef, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + itemdblclick: function(v, record) { + var ws = me.up('pveStdWorkspace'); + ws.selectById(record.data.id); + }, + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.form.FWMacroSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveFWMacroSelector', + allowBlank: true, + autoSelect: false, + valueField: 'macro', + displayField: 'macro', + listConfig: { + columns: [ + { + header: gettext('Macro'), + dataIndex: 'macro', + hideable: false, + width: 100 + }, + { + header: gettext('Description'), + renderer: Ext.String.htmlEncode, + flex: 1, + dataIndex: 'descr' + } + ] + }, + initComponent: function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: [ 'macro', 'descr' ], + idProperty: 'macro', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/firewall/macros" + }, + sorters: { + property: 'macro', + order: 'DESC' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.FirewallRulePanel', { + extend: 'Proxmox.panel.InputPanel', + + allow_iface: false, + + list_refs_url: undefined, + + onGetValues: function(values) { + var me = this; + + // hack: editable ComboGrid returns nothing when empty, so we need to set '' + // Also, disabled text fields return nothing, so we need to set '' + + Ext.Array.each(['source', 'dest', 'macro', 'proto', 'sport', 'dport', 'log'], function(key) { + if (values[key] === undefined) { + values[key] = ''; + } + }); + + delete values.modified_marker; + + return values; + }, + + initComponent : function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.column1 = [ + { + // hack: we use this field to mark the form 'dirty' when the + // record has errors- so that the user can safe the unmodified + // form again. + xtype: 'hiddenfield', + name: 'modified_marker', + value: '' + }, + { + xtype: 'proxmoxKVComboBox', + name: 'type', + value: 'in', + comboItems: [['in', 'in'], ['out', 'out']], + fieldLabel: gettext('Direction'), + allowBlank: false + }, + { + xtype: 'proxmoxKVComboBox', + name: 'action', + value: 'ACCEPT', + comboItems: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']], + fieldLabel: gettext('Action'), + allowBlank: false + } + ]; + + if (me.allow_iface) { + me.column1.push({ + xtype: 'proxmoxtextfield', + name: 'iface', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('Interface') + }); + } else { + me.column1.push({ + xtype: 'displayfield', + fieldLabel: '', + value: '' + }); + } + + me.column1.push( + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'pveIPRefSelector', + name: 'source', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Source') + + }, + { + xtype: 'pveIPRefSelector', + name: 'dest', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Destination') + } + ); + + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Enable') + }, + { + xtype: 'pveFWMacroSelector', + name: 'macro', + fieldLabel: gettext('Macro'), + editable: true, + allowBlank: true, + listeners: { + change: function(f, value) { + if (value === null) { + me.down('field[name=proto]').setDisabled(false); + me.down('field[name=sport]').setDisabled(false); + me.down('field[name=dport]').setDisabled(false); + } else { + me.down('field[name=proto]').setDisabled(true); + me.down('field[name=proto]').setValue(''); + me.down('field[name=sport]').setDisabled(true); + me.down('field[name=sport]').setValue(''); + me.down('field[name=dport]').setDisabled(true); + me.down('field[name=dport]').setValue(''); + } + } + } + }, + { + xtype: 'pveIPProtocolSelector', + name: 'proto', + autoSelect: false, + editable: true, + value: '', + fieldLabel: gettext('Protocol') + }, + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'textfield', + name: 'sport', + value: '', + fieldLabel: gettext('Source port') + }, + { + xtype: 'textfield', + name: 'dport', + value: '', + fieldLabel: gettext('Dest. port') + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'pveFirewallLogLevels' + } + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.FirewallRuleEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + list_refs_url: undefined, + + allow_iface: false, + + initComponent : function() { + + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.isCreate = (me.rule_pos === undefined); + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.FirewallRulePanel', { + isCreate: me.isCreate, + list_refs_url: me.list_refs_url, + allow_iface: me.allow_iface, + rule_pos: me.rule_pos + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + if (values.errors) { + var field = me.query('[isFormField][name=modified_marker]')[0]; + field.setValue(1); + Ext.Function.defer(function() { + var form = ipanel.up('form').getForm(); + form.markInvalid(values.errors); + }, 100); + } + } + }); + } else if (me.rec) { + ipanel.setValues(me.rec.data); + } + } +}); + +Ext.define('PVE.FirewallGroupRuleEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + + allow_iface: false, + + initComponent : function() { + + var me = this; + + me.isCreate = (me.rule_pos === undefined); + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var column1 = [ + { + xtype: 'hiddenfield', + name: 'type', + value: 'group' + }, + { + xtype: 'pveSecurityGroupsSelector', + name: 'action', + value: '', + fieldLabel: gettext('Security Group'), + allowBlank: false + } + ]; + + if (me.allow_iface) { + column1.push({ + xtype: 'proxmoxtextfield', + name: 'iface', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('Interface') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + column1: column1, + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Enable') + } + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.FirewallRules', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveFirewallRules', + + onlineHelp: 'chapter_pve_firewall', + + stateful: true, + stateId: 'grid-firewall-rules', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + groupBtn: undefined, + + tbar_prefix: undefined, + + allow_groups: true, + allow_iface: false, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + if (me.groupBtn) { + me.groupBtn.setDisabled(true); + } + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.removeBtn.baseurl = url + '/'; + if (me.groupBtn) { + me.groupBtn.setDisabled(false); + } + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json' + url + }); + + me.store.load(); + } + }, + + moveRule: function(from, to) { + var me = this; + + if (!me.base_url) { + return; + } + + Proxmox.Utils.API2Request({ + url: me.base_url + "/" + from, + method: 'PUT', + params: { moveto: to }, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + updateRule: function(rule) { + var me = this; + + if (!me.base_url) { + return; + } + + rule.enable = rule.enable ? 1 : 0; + + var pos = rule.pos; + delete rule.pos; + delete rule.errors; + + Proxmox.Utils.API2Request({ + url: me.base_url + '/' + pos.toString(), + method: 'PUT', + params: rule, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + var store = Ext.create('Ext.data.Store',{ + model: 'pve-fw-rule' + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var type = rec.data.type; + + var editor; + if (type === 'in' || type === 'out') { + editor = 'PVE.FirewallRuleEdit'; + } else if (type === 'group') { + editor = 'PVE.FirewallGroupRuleEdit'; + } else { + return; + } + + var win = Ext.create(editor, { + digest: rec.data.digest, + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rule_pos: rec.data.pos + }); + + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = Ext.create('Proxmox.button.Button',{ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = Ext.create('Ext.Button', { + text: gettext('Add'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url + }); + win.on('destroy', reload); + win.show(); + } + }); + + var run_copy_editor = function() { + var rec = sm.getSelection()[0]; + + if (!rec) { + return; + } + var type = rec.data.type; + + + if (!(type === 'in' || type === 'out')) { + return; + } + + var win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rec: rec + }); + + win.show(); + win.on('destroy', reload); + }; + + me.copyBtn = Ext.create('Proxmox.button.Button',{ + text: gettext('Copy'), + selModel: sm, + enableFn: function(rec) { + return (rec.data.type === 'in' || rec.data.type === 'out'); + }, + disabled: true, + handler: run_copy_editor + }); + + if (me.allow_groups) { + me.groupBtn = Ext.create('Ext.Button', { + text: gettext('Insert') + ': ' + + gettext('Security Group'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallGroupRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url + }); + win.on('destroy', reload); + win.show(); + } + }); + } + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton',{ + selModel: sm, + baseurl: me.base_url + '/', + confirmMsg: false, + getRecordName: function(rec) { + var rule = rec.data; + return rule.pos.toString() + + '?digest=' + encodeURIComponent(rule.digest); + }, + callback: function() { + me.store.load(); + } + }); + + var tbar = me.tbar_prefix ? [ me.tbar_prefix ] : []; + tbar.push(me.addBtn, me.copyBtn); + if (me.groupBtn) { + tbar.push(me.groupBtn); + } + tbar.push(me.removeBtn, me.editBtn); + + var render_errors = function(name, value, metaData, record) { + var errors = record.data.errors; + if (errors && errors[name]) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + 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 = '
'; + status += '
'; + status += '
'; + status += '
'; + } + } + + info.text = status + info.text; + }, + + setToolTip: function(info) { + if (info.type === 'pool' || info.groupbyid !== undefined) { + return; + } + + var qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)]; + if (info.hastate != 'unmanaged') { + qtips.push(gettext('HA State') + ": " + info.hastate); + } + + info.qtip = qtips.join(', '); + }, + + // private + addChildSorted: function(node, info) { + var me = this; + + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + + var defaults; + if (info.groupbyid) { + info.text = info.groupbyid; + if (info.type === 'type') { + defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid]; + if (defaults && defaults.text) { + info.text = defaults.text; + } + } + } + var child = Ext.create('PVETree', info); + + var cs = node.childNodes; + var pos; + if (cs) { + pos = cs[me.findInsertIndex(node, child, 0, cs.length)]; + } + + node.insertBefore(child, pos); + + return child; + }, + + // private + groupChild: function(node, info, groups, level) { + var me = this; + + var groupby = groups[level]; + var v = info[groupby]; + + if (v) { + var group = node.findChild('groupbyid', v); + if (!group) { + var groupinfo; + if (info.type === groupby) { + groupinfo = info; + } else { + groupinfo = { + type: groupby, + id : groupby + "/" + v + }; + if (groupby !== 'type') { + groupinfo[groupby] = v; + } + } + groupinfo.leaf = false; + groupinfo.groupbyid = v; + group = me.addChildSorted(node, groupinfo); + } + if (info.type === groupby) { + return group; + } + if (group) { + return me.groupChild(group, info, groups, level + 1); + } + } + + return me.addChildSorted(node, info); + }, + + initComponent : function() { + var me = this; + + var rstore = PVE.data.ResourceStore; + var sp = Ext.state.Manager.getProvider(); + + if (!me.viewFilter) { + me.viewFilter = {}; + } + + var pdata = { + dataIndex: {}, + updateCount: 0 + }; + + var store = Ext.create('Ext.data.TreeStore', { + model: 'PVETree', + root: { + expanded: true, + id: 'root', + text: gettext('Datacenter'), + iconCls: 'fa fa-server' + } + }); + + var stateid = 'rid'; + + var updateTree = function() { + var tmp; + + store.suspendEvents(); + + var rootnode = me.store.getRootNode(); + // remember selected node (and all parents) + var sm = me.getSelectionModel(); + + var lastsel = sm.getSelection()[0]; + var reselect = false; + var parents = []; + var p = lastsel; + while (p && !!(p = p.parentNode)) { + parents.push(p); + } + + var index = pdata.dataIndex; + + var groups = me.viewFilter.groups || []; + var filterfn = me.viewFilter.filterfn; + + // remove vanished or moved items + // update in place changed items + var key; + for (key in index) { + if (index.hasOwnProperty(key)) { + var olditem = index[key]; + + // getById() use find(), which is slow (ExtJS4 DP5) + //var item = rstore.getById(olditem.data.id); + var item = rstore.data.get(olditem.data.id); + + var changed = false; + var moved = false; + if (item) { + // test if any grouping attributes changed + // this will also catch migrated nodes + // in server view + var i, len; + for (i = 0, len = groups.length; i < len; i++) { + var attr = groups[i]; + if (item.data[attr] != olditem.data[attr]) { + //console.log("changed " + attr); + moved = true; + break; + } + } + + // explicitely check for node, since + // in some views, node is not a grouping + // attribute + if (!moved && item.data.node !== olditem.data.node) { + moved = true; + } + + // tree item has been updated + if ((item.data.text !== olditem.data.text) || + (item.data.running !== olditem.data.running) || + (item.data.template !== olditem.data.template) || + (item.data.status !== olditem.data.status) || + (item.data.hastate!== olditem.data.hastate)) { + //console.log("changed node/text/running " + olditem.data.id); + changed = true; + } + + // fixme: also test filterfn()? + } + + if (changed) { + olditem.beginEdit(); + //console.log("REM UPDATE UID: " + key + " ITEM " + item.data.running); + var info = olditem.data; + Ext.apply(info, item.data); + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + olditem.commit(); + } + if ((!item || moved) && olditem.isLeaf()) { + //console.log("REM UID: " + key + " ITEM " + olditem.data.id); + delete index[key]; + var parentNode = olditem.parentNode; + // when the selected item disappears, + // we have to deselect it here, and reselect it + // later + if (lastsel && olditem.data.id === lastsel.data.id) { + reselect = true; + sm.deselect(olditem); + } + // since the store events are suspended, we + // manually remove the item from the store also + store.remove(olditem); + parentNode.removeChild(olditem, true); + } + } + } + + // add new items + rstore.each(function(item) { + var olditem = index[item.data.id]; + if (olditem) { + return; + } + + if (filterfn && !filterfn(item)) { + return; + } + + //console.log("ADD UID: " + item.data.id); + + var info = Ext.apply({ leaf: true }, item.data); + + var child = me.groupChild(rootnode, info, groups, 0); + if (child) { + index[item.data.id] = child; + } + }); + + store.resumeEvents(); + store.fireEvent('refresh', store); + + // select parent node is selection vanished + if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) { + lastsel = rootnode; + while (!!(p = parents.shift())) { + if (!!(tmp = rootnode.findChild('id', p.data.id, true))) { + lastsel = tmp; + break; + } + } + me.selectById(lastsel.data.id); + } else if (lastsel && reselect) { + me.selectById(lastsel.data.id); + } + + // on first tree load set the selection from the stateful provider + if (!pdata.updateCount) { + rootnode.expand(); + me.applyState(sp.get(stateid)); + } + + pdata.updateCount++; + }; + + var statechange = function(sp, key, value) { + if (key === stateid) { + me.applyState(value); + } + }; + + sp.on('statechange', statechange); + + Ext.apply(me, { + allowSelection: true, + store: store, + viewConfig: { + // note: animate cause problems with applyState + animate: false + }, + //useArrows: true, + //rootVisible: false, + //title: 'Resource Tree', + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + destroy: function() { + rstore.un("load", updateTree); + }, + beforecellmousedown: function (tree, td, cellIndex, record, tr, rowIndex, ev) { + var sm = me.getSelectionModel(); + // disable selection when right clicking + // except the record is already selected + me.allowSelection = (ev.button !== 2) || sm.isSelected(record); + }, + beforeselect: function (tree, record, index, eopts) { + var allow = me.allowSelection; + me.allowSelection = true; + return allow; + }, + itemdblclick: PVE.Utils.openTreeConsole + }, + setViewFilter: function(view) { + me.viewFilter = view; + me.clearTree(); + updateTree(); + }, + setDatacenterText: function(clustername) { + var rootnode = me.store.getRootNode(); + + var rnodeText = gettext('Datacenter'); + if (clustername !== undefined) { + rnodeText += ' (' + clustername + ')'; + } + + rootnode.beginEdit(); + rootnode.data.text = rnodeText; + rootnode.commit(); + }, + clearTree: function() { + pdata.updateCount = 0; + var rootnode = me.store.getRootNode(); + rootnode.collapse(); + rootnode.removeAll(); + pdata.dataIndex = {}; + me.getSelectionModel().deselectAll(); + }, + selectExpand: function(node) { + var sm = me.getSelectionModel(); + if (!sm.isSelected(node)) { + sm.select(node); + var cn = node; + while (!!(cn = cn.parentNode)) { + if (!cn.isExpanded()) { + cn.expand(); + } + } + me.getView().focusRow(node); + } + }, + selectById: function(nodeid) { + var rootnode = me.store.getRootNode(); + var sm = me.getSelectionModel(); + var node; + if (nodeid === 'root') { + node = rootnode; + } else { + node = rootnode.findChild('id', nodeid, true); + } + if (node) { + me.selectExpand(node); + } + return node; + }, + applyState : function(state) { + var sm = me.getSelectionModel(); + if (state && state.value) { + me.selectById(state.value); + } else { + sm.deselectAll(); + } + } + }); + + me.callParent(); + + var sm = me.getSelectionModel(); + sm.on('select', function(sm, n) { + sp.set(stateid, { value: n.data.id}); + }); + + rstore.on("load", updateTree); + rstore.startUpdate(); + //rstore.stopUpdate(); + } + +}); +Ext.define('pve-fw-ipsets', { + extend: 'Ext.data.Model', + fields: [ 'name', 'comment', 'digest' ], + idProperty: 'name' +}); + +Ext.define('PVE.IPSetList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetList', + + stateful: true, + stateId: 'grid-firewall-ipsetlist', + + ipset_panel: undefined, + + base_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + initComponent: function() { + + var me = this; + + if (me.ipset_panel == undefined) { + throw "no rule panel specified"; + } + + if (me.base_url == undefined) { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-fw-ipsets', + proxy: { + type: 'proxmox', + url: "/api2/json" + me.base_url + }, + sorters: { + property: 'name', + 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 rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('Proxmox.window.Edit', { + subject: "IPSet '" + rec.data.name + "'", + url: me.base_url, + method: 'POST', + digest: rec.data.digest, + items: [ + { + xtype: 'hiddenfield', + name: 'rename', + value: rec.data.name + }, + { + xtype: 'textfield', + name: 'name', + value: rec.data.name, + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: rec.data.comment, + fieldLabel: gettext('Comment') + } + ] + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('Proxmox.window.Edit', { + subject: 'IPSet', + url: me.base_url, + method: 'POST', + items: [ + { + xtype: 'textfield', + name: 'name', + value: '', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + win.show(); + win.on('destroy', reload); + + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload + }); + + Ext.apply(me, { + store: store, + tbar: [ 'IPSet:', me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: 'IPSet', dataIndex: 'name', width: '100' }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } + ], + listeners: { + itemdblclick: run_editor, + select: function(sm, rec) { + var url = me.base_url + '/' + rec.data.name; + me.ipset_panel.setBaseUrl(url); + }, + deselect: function() { + me.ipset_panel.setBaseUrl(undefined); + }, + show: reload + } + }); + + me.callParent(); + + store.load(); + } +}); + +Ext.define('PVE.IPSetCidrEdit', { + extend: 'Proxmox.window.Edit', + + cidr: undefined, + + initComponent : function() { + + var me = this; + + me.isCreate = (me.cidr === undefined); + + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.cidr; + me.method = 'PUT'; + } + + var column1 = []; + + if (me.isCreate) { + if (!me.list_refs_url) { + throw "no alias_base_url specified"; + } + + column1.push({ + xtype: 'pveIPRefSelector', + name: 'cidr', + ref_type: 'alias', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('IP/CIDR') + }); + } else { + column1.push({ + xtype: 'displayfield', + name: 'cidr', + value: '', + fieldLabel: gettext('IP/CIDR') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + column1: column1, + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'nomatch', + checked: false, + uncheckedValue: 0, + fieldLabel: 'nomatch' + } + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + + Ext.apply(me, { + subject: gettext('IP/CIDR'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.IPSetGrid', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetGrid', + + stateful: true, + stateId: 'grid-firewall-ipsets', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.removeBtn.baseurl = url + '/'; + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json' + url + }); + + me.store.load(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.list_refs_url) { + throw "no1 list_refs_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-ipset' + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + cidr: rec.data.cidr + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Add'), + disabled: true, + handler: function() { + if (!me.base_url) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + list_refs_url: me.list_refs_url + }); + win.show(); + win.on('destroy', reload); + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload + }); + + var render_errors = function(value, metaData, record) { + var errors = record.data.errors; + if (errors) { + var msg = errors.cidr || errors.nomatch; + if (msg) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + 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: [ + '', + '{osd}:', + '', + gettext('No OSD selected'), + '' + ] + }); + + set_button_status = function() { + var rec = sm.getSelection()[0]; + noout_btn.setText(noout?gettext('Unset noout'):gettext('Set noout')); + + if (!rec) { + start_btn.setDisabled(true); + stop_btn.setDisabled(true); + restart_btn.setDisabled(true); + remove_btn.setDisabled(true); + osd_out_btn.setDisabled(true); + osd_in_btn.setDisabled(true); + return; + } + + var isOsd = (rec.data.host && (rec.data.type === 'osd') && (rec.data.id >= 0)); + + start_btn.setDisabled(!(isOsd && (rec.data.status !== 'up'))); + stop_btn.setDisabled(!(isOsd && (rec.data.status !== 'down'))); + restart_btn.setDisabled(!(isOsd && (rec.data.status !== 'down'))); + remove_btn.setDisabled(!(isOsd && (rec.data.status === 'down'))); + + osd_out_btn.setDisabled(!(isOsd && rec.data['in'])); + osd_in_btn.setDisabled(!(isOsd && !rec.data['in'])); + + osd_label.update(isOsd?{osd:rec.data.name}:undefined); + }; + + sm.on('selectionchange', set_button_status); + + var reload_btn = new Ext.Button({ + text: gettext('Reload'), + handler: reload + }); + + Ext.apply(me, { + tbar: [ create_btn, reload_btn, noout_btn, '->', osd_label, start_btn, stop_btn, restart_btn, osd_out_btn, osd_in_btn, remove_btn ], + rootVisible: false, + useArrows: true, + fields: ['name', 'type', 'status', 'host', 'in', 'id' , + { type: 'number', name: 'reweight' }, + { type: 'number', name: 'percent_used' }, + { type: 'integer', name: 'bytes_used' }, + { type: 'integer', name: 'total_space' }, + { type: 'integer', name: 'apply_latency_ms' }, + { type: 'integer', name: 'commit_latency_ms' }, + { type: 'string', name: 'device_class' }, + { type: 'string', name: 'osdtype' }, + { type: 'string', name: 'blfsdev' }, + { type: 'string', name: 'dbdev' }, + { type: 'string', name: 'waldev' }, + { type: 'string', name: 'iconCls', calculate: function(data) { + var iconCls = 'fa x-fa-tree fa-'; + switch (data.type) { + case 'host': + iconCls += 'building'; + break; + case 'osd': + iconCls += 'hdd-o'; + break; + case 'root': + iconCls += 'server'; + break; + default: + return undefined; + } + return iconCls; + } }, + { type: 'number', name: 'crush_weight' }], + selModel: sm, + + listeners: { + activate: function() { + reload(); + } + } + }); + + me.callParent(); + + reload(); + } +}); +Ext.define('PVE.CephCreateMon', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveCephCreateMon'], + + subject: 'Ceph Monitor/Manager', + onlineHelp: 'pve_ceph_monitors', + + showProgress: true, + + setNode: function(nodename) { + var me = this; + + me.nodename = nodename; + me.url = "/nodes/" + nodename + "/ceph/mon"; + }, + + initComponent : function() { + + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.setNode(me.nodename); + + me.isCreate = true; + + Ext.applyIf(me, { + method: 'POST', + items: [ + { + xtype: 'pveNodeSelector', + submitValue: false, + fieldLabel: gettext('Host'), + selectCurNode: true, + allowBlank: false, + listeners: { + change: function(f, value) { + me.setNode(value); + } + } + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephMonList', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveNodeCephMonList'], + + onlineHelp: 'chapter_pveceph', + + stateful: true, + stateId: 'grid-ceph-monitor', + + 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-mon-list' + nodename, + model: 'ceph-mon-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/ceph/mon" + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: rstore, + sorters: [{ property: 'name'}] + }); + + + var service_cmd = function(cmd) { + var rec = sm.getSelection()[0]; + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + Proxmox.Utils.API2Request({ + url: "/nodes/" + rec.data.host + "/ceph/" + cmd, + method: 'POST', + params: { service: "mon." + rec.data.name }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var start_btn = new Proxmox.button.Button({ + text: gettext('Start'), + selModel: sm, + disabled: true, + handler: function(){ + service_cmd("start"); + } + }); + + var stop_btn = new Proxmox.button.Button({ + text: gettext('Stop'), + selModel: sm, + disabled: true, + handler: function(){ + service_cmd("stop"); + } + }); + + var restart_btn = new Proxmox.button.Button({ + text: gettext('Restart'), + selModel: sm, + disabled: true, + handler: function(){ + service_cmd("restart"); + } + }); + + var create_btn = new Ext.Button({ + text: gettext('Create'), + handler: function(){ + var win = Ext.create('PVE.CephCreateMon', { + nodename: nodename + }); + win.show(); + } + }); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + + Proxmox.Utils.API2Request({ + url: "/nodes/" + rec.data.host + "/ceph/mon/" + + rec.data.name, + method: 'DELETE', + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ start_btn, stop_btn, restart_btn, '-', create_btn, remove_btn ], + columns: [ + { + header: gettext('Name'), + width: 100, + sortable: true, + renderer: function(v) { return "mon." + v; }, + dataIndex: 'name' + }, + { + header: gettext('Host'), + width: 100, + sortable: true, + renderer: function(v) { + return v || 'unknown'; + }, + dataIndex: 'host' + }, + { + header: gettext('Quorum'), + width: 70, + sortable: false, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'quorum' + }, + { + header: gettext('Address'), + flex: 1, + sortable: true, + dataIndex: 'addr' + } + ], + listeners: { + activate: rstore.startUpdate, + destroy: rstore.stopUpdate + } + }); + + 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(); + }); + } + ); + }); + + me.callParent(); + } +}, function() { + + Ext.define('ceph-mon-list', { + extend: 'Ext.data.Model', + fields: [ 'addr', 'name', 'rank', 'host', 'quorum' ], + idProperty: 'name' + }); +}); +Ext.define('PVE.node.CephCrushMap', { + extend: 'Ext.panel.Panel', + alias: ['widget.pveNodeCephCrushMap'], + bodyStyle: 'white-space:pre', + bodyPadding: 5, + border: false, + stateful: true, + stateId: 'layout-ceph-crush', + scrollable: true, + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.load(); + }); + } + ); + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: '/nodes/' + nodename + '/ceph/crush', + + listeners: { + activate: function() { + me.load(); + } + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.node.CephStatus', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephStatus', + + onlineHelp: 'chapter_pveceph', + + scrollable: true, + + bodyPadding: 5, + + layout: { + type: 'column' + }, + + defaults: { + padding: 5 + }, + + items: [ + { + xtype: 'panel', + title: gettext('Health'), + bodyPadding: 10, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + }, + minHeight: 210, + layout: { + type: 'hbox', + align: 'stretch' + }, + items: [ + { + flex: 1, + itemId: 'overallhealth', + xtype: 'pveHealthWidget', + title: gettext('Status') + }, + { + flex: 2, + itemId: 'warnings', + stateful: true, + stateId: 'ceph-status-warnings', + xtype: 'grid', + // since we load the store manually, + // to show the emptytext, we have to + // specify an empty store + store: { data:[] }, + emptyText: gettext('No Warnings/Errors'), + columns: [ + { + dataIndex: 'severity', + header: gettext('Severity'), + align: 'center', + width: 70, + renderer: function(value) { + var health = PVE.Utils.map_ceph_health[value]; + var classes = PVE.Utils.get_health_icon(health); + + return ''; + }, + sorter: { + sorterFn: function(a,b) { + var healthArr = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK']; + return healthArr.indexOf(b.data.severity) - healthArr.indexOf(a.data.severity); + } + } + }, + { + dataIndex: 'summary', + header: gettext('Summary'), + flex: 1 + }, + { + xtype: 'actioncolumn', + width: 40, + align: 'center', + tooltip: gettext('Detail'), + items: [ + { + iconCls: 'x-fa fa-info-circle', + handler: function(grid, rowindex, colindex, item, e, record) { + var win = Ext.create('Ext.window.Window', { + title: gettext('Detail'), + resizable: true, + modal: true, + width: 650, + height: 400, + layout: { + type: 'fit' + }, + items: [{ + scrollable: true, + padding: 10, + xtype: 'box', + html: [ + '' + Ext.htmlEncode(record.data.summary) + '', + '
' + 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('Monitors') + '

' + } + ] + },{ + flex: 1, + itemId: 'osds', + data: { + total: 0, + upin: 0, + upout: 0, + downin: 0, + downout: 0 + }, + tpl: [ + '

' + 'OSDs' + '

', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
', + gettext('In'), + '', + gettext('Out'), + '
', + gettext('Up'), + '{upin}{upout}
', + gettext('Down'), + '{downin}{downout}
', + '
', + gettext('Total'), + ': {total}', + '
' + ] + }, + { + flex: 1.6, + itemId: 'pgs', + padding: '0 10', + data: { + states: [] + }, + tpl: [ + '

' + 'PGs' + '

', + '', + '
{state_name}:
', + '
{count}

', + '
', + '
' + ] + }], + + updateAll: function(health, monmap, pgmap, osdmap, quorum_names) { + var me = this; + me.suspendLayout = true; + + // update pgs sorted + var pgs_by_state = pgmap.pgs_by_state || []; + pgs_by_state.sort(function(a,b){ + return (a.state_name < b.state_name)?-1:(a.state_name === b.state_name)?0:1; + }); + me.getComponent('pgs').update({states: pgs_by_state}); + + var downinregex = /(\d+) osds down/; + var monnameregex = /^mon.(\S+) /; + var downin_osds = 0; + var monmsgs = {}; + + // we collect monitor/osd information from the checks + Ext.Object.each(health.checks, function(key, value, obj) { + var found = null; + if (key === 'OSD_DOWN') { + found = value.summary.message.match(downinregex); + if (found !== null) { + downin_osds = parseInt(found[1],10); + } + } + else if (Ext.String.startsWith(key, 'MON_')) { + if (!value.detail) { + return; + } + found = value.detail[0].message.match(monnameregex); + if (found !== null) { + if (!monmsgs[found[1]]) { + monmsgs[found[1]] = []; + } + monmsgs[found[1]].push({ + text: Ext.Array.reduce(value.detail, function(first, second) { + return first + '\n' + second.message; + }, ''), + severity: value.severity + }); + } + } + }); + + // update osds counts + + var total_osds = osdmap.osdmap.num_osds || 0; + var in_osds = osdmap.osdmap.num_in_osds || 0; + var up_osds = osdmap.osdmap.num_up_osds || 0; + var out_osds = total_osds - in_osds; + var down_osds = total_osds - up_osds; + + var downout_osds = down_osds - downin_osds; + var upin_osds = in_osds - downin_osds; + var upout_osds = up_osds - upin_osds; + var osds = { + total: total_osds, + upin: upin_osds, + upout: upout_osds, + downin: downin_osds, + downout: downout_osds + }; + me.getComponent('osds').update(osds); + + // update the monitors + var mons = monmap.mons.sort(function(a,b) { + return (a.name < b.name)?-1:(a.name > b.name)?1:0; + }); + + var monContainer = me.getComponent('monitors'); + + var i; + for (i = 0; i < mons.length; i++) { + var monitor = monContainer.getComponent('mon.' + mons[i].name); + if (!monitor) { + // since mons are already sorted, and + // we always have a sorted list + // we can add it at the mons+1 position (because of the title) + monitor = monContainer.insert(i+1, { + xtype: 'pveCephMonitorWidget', + itemId: 'mon.' + mons[i].name + }); + } + monitor.updateMonitor(mons[i], monmsgs, quorum_names); + } + me.suspendLayout = false; + me.updateLayout(); + } +}); + +Ext.define('PVE.ceph.MonitorWidget', { + extend: 'Ext.Component', + alias: 'widget.pveCephMonitorWidget', + + userCls: 'monitor inline-block', + data: { + name: '0', + health: 'HEALTH_ERR', + text: '', + iconCls: PVE.Utils.get_health_icon(), + addr: '' + }, + + tpl: [ + '{name}: ', + '' + ], + + // expects 3 variables which are + // timestate: the status from timechecks.mons + // data: the monmap.mons data + // quorum_names: the quorum_names array + updateMonitor: function(data, monmsgs, quorum_names) { + var me = this; + var state = 'HEALTH_ERR'; + var text = ''; + var healthstates = { + 'HEALTH_OK': 3, + 'HEALTH_WARN': 2, + 'HEALTH_ERR': 1 + }; + + if (quorum_names && + quorum_names.indexOf(data.name) !== -1) { + state = 'HEALTH_OK'; + } + + if (monmsgs[data.name]) { + Ext.Array.forEach(monmsgs[data.name], function(msg) { + if (healthstates[msg.severity] < healthstates[state]) { + state = msg.severity; + } + + text += msg.text + "\n"; + }); + } + + me.update(Ext.apply(me.data, { + health: state, + text: text, + addr: data.addr, + name: data.name, + iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[state]) + })); + }, + + listeners: { + mouseenter: { + element: 'el', + fn: function(events, element) { + var me = this.component; + if (!me) { + return; + } + if (!me.tooltip) { + me.tooltip = Ext.create('Ext.tip.ToolTip', { + target: me.el, + trackMouse: true, + renderTo: Ext.getBody(), + html: gettext('Monitor') + ': ' + me.data.name + '
' + + gettext('Address') + ': ' + me.data.addr + '
' + + gettext('Health') + ': ' + me.data.health + '
' + + me.data.text + }); + } + me.tooltip.show(); + } + }, + mouseleave: { + element: 'el', + fn: function(events, element) { + var me = this.component; + if (me.tooltip) { + me.tooltip.destroy(); + delete me.tooltip; + } + } + } + } +}); +Ext.define('PVE.node.CephConfig', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfig', + + bodyStyle: 'white-space:pre', + bodyPadding: 5, + border: false, + scrollable: true, + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.load(); + }); + } + ); + + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: '/nodes/' + nodename + '/ceph/config', + listeners: { + activate: function() { + me.load(); + } + } + }); + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.node.CephConfigCrush', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfigCrush', + + onlineHelp: 'chapter_pveceph', + + layout: 'border', + items: [{ + title: gettext('Configuration'), + xtype: 'pveNodeCephConfig', + region: 'center' + }, + { + title: 'Crush Map', // do not localize + xtype: 'pveNodeCephCrushMap', + region: 'east', + split: true, + width: '50%' + }], + + initComponent: function() { + var me = this; + me.defaults = { + pveSelNode: me.pveSelNode + }; + me.callParent(); + } +}); +Ext.define('PVE.ceph.Log', { + extend: 'Proxmox.panel.LogView', + xtype: 'cephLogView', + nodename: undefined, + failCallback: function(response) { + var me = this; + var msg = response.htmlStatus; + var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.loadTask.delay(200); + }); + } + ); + if (!windowShow) { + Proxmox.Utils.setErrorMask(me, msg); + } + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.ceph.CephInstallWizard', { + extend: 'PVE.window.Wizard', + alias: 'widget.pveCephInstallWizard', + mixins: ['Proxmox.Mixin.CBind'], + resizable: false, + nodename: undefined, + viewModel: { + data: { + nodename: '', + configuration: true, + isInstalled: false + } + }, + cbindData: { + nodename: undefined + }, + title: gettext('Setup'), + navigateNext: function() { + var tp = this.down('#wizcontent'); + var atab = tp.getActiveTab(); + + var next = tp.items.indexOf(atab) + 1; + var ntab = tp.items.getAt(next); + if (ntab) { + ntab.enable(); + tp.setActiveTab(ntab); + } + }, + setInitialTab: function (index) { + var tp = this.down('#wizcontent'); + var initialTab = tp.items.getAt(index); + initialTab.enable(); + tp.setActiveTab(initialTab); + }, + onShow: function() { + this.callParent(arguments); + var isInstalled = this.getViewModel().get('isInstalled'); + if (isInstalled) { + this.getViewModel().set('configuration', false); + this.setInitialTab(2); + } + }, + items: [ + { + title: gettext('Info'), + xtype: 'panel', + border: false, + bodyBorder: false, + onlineHelp: 'chapter_pveceph', + html: '

Ceph?

'+ + '

"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: '

Installation successful!

'+ + '

The basic installation and configuration is completed, depending on your setup some of the following steps are required to start using Ceph:

'+ + '
  1. Install Ceph on other nodes
  2. '+ + '
  3. Create additional Ceph Monitors
  4. '+ + '
  5. Create Ceph OSDs
  6. '+ + '
  7. Create Ceph Pools
'+ + '

To learn more click on the help button below.

', + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + + var tp = this.up('#wizcontent'); + var idx = tp.items.indexOf(this)-1; + for(;idx >= 0;idx--) { + 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('
'); + } + return Proxmox.Utils.noneText; + } + } + }; + /*jslint confusion: false*/ + + me.callParent(); + me.mon(me.rstore, 'load', me.set_button_status, me); + me.rstore.startUpdate(); + me.load_account(); + } +}); +Ext.define('PVE.node.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.node.Config', + + onlineHelp: 'chapter_system_administration', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + nodename + "/status", + interval: 1000 + }); + + var node_command = function(cmd) { + Proxmox.Utils.API2Request({ + params: { command: cmd }, + url: '/nodes/' + nodename + '/status', + method: 'POST', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var actionBtn = Ext.create('Ext.Button', { + text: gettext('Bulk Actions'), + iconCls: 'fa fa-fw fa-ellipsis-v', + disabled: !caps.nodes['Sys.PowerMgmt'], + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Bulk Start'), + iconCls: 'fa fa-fw fa-play', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Start'), + btnText: gettext('Start'), + action: 'startall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Stop'), + iconCls: 'fa fa-fw fa-stop', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Stop'), + btnText: gettext('Stop'), + action: 'stopall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Migrate'), + iconCls: 'fa fa-fw fa-send-o', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Migrate'), + btnText: gettext('Migrate'), + action: 'migrateall' + }); + win.show(); + } + } + ] + }) + }); + + var restartBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Reboot'), + disabled: !caps.nodes['Sys.PowerMgmt'], + dangerous: true, + confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename), + handler: function() { + node_command('reboot'); + }, + iconCls: 'fa fa-undo' + }); + + var shutdownBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Shutdown'), + disabled: !caps.nodes['Sys.PowerMgmt'], + dangerous: true, + confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename), + handler: function() { + node_command('shutdown'); + }, + iconCls: 'fa fa-power-off' + }); + + var shellBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.nodes['Sys.Console'], + text: gettext('Shell'), + consoleType: 'shell', + nodename: nodename + }); + + me.items = []; + + Ext.apply(me, { + title: gettext('Node') + " '" + nodename + "'", + hstateid: 'nodetab', + defaults: { statusStore: me.statusStore }, + tbar: [ restartBtn, shutdownBtn, shellBtn, actionBtn] + }); + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('Summary'), + iconCls: 'fa fa-book', + itemId: 'summary', + xtype: 'pveNodeSummary' + }, + { + title: gettext('Notes'), + iconCls: 'fa fa-sticky-note-o', + itemId: 'notes', + xtype: 'pveNotesView' + } + ); + } + + if (caps.nodes['Sys.Console']) { + me.items.push( + { + title: gettext('Shell'), + iconCls: 'fa fa-terminal', + itemId: 'jsconsole', + xtype: 'pveNoVncConsole', + consoleType: 'shell', + xtermjs: true, + nodename: nodename + } + ); + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('System'), + iconCls: 'fa fa-cogs', + itemId: 'services', + expandedOnInit: true, + startOnlyServices: { + 'pveproxy': true, + 'pvedaemon': true, + 'pve-cluster': true + }, + nodename: nodename, + onlineHelp: 'pve_service_daemons', + xtype: 'proxmoxNodeServiceView' + }, + { + title: gettext('Network'), + iconCls: 'fa fa-exchange', + itemId: 'network', + groups: ['services'], + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeNetworkView' + }, + { + title: gettext('Certificates'), + iconCls: 'fa fa-certificate', + itemId: 'certificates', + groups: ['services'], + nodename: nodename, + xtype: 'pveCertificatesView' + }, + { + title: gettext('DNS'), + iconCls: 'fa fa-globe', + groups: ['services'], + itemId: 'dns', + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeDNSView' + }, + { + title: gettext('Hosts'), + iconCls: 'fa fa-globe', + groups: ['services'], + itemId: 'hosts', + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeHostsView' + }, + { + title: gettext('Time'), + itemId: 'time', + groups: ['services'], + nodename: nodename, + xtype: 'proxmoxNodeTimeView', + iconCls: 'fa fa-clock-o' + }); + } + + if (caps.nodes['Sys.Syslog']) { + me.items.push({ + title: 'Syslog', + iconCls: 'fa fa-list', + groups: ['services'], + disabled: !caps.nodes['Sys.Syslog'], + itemId: 'syslog', + xtype: 'proxmoxLogView', + url: "/api2/extjs/nodes/" + nodename + "/syslog", + log_select_timespan: 1 + }); + + if (caps.nodes['Sys.Modify']) { + me.items.push({ + title: gettext('Updates'), + iconCls: 'fa fa-refresh', + disabled: !caps.nodes['Sys.Console'], + // do we want to link to system updates instead? + itemId: 'apt', + xtype: 'proxmoxNodeAPT', + upgradeBtn: { + xtype: 'pveConsoleButton', + disabled: Proxmox.UserName !== 'root@pam', + text: gettext('Upgrade'), + consoleType: 'upgrade', + nodename: nodename + }, + nodename: nodename + }); + } + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + xtype: 'pveFirewallRules', + iconCls: 'fa fa-shield', + title: gettext('Firewall'), + allow_iface: true, + base_url: '/nodes/' + nodename + '/firewall/rules', + list_refs_url: '/cluster/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + title: gettext('Options'), + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_host_specific_configuration', + groups: ['firewall'], + base_url: '/nodes/' + nodename + '/firewall/options', + fwtype: 'node', + itemId: 'firewall-options' + }); + } + + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('Disks'), + itemId: 'storage', + expandedOnInit: true, + iconCls: 'fa fa-hdd-o', + xtype: 'pveNodeDiskList' + }, + { + title: 'LVM', + itemId: 'lvm', + onlineHelp: 'chapter_lvm', + iconCls: 'fa fa-square', + groups: ['storage'], + xtype: 'pveLVMList' + }, + { + title: 'LVM-Thin', + itemId: 'lvmthin', + onlineHelp: 'chapter_lvm', + iconCls: 'fa fa-square-o', + groups: ['storage'], + xtype: 'pveLVMThinList' + }, + { + title: Proxmox.Utils.directoryText, + itemId: 'directory', + onlineHelp: 'chapter_storage', + iconCls: 'fa fa-folder', + groups: ['storage'], + xtype: 'pveDirectoryList' + }, + { + title: 'ZFS', + itemId: 'zfs', + onlineHelp: 'chapter_zfs', + iconCls: 'fa fa-th-large', + groups: ['storage'], + xtype: 'pveZFSList' + }, + { + title: 'Ceph', + itemId: 'ceph', + iconCls: 'fa fa-ceph', + xtype: 'pveNodeCephStatus' + }, + { + xtype: 'pveReplicaView', + iconCls: 'fa fa-retweet', + title: gettext('Replication'), + itemId: 'replication' + }, + { + xtype: 'pveNodeCephConfigCrush', + title: gettext('Configuration'), + iconCls: 'fa fa-gear', + groups: ['ceph'], + itemId: 'ceph-config' + }, + { + xtype: 'pveNodeCephMonList', + title: gettext('Monitor'), + iconCls: 'fa fa-tv', + groups: ['ceph'], + itemId: 'ceph-monlist' + }, + { + xtype: 'pveNodeCephOsdTree', + title: 'OSD', + iconCls: 'fa fa-hdd-o', + groups: ['ceph'], + itemId: 'ceph-osdtree' + }, + { + xtype: 'pveNodeCephFSPanel', + title: 'CephFS', + iconCls: 'fa fa-folder', + groups: ['ceph'], + nodename: nodename, + itemId: 'ceph-cephfspanel' + }, + { + xtype: 'pveNodeCephPoolList', + title: 'Pools', + iconCls: 'fa fa-sitemap', + groups: ['ceph'], + itemId: 'ceph-pools' + } + ); + } + + if (caps.nodes['Sys.Syslog']) { + me.items.push( + { + xtype: 'proxmoxLogView', + title: gettext('Log'), + iconCls: 'fa fa-list', + groups: ['firewall'], + onlineHelp: 'chapter_pve_firewall', + url: '/api2/extjs/nodes/' + nodename + '/firewall/log', + itemId: 'firewall-fwlog' + }, + { + title: gettext('Log'), + itemId: 'ceph-log', + iconCls: 'fa fa-list', + groups: ['ceph'], + onlineHelp: 'chapter_pveceph', + xtype: 'cephLogView', + url: "/api2/extjs/nodes/" + nodename + "/ceph/log", + nodename: nodename + }); + } + + me.items.push( + { + title: gettext('Task History'), + iconCls: 'fa fa-list', + itemId: 'tasks', + nodename: nodename, + xtype: 'proxmoxNodeTasks' + }, + { + title: gettext('Subscription'), + iconCls: 'fa fa-support', + itemId: 'support', + xtype: 'pveNodeSubscription', + nodename: nodename + } + ); + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var uptimerec = s.data.get('uptime'); + var powermgmt = uptimerec ? uptimerec.data.value : false; + if (!caps.nodes['Sys.PowerMgmt']) { + powermgmt = false; + } + restartBtn.setDisabled(!powermgmt); + shutdownBtn.setDisabled(!powermgmt); + shellBtn.setDisabled(!powermgmt); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +Ext.define('PVE.window.Migrate', { + extend: 'Ext.window.Window', + + config: { + vmtype: undefined, + nodename: undefined, + vmid: undefined + }, + // private, used to store the migration mode after checking if the guest runs + liveMode: undefined, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=formPanel]': { + validityChange: function(panel, isValid) { + this.lookup('submitButton').setDisabled(!isValid); + } + }, + 'button[reference=submitButton]': { + click: function() { + var me = this; + var view = me.getView(); + + var values = me.lookup('formPanel').getValues(); + var params = { + target: values.target + }; + + if (view.liveMode) { + params[view.liveMode] = 1; + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + view.nodename + '/' + view.vmtype + '/' + view.vmid + '/migrate', + waitMsgTarget: view, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var extraTitle = Ext.String.format(' ({0} ---> {1})', view.nodename, params.target); + + Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + extraTitle: extraTitle + }).show(); + + view.close(); + } + }); + } + } + } + }, + + width: 350, + modal: true, + layout: 'auto', + border: false, + resizable: false, + items: [ + { + xtype: 'form', + reference: 'formPanel', + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + { + xtype: 'pveNodeSelector', + reference: 'pveNodeSelector', + name: 'target', + fieldLabel: gettext('Target node'), + allowBlank: false, + disallowedNodes: undefined, + onlineValidator: true + }, + { + xtype: 'displayfield', + reference: 'migrationMode', + fieldLabel: gettext('Mode'), + value: gettext('Offline') + } + ] + } + ], + buttons: [ + { + xtype: 'proxmoxHelpButton', + reference: 'proxmoxHelpButton', + onlineHelp: 'pct_migration', + listenToGlobalEvent: false, + hidden: false + }, + '->', + { + xtype: 'button', + reference: 'submitButton', + text: gettext('Migrate') + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.vmtype) { + throw "no VM type specified"; + } + + me.callParent(); + + var title = gettext('Migrate') + (' CT ') + me.vmid; + me.liveMode = 'restart'; + + if (me.vmtype === 'qemu') { + me.lookup('proxmoxHelpButton').setHelpConfig({ + onlineHelp: 'qm_migration' + }); + title = gettext('Migrate') + (' VM ') + me.vmid; + me.liveMode = 'online'; + } + + var running = false; + var vmrec = PVE.data.ResourceStore.findRecord('vmid', me.vmid, + 0, false, false, true); + if (vmrec && vmrec.data && vmrec.data.running) { + running = true; + } + + if (running) { + var displayField = me.lookup('migrationMode'); + if (me.vmtype === 'qemu') { + displayField.setValue(gettext('Online')); + me.liveMode = 'online'; + } else { + displayField.setValue(gettext('Restart Mode')); + me.liveMode = 'restart'; + } + } + + me.setTitle(title); + me.lookup('pveNodeSelector').disallowedNodes = [me.nodename]; + me.lookup('formPanel').isValid(); + } +});Ext.define('PVE.window.BulkAction', { + extend: 'Ext.window.Window', + + resizable: true, + width: 800, + modal: true, + layout: { + type: 'fit' + }, + border: false, + + // the action to be set + // currently there are + // startall + // migrateall + // stopall + action: undefined, + + submit: function(params) { + var me = this; + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/' + me.action, + 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(); + me.hide(); + win.on('destroy', function() { + me.close(); + }); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.action) { + throw "no action specified"; + } + + if (!me.btnText) { + throw "no button text specified"; + } + + if (!me.title) { + throw "no title specified"; + } + + var items = []; + + if (me.action === 'migrateall') { + /*jslint confusion: true*/ + /*value is string and number*/ + items.push( + { + xtype: 'pveNodeSelector', + name: 'target', + disallowedNodes: [me.nodename], + fieldLabel: gettext('Target node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'proxmoxintegerfield', + name: 'maxworkers', + minValue: 1, + maxValue: 100, + value: 1, + fieldLabel: gettext('Parallel jobs'), + allowBlank: false + }, + { + itemId: 'lxcwarning', + xtype: 'displayfield', + userCls: 'pve-hint', + value: 'Warning: Running CTs will be migrated in Restart Mode.', + hidden: true // only visible if running container chosen + } + ); + /*jslint confusion: false*/ + } else if (me.action === 'startall') { + items.push({ + xtype: 'hiddenfield', + name: 'force', + value: 1 + }); + } + + items.push({ + xtype: 'vmselector', + itemId: 'vms', + name: 'vms', + flex: 1, + height: 300, + selectAll: true, + allowBlank: false, + nodename: me.nodename, + action: me.action, + listeners: { + selectionchange: function(vmselector, records) { + if (me.action == 'migrateall') { + var showWarning = records.some(function(item) { + return (item.data.type == 'lxc' && + item.data.status == 'running'); + }); + me.down('#lxcwarning').setVisible(showWarning); + } + } + } + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + layout: { + type: 'vbox', + align: 'stretch' + }, + fieldDefaults: { + labelWidth: 300, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn = Ext.create('Ext.Button', { + text: me.btnText, + handler: function() { + form.isValid(); + me.submit(form.getValues()); + } + }); + + Ext.apply(me, { + items: [ me.formPanel ], + buttons: [ submitBtn ] + }); + + me.callParent(); + + form.on('validitychange', function() { + var valid = form.isValid(); + submitBtn.setDisabled(!valid); + }); + form.isValid(); + } +}); +Ext.define('PVE.window.Clone', { + extend: 'Ext.window.Window', + + resizable: false, + + isTemplate: false, + + onlineHelp: 'qm_copy_and_clone', + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=cloneform]': { + validitychange: 'disableSubmit' + } + }, + disableSubmit: function(form) { + this.lookupReference('submitBtn').setDisabled(!form.isValid()); + } + }, + + statics: { + // display a snapshot selector only if needed + wrap: function(nodename, vmid, isTemplate, guestType) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + var snapshotList = response.result.data; + var hasSnapshots = snapshotList.length === 1 && + snapshotList[0].name === 'current' ? false : true; + + Ext.create('PVE.window.Clone', { + nodename: nodename, + guestType: guestType, + vmid: vmid, + isTemplate: isTemplate, + hasSnapshots: hasSnapshots + }).show(); + } + }); + } + }, + + create_clone: function(values) { + var me = this; + + var params = { newid: values.newvmid }; + + if (values.snapname && values.snapname !== 'current') { + params.snapname = values.snapname; + } + + if (values.pool) { + params.pool = values.pool; + } + + if (values.name) { + if (me.guestType === 'lxc') { + params.hostname = values.name; + } else { + params.name = values.name; + } + } + + if (values.target) { + params.target = values.target; + } + + if (values.clonemode === 'copy') { + params.full = 1; + if (values.hdstorage) { + params.storage = values.hdstorage; + if (values.diskformat && me.guestType !== 'lxc') { + params.format = values.diskformat; + } + } + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + + }, + + // disable the Storage selector when clone mode is linked clone + updateVisibility: function() { + var me = this; + var clonemode = me.lookupReference('clonemodesel').getValue(); + var disksel = me.lookup('diskselector'); + disksel.setDisabled(clonemode === 'clone'); + }, + + // add to the list of valid nodes each node where + // all the VM disks are available + verifyFeature: function() { + var me = this; + + var snapname = me.lookupReference('snapshotsel').getValue(); + var clonemode = me.lookupReference('clonemodesel').getValue(); + + var params = { feature: clonemode }; + if (snapname !== 'current') { + params.snapname = snapname; + } + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature', + params: params, + method: 'GET', + failure: function(response, opts) { + me.lookupReference('submitBtn').setDisabled(true); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var res = response.result.data; + + me.lookupReference('targetsel').allowedNodes = res.nodes; + me.lookupReference('targetsel').validate(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.snapname) { + me.snapname = 'current'; + } + + if (!me.guestType) { + throw "no Guest Type specified"; + } + + var titletext = me.guestType === 'lxc' ? 'CT' : 'VM'; + if (me.isTemplate) { + titletext += ' Template'; + } + me.title = "Clone " + titletext + " " + me.vmid; + + var col1 = []; + var col2 = []; + + col1.push({ + xtype: 'pveNodeSelector', + name: 'target', + reference: 'targetsel', + fieldLabel: gettext('Target node'), + selectCurNode: true, + allowBlank: false, + onlineValidator: true, + listeners: { + change: function(f, value) { + me.lookupReference('hdstorage').setTargetNode(value); + } + } + }); + + var modelist = [['copy', gettext('Full Clone')]]; + if (me.isTemplate) { + modelist.push(['clone', gettext('Linked Clone')]); + } + + col1.push({ + xtype: 'pveGuestIDSelector', + name: 'newvmid', + guestType: me.guestType, + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'textfield', + name: 'name', + allowBlank: true, + fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name') + }, + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + } + ); + + col2.push({ + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Mode'), + name: 'clonemode', + reference: 'clonemodesel', + allowBlank: false, + hidden: !me.isTemplate, + value: me.isTemplate ? 'clone' : 'copy', + comboItems: modelist, + listeners: { + change: function(t, value) { + me.updateVisibility(); + me.verifyFeature(); + } + } + }, + { + xtype: 'PVE.form.SnapshotSelector', + name: 'snapname', + reference: 'snapshotsel', + fieldLabel: gettext('Snapshot'), + nodename: me.nodename, + guestType: me.guestType, + vmid: me.vmid, + hidden: me.isTemplate || !me.hasSnapshots ? true : false, + disabled: false, + allowBlank: false, + value : me.snapname, + listeners: { + change: function(f, value) { + me.verifyFeature(); + } + } + }, + { + xtype: 'pveDiskStorageSelector', + reference: 'diskselector', + nodename: me.nodename, + autoSelect: false, + hideSize: true, + hideSelection: true, + storageLabel: gettext('Target Storage'), + allowBlank: true, + storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir', + emptyText: gettext('Same as source'), + disabled: me.isTemplate ? true : false // because default mode is clone for templates + }); + + var formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + reference: 'cloneform', + border: false, + layout: 'column', + defaultType: 'container', + columns: 2, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + { + columnWidth: 0.5, + padding: '0 10 0 0', + layout: 'anchor', + items: col1 + }, + { + columnWidth: 0.5, + padding: '0 0 0 10', + layout: 'anchor', + items: col2 + } + ] + }); + + Ext.apply(me, { + modal: true, + width: 600, + height: 250, + border: false, + layout: 'fit', + buttons: [ { + xtype: 'proxmoxHelpButton', + listenToGlobalEvent: false, + hidden: false, + onlineHelp: me.onlineHelp + }, + '->', + { + reference: 'submitBtn', + text: gettext('Clone'), + disabled: true, + handler: function() { + var cloneForm = me.lookupReference('cloneform'); + if (cloneForm.isValid()) { + me.create_clone(cloneForm.getValues()); + } + } + } ], + items: [ formPanel ] + }); + + me.callParent(); + + me.verifyFeature(); + } +}); +Ext.define('PVE.qemu.Monitor', { + extend: 'Ext.panel.Panel', + + alias: 'widget.pveQemuMonitor', + + maxLines: 500, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var history = []; + var histNum = -1; + var lines = []; + + var textbox = Ext.createWidget('panel', { + region: 'center', + xtype: 'panel', + autoScroll: true, + border: true, + margins: '5 5 5 5', + bodyStyle: 'font-family: monospace;' + }); + + var scrollToEnd = function() { + var el = textbox.getTargetEl(); + var dom = Ext.getDom(el); + + var clientHeight = dom.clientHeight; + // BrowserBug: clientHeight reports 0 in IE9 StrictMode + // Instead we are using offsetHeight and hardcoding borders + if (Ext.isIE9 && Ext.isStrict) { + clientHeight = dom.offsetHeight + 2; + } + dom.scrollTop = dom.scrollHeight - clientHeight; + }; + + var refresh = function() { + textbox.update('
' + 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: [ + '', + ' ({lock})', + '' + ] + }); + + Ext.apply(me, { + title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename), + hstateid: 'kvmtab', + tbarSpacing: false, + tbar: [ statusTxt, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn ], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveQemuSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ] + }); + + if (caps.vms['VM.Console'] && !template) { + me.items.push({ + title: gettext('Console'), + itemId: 'console', + iconCls: 'fa fa-terminal', + xtype: 'pveNoVncConsole', + vmid: vmid, + consoleType: 'kvm', + nodename: nodename + }); + } + + me.items.push( + { + title: gettext('Hardware'), + itemId: 'hardware', + iconCls: 'fa fa-desktop', + xtype: 'PVE.qemu.HardwareView' + }, + { + title: 'Cloud-Init', + itemId: 'cloudinit', + iconCls: 'fa fa-cloud', + xtype: 'pveCiPanel' + }, + { + title: gettext('Options'), + iconCls: 'fa fa-gear', + itemId: 'options', + xtype: 'PVE.qemu.Options' + }, + { + title: gettext('Task History'), + itemId: 'tasks', + xtype: 'proxmoxNodeTasks', + iconCls: 'fa fa-list', + nodename: nodename, + vmidFilter: vmid + } + ); + + if (caps.vms['VM.Monitor'] && !template) { + me.items.push({ + title: gettext('Monitor'), + iconCls: 'fa fa-eye', + itemId: 'monitor', + xtype: 'pveQemuMonitor' + }); + } + + if (caps.vms['VM.Backup']) { + me.items.push({ + title: gettext('Backup'), + iconCls: 'fa fa-floppy-o', + xtype: 'pveBackupView', + itemId: 'backup' + }, + { + title: gettext('Replication'), + iconCls: 'fa fa-retweet', + xtype: 'pveReplicaView', + itemId: 'replication' + }); + } + + if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback']) && !template) { + me.items.push({ + title: gettext('Snapshots'), + iconCls: 'fa fa-history', + xtype: 'pveQemuSnapshotTree', + itemId: 'snapshot' + }); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + iconCls: 'fa fa-shield', + allow_iface: true, + base_url: base_url + '/firewall/rules', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + groups: ['firewall'], + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_vm_container_configuration', + title: gettext('Options'), + base_url: base_url + '/firewall/options', + fwtype: 'vm', + itemId: 'firewall-options' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: base_url + '/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: gettext('IPSet'), + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: base_url + '/firewall/ipset', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall-ipset' + }, + { + title: gettext('Log'), + groups: ['firewall'], + iconCls: 'fa fa-list', + onlineHelp: 'chapter_pve_firewall', + itemId: 'firewall-fwlog', + xtype: 'proxmoxLogView', + url: '/api2/extjs' + base_url + '/firewall/log' + } + ); + } + + if (caps.vms['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/vms/' + vmid + }); + } + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var status; + var qmpstatus; + var spice = false; + var xtermjs = false; + var lock; + + if (!success) { + status = qmpstatus = 'unknown'; + } else { + var rec = s.data.get('status'); + status = rec ? rec.data.value : 'unknown'; + rec = s.data.get('qmpstatus'); + qmpstatus = rec ? rec.data.value : 'unknown'; + rec = s.data.get('template'); + template = rec.data.value || false; + rec = s.data.get('lock'); + lock = rec ? rec.data.value : undefined; + + spice = s.data.get('spice') ? true : false; + xtermjs = s.data.get('serial') ? true : false; + + } + + if (template) { + return; + } + + var resume = (['prelaunch', 'paused', 'suspended'].indexOf(qmpstatus) !== -1); + + if (resume || lock === 'suspended') { + startBtn.setVisible(false); + resumeBtn.setVisible(true); + } else { + startBtn.setVisible(true); + resumeBtn.setVisible(false); + } + + consoleBtn.setEnableSpice(spice); + consoleBtn.setEnableXtermJS(xtermjs); + + statusTxt.update({ lock: lock }); + + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template); + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + consoleBtn.setDisabled(template); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.qemu.CreateWizard', { + extend: 'PVE.window.Wizard', + alias: 'widget.pveQemuCreateWizard', + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + nodename: '', + current: { + scsihw: '' + } + } + }, + + cbindData: { + nodename: undefined + }, + + subject: gettext('Virtual Machine'), + + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + onlineHelp: 'qm_general_settings', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'nodename', + cbind: { + selectCurNode: '{!nodename}', + preferredValue: '{nodename}' + }, + bind: { + value: '{nodename}' + }, + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'pveGuestIDSelector', + name: 'vmid', + guestType: 'qemu', + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'textfield', + name: 'name', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Name'), + allowBlank: true + } + ], + column2: [ + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + } + ], + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Start at boot') + } + ], + advancedColumn2: [ + { + xtype: 'textfield', + name: 'order', + defaultValue: '', + emptyText: 'any', + labelWidth: 120, + fieldLabel: gettext('Start/Shutdown order') + }, + { + xtype: 'textfield', + name: 'up', + defaultValue: '', + emptyText: 'default', + labelWidth: 120, + fieldLabel: gettext('Startup delay') + }, + { + xtype: 'textfield', + name: 'down', + defaultValue: '', + emptyText: 'default', + labelWidth: 120, + fieldLabel: gettext('Shutdown timeout') + } + ], + onGetValues: function(values) { + + ['name', 'pool', 'onboot', 'agent'].forEach(function(field) { + if (!values[field]) { + delete values[field]; + } + }); + + var res = PVE.Parser.printStartup({ + order: values.order, + up: values.up, + down: values.down + }); + + if (res) { + values.startup = res; + } + + delete values.order; + delete values.up; + delete values.down; + + return values; + } + }, + { + xtype: 'container', + layout: 'hbox', + defaults: { + flex: 1, + padding: '0 10' + }, + title: gettext('OS'), + items: [ + { + xtype: 'pveQemuCDInputPanel', + bind: { + nodename: '{nodename}' + }, + confid: 'ide2', + insideWizard: true + }, + { + xtype: 'pveQemuOSTypePanel', + insideWizard: true + } + ] + }, + { + xtype: 'pveQemuSystemPanel', + title: gettext('System'), + isCreate: true, + insideWizard: true + }, + { + xtype: 'pveQemuHDInputPanel', + bind: { + nodename: '{nodename}' + }, + title: gettext('Hard Disk'), + isCreate: true, + insideWizard: true + }, + { + xtype: 'pveQemuProcessorPanel', + insideWizard: true, + title: gettext('CPU') + }, + { + xtype: 'pveQemuMemoryPanel', + insideWizard: true, + title: gettext('Memory') + }, + { + xtype: 'pveQemuNetworkInputPanel', + bind: { + nodename: '{nodename}' + }, + title: gettext('Network'), + insideWizard: true + }, + { + title: gettext('Confirm'), + layout: 'fit', + items: [ + { + xtype: 'grid', + store: { + model: 'KeyValue', + sorters: [{ + property : 'key', + direction: 'ASC' + }] + }, + columns: [ + {header: 'Key', width: 150, dataIndex: 'key'}, + {header: 'Value', flex: 1, dataIndex: 'value'} + ] + } + ], + dockedItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'start', + dock: 'bottom', + margin: '5 0 0 0', + boxLabel: gettext('Start after created') + } + ], + listeners: { + show: function(panel) { + var kv = this.up('window').getValues(); + var data = []; + Ext.Object.each(kv, function(key, value) { + if (key === 'delete') { // ignore + return; + } + data.push({ key: key, value: value }); + }); + + var summarystore = panel.down('grid').getStore(); + summarystore.suspendEvents(); + summarystore.removeAll(); + summarystore.add(data); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh'); + + } + }, + onSubmit: function() { + var wizard = this.up('window'); + var kv = wizard.getValues(); + delete kv['delete']; + + var nodename = kv.nodename; + delete kv.nodename; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function(response){ + wizard.close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + ] +}); + + + + +Ext.define('PVE.qemu.USBInputPanel', { + extend: 'Proxmox.panel.InputPanel', + mixins: ['Proxmox.Mixin.CBind' ], + + autoComplete: false, + onlineHelp: 'qm_usb_passthrough', + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=usb]': { + change: function(field, newValue, oldValue) { + var hwidfield = this.lookupReference('hwid'); + var portfield = this.lookupReference('port'); + var usb3field = this.lookupReference('usb3'); + if (field.inputValue === 'hostdevice') { + hwidfield.setDisabled(!newValue); + } else if(field.inputValue === 'port') { + portfield.setDisabled(!newValue); + } else if(field.inputValue === 'spice') { + usb3field.setDisabled(newValue); + } + } + }, + 'pveUSBSelector': { + change: function(field, newValue, oldValue) { + var usbval = field.getUSBValue(); + var usb3field = this.lookupReference('usb3'); + var usb3 = /usb3/.test(usbval); + if(usb3 && !usb3field.isDisabled()) { + usb3field.savedVal = usb3field.getValue(); + usb3field.setValue(true); + usb3field.setDisabled(true); + } else if(!usb3 && usb3field.isDisabled()){ + var val = (usb3field.savedVal === undefined)?usb3field.originalValue:usb3field.savedVal; + usb3field.setValue(val); + usb3field.setDisabled(false); + } + } + } + } + }, + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + }, + + onGetValues: function(values) { + var me = this; + if(!me.confid) { + var i; + for (i = 0; i < 6; i++) { + if (!me.vmconfig['usb' + i.toString()]) { + me.confid = 'usb' + i.toString(); + break; + } + } + } + var val = ""; + var type = me.down('radiofield').getGroupValue(); + switch (type) { + case 'spice': + val = 'spice'; break; + case 'hostdevice': + case 'port': + val = me.down('pveUSBSelector[name=' + type + ']').getUSBValue(); + if (!/usb3/.test(val) && me.down('field[name=usb3]').getValue() === true) { + val += ',usb3=1'; + } + break; + default: + throw "invalid type selected"; + } + + values[me.confid] = val; + return values; + }, + + items: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + items:[ + { + name: 'usb', + inputValue: 'spice', + boxLabel: gettext('Spice Port'), + submitValue: false, + checked: true + }, + { + name: 'usb', + inputValue: 'hostdevice', + boxLabel: gettext('Use USB Vendor/Device ID'), + submitValue: false + }, + { + xtype: 'pveUSBSelector', + disabled: true, + type: 'device', + name: 'hostdevice', + cbind: { pveSelNode: '{pveSelNode}' }, + editable: true, + reference: 'hwid', + allowBlank: false, + fieldLabel: 'Choose Device', + labelAlign: 'right', + submitValue: false + }, + { + name: 'usb', + inputValue: 'port', + boxLabel: gettext('Use USB Port'), + submitValue: false + }, + { + xtype: 'pveUSBSelector', + disabled: true, + name: 'port', + cbind: { pveSelNode: '{pveSelNode}' }, + editable: true, + type: 'port', + reference: 'port', + allowBlank: false, + fieldLabel: gettext('Choose Port'), + labelAlign: 'right', + submitValue: false + }, + { + xtype: 'checkbox', + name: 'usb3', + submitValue: false, + reference: 'usb3', + fieldLabel: gettext('Use USB3') + } + ] + } + ] +}); + +Ext.define('PVE.qemu.USBEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('USB Device'), + + + initComponent : function() { + var me = this; + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.USBInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var data = response.result.data[me.confid].split(','); + var port, hostdevice, usb3 = false; + var type = 'spice'; + var i; + for (i = 0; i < data.length; i++) { + if (/^(host=)?(0x)?[a-zA-Z0-9]{4}\:(0x)?[a-zA-Z0-9]{4}$/.test(data[i])) { + hostdevice = data[i]; + hostdevice = hostdevice.replace('host=', '').replace('0x',''); + type = 'hostdevice'; + } else if (/^(host=)?(\d+)\-(\d+(\.\d+)*)$/.test(data[i])) { + port = data[i]; + port = port.replace('host=',''); + type = 'port'; + } + + if (/^usb3=(1|on|true)$/.test(data[i])) { + usb3 = true; + } + } + var values = { + usb : type, + hostdevice: hostdevice, + port: port, + usb3: usb3 + }; + + ipanel.setValues(values); + } + } + }); + } +}); +Ext.define('PVE.qemu.PCIInputPanel', { + extend: 'Proxmox.panel.InputPanel', + + onlineHelp: 'qm_pci_passthrough', + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + + var hostpci = me.vmconfig[me.confid] || ''; + + var values = PVE.Parser.parsePropertyString(hostpci, 'host'); + if (values.host && values.host.length < 6) { // 00:00 format not 00:00.0 + values.host += ".0"; + values.multifunction = true; + } + values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0); + values.pcie = PVE.Parser.parseBoolean(values.pcie, 0); + values.rombar = PVE.Parser.parseBoolean(values.rombar, 1); + + me.setValues(values); + if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) { + // machine is not set to some variant of q35, so we disable pcie + var pcie = me.down('field[name=pcie]'); + pcie.setDisabled(true); + pcie.setBoxLabel(gettext('Q35 only')); + } + + if (values.romfile) { + me.down('field[name=romfile]').setVisible(true); + } + }, + + onGetValues: function(values) { + var me = this; + var ret = {}; + if(!me.confid) { + var i; + for (i = 0; i < 5; i++) { + if (!me.vmconfig['hostpci' + i.toString()]) { + me.confid = 'hostpci' + i.toString(); + break; + } + } + } + if (values.multifunction) { + // modify host to skip the '.X' + values.host = values.host.substring(0,5); + delete values.multifunction; + } + + if (values.rombar) { + delete values.rombar; + } else { + values.rombar = 0; + } + + if (!values.romfile) { + delete values.romfile; + } + + ret[me.confid] = PVE.Parser.printPropertyString(values, 'host'); + return ret; + }, + + initComponent: function() { + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + me.column1 = [ + { + xtype: 'pvePCISelector', + fieldLabel: gettext('Device'), + name: 'host', + nodename: me.nodename, + allowBlank: false, + onLoadCallBack: function(store, records, success) { + if (!success || !records.length) { + return; + } + + var first = records[0]; + if (first.data.iommugroup === -1) { + // no iommu groups + var warning = Ext.create('Ext.form.field.Display', { + columnWidth: 1, + padding: '0 0 10 0', + value: 'No IOMMU detected, please activate it.' + + 'See Documentation for further information.', + userCls: 'pve-hint' + }); + me.items.insert(0, warning); + me.updateLayout(); // insert does not trigger that + } + }, + listeners: { + change: function(pcisel, value) { + if (!value) { + return; + } + var pcidev = pcisel.getStore().getById(value); + var mdevfield = me.down('field[name=mdev]'); + mdevfield.setDisabled(!pcidev || !pcidev.data.mdev); + if (!pcidev) { + return; + } + var id = pcidev.data.id.substring(0,5); // 00:00 + var iommu = pcidev.data.iommugroup; + // try to find out if there are more devices + // in that iommu group + if (iommu !== -1) { + var count = 0; + pcisel.getStore().each(function(record) { + if (record.data.iommugroup === iommu && + record.data.id.substring(0,5) !== id) + { + count++; + return false; + } + }); + var warning = me.down('#iommuwarning'); + if (count && !warning) { + warning = Ext.create('Ext.form.field.Display', { + columnWidth: 1, + padding: '0 0 10 0', + itemId: 'iommuwarning', + value: 'The selected Device is not in a seperate' + + 'IOMMU group, make sure this is intended.', + userCls: 'pve-hint' + }); + me.items.insert(0, warning); + me.updateLayout(); // insert does not trigger that + } else if (!count && warning) { + me.remove(warning); + } + } + if (pcidev.data.mdev) { + mdevfield.setPciID(value); + } + } + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('All Functions'), + name: 'multifunction' + } + ]; + + me.column2 = [ + { + xtype: 'pveMDevSelector', + name: 'mdev', + disabled: true, + fieldLabel: gettext('MDev Type'), + nodename: me.nodename, + listeners: { + change: function(field, value) { + var mf = me.down('field[name=multifunction]'); + if (!!value) { + mf.setValue(false); + } + mf.setDisabled(!!value); + } + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Primary GPU'), + name: 'x-vga' + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'ROM-Bar', + name: 'rombar' + }, + { + xtype: 'displayfield', + submitValue: true, + hidden: true, + fieldLabel: 'ROM-File', + name: 'romfile' + } + ]; + + me.advancedColumn2 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'PCI-Express', + name: 'pcie' + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.PCIEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('PCI Device'), + + + initComponent : function() { + var me = this; + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.PCIInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response) { + ipanel.setVMConfig(response.result.data); + } + }); + } +}); +/*jslint confusion: true */ +Ext.define('PVE.qemu.SerialnputPanel', { + extend: 'Proxmox.panel.InputPanel', + + autoComplete: false, + + setVMConfig: function(vmconfig) { + var me = this, i; + me.vmconfig = vmconfig; + + for (i = 0; i < 4; i++) { + var port = 'serial' + i.toString(); + if (!me.vmconfig[port]) { + me.down('field[name=serialid]').setValue(i); + break; + } + } + + }, + + onGetValues: function(values) { + var me = this; + + var id = 'serial' + values.serialid; + delete values.serialid; + values[id] = 'socket'; + return values; + }, + + items: [ + { + xtype: 'proxmoxintegerfield', + name: 'serialid', + fieldLabel: gettext('Serial Port'), + minValue: 0, + maxValue: 3, + allowBlank: false, + validator: function(id) { + if (!this.rendered) { + return true; + } + var me = this.up('panel'); + if (me.vmconfig !== undefined && Ext.isDefined(me.vmconfig['serial' + id])) { + return "This device is already in use."; + } + return true; + } + } + ] +}); + +Ext.define('PVE.qemu.SerialEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('Serial Port'), + + initComponent : function() { + var me = this; + + // for now create of (socket) serial port only + me.isCreate = true; + + var ipanel = Ext.create('PVE.qemu.SerialnputPanel', {}); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + } + }); + } +}); +Ext.define('PVE.window.IPInfo', { + extend: 'Ext.window.Window', + width: 600, + title: gettext('Guest Agent Network Information'), + height: 300, + layout: { + type: 'fit' + }, + modal: true, + items: [ + { + xtype: 'grid', + emptyText: gettext('No network information'), + columns: [ + { + dataIndex: 'name', + text: gettext('Name'), + flex: 3 + }, + { + dataIndex: 'hardware-address', + text: gettext('MAC address'), + width: 140 + }, + { + dataIndex: 'ip-addresses', + text: gettext('IP address'), + align: 'right', + flex: 4, + renderer: function(val) { + if (!Ext.isArray(val)) { + return ''; + } + var ips = []; + val.forEach(function(ip) { + var addr = ip['ip-address']; + var pref = ip.prefix; + if (addr && pref) { + ips.push(addr + '/' + pref); + } + }); + return ips.join('
'); + } + } + ] + } + ] +}); + +Ext.define('PVE.qemu.AgentIPView', { + extend: 'Ext.container.Container', + xtype: 'pveAgentIPView', + + layout: { + type: 'hbox', + align: 'top' + }, + + nics: [], + + items: [ + { + xtype: 'box', + html: ' IPs' + }, + { + xtype: 'container', + flex: 1, + layout: { + type: 'vbox', + align: 'right', + pack: 'end' + }, + items: [ + { + xtype: 'label', + flex: 1, + itemId: 'ipBox', + style: { + 'text-align': 'right' + } + }, + { + xtype: 'button', + itemId: 'moreBtn', + hidden: true, + ui: 'default-toolbar', + handler: function(btn) { + var me = this.up('pveAgentIPView'); + + var win = Ext.create('PVE.window.IPInfo'); + win.down('grid').getStore().setData(me.nics); + win.show(); + }, + text: gettext('More') + } + ] + } + ], + + getDefaultIps: function(nics) { + var me = this; + var ips = []; + nics.forEach(function(nic) { + if (nic['hardware-address'] && + nic['hardware-address'] != '00:00:00:00:00:00') { + + var nic_ips = nic['ip-addresses'] || []; + nic_ips.forEach(function(ip) { + var p = ip['ip-address']; + // show 2 ips at maximum + if (ips.length < 2) { + ips.push(p); + } + }); + } + }); + + return ips; + }, + + startIPStore: function(store, records, success) { + var me = this; + var agentRec = store.getById('agent'); + /*jslint confusion: true*/ + /* value is number and string */ + me.agent = (agentRec && agentRec.data.value === 1); + me.running = (store.getById('status').data.value === 'running'); + /*jslint confusion: false*/ + + var caps = Ext.state.Manager.get('GuiCap'); + + if (!caps.vms['VM.Monitor']) { + var errorText = gettext("Requires '{0}' Privileges"); + me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor')); + return; + } + + if (me.agent && me.running && me.ipStore.isStopped) { + me.ipStore.startUpdate(); + } else if (me.ipStore.isStopped) { + me.updateStatus(); + } + }, + + updateStatus: function(unsuccessful, defaulttext) { + var me = this; + var text = defaulttext || gettext('No network information'); + var more = false; + if (unsuccessful) { + text = gettext('Guest Agent not running'); + } else if (me.agent && me.running) { + if (Ext.isArray(me.nics) && me.nics.length) { + more = true; + var ips = me.getDefaultIps(me.nics); + if (ips.length !== 0) { + text = ips.join('
'); + } + } else if (me.nics && me.nics.error) { + var msg = gettext('Cannot get info from Guest Agent
Error: {0}'); + text = Ext.String.format(text, me.nics.error.desc); + } + } else if (me.agent) { + text = gettext('Guest Agent not running'); + } else { + text = gettext('No Guest Agent configured'); + } + + var ipBox = me.down('#ipBox'); + ipBox.update(text); + + var moreBtn = me.down('#moreBtn'); + moreBtn.setVisible(more); + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw 'rstore not given'; + } + + if (!me.pveSelNode) { + throw 'pveSelNode not given'; + } + + var nodename = me.pveSelNode.data.node; + var vmid = me.pveSelNode.data.vmid; + + me.ipStore = Ext.create('Proxmox.data.UpdateStore', { + interval: 10000, + storeid: 'pve-qemu-agent-' + vmid, + method: 'POST', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/agent/network-get-interfaces' + } + }); + + me.callParent(); + + me.mon(me.ipStore, 'load', function(store, records, success) { + if (records && records.length) { + me.nics = records[0].data.result; + } else { + me.nics = undefined; + } + me.updateStatus(!success); + }); + + me.on('destroy', me.ipStore.stopUpdate); + + // if we already have info about the vm, use it immediately + if (me.rstore.getCount()) { + me.startIPStore(me.rstore, me.rstore.getData(), false); + } + + // check if the guest agent is there on every statusstore load + me.mon(me.rstore, 'load', me.startIPStore, me); + } +}); +Ext.define('PVE.qemu.CloudInit', { + extend: 'Proxmox.grid.PendingObjectGrid', + xtype: 'pveCiPanel', + + onlineHelp: 'qm_cloud_init', + + tbar: [ + { + xtype: 'proxmoxButton', + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + var me = this.up('grid'); + var warn = gettext('Are you sure you want to remove entry {0}'); + + var entry = rec.data.key; + var msg = Ext.String.format(warn, "'" + + me.renderKey(entry, {}, rec) + "'"); + + return msg; + }, + enableFn: function(record) { + var me = this.up('grid'); + var caps = Ext.state.Manager.get('GuiCap'); + if (me.rows[record.data.key].never_delete || + !caps.vms['VM.Config.Network']) { + return false; + } + + if (record.data.key === 'cipassword' && !record.data.value) { + return false; + } + return true; + }, + handler: function() { + var me = this.up('grid'); + var records = me.getSelection(); + if (!records || !records.length) { + return; + } + + var id = records[0].data.key; + var match = id.match(/^net(\d+)$/); + if (match) { + id = 'ipconfig' + match[1]; + } + + var params = {}; + params['delete'] = id; + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: params, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + callback: function() { + me.reload(); + } + }); + }, + text: gettext('Remove') + }, + { + xtype: 'proxmoxButton', + disabled: true, + handler: function() { + var me = this.up('grid'); + me.run_editor(); + }, + text: gettext('Edit') + }, + '-', + { + xtype: 'button', + itemId: 'savebtn', + text: gettext('Regenerate Image'), + handler: function() { + var me = this.up('grid'); + var eject_params = {}; + var insert_params = {}; + var disk = PVE.Parser.parseQemuDrive(me.ciDriveId, me.ciDrive); + var storage = ''; + var stormatch = disk.file.match(/^([^\:]+)\:/); + if (stormatch) { + storage = stormatch[1]; + } + eject_params[me.ciDriveId] = 'none,media=cdrom'; + insert_params[me.ciDriveId] = storage + ':cloudinit'; + + var failure = function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }; + + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: eject_params, + failure: failure, + callback: function() { + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: insert_params, + failure: failure, + callback: function() { + me.reload(); + } + }); + } + }); + } + } + ], + + border: false, + + set_button_status: function(rstore, records, success) { + if (!success || records.length < 1) { + return; + } + var me = this; + var found; + records.forEach(function(record) { + if (found) { + return; + } + var id = record.data.key; + var value = record.data.value; + var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit"); + if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) { + found = id; + me.ciDriveId = found; + me.ciDrive = value; + } + }); + + me.down('#savebtn').setDisabled(!found); + me.setDisabled(!found); + if (!found) { + me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']); + } else { + me.getView().unmask(); + } + }, + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = rows[key] || {}; + + var icon = ""; + if (rowdef.iconCls) { + icon = ' '; + } + return icon + (rowdef.header || key); + }, + + listeners: { + activate: function () { + var me = this; + me.rstore.startUpdate(); + }, + itemdblclick: function() { + var me = this; + me.run_editor(); + } + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + var caps = Ext.state.Manager.get('GuiCap'); + me.baseurl = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid; + me.url = me.baseurl + '/pending'; + me.editorConfig.url = me.baseurl + '/config'; + me.editorConfig.pveSelNode = me.pveSelNode; + + /*jslint confusion: true*/ + /* editor is string and object */ + me.rows = { + ciuser: { + header: gettext('User'), + iconCls: 'fa fa-user', + never_delete: true, + defaultValue: '', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('User'), + items: [ + { + xtype: 'proxmoxtextfield', + deleteEmpty: true, + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('User'), + name: 'ciuser' + } + ] + } : undefined, + renderer: function(value) { + return value || Proxmox.Utils.defaultText; + } + }, + cipassword: { + header: gettext('Password'), + iconCls: 'fa fa-unlock', + defaultValue: '', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Password'), + items: [ + { + xtype: 'proxmoxtextfield', + inputType: 'password', + deleteEmpty: true, + emptyText: Proxmox.Utils.noneText, + fieldLabel: gettext('Password'), + name: 'cipassword' + } + ] + } : undefined, + renderer: function(value) { + return value || Proxmox.Utils.noneText; + } + }, + searchdomain: { + header: gettext('DNS domain'), + iconCls: 'fa fa-globe', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + never_delete: true, + defaultValue: gettext('use host settings') + }, + nameserver: { + header: gettext('DNS servers'), + iconCls: 'fa fa-globe', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + never_delete: true, + defaultValue: gettext('use host settings') + }, + sshkeys: { + header: gettext('SSH public key'), + iconCls: 'fa fa-key', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.SSHKeyEdit' : undefined, + never_delete: true, + renderer: function(value) { + value = decodeURIComponent(value); + var keys = value.split('\n'); + var text = []; + keys.forEach(function(key) { + if (key.length) { + // First erase all quoted strings (eg. command="foo" + var v = key.replace(/"(?:\\.|[^"\\])*"/g, ''); + // Now try to detect the comment: + var res = v.match(/^\s*(\S+\s+)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)\s+\S+\s+(.*?)\s*$/, ''); + if (res) { + key = Ext.String.htmlEncode(res[2]); + if (res[1]) { + key += ' (' + gettext('with options') + ')'; + } + text.push(key); + return; + } + // Most likely invalid at this point, so just stick to + // the old value. + text.push(Ext.String.htmlEncode(key)); + } + }); + if (text.length) { + return text.join('
'); + } else { + return Proxmox.Utils.noneText; + } + }, + defaultValue: '' + } + }; + var i; + var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) { + var id = record.data.key; + var match = id.match(/^net(\d+)$/); + var val = ''; + if (match) { + val = me.getObjectValue('ipconfig'+match[1], '', pending); + } + return val; + }; + for (i = 0; i < 32; i++) { + // we want to show an entry for every network device + // even if it is empty + me.rows['net' + i.toString()] = { + multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()], + header: gettext('IP Config') + ' (net' + i.toString() +')', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined, + iconCls: 'fa fa-exchange', + renderer: ipconfig_renderer + }; + me.rows['ipconfig' + i.toString()] = { + visible: false + }; + } + /*jslint confusion: false*/ + + PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) { + me.rows[type+id] = { + visible: false + }; + }); + me.callParent(); + me.mon(me.rstore, 'load', me.set_button_status, me); + } +}); +Ext.define('PVE.qemu.CIDriveInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveCIDriveInputPanel', + + insideWizard: false, + + vmconfig: {}, // used to select usused disks + + onGetValues: function(values) { + var me = this; + + var drive = {}; + var params = {}; + drive.file = values.hdstorage + ":cloudinit"; + drive.format = values.diskformat; + params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive); + return params; + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + setVMConfig: function(config) { + var me = this; + me.down('#drive').setVMConfig(config, 'cdrom'); + }, + + initComponent : function() { + var me = this; + + me.drive = {}; + + me.items = [ + { + xtype: 'pveControllerSelector', + noVirtIO: true, + itemId: 'drive', + fieldLabel: gettext('CloudInit Drive'), + name: 'drive' + }, + { + xtype: 'pveDiskStorageSelector', + itemId: 'storselector', + storageContent: 'images', + nodename: me.nodename, + hideSize: true + } + ]; + me.callParent(); + } +}); + +Ext.define('PVE.qemu.CIDriveEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCIDriveEdit', + + isCreate: true, + subject: gettext('CloudInit Drive'), + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.items = [{ + xtype: 'pveCIDriveInputPanel', + itemId: 'cipanel', + nodename: nodename + }]; + + me.callParent(); + + me.load({ + success: function(response, opts) { + me.down('#cipanel').setVMConfig(response.result.data); + } + }); + } +}); +Ext.define('PVE.qemu.SSHKeyInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveQemuSSHKeyInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + if (values.sshkeys) { + values.sshkeys.trim(); + } + if (!values.sshkeys.length) { + values = {}; + values['delete'] = 'sshkeys'; + return values; + } else { + values.sshkeys = encodeURIComponent(values.sshkeys); + } + return values; + }, + + items: [ + { + xtype: 'textarea', + itemId: 'sshkeys', + name: 'sshkeys', + height: 250 + }, + { + xtype: 'filebutton', + itemId: 'filebutton', + name: 'file', + text: gettext('Load SSH Key File'), + fieldLabel: 'test', + listeners: { + change: function(btn, e, value) { + var me = this.up('inputpanel'); + e = e.event; + Ext.Array.each(e.target.files, function(file) { + PVE.Utils.loadSSHKeyFromFile(file, function(res) { + var keysField = me.down('#sshkeys'); + var old = keysField.getValue(); + keysField.setValue(old + res); + }); + }); + btn.reset(); + } + } + } + ], + + initComponent: function() { + var me = this; + + me.callParent(); + if (!window.FileReader) { + me.down('#filebutton').setVisible(false); + } + + } +}); + +Ext.define('PVE.qemu.SSHKeyEdit', { + extend: 'Proxmox.window.Edit', + + width: 800, + + initComponent : function() { + var me = this; + + var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel'); + + Ext.apply(me, { + subject: gettext('SSH Keys'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.create) { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (data.sshkeys) { + data.sshkeys = decodeURIComponent(data.sshkeys); + ipanel.setValues(data); + } + } + }); + } + } +}); +Ext.define('PVE.qemu.IPConfigPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveIPConfigPanel', + + insideWizard: false, + + vmconfig: {}, + + onGetValues: function(values) { + var me = this; + + if (values.ipv4mode !== 'static') { + values.ip = values.ipv4mode; + } + + if (values.ipv6mode !== 'static') { + values.ip6 = values.ipv6mode; + } + + var params = {}; + + var cfg = PVE.Parser.printIPConfig(values); + if (cfg === '') { + params['delete'] = [me.confid]; + } else { + params[me.confid] = cfg; + } + return params; + }, + + setVMConfig: function(config) { + var me = this; + me.vmconfig = config; + }, + + setIPConfig: function(confid, data) { + var me = this; + + me.confid = confid; + + if (data.ip === 'dhcp') { + data.ipv4mode = data.ip; + data.ip = ''; + } else { + data.ipv4mode = 'static'; + } + if (data.ip6 === 'dhcp' || data.ip6 === 'auto') { + data.ipv6mode = data.ip6; + data.ip6 = ''; + } else { + data.ipv6mode = 'static'; + } + + me.ipconfig = data; + me.setValues(me.ipconfig); + }, + + initComponent : function() { + var me = this; + + me.ipconfig = {}; + + me.column1 = [ + { + xtype: 'displayfield', + fieldLabel: gettext('Network Device'), + value: me.netid + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: gettext('IPv4') + ':' + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv4mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: '', + disabled: true, + fieldLabel: gettext('IPv4/CIDR') + }, + { + xtype: 'textfield', + name: 'gw', + value: '', + vtype: 'IPAddress', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')' + } + ]; + + me.column2 = [ + { + xtype: 'displayfield' + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: gettext('IPv6') + ':' + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv6mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip6', + value: '', + vtype: 'IP6CIDRAddress', + disabled: true, + fieldLabel: gettext('IPv6/CIDR') + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: '', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')' + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.IPConfigEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + // convert confid from netX to ipconfigX + var match = me.confid.match(/^net(\d+)$/); + if (match) { + me.netid = me.confid; + me.confid = 'ipconfig' + match[1]; + } + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = me.confid ? false : true; + + var ipanel = Ext.create('PVE.qemu.IPConfigPanel', { + confid: me.confid, + netid: me.netid, + nodename: nodename + }); + + Ext.applyIf(me, { + subject: gettext('Network Config'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + me.vmconfig = response.result.data; + var ipconfig = {}; + var value = me.vmconfig[me.confid]; + if (value) { + ipconfig = PVE.Parser.parseIPConfig(me.confid, value); + if (!ipconfig) { + Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration')); + me.close(); + return; + } + } + ipanel.setIPConfig(me.confid, ipconfig); + ipanel.setVMConfig(me.vmconfig); + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.qemu.SystemInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveQemuSystemPanel', + + onlineHelp: 'qm_system_settings', + + viewModel: { + data: { + efi: false, + addefi: true + }, + + formulas: { + efidisk: function(get) { + return get('efi') && get('addefi'); + } + } + }, + + onGetValues: function(values) { + if (values.vga && values.vga.substr(0,6) === 'serial') { + values['serial' + values.vga.substr(6,1)] = 'socket'; + } + + var efidrive = {}; + if (values.hdimage) { + efidrive.file = values.hdimage; + } else if (values.hdstorage) { + efidrive.file = values.hdstorage + ":1"; + } + + if (values.diskformat) { + efidrive.format = values.diskformat; + } + + delete values.hdimage; + delete values.hdstorage; + delete values.diskformat; + + if (efidrive.file) { + values.efidisk0 = PVE.Parser.printQemuDrive(efidrive); + } + + return values; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + scsihwChange: function(field, value) { + var me = this; + if (me.getView().insideWizard) { + me.getViewModel().set('current.scsihw', value); + } + }, + + biosChange: function(field, value) { + var me = this; + if (me.getView().insideWizard) { + me.getViewModel().set('efi', value === 'ovmf'); + } + }, + + control: { + 'pveScsiHwSelector': { + change: 'scsihwChange' + }, + 'pveQemuBiosSelector': { + change: 'biosChange' + } + } + }, + + column1: [ + { + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + fieldLabel: gettext('Graphic card'), + name: 'vga', + comboItems: PVE.Utils.kvm_vga_driver_array() + }, + { + xtype: 'proxmoxcheckbox', + name: 'agent', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Qemu Agent') + } + ], + + column2: [ + { + xtype: 'pveScsiHwSelector', + name: 'scsihw', + value: '__default__', + bind: { + value: '{current.scsihw}' + }, + fieldLabel: gettext('SCSI Controller') + } + ], + + advancedColumn1: [ + { + xtype: 'pveQemuBiosSelector', + name: 'bios', + value: '__default__', + fieldLabel: 'BIOS' + }, + { + xtype: 'proxmoxcheckbox', + bind: { + value: '{addefi}', + hidden: '{!efi}', + disabled: '{!efi}' + }, + hidden: true, + submitValue: false, + disabled: true, + fieldLabel: gettext('Add EFI Disk') + }, + { + xtype: 'pveDiskStorageSelector', + name: 'efidisk0', + storageContent: 'images', + bind: { + nodename: '{nodename}', + hidden: '{!efi}', + disabled: '{!efidisk}' + }, + autoSelect: false, + disabled: true, + hidden: true, + hideSize: true + } + ], + + advancedColumn2: [ + { + xtype: 'proxmoxKVComboBox', + name: 'machine', + value: '__default__', + fieldLabel: gettext('Machine'), + comboItems: [ + ['__default__', PVE.Utils.render_qemu_machine('')], + ['q35', 'q35'] + ] + } + ] + +}); +Ext.define('PVE.lxc.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveLxcSummary', + + 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: 320, + 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 + "/lxc/" + 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: 320, + 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.lxc.NetworkInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcNetworkInputPanel', + + insideWizard: false, + + onlineHelp: 'pct_container_network', + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + var bridgesel = me.query("[isFormField][name=bridge]")[0]; + bridgesel.setNodename(nodename); + }, + + onGetValues: function(values) { + var me = this; + + var id; + if (me.isCreate) { + id = values.id; + delete values.id; + } else { + id = me.ifname; + } + + if (!id) { + return {}; + } + + var newdata = {}; + + if (values.ipv6mode !== 'static') { + values.ip6 = values.ipv6mode; + } + if (values.ipv4mode !== 'static') { + values.ip = values.ipv4mode; + } + newdata[id] = PVE.Parser.printLxcNetwork(values); + return newdata; + }, + + initComponent : function() { + var me = this; + + var cdata = {}; + + if (me.insideWizard) { + me.ifname = 'net0'; + cdata.name = 'eth0'; + me.dataCache = {}; + } + cdata.firewall = (me.insideWizard || me.isCreate); + + if (!me.dataCache) { + throw "no dataCache specified"; + } + + if (!me.isCreate) { + if (!me.ifname) { + throw "no interface name specified"; + } + if (!me.dataCache[me.ifname]) { + throw "no such interface '" + me.ifname + "'"; + } + + cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]); + } + + var i; + for (i = 0; i < 10; i++) { + if (me.isCreate && !me.dataCache['net'+i.toString()]) { + me.ifname = 'net' + i.toString(); + break; + } + } + + var idselector = { + xtype: 'hidden', + name: 'id', + value: me.ifname + }; + + me.column1 = [ + idselector, + { + xtype: 'textfield', + name: 'name', + fieldLabel: gettext('Name'), + emptyText: '(e.g., eth0)', + allowBlank: false, + value: cdata.name, + validator: function(value) { + var result = ''; + Ext.Object.each(me.dataCache, function(key, netstr) { + if (!key.match(/^net\d+/) || key === me.ifname) { + return; // continue + } + var net = PVE.Parser.parseLxcNetwork(netstr); + if (net.name === value) { + result = "interface name already in use"; + return false; + } + }); + if (result !== '') { + return result; + } + // validator can return bool/string + /*jslint confusion:true*/ + return true; + } + }, + { + xtype: 'textfield', + name: 'hwaddr', + fieldLabel: gettext('MAC address'), + vtype: 'MacAddress', + value: cdata.hwaddr, + allowBlank: true, + emptyText: 'auto' + }, + { + xtype: 'PVE.form.BridgeSelector', + name: 'bridge', + nodename: me.nodename, + fieldLabel: gettext('Bridge'), + value: cdata.bridge, + allowBlank: false + }, + { + xtype: 'pveVlanField', + name: 'tag', + value: cdata.tag + }, + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + minValue: 0, + maxValue: 10*1024, + value: cdata.rate, + emptyText: 'unlimited', + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Firewall'), + name: 'firewall', + value: cdata.firewall + } + ]; + + var dhcp4 = (cdata.ip === 'dhcp'); + if (dhcp4) { + cdata.ip = ''; + cdata.gw = ''; + } + + var auto6 = (cdata.ip6 === 'auto'); + var dhcp6 = (cdata.ip6 === 'dhcp'); + if (auto6 || dhcp6) { + cdata.ip6 = ''; + cdata.gw6 = ''; + } + + me.column2 = [ + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: 'IPv4:' // do not localize + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: !dhcp4, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: 'DHCP', // do not localize + name: 'ipv4mode', + inputValue: 'dhcp', + checked: dhcp4, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: cdata.ip, + disabled: dhcp4, + fieldLabel: 'IPv4/CIDR' // do not localize + }, + { + xtype: 'textfield', + name: 'gw', + value: cdata.gw, + vtype: 'IPAddress', + disabled: dhcp4, + fieldLabel: gettext('Gateway') + ' (IPv4)', + margin: '0 0 3 0' // override bottom margin to account for the menuseparator + }, + { + xtype: 'menuseparator', + height: '3', + margin: '0' + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: 'IPv6:' // do not localize + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: !(auto6 || dhcp6), + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: 'DHCP', // do not localize + name: 'ipv6mode', + inputValue: 'dhcp', + checked: dhcp6, + margin: '0 0 0 10' + }, + { + xtype: 'radiofield', + boxLabel: 'SLAAC', // do not localize + name: 'ipv6mode', + inputValue: 'auto', + checked: auto6, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip6', + value: cdata.ip6, + vtype: 'IP6CIDRAddress', + disabled: (dhcp6 || auto6), + fieldLabel: 'IPv6/CIDR' // do not localize + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: cdata.gw6, + disabled: (dhcp6 || auto6), + fieldLabel: gettext('Gateway') + ' (IPv6)' + } + ]; + + me.callParent(); + } +}); + + +Ext.define('PVE.lxc.NetworkEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent : function() { + var me = this; + + if (!me.dataCache) { + throw "no dataCache specified"; + } + + if (!me.nodename) { + throw "no node name specified"; + } + + var ipanel = Ext.create('PVE.lxc.NetworkInputPanel', { + ifname: me.ifname, + nodename: me.nodename, + dataCache: me.dataCache, + isCreate: me.isCreate + }); + + Ext.apply(me, { + subject: gettext('Network Device') + ' (veth)', + digest: me.dataCache.digest, + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.lxc.NetworkView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveLxcNetworkView', + + onlineHelp: 'pct_container_network', + + dataCache: {}, // used to store result of last load + + stateful: true, + stateId: 'grid-lxc-network', + + load: function() { + var me = this; + + Proxmox.Utils.setErrorMask(me, true); + + Proxmox.Utils.API2Request({ + url: me.url, + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + var result = Ext.decode(response.responseText); + var data = result.data || {}; + me.dataCache = data; + var records = []; + Ext.Object.each(data, function(key, value) { + if (!key.match(/^net\d+/)) { + return; // continue + } + var net = PVE.Parser.parseLxcNetwork(value); + net.id = key; + records.push(net); + }); + me.store.loadData(records); + me.down('button[name=addButton]').setDisabled((records.length >= 10)); + } + }); + }, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + me.url = '/nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var store = new Ext.data.Store({ + model: 'pve-lxc-network', + sorters: [ + { + property : 'id', + direction: 'ASC' + } + ] + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!caps.vms['VM.Config.Network']; + }, + confirmMsg: function (rec) { + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.id + "'"); + }, + handler: function(btn, event, rec) { + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + method: 'PUT', + params: { 'delete': rec.data.id, digest: me.dataCache.digest }, + callback: function() { + me.load(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + if (!caps.vms['VM.Config.Network']) { + return false; + } + + var win = Ext.create('PVE.lxc.NetworkEdit', { + url: me.url, + nodename: nodename, + dataCache: me.dataCache, + ifname: rec.data.id + }); + win.on('destroy', me.load, me); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: sm, + disabled: true, + enableFn: function(rec) { + if (!caps.vms['VM.Config.Network']) { + return false; + } + return true; + }, + handler: run_editor + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + name: 'addButton', + disabled: !caps.vms['VM.Config.Network'], + handler: function() { + var win = Ext.create('PVE.lxc.NetworkEdit', { + url: me.url, + nodename: nodename, + isCreate: true, + dataCache: me.dataCache + }); + win.on('destroy', me.load, me); + win.show(); + } + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: 'ID', + width: 50, + dataIndex: 'id' + }, + { + header: gettext('Name'), + width: 80, + dataIndex: 'name' + }, + { + header: gettext('Bridge'), + width: 80, + dataIndex: 'bridge' + }, + { + header: gettext('Firewall'), + width: 80, + dataIndex: 'firewall', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('VLAN Tag'), + width: 80, + dataIndex: 'tag' + }, + { + header: gettext('MAC address'), + width: 110, + dataIndex: 'hwaddr' + }, + { + header: gettext('IP address'), + width: 150, + dataIndex: 'ip', + renderer: function(value, metaData, rec) { + if (rec.data.ip && rec.data.ip6) { + return rec.data.ip + "
" + rec.data.ip6; + } else if (rec.data.ip6) { + return rec.data.ip6; + } else { + return rec.data.ip; + } + } + }, + { + header: gettext('Gateway'), + width: 150, + dataIndex: 'gw', + renderer: function(value, metaData, rec) { + if (rec.data.gw && rec.data.gw6) { + return rec.data.gw + "
" + rec.data.gw6; + } else if (rec.data.gw6) { + return rec.data.gw6; + } else { + return rec.data.gw; + } + } + } + ], + listeners: { + activate: me.load, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-lxc-network', { + extend: "Ext.data.Model", + proxy: { type: 'memory' }, + fields: [ 'id', 'name', 'hwaddr', 'bridge', + 'ip', 'gw', 'ip6', 'gw6', 'tag', 'firewall' ] + }); + +}); + +/*jslint confusion: true */ +Ext.define('PVE.lxc.RessourceView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcRessourceView'], + + onlineHelp: 'pct_configuration', + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rowdef = me.rows[key] || {}; + + metaData.tdAttr = "valign=middle"; + if (rowdef.tdCls) { + metaData.tdCls = rowdef.tdCls; + } + return rowdef.header || key; + }, + + 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']; + + var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined; + + var rows = { + memory: { + header: gettext('Memory'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, + defaultValue: 512, + tdCls: 'pve-itype-icon-memory', + group: 1, + renderer: function(value) { + return Proxmox.Utils.format_size(value*1024*1024); + } + }, + swap: { + header: gettext('Swap'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, + defaultValue: 512, + tdCls: 'pve-itype-icon-swap', + group: 2, + renderer: function(value) { + return Proxmox.Utils.format_size(value*1024*1024); + } + }, + cores: { + header: gettext('Cores'), + editor: caps.vms['VM.Config.CPU'] ? 'PVE.lxc.CPUEdit' : undefined, + defaultValue: '', + tdCls: 'pve-itype-icon-processor', + group: 3, + renderer: function(value) { + var cpulimit = me.getObjectValue('cpulimit'); + var cpuunits = me.getObjectValue('cpuunits'); + var res; + if (value) { + res = value; + } else { + res = gettext('unlimited'); + } + + if (cpulimit) { + res += ' [cpulimit=' + cpulimit + ']'; + } + + if (cpuunits) { + res += ' [cpuunits=' + cpuunits + ']'; + } + return res; + } + }, + rootfs: { + header: gettext('Root Disk'), + defaultValue: Proxmox.Utils.noneText, + editor: mpeditor, + tdCls: 'pve-itype-icon-storage', + group: 4 + }, + cpulimit: { + visible: false + }, + cpuunits: { + visible: false + }, + unprivileged: { + visible: false + } + }; + + PVE.Utils.forEachMP(function(bus, i) { + confid = bus + i; + var group = 5; + var header; + if (bus === 'mp') { + header = gettext('Mount Point') + ' (' + confid + ')'; + } else { + header = gettext('Unused Disk') + ' ' + i; + group += 1; + } + rows[confid] = { + group: group, + order: i, + tdCls: 'pve-itype-icon-storage', + editor: mpeditor, + header: header + }; + }, true); + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + me.selModel = Ext.create('Ext.selection.RowModel', {}); + + var run_resize = function() { + var rec = me.selModel.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.MPResize', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid + }); + + win.show(); + }; + + var run_remove = function(b, e, rec) { + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'delete': rec.data.key + }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var run_move = function(b, e, rec) { + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.HDMove', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'lxc' + }); + + win.show(); + + win.on('destroy', me.reload, me); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: me.selModel, + disabled: true, + enableFn: function(rec) { + if (!rec) { + return false; + } + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: function() { me.run_editor(); } + }); + + var resize_btn = new Proxmox.button.Button({ + text: gettext('Resize disk'), + selModel: me.selModel, + disabled: true, + handler: run_resize + }); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + selModel: me.selModel, + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + me.renderKey(rec.data.key, {}, rec) + "'"); + if (rec.data.key.match(/^unused\d+$/)) { + msg += " " + gettext('This will permanently erase all data.'); + } + + return msg; + }, + handler: run_remove + }); + + var move_btn = new Proxmox.button.Button({ + text: gettext('Move Volume'), + selModel: me.selModel, + disabled: true, + dangerous: true, + handler: run_move + }); + + var set_button_status = function() { + var rec = me.selModel.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + remove_btn.disable(); + resize_btn.disable(); + return; + } + var key = rec.data.key; + var value = rec.data.value; + var rowdef = rows[key]; + + var isDisk = (rowdef.tdCls == 'pve-itype-icon-storage'); + + var noedit = rec.data['delete'] || !rowdef.editor; + if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) { + var mp = PVE.Parser.parseLxcMountPoint(value); + if (mp.type !== 'volume') { + noedit = true; + } + } + edit_btn.setDisabled(noedit); + + remove_btn.setDisabled(!isDisk || rec.data.key === 'rootfs' || !diskCap); + resize_btn.setDisabled(!isDisk || !diskCap); + move_btn.setDisabled(!isDisk || !diskCap); + + }; + + 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; + } + }; + + Ext.apply(me, { + url: '/api2/json/' + baseurl, + selModel: me.selModel, + interval: 2000, + cwidth1: 170, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Mount Point'), + iconCls: 'pve-itype-icon-storage', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.lxc.MountPointEdit', { + url: '/api2/extjs/' + baseurl, + unprivileged: me.getObjectValue('unprivileged'), + pveSelNode: me.pveSelNode + }); + win.show(); + } + } + ] + }) + }, + edit_btn, + remove_btn, + resize_btn, + move_btn + ], + rows: rows, + sorterFn: sorterFn, + editorConfig: { + pveSelNode: me.pveSelNode, + url: '/api2/extjs/' + baseurl + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.lxc.FeaturesInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveLxcFeaturesInputPanel', + + // used to save the mounts fstypes until sending + mounts: [], + + fstypes: ['nfs', 'cifs'], + + viewModel: { + parent: null, + data: { + unprivileged: false + }, + formulas: { + privilegedOnly: function(get) { + return (get('unprivileged') ? gettext('privileged only') : ''); + }, + unprivilegedOnly: function(get) { + return (!get('unprivileged') ? gettext('unprivileged only') : ''); + } + } + }, + + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('keyctl'), + name: 'keyctl', + bind: { + disabled: '{!unprivileged}', + boxLabel: '{unprivilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Nesting'), + name: 'nesting' + }, + { + xtype: 'proxmoxcheckbox', + name: 'nfs', + fieldLabel: 'NFS', + bind: { + disabled: '{unprivileged}', + boxLabel: '{privilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'cifs', + fieldLabel: 'CIFS', + bind: { + disabled: '{unprivileged}', + boxLabel: '{privilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'fuse', + fieldLabel: 'FUSE' + } + ], + + onGetValues: function(values) { + var me = this; + var mounts = me.mounts; + me.fstypes.forEach(function(fs) { + if (values[fs]) { + mounts.push(fs); + } + delete values[fs]; + }); + + if (mounts.length) { + values.mount = mounts.join(';'); + } + + var featuresstring = PVE.Parser.printPropertyString(values, undefined); + if (featuresstring == '') { + return { 'delete': 'features' }; + } + return { features: featuresstring }; + }, + + setValues: function(values) { + var me = this; + + me.viewModel.set({ unprivileged: values.unprivileged }); + + if (values.features) { + var res = PVE.Parser.parsePropertyString(values.features); + me.mounts = []; + if (res.mount) { + res.mount.split(/[; ]/).forEach(function(item) { + if (me.fstypes.indexOf(item) === -1) { + me.mounts.push(item); + } else { + res[item] = 1; + } + }); + } + this.callParent([res]); + } + } +}); + +Ext.define('PVE.lxc.FeaturesEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveLxcFeaturesEdit', + + subject: gettext('Features'), + + items: [{ + xtype: 'pveLxcFeaturesInputPanel' + }], + + initComponent : function() { + var me = this; + + me.callParent(); + + me.load(); + } +}); +/*jslint confusion: true */ +Ext.define('PVE.lxc.Options', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcOptions'], + + onlineHelp: 'pct_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 = { + onboot: { + header: gettext('Start at boot'), + defaultValue: '', + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Start at boot'), + items: { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + fieldLabel: gettext('Start at boot') + } + } : undefined + }, + startup: { + header: gettext('Start/Shutdown order'), + defaultValue: '', + renderer: PVE.Utils.render_kvm_startup, + editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ? + { + xtype: 'pveWindowStartupEdit', + onlineHelp: 'pct_startup_and_shutdown' + } : undefined + }, + ostype: { + header: gettext('OS Type'), + defaultValue: Proxmox.Utils.unknownText + }, + arch: { + header: gettext('Architecture'), + defaultValue: Proxmox.Utils.unknownText + }, + console: { + header: '/dev/console', + defaultValue: 1, + renderer: Proxmox.Utils.format_enabled_toggle, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: '/dev/console', + items: { + xtype: 'proxmoxcheckbox', + name: 'console', + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + checked: true, + fieldLabel: '/dev/console' + } + } : undefined + }, + tty: { + header: gettext('TTY count'), + defaultValue: 2, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('TTY count'), + items: { + xtype: 'proxmoxintegerfield', + name: 'tty', + minValue: 0, + maxValue: 6, + value: 2, + fieldLabel: gettext('TTY count'), + emptyText: gettext('Default'), + deleteEmpty: true + } + } : undefined + }, + cmode: { + header: gettext('Console mode'), + defaultValue: 'tty', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Console mode'), + items: { + xtype: 'proxmoxKVComboBox', + name: 'cmode', + deleteEmpty: true, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + " (tty)"], + ['tty', "/dev/tty[X]"], + ['console', "/dev/console"], + ['shell', "shell"] + ], + fieldLabel: gettext('Console mode') + } + } : undefined + }, + protection: { + header: gettext('Protection'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Protection'), + items: { + xtype: 'proxmoxcheckbox', + name: 'protection', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + unprivileged: { + header: gettext('Unprivileged container'), + renderer: Proxmox.Utils.format_boolean, + defaultValue: 0 + }, + features: { + header: gettext('Features'), + defaultValue: Proxmox.Utils.noneText, + editor: Proxmox.UserName === 'root@pam' ? + 'PVE.lxc.FeaturesEdit' : undefined + }, + hookscript: { + header: gettext('Hookscript') + } + }; + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: function() { me.run_editor(); } + }); + + Ext.apply(me, { + url: "/api2/json/" + baseurl, + selModel: sm, + interval: 5000, + tbar: [ edit_btn ], + rows: rows, + editorConfig: { + url: '/api2/extjs/' + baseurl + }, + listeners: { + itemdblclick: me.run_editor + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + } +}); + +Ext.define('PVE.lxc.DNSInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcDNSInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var deletes = []; + if (!values.searchdomain && !me.insideWizard) { + deletes.push('searchdomain'); + } + + if (values.nameserver) { + var list = values.nameserver.split(/[\ \,\;]+/); + values.nameserver = list.join(' '); + } else if(!me.insideWizard) { + deletes.push('nameserver'); + } + + if (deletes.length) { + values['delete'] = deletes.join(','); + } + + return values; + }, + + initComponent : function() { + var me = this; + + var items = [ + { + xtype: 'proxmoxtextfield', + name: 'searchdomain', + skipEmptyText: true, + fieldLabel: gettext('DNS domain'), + emptyText: gettext('use host settings'), + allowBlank: true + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS servers'), + vtype: 'IP64AddressList', + allowBlank: true, + emptyText: gettext('use host settings'), + name: 'nameserver', + itemId: 'nameserver' + } + ]; + + if (me.insideWizard) { + me.column1 = items; + } else { + me.items = items; + } + + me.callParent(); + } +}); + +Ext.define('PVE.lxc.DNSEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + var ipanel = Ext.create('PVE.lxc.DNSInputPanel'); + + Ext.apply(me, { + subject: gettext('Resources'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + if (values.nameserver) { + values.nameserver.replace(/[,;]/, ' '); + values.nameserver.replace(/^\s+/, ''); + } + + ipanel.setValues(values); + } + }); + } + } +}); + +/*jslint confusion: true */ +Ext.define('PVE.lxc.DNS', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcDNS'], + + onlineHelp: 'pct_container_network', + + 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 = { + hostname: { + required: true, + defaultValue: me.pveSelNode.data.name, + header: gettext('Hostname'), + editor: caps.vms['VM.Config.Network'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Hostname'), + items: { + xtype: 'inputpanel', + items:{ + fieldLabel: gettext('Hostname'), + xtype: 'textfield', + name: 'hostname', + vtype: 'DnsName', + allowBlank: true, + emptyText: 'CT' + vmid.toString() + }, + onGetValues: function(values) { + var params = values; + if (values.hostname === undefined || + values.hostname === null || + values.hostname === '') { + params = { hostname: 'CT'+vmid.toString()}; + } + return params; + } + } + } : undefined + }, + searchdomain: { + header: gettext('DNS domain'), + defaultValue: '', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + renderer: function(value) { + return value || gettext('use host settings'); + } + }, + nameserver: { + header: gettext('DNS server'), + defaultValue: '', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + renderer: function(value) { + return value || gettext('use host settings'); + } + } + }; + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var reload = function() { + me.rstore.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var win; + if (Ext.isString(rowdef.editor)) { + win = Ext.create(rowdef.editor, { + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + 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.load(); + win.show(); + win.on('destroy', reload); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: run_editor + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + var rowdef = rows[rec.data.key]; + edit_btn.setDisabled(!rowdef.editor); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/config", + selModel: sm, + cwidth1: 150, + run_editor: run_editor, + tbar: [ edit_btn ], + rows: rows, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status, + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.lxc.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.lxc.Config', + + onlineHelp: 'chapter_pct', + + 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 + '/lxc/' + vmid; + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json' + base_url + '/status/current', + interval: 1000 + }); + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: base_url + "/status/" + cmd, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var startBtn = Ext.create('Ext.Button', { + text: gettext('Start'), + disabled: !caps.vms['VM.PowerMgmt'] || running, + hidden: template, + handler: function() { + vm_command('start'); + }, + iconCls: 'fa fa-play' + }); + + var stopBtn = Ext.create('Ext.menu.Item',{ + text: gettext('Stop'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid), + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'), + dangerous: true, + handler: function() { + vm_command("stop"); + }, + iconCls: 'fa fa-stop' + }); + + var shutdownBtn = Ext.create('PVE.button.Split', { + text: gettext('Shutdown'), + disabled: !caps.vms['VM.PowerMgmt'] || !running, + hidden: template, + confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid), + handler: function() { + vm_command('shutdown'); + }, + menu: { + items:[stopBtn] + }, + iconCls: 'fa fa-power-off' + }); + + 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: 'lxc', + 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, 'lxc'); + } + }, + { + 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('vztemplate', 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, + guestType: 'ct', + isCreate: (!ha || ha === 'unmanaged') + }).show(); + } + }, + { + text: gettext('Remove'), + disabled: !caps.vms['VM.Allocate'], + itemId: 'removeBtn', + handler: function() { + Ext.create('PVE.window.SafeDestroy', { + url: base_url, + item: { type: 'CT', id: vmid } + }).show(); + }, + iconCls: 'fa fa-trash-o' + } + ]} + }); + + var vm = me.pveSelNode.data; + + var consoleBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.vms['VM.Console'], + consoleType: 'lxc', + consoleName: vm.name, + hidden: template, + nodename: nodename, + vmid: vmid + }); + + var statusTxt = Ext.create('Ext.toolbar.TextItem', { + data: { + lock: undefined + }, + tpl: [ + '', + ' ({lock})', + '' + ] + }); + + + Ext.apply(me, { + title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename), + hstateid: 'lxctab', + tbarSpacing: false, + tbar: [ statusTxt, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn ], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveLxcSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ] + }); + + if (caps.vms['VM.Console'] && !template) { + me.items.push( + { + title: gettext('Console'), + itemId: 'consolejs', + iconCls: 'fa fa-terminal', + xtype: 'pveNoVncConsole', + vmid: vmid, + consoleType: 'lxc', + xtermjs: true, + nodename: nodename + } + ); + } + + me.items.push( + { + title: gettext('Resources'), + itemId: 'resources', + expandedOnInit: true, + iconCls: 'fa fa-cube', + xtype: 'pveLxcRessourceView' + }, + { + title: gettext('Network'), + iconCls: 'fa fa-exchange', + itemId: 'network', + xtype: 'pveLxcNetworkView' + }, + { + title: gettext('DNS'), + iconCls: 'fa fa-globe', + itemId: 'dns', + xtype: 'pveLxcDNS' + }, + { + title: gettext('Options'), + itemId: 'options', + iconCls: 'fa fa-gear', + xtype: 'pveLxcOptions' + }, + { + title: gettext('Task History'), + itemId: 'tasks', + iconCls: 'fa fa-list', + xtype: 'proxmoxNodeTasks', + nodename: nodename, + vmidFilter: vmid + } + ); + + if (caps.vms['VM.Backup']) { + me.items.push({ + title: gettext('Backup'), + iconCls: 'fa fa-floppy-o', + xtype: 'pveBackupView', + itemId: 'backup' + }, + { + title: gettext('Replication'), + iconCls: 'fa fa-retweet', + xtype: 'pveReplicaView', + itemId: 'replication' + }); + } + + if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback']) && !template) { + me.items.push({ + title: gettext('Snapshots'), + iconCls: 'fa fa-history', + xtype: 'pveLxcSnapshotTree', + itemId: 'snapshot' + }); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + iconCls: 'fa fa-shield', + allow_iface: true, + base_url: base_url + '/firewall/rules', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + groups: ['firewall'], + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_vm_container_configuration', + title: gettext('Options'), + base_url: base_url + '/firewall/options', + fwtype: 'vm', + itemId: 'firewall-options' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: base_url + '/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: gettext('IPSet'), + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: base_url + '/firewall/ipset', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall-ipset' + }, + { + title: gettext('Log'), + groups: ['firewall'], + iconCls: 'fa fa-list', + onlineHelp: 'chapter_pve_firewall', + itemId: 'firewall-fwlog', + xtype: 'proxmoxLogView', + url: '/api2/extjs' + base_url + '/firewall/log' + } + ); + } + + if (caps.vms['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + itemId: 'permissions', + iconCls: 'fa fa-unlock', + path: '/vms/' + vmid + }); + } + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var status; + var lock; + if (!success) { + status = 'unknown'; + } else { + var rec = s.data.get('status'); + status = rec ? rec.data.value : 'unknown'; + rec = s.data.get('template'); + template = rec.data.value || false; + rec = s.data.get('lock'); + lock = rec ? rec.data.value : undefined; + } + + statusTxt.update({ lock: lock }); + + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template); + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + stopBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'stopped'); + me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + consoleBtn.setDisabled(template); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.lxc.CreateWizard', { + extend: 'PVE.window.Wizard', + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + nodename: '', + storage: '', + unprivileged: true + } + }, + + cbindData: { + nodename: undefined + }, + + subject: gettext('LXC Container'), + + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + onlineHelp: 'pct_general', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'nodename', + cbind: { + selectCurNode: '{!nodename}', + preferredValue: '{nodename}' + }, + bind: { + value: '{nodename}' + }, + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'pveGuestIDSelector', + name: 'vmid', // backend only knows vmid + guestType: 'lxc', + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'proxmoxtextfield', + name: 'hostname', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Hostname'), + skipEmptyText: true, + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + name: 'unprivileged', + value: true, + bind: { + value: '{unprivileged}' + }, + fieldLabel: gettext('Unprivileged container') + } + ], + column2: [ + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + }, + { + xtype: 'textfield', + inputType: 'password', + name: 'password', + value: '', + fieldLabel: gettext('Password'), + allowBlank: false, + minLength: 5, + change: function(f, value) { + if (f.rendered) { + f.up().down('field[name=confirmpw]').validate(); + } + } + }, + { + xtype: 'textfield', + inputType: 'password', + name: 'confirmpw', + value: '', + fieldLabel: gettext('Confirm password'), + allowBlank: true, + submitValue: false, + validator: function(value) { + var pw = this.up().down('field[name=password]').getValue(); + if (pw !== value) { + return "Passwords do not match!"; + } + return true; + } + }, + { + xtype: 'proxmoxtextfield', + name: 'ssh-public-keys', + value: '', + fieldLabel: gettext('SSH public key'), + allowBlank: true, + validator: function(value) { + var pwfield = this.up().down('field[name=password]'); + if (value.length) { + var key = PVE.Parser.parseSSHKey(value); + if (!key) { + return "Failed to recognize ssh key"; + } + pwfield.allowBlank = true; + } else { + pwfield.allowBlank = false; + } + pwfield.validate(); + return true; + }, + afterRender: function() { + if (!window.FileReader) { + // No FileReader support in this browser + return; + } + var cancel = function(ev) { + ev = ev.event; + if (ev.preventDefault) { + ev.preventDefault(); + } + }; + var field = this; + field.inputEl.on('dragover', cancel); + field.inputEl.on('dragenter', cancel); + field.inputEl.on('drop', function(ev) { + ev = ev.event; + if (ev.preventDefault) { + ev.preventDefault(); + } + var files = ev.dataTransfer.files; + PVE.Utils.loadSSHKeyFromFile(files[0], function(v) { + field.setValue(v); + }); + }); + } + }, + { + xtype: 'filebutton', + name: 'file', + hidden: !window.FileReader, + text: gettext('Load SSH Key File'), + listeners: { + change: function(btn, e, value) { + e = e.event; + var field = this.up().down('proxmoxtextfield[name=ssh-public-keys]'); + PVE.Utils.loadSSHKeyFromFile(e.target.files[0], function(v) { + field.setValue(v); + }); + btn.reset(); + } + } + } + ] + }, + { + xtype: 'inputpanel', + title: gettext('Template'), + onlineHelp: 'pct_container_images', + column1: [ + { + xtype: 'pveStorageSelector', + name: 'tmplstorage', + fieldLabel: gettext('Storage'), + storageContent: 'vztmpl', + autoSelect: true, + allowBlank: false, + bind: { + value: '{storage}', + nodename: '{nodename}' + } + }, + { + xtype: 'pveFileSelector', + name: 'ostemplate', + storageContent: 'vztmpl', + fieldLabel: gettext('Template'), + bind: { + storage: '{storage}', + nodename: '{nodename}' + }, + allowBlank: false + } + ] + }, + { + xtype: 'pveLxcMountPointInputPanel', + title: gettext('Root Disk'), + insideWizard: true, + isCreate: true, + unused: false, + bind: { + nodename: '{nodename}', + unprivileged: '{unprivileged}' + }, + confid: 'rootfs' + }, + { + xtype: 'pveLxcCPUInputPanel', + title: gettext('CPU'), + insideWizard: true + }, + { + xtype: 'pveLxcMemoryInputPanel', + title: gettext('Memory'), + insideWizard: true + }, + { + xtype: 'pveLxcNetworkInputPanel', + title: gettext('Network'), + insideWizard: true, + bind: { + nodename: '{nodename}' + }, + isCreate: true + }, + { + xtype: 'pveLxcDNSInputPanel', + title: gettext('DNS'), + insideWizard: true + }, + { + title: gettext('Confirm'), + layout: 'fit', + items: [ + { + xtype: 'grid', + store: { + model: 'KeyValue', + sorters: [{ + property : 'key', + direction: 'ASC' + }] + }, + columns: [ + {header: 'Key', width: 150, dataIndex: 'key'}, + {header: 'Value', flex: 1, dataIndex: 'value'} + ] + } + ], + dockedItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'start', + dock: 'bottom', + margin: '5 0 0 0', + boxLabel: gettext('Start after created') + } + ], + listeners: { + show: function(panel) { + var wizard = this.up('window'); + var kv = wizard.getValues(); + var data = []; + Ext.Object.each(kv, function(key, value) { + if (key === 'delete' || key === 'tmplstorage') { // ignore + return; + } + if (key === 'password') { // don't show pw + return; + } + var html = Ext.htmlEncode(Ext.JSON.encode(value)); + data.push({ key: key, value: value }); + }); + + var summarystore = panel.down('grid').getStore(); + summarystore.suspendEvents(); + summarystore.removeAll(); + summarystore.add(data); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh'); + } + }, + onSubmit: function() { + var wizard = this.up('window'); + var kv = wizard.getValues(); + delete kv['delete']; + + var nodename = kv.nodename; + delete kv.nodename; + delete kv.tmplstorage; + + if (!kv.pool.length) { + delete kv.pool; + } + + if (!kv.password.length && kv['ssh-public-keys']) { + delete kv.password; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/lxc', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function(response, opts){ + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + wizard.close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + ] +}); + + + +Ext.define('PVE.lxc.SnapshotTree', { + extend: 'Ext.tree.Panel', + alias: ['widget.pveLxcSnapshotTree'], + + onlineHelp: 'pct_snapshots', + + load_delay: 3000, + + old_digest: 'invalid', + + stateful: true, + stateId: 'grid-lxc-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 + '/lxc/' + 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 + '/lxc/' + 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.LxcSnapshot', { + 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, + dangerous: true, + selModel: sm, + enableFn: valid_snapshot_rollback, + confirmMsg: function(rec) { + var taskdescription = Proxmox.Utils.format_task_description('vzrollback', me.vmid); + var snaptime = Ext.Date.format(rec.data.snaptime,'Y-m-d H:i:s'); + var snapname = rec.data.name; + + var msg = Ext.String.format(gettext('{0} to {1} ({2})'), + taskdescription, snapname, snaptime); + msg += '

' + 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 += '
' + gettext("You can delete the image from the guest's hardware pane"); + + Ext.Msg.show({ + title: gettext('Cannot remove disk image.'), + icon: Ext.Msg.ERROR, + msg: msg + }); + return; + } + } + var win = Ext.create('PVE.window.SafeDestroy', { + title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid), + showProgress: true, + url: url, + item: { type: 'Image', id: vmid } + }).show(); + win.on('destroy', function() { + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status' + }); + reload(); + + }); + } + }); + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status' + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Restore'), + selModel: sm, + disabled: true, + enableFn: function(rec) { + return rec && rec.data.content === 'backup'; + }, + handler: function(b, e, rec) { + var vmtype; + if (rec.data.volid.match(/vzdump-qemu-/)) { + vmtype = 'qemu'; + } else if (rec.data.volid.match(/vzdump-openvz-/) || rec.data.volid.match(/vzdump-lxc-/)) { + vmtype = 'lxc'; + } else { + return; + } + + var win = Ext.create('PVE.window.Restore', { + nodename: nodename, + volid: rec.data.volid, + volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec), + vmtype: vmtype + }); + win.show(); + win.on('destroy', reload); + } + }, + removeButton, + imageRemoveButton, + templateButton, + uploadButton, + { + xtype: 'proxmoxButton', + text: gettext('Show Configuration'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return rec && rec.data.content === 'backup'; + }, + handler: function(b,e,rec) { + var win = Ext.create('PVE.window.BackupConfig', { + volume: rec.data.volid, + pveSelNode: me.pveSelNode + }); + + win.show(); + } + }, + '->', + gettext('Search') + ':', ' ', + { + xtype: 'textfield', + width: 200, + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field) { + store.clearFilter(true); + store.filter([ + { + property: 'text', + value: field.getValue(), + anyMatch: true, + caseSensitive: false + } + ]); + } + } + } + ], + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + renderer: PVE.Utils.render_storage_content, + dataIndex: 'text' + }, + { + header: gettext('Format'), + width: 100, + dataIndex: 'format' + }, + { + header: gettext('Type'), + width: 100, + dataIndex: 'content', + renderer: PVE.Utils.format_content_types + }, + { + header: gettext('Size'), + width: 100, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + } + ], + listeners: { + activate: reload + } + }); + + me.callParent(); + + // disable the buttons/restrict the upload window + // if templates or uploads are not allowed + me.mon(me.statusStore, 'load', function(s,records,succes) { + var availcontent = []; + Ext.Array.each(records, function(item){ + if (item.id === 'content') { + availcontent = item.data.value.split(','); + } + }); + var templ = false; + var upload = false; + var cts = []; + + Ext.Array.each(availcontent, function(content) { + if (content === 'vztmpl') { + templ = true; + cts.push('vztmpl'); + } else if (content === 'iso') { + upload = true; + cts.push('iso'); + } + }); + + if (templ !== upload) { + uploadButton.contents = cts; + } + + templateButton.setDisabled(!templ); + uploadButton.setDisabled(!upload && !templ); + }); + } +}, function() { + + Ext.define('pve-storage-content', { + extend: 'Ext.data.Model', + fields: [ + 'volid', 'content', 'format', 'size', 'used', 'vmid', + 'channel', 'id', 'lun', + { + name: 'text', + convert: function(value, record) { + // check for volid, because if you click on a grouping header, + // it calls convert (but with an empty volid) + if (value || record.data.volid === null) { + return value; + } + return PVE.Utils.render_storage_content(value, {}, record); + } + } + ], + idProperty: 'volid' + }); + +}); +Ext.define('PVE.storage.StatusView', { + extend: 'PVE.panel.StatusView', + alias: 'widget.pveStorageStatusView', + + height: 230, + title: gettext('Status'), + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + padding: '0 30 5 30' + }, + items: [ + { + xtype: 'box', + height: 30 + }, + { + itemId: 'enabled', + title: gettext('Enabled'), + printBar: false, + textField: 'disabled', + renderer: Proxmox.Utils.format_neg_boolean + }, + { + itemId: 'active', + title: gettext('Active'), + printBar: false, + textField: 'active', + renderer: Proxmox.Utils.format_boolean + }, + { + itemId: 'content', + title: gettext('Content'), + printBar: false, + textField: 'content', + renderer: PVE.Utils.format_content_types + }, + { + itemId: 'type', + title: gettext('Type'), + printBar: false, + textField: 'type', + renderer: PVE.Utils.format_storage_type + }, + { + xtype: 'box', + height: 10 + }, + { + itemId: 'usage', + title: gettext('Usage'), + valueField: 'used', + maxField: 'total' + } + ], + + updateTitle: function() { + return; + } +}); +Ext.define('PVE.storage.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveStorageSummary', + scrollable: true, + bodyPadding: 5, + tbar: [ + '->', + { + xtype: 'proxmoxRRDTypeSelector' + } + ], + layout: { + type: 'column' + }, + defaults: { + padding: 5, + columnWidth: 1 + }, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storage = me.pveSelNode.data.storage; + if (!storage) { + throw "no storage ID specified"; + } + + var rstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status", + interval: 1000 + }); + + var rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata", + model: 'pve-rrd-storage' + }); + + Ext.apply(me, { + items: [ + { + xtype: 'pveStorageStatusView', + pveSelNode: me.pveSelNode, + rstore: rstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Usage'), + fields: ['total','used'], + fieldTitles: ['Total Size', 'Used Size'], + store: rrdstore + } + ], + listeners: { + activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, + destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.storage.Browser', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.storage.Browser', + + onlineHelp: 'chapter_storage', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storeid = me.pveSelNode.data.storage; + if (!storeid) { + throw "no storage ID specified"; + } + + + me.items = [ + { + title: gettext('Summary'), + xtype: 'pveStorageSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ]; + + var caps = Ext.state.Manager.get('GuiCap'); + + Ext.apply(me, { + title: Ext.String.format(gettext("Storage {0} on node {1}"), + "'" + storeid + "'", "'" + nodename + "'"), + hstateid: 'storagetab' + }); + + if (caps.storage['Datastore.Allocate'] || + caps.storage['Datastore.AllocateSpace'] || + caps.storage['Datastore.Audit']) { + me.items.push({ + xtype: 'pveStorageContentView', + title: gettext('Content'), + iconCls: 'fa fa-th', + itemId: 'content' + }); + } + + if (caps.storage['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/storage/' + storeid + }); + } + + me.callParent(); + } +}); +Ext.define('PVE.storage.DirInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_directory', + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'path', + value: '', + fieldLabel: gettext('Directory'), + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'shared', + uncheckedValue: 0, + fieldLabel: gettext('Shared') + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.NFSScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveNFSScan', + + queryParam: 'server', + + valueField: 'path', + displayField: 'path', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.nfsServer) { + me.store.removeAll(); + } + + me.allQuery = me.nfsServer; + + me.callParent(); + }, + + setServer: function(server) { + var me = this; + + me.nfsServer = server; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'path', 'options' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/nfs' + } + }); + + store.sort('path', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.NFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_nfs', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + // hack: for now we always create nvf v3 + // fixme: make this configurable + values.options = 'vers=3'; + } + + return me.callParent([values]); + }, + + initComponent : function() { + var me = this; + + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=export]'); + exportField.setServer(value); + exportField.setValue(''); + } + } + } + }, + { + xtype: me.isCreate ? 'pveNFSScan' : 'displayfield', + name: 'export', + value: '', + fieldLabel: 'Export', + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.CIFSScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCIFSScan', + + queryParam: 'server', + + valueField: 'share', + displayField: 'share', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.cifsServer) { + me.store.removeAll(); + } + + var params = {}; + if (me.cifsUsername && me.cifsPassword) { + params.username = me.cifsUsername; + params.password = me.cifsPassword; + } + + if (me.cifsDomain) { + params.domain = me.cifsDomain; + } + + me.store.getProxy().setExtraParams(params); + me.allQuery = me.cifsServer; + + me.callParent(); + }, + + setServer: function(server) { + this.cifsServer = server; + }, + + setUsername: function(username) { + this.cifsUsername = username; + }, + + setPassword: function(password) { + this.cifsPassword = password; + }, + + setDomain: function(domain) { + this.cifsDomain = domain; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['description', 'share'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/cifs' + } + }); + store.sort('share', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.CIFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_cifs', + + initComponent : function() { + var me = this; + + var passwordfield = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + inputType: 'password', + name: 'password', + value: me.isCreate ? '' : '********', + fieldLabel: gettext('Password'), + allowBlank: false, + disabled: me.isCreate, + minLength: 1, + listeners: { + change: function(f, value) { + + if (me.isCreate) { + var exportField = me.down('field[name=share]'); + exportField.setPassword(value); + } + } + } + }); + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=share]'); + exportField.setServer(value); + } + } + } + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: '', + fieldLabel: gettext('Username'), + emptyText: gettext('Guest user'), + allowBlank: true, + listeners: { + change: function(f, value) { + if (!me.isCreate) { + return; + } + var exportField = me.down('field[name=share]'); + exportField.setUsername(value); + + if (value == "") { + passwordfield.disable(); + } else { + passwordfield.enable(); + } + passwordfield.validate(); + } + } + }, + passwordfield, + { + xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield', + name: 'share', + value: '', + fieldLabel: 'Share', + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'domain', + value: me.isCreate ? '' : undefined, + fieldLabel: gettext('Domain'), + allowBlank: true, + listeners: { + change: function(f, value) { + if (me.isCreate) { + + var exportField = me.down('field[name=share]'); + exportField.setDomain(value); + } + } + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.GlusterFsScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveGlusterFsScan', + + queryParam: 'server', + + valueField: 'volname', + displayField: 'volname', + matchFieldWidth: false, + listConfig: { + loadingText: 'Scanning...', + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.glusterServer) { + me.store.removeAll(); + } + + me.allQuery = me.glusterServer; + + me.callParent(); + }, + + setServer: function(server) { + var me = this; + + me.glusterServer = server; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'volname' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs' + } + }); + + store.sort('volname', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.GlusterFsInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_glusterfs', + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var volumeField = me.down('field[name=volume]'); + volumeField.setServer(value); + volumeField.setValue(''); + } + } + } + }, + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + name: 'server2', + value: '', + fieldLabel: gettext('Second Server'), + allowBlank: true + }, + { + xtype: me.isCreate ? 'pveGlusterFsScan' : 'displayfield', + name: 'volume', + value: '', + fieldLabel: 'Volume name', + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'iso', 'backup', 'vztmpl', 'snippets'], + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.IScsiScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveIScsiScan', + + queryParam: 'portal', + valueField: 'target', + displayField: 'target', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.portal) { + me.store.removeAll(); + } + + me.allQuery = me.portal; + + me.callParent(); + }, + + setPortal: function(portal) { + var me = this; + + me.portal = portal; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'target', 'portal' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/iscsi' + } + }); + + store.sort('target', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.IScsiInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_open_iscsi', + + onGetValues: function(values) { + var me = this; + + values.content = values.luns ? 'images' : 'none'; + delete values.luns; + + return me.callParent([values]); + }, + + setValues: function(values) { + values.luns = (values.content.indexOf('images') !== -1) ? true : false; + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', + value: '', + fieldLabel: 'Portal', + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=target]'); + exportField.setPortal(value); + exportField.setValue(''); + } + } + } + }, + { + readOnly: !me.isCreate, + xtype: me.isCreate ? 'pveIScsiScan' : 'displayfield', + name: 'target', + value: '', + fieldLabel: 'Target', + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'checkbox', + name: 'luns', + checked: true, + fieldLabel: gettext('Use LUNs directly') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.VgSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveVgSelector', + valueField: 'vg', + displayField: 'vg', + queryMode: 'local', + editable: false, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, // true, + fields: [ 'vg', 'size', 'free' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvm' + } + }); + + store.sort('vg', 'ASC'); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.BaseStorageSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveBaseStorageSelector', + + existingGroupsText: gettext("Existing volume groups"), + queryMode: 'local', + editable: false, + value: '', + valueField: 'storage', + displayField: 'text', + initComponent : function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: { + addRecords: true, + params: { + type: 'iscsi' + } + }, + fields: [ 'storage', 'type', 'content', + { + name: 'text', + convert: function(value, record) { + if (record.data.storage) { + return record.data.storage + " (iSCSI)"; + } else { + return me.existingGroupsText; + } + } + }], + proxy: { + type: 'proxmox', + url: '/api2/json/storage/' + } + }); + + store.loadData([{ storage: '' }], true); + + store.sort('storage', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.LVMInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_lvm', + + initComponent : function() { + var me = this; + + me.column1 = []; + + var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'vgname', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + if (me.isCreate) { + var vgField = Ext.create('PVE.storage.VgSelector', { + name: 'vgname', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + var baseField = Ext.createWidget('pveFileSelector', { + name: 'base', + hidden: true, + disabled: true, + nodename: 'localhost', + storageContent: 'images', + fieldLabel: gettext('Base volume'), + allowBlank: false + }); + + me.column1.push({ + xtype: 'pveBaseStorageSelector', + name: 'basesel', + fieldLabel: gettext('Base storage'), + submitValue: false, + listeners: { + change: function(f, value) { + if (value) { + vgnameField.setVisible(true); + vgnameField.setDisabled(false); + vgField.setVisible(false); + vgField.setDisabled(true); + baseField.setVisible(true); + baseField.setDisabled(false); + } else { + vgnameField.setVisible(false); + vgnameField.setDisabled(true); + vgField.setVisible(true); + vgField.setDisabled(false); + baseField.setVisible(false); + baseField.setDisabled(true); + } + baseField.setStorage(value); + } + } + }); + + me.column1.push(baseField); + + me.column1.push(vgField); + } + + me.column1.push(vgnameField); + + // here value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push({ + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'shared', + uncheckedValue: 0, + fieldLabel: gettext('Shared') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.TPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveTPSelector', + + queryParam: 'vg', + valueField: 'lv', + displayField: 'lv', + editable: false, + + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.vg) { + me.store.removeAll(); + } + + me.allQuery = me.vg; + + me.callParent(); + }, + + setVG: function(myvg) { + var me = this; + + me.vg = myvg; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'lv' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvmthin' + } + }); + + store.sort('lv', 'ASC'); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.BaseVGSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveBaseVGSelector', + + valueField: 'vg', + displayField: 'vg', + queryMode: 'local', + editable: false, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, + fields: [ 'vg', 'size', 'free'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvm' + } + }); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.LvmThinInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_lvmthin', + + initComponent : function() { + var me = this; + + me.column1 = []; + + var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'vgname', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + var thinpoolField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'thinpool', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Thin Pool'), + allowBlank: false + }); + + if (me.isCreate) { + var vgField = Ext.create('PVE.storage.TPoolSelector', { + name: 'thinpool', + fieldLabel: gettext('Thin Pool'), + allowBlank: false + }); + + me.column1.push({ + xtype: 'pveBaseVGSelector', + name: 'vgname', + fieldLabel: gettext('Volume group'), + listeners: { + change: function(f, value) { + if (me.isCreate) { + vgField.setVG(value); + vgField.setValue(''); + } + } + } + }); + + me.column1.push(vgField); + } + + me.column1.push(vgnameField); + + me.column1.push(thinpoolField); + + // here value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push({ + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + + me.column2 = []; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.CephFSInputPanel', { + extend: 'PVE.panel.StorageBase', + controller: 'cephstorage', + + onlineHelp: 'storage_cephfs', + + viewModel: { + type: 'cephstorage' + }, + + setValues: function(values) { + if (values.monhost) { + this.viewModel.set('pveceph', false); + this.lookupReference('pvecephRef').setValue(false); + this.lookupReference('pvecephRef').resetOriginalValue(); + } + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + me.type = 'cephfs'; + + me.column1 = []; + + me.column1.push( + { + xtype: 'textfield', + name: 'monhost', + vtype: 'HostList', + value: '', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + fieldLabel: 'Monitor(s)', + allowBlank: false + }, + { + xtype: 'displayfield', + reference: 'monhost', + bind: { + disabled: '{!pveceph}', + hidden: '{!pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)' + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: 'admin', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}' + }, + fieldLabel: gettext('User name'), + allowBlank: true + } + ); + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + cts: ['backup', 'iso', 'vztmpl', 'snippets'], + fieldLabel: gettext('Content'), + name: 'content', + value: 'backup', + multiSelect: true, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.columnB = [{ + xtype: 'proxmoxcheckbox', + name: 'pveceph', + reference: 'pvecephRef', + bind : { + disabled: '{!pvecephPossible}', + value: '{pveceph}' + }, + checked: true, + uncheckedValue: 0, + submitValue: false, + hidden: !me.isCreate, + boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS') + }]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.Ceph.Model', { + extend: 'Ext.app.ViewModel', + alias: 'viewmodel.cephstorage', + + data: { + pveceph: true, + pvecephPossible: true + } +}); + +Ext.define('PVE.storage.Ceph.Controller', { + extend: 'PVE.controller.StorageEdit', + alias: 'controller.cephstorage', + + control: { + '#': { + afterrender: 'queryMonitors' + }, + 'textfield[name=username]': { + disable: 'resetField' + }, + 'displayfield[name=monhost]': { + enable: 'queryMonitors' + }, + 'textfield[name=monhost]': { + disable: 'resetField', + enable: 'resetField' + } + }, + resetField: function(field) { + field.reset(); + }, + queryMonitors: function(field, newVal, oldVal) { + // we get called with two signatures, the above one for a field + // change event and the afterrender from the view, this check only + // can be true for the field change one and omit the API request if + // pveceph got unchecked - as it's not needed there. + if (field && !newVal && oldVal) { + return; + } + var view = this.getView(); + var vm = this.getViewModel(); + if (!(view.isCreate || vm.get('pveceph'))) { + return; // only query on create or if editing a pveceph store + } + + var monhostField = this.lookupReference('monhost'); + + Proxmox.Utils.API2Request({ + url: '/api2/json/nodes/localhost/ceph/mon', + method: 'GET', + scope: this, + callback: function(options, success, response) { + var data = response.result.data; + if (response.status === 200) { + if (data.length > 0) { + var monhost = Ext.Array.pluck(data, 'name').sort().join(','); + monhostField.setValue(monhost); + monhostField.resetOriginalValue(); + if (view.isCreate) { + vm.set('pvecephPossible', true); + } + } else { + vm.set('pveceph', false); + } + } else { + vm.set('pveceph', false); + vm.set('pvecephPossible', false); + } + } + }); + } +}); + +Ext.define('PVE.storage.RBDInputPanel', { + extend: 'PVE.panel.StorageBase', + controller: 'cephstorage', + + onlineHelp: 'ceph_rados_block_devices', + + viewModel: { + type: 'cephstorage' + }, + + setValues: function(values) { + if (values.monhost) { + this.viewModel.set('pveceph', false); + this.lookupReference('pvecephRef').setValue(false); + this.lookupReference('pvecephRef').resetOriginalValue(); + } + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + me.type = 'rbd'; + + me.column1 = []; + + if (me.isCreate) { + me.column1.push({ + xtype: 'pveCephPoolSelector', + nodename: me.nodename, + name: 'pool', + bind: { + disabled: '{!pveceph}', + submitValue: '{pveceph}', + hidden: '{!pveceph}' + }, + fieldLabel: gettext('Pool'), + allowBlank: false + },{ + xtype: 'textfield', + name: 'pool', + value: 'rbd', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + fieldLabel: gettext('Pool'), + allowBlank: false + }); + } else { + me.column1.push({ + xtype: 'displayfield', + nodename: me.nodename, + name: 'pool', + fieldLabel: gettext('Pool'), + allowBlank: false + }); + } + + me.column1.push( + { + xtype: 'textfield', + name: 'monhost', + vtype: 'HostList', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)', + allowBlank: false + }, + { + xtype: 'displayfield', + reference: 'monhost', + bind: { + disabled: '{!pveceph}', + hidden: '{!pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)' + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}' + }, + value: 'admin', + fieldLabel: gettext('User name'), + allowBlank: true + } + ); + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images'], + multiSelect: true, + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'krbd', + uncheckedValue: 0, + fieldLabel: 'KRBD' + } + ]; + + me.columnB = [{ + xtype: 'proxmoxcheckbox', + name: 'pveceph', + reference: 'pvecephRef', + bind : { + disabled: '{!pvecephPossible}', + value: '{pveceph}' + }, + checked: true, + uncheckedValue: 0, + submitValue: false, + hidden: !me.isCreate, + boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool') + }]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.SheepdogInputPanel', { + extend: 'PVE.panel.StorageBase', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.content = 'images'; + } + + return me.callParent([values]); + }, + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', + value: '127.0.0.1:7000', + fieldLabel: gettext('Gateway'), + allowBlank: false + } + ]; + me.column2 = []; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.ZFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + viewModel: { + parent: null, + data: { + isLIO: false, + isComstar: true, + hasWriteCacheOption: true + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'field[name=iscsiprovider]': { + change: 'changeISCSIProvider' + } + }, + changeISCSIProvider: function(f, newVal, oldVal) { + var vm = this.getViewModel(); + vm.set('isLIO', newVal === 'LIO'); + vm.set('isComstar', newVal === 'comstar'); + vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt'); + } + }, + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.content = 'images'; + } + + values.nowritecache = values.writecache ? 0 : 1; + delete values.writecache; + + return me.callParent([values]); + }, + + setValues: function diff(values) { + values.writecache = values.nowritecache ? 0 : 1; + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', + value: '', + fieldLabel: gettext('Portal'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'pool', + value: '', + fieldLabel: gettext('Pool'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'blocksize', + value: '4k', + fieldLabel: gettext('Block Size'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'target', + value: '', + fieldLabel: gettext('Target'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_tg', + value: '', + fieldLabel: gettext('Target group'), + bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, + allowBlank: true + } + ]; + + me.column2 = [ + { + xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield', + name: 'iscsiprovider', + value: 'comstar', + fieldLabel: gettext('iSCSI Provider'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision') + }, + { + xtype: 'proxmoxcheckbox', + name: 'writecache', + checked: true, + bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' }, + uncheckedValue: 0, + fieldLabel: gettext('Write cache') + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_hg', + value: '', + bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, + fieldLabel: gettext('Host group'), + allowBlank: true + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'lio_tpg', + value: '', + bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' }, + allowBlank: false, + fieldLabel: gettext('Target portal group') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.ZFSPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveZFSPoolSelector', + valueField: 'pool', + displayField: 'pool', + queryMode: 'local', + editable: false, + listConfig: { + loadingText: gettext('Scanning...') + }, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, // true, + fields: [ 'pool', 'size', 'free' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/zfs' + } + }); + + store.sort('pool', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.ZFSPoolInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_zfspool', + + initComponent : function() { + var me = this; + + me.column1 = []; + + if (me.isCreate) { + me.column1.push(Ext.create('PVE.storage.ZFSPoolSelector', { + name: 'pool', + fieldLabel: gettext('ZFS Pool'), + allowBlank: false + })); + } else { + me.column1.push(Ext.createWidget('displayfield', { + name: 'pool', + value: '', + fieldLabel: gettext('ZFS Pool'), + allowBlank: false + })); + } + + // value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push( + {xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision') + }, + { + xtype: 'textfield', + name: 'blocksize', + emptyText: '8k', + fieldLabel: gettext('Block Size'), + allowBlank: true + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.ha.StatusView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAStatusView'], + + onlineHelp: 'chapter_ha_manager', + + sortPriority: { + quorum: 1, + master: 2, + lrm: 3, + service: 4 + }, + + initComponent : function() { + var me = this; + + if (!me.rstore) { + throw "no rstore given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + sortAfterUpdate: true, + sorters: [{ + sorterFn: function(rec1, rec2) { + var p1 = me.sortPriority[rec1.data.type]; + var p2 = me.sortPriority[rec2.data.type]; + return (p1 !== p2) ? ((p1 > p2) ? 1 : -1) : 0; + } + }], + filters: { + property: 'type', + value: 'service', + operator: '!=' + } + }); + + Ext.apply(me, { + store: store, + stateful: false, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Type'), + width: 80, + dataIndex: 'type' + }, + { + header: gettext('Status'), + width: 80, + flex: 1, + dataIndex: 'status' + } + ] + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + + } +}, function() { + + Ext.define('pve-ha-status', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'type', 'node', 'status', 'sid', + 'state', 'group', 'comment', + 'max_restart', 'max_relocate', 'type', + 'crm_state', 'request_state' + ], + idProperty: 'id' + }); + +}); +Ext.define('PVE.ha.Status', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveHAStatus', + + onlineHelp: 'chapter_ha_manager', + layout: { + type: 'vbox', + align: 'stretch' + }, + + initComponent: function() { + var me = this; + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + interval: me.interval, + model: 'pve-ha-status', + storeid: 'pve-store-' + (++Ext.idSeed), + groupField: 'type', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/ha/status/current' + } + }); + + me.items = [{ + xtype: 'pveHAStatusView', + title: gettext('Status'), + rstore: me.rstore, + border: 0, + collapsible: true, + padding: '0 0 20 0' + },{ + xtype: 'pveHAResourcesView', + flex: 1, + collapsible: true, + title: gettext('Resources'), + border: 0, + rstore: me.rstore + }]; + + me.callParent(); + me.on('activate', me.rstore.startUpdate); + } +}); +Ext.define('PVE.ha.GroupSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveHAGroupSelector'], + + value: [], + autoSelect: false, + valueField: 'group', + displayField: 'group', + listConfig: { + columns: [ + { + header: gettext('Group'), + width: 100, + sortable: true, + dataIndex: 'group' + }, + { + header: gettext('Nodes'), + width: 100, + sortable: false, + dataIndex: 'nodes' + }, + { + header: gettext('Comment'), + flex: 1, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode + } + ] + }, + store: { + model: 'pve-ha-groups', + sorters: { + property: 'group', + order: 'DESC' + } + }, + + initComponent: function() { + var me = this; + me.callParent(); + me.getStore().load(); + } + +}, function() { + + Ext.define('pve-ha-groups', { + extend: 'Ext.data.Model', + fields: [ + 'group', 'type', 'digest', 'nodes', 'comment', + { + name : 'restricted', + type: 'boolean' + }, + { + name : 'nofailback', + type: 'boolean' + } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/ha/groups" + }, + idProperty: 'group' + }); +}); +Ext.define('PVE.ha.VMResourceInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'ha_manager_resource_config', + vmid: undefined, + + onGetValues: function(values) { + var me = this; + + if (values.vmid) { + values.sid = values.vmid; + } + delete values.vmid; + + PVE.Utils.delete_if_default(values, 'group', '', me.isCreate); + PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate); + PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate); + + return values; + }, + + initComponent : function() { + var me = this; + var MIN_QUORUM_VOTES = 3; + + var disabledHint = Ext.createWidget({ + xtype: 'displayfield', // won't get submitted by default + userCls: 'pve-hint', + value: 'Disabling the resource will stop the guest system. ' + + 'See the online help for details.', + hidden: true + }); + + var fewVotesHint = Ext.createWidget({ + itemId: 'fewVotesHint', + xtype: 'displayfield', + userCls: 'pve-hint', + value: 'At least three quorum votes are recommended for reliable HA.', + hidden: true + }); + + Proxmox.Utils.API2Request({ + url: '/cluster/config/nodes', + method: 'GET', + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + var nodes = response.result.data; + var votes = 0; + Ext.Array.forEach(nodes, function(node) { + var vote = parseInt(node.quorum_votes, 10); // parse as base 10 + votes += vote || 0; // parseInt might return NaN, which is false + }); + + if (votes < MIN_QUORUM_VOTES) { + fewVotesHint.setVisible(true); + } + } + }); + + /*jslint confusion: true */ + var vmidStore = (me.vmid) ? {} : { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [ + { + property: 'type', + value: /lxc|qemu/ + }, + { + property: 'hastate', + value: /unmanaged/ + } + ] + }; + + // value is a string above, but a number below + me.column1 = [ + { + xtype: me.vmid ? 'displayfield' : 'vmComboSelector', + submitValue: me.isCreate, + name: 'vmid', + fieldLabel: (me.vmid && me.guestType === 'ct') ? 'CT' : 'VM', + value: me.vmid, + store: vmidStore, + validateExists: true + }, + { + xtype: 'proxmoxintegerfield', + name: 'max_restart', + fieldLabel: gettext('Max. Restart'), + value: 1, + minValue: 0, + maxValue: 10, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + name: 'max_relocate', + fieldLabel: gettext('Max. Relocate'), + value: 1, + minValue: 0, + maxValue: 10, + allowBlank: false + } + ]; + /*jslint confusion: false */ + + me.column2 = [ + { + xtype: 'pveHAGroupSelector', + name: 'group', + fieldLabel: gettext('Group') + }, + { + xtype: 'proxmoxKVComboBox', + name: 'state', + value: 'started', + fieldLabel: gettext('Request State'), + comboItems: [ + ['started', 'started'], + ['stopped', 'stopped'], + ['ignored', 'ignored'], + ['disabled', 'disabled'] + ], + listeners: { + 'change': function(field, newValue) { + if (newValue === 'disabled') { + disabledHint.setVisible(true); + } + else { + if (disabledHint.isVisible()) { + disabledHint.setVisible(false); + } + } + } + } + }, + disabledHint + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + }, + fewVotesHint + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.ha.VMResourceEdit', { + extend: 'Proxmox.window.Edit', + + vmid: undefined, + guestType: undefined, + isCreate: undefined, + + initComponent : function() { + var me = this; + + if (me.isCreate === undefined) { + me.isCreate = !me.vmid; + } + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/ha/resources'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid; + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', { + isCreate: me.isCreate, + vmid: me.vmid, + guestType: me.guestType + }); + + Ext.apply(me, { + subject: gettext('Resource') + ': ' + gettext('Container') + + '/' + gettext('Virtual Machine'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + var regex = /^(\S+):(\S+)$/; + var res = regex.exec(values.sid); + + if (res[1] !== 'vm' && res[1] !== 'ct') { + throw "got unexpected resource type"; + } + + values.vmid = res[2]; + + ipanel.setValues(values); + } + }); + } + } +}); +Ext.define('PVE.ha.ResourcesView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAResourcesView'], + + onlineHelp: 'ha_manager_resources', + + stateful: true, + stateId: 'grid-ha-resources', + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + if (!me.rstore) { + throw "no store given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + filters: { + property: 'type', + value: 'service' + } + }); + + var reload = function() { + me.rstore.load(); + }; + + var render_error = function(dataIndex, value, metaData, record) { + var errors = record.data.errors; + if (errors) { + var msg = errors[dataIndex]; + if (msg) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + 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: [ + '

' + gettext('Nodes') + '


', + '
', + '
', + ' ', + gettext('Online'), + '
', + '
{online}
', + '

', + '
', + ' ', + gettext('Offline'), + '
', + '
{offline}
', + '
' + ] + }, + { + itemId: 'ceph', + width: 250, + columnWidth: undefined, + userCls: 'pointer', + title: 'Ceph', + xtype: 'pveHealthWidget', + hidden: true, + listeners: { + element: 'el', + click: function() { + var me = this.component.up('pveDcHealth'); + var sp = Ext.state.Manager.getProvider(); + + // preselect the ceph tab + sp.set('nodetab', {value:'ceph'}); + + // select the node that had the successfull api call + var id = me.nodeList[me.nodeIndex].id; + Ext.ComponentQuery.query('pveResourceTree')[0].selectById(id); + } + } + } + ], + + initComponent: function() { + var me = this; + + me.nodeList = PVE.data.ResourceStore.getNodes(); + me.nodeIndex = 0; + me.cephstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'pve-cluster-ceph', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodeList[me.nodeIndex].node + '/ceph/status' + } + }); + me.callParent(); + me.mon(me.cephstore, 'load', me.updateCeph, me); + me.cephstore.startUpdate(); + } +}); +Ext.define('PVE.dc.Guests', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcGuests', + + + title: gettext('Guests'), + height: 220, + layout: { + type: 'table', + columns: 2, + tableAttrs: { + style: { + width: '100%' + } + } + }, + bodyPadding: '0 20 20 20', + + defaults: { + xtype: 'box', + padding: '0 50 0 50', + style: { + 'text-align':'center', + 'line-height':'1.2' + } + }, + items: [{ + itemId: 'qemu', + data: { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }, + tpl: [ + '

' + gettext("Virtual Machines") + '

', + '
', + ' ', + gettext('Running'), + '
', + '
{running}
' + '
', + '', + '
', + ' ', + gettext('Paused'), + '
', + '
{paused}
' + '
', + '
', + '
', + ' ', + gettext('Stopped'), + '
', + '
{stopped}
' + '
', + '', + '
', + ' ', + gettext('Templates'), + '
', + '
{template}
', + '
' + ] + },{ + itemId: 'lxc', + data: { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }, + tpl: [ + '

' + gettext("LXC Container") + '

', + '
', + ' ', + gettext('Running'), + '
', + '
{running}
' + '
', + '', + '
', + ' ', + gettext('Paused'), + '
', + '
{paused}
' + '
', + '
', + '
', + ' ', + gettext('Stopped'), + '
', + '
{stopped}
' + '
', + '', + '
', + ' ', + gettext('Templates'), + '
', + '
{template}
', + '
' + ] + },{ + itemId: 'error', + colspan: 2, + data: { + num: 0 + }, + columnWidth: 1, + padding: '10 250 0 250', + tpl: [ + '', + '
', + ' ', + gettext('Error'), + '
', + '
{num}
', + '
' + ] + }], + + updateValues: function(qemu, lxc, error) { + var me = this; + me.getComponent('qemu').update(qemu); + me.getComponent('lxc').update(lxc); + me.getComponent('error').update({num: error}); + } +}); + /*jslint confusion: true*/ +Ext.define('PVE.dc.OptionView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveDcOptionView'], + + onlineHelp: 'datacenter_configuration_file', + + monStoreErrors: true, + + add_inputpanel_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + var canEdit = (opts.caps === undefined || opts.caps); + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: canEdit ? { + xtype: 'proxmoxWindowEdit', + width: 350, + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + setValues: function(values) { + // FIXME: run through parsePropertyString if not an object? + var edit_value = values[name]; + Ext.Array.each(this.query('inputpanel'), function(panel) { + panel.setValues(edit_value); + }); + }, + url: opts.url, + items: [{ + xtype: 'inputpanel', + onGetValues: function(values) { + if (values === undefined || Object.keys(values).length === 0) { + return { 'delete': name }; + } + var ret_val = {}; + ret_val[name] = PVE.Parser.printPropertyString(values); + return ret_val; + }, + items: opts.items + }] + } : undefined + }; + }, + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + me.add_combobox_row('keyboard', gettext('Keyboard Layout'), { + renderer: PVE.Utils.render_kvm_language, + comboItems: PVE.Utils.kvm_keymap_array(), + defaultValue: '__default__', + deleteEmpty: true + }); + me.add_text_row('http_proxy', gettext('HTTP proxy'), { + defaultValue: Proxmox.Utils.noneText, + vtype: 'HttpProxy', + deleteEmpty: true + }); + me.add_combobox_row('console', gettext('Console Viewer'), { + renderer: PVE.Utils.render_console_viewer, + comboItems: PVE.Utils.console_viewer_array(), + defaultValue: '__default__', + deleteEmpty: true + }); + me.add_text_row('email_from', gettext('Email from address'), { + deleteEmpty: true, + vtype: 'proxmoxMail', + defaultValue: 'root@$hostname' + }); + me.add_text_row('mac_prefix', gettext('MAC address prefix'), { + deleteEmpty: true, + vtype: 'MacPrefix', + defaultValue: Proxmox.Utils.noneText + }); + me.add_inputpanel_row('migration', gettext('Migration Settings'), { + renderer: PVE.Utils.render_dc_ha_opts, + caps: caps.vms['Sys.Modify'], + labelWidth: 120, + url: "/api2/extjs/cluster/options", + defaultKey: 'type', + items: [{ + xtype: 'displayfield', + name: 'type', + fieldLabel: gettext('Type'), + value: 'secure', + submitValue: true, + vtype: 'IPCIDRAddress' + }, { + xtype: 'textfield', + name: 'network', + fieldLabel: gettext('Network'), + vtype: 'IPCIDRAddress', + emptyText: Proxmox.Utils.defaultText, + value: '' + }] + }); + me.add_inputpanel_row('ha', gettext('HA Settings'), { + renderer: PVE.Utils.render_dc_ha_opts, + caps: caps.vms['Sys.Modify'], + labelWidth: 120, + url: "/api2/extjs/cluster/options", + items: [{ + xtype: 'proxmoxKVComboBox', + name: 'shutdown_policy', + fieldLabel: gettext('Shutdown Policy'), + deleteEmpty: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (conditional)' ], + ['freeze', 'freeze'], + ['failover', 'failover'], + ['conditional', 'conditional'] + ], + defaultValue: '__default__' + }] + }); + + // TODO: bwlimits, migration net, u2f? + + me.selModel = Ext.create('Ext.selection.RowModel', {}); + + Ext.apply(me, { + tbar: [{ + text: gettext('Edit'), + xtype: 'proxmoxButton', + disabled: true, + handler: function() { me.run_editor(); }, + selModel: me.selModel + }], + url: "/api2/json/cluster/options", + editorConfig: { + url: "/api2/extjs/cluster/options" + }, + interval: 5000, + cwidth1: 200, + listeners: { + itemdblclick: me.run_editor + } + }); + + me.callParent(); + + // set the new value for the default console + me.mon(me.rstore, 'load', function(store, records, success) { + if (!success) { + return; + } + + var rec = store.getById('console'); + PVE.VersionInfo.console = rec.data.value; + if (rec.data.value === '__default__') { + delete PVE.VersionInfo.console; + } + }); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + } +}); +Ext.define('PVE.dc.StorageView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveStorageView'], + + onlineHelp: 'chapter_storage', + + stateful: true, + stateId: 'grid-dc-storage', + + createStorageEditWindow: function(type, sid) { + var schema = PVE.Utils.storageSchema[type]; + if (!schema || !schema.ipanel) { + throw "no editor registered for storage type: " + type; + } + + Ext.create('PVE.storage.BaseEdit', { + paneltype: 'PVE.storage.' + schema.ipanel, + type: type, + storageId: sid, + autoShow: true, + listeners: { + destroy: this.reloadStore + } + }); + }, + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-storage', + proxy: { + type: 'proxmox', + url: "/api2/json/storage" + }, + sorters: { + property: 'storage', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var type = rec.data.type, + sid = rec.data.storage; + + me.createStorageEditWindow(type, sid); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/storage/', + callback: reload + }); + + // else we cannot dynamically generate the add menu handlers + var addHandleGenerator = function(type) { + return function() { me.createStorageEditWindow(type); }; + }; + var addMenuItems = [], type; + /*jslint forin: true */ + for (type in PVE.Utils.storageSchema) { + var storage = PVE.Utils.storageSchema[type]; + if (storage.hideAdd) { + continue; + } + addMenuItems.push({ + text: PVE.Utils.format_storage_type(type), + iconCls: 'fa fa-fw fa-' + storage.faIcon, + handler: addHandleGenerator(type) + }); + } + + Ext.apply(me, { + store: store, + reloadStore: reload, + selModel: sm, + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: addMenuItems + }) + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: 'ID', + flex: 2, + sortable: true, + dataIndex: 'storage' + }, + { + header: gettext('Type'), + flex: 1, + sortable: true, + dataIndex: 'type', + renderer: PVE.Utils.format_storage_type + }, + { + header: gettext('Content'), + flex: 3, + sortable: true, + dataIndex: 'content', + renderer: PVE.Utils.format_content_types + }, + { + header: gettext('Path') + '/' + gettext('Target'), + flex: 2, + sortable: true, + dataIndex: 'path', + renderer: function(value, metaData, record) { + if (record.data.target) { + return record.data.target; + } + return value; + } + }, + { + header: gettext('Shared'), + flex: 1, + sortable: true, + dataIndex: 'shared', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('Enabled'), + flex: 1, + sortable: true, + dataIndex: 'disable', + renderer: Proxmox.Utils.format_neg_boolean + }, + { + header: gettext('Bandwidth Limit'), + flex: 2, + sortable: true, + dataIndex: 'bwlimit' + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-storage', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'content', 'server', 'portal', 'target', 'export', 'storage', + { name: 'shared', type: 'boolean'}, + { name: 'disable', type: 'boolean'} + ], + idProperty: 'storage' + }); + +}); +/*global u2f,QRCode,Uint8Array*/ +/*jslint confusion: true*/ +Ext.define('PVE.window.TFAEdit', { + extend: 'Ext.window.Window', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'pveum_tfa_auth', // fake to ensure this gets a link target + + modal: true, + resizable: false, + title: gettext('Two Factor Authentication'), + subject: 'TFA', + url: '/api2/extjs/access/tfa', + width: 512, + + layout: { + type: 'vbox', + align: 'stretch' + }, + + updateQrCode: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var algorithm = values.algorithm; + if (!algorithm) { + algorithm = 'SHA1'; + } + + me.qrcode.makeCode( + 'otpauth://totp/' + encodeURIComponent(me.userid) + + '?secret=' + values.secret + + '&period=' + values.step + + '&digits=' + values.digits + + '&algorithm=' + algorithm + + '&issuer=' + encodeURIComponent(values.issuer) + ); + + me.lookup('challenge').setVisible(true); + me.down('#qrbox').setVisible(true); + }, + + showError: function(error) { + Ext.Msg.alert( + gettext('Error'), + PVE.Utils.render_u2f_error(error) + ); + }, + + doU2FChallenge: function(response) { + var me = this; + + var data = response.result.data; + me.lookup('password').setDisabled(true); + var msg = Ext.Msg.show({ + title: 'U2F: '+gettext('Setup'), + message: gettext('Please press the button on your U2F Device'), + buttons: [] + }); + Ext.Function.defer(function() { + u2f.register(data.appId, [data], [], function(data) { + msg.close(); + if (data.errorCode) { + me.showError(data.errorCode); + } else { + me.respondToU2FChallenge(data); + } + }); + }, 500, me); + }, + + respondToU2FChallenge: function(data) { + var me = this; + var params = { + userid: me.userid, + action: 'confirm', + response: JSON.stringify(data) + }; + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + success: function() { + me.close(); + Ext.Msg.show({ + title: gettext('Success'), + message: gettext('U2F Device successfully connected.'), + buttons: Ext.Msg.OK + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + viewModel: { + data: { + in_totp_tab: true, + tfa_required: false, + has_tfa: false, + valid: false, + u2f_available: true + }, + formulas: { + canDeleteTFA: function(get) { + return (get('has_tfa') && !get('tfa_required')); + } + } + }, + + afterLoadingRealm: function(realm_tfa_type) { + var me = this; + var viewmodel = me.getViewModel(); + if (!realm_tfa_type) { + // There's no TFA enforced by the realm, everything works. + viewmodel.set('u2f_available', true); + viewmodel.set('tfa_required', false); + } else if (realm_tfa_type === 'oath') { + // The realm explicitly requires TOTP + viewmodel.set('tfa_required', true); + viewmodel.set('u2f_available', false); + } else { + // The realm enforces some other TFA type (yubico) + me.close(); + Ext.Msg.alert( + gettext('Error'), + Ext.String.format( + gettext("Custom 2nd factor configuration is not supported on realms with '{0}' TFA."), + realm_tfa_type + ) + ); + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'field[qrupdate=true]': { + change: function() { + var me = this.getView(); + me.updateQrCode(); + } + }, + 'field': { + validitychange: function(field, valid) { + var me = this; + var viewModel = me.getViewModel(); + var form = me.lookup('totp_form'); + var challenge = me.lookup('challenge'); + var password = me.lookup('password'); + viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid()); + } + }, + '#': { + show: function() { + var me = this.getView(); + var viewmodel = this.getViewModel(); + + me.qrdiv = document.createElement('center'); + me.qrcode = new QRCode(me.qrdiv, { + width: 256, + height: 256, + correctLevel: QRCode.CorrectLevel.M + }); + me.down('#qrbox').getEl().appendChild(me.qrdiv); + + viewmodel.set('has_tfa', me.hasTFA); + if (!me.hasTFA) { + this.randomizeSecret(); + } else { + me.down('#qrbox').setVisible(false); + me.lookup('challenge').setVisible(false); + } + + if (Proxmox.UserName === 'root@pam') { + me.lookup('password').setVisible(false); + me.lookup('password').setDisabled(true); + } + } + }, + '#tfatabs': { + tabchange: function(panel, newcard) { + var viewmodel = this.getViewModel(); + viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel'); + } + } + }, + + applySettings: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var params = { + userid: me.getView().userid, + action: 'new', + key: values.secret, + config: PVE.Parser.printPropertyString({ + type: 'oath', + digits: values.digits, + step: values.step + }), + // this is used to verify that the client generates the correct codes: + response: me.lookup('challenge').value + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response, opts) { + me.getView().close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + deleteTFA: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var params = { + userid: me.getView().userid, + action: 'delete' + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response, opts) { + me.getView().close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + randomizeSecret: function() { + var me = this; + var rnd = new Uint8Array(16); + window.crypto.getRandomValues(rnd); + var data = ''; + rnd.forEach(function(b) { + // just use the first 5 bit + b = b & 0x1f; + if (b < 26) { + // A..Z + data += String.fromCharCode(b + 0x41); + } else { + // 2..7 + data += String.fromCharCode(b-26 + 0x32); + } + }); + me.lookup('tfa_secret').setValue(data); + }, + + startU2FRegistration: function() { + var me = this; + + var params = { + userid: me.getView().userid, + action: 'new' + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response) { + me.getView().doU2FChallenge(response); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }, + + items: [ + { + xtype: 'tabpanel', + itemId: 'tfatabs', + border: false, + items: [ + { + xtype: 'panel', + title: 'TOTP', + itemId: 'totp-panel', + border: false, + layout: { + type: 'vbox', + align: 'stretch' + }, + items: [ + { + xtype: 'form', + layout: 'anchor', + border: false, + reference: 'totp_form', + fieldDefaults: { + anchor: '100%', + padding: '0 5' + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('User name'), + cbind: { + value: '{userid}' + } + }, + { + layout: 'hbox', + border: false, + padding: '0 0 5 0', + items: [{ + xtype: 'textfield', + fieldLabel: gettext('Secret'), + emptyText: gettext('Unchanged'), + name: 'secret', + reference: 'tfa_secret', + regex: /^[A-Z2-7=]+$/, + regexText: 'Must be base32 [A-Z2-7=]', + maskRe: /[A-Z2-7=]/, + qrupdate: true, + flex: 4 + }, + { + xtype: 'button', + text: gettext('Randomize'), + reference: 'randomize_button', + handler: 'randomizeSecret', + flex: 1 + }] + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Time period'), + name: 'step', + // Google Authenticator ignores this and generates bogus data + hidden: true, + value: 30, + minValue: 10, + qrupdate: true + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Digits'), + name: 'digits', + value: 6, + // Google Authenticator ignores this and generates bogus data + hidden: true, + minValue: 6, + maxValue: 8, + qrupdate: true + }, + { + xtype: 'textfield', + fieldLabel: gettext('Issuer Name'), + name: 'issuer', + value: 'Proxmox Web UI', + qrupdate: true + } + ] + }, + { + xtype: 'box', + itemId: 'qrbox', + visible: false, // will be enabled when generating a qr code + style: { + 'background-color': '#23272a', + padding: '5px', + width: '266px', + height: '266px' + } + }, + { + xtype: 'textfield', + fieldLabel: gettext('Verification Code'), + allowBlank: false, + reference: 'challenge', + padding: '0 5', + emptyText: gettext('Scan QR code and enter TOTP auth. code to verify') + } + ] + }, + { + title: 'U2F', + itemId: 'u2f-panel', + reference: 'u2f_panel', + border: false, + padding: '5 5', + layout: { + type: 'vbox', + align: 'middle' + }, + bind: { + disabled: '{!u2f_available}' + }, + items: [ + { + xtype: 'label', + width: 500, + text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.') + } + ] + } + ] + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + reference: 'password', + allowBlank: false, + validateBlank: true, + padding: '0 0 5 5', + emptyText: gettext('verify current password') + } + ], + + buttons: [ + { + xtype: 'proxmoxHelpButton' + }, + '->', + { + text: gettext('Apply'), + handler: 'applySettings', + bind: { + hidden: '{!in_totp_tab}', + disabled: '{!valid}' + } + }, + { + xtype: 'button', + text: gettext('Register U2F Device'), + handler: 'startU2FRegistration', + bind: { + hidden: '{in_totp_tab}' + } + }, + { + text: gettext('Delete'), + reference: 'delete_button', + handler: 'deleteTFA', + bind: { + disabled: '{!canDeleteTFA}' + } + } + ], + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-domains', + autoLoad: true + }); + + store.on('load', function() { + var user_realm = me.userid.split('@')[1]; + var realm = me.store.findRecord('realm', user_realm); + me.afterLoadingRealm(realm && realm.data && realm.data.tfa); + }, me); + + Ext.apply(me, { store: store }); + + me.callParent(); + + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth'); + } +}); +Ext.define('PVE.dc.UserEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcUserEdit'], + + isAdd: true, + + initComponent : function() { + var me = this; + + me.isCreate = !me.userid; + + var url; + var method; + var realm; + + if (me.isCreate) { + url = '/api2/extjs/access/users'; + method = 'POST'; + } else { + url = '/api2/extjs/access/users/' + me.userid; + method = 'PUT'; + } + + var verifypw; + var pwfield; + + var validate_pw = function() { + if (verifypw.getValue() !== pwfield.getValue()) { + return gettext("Passwords do not match"); + } + return true; + }; + + verifypw = Ext.createWidget('textfield', { + inputType: 'password', + fieldLabel: gettext('Confirm password'), + name: 'verifypassword', + submitValue: false, + disabled: true, + hidden: true, + validator: validate_pw + }); + + pwfield = Ext.createWidget('textfield', { + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + name: 'password', + disabled: true, + hidden: true, + validator: validate_pw + }); + + var update_passwd_field = function(realm) { + if (realm === 'pve') { + pwfield.setVisible(true); + pwfield.setDisabled(false); + verifypw.setVisible(true); + verifypw.setDisabled(false); + } else { + pwfield.setVisible(false); + pwfield.setDisabled(true); + verifypw.setVisible(false); + verifypw.setDisabled(true); + } + + }; + + var column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'userid', + fieldLabel: gettext('User name'), + value: me.userid, + allowBlank: false, + submitValue: me.isCreate ? true : false + }, + pwfield, verifypw, + { + xtype: 'pveGroupSelector', + name: 'groups', + multiSelect: true, + allowBlank: true, + fieldLabel: gettext('Group') + }, + { + xtype: 'datefield', + name: 'expire', + emptyText: 'never', + format: 'Y-m-d', + submitFormat: 'U', + fieldLabel: gettext('Expire') + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enabled'), + name: 'enable', + uncheckedValue: 0, + defaultValue: 1, + checked: true + } + ]; + + var column2 = [ + { + xtype: 'textfield', + name: 'firstname', + fieldLabel: gettext('First Name') + }, + { + xtype: 'textfield', + name: 'lastname', + fieldLabel: gettext('Last Name') + }, + { + xtype: 'textfield', + name: 'email', + fieldLabel: gettext('E-Mail'), + vtype: 'proxmoxMail' + } + ]; + + if (me.isCreate) { + column1.splice(1,0,{ + xtype: 'pveRealmComboBox', + name: 'realm', + fieldLabel: gettext('Realm'), + allowBlank: false, + matchFieldWidth: false, + listConfig: { width: 300 }, + listeners: { + change: function(combo, newValue){ + realm = newValue; + update_passwd_field(realm); + } + }, + submitValue: false + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + column1: column1, + column2: column2, + columnB: [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + } + ], + advancedItems: [ + { + xtype: 'textfield', + name: 'keys', + fieldLabel: gettext('Key IDs') + } + ], + onGetValues: function(values) { + // hack: ExtJS datefield does not submit 0, so we need to set that + if (!values.expire) { + values.expire = 0; + } + + if (realm) { + values.userid = values.userid + '@' + realm; + } + + if (!values.password) { + delete values.password; + } + + return values; + } + }); + + Ext.applyIf(me, { + subject: gettext('User'), + url: url, + method: method, + fieldDefaults: { + labelWidth: 110 // for spanish translation + }, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (Ext.isDefined(data.expire)) { + if (data.expire) { + data.expire = new Date(data.expire * 1000); + } else { + // display 'never' instead of '1970-01-01' + data.expire = null; + } + } + me.setValues(data); + } + }); + } + } +}); +/*jslint confusion: true */ +Ext.define('PVE.dc.UserView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveUserView'], + + onlineHelp: 'pveum_users', + + stateful: true, + stateId: 'grid-users', + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + var store = new Ext.data.Store({ + id: "users", + model: 'pve-users', + sorters: { + property: 'userid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/access/users/', + enableFn: function(rec) { + if (!caps.access['User.Modify']) { + return false; + } + return rec.data.userid !== 'root@pam'; + }, + callback: function() { + reload(); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec || !caps.access['User.Modify']) { + return; + } + + var win = Ext.create('PVE.dc.UserEdit',{ + userid: rec.data.userid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + enableFn: function(rec) { + return !!caps.access['User.Modify']; + }, + selModel: sm, + handler: run_editor + }); + + var pwchange_btn = new Proxmox.button.Button({ + text: gettext('Password'), + disabled: true, + selModel: sm, + handler: function(btn, event, rec) { + var win = Ext.create('Proxmox.window.PasswordEdit', { + userid: rec.data.userid + }); + win.on('destroy', reload); + win.show(); + } + }); + + var tfachange_btn = new Proxmox.button.Button({ + text: 'TFA', + disabled: true, + selModel: sm, + handler: function(btn, event, rec) { + var d = rec.data; + var win = Ext.create('PVE.window.TFAEdit',{ + hasTFA: d.keys != undefined && d.keys.length, + userid: d.userid + }); + win.on('destroy', reload); + win.show(); + } + }); + + var tbar = [ + { + text: gettext('Add'), + disabled: !caps.access['User.Modify'], + handler: function() { + var win = Ext.create('PVE.dc.UserEdit',{ + }); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn, pwchange_btn, tfachange_btn + ]; + + var render_username = function(userid) { + return userid.match(/^(.+)(@[^@]+)$/)[1]; + }; + + var render_realm = function(userid) { + return userid.match(/@([^@]+)$/)[1]; + }; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('User name'), + width: 200, + sortable: true, + renderer: render_username, + dataIndex: 'userid' + }, + { + header: gettext('Realm'), + width: 100, + sortable: true, + renderer: render_realm, + dataIndex: 'userid' + }, + { + header: gettext('Enabled'), + width: 80, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'enable' + }, + { + header: gettext('Expire'), + width: 80, + sortable: true, + renderer: Proxmox.Utils.format_expire, + dataIndex: 'expire' + }, + { + header: gettext('Name'), + width: 150, + sortable: true, + renderer: PVE.Utils.render_full_name, + dataIndex: 'firstname' + }, + { + header: 'TFA', + width: 50, + sortable: true, + renderer: function(v) { + return Proxmox.Utils.format_boolean(v !== undefined && v.length); + }, + dataIndex: 'keys' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.PoolView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pvePoolView'], + + onlineHelp: 'pveum_pools', + + stateful: true, + stateId: 'grid-pools', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-pools', + sorters: { + property: 'poolid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/pools/', + callback: function () { + reload(); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.PoolEdit',{ + poolid: rec.data.poolid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var tbar = [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.PoolEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ]; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Name'), + width: 200, + sortable: true, + dataIndex: 'poolid' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.PoolEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcPoolEdit'], + + initComponent : function() { + var me = this; + + me.isCreate = !me.poolid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/pools'; + method = 'POST'; + } else { + url = '/api2/extjs/pools/' + me.poolid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Pool'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'poolid', + value: me.poolid, + allowBlank: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + allowBlank: true + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load(); + } + } +}); +Ext.define('PVE.dc.GroupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveGroupView'], + + onlineHelp: 'pveum_groups', + + stateful: true, + stateId: 'grid-groups', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-groups', + sorters: { + property: 'groupid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + callback: function() { + reload(); + }, + baseurl: '/access/groups/' + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.GroupEdit',{ + groupid: rec.data.groupid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var tbar = [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.GroupEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ]; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Name'), + width: 200, + sortable: true, + dataIndex: 'groupid' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.GroupEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcGroupEdit'], + + initComponent : function() { + var me = this; + + me.isCreate = !me.groupid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/access/groups'; + method = 'POST'; + } else { + url = '/api2/extjs/access/groups/' + me.groupid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Group'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'groupid', + value: me.groupid, + allowBlank: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + allowBlank: true + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load(); + } + } +}); +Ext.define('PVE.dc.RoleView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveRoleView'], + + onlineHelp: 'pveum_roles', + + stateful: true, + stateId: 'grid-roles', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-roles', + sorters: { + property: 'roleid', + order: 'DESC' + } + }); + + var render_privs = function(value, metaData) { + + if (!value) { + return '-'; + } + + // allow word wrap + metaData.style = 'white-space:normal;'; + + return value.replace(/\,/g, ' '); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + if (rec.data.special === "1") { + return; + } + + var win = Ext.create('PVE.dc.RoleEdit',{ + roleid: rec.data.roleid, + privs: rec.data.privs + }); + win.on('destroy', reload); + win.show(); + }; + + Ext.apply(me, { + store: store, + selModel: sm, + + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Built-In'), + width: 65, + sortable: true, + dataIndex: 'special', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('Name'), + width: 150, + sortable: true, + dataIndex: 'roleid' + }, + { + itemid: 'privs', + header: gettext('Privileges'), + sortable: false, + renderer: render_privs, + dataIndex: 'privs', + flex: 1 + } + ], + listeners: { + activate: function() { + store.load(); + }, + itemdblclick: run_editor + }, + tbar: [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.RoleEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + enableFn: function(record) { + return record.data.special !== '1'; + } + }, + { + xtype: 'proxmoxStdRemoveButton', + selModel: sm, + callback: function() { + reload(); + }, + baseurl: '/access/roles/', + enableFn: function(record) { + return record.data.special !== '1'; + } + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.RoleEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveDcRoleEdit', + + width: 400, + + initComponent : function() { + var me = this; + + me.isCreate = !me.roleid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/access/roles'; + method = 'POST'; + } else { + url = '/api2/extjs/access/roles/' + me.roleid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Role'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + name: 'roleid', + value: me.roleid, + allowBlank: false, + fieldLabel: gettext('Name') + }, + { + xtype: 'pvePrivilegesSelector', + name: 'privs', + value: me.privs, + allowBlank: false, + fieldLabel: gettext('Privileges') + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response) { + var data = response.result.data; + var keys = Ext.Object.getKeys(data); + + me.setValues({ + privs: keys, + roleid: me.roleid + }); + } + }); + } + } +}); +Ext.define('PVE.dc.ACLAdd', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveACLAdd'], + url: '/access/acl', + method: 'PUT', + isAdd: true, + initComponent : function() { + + var me = this; + + me.isCreate = true; + + var items = [ + { + xtype: me.path ? 'hiddenfield' : 'pvePermPathSelector', + name: 'path', + value: me.path, + allowBlank: false, + fieldLabel: gettext('Path') + } + ]; + + if (me.aclType === 'group') { + me.subject = gettext("Group Permission"); + items.push({ + xtype: 'pveGroupSelector', + name: 'groups', + fieldLabel: gettext('Group') + }); + } else if (me.aclType === 'user') { + me.subject = gettext("User Permission"); + items.push({ + xtype: 'pveUserSelector', + name: 'users', + fieldLabel: gettext('User') + }); + } else { + throw "unknown ACL type"; + } + + items.push({ + xtype: 'pveRoleSelector', + name: 'roles', + value: 'NoAccess', + fieldLabel: gettext('Role') + }); + + if (!me.path) { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'propagate', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Propagate') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + items: items, + onlineHelp: 'pveum_permission_management' + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.dc.ACLView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveACLView'], + + onlineHelp: 'chapter_user_management', + + stateful: true, + stateId: 'grid-acls', + + // use fixed path + path: undefined, + + initComponent : function() { + var me = this; + + var store = Ext.create('Ext.data.Store',{ + model: 'pve-acl', + proxy: { + type: 'proxmox', + url: "/api2/json/access/acl" + }, + sorters: { + property: 'path', + order: 'DESC' + } + }); + + if (me.path) { + store.addFilter(Ext.create('Ext.util.Filter',{ + filterFn: function(item) { + if (item.data.path === me.path) { + return true; + } + } + })); + } + + var render_ugid = function(ugid, metaData, record) { + if (record.data.type == 'group') { + return '@' + ugid; + } + + return ugid; + }; + + var columns = [ + { + header: gettext('User') + '/' + gettext('Group'), + flex: 1, + sortable: true, + renderer: render_ugid, + dataIndex: 'ugid' + }, + { + header: gettext('Role'), + flex: 1, + sortable: true, + dataIndex: 'roleid' + } + ]; + + if (!me.path) { + columns.unshift({ + header: gettext('Path'), + flex: 1, + sortable: true, + dataIndex: 'path' + }); + columns.push({ + header: gettext('Propagate'), + width: 80, + sortable: true, + dataIndex: 'propagate' + }); + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + }; + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: gettext('Are you sure you want to remove this entry'), + handler: function(btn, event, rec) { + var params = { + 'delete': 1, + path: rec.data.path, + roles: rec.data.roleid + }; + if (rec.data.type === 'group') { + params.groups = rec.data.ugid; + } else if (rec.data.type === 'user') { + params.users = rec.data.ugid; + } else { + throw 'unknown data type'; + } + + Proxmox.Utils.API2Request({ + url: '/access/acl', + params: params, + method: 'PUT', + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: { + xtype: 'menu', + items: [ + { + text: gettext('Group Permission'), + iconCls: 'fa fa-fw fa-group', + handler: function() { + var win = Ext.create('PVE.dc.ACLAdd',{ + aclType: 'group', + path: me.path + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('User Permission'), + iconCls: 'fa fa-fw fa-user', + handler: function() { + var win = Ext.create('PVE.dc.ACLAdd',{ + aclType: 'user', + path: me.path + }); + win.on('destroy', reload); + win.show(); + } + } + ] + } + }, + remove_btn + ], + viewConfig: { + trackOver: false + }, + columns: columns, + listeners: { + activate: reload + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-acl', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'ugid', 'roleid', + { + name: 'propagate', + type: 'boolean' + } + ] + }); + +}); +Ext.define('PVE.dc.AuthView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveAuthView'], + + onlineHelp: 'pveum_authentication_realms', + + stateful: true, + stateId: 'grid-authrealms', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-domains', + sorters: { + property: 'realm', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.AuthEdit',{ + realm: rec.data.realm, + authType: rec.data.type + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + baseurl: '/access/domains/', + selModel: sm, + enableFn: function(rec) { + return !(rec.data.type === 'pve' || rec.data.type === 'pam'); + }, + callback: function() { + reload(); + } + }); + + var tbar = [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Active Directory Server'), + handler: function() { + var win = Ext.create('PVE.dc.AuthEdit', { + authType: 'ad' + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('LDAP Server'), + handler: function() { + var win = Ext.create('PVE.dc.AuthEdit',{ + authType: 'ldap' + }); + win.on('destroy', reload); + win.show(); + } + } + ] + }) + }, + edit_btn, remove_btn + ]; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Realm'), + width: 100, + sortable: true, + dataIndex: 'realm' + }, + { + header: gettext('Type'), + width: 100, + sortable: true, + dataIndex: 'type' + }, + { + header: gettext('TFA'), + width: 100, + sortable: true, + dataIndex: 'tfa' + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.AuthEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcAuthEdit'], + + isAdd: true, + + initComponent : function() { + var me = this; + + me.isCreate = !me.realm; + + var url; + var method; + var serverlist; + + if (me.isCreate) { + url = '/api2/extjs/access/domains'; + method = 'POST'; + } else { + url = '/api2/extjs/access/domains/' + me.realm; + method = 'PUT'; + } + + var column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'realm', + fieldLabel: gettext('Realm'), + value: me.realm, + allowBlank: false + } + ]; + + if (me.authType === 'ad') { + + me.subject = gettext('Active Directory Server'); + + column1.push({ + xtype: 'textfield', + name: 'domain', + fieldLabel: gettext('Domain'), + emptyText: 'company.net', + allowBlank: false + }); + + } else if (me.authType === 'ldap') { + + me.subject = gettext('LDAP Server'); + + column1.push({ + xtype: 'textfield', + name: 'base_dn', + fieldLabel: gettext('Base Domain Name'), + emptyText: 'CN=Users,DC=Company,DC=net', + allowBlank: false + }); + + column1.push({ + xtype: 'textfield', + name: 'user_attr', + emptyText: 'uid / sAMAccountName', + fieldLabel: gettext('User Attribute Name'), + allowBlank: false + }); + } else if (me.authType === 'pve') { + + if (me.isCreate) { + throw 'unknown auth type'; + } + + me.subject = 'Proxmox VE authentication server'; + + } else if (me.authType === 'pam') { + + if (me.isCreate) { + throw 'unknown auth type'; + } + + me.subject = 'linux PAM'; + + } else { + throw 'unknown auth type '; + } + + column1.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Default'), + name: 'default', + uncheckedValue: 0 + }); + + var column2 = []; + + if (me.authType === 'ldap' || me.authType === 'ad') { + column2.push( + { + xtype: 'textfield', + fieldLabel: gettext('Server'), + name: 'server1', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Fallback Server'), + deleteEmpty: !me.isCreate, + name: 'server2' + }, + { + xtype: 'proxmoxintegerfield', + name: 'port', + fieldLabel: gettext('Port'), + minValue: 1, + maxValue: 65535, + emptyText: gettext('Default'), + submitEmptyText: false + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'SSL', + name: 'secure', + uncheckedValue: 0 + } + ); + } + + // Two Factor Auth settings + + column2.push({ + xtype: 'proxmoxKVComboBox', + name: 'tfa', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('TFA'), + comboItems: [ ['__default__', Proxmox.Utils.noneText], ['oath', 'OATH'], ['yubico', 'Yubico']], + listeners: { + change: function(f, value) { + if (!me.rendered) { + return; + } + me.down('field[name=oath_step]').setVisible(value === 'oath'); + me.down('field[name=oath_digits]').setVisible(value === 'oath'); + me.down('field[name=yubico_api_id]').setVisible(value === 'yubico'); + me.down('field[name=yubico_api_key]').setVisible(value === 'yubico'); + me.down('field[name=yubico_url]').setVisible(value === 'yubico'); + } + } + }); + + column2.push({ + xtype: 'proxmoxintegerfield', + name: 'oath_step', + value: '', + minValue: 10, + emptyText: Proxmox.Utils.defaultText + ' (30)', + submitEmptyText: false, + hidden: true, + fieldLabel: 'OATH time step' + }); + + column2.push({ + xtype: 'proxmoxintegerfield', + name: 'oath_digits', + value: '', + minValue: 6, + maxValue: 8, + emptyText: Proxmox.Utils.defaultText + ' (6)', + submitEmptyText: false, + hidden: true, + fieldLabel: 'OATH password length' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_api_id', + hidden: true, + fieldLabel: 'Yubico API Id' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_api_key', + hidden: true, + fieldLabel: 'Yubico API Key' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_url', + hidden: true, + fieldLabel: 'Yubico URL' + }); + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + column1: column1, + column2: column2, + columnB: [{ + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + }], + onGetValues: function(values) { + if (!values.port) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'port' }); + } + delete values.port; + } + + if (me.isCreate) { + values.type = me.authType; + } + + if (values.tfa === 'oath') { + values.tfa = "type=oath"; + if (values.oath_step) { + values.tfa += ",step=" + values.oath_step; + } + if (values.oath_digits) { + values.tfa += ",digits=" + values.oath_digits; + } + } else if (values.tfa === 'yubico') { + values.tfa = "type=yubico"; + values.tfa += ",id=" + values.yubico_api_id; + values.tfa += ",key=" + values.yubico_api_key; + if (values.yubico_url) { + values.tfa += ",url=" + values.yubico_url; + } + } else { + delete values.tfa; + } + + delete values.oath_step; + delete values.oath_digits; + delete values.yubico_api_id; + delete values.yubico_api_key; + delete values.yubico_url; + + return values; + } + }); + + Ext.applyIf(me, { + url: url, + method: method, + fieldDefaults: { + labelWidth: 120 + }, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var data = response.result.data || {}; + // just to be sure (should not happen) + if (data.type !== me.authType) { + me.close(); + throw "got wrong auth type"; + } + + if (data.tfa) { + var tfacfg = PVE.Parser.parseTfaConfig(data.tfa); + data.tfa = tfacfg.type; + if (tfacfg.type === 'yubico') { + data.yubico_api_key = tfacfg.key; + data.yubico_api_id = tfacfg.id; + data.yubico_url = tfacfg.url; + } else if (tfacfg.type === 'oath') { + // step is a number before + /*jslint confusion: true*/ + data.oath_step = tfacfg.step; + data.oath_digits = tfacfg.digits; + /*jslint confusion: false*/ + } + } + + me.setValues(data); + } + }); + } + } +}); +Ext.define('PVE.dc.BackupEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcBackupEdit'], + + defaultFocus: undefined, + + initComponent : function() { + var me = this; + + me.isCreate = !me.jobid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/cluster/backup'; + method = 'POST'; + } else { + url = '/api2/extjs/cluster/backup/' + me.jobid; + method = 'PUT'; + } + + var vmidField = Ext.create('Ext.form.field.Hidden', { + name: 'vmid' + }); + + /*jslint confusion: true*/ + // 'value' can be assigned a string or an array + var selModeField = Ext.create('Proxmox.form.KVComboBox', { + xtype: 'proxmoxKVComboBox', + comboItems: [ + ['include', gettext('Include selected VMs')], + ['all', gettext('All')], + ['exclude', gettext('Exclude selected VMs')] + ], + fieldLabel: gettext('Selection mode'), + name: 'selMode', + value: '' + }); + + var sm = Ext.create('Ext.selection.CheckboxModel', { + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected) { + var sel = []; + Ext.Array.each(selected, function(record) { + sel.push(record.data.vmid); + }); + + // to avoid endless recursion suspend the vmidField change + // event temporary as it calls us again + vmidField.suspendEvent('change'); + vmidField.setValue(sel); + vmidField.resumeEvent('change'); + } + } + }); + + var storagesel = Ext.create('PVE.form.StorageSelector', { + fieldLabel: gettext('Storage'), + nodename: 'localhost', + storageContent: 'backup', + allowBlank: false, + name: 'storage' + }); + + var store = new Ext.data.Store({ + model: 'PVEResources', + sorters: { + property: 'vmid', + order: 'ASC' + } + }); + + var vmgrid = Ext.createWidget('grid', { + store: store, + border: true, + height: 300, + selModel: sm, + disabled: true, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 60 + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'uptime', + renderer: function(value) { + if (value) { + return Proxmox.Utils.runningText; + } else { + return Proxmox.Utils.stoppedText; + } + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('Type'), + dataIndex: 'type' + } + ] + }); + + var nodesel = Ext.create('PVE.form.NodeSelector', { + name: 'node', + fieldLabel: gettext('Node'), + allowBlank: true, + editable: true, + autoSelect: false, + emptyText: '-- ' + gettext('All') + ' --', + listeners: { + change: function(f, value) { + storagesel.setNodename(value || 'localhost'); + var mode = selModeField.getValue(); + store.clearFilter(); + store.filterBy(function(rec) { + return (!value || rec.get('node') === value); + }); + if (mode === 'all') { + sm.selectAll(true); + } + } + } + }); + + var column1 = [ + nodesel, + storagesel, + { + xtype: 'pveDayOfWeekSelector', + name: 'dow', + fieldLabel: gettext('Day of week'), + multiSelect: true, + value: ['sat'], + allowBlank: false + }, + { + xtype: 'timefield', + fieldLabel: gettext('Start Time'), + name: 'starttime', + format: 'H:i', + formatText: 'HH:MM', + value: '00:00', + allowBlank: false + }, + selModeField + ]; + + var column2 = [ + { + xtype: 'textfield', + fieldLabel: gettext('Send email to'), + name: 'mailto' + }, + { + xtype: 'pveEmailNotificationSelector', + fieldLabel: gettext('Email notification'), + name: 'mailnotification', + deleteEmpty: me.isCreate ? false : true, + value: me.isCreate ? 'always' : '' + }, + { + xtype: 'pveCompressionSelector', + fieldLabel: gettext('Compression'), + name: 'compress', + deleteEmpty: me.isCreate ? false : true, + value: 'lzo' + }, + { + xtype: 'pveBackupModeSelector', + fieldLabel: gettext('Mode'), + value: 'snapshot', + name: 'mode' + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable'), + name: 'enabled', + uncheckedValue: 0, + defaultValue: 1, + checked: true + }, + vmidField + ]; + /*jslint confusion: false*/ + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + onlineHelp: 'chapter_vzdump', + column1: column1, + column2: column2, + onGetValues: function(values) { + if (!values.node) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' }); + } + delete values.node; + } + + var selMode = values.selMode; + delete values.selMode; + + if (selMode === 'all') { + values.all = 1; + values.exclude = ''; + delete values.vmid; + } else if (selMode === 'exclude') { + values.all = 1; + values.exclude = values.vmid; + delete values.vmid; + } + return values; + } + }); + + var update_vmid_selection = function(list, mode) { + if (mode !== 'all') { + sm.deselectAll(true); + if (list) { + Ext.Array.each(list.split(','), function(vmid) { + var rec = store.findRecord('vmid', vmid); + if (rec) { + sm.select(rec, true); + } + }); + } + } + }; + + vmidField.on('change', function(f, value) { + var mode = selModeField.getValue(); + update_vmid_selection(value, mode); + }); + + selModeField.on('change', function(f, value, oldValue) { + if (value === 'all') { + sm.selectAll(true); + vmgrid.setDisabled(true); + } else { + vmgrid.setDisabled(false); + } + if (oldValue === 'all') { + sm.deselectAll(true); + vmidField.setValue(''); + } + var list = vmidField.getValue(); + update_vmid_selection(list, value); + }); + + var reload = function() { + store.load({ + params: { type: 'vm' }, + callback: function() { + var node = nodesel.getValue(); + store.clearFilter(); + store.filterBy(function(rec) { + return (!node || node.length === 0 || rec.get('node') === node); + }); + var list = vmidField.getValue(); + var mode = selModeField.getValue(); + if (mode === 'all') { + sm.selectAll(true); + } else { + update_vmid_selection(list, mode); + } + } + }); + }; + + Ext.applyIf(me, { + subject: gettext("Backup Job"), + url: url, + method: method, + items: [ ipanel, vmgrid ] + }); + + me.callParent(); + + if (me.isCreate) { + selModeField.setValue('include'); + } else { + me.load({ + success: function(response, options) { + var data = response.result.data; + + data.dow = data.dow.split(','); + + if (data.all || data.exclude) { + if (data.exclude) { + data.vmid = data.exclude; + data.selMode = 'exclude'; + } else { + data.vmid = ''; + data.selMode = 'all'; + } + } else { + data.selMode = 'include'; + } + + me.setValues(data); + } + }); + } + + reload(); + } +}); + + +Ext.define('PVE.dc.BackupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveDcBackupView'], + + onlineHelp: 'chapter_vzdump', + + allText: '-- ' + gettext('All') + ' --', + allExceptText: gettext('All except {0}'), + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-cluster-backup', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/backup" + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.BackupEdit',{ + jobid: rec.data.id + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/backup', + callback: function() { + reload(); + } + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + stateful: true, + stateId: 'grid-dc-backup', + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Add'), + handler: function() { + var win = Ext.create('PVE.dc.BackupEdit',{}); + win.on('destroy', reload); + win.show(); + } + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: gettext('Enabled'), + width: 80, + dataIndex: 'enabled', + xtype: 'checkcolumn', + sortable: true, + disabled: true, + disabledCls: 'x-item-enabled', + stopSelection: false + }, + { + header: gettext('Node'), + width: 100, + sortable: true, + dataIndex: 'node', + renderer: function(value) { + if (value) { + return value; + } + return me.allText; + } + }, + { + header: gettext('Day of week'), + width: 200, + sortable: false, + dataIndex: 'dow', + renderer: function(val) { + var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + var selected = []; + var cur = -1; + val.split(',').forEach(function(day){ + cur++; + var dow = (dows.indexOf(day)+6)%7; + if (cur === dow) { + if (selected.length === 0 || selected[selected.length-1] === 0) { + selected.push(1); + } else { + selected[selected.length-1]++; + } + } else { + while (cur < dow) { + cur++; + selected.push(0); + } + selected.push(1); + } + }); + + cur = -1; + var days = []; + selected.forEach(function(item) { + cur++; + if (item > 2) { + days.push(Ext.Date.dayNames[(cur+1)] + '-' + Ext.Date.dayNames[(cur+item)%7]); + cur += item-1; + } else if (item == 2) { + days.push(Ext.Date.dayNames[cur+1]); + days.push(Ext.Date.dayNames[(cur+2)%7]); + cur++; + } else if (item == 1) { + days.push(Ext.Date.dayNames[(cur+1)%7]); + } + }); + return days.join(', '); + } + }, + { + header: gettext('Start Time'), + width: 60, + sortable: true, + dataIndex: 'starttime' + }, + { + header: gettext('Storage'), + width: 100, + sortable: true, + dataIndex: 'storage' + }, + { + header: gettext('Selection'), + flex: 1, + sortable: false, + dataIndex: 'vmid', + renderer: function(value, metaData, record) { + /*jslint confusion: true */ + if (record.data.all) { + if (record.data.exclude) { + return Ext.String.format(me.allExceptText, record.data.exclude); + } + return me.allText; + } + if (record.data.vmid) { + return record.data.vmid; + } + + return "-"; + } + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-cluster-backup', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'starttime', 'dow', + 'storage', 'node', 'vmid', 'exclude', + 'mailto', + { name: 'enabled', type: 'boolean' }, + { name: 'all', type: 'boolean' }, + { name: 'snapshot', type: 'boolean' }, + { name: 'stop', type: 'boolean' }, + { name: 'suspend', type: 'boolean' }, + { name: 'compress', type: 'boolean' } + ] + }); +}); +Ext.define('PVE.dc.Support', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcSupport', + pveGuidePath: '/pve-docs/index.html', + onlineHelp: 'getting_help', + + invalidHtml: '

No valid subscription

' + PVE.Utils.noSubKeyHtml, + + communityHtml: 'Please use the public community forum for any questions.', + + activeHtml: 'Please use our support portal for any questions. You can also use the public community forum to get additional information.', + + bugzillaHtml: '

Bug Tracking

Our bug tracking system is available here.', + + docuHtml: function() { + var me = this; + var guideUrl = window.location.origin + me.pveGuidePath; + var text = Ext.String.format('

Documentation

' + + 'The official Proxmox VE Administration Guide' + + ' is included with this installation and can be browsed at ' + + '{0}', guideUrl); + return text; + }, + + updateActive: function(data) { + var me = this; + + var html = '

' + data.productname + '

' + me.activeHtml; + html += '

' + me.docuHtml(); + html += '

' + me.bugzillaHtml; + + me.update(html); + }, + + updateCommunity: function(data) { + var me = this; + + var html = '

' + data.productname + '

' + me.communityHtml; + html += '

' + me.docuHtml(); + html += '

' + me.bugzillaHtml; + + me.update(html); + }, + + updateInactive: function(data) { + var me = this; + me.update(me.invalidHtml); + }, + + initComponent: function() { + var me = this; + + var reload = function() { + Proxmox.Utils.API2Request({ + url: '/nodes/localhost/subscription', + method: 'GET', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.update('Unable to load subscription status' + ": " + response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data; + + if (data.status === 'Active') { + if (data.level === 'c') { + me.updateCommunity(data); + } else { + me.updateActive(data); + } + } else { + me.updateInactive(data); + } + } + }); + }; + + Ext.apply(me, { + autoScroll: true, + bodyStyle: 'padding:10px', + listeners: { + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('pve-security-groups', { + extend: 'Ext.data.Model', + + fields: [ 'group', 'comment', 'digest' ], + idProperty: 'group' +}); + +Ext.define('PVE.SecurityGroupEdit', { + extend: 'Proxmox.window.Edit', + + base_url: "/cluster/firewall/groups", + + allow_iface: false, + + initComponent : function() { + var me = this; + + me.isCreate = (me.group_name === undefined); + + var subject; + + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + + var items = [ + { + xtype: 'textfield', + name: 'group', + value: me.group_name || '', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: me.group_comment || '', + fieldLabel: gettext('Comment') + } + ]; + + if (me.isCreate) { + subject = gettext('Security Group'); + } else { + subject = gettext('Security Group') + " '" + me.group_name + "'"; + items.push({ + xtype: 'hiddenfield', + name: 'rename', + value: me.group_name + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + // InputPanel does not have a 'create' property, does it need a 'isCreate' + isCreate: me.isCreate, + items: items + }); + + + Ext.apply(me, { + subject: subject, + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.SecurityGroupList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveSecurityGroupList', + + stateful: true, + stateId: 'grid-securitygroups', + + rule_panel: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + base_url: "/cluster/firewall/groups", + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (me.rule_panel == undefined) { + throw "no rule panel specified"; + } + + if (me.base_url == undefined) { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-security-groups', + proxy: { + type: 'proxmox', + url: '/api2/json' + me.base_url + }, + sorters: { + property: 'group', + 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('group', oldrec.data.group); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.SecurityGroupEdit', { + digest: rec.data.digest, + group_name: rec.data.group, + group_comment: rec.data.comment + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('PVE.SecurityGroupEdit', {}); + win.show(); + win.on('destroy', reload); + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + enableFn: function(rec) { + return (rec && me.base_url); + }, + callback: function() { + reload(); + } + }); + + Ext.apply(me, { + store: store, + tbar: [ '' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: gettext('Group'), dataIndex: 'group', width: '100' }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } + ], + listeners: { + itemdblclick: run_editor, + select: function(sm, rec) { + var url = '/cluster/firewall/groups/' + rec.data.group; + me.rule_panel.setBaseUrl(url); + }, + deselect: function() { + me.rule_panel.setBaseUrl(undefined); + }, + show: reload + } + }); + + me.callParent(); + + store.load(); + } +}); + +Ext.define('PVE.SecurityGroups', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSecurityGroups', + + title: 'Security Groups', + + initComponent: function() { + var me = this; + + var rule_panel = Ext.createWidget('pveFirewallRules', { + region: 'center', + allow_groups: false, + list_refs_url: '/cluster/firewall/refs', + tbar_prefix: '' + gettext('Rules') + ':', + border: false + }); + + var sglist = Ext.createWidget('pveSecurityGroupList', { + region: 'west', + rule_panel: rule_panel, + width: '25%', + border: false, + split: true + }); + + + Ext.apply(me, { + layout: 'border', + items: [ sglist, rule_panel ], + listeners: { + show: function() { + sglist.fireEvent('show', sglist); + } + } + }); + + me.callParent(); + } +}); +/* + * Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected + */ + +Ext.define('PVE.dc.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.dc.Config', + + onlineHelp: 'pve_admin_guide', + + initComponent: function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + me.items = []; + + Ext.apply(me, { + title: gettext("Datacenter"), + hstateid: 'dctab' + }); + + if (caps.dc['Sys.Audit']) { + me.items.push({ + title: gettext('Summary'), + xtype: 'pveDcSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + }, + { + title: gettext('Cluster'), + xtype: 'pveClusterAdministration', + iconCls: 'fa fa-server', + itemId: 'cluster' + }, + { + xtype: 'pveDcOptionView', + title: gettext('Options'), + iconCls: 'fa fa-gear', + itemId: 'options' + }); + } + + if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveStorageView', + title: gettext('Storage'), + iconCls: 'fa fa-database', + itemId: 'storage' + }); + } + + if (caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveDcBackupView', + iconCls: 'fa fa-floppy-o', + title: gettext('Backup'), + itemId: 'backup' + }, + { + xtype: 'pveReplicaView', + iconCls: 'fa fa-retweet', + title: gettext('Replication'), + itemId: 'replication' + }, + { + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + expandedOnInit: true + }); + } + + me.items.push({ + xtype: 'pveUserView', + groups: ['permissions'], + iconCls: 'fa fa-user', + title: gettext('Users'), + itemId: 'users' + }); + + if (caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveGroupView', + title: gettext('Groups'), + iconCls: 'fa fa-users', + groups: ['permissions'], + itemId: 'groups' + }, + { + xtype: 'pvePoolView', + title: gettext('Pools'), + iconCls: 'fa fa-tags', + groups: ['permissions'], + itemId: 'pools' + }, + { + xtype: 'pveRoleView', + title: gettext('Roles'), + iconCls: 'fa fa-male', + groups: ['permissions'], + itemId: 'roles' + }, + { + xtype: 'pveAuthView', + title: gettext('Authentication'), + groups: ['permissions'], + iconCls: 'fa fa-key', + itemId: 'domains' + }, + { + xtype: 'pveHAStatus', + title: 'HA', + iconCls: 'fa fa-heartbeat', + itemId: 'ha' + }, + { + title: gettext('Groups'), + groups: ['ha'], + xtype: 'pveHAGroupsView', + iconCls: 'fa fa-object-group', + itemId: 'ha-groups' + }, + { + title: gettext('Fencing'), + groups: ['ha'], + iconCls: 'fa fa-bolt', + xtype: 'pveFencingView', + itemId: 'ha-fencing' + }, + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + allow_iface: true, + base_url: '/cluster/firewall/rules', + list_refs_url: '/cluster/firewall/refs', + iconCls: 'fa fa-shield', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + title: gettext('Options'), + groups: ['firewall'], + iconCls: 'fa fa-gear', + base_url: '/cluster/firewall/options', + onlineHelp: 'pve_firewall_cluster_wide_setup', + fwtype: 'dc', + itemId: 'firewall-options' + }, + { + xtype: 'pveSecurityGroups', + title: gettext('Security Group'), + groups: ['firewall'], + iconCls: 'fa fa-group', + itemId: 'firewall-sg' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: '/cluster/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: 'IPSet', + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: '/cluster/firewall/ipset', + list_refs_url: '/cluster/firewall/refs', + itemId: 'firewall-ipset' + }, + { + xtype: 'pveDcSupport', + title: gettext('Support'), + itemId: 'support', + iconCls: 'fa fa-comments-o' + }); + } + + me.callParent(); + } +}); +Ext.define('PVE.dc.NodeView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveDcNodeView', + + title: gettext('Nodes'), + disableSelection: true, + scrollable: true, + + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + dataIndex: 'name' + }, + { + header: 'ID', + width: 40, + sortable: true, + dataIndex: 'nodeid' + }, + { + header: gettext('Online'), + width: 60, + sortable: true, + dataIndex: 'online', + renderer: function(value) { + var cls = (value)?'good':'critical'; + return ''; + } + }, + { + header: gettext('Support'), + width: 100, + sortable: true, + dataIndex: 'level', + renderer: PVE.Utils.render_support_level + }, + { + header: gettext('Server Address'), + width: 115, + sortable: true, + dataIndex: 'ip' + }, + { + header: gettext('CPU usage'), + sortable: true, + width: 110, + dataIndex: 'cpuusage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Memory usage'), + width: 110, + sortable: true, + tdCls: 'x-progressbar-default-cell', + dataIndex: 'memoryusage', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Uptime'), + sortable: true, + dataIndex: 'uptime', + align: 'right', + renderer: Proxmox.Utils.render_uptime + } + ], + + stateful: true, + stateId: 'grid-cluster-nodes', + tools: [ + { + type: 'up', + handler: function(){ + var me = this.up('grid'); + var height = Math.max(me.getHeight()-50, 250); + me.setHeight(height); + } + }, + { + type: 'down', + handler: function(){ + var me = this.up('grid'); + var height = me.getHeight()+50; + me.setHeight(height); + } + } + ] +}, function() { + + Ext.define('pve-dc-nodes', { + extend: 'Ext.data.Model', + fields: [ 'id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'], + idProperty: 'id' + }); + +}); + +Ext.define('PVE.widget.ProgressBar',{ + extend: 'Ext.Progress', + alias: 'widget.pveProgressBar', + + animate: true, + textTpl: [ + '{percent}%' + ], + + setValue: function(value){ + var me = this; + me.callParent([value]); + + me.removeCls(['warning', 'critical']); + + if (value > 0.89) { + me.addCls('critical'); + } else if (value > 0.59) { + me.addCls('warning'); + } + } +}); +/*jslint confusion: true*/ +Ext.define('pve-cluster-nodes', { + extend: 'Ext.data.Model', + fields: [ + 'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr', + { type: 'integer', name: 'quorum_votes' } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/config/nodes" + }, + idProperty: 'nodeid' +}); + +Ext.define('pve-cluster-info', { + extend: 'Ext.data.Model', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/config/join" + } +}); + +Ext.define('PVE.ClusterAdministration', { + extend: 'Ext.panel.Panel', + xtype: 'pveClusterAdministration', + + title: gettext('Cluster Administration'), + onlineHelp: 'chapter_pvecm', + + border: false, + defaults: { border: false }, + + viewModel: { + parent: null, + data: { + totem: {}, + nodelist: [], + preferred_node: { + name: '', + fp: '', + addr: '' + }, + isInCluster: false, + nodecount: 0 + } + }, + + items: [ + { + xtype: 'panel', + title: gettext('Cluster Information'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.store = Ext.create('Proxmox.data.UpdateStore', { + autoStart: true, + interval: 15 * 1000, + storeid: 'pve-cluster-info', + model: 'pve-cluster-info' + }); + view.store.on('load', this.onLoad, this); + view.on('destroy', view.store.stopUpdate); + }, + + onLoad: function(store, records, success) { + var vm = this.getViewModel(); + if (!success || !records || !records[0].data) { + vm.set('totem', {}); + vm.set('isInCluster', false); + vm.set('nodelist', []); + vm.set('preferred_node', { + name: '', + addr: '', + fp: '' + }); + return; + } + var data = records[0].data; + vm.set('totem', data.totem); + vm.set('isInCluster', !!data.totem.cluster_name); + vm.set('nodelist', data.nodelist); + + var nodeinfo = Ext.Array.findBy(data.nodelist, function (el) { + return el.name === data.preferred_node; + }); + + vm.set('preferred_node', { + name: data.preferred_node, + addr: nodeinfo.pve_addr, + ring_addr: [ nodeinfo.ring0_addr, nodeinfo.ring1_addr ], + fp: nodeinfo.pve_fp + }); + }, + + onCreate: function() { + var view = this.getView(); + view.store.stopUpdate(); + var win = Ext.create('PVE.ClusterCreateWindow', { + autoShow: true, + listeners: { + destroy: function() { + view.store.startUpdate(); + } + } + }); + }, + + onClusterInfo: function() { + var vm = this.getViewModel(); + var win = Ext.create('PVE.ClusterInfoWindow', { + joinInfo: { + ipAddress: vm.get('preferred_node.addr'), + fingerprint: vm.get('preferred_node.fp'), + ring_addr: vm.get('preferred_node.ring_addr'), + totem: vm.get('totem') + } + }); + win.show(); + }, + + onJoin: function() { + var view = this.getView(); + view.store.stopUpdate(); + var win = Ext.create('PVE.ClusterJoinNodeWindow', { + autoShow: true, + listeners: { + destroy: function() { + view.store.startUpdate(); + } + } + }); + } + }, + tbar: [ + { + text: gettext('Create Cluster'), + reference: 'createButton', + handler: 'onCreate', + bind: { + disabled: '{isInCluster}' + } + }, + { + text: gettext('Join Information'), + reference: 'addButton', + handler: 'onClusterInfo', + bind: { + disabled: '{!isInCluster}' + } + }, + { + text: gettext('Join Cluster'), + reference: 'joinButton', + handler: 'onJoin', + bind: { + disabled: '{isInCluster}' + } + } + ], + layout: 'hbox', + bodyPadding: 5, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Cluster Name'), + bind: { + value: '{totem.cluster_name}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Config Version'), + bind: { + value: '{totem.config_version}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Number of Nodes'), + labelWidth: 120, + bind: { + value: '{nodecount}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + value: gettext('Standalone node - no cluster defined'), + bind: { + hidden: '{isInCluster}' + }, + flex: 1 + } + ] + }, + { + xtype: 'grid', + title: gettext('Cluster Nodes'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + xtype: 'update', + interval: 5 * 1000, + autoStart: true, + storeid: 'pve-cluster-nodes', + model: 'pve-cluster-nodes' + }); + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: { + property: 'nodeid', + order: 'DESC' + } + })); + Proxmox.Utils.monStoreErrors(view, view.rstore); + 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('nodecount', 0); + return; + } + vm.set('nodecount', records.length); + } + }, + columns: [ + { + header: gettext('Nodename'), + flex: 2, + dataIndex: 'name' + }, + { + header: gettext('ID'), + flex: 1, + dataIndex: 'nodeid' + }, + { + header: gettext('Votes'), + flex: 1, + dataIndex: 'quorum_votes' + }, + { + header: gettext('Ring 0'), + flex: 2, + dataIndex: 'ring0_addr' + }, + { + header: gettext('Ring 1'), + flex: 2, + dataIndex: 'ring1_addr' + } + ] + } + ] +}); +/*jslint confusion: true*/ +Ext.define('PVE.ClusterCreateWindow', { + extend: 'Proxmox.window.Edit', + xtype: 'pveClusterCreateWindow', + + title: gettext('Create Cluster'), + width: 600, + + method: 'POST', + url: '/cluster/config', + + isCreate: true, + subject: gettext('Cluster'), + showTaskViewer: true, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Cluster Name'), + allowBlank: false, + name: 'clustername' + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Ring 0 Address'), + emptyText: gettext("Optional, defaults to IP resolved by node's hostname"), + name: 'ring0_addr', + skipEmptyText: true + } + // TODO: for advanced options: ring1_addr + ] +}); + +Ext.define('PVE.ClusterInfoWindow', { + extend: 'Ext.window.Window', + xtype: 'pveClusterInfoWindow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 800, + modal: true, + resizable: false, + title: gettext('Cluster Join Information'), + + joinInfo: { + ipAddress: undefined, + fingerprint: undefined, + totem: {} + }, + + items: [ + { + xtype: 'component', + border: false, + padding: '10 10 10 10', + html: gettext("Copy the Join Information here and use it on the node you want to add.") + }, + { + xtype: 'container', + layout: 'form', + border: false, + padding: '0 10 10 10', + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('IP Address'), + cbind: { value: '{joinInfo.ipAddress}' }, + editable: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Fingerprint'), + cbind: { value: '{joinInfo.fingerprint}' }, + editable: false + }, + { + xtype: 'textarea', + inputId: 'pveSerializedClusterInfo', + fieldLabel: gettext('Join Information'), + grow: true, + cbind: { joinInfo: '{joinInfo}' }, + editable: false, + listeners: { + afterrender: function(field) { + if (!field.joinInfo) { + return; + } + var jsons = Ext.JSON.encode(field.joinInfo); + var base64s = Ext.util.Base64.encode(jsons); + field.setValue(base64s); + } + } + } + ] + } + ], + dockedItems: [{ + dock: 'bottom', + xtype: 'toolbar', + items: [{ + xtype: 'button', + handler: function(b) { + var el = document.getElementById('pveSerializedClusterInfo'); + el.select(); + document.execCommand("copy"); + }, + text: gettext('Copy Information') + }] + }] +}); + +Ext.define('PVE.ClusterJoinNodeWindow', { + extend: 'Proxmox.window.Edit', + xtype: 'pveClusterJoinNodeWindow', + + title: gettext('Cluster Join'), + width: 800, + + method: 'POST', + url: '/cluster/config/join', + + defaultFocus: 'textarea[name=serializedinfo]', + isCreate: true, + submitText: gettext('Join'), + showTaskViewer: true, + + onlineHelp: 'chapter_pvecm', + + viewModel: { + parent: null, + data: { + info: { + fp: '', + ip: '', + ring0Needed: false, + ring1Possible: false, + ring1Needed: false + } + }, + formulas: { + ring0EmptyText: function(get) { + if (get('info.ring0Needed')) { + return gettext("Cannot use default address safely"); + } else { + return gettext("Default: IP resolved by node's hostname"); + } + } + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + '#': { + close: function() { + delete PVE.Utils.silenceAuthFailures; + } + }, + 'proxmoxcheckbox[name=assistedEntry]': { + change: 'onInputTypeChange' + }, + 'textarea[name=serializedinfo]': { + change: 'recomputeSerializedInfo', + enable: 'resetField' + }, + 'proxmoxtextfield[name=ring1_addr]': { + enable: 'ring1Needed' + }, + 'textfield': { + disable: 'resetField' + } + }, + resetField: function(field) { + field.reset(); + }, + ring1Needed: function(f) { + var vm = this.getViewModel(); + f.allowBlank = !vm.get('info.ring1Needed'); + }, + onInputTypeChange: function(field, assistedInput) { + var vm = this.getViewModel(); + if (!assistedInput) { + vm.set('info.ring1Possible', true); + } + }, + recomputeSerializedInfo: function(field, value) { + var vm = this.getViewModel(); + var jsons = Ext.util.Base64.decode(value); + var joinInfo = Ext.JSON.decode(jsons, true); + + var info = { + fp: '', + ring1Needed: false, + ring1Possible: false, + ip: '' + }; + + var totem = {}; + if (!(joinInfo && joinInfo.totem)) { + field.valid = false; + } else { + var ring0Needed = false; + if (joinInfo.ring_addr !== undefined) { + ring0Needed = joinInfo.ring_addr[0] !== joinInfo.ipAddress; + } + + info = { + ip: joinInfo.ipAddress, + fp: joinInfo.fingerprint, + ring0Needed: ring0Needed, + ring1Possible: !!joinInfo.totem['interface']['1'], + ring1Needed: !!joinInfo.totem['interface']['1'] + }; + totem = joinInfo.totem; + field.valid = true; + } + + vm.set('info', info); + } + }, + + submit: function() { + // joining may produce temporarily auth failures, ignore as long the task runs + PVE.Utils.silenceAuthFailures = true; + this.callParent(); + }, + + taskDone: function(success) { + delete PVE.Utils.silenceAuthFailures; + if (success) { + var txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!'); + // ensure user cannot do harm + Ext.getBody().mask(txt, ['pve-static-mask']); + // TaskView may hide above mask, so tell him directly + Ext.Msg.show({ + title: gettext('Join Task Finished'), + icon: Ext.Msg.INFO, + msg: txt + }); + // reload always (if user wasn't faster), but wait a bit for pveproxy + Ext.defer(function() { + window.location.reload(true); + }, 5000); + } + }, + + items: [{ + xtype: 'proxmoxcheckbox', + reference: 'assistedEntry', + name: 'assistedEntry', + submitValue: false, + value: true, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering') + }, + boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.') + }, + { + xtype: 'textarea', + name: 'serializedinfo', + submitValue: false, + allowBlank: false, + fieldLabel: gettext('Information'), + emptyText: gettext('Paste encoded Cluster Information here'), + validator: function(val) { + return val === '' || this.valid || + gettext('Does not seem like a valid encoded Cluster Information!'); + }, + bind: { + disabled: '{!assistedEntry.checked}', + hidden: '{!assistedEntry.checked}' + }, + value: '' + }, + { + xtype: 'inputpanel', + column1: [ + { + xtype: 'textfield', + fieldLabel: gettext('Peer Address'), + allowBlank: false, + bind: { + value: '{info.ip}', + readOnly: '{assistedEntry.checked}' + }, + name: 'hostname' + }, + { + xtype: 'textfield', + inputType: 'password', + emptyText: gettext("Peer's root password"), + fieldLabel: gettext('Password'), + allowBlank: false, + name: 'password' + } + ], + column2: [ + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Corosync Ring 0'), + bind: { + emptyText: '{ring0EmptyText}', + allowBlank: '{!info.ring0Needed}' + }, + skipEmptyText: true, + name: 'ring0_addr' + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Corosync Ring 1'), + skipEmptyText: true, + bind: { + disabled: '{!info.ring1Possible}' + }, + name: 'ring1_addr' + } + ], + columnB: [ + { + xtype: 'textfield', + fieldLabel: gettext('Fingerprint'), + allowBlank: false, + bind: { + value: '{info.fp}', + readOnly: '{assistedEntry.checked}' + }, + name: 'fingerprint' + } + ] + }] +}); +/* + * Workspace base class + * + * popup login window when auth fails (call onLogin handler) + * update (re-login) ticket every 15 minutes + * + */ + +Ext.define('PVE.Workspace', { + extend: 'Ext.container.Viewport', + + title: 'Proxmox Virtual Environment', + + loginData: null, // Data from last login call + + onLogin: function(loginData) {}, + + // private + updateLoginData: function(loginData) { + var me = this; + me.loginData = loginData; + Proxmox.Utils.setAuthData(loginData); + + var rt = me.down('pveResourceTree'); + rt.setDatacenterText(loginData.clustername); + + if (loginData.cap) { + Ext.state.Manager.set('GuiCap', loginData.cap); + } + + me.onLogin(loginData); + }, + + // private + showLogin: function() { + var me = this; + + Proxmox.Utils.authClear(); + Proxmox.UserName = null; + me.loginData = null; + + if (!me.login) { + me.login = Ext.create('PVE.window.LoginWindow', { + handler: function(data) { + me.login = null; + me.updateLoginData(data); + Proxmox.Utils.checked_command(function() {}); // display subscription status + } + }); + } + me.onLogin(null); + me.login.show(); + }, + + initComponent : function() { + var me = this; + + Ext.tip.QuickTipManager.init(); + + // fixme: what about other errors + Ext.Ajax.on('requestexception', function(conn, response, options) { + if (response.status == 401 && !PVE.Utils.silenceAuthFailures) { // auth failure + me.showLogin(); + } + }); + + me.callParent(); + + if (!Proxmox.Utils.authOK()) { + me.showLogin(); + } else { + if (me.loginData) { + me.onLogin(me.loginData); + } + } + + Ext.TaskManager.start({ + run: function() { + var ticket = Proxmox.Utils.authOK(); + if (!ticket || !Proxmox.UserName) { + return; + } + + Ext.Ajax.request({ + params: { + username: Proxmox.UserName, + password: ticket + }, + url: '/api2/json/access/ticket', + method: 'POST', + success: function(response, opts) { + var obj = Ext.decode(response.responseText); + me.updateLoginData(obj.data); + } + }); + }, + interval: 15*60*1000 + }); + + } +}); + +Ext.define('PVE.StdWorkspace', { + extend: 'PVE.Workspace', + + alias: ['widget.pveStdWorkspace'], + + // private + setContent: function(comp) { + var me = this; + + var cont = me.child('#content'); + + var lay = cont.getLayout(); + + var cur = lay.getActiveItem(); + + if (comp) { + Proxmox.Utils.setErrorMask(cont, false); + comp.border = false; + cont.add(comp); + if (cur !== null && lay.getNext()) { + lay.next(); + var task = Ext.create('Ext.util.DelayedTask', function(){ + cont.remove(cur); + }); + task.delay(10); + } + } + else { + // helper for cleaning the content when logging out + cont.removeAll(); + } + }, + + selectById: function(nodeid) { + var me = this; + var tree = me.down('pveResourceTree'); + tree.selectById(nodeid); + }, + + onLogin: function(loginData) { + var me = this; + + me.updateUserInfo(); + + if (loginData) { + PVE.data.ResourceStore.startUpdate(); + + Proxmox.Utils.API2Request({ + url: '/version', + method: 'GET', + success: function(response) { + PVE.VersionInfo = response.result.data; + me.updateVersionInfo(); + } + }); + } + }, + + updateUserInfo: function() { + var me = this; + + var ui = me.query('#userinfo')[0]; + + if (Proxmox.UserName) { + var msg = Ext.String.format(gettext("You are logged in as {0}"), "'" + Proxmox.UserName + "'"); + ui.update('
' + msg + '
'); + } else { + ui.update(''); + } + ui.updateLayout(); + }, + + updateVersionInfo: function() { + var me = this; + + var ui = me.query('#versioninfo')[0]; + + if (PVE.VersionInfo) { + var version = PVE.VersionInfo.version + '-' + PVE.VersionInfo.release; + ui.update('Virtual Environment ' + version); + } else { + ui.update('Virtual Environment'); + } + ui.updateLayout(); + }, + + initComponent : function() { + var me = this; + + Ext.History.init(); + + var sprovider = Ext.create('PVE.StateProvider'); + Ext.state.Manager.setProvider(sprovider); + + var selview = Ext.create('PVE.form.ViewSelector'); + + var rtree = Ext.createWidget('pveResourceTree', { + viewFilter: selview.getViewFilter(), + flex: 1, + selModel: { + selType: 'treemodel', + listeners: { + selectionchange: function(sm, selected) { + if (selected.length > 0) { + var n = selected[0]; + var tlckup = { + root: 'PVE.dc.Config', + node: 'PVE.node.Config', + qemu: 'PVE.qemu.Config', + lxc: 'PVE.lxc.Config', + storage: 'PVE.storage.Browser', + pool: 'pvePoolConfig' + }; + var comp = { + xtype: tlckup[n.data.type || 'root'] || + 'pvePanelConfig', + showSearch: (n.data.id === 'root') || + Ext.isDefined(n.data.groupbyid), + pveSelNode: n, + workspace: me, + viewFilter: selview.getViewFilter() + }; + PVE.curSelectedNode = n; + me.setContent(comp); + } + } + } + } + }); + + selview.on('select', function(combo, records) { + if (records) { + var view = combo.getViewFilter(); + rtree.setViewFilter(view); + } + }); + + var caps = sprovider.get('GuiCap'); + + var createVM = Ext.createWidget('button', { + pack: 'end', + margin: '3 5 0 0', + baseCls: 'x-btn', + iconCls: 'fa fa-desktop', + text: gettext("Create VM"), + disabled: !caps.vms['VM.Allocate'], + handler: function() { + var wiz = Ext.create('PVE.qemu.CreateWizard', {}); + wiz.show(); + } + }); + + var createCT = Ext.createWidget('button', { + pack: 'end', + margin: '3 5 0 0', + baseCls: 'x-btn', + iconCls: 'fa fa-cube', + text: gettext("Create CT"), + disabled: !caps.vms['VM.Allocate'], + handler: function() { + var wiz = Ext.create('PVE.lxc.CreateWizard', {}); + wiz.show(); + } + }); + + sprovider.on('statechange', function(sp, key, value) { + if (key === 'GuiCap' && value) { + caps = value; + createVM.setDisabled(!caps.vms['VM.Allocate']); + createCT.setDisabled(!caps.vms['VM.Allocate']); + } + }); + + Ext.apply(me, { + layout: { type: 'border' }, + border: false, + items: [ + { + region: 'north', + layout: { + type: 'hbox', + align: 'middle' + }, + baseCls: 'x-plain', + defaults: { + baseCls: 'x-plain' + }, + border: false, + margin: '2 0 2 5', + items: [ + { + html: '' + + '' + }, + { + minWidth: 150, + id: 'versioninfo', + html: 'Virtual Environment' + }, + { + xtype: 'pveGlobalSearchField', + tree: rtree + }, + { + flex: 1 + }, + { + pack: 'end', + id: 'userinfo', + stateful: false + }, + { + xtype: 'button', + margin: '0 10 0 3', + iconCls: 'fa black fa-gear', + userCls: 'pointer', + handler: function() { + var win = Ext.create('PVE.window.Settings'); + win.show(); + } + }, + { + xtype: 'proxmoxHelpButton', + hidden: false, + baseCls: 'x-btn', + iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ', + listenToGlobalEvent: false, + onlineHelp: 'pve_documentation_index', + text: gettext('Documentation'), + margin: '0 5 0 0' + }, + createVM, + createCT, + { + pack: 'end', + margin: '0 5 0 0', + xtype: 'button', + baseCls: 'x-btn', + iconCls: 'fa fa-sign-out', + text: gettext("Logout"), + handler: function() { + PVE.data.ResourceStore.loadData([], false); + me.showLogin(); + me.setContent(null); + var rt = me.down('pveResourceTree'); + rt.setDatacenterText(undefined); + rt.clearTree(); + + // empty the stores of the StatusPanel child items + var statusPanels = Ext.ComponentQuery.query('pveStatusPanel grid'); + Ext.Array.forEach(statusPanels, function(comp) { + if (comp.getStore()) { + comp.getStore().loadData([], false); + } + }); + } + } + ] + }, + { + region: 'center', + stateful: true, + stateId: 'pvecenter', + minWidth: 100, + minHeight: 100, + id: 'content', + xtype: 'container', + layout: { type: 'card' }, + border: false, + margin: '0 5 0 0', + items: [] + }, + { + region: 'west', + stateful: true, + stateId: 'pvewest', + itemId: 'west', + xtype: 'container', + border: false, + layout: { type: 'vbox', align: 'stretch' }, + margin: '0 0 0 5', + split: true, + width: 200, + items: [ selview, rtree ], + listeners: { + resize: function(panel, width, height) { + var viewWidth = me.getSize().width; + if (width > viewWidth - 100) { + panel.setWidth(viewWidth - 100); + } + } + } + }, + { + xtype: 'pveStatusPanel', + stateful: true, + stateId: 'pvesouth', + itemId: 'south', + region: 'south', + margin:'0 5 5 5', + title: gettext('Logs'), + collapsible: true, + header: false, + height: 200, + split:true, + listeners: { + resize: function(panel, width, height) { + var viewHeight = me.getSize().height; + if (height > (viewHeight - 150)) { + panel.setHeight(viewHeight - 150); + } + } + } + } + ] + }); + + me.callParent(); + + me.updateUserInfo(); + + // on resize, center all modal windows + Ext.on('resize', function(){ + var wins = Ext.ComponentQuery.query('window[modal]'); + if (wins.length > 0) { + wins.forEach(function(win){ + win.alignTo(me, 'c-c'); + }); + } + }); + } +}); +