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': 'white', 'white-space': 'pre', 'font-family': 'monospace' } } ], buttons: [ { text: gettext('Reload'), name: 'reload', handler: function() { var me = this; me.up('window').store.reload(); } }, { text: gettext('Close'), name: 'close', handler: function() { var me = this; me.up('window').close(); } } ], layout: { type: 'vbox', align: 'stretch' }, width: 800, height: 500, minWidth: 600, minHeight: 400, bodyPadding: 5, title: gettext('S.M.A.R.T. Values'), initComponent: function() { var me = this; var nodename = me.nodename; if (!nodename) { throw "no node name specified"; } var dev = me.dev; if (!dev) { throw "no device specified"; } me.store = Ext.create('Ext.data.Store', { model: 'disk-smart', proxy: { type: 'proxmox', url: "/api2/json/nodes/" + nodename + "/disks/smart?disk=" + dev } }); me.callParent(); var grid = me.down('#smarts'); var text = me.down('#text'); Proxmox.Utils.monStoreErrors(grid, me.store); me.mon(me.store, 'load', function(s, records, success) { if (success && records.length > 0) { var rec = records[0]; switch (rec.data.type) { case 'text': grid.setVisible(false); text.setVisible(true); text.setHtml(Ext.String.htmlEncode(rec.data.text)); break; default: // includes 'ata' // cannot use empty case because // of jslint grid.setVisible(true); text.setVisible(false); grid.setStore(rec.attributes()); break; } } }); me.store.load(); } }, function() { Ext.define('disk-smart', { extend: 'Ext.data.Model', fields: [ { name:'health'}, { name:'type'}, { name:'text'} ], hasMany: {model: 'smart-attribute', name: 'attributes'} }); Ext.define('smart-attribute', { extend: 'Ext.data.Model', fields: [ { name:'id', type:'number' }, 'name', 'value', 'worst', 'threshold', 'flags', 'fail', 'raw' ] }); }); Ext.define('PVE.node.CreateLVM', { extend: 'Proxmox.window.Edit', xtype: 'pveCreateLVM', subject: 'LVM Volume Group', showProgress: true, onlineHelp: 'chapter_lvm', initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.isCreate = true; Ext.applyIf(me, { url: "/nodes/" + me.nodename + "/disks/lvm", method: 'POST', items: [ { xtype: 'pveDiskSelector', name: 'device', nodename: me.nodename, diskType: 'unused', fieldLabel: gettext('Disk'), allowBlank: false }, { xtype: 'proxmoxtextfield', name: 'name', fieldLabel: gettext('Name'), allowBlank: false }, { xtype: 'proxmoxcheckbox', name: 'add_storage', fieldLabel: gettext('Add Storage'), value: '1' } ] }); me.callParent(); } }); Ext.define('PVE.node.LVMList', { extend: 'Ext.tree.Panel', xtype: 'pveLVMList', emptyText: gettext('No Volume Groups found'), stateful: true, stateId: 'grid-node-lvm', columns: [ { xtype: 'treecolumn', text: gettext('Name'), dataIndex: 'name', flex: 1 }, { text: gettext('Number of LVs'), dataIndex: 'lvcount', width: 150, align: 'right' }, { header: gettext('Usage'), width: 110, dataIndex: 'usage', tdCls: 'x-progressbar-default-cell', xtype: 'widgetcolumn', widget: { xtype: 'pveProgressBar' } }, { header: gettext('Size'), width: 100, align: 'right', sortable: true, renderer: Proxmox.Utils.format_size, dataIndex: 'size' }, { header: gettext('Free'), width: 100, align: 'right', sortable: true, renderer: Proxmox.Utils.format_size, dataIndex: 'free' } ], rootVisible: false, useArrows: true, tbar: [ { text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { var me = this.up('panel'); me.reload(); } }, { text: gettext('Create') + ': Volume Group', handler: function() { var me = this.up('panel'); var win = Ext.create('PVE.node.CreateLVM', { nodename: me.nodename, taskDone: function() { me.reload(); } }).show(); } } ], reload: function() { var me = this; var sm = me.getSelectionModel(); Proxmox.Utils.API2Request({ url: "/nodes/" + me.nodename + "/disks/lvm", waitMsgTarget: me, method: 'GET', failure: function(response, opts) { Proxmox.Utils.setErrorMask(me, response.htmlStatus); }, success: function(response, opts) { sm.deselectAll(); me.setRootNode(response.result.data); me.expandAll(); } }); }, listeners: { activate: function() { var me = this; me.reload(); } }, initComponent: function() { /*jslint confusion: true */ var me = this; me.nodename = me.pveSelNode.data.node; if (!me.nodename) { throw "no node name specified"; } var sm = Ext.create('Ext.selection.TreeModel', {}); Ext.apply(me, { selModel: sm, fields: ['name', 'size', 'free', { type: 'string', name: 'iconCls', calculate: function(data) { var txt = 'fa x-fa-tree fa-'; txt += (data.leaf) ? 'hdd-o' : 'object-group'; return txt; } }, { type: 'number', name: 'usage', calculate: function(data) { return ((data.size-data.free)/data.size); } } ], sorters: 'name' }); me.callParent(); me.reload(); } }); Ext.define('PVE.node.CreateLVMThin', { extend: 'Proxmox.window.Edit', xtype: 'pveCreateLVMThin', subject: 'LVM Thinpool', showProgress: true, onlineHelp: 'chapter_lvm', initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.isCreate = true; Ext.applyIf(me, { url: "/nodes/" + me.nodename + "/disks/lvmthin", method: 'POST', items: [ { xtype: 'pveDiskSelector', name: 'device', nodename: me.nodename, diskType: 'unused', fieldLabel: gettext('Disk'), allowBlank: false }, { xtype: 'proxmoxtextfield', name: 'name', fieldLabel: gettext('Name'), allowBlank: false }, { xtype: 'proxmoxcheckbox', name: 'add_storage', fieldLabel: gettext('Add Storage'), value: '1' } ] }); me.callParent(); } }); Ext.define('PVE.node.LVMThinList', { extend: 'Ext.grid.Panel', xtype: 'pveLVMThinList', emptyText: gettext('No thinpools found'), stateful: true, stateId: 'grid-node-lvmthin', columns: [ { text: gettext('Name'), dataIndex: 'lv', flex: 1 }, { header: gettext('Usage'), width: 110, dataIndex: 'usage', tdCls: 'x-progressbar-default-cell', xtype: 'widgetcolumn', widget: { xtype: 'pveProgressBar' } }, { header: gettext('Size'), width: 100, align: 'right', sortable: true, renderer: Proxmox.Utils.format_size, dataIndex: 'lv_size' }, { header: gettext('Used'), width: 100, align: 'right', sortable: true, renderer: Proxmox.Utils.format_size, dataIndex: 'used' }, { header: gettext('Metadata Usage'), width: 120, dataIndex: 'metadata_usage', tdCls: 'x-progressbar-default-cell', xtype: 'widgetcolumn', widget: { xtype: 'pveProgressBar' } }, { header: gettext('Metadata Size'), width: 120, align: 'right', sortable: true, renderer: Proxmox.Utils.format_size, dataIndex: 'metadata_size' }, { header: gettext('Metadata Used'), width: 125, align: 'right', sortable: true, renderer: Proxmox.Utils.format_size, dataIndex: 'metadata_used' } ], rootVisible: false, useArrows: true, tbar: [ { text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { var me = this.up('panel'); me.reload(); } }, { text: gettext('Create') + ': Thinpool', handler: function() { var me = this.up('panel'); var win = Ext.create('PVE.node.CreateLVMThin', { nodename: me.nodename, taskDone: function() { me.reload(); } }).show(); } } ], reload: function() { var me = this; me.store.load(); me.store.sort(); }, listeners: { activate: function() { var me = this; me.reload(); } }, initComponent: function() { /*jslint confusion: true */ var me = this; me.nodename = me.pveSelNode.data.node; if (!me.nodename) { throw "no node name specified"; } Ext.apply(me, { store: { fields: ['lv', 'lv_size', 'used', 'metadata_size', 'metadata_used', { type: 'number', name: 'usage', calculate: function(data) { return data.used/data.lv_size; } }, { type: 'number', name: 'metadata_usage', calculate: function(data) { return data.metadata_used/data.metadata_size; } } ], proxy: { type: 'proxmox', url: "/api2/json/nodes/" + me.nodename + '/disks/lvmthin' }, sorters: 'lv' } }); me.callParent(); Proxmox.Utils.monStoreErrors(me, me.getStore(), true); me.reload(); } }); Ext.define('PVE.node.CreateDirectory', { extend: 'Proxmox.window.Edit', xtype: 'pveCreateDirectory', subject: Proxmox.Utils.directoryText, showProgress: true, onlineHelp: 'chapter_storage', initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.isCreate = true; Ext.applyIf(me, { url: "/nodes/" + me.nodename + "/disks/directory", method: 'POST', items: [ { xtype: 'pveDiskSelector', name: 'device', nodename: me.nodename, diskType: 'unused', fieldLabel: gettext('Disk'), allowBlank: false }, { xtype: 'proxmoxKVComboBox', comboItems: [ ['ext4', 'ext4'], ['xfs', 'xfs'] ], fieldLabel: gettext('Filesystem'), name: 'filesystem', value: '', allowBlank: false }, { xtype: 'proxmoxtextfield', name: 'name', fieldLabel: gettext('Name'), allowBlank: false }, { xtype: 'proxmoxcheckbox', name: 'add_storage', fieldLabel: gettext('Add Storage'), value: '1' } ] }); me.callParent(); } }); Ext.define('PVE.node.Directorylist', { extend: 'Ext.grid.Panel', xtype: 'pveDirectoryList', stateful: true, stateId: 'grid-node-directory', columns: [ { text: gettext('Path'), dataIndex: 'path', flex: 1 }, { header: gettext('Device'), flex: 1, dataIndex: 'device' }, { header: gettext('Type'), width: 100, dataIndex: 'type' }, { header: gettext('Options'), width: 100, dataIndex: 'options' }, { header: gettext('Unit File'), hidden: true, dataIndex: 'unitfile' } ], rootVisible: false, useArrows: true, tbar: [ { text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { var me = this.up('panel'); me.reload(); } }, { text: gettext('Create') + ': Directory', handler: function() { var me = this.up('panel'); var win = Ext.create('PVE.node.CreateDirectory', { nodename: me.nodename }).show(); win.on('destroy', function() { me.reload(); }); } } ], reload: function() { var me = this; me.store.load(); me.store.sort(); }, listeners: { activate: function() { var me = this; me.reload(); } }, initComponent: function() { /*jslint confusion: true */ var me = this; me.nodename = me.pveSelNode.data.node; if (!me.nodename) { throw "no node name specified"; } Ext.apply(me, { store: { fields: ['path', 'device', 'type', 'options', 'unitfile' ], proxy: { type: 'proxmox', url: "/api2/json/nodes/" + me.nodename + '/disks/directory' }, sorters: 'path' } }); me.callParent(); Proxmox.Utils.monStoreErrors(me, me.getStore(), true); me.reload(); } }); /*jslint confusion: true*/ Ext.define('PVE.node.CreateZFS', { extend: 'Proxmox.window.Edit', xtype: 'pveCreateZFS', subject: 'ZFS', showProgress: true, onlineHelp: 'chapter_zfs', initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } me.isCreate = true; var update_disklist = function() { var grid = me.down('#disklist'); var disks = grid.getSelection(); var val = []; disks.sort(function(a,b) { var aorder = a.get('order') || 0; var border = b.get('order') || 0; return (aorder - border); }); disks.forEach(function(disk) { val.push(disk.get('devpath')); }); me.down('field[name=devices]').setValue(val.join(',')); }; Ext.apply(me, { url: '/nodes/' + me.nodename + '/disks/zfs', method: 'POST', items: [ { xtype: 'inputpanel', onGetValues: function(values) { return values; }, column1: [ { xtype: 'textfield', hidden: true, name: 'devices', allowBlank: false }, { xtype: 'proxmoxtextfield', name: 'name', fieldLabel: gettext('Name'), allowBlank: false }, { xtype: 'proxmoxcheckbox', name: 'add_storage', fieldLabel: gettext('Add Storage'), value: '1' } ], column2: [ { xtype: 'proxmoxKVComboBox', fieldLabel: gettext('RAID Level'), name: 'raidlevel', value: 'single', comboItems: [ ['single', gettext('Single Disk')], ['mirror', 'Mirror'], ['raid10', 'RAID10'], ['raidz', 'RAIDZ'], ['raidz2', 'RAIDZ2'], ['raidz3', 'RAIDZ3'] ] }, { xtype: 'proxmoxKVComboBox', fieldLabel: gettext('Compression'), name: 'compression', value: 'on', comboItems: [ ['on', 'on'], ['off', 'off'], ['gzip', 'gzip'], ['lz4', 'lz4'], ['lzjb', 'lzjb'], ['zle', 'zle'] ] }, { xtype: 'proxmoxintegerfield', fieldLabel: gettext('ashift'), minValue: 9, maxValue: 16, value: '12', name: 'ashift' } ], columnB: [ { xtype: 'grid', height: 200, emptyText: gettext('No Disks unused'), itemId: 'disklist', selModel: 'checkboxmodel', listeners: { selectionchange: update_disklist }, store: { proxy: { type: 'proxmox', url: '/api2/json/nodes/' + me.nodename + '/disks/list?type=unused' } }, columns: [ { text: gettext('Device'), dataIndex: 'devpath', flex: 1 }, { text: gettext('Serial'), dataIndex: 'serial' }, { text: gettext('Size'), dataIndex: 'size', renderer: PVE.Utils.render_size }, { header: gettext('Order'), xtype: 'widgetcolumn', dataIndex: 'order', sortable: true, widget: { xtype: 'proxmoxintegerfield', minValue: 1, isFormField: false, listeners: { change: function(numberfield, value, old_value) { var record = numberfield.getWidgetRecord(); record.set('order', value); update_disklist(record); } } } } ] } ] } ] }); me.callParent(); me.down('#disklist').getStore().load(); } }); Ext.define('PVE.node.ZFSDevices', { extend: 'Ext.tree.Panel', xtype: 'pveZFSDevices', stateful: true, stateId: 'grid-node-zfsstatus', columns: [ { xtype: 'treecolumn', text: gettext('Name'), dataIndex: 'name', flex: 1 }, { text: gettext('Health'), renderer: PVE.Utils.render_zfs_health, dataIndex: 'state' }, { text: 'READ', dataIndex: 'read' }, { text: 'WRITE', dataIndex: 'write' }, { text: 'CKSUM', dataIndex: 'cksum' }, { text: gettext('Message'), dataIndex: 'msg' } ], rootVisible: true, reload: function() { var me = this; var sm = me.getSelectionModel(); Proxmox.Utils.API2Request({ url: "/nodes/" + me.nodename + "/disks/zfs/" + me.zpool, waitMsgTarget: me, method: 'GET', failure: function(response, opts) { Proxmox.Utils.setErrorMask(me, response.htmlStatus); }, success: function(response, opts) { sm.deselectAll(); me.setRootNode(response.result.data); me.expandAll(); } }); }, initComponent: function() { /*jslint confusion: true */ var me = this; if (!me.nodename) { throw "no node name specified"; } if (!me.zpool) { throw "no zpool specified"; } var sm = Ext.create('Ext.selection.TreeModel', {}); Ext.apply(me, { selModel: sm, fields: ['name', 'status', { type: 'string', name: 'iconCls', calculate: function(data) { var txt = 'fa x-fa-tree fa-'; if (data.leaf) { return txt + 'hdd-o'; } } } ], sorters: 'name' }); me.callParent(); me.reload(); } }); Ext.define('PVE.node.ZFSStatus', { extend: 'Proxmox.grid.ObjectGrid', xtype: 'pveZFSStatus', layout: 'fit', border: false, initComponent: function() { /*jslint confusion: true */ var me = this; if (!me.nodename) { throw "no node name specified"; } if (!me.zpool) { throw "no zpool specified"; } me.url = "/api2/extjs/nodes/" + me.nodename + "/disks/zfs/" + me.zpool; me.rows = { scan: { header: gettext('Scan') }, status: { header: gettext('Status') }, action: { header: gettext('Action') }, errors: { header: gettext('Errors') } }; me.callParent(); me.reload(); } }); Ext.define('PVE.node.ZFSList', { extend: 'Ext.grid.Panel', xtype: 'pveZFSList', stateful: true, stateId: 'grid-node-zfs', columns: [ { text: gettext('Name'), dataIndex: 'name', flex: 1 }, { header: gettext('Size'), renderer: Proxmox.Utils.format_size, dataIndex: 'size' }, { header: gettext('Free'), renderer: Proxmox.Utils.format_size, dataIndex: 'free' }, { header: gettext('Allocated'), renderer: Proxmox.Utils.format_size, dataIndex: 'alloc' }, { header: gettext('Fragmentation'), renderer: function(value) { return value.toString() + '%'; }, dataIndex: 'frag' }, { header: gettext('Health'), renderer: PVE.Utils.render_zfs_health, dataIndex: 'health' }, { header: gettext('Deduplication'), hidden: true, renderer: function(value) { return value.toFixed(2).toString() + 'x'; }, dataIndex: 'dedup' } ], rootVisible: false, useArrows: true, tbar: [ { text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { var me = this.up('panel'); me.reload(); } }, { text: gettext('Create') + ': ZFS', handler: function() { var me = this.up('panel'); var win = Ext.create('PVE.node.CreateZFS', { nodename: me.nodename }).show(); win.on('destroy', function() { me.reload(); }); } }, { text: gettext('Detail'), itemId: 'detailbtn', disabled: true, handler: function() { var me = this.up('panel'); var selection = me.getSelection(); if (selection.length < 1) { return; } me.show_detail(selection[0].get('name')); } } ], show_detail: function(zpool) { var me = this; var detailsgrid = Ext.create('PVE.node.ZFSStatus', { layout: 'fit', nodename: me.nodename, flex: 0, zpool: zpool }); var devicetree = Ext.create('PVE.node.ZFSDevices', { title: gettext('Devices'), nodename: me.nodename, flex: 1, zpool: zpool }); var win = Ext.create('Ext.window.Window', { modal: true, width: 800, height: 400, resizable: true, layout: 'fit', title: gettext('Status') + ': ' + zpool, items:[{ xtype: 'panel', region: 'center', layout: { type: 'vbox', align: 'stretch' }, items: [detailsgrid, devicetree], tbar: [{ text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { devicetree.reload(); detailsgrid.reload(); } }] }] }).show(); }, set_button_status: function() { var me = this; var selection = me.getSelection(); me.down('#detailbtn').setDisabled(selection.length === 0); }, reload: function() { var me = this; me.store.load(); me.store.sort(); }, listeners: { activate: function() { var me = this; me.reload(); }, selectionchange: function() { this.set_button_status(); }, itemdblclick: function(grid, record) { var me = this; me.show_detail(record.get('name')); } }, initComponent: function() { /*jslint confusion: true */ var me = this; me.nodename = me.pveSelNode.data.node; if (!me.nodename) { throw "no node name specified"; } Ext.apply(me, { store: { fields: ['name', 'size', 'free', 'alloc', 'dedup', 'frag', 'health'], proxy: { type: 'proxmox', url: "/api2/json/nodes/" + me.nodename + '/disks/zfs' }, sorters: 'name' } }); me.callParent(); Proxmox.Utils.monStoreErrors(me, me.getStore(), true); me.reload(); } }); Ext.define('PVE.node.StatusView', { extend: 'PVE.panel.StatusView', alias: 'widget.pveNodeStatus', height: 300, bodyPadding: '20 15 20 15', layout: { type: 'table', columns: 2, tableAttrs: { style: { width: '100%' } } }, defaults: { xtype: 'pveInfoWidget', padding: '0 15 5 15' }, items: [ { itemId: 'cpu', iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', title: gettext('CPU usage'), valueField: 'cpu', maxField: 'cpuinfo', renderer: PVE.Utils.render_node_cpu_usage }, { itemId: 'wait', iconCls: 'fa fa-fw fa-clock-o', title: gettext('IO delay'), valueField: 'wait', rowspan: 2 }, { itemId: 'load', iconCls: 'fa fa-fw fa-tasks', title: gettext('Load average'), printBar: false, textField: 'loadavg' }, { xtype: 'box', colspan: 2, padding: '0 0 20 0' }, { iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', itemId: 'memory', title: gettext('RAM usage'), valueField: 'memory', maxField: 'memory', renderer: PVE.Utils.render_node_size_usage }, { itemId: 'ksm', printBar: false, title: gettext('KSM sharing'), textField: 'ksm', renderer: function(record) { return PVE.Utils.render_size(record.shared); }, padding: '0 15 10 15' }, { iconCls: 'fa fa-fw fa-hdd-o', itemId: 'rootfs', title: gettext('HD space') + '(root)', valueField: 'rootfs', maxField: 'rootfs', renderer: PVE.Utils.render_node_size_usage }, { iconCls: 'fa fa-fw fa-refresh', itemId: 'swap', printSize: true, title: gettext('SWAP usage'), valueField: 'swap', maxField: 'swap', renderer: PVE.Utils.render_node_size_usage }, { xtype: 'box', colspan: 2, padding: '0 0 20 0' }, { itemId: 'cpus', colspan: 2, printBar: false, title: gettext('CPU(s)'), textField: 'cpuinfo', renderer: function(cpuinfo) { return cpuinfo.cpus + " x " + cpuinfo.model + " (" + cpuinfo.sockets.toString() + " " + (cpuinfo.sockets > 1 ? gettext('Sockets') : gettext('Socket') ) + ")"; }, value: '' }, { itemId: 'kversion', colspan: 2, title: gettext('Kernel Version'), printBar: false, textField: 'kversion', value: '' }, { itemId: 'version', colspan: 2, printBar: false, title: gettext('PVE Manager Version'), textField: 'pveversion', value: '' } ], updateTitle: function() { var me = this; var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime')); me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')'); } }); Ext.define('PVE.node.Summary', { extend: 'Ext.panel.Panel', alias: 'widget.pveNodeSummary', scrollable: true, bodyPadding: 5, showVersions: function() { var me = this; // Note: we use simply text/html here, because ExtJS grid has problems // with cut&paste var nodename = me.pveSelNode.data.node; var view = Ext.createWidget('component', { autoScroll: true, padding: 5, style: { 'background-color': 'white', 'white-space': 'pre', 'font-family': 'monospace' } }); var win = Ext.create('Ext.window.Window', { title: gettext('Package versions'), width: 600, height: 400, layout: 'fit', modal: true, items: [ view ] }); Proxmox.Utils.API2Request({ waitMsgTarget: me, url: "/nodes/" + nodename + "/apt/versions", method: 'GET', failure: function(response, opts) { win.close(); Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function(response, opts) { win.show(); var text = ''; Ext.Array.each(response.result.data, function(rec) { var version = "not correctly installed"; var pkg = rec.Package; if (rec.OldVersion && rec.CurrentState === 'Installed') { version = rec.OldVersion; } if (rec.RunningKernel) { text += pkg + ': ' + version + ' (running kernel: ' + rec.RunningKernel + ')\n'; } else if (rec.ManagerVersion) { text += pkg + ': ' + version + ' (running version: ' + rec.ManagerVersion + ')\n'; } else { text += pkg + ': ' + version + '\n'; } }); view.update(Ext.htmlEncode(text)); } }); }, initComponent: function() { var me = this; var nodename = me.pveSelNode.data.node; if (!nodename) { throw "no node name specified"; } if (!me.statusStore) { throw "no status storage specified"; } var rstore = me.statusStore; var version_btn = new Ext.Button({ text: gettext('Package versions'), handler: function(){ Proxmox.Utils.checked_command(function() { me.showVersions(); }); } }); var rrdstore = Ext.create('Proxmox.data.RRDStore', { rrdurl: "/api2/json/nodes/" + nodename + "/rrddata", model: 'pve-rrd-node' }); Ext.apply(me, { tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' } ], items: [ { xtype: 'container', layout: 'column', defaults: { minHeight: 320, padding: 5, plugins: 'responsive', responsiveConfig: { 'width < 1900': { columnWidth: 1 }, 'width >= 1900': { columnWidth: 0.5 } } }, items: [ { xtype: 'pveNodeStatus', rstore: rstore, width: 770, pveSelNode: me.pveSelNode }, { xtype: 'proxmoxRRDChart', title: gettext('CPU usage'), fields: ['cpu','iowait'], fieldTitles: [gettext('CPU usage'), gettext('IO delay')], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Server load'), fields: ['loadavg'], fieldTitles: [gettext('Load average')], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Memory usage'), fields: ['memtotal','memused'], fieldTitles: [gettext('Total'), gettext('RAM usage')], store: rrdstore }, { xtype: 'proxmoxRRDChart', title: gettext('Network traffic'), fields: ['netin','netout'], store: rrdstore } ] } ], listeners: { activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } } }); me.callParent(); } }); /*global Blob*/ Ext.define('PVE.node.SubscriptionKeyEdit', { extend: 'Proxmox.window.Edit', title: gettext('Upload Subscription Key'), width: 300, items: { xtype: 'textfield', name: 'key', value: '', fieldLabel: gettext('Subscription Key') }, initComponent : function() { var me = this; me.callParent(); me.load(); } }); Ext.define('PVE.node.Subscription', { extend: 'Proxmox.grid.ObjectGrid', alias: ['widget.pveNodeSubscription'], onlineHelp: 'getting_help', viewConfig: { enableTextSelection: true }, showReport: function() { var me = this; var nodename = me.pveSelNode.data.node; var getReportFileName = function() { var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i'); return me.nodename + '-report-' + now + '.txt'; }; var view = Ext.createWidget('component', { itemId: 'system-report-view', scrollable: true, style: { 'background-color': 'white', 'white-space': 'pre', 'font-family': 'monospace', padding: '5px' } }); var reportWindow = Ext.create('Ext.window.Window', { title: gettext('System Report'), width: 1024, height: 600, layout: 'fit', modal: true, buttons: [ '->', { text: gettext('Download'), handler: function() { var fileContent = reportWindow.getComponent('system-report-view').html; var fileName = getReportFileName(); // Internet Explorer if (window.navigator.msSaveOrOpenBlob) { navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName); } else { var element = document.createElement('a'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(fileContent)); element.setAttribute('download', fileName); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } } } ], items: view }); Proxmox.Utils.API2Request({ url: '/api2/extjs/nodes/' + me.nodename + '/report', method: 'GET', waitMsgTarget: me, failure: function(response) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, success: function(response) { var report = Ext.htmlEncode(response.result.data); reportWindow.show(); view.update(report); } }); }, initComponent : function() { var me = this; if (!me.nodename) { throw "no node name specified"; } var reload = function() { me.rstore.load(); }; var baseurl = '/nodes/' + me.nodename + '/subscription'; var render_status = function(value) { var message = me.getObjectValue('message'); if (message) { return value + ": " + message; } return value; }; var rows = { productname: { header: gettext('Type') }, key: { header: gettext('Subscription Key') }, status: { header: gettext('Status'), renderer: render_status }, message: { visible: false }, serverid: { header: gettext('Server ID') }, sockets: { header: gettext('Sockets') }, checktime: { header: gettext('Last checked'), renderer: Proxmox.Utils.render_timestamp }, nextduedate: { header: gettext('Next due date') } }; Ext.apply(me, { url: '/api2/json' + baseurl, cwidth1: 170, tbar: [ { text: gettext('Upload Subscription Key'), handler: function() { var win = Ext.create('PVE.node.SubscriptionKeyEdit', { url: '/api2/extjs/' + baseurl }); win.show(); win.on('destroy', reload); } }, { text: gettext('Check'), handler: function() { Proxmox.Utils.API2Request({ params: { force: 1 }, url: baseurl, method: 'POST', waitMsgTarget: me, failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, callback: reload }); } }, { text: gettext('System Report'), handler: function() { Proxmox.Utils.checked_command(function (){ me.showReport(); }); } } ], rows: rows, listeners: { activate: reload } }); me.callParent(); } }); Ext.define('PVE.node.CertificateView', { extend: 'Ext.container.Container', xtype: 'pveCertificatesView', onlineHelp: 'sysadmin_certificate_management', mixins: ['Proxmox.Mixin.CBind' ], items: [ { xtype: 'pveCertView', border: 0, cbind: { nodename: '{nodename}' } }, { xtype: 'pveACMEView', border: 0, cbind: { nodename: '{nodename}' } } ] }); Ext.define('PVE.node.CertificateViewer', { extend: 'Proxmox.window.Edit', title: gettext('Certificate'), fieldDefaults: { labelWidth: 120 }, width: 800, resizable: true, items: [ { xtype: 'displayfield', fieldLabel: gettext('Name'), name: 'filename' }, { xtype: 'displayfield', fieldLabel: gettext('Fingerprint'), name: 'fingerprint' }, { xtype: 'displayfield', fieldLabel: gettext('Issuer'), name: 'issuer' }, { xtype: 'displayfield', fieldLabel: gettext('Subject'), name: 'subject' }, { xtype: 'displayfield', fieldLabel: gettext('Valid Since'), renderer: Proxmox.Utils.render_timestamp, name: 'notbefore' }, { xtype: 'displayfield', fieldLabel: gettext('Expires'), renderer: Proxmox.Utils.render_timestamp, name: 'notafter' }, { xtype: 'displayfield', fieldLabel: gettext('Subject Alternative Names'), name: 'san', renderer: PVE.Utils.render_san }, { xtype: 'textarea', editable: false, grow: true, growMax: 200, fieldLabel: gettext('Certificate'), name: 'pem' } ], initComponent: function() { var me = this; if (!me.cert) { throw "no cert given"; } if (!me.nodename) { throw "no nodename given"; } me.url = '/nodes/' + me.nodename + '/certificates/info'; me.callParent(); // hide OK/Reset button, because we just want to show data me.down('toolbar[dock=bottom]').setVisible(false); me.load({ success: function(response) { if (Ext.isArray(response.result.data)) { Ext.Array.each(response.result.data, function(item) { if (item.filename === me.cert) { me.setValues(item); return false; } }); } } }); } }); Ext.define('PVE.node.CertUpload', { extend: 'Proxmox.window.Edit', xtype: 'pveCertUpload', title: gettext('Upload Custom Certificate'), resizable: false, isCreate: true, submitText: gettext('Upload'), method: 'POST', width: 600, apiCallDone: function(success, response, options) { if (!success) { return; } var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); Ext.getBody().mask(txt, ['pve-static-mask']); // reload after 10 seconds automatically Ext.defer(function() { window.location.reload(true); }, 10000); }, items: [ { fieldLabel: gettext('Private Key (Optional)'), labelAlign: 'top', emptyText: gettext('No change'), name: 'key', xtype: 'textarea' }, { xtype: 'filebutton', text: gettext('From File'), listeners: { change: function(btn, e, value) { var me = this.up('form'); e = e.event; Ext.Array.each(e.target.files, function(file) { PVE.Utils.loadSSHKeyFromFile(file, function(res) { me.down('field[name=key]').setValue(res); }); }); btn.reset(); } } }, { xtype: 'box', autoEl: 'hr' }, { fieldLabel: gettext('Certificate Chain'), labelAlign: 'top', allowBlank: false, name: 'certificates', xtype: 'textarea' }, { xtype: 'filebutton', text: gettext('From File'), listeners: { change: function(btn, e, value) { var me = this.up('form'); e = e.event; Ext.Array.each(e.target.files, function(file) { PVE.Utils.loadSSHKeyFromFile(file, function(res) { me.down('field[name=certificates]').setValue(res); }); }); btn.reset(); } } }, { xtype: 'hidden', name: 'restart', value: '1' }, { xtype: 'hidden', name: 'force', value: '1' } ], initComponent: function() { var me = this; if (!me.nodename) { throw "no nodename given"; } me.url = '/nodes/' + me.nodename + '/certificates/custom'; me.callParent(); } }); Ext.define('pve-certificate', { extend: 'Ext.data.Model', fields: [ 'filename', 'fingerprint', 'issuer', 'notafter', 'notbefore', 'subject', 'san' ], idProperty: 'filename' }); Ext.define('PVE.node.Certificates', { extend: 'Ext.grid.Panel', xtype: 'pveCertView', tbar: [ { xtype: 'button', text: gettext('Upload Custom Certificate'), handler: function() { var me = this.up('grid'); var win = Ext.create('PVE.node.CertUpload', { nodename: me.nodename }); win.show(); win.on('destroy', me.reload, me); } }, { xtype: 'button', itemId: 'deletebtn', text: gettext('Delete Custom Certificate'), handler: function() { var me = this.up('grid'); Proxmox.Utils.API2Request({ url: '/nodes/' + me.nodename + '/certificates/custom?restart=1', method: 'DELETE', success: function(response, opt) { var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); Ext.getBody().mask(txt, ['pve-static-mask']); // reload after 10 seconds automatically Ext.defer(function() { window.location.reload(true); }, 10000); }, failure: function(response, opt) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); } }, '-', { xtype: 'proxmoxButton', itemId: 'viewbtn', disabled: true, text: gettext('View Certificate'), handler: function() { var me = this.up('grid'); me.view_certificate(); } } ], columns: [ { header: gettext('File'), width: 150, dataIndex: 'filename' }, { header: gettext('Issuer'), flex: 1, dataIndex: 'issuer' }, { header: gettext('Subject'), flex: 1, dataIndex: 'subject' }, { header: gettext('Valid Since'), width: 150, dataIndex: 'notbefore', renderer: Proxmox.Utils.render_timestamp }, { header: gettext('Expires'), width: 150, dataIndex: 'notafter', renderer: Proxmox.Utils.render_timestamp }, { header: gettext('Subject Alternative Names'), flex: 1, dataIndex: 'san', renderer: PVE.Utils.render_san }, { header: gettext('Fingerprint'), dataIndex: 'fingerprint', hidden: true }, { header: gettext('PEM'), dataIndex: 'pem', hidden: true } ], reload: function() { var me = this; me.rstore.load(); }, set_button_status: function() { var me = this; var rec = me.rstore.getById('pveproxy-ssl.pem'); me.down('#deletebtn').setDisabled(!rec); }, view_certificate: function() { var me = this; var selection = me.getSelection(); if (!selection || selection.length < 1) { return; } var win = Ext.create('PVE.node.CertificateViewer', { cert: selection[0].data.filename, nodename : me.nodename }); win.show(); }, listeners: { itemdblclick: 'view_certificate' }, initComponent: function() { var me = this; if (!me.nodename) { throw "no nodename given"; } me.rstore = Ext.create('Proxmox.data.UpdateStore', { storeid: 'certs-' + me.nodename, model: 'pve-certificate', proxy: { type: 'proxmox', url: '/api2/json/nodes/' + me.nodename + '/certificates/info' } }); me.store = { type: 'diff', rstore: me.rstore }; me.callParent(); me.mon(me.rstore, 'load', me.set_button_status, me); me.rstore.startUpdate(); } }); Ext.define('PVE.node.ACMEEditor', { extend: 'Proxmox.window.Edit', xtype: 'pveACMEEditor', subject: gettext('Domains'), items: [ { xtype: 'inputpanel', items: [ { xtype: 'textarea', fieldLabel: gettext('Domains'), emptyText: "domain1.example.com\ndomain2.example.com", name: 'domains' } ], onGetValues: function(values) { if (!values.domains) { return { 'delete': 'acme' }; } var domains = values.domains.split(/\n/).join(';'); return { 'acme': 'domains=' + domains }; } } ], initComponent: function() { var me = this; me.callParent(); me.load({ success: function(response, opts) { var res = PVE.Parser.parseACME(response.result.data.acme); if (res) { res.domains = res.domains.join(' '); me.setValues(res); } } }); } }); Ext.define('PVE.node.ACMEAccountCreate', { extend: 'Proxmox.window.Edit', width: 400, title: gettext('Register Account'), isCreate: true, method: 'POST', submitText: gettext('Register'), url: '/cluster/acme/account', showTaskViewer: true, items: [ { xtype: 'proxmoxComboGrid', name: 'directory', allowBlank: false, valueField: 'url', displayField: 'name', fieldLabel: gettext('ACME Directory'), store: { autoLoad: true, fields: ['name', 'url'], idProperty: ['name'], proxy: { type: 'proxmox', url: '/api2/json/cluster/acme/directories' }, sorters: { property: 'name', order: 'ASC' } }, listConfig: { columns: [ { header: gettext('Name'), dataIndex: 'name', flex: 1 }, { header: gettext('URL'), dataIndex: 'url', flex: 1 } ] }, listeners: { change: function(combogrid, value) { var me = this; if (!value) { return; } var disp = me.up('window').down('#tos_url_display'); var field = me.up('window').down('#tos_url'); var checkbox = me.up('window').down('#tos_checkbox'); disp.setValue(gettext('Loading')); field.setValue(undefined); checkbox.setValue(undefined); Proxmox.Utils.API2Request({ url: '/cluster/acme/tos', method: 'GET', params: { directory: value }, success: function(response, opt) { me.up('window').down('#tos_url').setValue(response.result.data); me.up('window').down('#tos_url_display').setValue(response.result.data); }, failure: function(response, opt) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); } } }, { xtype: 'displayfield', itemId: 'tos_url_display', fieldLabel: gettext('Terms of Service'), renderer: PVE.Utils.render_optional_url, name: 'tos_url_display' }, { xtype: 'hidden', itemId: 'tos_url', name: 'tos_url' }, { xtype: 'proxmoxcheckbox', itemId: 'tos_checkbox', fieldLabel: gettext('Accept TOS'), submitValue: false, validateValue: function(value) { if (value && this.checked) { return true; } return false; } }, { xtype: 'textfield', name: 'contact', vtype: 'email', allowBlank: false, fieldLabel: gettext('E-Mail') } ] }); Ext.define('PVE.node.ACMEAccountView', { extend: 'Proxmox.window.Edit', width: 600, fieldDefaults: { labelWidth: 140 }, title: gettext('Account'), items: [ { xtype: 'displayfield', fieldLabel: gettext('E-Mail'), name: 'email' }, { xtype: 'displayfield', fieldLabel: gettext('Created'), name: 'createdAt' }, { xtype: 'displayfield', fieldLabel: gettext('Status'), name: 'status' }, { xtype: 'displayfield', fieldLabel: gettext('Directory'), renderer: PVE.Utils.render_optional_url, name: 'directory' }, { xtype: 'displayfield', fieldLabel: gettext('Terms of Services'), renderer: PVE.Utils.render_optional_url, name: 'tos' } ], initComponent: function() { var me = this; if (!me.accountname) { throw "no account name defined"; } me.url = '/cluster/acme/account/' + me.accountname; me.callParent(); // hide OK/Reset button, because we just want to show data me.down('toolbar[dock=bottom]').setVisible(false); me.load({ success: function(response) { var data = response.result.data; data.email = data.account.contact[0]; data.createdAt = data.account.createdAt; data.status = data.account.status; me.setValues(data); } }); } }); Ext.define('PVE.node.ACME', { extend: 'Proxmox.grid.ObjectGrid', xtype: 'pveACMEView', margin: '10 0 0 0', title: 'ACME', tbar: [ { xtype: 'button', itemId: 'edit', text: gettext('Edit Domains'), handler: function() { this.up('grid').run_editor(); } }, { xtype: 'button', itemId: 'createaccount', text: gettext('Register Account'), handler: function() { var me = this.up('grid'); var win = Ext.create('PVE.node.ACMEAccountCreate', { taskDone: function() { me.load_account(); me.reload(); } }); win.show(); } }, { xtype: 'button', itemId: 'viewaccount', text: gettext('View Account'), handler: function() { var me = this.up('grid'); var win = Ext.create('PVE.node.ACMEAccountView', { accountname: 'default' }); win.show(); } }, { xtype: 'button', itemId: 'order', text: gettext('Order Certificate'), handler: function() { var me = this.up('grid'); Proxmox.Utils.API2Request({ method: 'POST', params: { force: 1 }, url: '/nodes/' + me.nodename + '/certificates/acme/certificate', success: function(response, opt) { var win = Ext.create('Proxmox.window.TaskViewer', { upid: response.result.data, taskDone: function(success) { me.certificate_order_finished(success); } }); win.show(); }, failure: function(response, opt) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); } }); } } ], certificate_order_finished: function(success) { if (!success) { return; } var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); Ext.getBody().mask(txt, ['pve-static-mask']); // reload after 10 seconds automatically Ext.defer(function() { window.location.reload(true); }, 10000); }, set_button_status: function() { var me = this; var account = !!me.account; var acmeObj = PVE.Parser.parseACME(me.getObjectValue('acme')); var domains = acmeObj ? acmeObj.domains.length : 0; var order = me.down('#order'); order.setVisible(account); order.setDisabled(!account || !domains); me.down('#createaccount').setVisible(!account); me.down('#viewaccount').setVisible(account); }, load_account: function() { var me = this; // for now we only use the 'default' account Proxmox.Utils.API2Request({ url: '/cluster/acme/account/default', success: function(response, opt) { me.account = response.result.data; me.set_button_status(); }, failure: function(response, opt) { me.account = undefined; me.set_button_status(); } }); }, run_editor: function() { var me = this; var win = Ext.create(me.rows.acme.editor, me.editorConfig); win.show(); win.on('destroy', me.reload, me); }, listeners: { itemdblclick: 'run_editor' }, // account data gets loaded here account: undefined, disableSelection: true, initComponent: function() { var me = this; if (!me.nodename) { throw "no nodename given"; } me.url = '/api2/json/nodes/' + me.nodename + '/config'; me.editorConfig = { url: '/api2/extjs/nodes/' + me.nodename + '/config' }; /*jslint confusion: true*/ /*acme is a string above*/ me.rows = { acme: { defaultValue: '', header: gettext('Domains'), editor: 'PVE.node.ACMEEditor', renderer: function(value) { var acmeObj = PVE.Parser.parseACME(value); if (acmeObj) { return acmeObj.domains.join('
'); } 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': 'white', 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'); }); } }); } });