diff --git a/stable-7/pve-manager/js/pvemanagerlib.js b/stable-7/pve-manager/js/pvemanagerlib.js
new file mode 100644
index 0000000..8470957
--- /dev/null
+++ b/stable-7/pve-manager/js/pvemanagerlib.js
@@ -0,0 +1,54038 @@
+const pveOnlineHelpInfo = {
+ "ceph_rados_block_devices" : {
+ "link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices",
+ "title" : "Ceph RADOS Block Devices (RBD)"
+ },
+ "chapter_ha_manager" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager",
+ "title" : "High Availability"
+ },
+ "chapter_lvm" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm",
+ "title" : "Logical Volume Manager (LVM)"
+ },
+ "chapter_pct" : {
+ "link" : "/pve-docs/chapter-pct.html#chapter_pct",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "chapter_pve_firewall" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall",
+ "title" : "Proxmox VE Firewall"
+ },
+ "chapter_pveceph" : {
+ "link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph",
+ "title" : "Deploy Hyper-Converged Ceph Cluster"
+ },
+ "chapter_pvecm" : {
+ "link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm",
+ "title" : "Cluster Manager"
+ },
+ "chapter_pvesdn" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#chapter_pvesdn",
+ "title" : "Software Defined Network"
+ },
+ "chapter_pvesr" : {
+ "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr",
+ "title" : "Storage Replication"
+ },
+ "chapter_storage" : {
+ "link" : "/pve-docs/chapter-pvesm.html#chapter_storage",
+ "title" : "Proxmox VE Storage"
+ },
+ "chapter_system_administration" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration",
+ "title" : "Host System Administration"
+ },
+ "chapter_user_management" : {
+ "link" : "/pve-docs/chapter-pveum.html#chapter_user_management",
+ "title" : "User Management"
+ },
+ "chapter_virtual_machines" : {
+ "link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "chapter_vzdump" : {
+ "link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump",
+ "title" : "Backup and Restore"
+ },
+ "chapter_zfs" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs",
+ "title" : "ZFS on Linux"
+ },
+ "datacenter_configuration_file" : {
+ "link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file",
+ "title" : "Datacenter Configuration"
+ },
+ "external_metric_server" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#external_metric_server",
+ "title" : "External Metric Server"
+ },
+ "getting_help" : {
+ "link" : "/pve-docs/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_crs" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_crs",
+ "subtitle" : "Cluster Resource Scheduling",
+ "title" : "High Availability"
+ },
+ "ha_manager_fencing" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing",
+ "subtitle" : "Fencing",
+ "title" : "High Availability"
+ },
+ "ha_manager_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"
+ },
+ "ha_manager_shutdown_policy" : {
+ "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_shutdown_policy",
+ "subtitle" : "Shutdown Policy",
+ "title" : "High Availability"
+ },
+ "markdown_basics" : {
+ "link" : "/pve-docs/pve-admin-guide.html#markdown_basics",
+ "title" : "Markdown Primer"
+ },
+ "metric_server_graphite" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#metric_server_graphite",
+ "subtitle" : "Graphite server configuration",
+ "title" : "External Metric Server"
+ },
+ "metric_server_influxdb" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#metric_server_influxdb",
+ "subtitle" : "Influxdb plugin configuration",
+ "title" : "External Metric Server"
+ },
+ "pct_configuration" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_configuration",
+ "subtitle" : "Configuration",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_images" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_images",
+ "subtitle" : "Container Images",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_network" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_network",
+ "subtitle" : "Network",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_container_storage" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_container_storage",
+ "subtitle" : "Container Storage",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_cpu" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_cpu",
+ "subtitle" : "CPU",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_general" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_general",
+ "subtitle" : "General Settings",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_memory" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_memory",
+ "subtitle" : "Memory",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_migration" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_migration",
+ "subtitle" : "Migration",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_options" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_options",
+ "subtitle" : "Options",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "pct_startup_and_shutdown" : {
+ "link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown",
+ "subtitle" : "Automatic Start and Shutdown of Containers",
+ "title" : "Proxmox Container Toolkit"
+ },
+ "proxmox_node_management" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#proxmox_node_management",
+ "title" : "Proxmox Node Management"
+ },
+ "pve_admin_guide" : {
+ "link" : "/pve-docs/pve-admin-guide.html",
+ "title" : "Proxmox VE Administration Guide"
+ },
+ "pve_ceph_install" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install",
+ "subtitle" : "CLI Installation of Ceph Packages",
+ "title" : "Deploy Hyper-Converged Ceph Cluster"
+ },
+ "pve_ceph_osds" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds",
+ "subtitle" : "Ceph OSDs",
+ "title" : "Deploy Hyper-Converged Ceph Cluster"
+ },
+ "pve_ceph_pools" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools",
+ "subtitle" : "Ceph Pools",
+ "title" : "Deploy Hyper-Converged Ceph Cluster"
+ },
+ "pve_documentation_index" : {
+ "link" : "/pve-docs/index.html",
+ "title" : "Proxmox VE Documentation Index"
+ },
+ "pve_firewall_cluster_wide_setup" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup",
+ "subtitle" : "Cluster Wide Setup",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_host_specific_configuration" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration",
+ "subtitle" : "Host Specific Configuration",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_ip_aliases" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases",
+ "subtitle" : "IP Aliases",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_ip_sets" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets",
+ "subtitle" : "IP Sets",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_security_groups" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_security_groups",
+ "subtitle" : "Security Groups",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_firewall_vm_container_configuration" : {
+ "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration",
+ "subtitle" : "VM/Container Configuration",
+ "title" : "Proxmox VE Firewall"
+ },
+ "pve_service_daemons" : {
+ "link" : "/pve-docs/index.html#_service_daemons",
+ "title" : "Service Daemons"
+ },
+ "pveceph_fs" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs",
+ "subtitle" : "CephFS",
+ "title" : "Deploy Hyper-Converged Ceph Cluster"
+ },
+ "pveceph_fs_create" : {
+ "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create",
+ "subtitle" : "Create CephFS",
+ "title" : "Deploy Hyper-Converged Ceph Cluster"
+ },
+ "pvecm_create_cluster" : {
+ "link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster",
+ "subtitle" : "Create a Cluster",
+ "title" : "Cluster Manager"
+ },
+ "pvecm_join_node_to_cluster" : {
+ "link" : "/pve-docs/chapter-pvecm.html#pvecm_join_node_to_cluster",
+ "subtitle" : "Adding Nodes to the Cluster",
+ "title" : "Cluster Manager"
+ },
+ "pvesdn_config_controllers" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_controllers",
+ "subtitle" : "Controllers",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_config_vnet" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_vnet",
+ "subtitle" : "VNets",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_config_zone" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_config_zone",
+ "subtitle" : "Zones",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_controller_plugin_evpn" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_controller_plugin_evpn",
+ "subtitle" : "EVPN Controller",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_dns_plugin_powerdns" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_dns_plugin_powerdns",
+ "subtitle" : "PowerDNS Plugin",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_ipam_plugin_netbox" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_netbox",
+ "subtitle" : "NetBox IPAM Plugin",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_ipam_plugin_phpipam" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_phpipam",
+ "subtitle" : "phpIPAM Plugin",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_ipam_plugin_pveipam" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_ipam_plugin_pveipam",
+ "subtitle" : "Proxmox VE IPAM Plugin",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_zone_plugin_evpn" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_evpn",
+ "subtitle" : "EVPN Zones",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_zone_plugin_qinq" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_qinq",
+ "subtitle" : "QinQ Zones",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_zone_plugin_simple" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_simple",
+ "subtitle" : "Simple Zones",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_zone_plugin_vlan" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vlan",
+ "subtitle" : "VLAN Zones",
+ "title" : "Software Defined Network"
+ },
+ "pvesdn_zone_plugin_vxlan" : {
+ "link" : "/pve-docs/chapter-pvesdn.html#pvesdn_zone_plugin_vxlan",
+ "subtitle" : "VXLAN Zones",
+ "title" : "Software Defined Network"
+ },
+ "pvesr_schedule_time_format" : {
+ "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format",
+ "subtitle" : "Schedule Format",
+ "title" : "Storage Replication"
+ },
+ "pveum_authentication_realms" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms",
+ "subtitle" : "Authentication Realms",
+ "title" : "User Management"
+ },
+ "pveum_configure_u2f" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_configure_u2f",
+ "subtitle" : "Server Side U2F Configuration",
+ "title" : "User Management"
+ },
+ "pveum_configure_webauthn" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_configure_webauthn",
+ "subtitle" : "Server Side Webauthn Configuration",
+ "title" : "User Management"
+ },
+ "pveum_groups" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_groups",
+ "subtitle" : "Groups",
+ "title" : "User Management"
+ },
+ "pveum_ldap_sync" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_ldap_sync",
+ "subtitle" : "Syncing LDAP-Based Realms",
+ "title" : "User Management"
+ },
+ "pveum_permission_management" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_permission_management",
+ "subtitle" : "Permission Management",
+ "title" : "User Management"
+ },
+ "pveum_pools" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_pools",
+ "subtitle" : "Pools",
+ "title" : "User Management"
+ },
+ "pveum_roles" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_roles",
+ "subtitle" : "Roles",
+ "title" : "User Management"
+ },
+ "pveum_tokens" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_tokens",
+ "subtitle" : "API Tokens",
+ "title" : "User Management"
+ },
+ "pveum_users" : {
+ "link" : "/pve-docs/chapter-pveum.html#pveum_users",
+ "subtitle" : "Users",
+ "title" : "User Management"
+ },
+ "qm_bios_and_uefi" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi",
+ "subtitle" : "BIOS and UEFI",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_bootorder" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_bootorder",
+ "subtitle" : "Device Boot Order",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_cloud_init" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_cloud_init",
+ "title" : "Cloud-Init Support"
+ },
+ "qm_copy_and_clone" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone",
+ "subtitle" : "Copies and Clones",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_cpu" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_cpu",
+ "subtitle" : "CPU",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_display" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_display",
+ "subtitle" : "Display",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_general_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_general_settings",
+ "subtitle" : "General Settings",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_hard_disk" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_hard_disk",
+ "subtitle" : "Hard Disk",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_memory" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_memory",
+ "subtitle" : "Memory",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_migration" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_migration",
+ "subtitle" : "Migration",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_network_device" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_network_device",
+ "subtitle" : "Network Device",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_options" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_options",
+ "subtitle" : "Options",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_os_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_os_settings",
+ "subtitle" : "OS Settings",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_pci_passthrough_vm_config" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough_vm_config",
+ "subtitle" : "VM Configuration",
+ "title" : "PCI(e) Passthrough"
+ },
+ "qm_qemu_agent" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_qemu_agent",
+ "subtitle" : "QEMU Guest Agent",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_spice_enhancements" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_spice_enhancements",
+ "subtitle" : "SPICE Enhancements",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_startup_and_shutdown" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown",
+ "subtitle" : "Automatic Start and Shutdown of Virtual Machines",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_system_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_system_settings",
+ "subtitle" : "System Settings",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_usb_passthrough" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough",
+ "subtitle" : "USB Passthrough",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_virtio_rng" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_virtio_rng",
+ "subtitle" : "VirtIO RNG",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "qm_virtual_machines_settings" : {
+ "link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings",
+ "subtitle" : "Virtual Machines Settings",
+ "title" : "QEMU/KVM Virtual Machines"
+ },
+ "storage_btrfs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_btrfs",
+ "title" : "BTRFS Backend"
+ },
+ "storage_cephfs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_cephfs",
+ "title" : "Ceph Filesystem (CephFS)"
+ },
+ "storage_cifs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_cifs",
+ "title" : "CIFS Backend"
+ },
+ "storage_directory" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_directory",
+ "title" : "Directory Backend"
+ },
+ "storage_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_pbs" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_pbs",
+ "title" : "Proxmox Backup Server"
+ },
+ "storage_pbs_encryption" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_pbs_encryption",
+ "subtitle" : "Encryption",
+ "title" : "Proxmox Backup Server"
+ },
+ "storage_zfspool" : {
+ "link" : "/pve-docs/chapter-pvesm.html#storage_zfspool",
+ "title" : "Local ZFS Pool Backend"
+ },
+ "sysadmin_certificate_management" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management",
+ "title" : "Certificate Management"
+ },
+ "sysadmin_certs_acme_plugins" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certs_acme_plugins",
+ "subtitle" : "ACME Plugins",
+ "title" : "Certificate Management"
+ },
+ "sysadmin_network_configuration" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration",
+ "title" : "Network Configuration"
+ },
+ "sysadmin_package_repositories" : {
+ "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_package_repositories",
+ "title" : "Package Repositories"
+ },
+ "user-realms-ldap" : {
+ "link" : "/pve-docs/chapter-pveum.html#user-realms-ldap",
+ "subtitle" : "LDAP",
+ "title" : "User Management"
+ },
+ "user_mgmt" : {
+ "link" : "/pve-docs/chapter-pveum.html#user_mgmt",
+ "title" : "User Management"
+ },
+ "vzdump_retention" : {
+ "link" : "/pve-docs/chapter-vzdump.html#vzdump_retention",
+ "subtitle" : "Backup Retention",
+ "title" : "Backup and Restore"
+ }
+};
+// Some configuration values are complex strings - so we need parsers/generators for them.
+Ext.define('PVE.Parser', {
+ statics: {
+
+ // this class only contains static functions
+
+ printACME: function(value) {
+ if (Ext.isArray(value.domains)) {
+ value.domains = value.domains.join(';');
+ }
+ return PVE.Parser.printPropertyString(value);
+ },
+
+ parseACME: function(value) {
+ if (!value) {
+ return {};
+ }
+
+ let res = {};
+ try {
+ value.split(',').forEach(property => {
+ let [k, v] = property.split('=', 2);
+ if (Ext.isDefined(v)) {
+ res[k] = v;
+ } else {
+ throw `Failed to parse key-value pair: ${property}`;
+ }
+ });
+ } catch (err) {
+ console.warn(err);
+ return undefined;
+ }
+
+ if (res.domains !== undefined) {
+ res.domains = res.domains.split(/;/);
+ }
+
+ return res;
+ },
+
+ parseBoolean: function(value, default_value) {
+ if (!Ext.isDefined(value)) {
+ return default_value;
+ }
+ value = value.toLowerCase();
+ return value === '1' ||
+ value === 'on' ||
+ value === 'yes' ||
+ value === 'true';
+ },
+
+ parsePropertyString: function(value, defaultKey) {
+ let res = {};
+
+ if (typeof value !== 'string' || value === '') {
+ return res;
+ }
+
+ try {
+ value.split(',').forEach(property => {
+ let [k, v] = property.split('=', 2);
+ if (Ext.isDefined(v)) {
+ res[k] = v;
+ } else if (Ext.isDefined(defaultKey)) {
+ if (Ext.isDefined(res[defaultKey])) {
+ throw 'defaultKey may be only defined once in propertyString';
+ }
+ res[defaultKey] = k; // k ist the value in this case
+ } else {
+ throw `Failed to parse key-value pair: ${property}`;
+ }
+ });
+ } catch (err) {
+ console.warn(err);
+ return undefined;
+ }
+
+ return res;
+ },
+
+ printPropertyString: function(data, defaultKey) {
+ var stringparts = [],
+ gotDefaultKeyVal = false,
+ defaultKeyVal;
+
+ Ext.Object.each(data, function(key, value) {
+ if (defaultKey !== undefined && key === defaultKey) {
+ gotDefaultKeyVal = true;
+ defaultKeyVal = value;
+ } else if (value !== '') {
+ stringparts.push(key + '=' + value);
+ }
+ });
+
+ stringparts = stringparts.sort();
+ if (gotDefaultKeyVal) {
+ stringparts.unshift(defaultKeyVal);
+ }
+
+ return stringparts.join(',');
+ },
+
+ parseQemuNetwork: function(key, value) {
+ if (!(key && value)) {
+ return undefined;
+ }
+
+ let res = {},
+ errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return undefined; // continue
+ }
+
+ let match_res;
+
+ if ((match_res = p.match(/^(ne2k_pci|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 if ((match_res = p.match(/^mtu=(\d+)$/)) !== null) {
+ res.mtu = match_res[1];
+ } else {
+ errors = true;
+ return false; // break
+ }
+ return undefined; // continue
+ });
+
+ if (errors || !res.model) {
+ return undefined;
+ }
+
+ return res;
+ },
+
+ printQemuNetwork: function(net) {
+ var netstr = net.model;
+ if (net.macaddr) {
+ netstr += "=" + net.macaddr;
+ }
+ if (net.bridge) {
+ netstr += ",bridge=" + net.bridge;
+ if (net.tag) {
+ netstr += ",tag=" + net.tag;
+ }
+ if (net.firewall) {
+ netstr += ",firewall=" + net.firewall;
+ }
+ }
+ if (net.rate) {
+ netstr += ",rate=" + net.rate;
+ }
+ if (net.queues) {
+ netstr += ",queues=" + net.queues;
+ }
+ if (net.disconnect) {
+ netstr += ",link_down=" + net.disconnect;
+ }
+ if (net.trunks) {
+ netstr += ",trunks=" + net.trunks;
+ }
+ if (net.mtu) {
+ netstr += ",mtu=" + net.mtu;
+ }
+ return netstr;
+ },
+
+ parseQemuDrive: function(key, value) {
+ if (!(key && value)) {
+ return undefined;
+ }
+
+ const [, bus, index] = key.match(/^([a-z]+)(\d+)$/);
+ if (!bus) {
+ return undefined;
+ }
+ let res = {
+ 'interface': bus,
+ index,
+ };
+
+ var errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return undefined; // continue
+ }
+ let match = p.match(/^([a-z_]+)=(\S+)$/);
+ if (!match) {
+ if (!p.match(/[=]/)) {
+ res.file = p;
+ return undefined; // continue
+ }
+ errors = true;
+ return false; // break
+ }
+ let [, k, v] = match;
+ if (k === 'volume') {
+ k = 'file';
+ }
+
+ if (Ext.isDefined(res[k])) {
+ errors = true;
+ return false; // break
+ }
+
+ if (k === 'cache' && v === 'off') {
+ v = 'none';
+ }
+
+ res[k] = v;
+
+ return undefined; // continue
+ });
+
+ if (errors || !res.file) {
+ return undefined;
+ }
+
+ return res;
+ },
+
+ printQemuDrive: function(drive) {
+ var drivestr = drive.file;
+
+ Ext.Object.each(drive, function(key, value) {
+ if (!Ext.isDefined(value) || key === 'file' ||
+ key === 'index' || key === 'interface') {
+ return; // continue
+ }
+ drivestr += ',' + key + '=' + value;
+ });
+
+ return drivestr;
+ },
+
+ parseIPConfig: function(key, value) {
+ if (!(key && value)) {
+ return undefined; // continue
+ }
+
+ let res = {};
+ try {
+ value.split(',').forEach(p => {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+
+ const match = p.match(/^(ip|gw|ip6|gw6)=(\S+)$/);
+ if (!match) {
+ throw `could not parse as IP config: ${p}`;
+ }
+ let [, k, v] = match;
+ res[k] = v;
+ });
+ } catch (err) {
+ console.warn(err);
+ return undefined; // continue
+ }
+
+ return res;
+ },
+
+ printIPConfig: function(cfg) {
+ return Object.entries(cfg)
+ .filter(([k, v]) => v && k.match(/^(ip|gw|ip6|gw6)$/))
+ .map(([k, v]) => `${k}=${v}`)
+ .join(',');
+ },
+
+ parseLxcNetwork: function(value) {
+ if (!value) {
+ return undefined;
+ }
+
+ let data = {};
+ value.split(',').forEach(p => {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+ let match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/);
+ if (match_res) {
+ data[match_res[1]] = match_res[2];
+ } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
+ data.firewall = PVE.Parser.parseBoolean(match_res[1]);
+ } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
+ data.link_down = PVE.Parser.parseBoolean(match_res[1]);
+ } else if (!p.match(/^type=\S+$/)) {
+ console.warn(`could not parse LXC network string ${p}`);
+ }
+ });
+
+ return data;
+ },
+
+ printLxcNetwork: function(config) {
+ let knownKeys = {
+ bridge: 1,
+ firewall: 1,
+ gw6: 1,
+ gw: 1,
+ hwaddr: 1,
+ ip6: 1,
+ ip: 1,
+ mtu: 1,
+ name: 1,
+ rate: 1,
+ tag: 1,
+ link_down: 1,
+ };
+ return Object.entries(config)
+ .filter(([k, v]) => v !== undefined && v !== '' && knownKeys[k])
+ .map(([k, v]) => `${k}=${v}`)
+ .join(',');
+ },
+
+ parseLxcMountPoint: function(value) {
+ if (!value) {
+ return undefined;
+ }
+
+ let res = {};
+ let errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return undefined; // continue
+ }
+ let match = p.match(/^([a-z_]+)=(.+)$/);
+ if (!match) {
+ if (!p.match(/[=]/)) {
+ res.file = p;
+ return undefined; // continue
+ }
+ errors = true;
+ return false; // break
+ }
+ let [, k, v] = match;
+ if (k === 'volume') {
+ k = 'file';
+ }
+
+ if (Ext.isDefined(res[k])) {
+ errors = true;
+ return false; // break
+ }
+
+ res[k] = v;
+
+ return undefined;
+ });
+
+ if (errors || !res.file) {
+ return undefined;
+ }
+
+ const match = res.file.match(/^([a-z][a-z0-9\-_.]*[a-z0-9]):/i);
+ if (match) {
+ res.storage = match[1];
+ res.type = 'volume';
+ } else if (res.file.match(/^\/dev\//)) {
+ res.type = 'device';
+ } else {
+ res.type = 'bind';
+ }
+
+ return res;
+ },
+
+ printLxcMountPoint: function(mp) {
+ let drivestr = mp.file;
+ for (const [key, value] of Object.entries(mp)) {
+ if (!Ext.isDefined(value) || key === 'file' || key === 'type' || key === 'storage') {
+ continue;
+ }
+ drivestr += `,${key}=${value}`;
+ }
+ return drivestr;
+ },
+
+ parseStartup: function(value) {
+ if (value === undefined) {
+ return undefined;
+ }
+
+ let res = {};
+ try {
+ value.split(',').forEach(p => {
+ if (!p || p.match(/^\s*$/)) {
+ return; // continue
+ }
+
+ let match_res;
+ if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) {
+ res.order = match_res[2];
+ } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) {
+ res.up = match_res[1];
+ } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) {
+ res.down = match_res[1];
+ } else {
+ throw `could not parse startup config ${p}`;
+ }
+ });
+ } catch (err) {
+ console.warn(err);
+ return undefined;
+ }
+
+ return res;
+ },
+
+ printStartup: function(startup) {
+ let arr = [];
+ if (startup.order !== undefined && startup.order !== '') {
+ arr.push('order=' + startup.order);
+ }
+ if (startup.up !== undefined && startup.up !== '') {
+ arr.push('up=' + startup.up);
+ }
+ if (startup.down !== undefined && startup.down !== '') {
+ arr.push('down=' + startup.down);
+ }
+
+ return arr.join(',');
+ },
+
+ parseQemuSmbios1: function(value) {
+ let res = value.split(',').reduce((acc, currentValue) => {
+ const [k, v] = currentValue.split(/[=](.+)/);
+ acc[k] = v;
+ return acc;
+ }, {});
+
+ if (PVE.Parser.parseBoolean(res.base64, false)) {
+ for (const [k, v] of Object.entries(res)) {
+ if (k !== 'uuid') {
+ res[k] = Ext.util.Base64.decode(v);
+ }
+ }
+ }
+
+ return res;
+ },
+
+ printQemuSmbios1: function(data) {
+ let base64 = false;
+ let datastr = Object.entries(data)
+ .map(([key, value]) => {
+ if (value === '') {
+ return undefined;
+ }
+ if (key !== 'uuid') {
+ base64 = true; // smbios values can be arbitrary, so encode and mark config as such
+ value = Ext.util.Base64.encode(value);
+ }
+ return `${key}=${value}`;
+ })
+ .filter(v => v !== undefined)
+ .join(',');
+
+ if (base64) {
+ datastr += ',base64=1';
+ }
+ return datastr;
+ },
+
+ parseTfaConfig: function(value) {
+ let res = {};
+ value.split(',').forEach(p => {
+ const [k, v] = p.split('=', 2);
+ res[k] = v;
+ });
+
+ return res;
+ },
+
+ parseTfaType: function(value) {
+ let match;
+ if (!value || !value.length) {
+ return undefined;
+ } else if (value === 'x!oath') {
+ return 'totp';
+ } else if ((match = value.match(/^x!(.+)$/)) !== null) {
+ return match[1];
+ } else {
+ return 1;
+ }
+ },
+
+ parseQemuCpu: function(value) {
+ if (!value) {
+ return {};
+ }
+
+ let res = {};
+ let errors = false;
+ Ext.Array.each(value.split(','), function(p) {
+ if (!p || p.match(/^\s*$/)) {
+ return undefined; // continue
+ }
+
+ if (!p.match(/[=]/)) {
+ if (Ext.isDefined(res.cpu)) {
+ errors = true;
+ return false; // break
+ }
+ res.cputype = p;
+ return undefined; // continue
+ }
+
+ let match = p.match(/^([a-z_]+)=(\S+)$/);
+ if (!match || Ext.isDefined(res[match[1]])) {
+ errors = true;
+ return false; // break
+ }
+
+ let [, k, v] = match;
+ res[k] = v;
+
+ return undefined;
+ });
+
+ if (errors || !res.cputype) {
+ return undefined;
+ }
+
+ return res;
+ },
+
+ printQemuCpu: function(cpu) {
+ let cpustr = cpu.cputype;
+ let optstr = '';
+
+ Ext.Object.each(cpu, function(key, value) {
+ if (!Ext.isDefined(value) || key === 'cputype') {
+ return; // continue
+ }
+ optstr += ',' + key + '=' + value;
+ });
+
+ if (!cpustr) {
+ if (optstr) {
+ return 'kvm64' + optstr;
+ } else {
+ return undefined;
+ }
+ }
+
+ return cpustr + optstr;
+ },
+
+ parseSSHKey: function(key) {
+ // |--- options can have quotes--| type key comment
+ let keyre = /^(?:((?:[^\s"]|"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
+ let typere = /^(?:(?:sk-)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)(?:@(?:[a-z0-9_-]+\.)+[a-z]{2,})?)$/;
+
+ let m = key.match(keyre);
+ if (!m || m.length < 3 || !m[2]) { // [2] is always either type or key
+ return null;
+ }
+ if (m[1] && m[1].match(typere)) {
+ return {
+ type: m[1],
+ key: m[2],
+ comment: m[3],
+ };
+ }
+ if (m[2].match(typere)) {
+ return {
+ options: m[1],
+ type: m[2],
+ key: m[3],
+ comment: m[4],
+ };
+ }
+ return null;
+ },
+
+ parseACMEPluginData: function(data) {
+ let res = {};
+ let extradata = [];
+ data.split('\n').forEach((line) => {
+ // capture everything after the first = as value
+ let [key, value] = line.split(/[=](.+)/);
+ if (value !== undefined) {
+ res[key] = value;
+ } else {
+ extradata.push(line);
+ }
+ });
+ return [res, extradata];
+ },
+},
+});
+/* This state provider keeps part of the state inside the browser history.
+ *
+ * We compress (shorten) url using dictionary based compression, i.e., we use
+ * column separated list instead of url encoded hash:
+ * #v\d* version/format
+ * := indicates string values
+ * :\d+ lookup value in dictionary hash
+ * #v1:=value1:5:=value2:=value3:...
+*/
+
+Ext.define('PVE.StateProvider', {
+ extend: 'Ext.state.LocalStorageProvider',
+
+ // private
+ setHV: function(name, newvalue, fireEvents) {
+ let me = this;
+
+ let changes = false;
+ let oldtext = Ext.encode(me.UIState[name]);
+ let newtext = Ext.encode(newvalue);
+ if (newtext !== oldtext) {
+ changes = true;
+ me.UIState[name] = newvalue;
+ if (fireEvents) {
+ me.fireEvent("statechange", me, name, { value: newvalue });
+ }
+ }
+ return changes;
+ },
+
+ // private
+ hslist: [
+ // order is important for notifications
+ // [ name, default ]
+ ['view', 'server'],
+ ['rid', 'root'],
+ ['ltab', 'tasks'],
+ ['nodetab', ''],
+ ['storagetab', ''],
+ ['sdntab', ''],
+ ['pooltab', ''],
+ ['kvmtab', ''],
+ ['lxctab', ''],
+ ['dctab', ''],
+ ],
+
+ hprefix: 'v1',
+
+ compDict: {
+ tfa: 54,
+ sdn: 53,
+ cloudinit: 52,
+ replication: 51,
+ system: 50,
+ monitor: 49,
+ 'ha-fencing': 48,
+ 'ha-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) {
+ let me = this;
+
+ let state = {};
+ if (!token) {
+ me.hslist.forEach(([k, v]) => { state[k] = v; });
+ return state;
+ }
+
+ let [prefix, ...items] = token.split(':');
+
+ if (prefix !== me.hprefix) {
+ return me.decodeHToken();
+ }
+
+ Ext.Array.each(me.hslist, function(rec) {
+ let value = items.shift();
+ if (value) {
+ if (value[0] === '=') {
+ value = decodeURIComponent(value.slice(1));
+ }
+ for (const [key, hash] of Object.entries(me.compDict)) {
+ if (String(value) === String(hash)) {
+ value = key;
+ break;
+ }
+ }
+ }
+ state[rec[0]] = value;
+ });
+
+ return state;
+ },
+
+ encodeHToken: function(state) {
+ let me = this;
+
+ let ctoken = me.hprefix;
+ Ext.Array.each(me.hslist, function(rec) {
+ let value = state[rec[0]];
+ if (!Ext.isDefined(value)) {
+ value = rec[1];
+ }
+ value = encodeURIComponent(value);
+ if (!value) {
+ ctoken += ':';
+ } else if (Ext.isDefined(me.compDict[value])) {
+ ctoken += ":" + me.compDict[value];
+ } else {
+ ctoken += ":=" + value;
+ }
+ });
+
+ return ctoken;
+ },
+
+ constructor: function(config) {
+ let me = this;
+
+ me.callParent([config]);
+
+ me.UIState = me.decodeHToken(); // set default
+
+ let history_change_cb = function(token) {
+ if (!token) {
+ Ext.History.back();
+ return;
+ }
+
+ let newstate = me.decodeHToken(token);
+ Ext.Array.each(me.hslist, function(rec) {
+ if (typeof newstate[rec[0]] === "undefined") {
+ return;
+ }
+ me.setHV(rec[0], newstate[rec[0]], true);
+ });
+ };
+
+ let start_token = Ext.History.getToken();
+ if (start_token) {
+ history_change_cb(start_token);
+ } else {
+ let htext = me.encodeHToken(me.UIState);
+ Ext.History.add(htext);
+ }
+
+ Ext.History.on('change', history_change_cb);
+ },
+
+ get: function(name, defaultValue) {
+ let me = this;
+
+ let data;
+ if (typeof me.UIState[name] !== "undefined") {
+ data = { value: me.UIState[name] };
+ } else {
+ data = me.callParent(arguments);
+ if (!data && name === 'GuiCap') {
+ data = {
+ vms: {},
+ storage: {},
+ access: {},
+ nodes: {},
+ dc: {},
+ sdn: {},
+ };
+ }
+ }
+ return data;
+ },
+
+ clear: function(name) {
+ let me = this;
+
+ if (typeof me.UIState[name] !== "undefined") {
+ me.UIState[name] = null;
+ }
+ me.callParent(arguments);
+ },
+
+ set: function(name, value, fireevent) {
+ let me = this;
+
+ if (typeof me.UIState[name] !== "undefined") {
+ var newvalue = value ? value.value : null;
+ if (me.setHV(name, newvalue, fireevent)) {
+ let htext = me.encodeHToken(me.UIState);
+ Ext.History.add(htext);
+ }
+ } else {
+ me.callParent(arguments);
+ }
+ },
+});
+Ext.ns('PVE');
+
+console.log("Starting Proxmox VE Manager");
+
+Ext.Ajax.defaultHeaders = {
+ 'Accept': 'application/json',
+};
+
+Ext.define('PVE.Utils', {
+ utilities: {
+
+ // this singleton contains miscellaneous utilities
+
+ toolkit: undefined, // (extjs|touch), set inside Toolkit.js
+
+ bus_match: /^(ide|sata|virtio|scsi)(\d+)$/,
+
+ log_severity_hash: {
+ 0: "panic",
+ 1: "alert",
+ 2: "critical",
+ 3: "error",
+ 4: "warning",
+ 5: "notice",
+ 6: "info",
+ 7: "debug",
+ },
+
+ support_level_hash: {
+ 'c': gettext('Community'),
+ 'b': gettext('Basic'),
+ 's': gettext('Standard'),
+ 'p': gettext('Premium'),
+ },
+
+ noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit '
+ +''
+ +'www.proxmox.com to get a list of available options.',
+
+ kvm_ostypes: {
+ 'Linux': [
+ { desc: '6.x - 2.6 Kernel', val: 'l26' },
+ { desc: '2.4 Kernel', val: 'l24' },
+ ],
+ 'Microsoft Windows': [
+ { desc: '11/2022', val: 'win11' },
+ { desc: '10/2016/2019', val: 'win10' },
+ { desc: '8.x/2012/2012r2', val: 'win8' },
+ { desc: '7/2008r2', val: 'win7' },
+ { desc: 'Vista/2008', val: 'w2k8' },
+ { desc: 'XP/2003', val: 'wxp' },
+ { desc: '2000', val: 'w2k' },
+ ],
+ 'Solaris Kernel': [
+ { desc: '-', val: 'solaris' },
+ ],
+ 'Other': [
+ { desc: '-', val: 'other' },
+ ],
+ },
+
+ is_windows: function(ostype) {
+ for (let entry of PVE.Utils.kvm_ostypes['Microsoft Windows']) {
+ if (entry.val === ostype) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ get_health_icon: function(state, circle) {
+ if (circle === undefined) {
+ circle = false;
+ }
+
+ if (state === undefined) {
+ state = 'uknown';
+ }
+
+ var icon = 'faded fa-question';
+ switch (state) {
+ case 'good':
+ icon = 'good fa-check';
+ break;
+ case 'upgrade':
+ icon = 'warning fa-upload';
+ break;
+ case 'old':
+ icon = 'warning fa-refresh';
+ break;
+ case 'warning':
+ icon = 'warning fa-exclamation';
+ break;
+ case 'critical':
+ icon = 'critical fa-times';
+ break;
+ default: break;
+ }
+
+ if (circle) {
+ icon += '-circle';
+ }
+
+ return icon;
+ },
+
+ parse_ceph_version: function(service) {
+ if (service.ceph_version_short) {
+ return service.ceph_version_short;
+ }
+
+ if (service.ceph_version) {
+ var match = service.ceph_version.match(/version (\d+(\.\d+)*)/);
+ if (match) {
+ return match[1];
+ }
+ }
+
+ return undefined;
+ },
+
+ compare_ceph_versions: function(a, b) {
+ let avers = [];
+ let bvers = [];
+
+ if (a === b) {
+ return 0;
+ }
+
+ if (Ext.isArray(a)) {
+ avers = a.slice(); // copy array
+ } else {
+ avers = a.toString().split('.');
+ }
+
+ if (Ext.isArray(b)) {
+ bvers = b.slice(); // copy array
+ } else {
+ bvers = b.toString().split('.');
+ }
+
+ for (;;) {
+ let av = avers.shift();
+ let bv = bvers.shift();
+
+ if (av === undefined && bv === undefined) {
+ return 0;
+ } else if (av === undefined) {
+ return -1;
+ } else if (bv === undefined) {
+ return 1;
+ } else {
+ let diff = parseInt(av, 10) - parseInt(bv, 10);
+ if (diff !== 0) return diff;
+ // else we need to look at the next parts
+ }
+ }
+ },
+
+ get_ceph_icon_html: function(health, fw) {
+ var state = PVE.Utils.map_ceph_health[health];
+ var cls = PVE.Utils.get_health_icon(state);
+ if (fw) {
+ cls += ' fa-fw';
+ }
+ return " ";
+ },
+
+ map_ceph_health: {
+ 'HEALTH_OK': 'good',
+ 'HEALTH_UPGRADE': 'upgrade',
+ 'HEALTH_OLD': 'old',
+ 'HEALTH_WARN': 'warning',
+ 'HEALTH_ERR': 'critical',
+ },
+
+ render_sdn_pending: function(rec, value, key, index) {
+ if (rec.data.state === undefined || rec.data.state === null) {
+ return value;
+ }
+
+ if (rec.data.state === 'deleted') {
+ if (value === undefined) {
+ return ' ';
+ } else {
+ return '
'+ value +'
';
+ }
+ } else if (rec.data.pending[key] !== undefined && rec.data.pending[key] !== null) {
+ if (rec.data.pending[key] === 'deleted') {
+ return ' ';
+ } else {
+ return rec.data.pending[key];
+ }
+ }
+ return value;
+ },
+
+ render_sdn_pending_state: function(rec, value) {
+ if (value === undefined || value === null) {
+ return ' ';
+ }
+
+ let icon = ``;
+
+ if (value === 'deleted') {
+ return '' + icon + value + '';
+ }
+
+ let tip = gettext('Pending Changes') + ':
';
+
+ for (const [key, keyvalue] of Object.entries(rec.data.pending)) {
+ if ((rec.data[key] !== undefined && rec.data.pending[key] !== rec.data[key]) ||
+ rec.data[key] === undefined
+ ) {
+ tip += `${key}: ${keyvalue}
`;
+ }
+ }
+ return ''+ icon + value + '';
+ },
+
+ render_ceph_health: function(healthObj) {
+ var state = {
+ iconCls: PVE.Utils.get_health_icon(),
+ text: '',
+ };
+
+ if (!healthObj || !healthObj.status) {
+ return state;
+ }
+
+ var health = PVE.Utils.map_ceph_health[healthObj.status];
+
+ state.iconCls = PVE.Utils.get_health_icon(health, true);
+ state.text = healthObj.status;
+
+ return state;
+ },
+
+ render_zfs_health: function(value) {
+ if (typeof value === 'undefined') {
+ return "";
+ }
+ var iconCls = 'question-circle';
+ switch (value) {
+ case 'AVAIL':
+ case 'ONLINE':
+ iconCls = 'check-circle good';
+ break;
+ case 'REMOVED':
+ case 'DEGRADED':
+ iconCls = 'exclamation-circle warning';
+ break;
+ case 'UNAVAIL':
+ case 'FAULTED':
+ case 'OFFLINE':
+ iconCls = 'times-circle critical';
+ break;
+ default: //unknown
+ }
+
+ return ' ' + value;
+ },
+
+ render_pbs_fingerprint: fp => fp.substring(0, 23),
+
+ render_backup_encryption: function(v, meta, record) {
+ if (!v) {
+ return gettext('No');
+ }
+
+ let tip = '';
+ if (v.match(/^[a-fA-F0-9]{2}:/)) { // fingerprint
+ tip = `Key fingerprint ${PVE.Utils.render_pbs_fingerprint(v)}`;
+ }
+ let icon = ``;
+ return `${icon} ${gettext('Encrypted')}`;
+ },
+
+ render_backup_verification: function(v, meta, record) {
+ let i = (cls, txt) => ` ${txt}`;
+ if (v === undefined || v === null) {
+ return i('question-circle-o warning', gettext('None'));
+ }
+ let tip = "";
+ let txt = gettext('Failed');
+ let iconCls = 'times critical';
+ if (v.state === 'ok') {
+ txt = gettext('OK');
+ iconCls = 'check good';
+ let now = Date.now() / 1000;
+ let task = Proxmox.Utils.parse_task_upid(v.upid);
+ let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
+ tip = `Last verify task started on ${verify_time}`;
+ if (now - v.starttime > 30 * 24 * 60 * 60) {
+ tip = `Last verify task over 30 days ago: ${verify_time}`;
+ iconCls = 'check warning';
+ }
+ }
+ return ` ${i(iconCls, txt)} `;
+ },
+
+ render_backup_status: function(value, meta, record) {
+ if (typeof value === 'undefined') {
+ return "";
+ }
+
+ let iconCls = 'check-circle good';
+ let text = gettext('Yes');
+
+ if (!PVE.Parser.parseBoolean(value.toString())) {
+ iconCls = 'times-circle critical';
+
+ text = gettext('No');
+
+ let reason = record.get('reason');
+ if (typeof reason !== 'undefined') {
+ if (reason in PVE.Utils.backup_reasons_table) {
+ reason = PVE.Utils.backup_reasons_table[record.get('reason')];
+ }
+ text = `${text} - ${reason}`;
+ }
+ }
+
+ return ` ${text}`;
+ },
+
+ render_backup_days_of_week: function(val) {
+ var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
+ var selected = [];
+ var cur = -1;
+ val.split(',').forEach(function(day) {
+ cur++;
+ var dow = (dows.indexOf(day)+6)%7;
+ if (cur === dow) {
+ if (selected.length === 0 || selected[selected.length-1] === 0) {
+ selected.push(1);
+ } else {
+ selected[selected.length-1]++;
+ }
+ } else {
+ while (cur < dow) {
+ cur++;
+ selected.push(0);
+ }
+ selected.push(1);
+ }
+ });
+
+ cur = -1;
+ var days = [];
+ selected.forEach(function(item) {
+ cur++;
+ if (item > 2) {
+ days.push(Ext.Date.dayNames[cur+1] + '-' + Ext.Date.dayNames[(cur+item)%7]);
+ cur += item-1;
+ } else if (item === 2) {
+ days.push(Ext.Date.dayNames[cur+1]);
+ days.push(Ext.Date.dayNames[(cur+2)%7]);
+ cur++;
+ } else if (item === 1) {
+ days.push(Ext.Date.dayNames[(cur+1)%7]);
+ }
+ });
+ return days.join(', ');
+ },
+
+ render_backup_selection: function(value, metaData, record) {
+ let allExceptText = gettext('All except {0}');
+ let allText = '-- ' + gettext('All') + ' --';
+ if (record.data.all) {
+ if (record.data.exclude) {
+ return Ext.String.format(allExceptText, record.data.exclude);
+ }
+ return allText;
+ }
+ if (record.data.vmid) {
+ return record.data.vmid;
+ }
+
+ if (record.data.pool) {
+ return "Pool '"+ record.data.pool + "'";
+ }
+
+ return "-";
+ },
+
+ backup_reasons_table: {
+ 'backup=yes': gettext('Enabled'),
+ 'backup=no': gettext('Disabled'),
+ 'enabled': gettext('Enabled'),
+ 'disabled': gettext('Disabled'),
+ 'not a volume': gettext('Not a volume'),
+ 'efidisk but no OMVF BIOS': gettext('EFI Disk without OMVF BIOS'),
+ },
+
+ renderNotFound: what => Ext.String.format(gettext("No {0} found"), what),
+
+ get_kvm_osinfo: function(value) {
+ var info = { base: 'Other' }; // default
+ if (value) {
+ Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) {
+ Ext.each(PVE.Utils.kvm_ostypes[k], function(e) {
+ if (e.val === value) {
+ info = { desc: e.desc, base: k };
+ }
+ });
+ });
+ }
+ return info;
+ },
+
+ render_kvm_ostype: function(value) {
+ var osinfo = PVE.Utils.get_kvm_osinfo(value);
+ if (osinfo.desc && osinfo.desc !== '-') {
+ return osinfo.base + ' ' + osinfo.desc;
+ } else {
+ return osinfo.base;
+ }
+ },
+
+ render_hotplug_features: function(value) {
+ var fa = [];
+
+ if (!value || value === '0') {
+ return gettext('Disabled');
+ }
+
+ if (value === '1') {
+ value = 'disk,network,usb';
+ }
+
+ Ext.each(value.split(','), function(el) {
+ if (el === 'disk') {
+ fa.push(gettext('Disk'));
+ } else if (el === 'network') {
+ fa.push(gettext('Network'));
+ } else if (el === 'usb') {
+ fa.push('USB');
+ } else if (el === 'memory') {
+ fa.push(gettext('Memory'));
+ } else if (el === 'cpu') {
+ fa.push(gettext('CPU'));
+ } else {
+ fa.push(el);
+ }
+ });
+
+ return fa.join(', ');
+ },
+
+ render_localtime: function(value) {
+ if (value === '__default__') {
+ return Proxmox.Utils.defaultText + ' (' + gettext('Enabled for Windows') + ')';
+ }
+ return Proxmox.Utils.format_boolean(value);
+ },
+
+ render_qga_features: function(config) {
+ if (!config) {
+ return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')';
+ }
+ let qga = PVE.Parser.parsePropertyString(config, 'enabled');
+ if (!PVE.Parser.parseBoolean(qga.enabled)) {
+ return Proxmox.Utils.disabledText;
+ }
+ delete qga.enabled;
+
+ let agentstring = Proxmox.Utils.enabledText;
+
+ for (const [key, value] of Object.entries(qga)) {
+ let displayText = Proxmox.Utils.disabledText;
+ if (key === 'type') {
+ let map = {
+ isa: "ISA",
+ virtio: "VirtIO",
+ };
+ displayText = map[value] || Proxmox.Utils.unknownText;
+ } else if (PVE.Parser.parseBoolean(value)) {
+ displayText = Proxmox.Utils.enabledText;
+ }
+ agentstring += `, ${key}: ${displayText}`;
+ }
+
+ return agentstring;
+ },
+
+ render_qemu_machine: function(value) {
+ return value || Proxmox.Utils.defaultText + ' (i440fx)';
+ },
+
+ render_qemu_bios: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText + ' (SeaBIOS)';
+ } else if (value === 'seabios') {
+ return "SeaBIOS";
+ } else if (value === 'ovmf') {
+ return "OVMF (UEFI)";
+ } else {
+ return value;
+ }
+ },
+
+ render_dc_ha_opts: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText;
+ } else {
+ return PVE.Parser.printPropertyString(value);
+ }
+ },
+ render_as_property_string: v => !v ? Proxmox.Utils.defaultText : PVE.Parser.printPropertyString(v),
+
+ render_scsihw: function(value) {
+ if (!value || value === '__default__') {
+ return Proxmox.Utils.defaultText + ' (LSI 53C895A)';
+ } else if (value === 'lsi') {
+ return 'LSI 53C895A';
+ } else if (value === 'lsi53c810') {
+ return 'LSI 53C810';
+ } else if (value === 'megasas') {
+ return 'MegaRAID SAS 8708EM2';
+ } else if (value === 'virtio-scsi-pci') {
+ return 'VirtIO SCSI';
+ } else if (value === 'virtio-scsi-single') {
+ return 'VirtIO SCSI single';
+ } else if (value === 'pvscsi') {
+ return 'VMware PVSCSI';
+ } else {
+ return value;
+ }
+ },
+
+ render_spice_enhancements: function(values) {
+ let props = PVE.Parser.parsePropertyString(values);
+ if (Ext.Object.isEmpty(props)) {
+ return Proxmox.Utils.noneText;
+ }
+
+ let output = [];
+ if (PVE.Parser.parseBoolean(props.foldersharing)) {
+ output.push('Folder Sharing: ' + gettext('Enabled'));
+ }
+ if (props.videostreaming === 'all' || props.videostreaming === 'filter') {
+ output.push('Video Streaming: ' + props.videostreaming);
+ }
+ return output.join(', ');
+ },
+
+ // fixme: auto-generate this
+ // for now, please keep in sync with PVE::Tools::kvmkeymaps
+ kvm_keymaps: {
+ '__default__': Proxmox.Utils.defaultText,
+ //ar: 'Arabic',
+ da: 'Danish',
+ de: 'German',
+ 'de-ch': 'German (Swiss)',
+ 'en-gb': 'English (UK)',
+ 'en-us': 'English (USA)',
+ es: 'Spanish',
+ //et: 'Estonia',
+ fi: 'Finnish',
+ //fo: 'Faroe Islands',
+ fr: 'French',
+ 'fr-be': 'French (Belgium)',
+ 'fr-ca': 'French (Canada)',
+ 'fr-ch': 'French (Swiss)',
+ //hr: 'Croatia',
+ hu: 'Hungarian',
+ is: 'Icelandic',
+ it: 'Italian',
+ ja: 'Japanese',
+ lt: 'Lithuanian',
+ //lv: 'Latvian',
+ mk: 'Macedonian',
+ nl: 'Dutch',
+ //'nl-be': 'Dutch (Belgium)',
+ no: 'Norwegian',
+ pl: 'Polish',
+ pt: 'Portuguese',
+ 'pt-br': 'Portuguese (Brazil)',
+ //ru: 'Russian',
+ sl: 'Slovenian',
+ sv: 'Swedish',
+ //th: 'Thai',
+ tr: 'Turkish',
+ },
+
+ kvm_vga_drivers: {
+ '__default__': Proxmox.Utils.defaultText,
+ std: gettext('Standard VGA'),
+ vmware: gettext('VMware compatible'),
+ qxl: 'SPICE',
+ qxl2: 'SPICE dual monitor',
+ qxl3: 'SPICE three monitors',
+ qxl4: 'SPICE four monitors',
+ serial0: gettext('Serial terminal') + ' 0',
+ serial1: gettext('Serial terminal') + ' 1',
+ serial2: gettext('Serial terminal') + ' 2',
+ serial3: gettext('Serial terminal') + ' 3',
+ virtio: 'VirtIO-GPU',
+ 'virtio-gl': 'VirGL GPU',
+ none: Proxmox.Utils.noneText,
+ },
+
+ render_kvm_language: function(value) {
+ if (!value || value === '__default__') {
+ return Proxmox.Utils.defaultText;
+ }
+ let text = PVE.Utils.kvm_keymaps[value];
+ return text ? `${text} (${value})` : value;
+ },
+
+ console_map: {
+ '__default__': Proxmox.Utils.defaultText + ' (xterm.js)',
+ 'vv': 'SPICE (remote-viewer)',
+ 'html5': 'HTML5 (noVNC)',
+ 'xtermjs': 'xterm.js',
+ },
+
+ render_console_viewer: function(value) {
+ value = value || '__default__';
+ return PVE.Utils.console_map[value] || value;
+ },
+
+ render_kvm_vga_driver: function(value) {
+ if (!value) {
+ return Proxmox.Utils.defaultText;
+ }
+ let vga = PVE.Parser.parsePropertyString(value, 'type');
+ let text = PVE.Utils.kvm_vga_drivers[vga.type];
+ if (!vga.type) {
+ text = Proxmox.Utils.defaultText;
+ }
+ return text ? `${text} (${value})` : value;
+ },
+
+ render_kvm_startup: function(value) {
+ var startup = PVE.Parser.parseStartup(value);
+
+ var res = 'order=';
+ if (startup.order === undefined) {
+ res += 'any';
+ } else {
+ res += startup.order;
+ }
+ if (startup.up !== undefined) {
+ res += ',up=' + startup.up;
+ }
+ if (startup.down !== undefined) {
+ res += ',down=' + startup.down;
+ }
+
+ return res;
+ },
+
+ extractFormActionError: function(action) {
+ var msg;
+ switch (action.failureType) {
+ case Ext.form.action.Action.CLIENT_INVALID:
+ msg = gettext('Form fields may not be submitted with invalid values');
+ break;
+ case Ext.form.action.Action.CONNECT_FAILURE:
+ msg = gettext('Connection error');
+ 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;
+ },
+
+ contentTypes: {
+ 'images': gettext('Disk image'),
+ 'backup': gettext('VZDump backup file'),
+ 'vztmpl': gettext('Container template'),
+ 'iso': gettext('ISO image'),
+ 'rootdir': gettext('Container'),
+ 'snippets': gettext('Snippets'),
+ },
+
+ volume_is_qemu_backup: function(volid, format) {
+ return format === 'pbs-vm' || volid.match(':backup/vzdump-qemu-');
+ },
+
+ volume_is_lxc_backup: function(volid, format) {
+ return format === 'pbs-ct' || volid.match(':backup/vzdump-(lxc|openvz)-');
+ },
+
+ authSchema: {
+ ad: {
+ name: gettext('Active Directory Server'),
+ ipanel: 'pveAuthADPanel',
+ syncipanel: 'pveAuthLDAPSyncPanel',
+ add: true,
+ tfa: true,
+ pwchange: true,
+ },
+ ldap: {
+ name: gettext('LDAP Server'),
+ ipanel: 'pveAuthLDAPPanel',
+ syncipanel: 'pveAuthLDAPSyncPanel',
+ add: true,
+ tfa: true,
+ pwchange: true,
+ },
+ openid: {
+ name: gettext('OpenID Connect Server'),
+ ipanel: 'pveAuthOpenIDPanel',
+ add: true,
+ tfa: false,
+ pwchange: false,
+ iconCls: 'pmx-itype-icon-openid-logo',
+ },
+ pam: {
+ name: 'Linux PAM',
+ ipanel: 'pveAuthBasePanel',
+ add: false,
+ tfa: true,
+ pwchange: true,
+ },
+ pve: {
+ name: 'Proxmox VE authentication server',
+ ipanel: 'pveAuthBasePanel',
+ add: false,
+ tfa: true,
+ pwchange: true,
+ },
+ },
+
+ storageSchema: {
+ dir: {
+ name: Proxmox.Utils.directoryText,
+ ipanel: 'DirInputPanel',
+ faIcon: 'folder',
+ backups: true,
+ },
+ lvm: {
+ name: 'LVM',
+ ipanel: 'LVMInputPanel',
+ faIcon: 'folder',
+ backups: false,
+ },
+ lvmthin: {
+ name: 'LVM-Thin',
+ ipanel: 'LvmThinInputPanel',
+ faIcon: 'folder',
+ backups: false,
+ },
+ btrfs: {
+ name: 'BTRFS',
+ ipanel: 'BTRFSInputPanel',
+ faIcon: 'folder',
+ backups: true,
+ },
+ nfs: {
+ name: 'NFS',
+ ipanel: 'NFSInputPanel',
+ faIcon: 'building',
+ backups: true,
+ },
+ cifs: {
+ name: 'SMB/CIFS',
+ ipanel: 'CIFSInputPanel',
+ faIcon: 'building',
+ backups: true,
+ },
+ glusterfs: {
+ name: 'GlusterFS',
+ ipanel: 'GlusterFsInputPanel',
+ faIcon: 'building',
+ backups: true,
+ },
+ iscsi: {
+ name: 'iSCSI',
+ ipanel: 'IScsiInputPanel',
+ faIcon: 'building',
+ backups: false,
+ },
+ cephfs: {
+ name: 'CephFS',
+ ipanel: 'CephFSInputPanel',
+ faIcon: 'building',
+ backups: true,
+ },
+ pvecephfs: {
+ name: 'CephFS (PVE)',
+ ipanel: 'CephFSInputPanel',
+ hideAdd: true,
+ faIcon: 'building',
+ backups: true,
+ },
+ rbd: {
+ name: 'RBD',
+ ipanel: 'RBDInputPanel',
+ faIcon: 'building',
+ backups: false,
+ },
+ pveceph: {
+ name: 'RBD (PVE)',
+ ipanel: 'RBDInputPanel',
+ hideAdd: true,
+ faIcon: 'building',
+ backups: false,
+ },
+ zfs: {
+ name: 'ZFS over iSCSI',
+ ipanel: 'ZFSInputPanel',
+ faIcon: 'building',
+ backups: false,
+ },
+ zfspool: {
+ name: 'ZFS',
+ ipanel: 'ZFSPoolInputPanel',
+ faIcon: 'folder',
+ backups: false,
+ },
+ pbs: {
+ name: 'Proxmox Backup Server',
+ ipanel: 'PBSInputPanel',
+ faIcon: 'floppy-o',
+ backups: true,
+ },
+ drbd: {
+ name: 'DRBD',
+ hideAdd: true,
+ backups: false,
+ },
+ },
+
+ sdnvnetSchema: {
+ vnet: {
+ name: 'vnet',
+ faIcon: 'folder',
+ },
+ },
+
+ sdnzoneSchema: {
+ zone: {
+ name: 'zone',
+ hideAdd: true,
+ },
+ simple: {
+ name: 'Simple',
+ ipanel: 'SimpleInputPanel',
+ faIcon: 'th',
+ },
+ vlan: {
+ name: 'VLAN',
+ ipanel: 'VlanInputPanel',
+ faIcon: 'th',
+ },
+ qinq: {
+ name: 'QinQ',
+ ipanel: 'QinQInputPanel',
+ faIcon: 'th',
+ },
+ vxlan: {
+ name: 'VXLAN',
+ ipanel: 'VxlanInputPanel',
+ faIcon: 'th',
+ },
+ evpn: {
+ name: 'EVPN',
+ ipanel: 'EvpnInputPanel',
+ faIcon: 'th',
+ },
+ },
+
+ sdncontrollerSchema: {
+ controller: {
+ name: 'controller',
+ hideAdd: true,
+ },
+ evpn: {
+ name: 'evpn',
+ ipanel: 'EvpnInputPanel',
+ faIcon: 'crosshairs',
+ },
+ bgp: {
+ name: 'bgp',
+ ipanel: 'BgpInputPanel',
+ faIcon: 'crosshairs',
+ },
+ },
+
+ sdnipamSchema: {
+ ipam: {
+ name: 'ipam',
+ hideAdd: true,
+ },
+ pve: {
+ name: 'PVE',
+ ipanel: 'PVEIpamInputPanel',
+ faIcon: 'th',
+ hideAdd: true,
+ },
+ netbox: {
+ name: 'Netbox',
+ ipanel: 'NetboxInputPanel',
+ faIcon: 'th',
+ },
+ phpipam: {
+ name: 'PhpIpam',
+ ipanel: 'PhpIpamInputPanel',
+ faIcon: 'th',
+ },
+ },
+
+ sdndnsSchema: {
+ dns: {
+ name: 'dns',
+ hideAdd: true,
+ },
+ powerdns: {
+ name: 'powerdns',
+ ipanel: 'PowerdnsInputPanel',
+ faIcon: 'th',
+ },
+ },
+
+ format_sdnvnet_type: function(value, md, record) {
+ var schema = PVE.Utils.sdnvnetSchema[value];
+ if (schema) {
+ return schema.name;
+ }
+ return Proxmox.Utils.unknownText;
+ },
+
+ format_sdnzone_type: function(value, md, record) {
+ var schema = PVE.Utils.sdnzoneSchema[value];
+ if (schema) {
+ return schema.name;
+ }
+ return Proxmox.Utils.unknownText;
+ },
+
+ format_sdncontroller_type: function(value, md, record) {
+ var schema = PVE.Utils.sdncontrollerSchema[value];
+ if (schema) {
+ return schema.name;
+ }
+ return Proxmox.Utils.unknownText;
+ },
+
+ format_sdnipam_type: function(value, md, record) {
+ var schema = PVE.Utils.sdnipamSchema[value];
+ if (schema) {
+ return schema.name;
+ }
+ return Proxmox.Utils.unknownText;
+ },
+
+ format_sdndns_type: function(value, md, record) {
+ var schema = PVE.Utils.sdndnsSchema[value];
+ if (schema) {
+ return schema.name;
+ }
+ return Proxmox.Utils.unknownText;
+ },
+
+ format_storage_type: function(value, md, record) {
+ if (value === 'rbd') {
+ value = !record || record.get('monhost') ? 'rbd' : 'pveceph';
+ } else if (value === 'cephfs') {
+ value = !record || record.get('monhost') ? 'cephfs' : 'pvecephfs';
+ }
+
+ let schema = PVE.Utils.storageSchema[value];
+ return schema?.name ?? value;
+ },
+
+ format_ha: function(value) {
+ var text = Proxmox.Utils.noneText;
+
+ if (value.managed) {
+ text = value.state || Proxmox.Utils.noneText;
+
+ 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;
+ },
+
+ calculate_hostcpu: function(data) {
+ if (!(data.uptime && Ext.isNumeric(data.cpu))) {
+ return -1;
+ }
+
+ if (data.type !== 'qemu' && data.type !== 'lxc') {
+ return -1;
+ }
+
+ var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node);
+ var node = PVE.data.ResourceStore.getAt(index);
+ if (!Ext.isDefined(node) || node === null) {
+ return -1;
+ }
+ var maxcpu = node.data.maxcpu || 1;
+
+ if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
+ return -1;
+ }
+
+ return (data.cpu/maxcpu) * data.maxcpu;
+ },
+
+ render_hostcpu: function(value, metaData, record, rowIndex, colIndex, store) {
+ if (!(record.data.uptime && Ext.isNumeric(record.data.cpu))) {
+ return '';
+ }
+
+ if (record.data.type !== 'qemu' && record.data.type !== 'lxc') {
+ return '';
+ }
+
+ var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node);
+ var node = PVE.data.ResourceStore.getAt(index);
+ if (!Ext.isDefined(node) || node === null) {
+ return '';
+ }
+ var maxcpu = node.data.maxcpu || 1;
+
+ if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
+ return '';
+ }
+
+ var per = (record.data.cpu/maxcpu) * record.data.maxcpu * 100;
+
+ return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU');
+ },
+
+ render_bandwidth: function(value) {
+ if (!Ext.isNumeric(value)) {
+ return '';
+ }
+
+ return Proxmox.Utils.format_size(value) + '/s';
+ },
+
+ render_timestamp_human_readable: function(value) {
+ return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s');
+ },
+
+ // render a timestamp or pending
+ render_next_event: function(value) {
+ if (!value) {
+ return '-';
+ }
+ let now = new Date(), next = new Date(value * 1000);
+ if (next < now) {
+ return gettext('pending');
+ }
+ return Proxmox.Utils.render_timestamp(value);
+ },
+
+ calculate_mem_usage: function(data) {
+ if (!Ext.isNumeric(data.mem) ||
+ data.maxmem === 0 ||
+ data.uptime < 1) {
+ return -1;
+ }
+
+ return data.mem / data.maxmem;
+ },
+
+ calculate_hostmem_usage: function(data) {
+ if (data.type !== 'qemu' && data.type !== 'lxc') {
+ return -1;
+ }
+
+ var index = PVE.data.ResourceStore.findExact('id', 'node/' + data.node);
+ var node = PVE.data.ResourceStore.getAt(index);
+
+ if (!Ext.isDefined(node) || node === null) {
+ return -1;
+ }
+ var maxmem = node.data.maxmem || 0;
+
+ if (!Ext.isNumeric(data.mem) ||
+ maxmem === 0 ||
+ data.uptime < 1) {
+ return -1;
+ }
+
+ return data.mem / maxmem;
+ },
+
+ render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
+ if (!Ext.isNumeric(value) || value === -1) {
+ return '';
+ }
+ if (value > 1) {
+ // we got no percentage but bytes
+ 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_hostmem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
+ if (!Ext.isNumeric(record.data.mem) || value === -1) {
+ return '';
+ }
+
+ if (record.data.type !== 'qemu' && record.data.type !== 'lxc') {
+ return '';
+ }
+
+ var index = PVE.data.ResourceStore.findExact('id', 'node/' + record.data.node);
+ var node = PVE.data.ResourceStore.getAt(index);
+ var maxmem = node.data.maxmem || 0;
+
+ if (record.data.mem > 1) {
+ // we got no percentage but bytes
+ var mem = record.data.mem;
+ if (!record.data.uptime ||
+ maxmem === 0 ||
+ !Ext.isNumeric(mem)) {
+ return '';
+ }
+
+ return ((mem*100)/maxmem).toFixed(1) + " %";
+ }
+ return (value*100).toFixed(1) + " %";
+ },
+
+ render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) {
+ var mem = value;
+ var maxmem = record.data.maxmem;
+
+ if (!record.data.uptime) {
+ return '';
+ }
+
+ if (!(Ext.isNumeric(mem) && maxmem)) {
+ return '';
+ }
+
+ return Proxmox.Utils.render_size(value);
+ },
+
+ calculate_disk_usage: function(data) {
+ if (!Ext.isNumeric(data.disk) ||
+ ((data.type === 'qemu' || data.type === 'lxc') && data.uptime === 0) ||
+ data.maxdisk === 0
+ ) {
+ return -1;
+ }
+
+ return data.disk / data.maxdisk;
+ },
+
+ render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
+ if (!Ext.isNumeric(value) || value === -1) {
+ return '';
+ }
+
+ return (value * 100).toFixed(1) + " %";
+ },
+
+ render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) {
+ var disk = value;
+ var maxdisk = record.data.maxdisk;
+ var type = record.data.type;
+
+ if (!Ext.isNumeric(disk) ||
+ maxdisk === 0 ||
+ ((type === 'qemu' || type === 'lxc') && record.data.uptime === 0)
+ ) {
+ return '';
+ }
+
+ return Proxmox.Utils.render_size(value);
+ },
+
+ get_object_icon_class: function(type, record) {
+ var status = '';
+ var objType = type;
+
+ if (type === 'type') {
+ // for folder view
+ objType = record.groupbyid;
+ } else if (record.template) {
+ // templates
+ objType = 'template';
+ status = type;
+ } else {
+ // everything else
+ status = record.status + ' ha-' + record.hastate;
+ }
+
+ if (record.lock) {
+ status += ' locked lock-' + record.lock;
+ }
+
+ var defaults = PVE.tree.ResourceTree.typeDefaults[objType];
+ if (defaults && defaults.iconCls) {
+ 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_optional_url: function(value) {
+ if (value && value.match(/^https?:\/\//)) {
+ return '' + value + '';
+ }
+ return value;
+ },
+
+ render_san: function(value) {
+ var names = [];
+ if (Ext.isArray(value)) {
+ value.forEach(function(val) {
+ if (!Ext.isNumber(val)) {
+ names.push(val);
+ }
+ });
+ return names.join('
');
+ }
+ return value;
+ },
+
+ render_full_name: function(firstname, metaData, record) {
+ var first = firstname || '';
+ var last = record.data.lastname || '';
+ return Ext.htmlEncode(first + " " + last);
+ },
+
+ // expecting the following format:
+ // [v2:10.10.10.1:6802/2008,v1:10.10.10.1:6803/2008]
+ render_ceph_osd_addr: function(value) {
+ value = value.trim();
+ if (value.startsWith('[') && value.endsWith(']')) {
+ value = value.slice(1, -1); // remove []
+ }
+ value = value.replaceAll(',', '\n'); // split IPs in lines
+ let retVal = '';
+ for (const i of value.matchAll(/^(v[0-9]):(.*):([0-9]*)\/([0-9]*)$/gm)) {
+ retVal += `${i[1]}: ${i[2]}:${i[3]}
`;
+ }
+ return retVal.length < 1 ? value : retVal;
+ },
+
+ windowHostname: function() {
+ return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match,
+ function(m, addr, offset, original) { return addr; });
+ },
+
+ openDefaultConsoleWindow: function(consoles, consoleType, vmid, nodename, vmname, cmd) {
+ var dv = PVE.Utils.defaultViewer(consoles, consoleType);
+ PVE.Utils.openConsoleWindow(dv, consoleType, vmid, nodename, vmname, cmd);
+ },
+
+ openConsoleWindow: function(viewer, consoleType, vmid, nodename, vmname, cmd) {
+ if (vmid === undefined && (consoleType === 'kvm' || consoleType === 'lxc')) {
+ throw "missing vmid";
+ }
+ if (!nodename) {
+ throw "no nodename specified";
+ }
+
+ if (viewer === 'html5') {
+ PVE.Utils.openVNCViewer(consoleType, vmid, nodename, vmname, cmd);
+ } else if (viewer === 'xtermjs') {
+ Proxmox.Utils.openXtermJsViewer(consoleType, vmid, nodename, vmname, cmd);
+ } else if (viewer === 'vv') {
+ let url = '/nodes/' + nodename + '/spiceshell';
+ let params = {
+ proxy: PVE.Utils.windowHostname(),
+ };
+ if (consoleType === 'kvm') {
+ url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy';
+ } else if (consoleType === 'lxc') {
+ url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy';
+ } else if (consoleType === 'upgrade') {
+ params.cmd = 'upgrade';
+ } else if (consoleType === 'cmd') {
+ params.cmd = cmd;
+ } else if (consoleType !== 'shell') {
+ throw `unknown spice viewer type '${consoleType}'`;
+ }
+ PVE.Utils.openSpiceViewer(url, params);
+ } else {
+ throw `unknown viewer type '${viewer}'`;
+ }
+ },
+
+ defaultViewer: function(consoles, type) {
+ var allowSpice, allowXtermjs;
+
+ if (consoles === true) {
+ allowSpice = true;
+ allowXtermjs = true;
+ } else if (typeof consoles === 'object') {
+ allowSpice = consoles.spice;
+ allowXtermjs = !!consoles.xtermjs;
+ }
+ let dv = PVE.UIOptions.options.console || (type === 'kvm' ? 'vv' : 'xtermjs');
+ if (dv === 'vv' && !allowSpice) {
+ dv = allowXtermjs ? 'xtermjs' : 'html5';
+ } else if (dv === 'xtermjs' && !allowXtermjs) {
+ dv = allowSpice ? 'vv' : 'html5';
+ }
+
+ return dv;
+ },
+
+ openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) {
+ let scaling = 'off';
+ if (Proxmox.Utils.toolkit !== 'touch') {
+ var sp = Ext.state.Manager.getProvider();
+ scaling = sp.get('novnc-scaling', 'off');
+ }
+ var url = Ext.Object.toQueryString({
+ console: vmtype, // kvm, lxc, upgrade or shell
+ novnc: 1,
+ vmid: vmid,
+ vmname: vmname,
+ node: nodename,
+ resize: scaling,
+ cmd: cmd,
+ });
+ var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427");
+ if (nw) {
+ nw.focus();
+ }
+ },
+
+ openSpiceViewer: function(url, params) {
+ var downloadWithName = function(uri, name) {
+ var link = Ext.DomHelper.append(document.body, {
+ tag: 'a',
+ href: uri,
+ css: 'display:none;visibility:hidden;height:0px;',
+ });
+
+ // Note: we need to tell Android and Chrome the correct file name extension
+ // but we do not set 'download' tag for other environments, because
+ // It can have strange side effects (additional user prompt on firefox)
+ if (navigator.userAgent.match(/Android|Chrome/i)) {
+ link.download = name;
+ }
+
+ if (link.fireEvent) {
+ link.fireEvent('onclick');
+ } else {
+ let evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
+ link.dispatchEvent(evt);
+ }
+ };
+
+ Proxmox.Utils.API2Request({
+ url: url,
+ params: params,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts) {
+ let cfg = response.result.data;
+ let raw = Object.entries(cfg).reduce((acc, [k, v]) => acc + `${k}=${v}\n`, "[virt-viewer]\n");
+ let spiceDownload = 'data:application/x-virt-viewer;charset=UTF-8,' + encodeURIComponent(raw);
+ downloadWithName(spiceDownload, "pve-spice.vv");
+ },
+ });
+ },
+
+ openTreeConsole: function(tree, record, item, index, e) {
+ e.stopEvent();
+ let nodename = record.data.node;
+ let vmid = record.data.vmid;
+ let vmname = record.data.name;
+ if (record.data.type === 'qemu' && !record.data.template) {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${nodename}/qemu/${vmid}/status/current`,
+ failure: response => Ext.Msg.alert('Error', response.htmlStatus),
+ success: function(response, opts) {
+ let conf = response.result.data;
+ let consoles = {
+ spice: !!conf.spice,
+ xtermjs: !!conf.serial,
+ };
+ PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
+ },
+ });
+ } else if (record.data.type === 'lxc' && !record.data.template) {
+ PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
+ }
+ },
+
+ // test automation helper
+ call_menu_handler: function(menu, text) {
+ let item = menu.query('menuitem').find(el => el.text === text);
+ if (item && item.handler) {
+ item.handler();
+ }
+ },
+
+ createCmdMenu: function(v, record, item, index, event) {
+ event.stopEvent();
+ if (!(v instanceof Ext.tree.View)) {
+ v.select(record);
+ }
+ let menu;
+ let type = record.data.type;
+
+ if (record.data.template) {
+ if (type === 'qemu' || type === 'lxc') {
+ menu = Ext.create('PVE.menu.TemplateMenu', {
+ pveSelNode: record,
+ });
+ }
+ } else if (type === 'qemu' || type === 'lxc' || type === 'node') {
+ menu = Ext.create('PVE.' + type + '.CmdMenu', {
+ pveSelNode: record,
+ nodename: record.data.node,
+ });
+ } else {
+ return undefined;
+ }
+
+ menu.showAt(event.getXY());
+ return menu;
+ },
+
+ // helper for deleting field which are set to there default values
+ delete_if_default: function(values, fieldname, default_val, create) {
+ if (values[fieldname] === '' || values[fieldname] === default_val) {
+ if (!create) {
+ if (values.delete) {
+ if (Ext.isArray(values.delete)) {
+ values.delete.push(fieldname);
+ } else {
+ values.delete += ',' + fieldname;
+ }
+ } else {
+ values.delete = fieldname;
+ }
+ }
+
+ delete values[fieldname];
+ }
+ },
+
+ loadSSHKeyFromFile: function(file, callback) {
+ // ssh-keygen produces ~ 740 bytes for a 4096 bit RSA key, current max is 16 kbit, so assume:
+ // 740 * 8 for max. 32kbit (5920 bytes), round upwards to 8192 bytes, leaves lots of comment space
+ PVE.Utils.loadFile(file, callback, 8192);
+ },
+
+ loadFile: function(file, callback, maxSize) {
+ maxSize = maxSize || 32 * 1024;
+ if (file.size > maxSize) {
+ Ext.Msg.alert(gettext('Error'), `${gettext("Invalid file size")}: ${file.size} > ${maxSize}`);
+ return;
+ }
+ let reader = new FileReader();
+ reader.onload = evt => callback(evt.target.result);
+ reader.readAsText(file);
+ },
+
+ loadTextFromFile: function(file, callback, maxBytes) {
+ let maxSize = maxBytes || 8192;
+ if (file.size > maxSize) {
+ Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
+ return;
+ }
+ let reader = new FileReader();
+ reader.onload = evt => callback(evt.target.result);
+ reader.readAsText(file);
+ },
+
+ diskControllerMaxIDs: {
+ ide: 4,
+ sata: 6,
+ scsi: 31,
+ virtio: 16,
+ unused: 256,
+ },
+
+ // types is either undefined (all busses), an array of busses, or a single bus
+ forEachBus: function(types, func) {
+ let busses = Object.keys(PVE.Utils.diskControllerMaxIDs);
+
+ if (Ext.isArray(types)) {
+ busses = types;
+ } else if (Ext.isDefined(types)) {
+ busses = [types];
+ }
+
+ // check if we only have valid busses
+ for (let i = 0; i < busses.length; i++) {
+ if (!PVE.Utils.diskControllerMaxIDs[busses[i]]) {
+ throw "invalid bus: '" + busses[i] + "'";
+ }
+ }
+
+ for (let i = 0; i < busses.length; i++) {
+ let count = PVE.Utils.diskControllerMaxIDs[busses[i]];
+ for (let j = 0; j < count; j++) {
+ let cont = func(busses[i], j);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+ }
+ },
+
+ mp_counts: {
+ mp: 256,
+ unused: 256,
+ },
+
+ forEachMP: function(func, includeUnused) {
+ for (let i = 0; i < PVE.Utils.mp_counts.mp; i++) {
+ let cont = func('mp', i);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+
+ if (!includeUnused) {
+ return;
+ }
+
+ for (let i = 0; i < PVE.Utils.mp_counts.unused; i++) {
+ let cont = func('unused', i);
+ if (!cont && cont !== undefined) {
+ return;
+ }
+ }
+ },
+
+ hardware_counts: {
+ net: 32,
+ usb: 14,
+ usb_old: 5,
+ hostpci: 16,
+ audio: 1,
+ efidisk: 1,
+ serial: 4,
+ rng: 1,
+ tpmstate: 1,
+ },
+
+ // we can have usb6 and up only for specific machine/ostypes
+ get_max_usb_count: function(ostype, machine) {
+ if (!ostype) {
+ return PVE.Utils.hardware_counts.usb_old;
+ }
+
+ let match = /-(\d+).(\d+)/.exec(machine ?? '');
+ if (!match || PVE.Utils.qemu_min_version([match[1], match[2]], [7, 1])) {
+ if (ostype === 'l26') {
+ return PVE.Utils.hardware_counts.usb;
+ }
+ let os_match = /^win(\d+)$/.exec(ostype);
+ if (os_match && os_match[1] > 7) {
+ return PVE.Utils.hardware_counts.usb;
+ }
+ }
+
+ return PVE.Utils.hardware_counts.usb_old;
+ },
+
+ // parameters are expected to be arrays, e.g. [7,1], [4,0,1]
+ // returns true if toCheck is equal or greater than minVersion
+ qemu_min_version: function(toCheck, minVersion) {
+ let i;
+ for (i = 0; i < toCheck.length && i < minVersion.length; i++) {
+ if (toCheck[i] < minVersion[i]) {
+ return false;
+ }
+ }
+
+ if (minVersion.length > toCheck.length) {
+ for (; i < minVersion.length; i++) {
+ if (minVersion[i] !== 0) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ },
+
+ cleanEmptyObjectKeys: function(obj) {
+ for (const propName of Object.keys(obj)) {
+ if (obj[propName] === null || obj[propName] === undefined) {
+ delete obj[propName];
+ }
+ }
+ },
+
+ acmedomain_count: 5,
+
+ add_domain_to_acme: function(acme, domain) {
+ if (acme.domains === undefined) {
+ acme.domains = [domain];
+ } else {
+ acme.domains.push(domain);
+ acme.domains = acme.domains.filter((value, index, self) => self.indexOf(value) === index);
+ }
+ return acme;
+ },
+
+ remove_domain_from_acme: function(acme, domain) {
+ if (acme.domains !== undefined) {
+ acme.domains = acme
+ .domains
+ .filter((value, index, self) => self.indexOf(value) === index && value !== domain);
+ }
+ return acme;
+ },
+
+ handleStoreErrorOrMask: function(view, store, regex, callback) {
+ view.mon(store, 'load', function(proxy, response, success, operation) {
+ if (success) {
+ Proxmox.Utils.setErrorMask(view, false);
+ return;
+ }
+ let msg;
+ if (operation.error.statusText) {
+ if (operation.error.statusText.match(regex)) {
+ callback(view, operation.error);
+ return;
+ } else {
+ msg = operation.error.statusText + ' (' + operation.error.status + ')';
+ }
+ } else {
+ msg = gettext('Connection error');
+ }
+ Proxmox.Utils.setErrorMask(view, msg);
+ });
+ },
+
+ showCephInstallOrMask: function(container, msg, nodename, callback) {
+ if (msg.match(/not (installed|initialized)/i)) {
+ if (Proxmox.UserName === 'root@pam') {
+ container.el.mask();
+ if (!container.down('pveCephInstallWindow')) {
+ var isInstalled = !!msg.match(/not initialized/i);
+ var win = Ext.create('PVE.ceph.Install', {
+ nodename: nodename,
+ });
+ win.getViewModel().set('isInstalled', isInstalled);
+ container.add(win);
+ win.on('close', () => {
+ container.el.unmask();
+ });
+ win.show();
+ callback(win);
+ }
+ } else {
+ container.mask(Ext.String.format(gettext('{0} not installed.') +
+ ' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']);
+ }
+ return true;
+ } else {
+ return false;
+ }
+ },
+
+ monitor_ceph_installed: function(view, rstore, nodename, maskOwnerCt) {
+ PVE.Utils.handleStoreErrorOrMask(
+ view,
+ rstore,
+ /not (installed|initialized)/i,
+ (_, error) => {
+ nodename = nodename || 'localhost';
+ let maskTarget = maskOwnerCt ? view.ownerCt : view;
+ rstore.stopUpdate();
+ PVE.Utils.showCephInstallOrMask(maskTarget, error.statusText, nodename, win => {
+ view.mon(win, 'cephInstallWindowClosed', () => rstore.startUpdate());
+ });
+ },
+ );
+ },
+
+
+ propertyStringSet: function(target, source, name, value) {
+ if (source) {
+ if (value === undefined) {
+ target[name] = source;
+ } else {
+ target[name] = value;
+ }
+ } else {
+ delete target[name];
+ }
+ },
+
+ forEachCorosyncLink: function(nodeinfo, cb) {
+ let re = /(?:ring|link)(\d+)_addr/;
+ Ext.iterate(nodeinfo, (prop, val) => {
+ let match = re.exec(prop);
+ if (match) {
+ cb(Number(match[1]), val);
+ }
+ });
+ },
+
+ cpu_vendor_map: {
+ 'default': 'QEMU',
+ 'AuthenticAMD': 'AMD',
+ 'GenuineIntel': 'Intel',
+ },
+
+ cpu_vendor_order: {
+ "AMD": 1,
+ "Intel": 2,
+ "QEMU": 3,
+ "Host": 4,
+ "_default_": 5, // includes custom models
+ },
+
+ verify_ip64_address_list: function(value, with_suffix) {
+ for (let addr of value.split(/[ ,;]+/)) {
+ if (addr === '') {
+ continue;
+ }
+
+ if (with_suffix) {
+ let parts = addr.split('%');
+ addr = parts[0];
+
+ if (parts.length > 2) {
+ return false;
+ }
+
+ if (parts.length > 1 && !addr.startsWith('fe80:')) {
+ return false;
+ }
+ }
+
+ if (!Proxmox.Utils.IP64_match.test(addr)) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ sortByPreviousUsage: function(vmconfig, controllerList) {
+ if (!controllerList) {
+ controllerList = ['ide', 'virtio', 'scsi', 'sata'];
+ }
+ let usedControllers = {};
+ for (const type of Object.keys(PVE.Utils.diskControllerMaxIDs)) {
+ usedControllers[type] = 0;
+ }
+
+ for (const property of Object.keys(vmconfig)) {
+ if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) {
+ const foundController = property.match(PVE.Utils.bus_match)[1];
+ usedControllers[foundController]++;
+ }
+ }
+
+ let sortPriority = PVE.qemu.OSDefaults.getDefaults(vmconfig.ostype).busPriority;
+
+ let sortedList = Ext.clone(controllerList);
+ sortedList.sort(function(a, b) {
+ if (usedControllers[b] === usedControllers[a]) {
+ return sortPriority[b] - sortPriority[a];
+ }
+ return usedControllers[b] - usedControllers[a];
+ });
+
+ return sortedList;
+ },
+
+ nextFreeDisk: function(controllers, config) {
+ for (const controller of controllers) {
+ for (let i = 0; i < PVE.Utils.diskControllerMaxIDs[controller]; i++) {
+ let confid = controller + i.toString();
+ if (!Ext.isDefined(config[confid])) {
+ return {
+ controller,
+ id: i,
+ confid,
+ };
+ }
+ }
+ }
+
+ return undefined;
+ },
+
+ nextFreeMP: function(type, config) {
+ for (let i = 0; i < PVE.Utils.mp_counts[type]; i++) {
+ let confid = `${type}${i}`;
+ if (!Ext.isDefined(config[confid])) {
+ return {
+ type,
+ id: i,
+ confid,
+ };
+ }
+ }
+
+ return undefined;
+ },
+
+ escapeNotesTemplate: function(value) {
+ let replace = {
+ '\\': '\\\\',
+ '\n': '\\n',
+ };
+ return value.replace(/(\\|[\n])/g, match => replace[match]);
+ },
+
+ unEscapeNotesTemplate: function(value) {
+ let replace = {
+ '\\\\': '\\',
+ '\\n': '\n',
+ };
+ return value.replace(/(\\\\|\\n)/g, match => replace[match]);
+ },
+
+ notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],
+
+ renderTags: function(tagstext, overrides) {
+ let text = '';
+ if (tagstext) {
+ let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
+ if (PVE.UIOptions.shouldSortTags()) {
+ tags = tags.sort((a, b) => {
+ let alc = a.toLowerCase();
+ let blc = b.toLowerCase();
+ return alc < blc ? -1 : blc < alc ? 1 : a.localeCompare(b);
+ });
+ }
+ text += ' ';
+ tags.forEach((tag) => {
+ text += Proxmox.Utils.getTagElement(tag, overrides);
+ });
+ }
+ return text;
+ },
+
+ tagCharRegex: /^[a-z0-9+_.-]+$/i,
+
+ verificationStateOrder: {
+ 'failed': 0,
+ 'none': 1,
+ 'ok': 2,
+ '__default__': 3,
+ },
+},
+
+ singleton: true,
+ constructor: function() {
+ var me = this;
+ Ext.apply(me, me.utilities);
+
+ Proxmox.Utils.override_task_descriptions({
+ acmedeactivate: ['ACME Account', gettext('Deactivate')],
+ acmenewcert: ['SRV', gettext('Order Certificate')],
+ acmerefresh: ['ACME Account', gettext('Refresh')],
+ acmeregister: ['ACME Account', gettext('Register')],
+ acmerenew: ['SRV', gettext('Renew Certificate')],
+ acmerevoke: ['SRV', gettext('Revoke Certificate')],
+ acmeupdate: ['ACME Account', gettext('Update')],
+ 'auth-realm-sync': [gettext('Realm'), gettext('Sync')],
+ 'auth-realm-sync-test': [gettext('Realm'), gettext('Sync Preview')],
+ cephcreatemds: ['Ceph Metadata Server', gettext('Create')],
+ cephcreatemgr: ['Ceph Manager', gettext('Create')],
+ cephcreatemon: ['Ceph Monitor', gettext('Create')],
+ cephcreateosd: ['Ceph OSD', gettext('Create')],
+ cephcreatepool: ['Ceph Pool', gettext('Create')],
+ cephdestroymds: ['Ceph Metadata Server', gettext('Destroy')],
+ cephdestroymgr: ['Ceph Manager', gettext('Destroy')],
+ cephdestroymon: ['Ceph Monitor', gettext('Destroy')],
+ cephdestroyosd: ['Ceph OSD', gettext('Destroy')],
+ cephdestroypool: ['Ceph Pool', gettext('Destroy')],
+ cephdestroyfs: ['CephFS', gettext('Destroy')],
+ cephfscreate: ['CephFS', gettext('Create')],
+ cephsetpool: ['Ceph Pool', gettext('Edit')],
+ cephsetflags: ['', gettext('Change global Ceph flags')],
+ clustercreate: ['', gettext('Create Cluster')],
+ clusterjoin: ['', gettext('Join Cluster')],
+ dircreate: [gettext('Directory Storage'), gettext('Create')],
+ dirremove: [gettext('Directory'), gettext('Remove')],
+ download: [gettext('File'), gettext('Download')],
+ hamigrate: ['HA', gettext('Migrate')],
+ hashutdown: ['HA', gettext('Shutdown')],
+ hastart: ['HA', gettext('Start')],
+ hastop: ['HA', gettext('Stop')],
+ imgcopy: ['', gettext('Copy data')],
+ imgdel: ['', gettext('Erase data')],
+ lvmcreate: [gettext('LVM Storage'), gettext('Create')],
+ lvmremove: ['Volume Group', gettext('Remove')],
+ lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
+ lvmthinremove: ['Thinpool', gettext('Remove')],
+ migrateall: ['', gettext('Migrate all VMs and Containers')],
+ 'move_volume': ['CT', gettext('Move Volume')],
+ 'pbs-download': ['VM/CT', gettext('File Restore Download')],
+ pull_file: ['CT', gettext('Pull file')],
+ push_file: ['CT', gettext('Push file')],
+ qmclone: ['VM', gettext('Clone')],
+ qmconfig: ['VM', gettext('Configure')],
+ qmcreate: ['VM', gettext('Create')],
+ qmdelsnapshot: ['VM', gettext('Delete Snapshot')],
+ qmdestroy: ['VM', gettext('Destroy')],
+ qmigrate: ['VM', gettext('Migrate')],
+ qmmove: ['VM', gettext('Move disk')],
+ qmpause: ['VM', gettext('Pause')],
+ qmreboot: ['VM', gettext('Reboot')],
+ qmreset: ['VM', gettext('Reset')],
+ qmrestore: ['VM', gettext('Restore')],
+ qmresume: ['VM', gettext('Resume')],
+ qmrollback: ['VM', gettext('Rollback')],
+ qmshutdown: ['VM', gettext('Shutdown')],
+ qmsnapshot: ['VM', gettext('Snapshot')],
+ qmstart: ['VM', gettext('Start')],
+ qmstop: ['VM', gettext('Stop')],
+ qmsuspend: ['VM', gettext('Hibernate')],
+ qmtemplate: ['VM', gettext('Convert to template')],
+ spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'],
+ spiceshell: ['', gettext('Shell') + ' (Spice)'],
+ startall: ['', gettext('Start all VMs and Containers')],
+ stopall: ['', gettext('Stop all VMs and Containers')],
+ unknownimgdel: ['', gettext('Destroy image from unknown guest')],
+ wipedisk: ['Device', gettext('Wipe Disk')],
+ vncproxy: ['VM/CT', gettext('Console')],
+ vncshell: ['', gettext('Shell')],
+ vzclone: ['CT', gettext('Clone')],
+ vzcreate: ['CT', gettext('Create')],
+ vzdelsnapshot: ['CT', gettext('Delete Snapshot')],
+ vzdestroy: ['CT', gettext('Destroy')],
+ vzdump: (type, id) => id ? `VM/CT ${id} - ${gettext('Backup')}` : gettext('Backup Job'),
+ vzmigrate: ['CT', gettext('Migrate')],
+ vzmount: ['CT', gettext('Mount')],
+ vzreboot: ['CT', gettext('Reboot')],
+ vzrestore: ['CT', gettext('Restore')],
+ vzresume: ['CT', gettext('Resume')],
+ vzrollback: ['CT', gettext('Rollback')],
+ vzshutdown: ['CT', gettext('Shutdown')],
+ vzsnapshot: ['CT', gettext('Snapshot')],
+ vzstart: ['CT', gettext('Start')],
+ vzstop: ['CT', gettext('Stop')],
+ vzsuspend: ['CT', gettext('Suspend')],
+ vztemplate: ['CT', gettext('Convert to template')],
+ vzumount: ['CT', gettext('Unmount')],
+ zfscreate: [gettext('ZFS Storage'), gettext('Create')],
+ zfsremove: ['ZFS Pool', gettext('Remove')],
+ });
+ },
+
+});
+Ext.define('PVE.UIOptions', {
+ singleton: true,
+
+ options: {
+ 'allowed-tags': [],
+ },
+
+ update: function() {
+ Proxmox.Utils.API2Request({
+ url: '/cluster/options',
+ method: 'GET',
+ success: function(response) {
+ for (const option of ['allowed-tags', 'console', 'tag-style']) {
+ PVE.UIOptions.options[option] = response?.result?.data?.[option];
+ }
+
+ PVE.UIOptions.updateTagList(PVE.UIOptions.options['allowed-tags']);
+ PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']);
+ PVE.UIOptions.fireUIConfigChanged();
+ },
+ });
+ },
+
+ tagList: [],
+
+ updateTagList: function(tags) {
+ PVE.UIOptions.tagList = [...new Set([...tags])].sort();
+ },
+
+ parseTagOverrides: function(overrides) {
+ let colors = {};
+ (overrides || "").split(';').forEach(color => {
+ if (!color) {
+ return;
+ }
+ let [tag, color_hex, font_hex] = color.split(':');
+ let r = parseInt(color_hex.slice(0, 2), 16);
+ let g = parseInt(color_hex.slice(2, 4), 16);
+ let b = parseInt(color_hex.slice(4, 6), 16);
+ colors[tag] = [r, g, b];
+ if (font_hex) {
+ colors[tag].push(parseInt(font_hex.slice(0, 2), 16));
+ colors[tag].push(parseInt(font_hex.slice(2, 4), 16));
+ colors[tag].push(parseInt(font_hex.slice(4, 6), 16));
+ }
+ });
+ return colors;
+ },
+
+ tagOverrides: {},
+
+ updateTagOverrides: function(colors) {
+ let sp = Ext.state.Manager.getProvider();
+ let color_state = sp.get('colors', '');
+ let browser_colors = PVE.UIOptions.parseTagOverrides(color_state);
+ PVE.UIOptions.tagOverrides = Ext.apply({}, browser_colors, colors);
+ },
+
+ updateTagSettings: function(style) {
+ let overrides = style?.['color-map'];
+ PVE.UIOptions.updateTagOverrides(PVE.UIOptions.parseTagOverrides(overrides ?? ""));
+
+ let shape = style?.shape ?? 'circle';
+ if (shape === '__default__') {
+ style = 'circle';
+ }
+
+ Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${shape}`);
+ },
+
+ tagTreeStyles: {
+ '__default__': `${Proxmox.Utils.defaultText} (${gettext('Circle')})`,
+ 'full': gettext('Full'),
+ 'circle': gettext('Circle'),
+ 'dense': gettext('Dense'),
+ 'none': Proxmox.Utils.NoneText,
+ },
+
+ tagOrderOptions: {
+ '__default__': `${Proxmox.Utils.defaultText} (${gettext('Alphabetical')})`,
+ 'config': gettext('Configuration'),
+ 'alphabetical': gettext('Alphabetical'),
+ },
+
+ shouldSortTags: function() {
+ return !(PVE.UIOptions.options['tag-style']?.ordering === 'config');
+ },
+
+ getTreeSortingValue: function(key) {
+ let localStorage = Ext.state.Manager.getProvider();
+ let browserValues = localStorage.get('pve-tree-sorting');
+ let defaults = {
+ 'sort-field': 'vmid',
+ 'group-templates': true,
+ 'group-guest-types': true,
+ };
+
+ return browserValues?.[key] ?? defaults[key];
+ },
+
+ fireUIConfigChanged: function() {
+ PVE.data.ResourceStore.refresh();
+ Ext.GlobalEvents.fireEvent('loadedUiOptions');
+ },
+});
+// ExtJS related things
+
+Proxmox.Utils.toolkit = 'extjs';
+
+// custom PVE specific VTypes
+Ext.apply(Ext.form.field.VTypes, {
+
+ QemuStartDate: function(v) {
+ return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v);
+ },
+ QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"',
+ IP64AddressList: v => PVE.Utils.verify_ip64_address_list(v, false),
+ IP64AddressWithSuffixList: v => PVE.Utils.verify_ip64_address_list(v, true),
+ IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2',
+ IP64AddressListMask: /[A-Fa-f0-9,:.; ]/,
+ PciIdText: gettext('Example') + ': 0x8086',
+ PciId: v => /^0x[0-9a-fA-F]{4}$/.test(v),
+});
+
+Ext.define('PVE.form.field.Display', {
+ override: 'Ext.form.field.Display',
+
+ setSubmitValue: function(value) {
+ // do nothing, this is only to allow generalized bindings for the:
+ // `me.isCreate ? 'textfield' : 'displayfield'` cases we have.
+ },
+});
+Ext.define('PVE.noVncConsole', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNoVncConsole',
+
+ nodename: undefined,
+ vmid: undefined,
+ cmd: undefined,
+
+ consoleType: undefined, // lxc, kvm, shell, cmd
+ xtermjs: false,
+
+ layout: 'fit',
+ border: false,
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.consoleType) {
+ throw "no console type specified";
+ }
+
+ if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') {
+ throw "no VM ID specified";
+ }
+
+ // always use same iframe, to avoid running several noVnc clients
+ // at same time (to avoid performance problems)
+ var box = Ext.create('Ext.ux.IFrame', { itemid: "vncconsole" });
+
+ var type = me.xtermjs ? 'xtermjs' : 'novnc';
+ Ext.apply(me, {
+ items: box,
+ listeners: {
+ activate: function() {
+ let sp = Ext.state.Manager.getProvider();
+ if (Ext.isFunction(me.beforeLoad)) {
+ me.beforeLoad();
+ }
+ let queryDict = {
+ console: me.consoleType, // kvm, lxc, upgrade or shell
+ vmid: me.vmid,
+ node: me.nodename,
+ cmd: me.cmd,
+ 'cmd-opts': me.cmdOpts,
+ resize: sp.get('novnc-scaling', 'scale'),
+ };
+ queryDict[type] = 1;
+ PVE.Utils.cleanEmptyObjectKeys(queryDict);
+ var url = '/?' + Ext.Object.toQueryString(queryDict);
+ box.load(url);
+ },
+ },
+ });
+
+ me.callParent();
+
+ me.on('afterrender', function() {
+ me.focus();
+ });
+ },
+
+ reload: function() {
+ // reload IFrame content to forcibly reconnect VNC/xterm.js to VM
+ var box = this.down('[itemid=vncconsole]');
+ box.getWin().location.reload();
+ },
+});
+
+Ext.define('PVE.button.ConsoleButton', {
+ extend: 'Ext.button.Split',
+ alias: 'widget.pveConsoleButton',
+
+ consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd'
+
+ cmd: undefined,
+
+ consoleName: undefined,
+
+ iconCls: 'fa fa-terminal',
+
+ enableSpice: true,
+ enableXtermjs: true,
+
+ nodename: undefined,
+
+ vmid: 0,
+
+ text: gettext('Console'),
+
+ setEnableSpice: function(enable) {
+ var me = this;
+
+ me.enableSpice = enable;
+ me.down('#spicemenu').setDisabled(!enable);
+ },
+
+ setEnableXtermJS: function(enable) {
+ var me = this;
+
+ me.enableXtermjs = enable;
+ me.down('#xtermjs').setDisabled(!enable);
+ },
+
+ handler: function() { // main, general, handler
+ let me = this;
+ PVE.Utils.openDefaultConsoleWindow(
+ {
+ spice: me.enableSpice,
+ xtermjs: me.enableXtermjs,
+ },
+ me.consoleType,
+ me.vmid,
+ me.nodename,
+ me.consoleName,
+ me.cmd,
+ );
+ },
+
+ openConsole: function(types) { // used by split-menu buttons
+ let me = this;
+ PVE.Utils.openConsoleWindow(
+ types,
+ me.consoleType,
+ me.vmid,
+ me.nodename,
+ me.consoleName,
+ me.cmd,
+ );
+ },
+
+ menu: [
+ {
+ xtype: 'menuitem',
+ text: 'noVNC',
+ iconCls: 'pve-itype-icon-novnc',
+ type: 'html5',
+ handler: function(button) {
+ let view = this.up('button');
+ view.openConsole(button.type);
+ },
+ },
+ {
+ xterm: 'menuitem',
+ itemId: 'spicemenu',
+ text: 'SPICE',
+ type: 'vv',
+ iconCls: 'pve-itype-icon-virt-viewer',
+ handler: function(button) {
+ let view = this.up('button');
+ view.openConsole(button.type);
+ },
+ },
+ {
+ text: 'xterm.js',
+ itemId: 'xtermjs',
+ iconCls: 'pve-itype-icon-xtermjs',
+ type: 'xtermjs',
+ handler: function(button) {
+ let view = this.up('button');
+ view.openConsole(button.type);
+ },
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.button.PendingRevert', {
+ extend: 'Proxmox.button.Button',
+ alias: 'widget.pvePendingRevertButton',
+
+ text: gettext('Revert'),
+ disabled: true,
+ config: {
+ pendingGrid: null,
+ apiurl: undefined,
+ },
+
+ handler: function() {
+ if (!this.pendingGrid) {
+ this.pendingGrid = this.up('proxmoxPendingObjectGrid');
+ if (!this.pendingGrid) throw "revert button requires a pendingGrid";
+ }
+ let view = this.pendingGrid;
+
+ let rec = view.getSelectionModel().getSelection()[0];
+ if (!rec) return;
+
+ let rowdef = view.rows[rec.data.key] || {};
+ let keys = rowdef.multiKey || [rec.data.key];
+
+ Proxmox.Utils.API2Request({
+ url: this.apiurl || view.editorConfig.url,
+ waitMsgTarget: view,
+ selModel: view.getSelectionModel(),
+ method: 'PUT',
+ params: {
+ 'revert': keys.join(','),
+ },
+ callback: () => view.reload(),
+ failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
+ });
+ },
+});
+/* Button features:
+ * - observe selection changes to enable/disable the button using enableFn()
+ * - pop up confirmation dialog using confirmMsg()
+ *
+ * does this for the button and every menu item
+ */
+Ext.define('PVE.button.Split', {
+ extend: 'Ext.button.Split',
+ alias: 'widget.pveSplitButton',
+
+ // the selection model to observe
+ selModel: undefined,
+
+ // if 'false' handler will not be called (button disabled)
+ enableFn: function(record) {
+ // do nothing
+ },
+
+ // function(record) or text
+ confirmMsg: false,
+
+ // take special care in confirm box (select no as default).
+ dangerous: false,
+
+ handlerWrapper: function(button, event) {
+ var me = this;
+ var rec, msg;
+ if (me.selModel) {
+ rec = me.selModel.getSelection()[0];
+ if (!rec || me.enableFn(rec) === false) {
+ return;
+ }
+ }
+
+ if (me.confirmMsg) {
+ msg = me.confirmMsg;
+ // confirMsg can be boolean or function
+ if (Ext.isFunction(me.confirmMsg)) {
+ msg = me.confirmMsg(rec);
+ }
+ Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
+ msg: msg,
+ buttons: Ext.Msg.YESNO,
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ me.realHandler(button, event, rec);
+ },
+ });
+ } else {
+ me.realHandler(button, event, rec);
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.handler) {
+ me.realHandler = me.handler;
+ me.handler = me.handlerWrapper;
+ }
+
+ if (me.menu && me.menu.items) {
+ me.menu.items.forEach(function(item) {
+ if (item.handler) {
+ item.realHandler = item.handler;
+ item.handler = me.handlerWrapper;
+ }
+
+ if (item.selModel) {
+ me.mon(item.selModel, "selectionchange", function() {
+ var rec = item.selModel.getSelection()[0];
+ if (!rec || item.enableFn(rec) === false) {
+ item.setDisabled(true);
+ } else {
+ item.setDisabled(false);
+ }
+ });
+ }
+ });
+ }
+
+ me.callParent();
+
+ if (me.selModel) {
+ me.mon(me.selModel, "selectionchange", function() {
+ var rec = me.selModel.getSelection()[0];
+ if (!rec || me.enableFn(rec) === false) {
+ me.setDisabled(true);
+ } else {
+ me.setDisabled(false);
+ }
+ });
+ }
+ },
+});
+Ext.define('PVE.controller.StorageEdit', {
+ extend: 'Ext.app.ViewController',
+ alias: 'controller.storageEdit',
+ control: {
+ 'field[name=content]': {
+ change: function(field, value) {
+ const hasImages = Ext.Array.contains(value, 'images');
+ const prealloc = field.up('form').getForm().findField('preallocation');
+ if (prealloc) {
+ prealloc.setDisabled(!hasImages);
+ }
+
+ 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.data.PermPathStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.pvePermPath',
+ fields: ['value'],
+ autoLoad: false,
+ data: [
+ { 'value': '/' },
+ { 'value': '/access' },
+ { 'value': '/access/groups' },
+ { 'value': '/access/realm' },
+ { 'value': '/nodes' },
+ { 'value': '/pool' },
+ { 'value': '/sdn/zones' },
+ { 'value': '/storage' },
+ { 'value': '/vms' },
+ ],
+
+ constructor: function(config) {
+ var me = this;
+
+ config = config || {};
+
+ me.callParent([config]);
+
+ let donePaths = {};
+ me.suspendEvents();
+ PVE.data.ResourceStore.each(function(record) {
+ let path;
+ switch (record.get('type')) {
+ case 'node': path = '/nodes/' + record.get('text');
+ break;
+ case 'qemu': path = '/vms/' + record.get('vmid');
+ break;
+ case 'lxc': path = '/vms/' + record.get('vmid');
+ break;
+ case 'sdn': path = '/sdn/zones/' + record.get('sdn');
+ break;
+ case 'storage': path = '/storage/' + record.get('storage');
+ break;
+ case 'pool': path = '/pool/' + record.get('pool');
+ break;
+ }
+ if (path !== undefined && !donePaths[path]) {
+ me.add({ value: path });
+ donePaths[path] = 1;
+ }
+ });
+ me.resumeEvents();
+
+ me.fireEvent('refresh', me);
+ me.fireEvent('datachanged', me);
+
+ me.sort({
+ property: 'value',
+ direction: 'ASC',
+ });
+ },
+});
+Ext.define('PVE.data.ResourceStore', {
+ extend: 'Proxmox.data.UpdateStore',
+ singleton: true,
+
+ findVMID: function(vmid) {
+ let me = this;
+ return me.findExact('vmid', parseInt(vmid, 10)) >= 0;
+ },
+
+ // returns the cached data from all nodes
+ getNodes: function() {
+ let me = this;
+
+ let nodes = [];
+ me.each(function(record) {
+ if (record.get('type') === "node") {
+ nodes.push(record.getData());
+ }
+ });
+
+ return nodes;
+ },
+
+ storageIsShared: function(storage_path) {
+ let me = this;
+
+ let index = me.findExact('id', storage_path);
+ if (index >= 0) {
+ return me.getAt(index).data.shared;
+ } else {
+ return undefined;
+ }
+ },
+
+ guestNode: function(vmid) {
+ let me = this;
+
+ let index = me.findExact('vmid', parseInt(vmid, 10));
+
+ return me.getAt(index).data.node;
+ },
+
+ guestName: function(vmid) {
+ let me = this;
+ let index = me.findExact('vmid', parseInt(vmid, 10));
+ if (index < 0) {
+ return '-';
+ }
+ let rec = me.getAt(index).data;
+ if ('name' in rec) {
+ return rec.name;
+ }
+ return '';
+ },
+
+ refresh: function() {
+ let me = this;
+ // can only refresh if we're loaded at least once and are not currently loading
+ if (!me.isLoading() && me.isLoaded()) {
+ let records = (me.getData().getSource() || me.getData()).getRange();
+ me.fireEvent('load', me, records);
+ }
+ },
+
+ constructor: function(config) {
+ let me = this;
+
+ config = config || {};
+
+ let field_defaults = {
+ type: {
+ header: gettext('Type'),
+ type: 'string',
+ renderer: PVE.Utils.render_resource_type,
+ sortable: true,
+ hideable: false,
+ width: 100,
+ },
+ id: {
+ header: 'ID',
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 80,
+ },
+ running: {
+ header: gettext('Online'),
+ type: 'boolean',
+ renderer: Proxmox.Utils.format_boolean,
+ hidden: true,
+ convert: function(value, record) {
+ var info = record.data;
+ return Ext.isNumeric(info.uptime) && info.uptime > 0;
+ },
+ },
+ text: {
+ header: gettext('Description'),
+ type: 'string',
+ sortable: true,
+ width: 200,
+ convert: function(value, record) {
+ if (value) {
+ return value;
+ }
+
+ let info = record.data, text;
+ if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
+ text = String(info.vmid);
+ if (info.name) {
+ text += " (" + info.name + ')';
+ }
+ } else { // node, pool, storage
+ text = info[info.type] || info.id;
+ if (info.node && info.type !== 'node') {
+ text += " (" + info.node + ")";
+ }
+ }
+
+ return text;
+ },
+ },
+ vmid: {
+ header: 'VMID',
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 80,
+ },
+ name: {
+ header: gettext('Name'),
+ hidden: true,
+ sortable: true,
+ type: 'string',
+ },
+ disk: {
+ header: gettext('Disk usage'),
+ type: 'integer',
+ renderer: PVE.Utils.render_disk_usage,
+ sortable: true,
+ width: 100,
+ hidden: true,
+ },
+ diskuse: {
+ header: gettext('Disk usage') + " %",
+ type: 'number',
+ sortable: true,
+ renderer: PVE.Utils.render_disk_usage_percent,
+ width: 100,
+ calculate: PVE.Utils.calculate_disk_usage,
+ sortType: 'asFloat',
+ },
+ maxdisk: {
+ header: gettext('Disk size'),
+ type: 'integer',
+ renderer: Proxmox.Utils.render_size,
+ sortable: true,
+ hidden: true,
+ width: 100,
+ },
+ mem: {
+ header: gettext('Memory usage'),
+ type: 'integer',
+ renderer: PVE.Utils.render_mem_usage,
+ sortable: true,
+ hidden: true,
+ width: 100,
+ },
+ memuse: {
+ header: gettext('Memory usage') + " %",
+ type: 'number',
+ renderer: PVE.Utils.render_mem_usage_percent,
+ calculate: PVE.Utils.calculate_mem_usage,
+ sortType: 'asFloat',
+ sortable: true,
+ width: 100,
+ },
+ maxmem: {
+ header: gettext('Memory size'),
+ type: 'integer',
+ renderer: Proxmox.Utils.render_size,
+ hidden: true,
+ sortable: true,
+ width: 100,
+ },
+ cpu: {
+ header: gettext('CPU usage'),
+ type: 'float',
+ renderer: Proxmox.Utils.render_cpu,
+ sortable: true,
+ width: 100,
+ },
+ maxcpu: {
+ header: gettext('maxcpu'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 60,
+ },
+ diskread: {
+ header: gettext('Total Disk Read'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100,
+ },
+ diskwrite: {
+ header: gettext('Total Disk Write'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100,
+ },
+ netin: {
+ header: gettext('Total NetIn'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100,
+ },
+ netout: {
+ header: gettext('Total NetOut'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ width: 100,
+ },
+ template: {
+ header: gettext('Template'),
+ type: 'integer',
+ hidden: true,
+ sortable: true,
+ width: 60,
+ },
+ uptime: {
+ header: gettext('Uptime'),
+ type: 'integer',
+ renderer: Proxmox.Utils.render_uptime,
+ sortable: true,
+ width: 110,
+ },
+ node: {
+ header: gettext('Node'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110,
+ },
+ storage: {
+ header: gettext('Storage'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110,
+ },
+ pool: {
+ header: gettext('Pool'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110,
+ },
+ hastate: {
+ header: gettext('HA State'),
+ type: 'string',
+ defaultValue: 'unmanaged',
+ hidden: true,
+ sortable: true,
+ },
+ status: {
+ header: gettext('Status'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110,
+ },
+ lock: {
+ header: gettext('Lock'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ width: 110,
+ },
+ hostcpu: {
+ header: gettext('Host CPU usage'),
+ type: 'float',
+ renderer: PVE.Utils.render_hostcpu,
+ calculate: PVE.Utils.calculate_hostcpu,
+ sortType: 'asFloat',
+ sortable: true,
+ width: 100,
+ },
+ hostmemuse: {
+ header: gettext('Host Memory usage') + " %",
+ type: 'number',
+ renderer: PVE.Utils.render_hostmem_usage_percent,
+ calculate: PVE.Utils.calculate_hostmem_usage,
+ sortType: 'asFloat',
+ sortable: true,
+ width: 100,
+ },
+ tags: {
+ header: gettext('Tags'),
+ renderer: (value) => PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
+ type: 'string',
+ sortable: true,
+ flex: 1,
+ },
+ // note: flex only last column to keep info closer together
+ };
+
+ let fields = [];
+ let fieldNames = [];
+ Ext.Object.each(field_defaults, function(key, value) {
+ var field = { name: key, type: value.type };
+ if (Ext.isDefined(value.convert)) {
+ field.convert = value.convert;
+ }
+
+ if (Ext.isDefined(value.calculate)) {
+ field.calculate = value.calculate;
+ }
+
+ if (Ext.isDefined(value.defaultValue)) {
+ field.defaultValue = value.defaultValue;
+ }
+
+ fields.push(field);
+ fieldNames.push(key);
+ });
+
+ Ext.define('PVEResources', {
+ extend: "Ext.data.Model",
+ fields: fields,
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/resources',
+ },
+ });
+
+ Ext.define('PVETree', {
+ extend: "Ext.data.Model",
+ fields: fields,
+ proxy: { type: 'memory' },
+ });
+
+ Ext.apply(config, {
+ storeid: 'PVEResources',
+ model: 'PVEResources',
+ defaultColumns: function() {
+ let res = [];
+ Ext.Object.each(field_defaults, function(field, info) {
+ let fieldInfo = Ext.apply({ dataIndex: field }, info);
+ res.push(fieldInfo);
+ });
+ return res;
+ },
+ fieldNames: fieldNames,
+ });
+
+ me.callParent([config]);
+ },
+});
+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-acme-challenges', {
+ extend: 'Ext.data.Model',
+ fields: ['id', 'type', 'schema'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/acme/challenge-schema",
+ },
+ idProperty: 'id',
+});
+
+Ext.define('PVE.form.ACMEApiSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveACMEApiSelector',
+
+ fieldLabel: gettext('DNS API'),
+ displayField: 'name',
+ valueField: 'id',
+
+ store: {
+ model: 'pve-acme-challenges',
+ autoLoad: true,
+ },
+
+ triggerAction: 'all',
+ queryMode: 'local',
+ allowBlank: false,
+ editable: true,
+ forceSelection: true,
+ anyMatch: true,
+ selectOnFocus: true,
+
+ getSchema: function() {
+ let me = this;
+ let val = me.getValue();
+ if (val) {
+ let record = me.getStore().findRecord('id', val, 0, false, true, true);
+ if (record) {
+ return record.data.schema;
+ }
+ }
+ return {};
+ },
+});
+Ext.define('PVE.form.ACMEAccountSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveACMEAccountSelector',
+
+ displayField: 'name',
+ valueField: 'name',
+
+ store: {
+ model: 'pve-acme-accounts',
+ autoLoad: true,
+ },
+
+ triggerAction: 'all',
+ queryMode: 'local',
+ allowBlank: false,
+ editable: false,
+ forceSelection: true,
+
+ isEmpty: function() {
+ return this.getStore().getData().length === 0;
+ },
+});
+Ext.define('PVE.form.ACMEPluginSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveACMEPluginSelector',
+
+ fieldLabel: gettext('Plugin'),
+ displayField: 'plugin',
+ valueField: 'plugin',
+
+ store: {
+ model: 'pve-acme-plugins',
+ autoLoad: true,
+ filters: item => item.data.type === 'dns',
+ },
+
+ triggerAction: 'all',
+ queryMode: 'local',
+ allowBlank: false,
+ editable: false,
+});
+Ext.define('PVE.form.AgentFeatureSelector', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: ['widget.pveAgentFeatureSelector'],
+
+ viewModel: {},
+
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabel: Ext.String.format(gettext('Use {0}'), 'QEMU Guest Agent'),
+ name: 'enabled',
+ reference: 'enabled',
+ uncheckedValue: 0,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabel: gettext('Run guest-trim after a disk move or VM migration'),
+ name: 'fstrim_cloned_disks',
+ bind: {
+ disabled: '{!enabled.checked}',
+ },
+ disabled: true,
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext('Make sure the QEMU Guest Agent is installed in the VM'),
+ bind: {
+ hidden: '{!enabled.checked}',
+ },
+ },
+ ],
+
+ advancedItems: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'type',
+ value: '__default__',
+ deleteEmpty: false,
+ fieldLabel: 'Type',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + " (VirtIO)"],
+ ['virtio', 'VirtIO'],
+ ['isa', 'ISA'],
+ ],
+ },
+ ],
+
+ onGetValues: function(values) {
+ var agentstr = PVE.Parser.printPropertyString(values, 'enabled');
+ return { agent: agentstr };
+ },
+
+ setValues: function(values) {
+ let res = PVE.Parser.parsePropertyString(values.agent, 'enabled');
+ this.callParent([res]);
+ },
+});
+Ext.define('PVE.form.BackupModeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveBackupModeSelector'],
+ comboItems: [
+ ['snapshot', gettext('Snapshot')],
+ ['suspend', gettext('Suspend')],
+ ['stop', gettext('Stop')],
+ ],
+});
+Ext.define('PVE.form.SizeField', {
+ extend: 'Ext.form.FieldContainer',
+ alias: 'widget.pveSizeField',
+
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ viewModel: {
+ data: {
+ unit: 'MiB',
+ unitPostfix: '',
+ },
+ formulas: {
+ unitlabel: (get) => get('unit') + get('unitPostfix'),
+ },
+ },
+
+ emptyText: '',
+
+ layout: 'hbox',
+ defaults: {
+ hideLabel: true,
+ },
+
+ units: {
+ 'B': 1,
+ 'KiB': 1024,
+ 'MiB': 1024*1024,
+ 'GiB': 1024*1024*1024,
+ 'TiB': 1024*1024*1024*1024,
+ 'KB': 1000,
+ 'MB': 1000*1000,
+ 'GB': 1000*1000*1000,
+ 'TB': 1000*1000*1000*1000,
+ },
+
+ // display unit (TODO: make (optionally) selectable)
+ unit: 'MiB',
+ unitPostfix: '',
+
+ // use this if the backend saves values in another unit tha bytes, e.g.,
+ // for KiB set it to 'KiB'
+ backendUnit: undefined,
+
+ // allow setting 0 and using it as a submit value
+ allowZero: false,
+
+ emptyValue: null,
+
+ items: [
+ {
+ xtype: 'numberfield',
+ cbind: {
+ name: '{name}',
+ emptyText: '{emptyText}',
+ allowZero: '{allowZero}',
+ emptyValue: '{emptyValue}',
+ },
+ minValue: 0,
+ step: 1,
+ submitLocaleSeparator: false,
+ fieldStyle: 'text-align: right',
+ flex: 1,
+ enableKeyEvents: true,
+ setValue: function(v) {
+ if (!this._transformed) {
+ let fieldContainer = this.up('fieldcontainer');
+ let vm = fieldContainer.getViewModel();
+ let unit = vm.get('unit');
+
+ v /= fieldContainer.units[unit];
+ v *= fieldContainer.backendFactor;
+
+ this._transformed = true;
+ }
+
+ if (Number(v) === 0 && !this.allowZero) {
+ v = undefined;
+ }
+
+ return Ext.form.field.Text.prototype.setValue.call(this, v);
+ },
+ getSubmitValue: function() {
+ let v = this.processRawValue(this.getRawValue());
+ v = v.replace(this.decimalSeparator, '.');
+
+ if (v === undefined || v === '') {
+ return this.emptyValue;
+ }
+
+ if (Number(v) === 0) {
+ return this.allowZero ? 0 : null;
+ }
+
+ let fieldContainer = this.up('fieldcontainer');
+ let vm = fieldContainer.getViewModel();
+ let unit = vm.get('unit');
+
+ v = parseFloat(v) * fieldContainer.units[unit];
+ v /= fieldContainer.backendFactor;
+
+ return String(Math.floor(v));
+ },
+ listeners: {
+ // our setValue gets only called if we have a value, avoid
+ // transformation of the first user-entered value
+ keydown: function() { this._transformed = true; },
+ },
+ },
+ {
+ xtype: 'displayfield',
+ name: 'unit',
+ submitValue: false,
+ padding: '0 0 0 10',
+ bind: {
+ value: '{unitlabel}',
+ },
+ listeners: {
+ change: (f, v) => {
+ f.originalValue = v;
+ },
+ },
+ width: 40,
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ me.unit = me.unit || 'MiB';
+ if (!(me.unit in me.units)) {
+ throw "unknown unit: " + me.unit;
+ }
+
+ me.backendFactor = 1;
+ if (me.backendUnit !== undefined) {
+ if (!(me.unit in me.units)) {
+ throw "unknown backend unit: " + me.backendUnit;
+ }
+ me.backendFactor = me.units[me.backendUnit];
+ }
+
+ me.callParent(arguments);
+
+ me.getViewModel().set('unit', me.unit);
+ me.getViewModel().set('unitPostfix', me.unitPostfix);
+ },
+});
+
+Ext.define('PVE.form.BandwidthField', {
+ extend: 'PVE.form.SizeField',
+ alias: 'widget.pveBandwidthField',
+
+ unitPostfix: '/s',
+});
+Ext.define('PVE.form.BridgeSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.PVE.form.BridgeSelector'],
+
+ bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge
+
+ store: {
+ fields: ['iface', 'active', 'type'],
+ filterOnLoad: true,
+ sorters: [
+ {
+ property: 'iface',
+ direction: 'ASC',
+ },
+ ],
+ },
+ valueField: 'iface',
+ displayField: 'iface',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Bridge'),
+ dataIndex: 'iface',
+ hideable: false,
+ width: 100,
+ },
+ {
+ header: gettext('Active'),
+ width: 60,
+ dataIndex: 'active',
+ renderer: Proxmox.Utils.format_boolean,
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comments',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || me.nodename === nodename) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/network?type=' +
+ me.bridgeType,
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ me.nodename = undefined;
+
+ me.callParent();
+
+ me.setNodename(nodename);
+ },
+});
+
+Ext.define('PVE.form.BusTypeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: 'widget.pveBusSelector',
+
+ withVirtIO: true,
+ withUnused: false,
+
+ initComponent: function() {
+ var me = this;
+
+ me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']];
+
+ if (me.withVirtIO) {
+ me.comboItems.push(['virtio', 'VirtIO Block']);
+ }
+
+ me.comboItems.push(['scsi', 'SCSI']);
+
+ if (me.withUnused) {
+ me.comboItems.push(['unused', 'Unused']);
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.data.CPUModel', {
+ extend: 'Ext.data.Model',
+ fields: [
+ { name: 'name' },
+ { name: 'vendor' },
+ { name: 'custom' },
+ { name: 'displayname' },
+ ],
+});
+
+Ext.define('PVE.form.CPUModelSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.CPUModelSelector'],
+
+ valueField: 'name',
+ displayField: 'displayname',
+
+ emptyText: Proxmox.Utils.defaultText + ' (kvm64)',
+ allowBlank: true,
+
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+ autoSelect: false,
+
+ deleteEmpty: true,
+
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Model'),
+ dataIndex: 'displayname',
+ hideable: false,
+ sortable: true,
+ flex: 3,
+ },
+ {
+ header: gettext('Vendor'),
+ dataIndex: 'vendor',
+ hideable: false,
+ sortable: true,
+ flex: 2,
+ },
+ ],
+ width: 360,
+ },
+
+ store: {
+ autoLoad: true,
+ model: 'PVE.data.CPUModel',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/localhost/capabilities/qemu/cpu',
+ },
+ sorters: [
+ {
+ sorterFn: function(recordA, recordB) {
+ let a = recordA.data;
+ let b = recordB.data;
+
+ let vendorOrder = PVE.Utils.cpu_vendor_order;
+ let orderA = vendorOrder[a.vendor] || vendorOrder._default_;
+ let orderB = vendorOrder[b.vendor] || vendorOrder._default_;
+
+ if (orderA > orderB) {
+ return 1;
+ } else if (orderA < orderB) {
+ return -1;
+ }
+
+ // Within same vendor, sort alphabetically
+ return a.name.localeCompare(b.name);
+ },
+ direction: 'ASC',
+ },
+ ],
+ listeners: {
+ load: function(store, records, success) {
+ if (success) {
+ records.forEach(rec => {
+ rec.data.displayname = rec.data.name.replace(/^custom-/, '');
+
+ let vendor = rec.data.vendor;
+
+ if (rec.data.name === 'host') {
+ vendor = 'Host';
+ }
+
+ // We receive vendor names as given to QEMU as CPUID
+ vendor = PVE.Utils.cpu_vendor_map[vendor] || vendor;
+
+ if (rec.data.custom) {
+ vendor = gettext('Custom') + ` (${vendor})`;
+ }
+
+ rec.data.vendor = vendor;
+ });
+
+ store.sort();
+ }
+ },
+ },
+ },
+});
+Ext.define('PVE.form.CacheTypeSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.CacheTypeSelector'],
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + " (" + gettext('No cache') + ")"],
+ ['directsync', 'Direct sync'],
+ ['writethrough', 'Write through'],
+ ['writeback', 'Write back'],
+ ['unsafe', 'Write back (' + gettext('unsafe') + ')'],
+ ['none', gettext('No cache')],
+ ],
+});
+Ext.define('PVE.form.CalendarEvent', {
+ extend: 'Ext.form.field.ComboBox',
+ xtype: 'pveCalendarEvent',
+
+ editable: true,
+ emptyText: gettext('Editable'), // FIXME: better way to convey that to confused users?
+
+ valueField: 'value',
+ queryMode: 'local',
+
+ matchFieldWidth: false,
+ listConfig: {
+ maxWidth: 450,
+ },
+
+ store: {
+ field: ['value', 'text'],
+ data: [
+ { value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) },
+ { value: '*/2:00', text: gettext("Every two hours") },
+ { value: '21:00', text: gettext("Every day") + " 21:00" },
+ { value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30" },
+ { value: 'mon..fri 00:00', text: gettext("Monday to Friday") + " 00:00" },
+ { value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly") },
+ {
+ value: 'mon..fri 7..18:00/15',
+ text: gettext("Monday to Friday") + ', '
+ + Ext.String.format(gettext('{0} to {1}'), '07:00', '18:45') + ': '
+ + Ext.String.format(gettext("Every {0} minutes"), 15),
+ },
+ { value: 'sun 01:00', text: gettext("Sunday") + " 01:00" },
+ { value: 'monthly', text: gettext("Every first day of the Month") + " 00:00" },
+ { value: 'sat *-1..7 15:00', text: gettext("First Saturday each month") + " 15:00" },
+ { value: 'yearly', text: gettext("First day of the year") + " 00:00" },
+ ],
+ },
+
+ tpl: [
+ '',
+ ],
+
+ displayTpl: [
+ '',
+ '{value}',
+ '',
+ ],
+
+});
+Ext.define('PVE.form.CephPoolSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveCephPoolSelector',
+
+ allowBlank: false,
+ valueField: 'pool_name',
+ displayField: 'pool_name',
+ editable: false,
+ queryMode: 'local',
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no nodename given";
+ }
+
+ let onlyRBDPools = ({ data }) =>
+ !data?.application_metadata || !!data?.application_metadata?.rbd;
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: ['name'],
+ sorters: 'name',
+ filters: [
+ onlyRBDPools,
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/ceph/pool',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+
+ store.load({
+ callback: function(rec, op, success) {
+ let filteredRec = rec.filter(onlyRBDPools);
+
+ if (success && filteredRec.length > 0) {
+ me.select(filteredRec[0]);
+ }
+ },
+ });
+ },
+
+});
+Ext.define('PVE.form.CephFSSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveCephFSSelector',
+
+ allowBlank: false,
+ valueField: 'name',
+ displayField: 'name',
+ editable: false,
+ queryMode: 'local',
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no nodename given";
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: ['name'],
+ sorters: 'name',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/ceph/fs',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+
+ store.load({
+ callback: function(rec, op, success) {
+ if (success && rec.length > 0) {
+ me.select(rec[0]);
+ }
+ },
+ });
+ },
+
+});
+Ext.define('PVE.form.ComboBoxSetStoreNode', {
+ extend: 'Proxmox.form.ComboGrid',
+ config: {
+ apiBaseUrl: '/api2/json/nodes/',
+ apiSuffix: '',
+ },
+
+ showNodeSelector: false,
+
+ setNodeName: function(value) {
+ let me = this;
+ value ||= Proxmox.NodeName;
+
+ me.getStore().getProxy().setUrl(`${me.apiBaseUrl}${value}${me.apiSuffix}`);
+ me.clearValue();
+ },
+
+ nodeChange: function(_field, value) {
+ let me = this;
+ // disable autoSelect if there is already a selection or we have the picker open
+ if (me.getValue() || me.isExpanded) {
+ let autoSelect = me.autoSelect;
+ me.autoSelect = false;
+ me.store.on('afterload', function() {
+ me.autoSelect = autoSelect;
+ }, { single: true });
+ }
+ me.setNodeName(value);
+ me.fireEvent('nodechanged', value);
+ },
+
+ tbarMouseDown: function() {
+ this.topBarMousePress = true;
+ },
+
+ tbarMouseUp: function() {
+ let me = this;
+ delete this.topBarMousePress;
+ if (me.focusLeft) {
+ me.focus();
+ delete me.focusLeft;
+ }
+ },
+
+ // conditionally prevent the focusLeave handler to continue, preventing collapsing of the picker
+ onFocusLeave: function() {
+ let me = this;
+ me.focusLeft = true;
+ if (!me.topBarMousePress) {
+ me.callParent(arguments);
+ }
+
+ return undefined;
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (me.showNodeSelector && PVE.data.ResourceStore.getNodes().length > 1) {
+ me.errorHeight = 140;
+ Ext.apply(me.listConfig ?? {}, {
+ tbar: {
+ xtype: 'toolbar',
+ minHeight: 40,
+ listeners: {
+ mousedown: me.tbarMouseDown,
+ mouseup: me.tbarMouseUp,
+ element: 'el',
+ scope: me,
+ },
+ items: [
+ {
+ xtype: "pveStorageScanNodeSelector",
+ autoSelect: false,
+ fieldLabel: gettext('Node to scan'),
+ listeners: {
+ change: (field, value) => me.nodeChange(field, value),
+ },
+ },
+ ],
+ },
+ emptyText: me.listConfig?.emptyText ?? gettext('Nothing found'),
+ });
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.form.CompressionSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveCompressionSelector'],
+ comboItems: [
+ ['0', Proxmox.Utils.noneText],
+ ['lzo', 'LZO (' + gettext('fast') + ')'],
+ ['gzip', 'GZIP (' + gettext('good') + ')'],
+ ['zstd', 'ZSTD (' + gettext('fast and good') + ')'],
+ ],
+});
+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.ControllerSelector', {
+ extend: 'Ext.form.FieldContainer',
+ alias: 'widget.pveControllerSelector',
+
+ withVirtIO: true,
+ withUnused: false,
+
+ vmconfig: {}, // used to check for existing devices
+
+ setToFree: function(controllers, busField, deviceIDField) {
+ let me = this;
+ let freeId = PVE.Utils.nextFreeDisk(controllers, me.vmconfig);
+
+ if (freeId !== undefined) {
+ busField?.setValue(freeId.controller);
+ deviceIDField.setValue(freeId.id);
+ }
+ },
+
+ updateVMConfig: function(vmconfig) {
+ let me = this;
+ me.vmconfig = Ext.apply({}, vmconfig);
+
+ me.down('field[name=deviceid]').validate();
+ },
+
+ setVMConfig: function(vmconfig, autoSelect) {
+ let me = this;
+
+ me.vmconfig = Ext.apply({}, vmconfig);
+
+ let bussel = me.down('field[name=controller]');
+ let deviceid = me.down('field[name=deviceid]');
+
+ let clist;
+ if (autoSelect === 'cdrom') {
+ if (!Ext.isDefined(me.vmconfig.ide2)) {
+ bussel.setValue('ide');
+ deviceid.setValue(2);
+ return;
+ }
+ clist = ['ide', 'scsi', 'sata'];
+ } else {
+ // in most cases we want to add a disk to the same controller we previously used
+ clist = PVE.Utils.sortByPreviousUsage(me.vmconfig);
+ }
+
+ me.setToFree(clist, bussel, deviceid);
+
+ deviceid.validate();
+ },
+
+ getConfId: function() {
+ let me = this;
+ let controller = me.getComponent('controller').getValue() || 'ide';
+ let id = me.getComponent('deviceid').getValue() || 0;
+
+ return `${controller}${id}`;
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ Ext.apply(me, {
+ fieldLabel: gettext('Bus/Device'),
+ layout: 'hbox',
+ defaults: {
+ hideLabel: true,
+ },
+ items: [
+ {
+ xtype: 'pveBusSelector',
+ name: 'controller',
+ itemId: 'controller',
+ value: PVE.qemu.OSDefaults.generic.busType,
+ withVirtIO: me.withVirtIO,
+ withUnused: me.withUnused,
+ allowBlank: false,
+ flex: 2,
+ listeners: {
+ change: function(t, value) {
+ if (!value) {
+ return;
+ }
+ let field = me.down('field[name=deviceid]');
+ me.setToFree([value], undefined, field);
+ field.setMaxValue(PVE.Utils.diskControllerMaxIDs[value] - 1);
+ field.validate();
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'deviceid',
+ itemId: 'deviceid',
+ minValue: 0,
+ maxValue: PVE.Utils.diskControllerMaxIDs.ide - 1,
+ value: '0',
+ flex: 1,
+ allowBlank: false,
+ validator: function(value) {
+ if (!me.rendered) {
+ return undefined;
+ }
+ let controller = me.down('field[name=controller]').getValue();
+ let confid = controller + value;
+ if (Ext.isDefined(me.vmconfig[confid])) {
+ return "This device is already in use.";
+ }
+ return true;
+ },
+ },
+ ],
+ });
+
+ me.callParent();
+
+ if (me.selectFree) {
+ me.setVMConfig(me.vmconfig);
+ }
+ },
+});
+Ext.define('PVE.form.DayOfWeekSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveDayOfWeekSelector'],
+ comboItems: [],
+ initComponent: function() {
+ var me = this;
+ me.comboItems = [
+ ['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])],
+ ['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])],
+ ['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])],
+ ['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])],
+ ['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])],
+ ['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])],
+ ['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])],
+ ];
+ this.callParent();
+ },
+});
+Ext.define('PVE.form.DiskFormatSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: 'widget.pveDiskFormatSelector',
+ comboItems: [
+ ['raw', gettext('Raw disk image') + ' (raw)'],
+ ['qcow2', gettext('QEMU image format') + ' (qcow2)'],
+ ['vmdk', gettext('VMware image format') + ' (vmdk)'],
+ ],
+});
+Ext.define('PVE.form.DiskStorageSelector', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveDiskStorageSelector',
+
+ layout: 'fit',
+ defaults: {
+ margin: '0 0 5 0',
+ },
+
+ // the fieldLabel for the storageselector
+ storageLabel: gettext('Storage'),
+
+ // the content to show (e.g., images or rootdir)
+ storageContent: undefined,
+
+ // if true, selects the first available storage
+ autoSelect: false,
+
+ allowBlank: false,
+ emptyText: '',
+
+ // hides the selection field
+ // this is always hidden on creation,
+ // and only shown when the storage needs a selection and
+ // hideSelection is not true
+ hideSelection: undefined,
+
+ // hides the size field (e.g, for the efi disk dialog)
+ hideSize: false,
+
+ // hides the format field (e.g. for TPM state)
+ hideFormat: false,
+
+ // sets the initial size value
+ // string because else we get a type confusion
+ defaultSize: '32',
+
+ changeStorage: function(f, value) {
+ var me = this;
+ var formatsel = me.getComponent('diskformat');
+ var hdfilesel = me.getComponent('hdimage');
+ var hdsizesel = me.getComponent('disksize');
+
+ // initial store load, and reset/deletion of the storage
+ if (!value) {
+ hdfilesel.setDisabled(true);
+ hdfilesel.setVisible(false);
+
+ formatsel.setDisabled(true);
+ return;
+ }
+
+ var rec = f.store.getById(value);
+ // if the storage is not defined, or valid,
+ // we cannot know what to enable/disable
+ if (!rec) {
+ return;
+ }
+
+ let validFormats = {};
+ let selectFormat = 'raw';
+ if (rec.data.format) {
+ validFormats = rec.data.format[0]; // 0 is the formats, 1 the default in the backend
+ delete validFormats.subvol; // we never need subvol in the gui
+ if (validFormats.qcow2) {
+ selectFormat = 'qcow2';
+ } else if (validFormats.raw) {
+ selectFormat = 'raw';
+ } else {
+ selectFormat = rec.data.format[1];
+ }
+ }
+
+ var select = !!rec.data.select_existing && !me.hideSelection;
+
+ formatsel.setDisabled(me.hideFormat || Ext.Object.getSize(validFormats) <= 1);
+ formatsel.setValue(selectFormat);
+
+ hdfilesel.setDisabled(!select);
+ hdfilesel.setVisible(select);
+ if (select) {
+ hdfilesel.setStorage(value);
+ }
+
+ hdsizesel.setDisabled(select || me.hideSize);
+ hdsizesel.setVisible(!select && !me.hideSize);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ var hdstorage = me.getComponent('hdstorage');
+ var hdfilesel = me.getComponent('hdimage');
+
+ hdstorage.setNodename(nodename);
+ hdfilesel.setNodename(nodename);
+ },
+
+ setDisabled: function(value) {
+ var me = this;
+ var hdstorage = me.getComponent('hdstorage');
+
+ // reset on disable
+ if (value) {
+ hdstorage.setValue();
+ }
+ hdstorage.setDisabled(value);
+
+ // disabling does not always fire this event and we do not need
+ // the value of the validity
+ hdstorage.fireEvent('validitychange');
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: 'pveStorageSelector',
+ itemId: 'hdstorage',
+ name: 'hdstorage',
+ 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.hideFormat || me.storageContent === 'rootdir',
+ value: 'qcow2',
+ allowBlank: false,
+ },
+ ];
+
+ // use it to disable the children but not ourself
+ me.disabled = false;
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.form.EmailNotificationSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveEmailNotificationSelector'],
+ comboItems: [
+ ['always', gettext('Notify always')],
+ ['failure', gettext('On failure only')],
+ ],
+});
+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.FirewallPolicySelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveFirewallPolicySelector'],
+ comboItems: [
+ ['ACCEPT', 'ACCEPT'],
+ ['REJECT', 'REJECT'],
+ ['DROP', 'DROP'],
+ ],
+});
+/*
+ * This is a global search field it loads the /cluster/resources on focus and displays the
+ * result in a floating grid. Filtering and sorting is done in the customFilter function
+ *
+ * Accepts key up/down and enter for input, and it opens to CTRL+SHIFT+F and CTRL+SPACE
+ */
+Ext.define('PVE.form.GlobalSearchField', {
+ extend: 'Ext.form.field.Text',
+ alias: 'widget.pveGlobalSearchField',
+
+ emptyText: gettext('Search'),
+ enableKeyEvents: true,
+ selectOnFocus: true,
+ padding: '0 5 0 5',
+
+ grid: {
+ xtype: 'gridpanel',
+ userCls: 'proxmox-tags-full',
+ focusOnToFront: false,
+ floating: true,
+ emptyText: Proxmox.Utils.noneText,
+ width: 600,
+ height: 400,
+ scrollable: {
+ xtype: 'scroller',
+ y: true,
+ x: true,
+ },
+ store: {
+ model: 'PVEResources',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/extjs/cluster/resources',
+ },
+ },
+ plugins: {
+ ptype: 'bufferedrenderer',
+ trailingBufferZone: 20,
+ leadingBufferZone: 20,
+ },
+
+ hideMe: function() {
+ var me = this;
+ if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
+ return;
+ }
+ me.hasFocus = false;
+ if (!me.textfield.hasFocus) {
+ me.hide();
+ }
+ },
+
+ setFocus: function() {
+ var me = this;
+ me.hasFocus = true;
+ },
+
+ listeners: {
+ rowclick: function(grid, record) {
+ var me = this;
+ me.textfield.selectAndHide(record.id);
+ },
+ itemcontextmenu: function(v, record, item, index, event) {
+ var me = this;
+ me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
+ },
+ focusleave: 'hideMe',
+ focusenter: 'setFocus',
+ },
+
+ columns: [
+ {
+ text: gettext('Type'),
+ dataIndex: 'type',
+ width: 100,
+ renderer: PVE.Utils.render_resource_type,
+ },
+ {
+ text: gettext('Description'),
+ flex: 1,
+ dataIndex: 'text',
+ renderer: function(value, mD, rec) {
+ let overrides = PVE.UIOptions.tagOverrides;
+ let tags = PVE.Utils.renderTags(rec.data.tags, overrides);
+ return `${value}${tags}`;
+ },
+ },
+ {
+ text: gettext('Node'),
+ dataIndex: 'node',
+ },
+ {
+ text: gettext('Pool'),
+ dataIndex: 'pool',
+ },
+ ],
+ },
+
+ customFilter: function(item) {
+ let me = this;
+
+ if (me.filterVal === '') {
+ item.data.relevance = 0;
+ return true;
+ }
+ // different types have different fields to search, e.g., a node will never have a pool
+ const fieldMap = {
+ 'pool': ['type', 'pool', 'text'],
+ 'node': ['type', 'node', 'text'],
+ 'storage': ['type', 'pool', 'node', 'storage'],
+ 'default': ['name', 'type', 'node', 'pool', 'vmid'],
+ };
+ let fields = fieldMap[item.data.type] || fieldMap.default;
+ let fieldArr = fields.map(field => item.data[field]?.toString().toLowerCase());
+ if (item.data.tags) {
+ let tags = item.data.tags.split(/[;, ]/);
+ fieldArr.push(...tags);
+ }
+
+ let filterWords = me.filterVal.split(/\s+/);
+
+ // all text is case insensitive and each split-out word is searched for separately.
+ // a row gets 1 point for every partial match, and and additional point for every exact match
+ let match = 0;
+ for (let fieldValue of fieldArr) {
+ if (fieldValue === undefined || fieldValue === "") {
+ continue;
+ }
+ for (let filterWord of filterWords) {
+ if (fieldValue.indexOf(filterWord) !== -1) {
+ match++; // partial match
+ if (fieldValue === filterWord) {
+ match++; // exact match is worth more
+ }
+ }
+ }
+ }
+ item.data.relevance = match; // set the row's virtual 'relevance' value for ordering
+ return match > 0;
+ },
+
+ updateFilter: function(field, newValue, oldValue) {
+ let me = this;
+ // parse input and filter store, show grid
+ me.grid.store.filterVal = newValue.toLowerCase().trim();
+ me.grid.store.clearFilter(true);
+ me.grid.store.filterBy(me.customFilter);
+ me.grid.getSelectionModel().select(0);
+ },
+
+ selectAndHide: function(id) {
+ var me = this;
+ me.tree.selectById(id);
+ me.grid.hide();
+ me.setValue('');
+ me.blur();
+ },
+
+ onKey: function(field, e) {
+ var me = this;
+ var key = e.getKey();
+
+ switch (key) {
+ case Ext.event.Event.ENTER:
+ // go to first entry if there is one
+ if (me.grid.store.getCount() > 0) {
+ me.selectAndHide(me.grid.getSelection()[0].data.id);
+ }
+ break;
+ case Ext.event.Event.UP:
+ me.grid.getSelectionModel().selectPrevious();
+ break;
+ case Ext.event.Event.DOWN:
+ me.grid.getSelectionModel().selectNext();
+ break;
+ case Ext.event.Event.ESC:
+ me.grid.hide();
+ me.blur();
+ break;
+ }
+ },
+
+ loadValues: function(field) {
+ let me = this;
+ me.hasFocus = true;
+ me.grid.textfield = me;
+ me.grid.store.load();
+ me.grid.showBy(me, 'tl-bl');
+ },
+
+ hideGrid: function() {
+ let me = this;
+ me.hasFocus = false;
+ if (!me.grid.hasFocus) {
+ me.grid.hide();
+ }
+ },
+
+ listeners: {
+ change: {
+ fn: 'updateFilter',
+ buffer: 250,
+ },
+ specialkey: 'onKey',
+ focusenter: 'loadValues',
+ focusleave: {
+ fn: 'hideGrid',
+ delay: 100,
+ },
+ },
+
+ toggleFocus: function() {
+ let me = this;
+ if (!me.hasFocus) {
+ me.focus();
+ } else {
+ me.blur();
+ }
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.tree) {
+ throw "no tree given";
+ }
+
+ me.grid = Ext.create(me.grid);
+
+ me.callParent();
+
+ // bind CTRL + SHIFT + F and CTRL + SPACE to open/close the search
+ me.keymap = new Ext.KeyMap({
+ target: Ext.get(document),
+ binding: [{
+ key: 'F',
+ ctrl: true,
+ shift: true,
+ fn: me.toggleFocus,
+ scope: me,
+ }, {
+ key: ' ',
+ ctrl: true,
+ fn: me.toggleFocus,
+ scope: me,
+ }],
+ });
+
+ // always select first item and sort by relevance after load
+ me.mon(me.grid.store, 'load', function() {
+ me.grid.getSelectionModel().select(0);
+ me.grid.store.sort({
+ property: 'relevance',
+ direction: 'DESC',
+ });
+ });
+ },
+});
+Ext.define('pve-groups', {
+ extend: 'Ext.data.Model',
+ fields: ['groupid', 'comment', 'users'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/groups",
+ },
+ idProperty: 'groupid',
+});
+
+Ext.define('PVE.form.GroupSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pveGroupSelector',
+
+ allowBlank: false,
+ autoSelect: false,
+ valueField: 'groupid',
+ displayField: 'groupid',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Group'),
+ sortable: true,
+ dataIndex: 'groupid',
+ flex: 1,
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ {
+ header: gettext('Users'),
+ sortable: false,
+ dataIndex: 'users',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-groups',
+ sorters: [{
+ property: 'groupid',
+ }],
+ });
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+
+ store.load();
+ },
+});
+Ext.define('PVE.form.GuestIDSelector', {
+ extend: 'Ext.form.field.Number',
+ alias: 'widget.pveGuestIDSelector',
+
+ allowBlank: false,
+
+ minValue: 100,
+
+ maxValue: 999999999,
+
+ validateExists: undefined,
+
+ loadNextFreeID: false,
+
+ guestType: undefined,
+
+ validator: function(value) {
+ var me = this;
+
+ if (!Ext.isNumeric(value) ||
+ value < me.minValue ||
+ value > me.maxValue) {
+ // check is done by ExtJS
+ return true;
+ }
+
+ if (me.validateExists === true && !me.exists) {
+ return me.unknownID;
+ }
+
+ if (me.validateExists === false && me.exists) {
+ return me.inUseID;
+ }
+
+ return true;
+ },
+
+ initComponent: function() {
+ var me = this;
+ var label = '{0} ID';
+ var unknownID = gettext('This {0} ID does not exist');
+ var inUseID = gettext('This {0} ID is already in use');
+ var type = 'CT/VM';
+
+ if (me.guestType === 'lxc') {
+ type = 'CT';
+ } else if (me.guestType === 'qemu') {
+ type = 'VM';
+ }
+
+ me.label = Ext.String.format(label, type);
+ me.unknownID = Ext.String.format(unknownID, type);
+ me.inUseID = Ext.String.format(inUseID, type);
+
+ Ext.apply(me, {
+ fieldLabel: me.label,
+ listeners: {
+ 'change': function(field, newValue, oldValue) {
+ if (!Ext.isDefined(me.validateExists)) {
+ return;
+ }
+ Proxmox.Utils.API2Request({
+ params: { vmid: newValue },
+ url: '/cluster/nextid',
+ method: 'GET',
+ success: function(response, opts) {
+ me.exists = false;
+ me.validate();
+ },
+ failure: function(response, opts) {
+ me.exists = true;
+ me.validate();
+ },
+ });
+ },
+ },
+ });
+
+ me.callParent();
+
+ if (me.loadNextFreeID) {
+ Proxmox.Utils.API2Request({
+ url: '/cluster/nextid',
+ method: 'GET',
+ success: function(response, opts) {
+ me.setRawValue(response.result.data);
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.form.hashAlgorithmSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveHashAlgorithmSelector'],
+ config: {
+ deleteEmpty: false,
+ },
+ comboItems: [
+ ['__default__', 'None'],
+ ['md5', 'MD5'],
+ ['sha1', 'SHA-1'],
+ ['sha224', 'SHA-224'],
+ ['sha256', 'SHA-256'],
+ ['sha384', 'SHA-384'],
+ ['sha512', 'SHA-512'],
+ ],
+});
+Ext.define('PVE.form.HotplugFeatureSelector', {
+ extend: 'Ext.form.CheckboxGroup',
+ alias: 'widget.pveHotplugFeatureSelector',
+
+ columns: 1,
+ vertical: true,
+
+ defaults: {
+ name: 'hotplugCbGroup',
+ submitValue: false,
+ },
+ items: [
+ {
+ boxLabel: gettext('Disk'),
+ inputValue: 'disk',
+ checked: true,
+ },
+ {
+ boxLabel: gettext('Network'),
+ inputValue: 'network',
+ checked: true,
+ },
+ {
+ boxLabel: 'USB',
+ inputValue: 'usb',
+ checked: true,
+ },
+ {
+ boxLabel: gettext('Memory'),
+ inputValue: 'memory',
+ },
+ {
+ boxLabel: gettext('CPU'),
+ inputValue: 'cpu',
+ },
+ ],
+
+ setValue: function(value) {
+ var me = this;
+ var newVal = [];
+ if (value === '1') {
+ newVal = ['disk', 'network', 'usb'];
+ } else if (value !== '0') {
+ newVal = value.split(',');
+ }
+ me.callParent([{ hotplugCbGroup: newVal }]);
+ },
+
+ // override framework function to
+ // assemble the hotplug value
+ getSubmitData: function() {
+ var me = this,
+ boxes = me.getBoxes(),
+ data = [];
+ Ext.Array.forEach(boxes, function(box) {
+ if (box.getValue()) {
+ data.push(box.inputValue);
+ }
+ });
+
+ /* because above is hotplug an array */
+ if (data.length === 0) {
+ return { 'hotplug': '0' };
+ } else {
+ return { 'hotplug': data.join(',') };
+ }
+ },
+
+});
+Ext.define('PVE.form.IPProtocolSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveIPProtocolSelector'],
+ valueField: 'p',
+ displayField: 'p',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Protocol'),
+ dataIndex: 'p',
+ hideable: false,
+ sortable: false,
+ width: 100,
+ },
+ {
+ header: gettext('Number'),
+ dataIndex: 'n',
+ hideable: false,
+ sortable: false,
+ width: 50,
+ },
+ {
+ header: gettext('Description'),
+ dataIndex: 'd',
+ hideable: false,
+ sortable: false,
+ flex: 1,
+ },
+ ],
+ },
+ store: {
+ fields: ['p', 'd', 'n'],
+ data: [
+ { p: 'tcp', n: 6, d: 'Transmission Control Protocol' },
+ { p: 'udp', n: 17, d: 'User Datagram Protocol' },
+ { p: 'icmp', n: 1, d: 'Internet Control Message Protocol' },
+ { p: 'igmp', n: 2, d: 'Internet Group Management' },
+ { p: 'ggp', n: 3, d: 'gateway-gateway protocol' },
+ { p: 'ipencap', n: 4, d: 'IP encapsulated in IP' },
+ { p: 'st', n: 5, d: 'ST datagram mode' },
+ { p: 'egp', n: 8, d: 'exterior gateway protocol' },
+ { p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' },
+ { p: 'pup', n: 12, d: 'PARC universal packet protocol' },
+ { p: 'hmp', n: 20, d: 'host monitoring protocol' },
+ { p: 'xns-idp', n: 22, d: 'Xerox NS IDP' },
+ { p: 'rdp', n: 27, d: '"reliable datagram" protocol' },
+ { p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' },
+ { p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' },
+ { p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' },
+ { p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' },
+ { p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' },
+ { p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' },
+ { p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' },
+ { p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' },
+ { p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' },
+ { p: 'rsvp', n: 46, d: 'Reservation Protocol' },
+ { p: 'gre', n: 47, d: 'General Routing Encapsulation' },
+ { p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' },
+ { p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' },
+ { p: 'skip', n: 57, d: 'SKIP' },
+ { p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' },
+ { p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' },
+ { p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' },
+ { p: 'vmtp', n: 81, d: 'Versatile Message Transport' },
+ { p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' },
+ { p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' },
+ { p: 'ax.25', n: 93, d: 'AX.25 frames' },
+ { p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' },
+ { p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' },
+ { p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' },
+ { p: 'pim', n: 103, d: 'Protocol Independent Multicast' },
+ { p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' },
+ { p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' },
+ { p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' },
+ { p: 'isis', n: 124, d: 'IS-IS over IPv4' },
+ { p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' },
+ { p: 'fc', n: 133, d: 'Fibre Channel' },
+ { p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' },
+ { p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' },
+ { p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' },
+ { p: 'hip', n: 139, d: 'Host Identity Protocol' },
+ { p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' },
+ { p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' },
+ { p: 'rohc', n: 142, d: 'Robust Header Compression' },
+ ],
+ },
+});
+Ext.define('PVE.form.IPRefSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveIPRefSelector'],
+
+ base_url: undefined,
+
+ preferredValue: '', // hack: else Form sets dirty flag?
+
+ ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias']
+
+ valueField: 'ref',
+ displayField: 'ref',
+ notFoundIsValid: true,
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.base_url) {
+ throw "no base_url specified";
+ }
+
+ var url = "/api2/json" + me.base_url;
+ if (me.ref_type) {
+ url += "?type=" + me.ref_type;
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ fields: ['type', 'name', 'ref', 'comment'],
+ idProperty: 'ref',
+ proxy: {
+ type: 'proxmox',
+ url: url,
+ },
+ sorters: {
+ property: 'ref',
+ direction: 'ASC',
+ },
+ });
+
+ var disable_query_for_ips = function(f, value) {
+ if (value === null ||
+ value.match(/^\d/)) { // IP address starts with \d
+ f.queryDelay = 9999999999; // hack: disable 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.MDevSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ xtype: 'pveMDevSelector',
+
+ store: {
+ fields: ['type', 'available', 'description'],
+ filterOnLoad: true,
+ sorters: [
+ {
+ property: 'type',
+ direction: 'ASC',
+ },
+ ],
+ },
+ autoSelect: false,
+ valueField: 'type',
+ displayField: 'type',
+ listConfig: {
+ width: 550,
+ columns: [
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ renderer: function(value, md, rec) {
+ if (rec.data.name !== undefined) {
+ return `${rec.data.name} (${value})`;
+ }
+ return value;
+ },
+ flex: 1,
+ },
+ {
+ header: gettext('Avail'),
+ dataIndex: 'available',
+ width: 60,
+ },
+ {
+ header: gettext('Description'),
+ dataIndex: 'description',
+ flex: 1,
+ cellWrap: true,
+ renderer: function(value) {
+ if (!value) {
+ return '';
+ }
+
+ return value.split('\n').join('
');
+ },
+ },
+ ],
+ },
+
+ 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.MemoryField', {
+ extend: 'Ext.form.field.Number',
+ alias: 'widget.pveMemoryField',
+
+ allowBlank: false,
+
+ hotplug: false,
+
+ minValue: 32,
+
+ maxValue: 4178944,
+
+ step: 32,
+
+ value: '512', // qm backend default
+
+ allowDecimals: false,
+
+ allowExponential: false,
+
+ computeUpDown: function(value) {
+ var me = this;
+
+ if (!me.hotplug) {
+ return { up: value + me.step, down: value - me.step };
+ }
+
+ var dimm_size = 512;
+ var prev_dimm_size = 0;
+ var min_size = 1024;
+ var current_size = min_size;
+ var value_up = min_size;
+ var value_down = min_size;
+ var value_start = min_size;
+
+ var i, j;
+ for (j = 0; j < 9; j++) {
+ for (i = 0; i < 32; i++) {
+ if (value >= current_size && value < current_size + dimm_size) {
+ value_start = current_size;
+ value_up = current_size + dimm_size;
+ value_down = current_size - (i === 0 ? prev_dimm_size : dimm_size);
+ }
+ current_size += dimm_size;
+ }
+ prev_dimm_size = dimm_size;
+ dimm_size = dimm_size*2;
+ }
+
+ return { up: value_up, down: value_down, start: value_start };
+ },
+
+ onSpinUp: function() {
+ var me = this;
+ if (!me.readOnly) {
+ 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.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: Proxmox.Utils.render_cpu,
+ sortable: true,
+ width: 100,
+ dataIndex: 'cpu',
+ },
+ ],
+ },
+
+ validator: function(value) {
+ let me = this;
+ if (!me.onlineValidator || (me.allowBlank && !value)) {
+ return true;
+ }
+
+ let offline = [], notAllowed = [];
+ Ext.Array.each(value.split(/\s*,\s*/), function(node) {
+ let rec = me.store.findRecord(me.valueField, node, 0, false, true, true);
+ if (!(rec && rec.data) || rec.data.status !== 'online') {
+ offline.push(node);
+ } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) {
+ notAllowed.push(node);
+ }
+ });
+
+ if (value && notAllowed.length !== 0) {
+ return "Node " + notAllowed.join(', ') + " is not allowed for this action!";
+ }
+ if (value && offline.length !== 0) {
+ return "Node " + offline.join(', ') + " seems to be offline!";
+ }
+ return true;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) {
+ me.preferredValue = PVE.curSelectedNode.data.node;
+ }
+
+ me.callParent();
+ me.getStore().load();
+
+ me.getStore().addFilter(new Ext.util.Filter({ // filter out disallowed nodes
+ filterFn: (item) => !(me.disallowedNodes && me.disallowedNodes.includes(item.data.node)),
+ }));
+
+ me.mon(me.getStore(), 'load', () => me.isValid());
+ },
+});
+Ext.define('PVE.form.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: 100,
+ },
+ {
+ header: gettext('IOMMU Group'),
+ dataIndex: 'iommugroup',
+ renderer: v => v === -1 ? '-' : v,
+ width: 75,
+ },
+ {
+ header: gettext('Vendor'),
+ dataIndex: 'vendor_name',
+ flex: 2,
+ },
+ {
+ header: gettext('Device'),
+ dataIndex: 'device_name',
+ flex: 6,
+ },
+ {
+ header: gettext('Mediated Devices'),
+ dataIndex: 'mdev',
+ flex: 1,
+ renderer: function(val) {
+ return Proxmox.Utils.format_boolean(!!val);
+ },
+ },
+ ],
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ if (!nodename || me.nodename === nodename) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/hardware/pci',
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename;
+ me.nodename = undefined;
+
+ me.callParent();
+
+ if (me.onLoadCallBack !== undefined) {
+ me.mon(me.getStore(), 'load', me.onLoadCallBack);
+ }
+
+ me.setNodename(nodename);
+ },
+});
+
+Ext.define('PVE.form.PermPathSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ xtype: 'pvePermPathSelector',
+
+ valueField: 'value',
+ displayField: 'value',
+ typeAhead: true,
+ queryMode: 'local',
+
+ store: {
+ type: 'pvePermPath',
+ },
+});
+Ext.define('PVE.form.PoolSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pvePoolSelector'],
+
+ allowBlank: false,
+ valueField: 'poolid',
+ displayField: 'poolid',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-pools',
+ sorters: 'poolid',
+ });
+
+ Ext.apply(me, {
+ store: store,
+ autoSelect: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Pool'),
+ sortable: true,
+ dataIndex: 'poolid',
+ flex: 1,
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+
+ store.load();
+ },
+
+}, function() {
+ Ext.define('pve-pools', {
+ extend: 'Ext.data.Model',
+ fields: ['poolid', 'comment'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/pools",
+ },
+ idProperty: 'poolid',
+ });
+});
+Ext.define('PVE.form.preallocationSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pvePreallocationSelector'],
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText],
+ ['off', 'Off'],
+ ['metadata', 'Metadata'],
+ ['falloc', 'Full (posix_fallocate)'],
+ ['full', 'Full'],
+ ],
+});
+Ext.define('PVE.form.PrivilegesSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ xtype: 'pvePrivilegesSelector',
+
+ multiSelect: true,
+
+ initComponent: function() {
+ let me = this;
+
+ me.callParent();
+
+ Proxmox.Utils.API2Request({
+ url: '/access/roles/Administrator',
+ method: 'GET',
+ success: function(response, options) {
+ let data = Object.keys(response.result.data).map(key => [key, key]);
+
+ me.store.setData(data);
+
+ me.store.sort({
+ property: 'key',
+ direction: 'ASC',
+ });
+ },
+ failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ },
+});
+Ext.define('PVE.form.QemuBiosSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveQemuBiosSelector'],
+
+ initComponent: function() {
+ var me = this;
+
+ me.comboItems = [
+ ['__default__', PVE.Utils.render_qemu_bios('')],
+ ['seabios', PVE.Utils.render_qemu_bios('seabios')],
+ ['ovmf', PVE.Utils.render_qemu_bios('ovmf')],
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.form.SDNControllerSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveSDNControllerSelector'],
+
+ allowBlank: false,
+ valueField: 'controller',
+ displayField: 'controller',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-sdn-controller',
+ sorters: {
+ property: 'controller',
+ direction: 'ASC',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ autoSelect: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Controller'),
+ sortable: true,
+ dataIndex: 'controller',
+ flex: 1,
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+
+ store.load();
+ },
+
+}, function() {
+ Ext.define('pve-sdn-controller', {
+ extend: 'Ext.data.Model',
+ fields: ['controller'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/controllers",
+ },
+ idProperty: 'controller',
+ });
+});
+Ext.define('PVE.form.SDNZoneSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveSDNZoneSelector'],
+
+ allowBlank: false,
+ valueField: 'zone',
+ displayField: 'zone',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-sdn-zone',
+ sorters: {
+ property: 'zone',
+ direction: 'ASC',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ autoSelect: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Zone'),
+ sortable: true,
+ dataIndex: 'zone',
+ flex: 1,
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+
+ store.load();
+ },
+
+}, function() {
+ Ext.define('pve-sdn-zone', {
+ extend: 'Ext.data.Model',
+ fields: ['zone'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/zones",
+ },
+ idProperty: 'zone',
+ });
+});
+Ext.define('PVE.form.SDNVnetSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveSDNVnetSelector'],
+
+ allowBlank: false,
+ valueField: 'vnet',
+ displayField: 'vnet',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-sdn-vnet',
+ sorters: {
+ property: 'vnet',
+ direction: 'ASC',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ autoSelect: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Vnet'),
+ sortable: true,
+ dataIndex: 'vnet',
+ flex: 1,
+ },
+ {
+ header: gettext('Alias'),
+ flex: 1,
+ dataIndex: 'alias',
+ },
+ {
+ header: gettext('Tag'),
+ flex: 1,
+ dataIndex: 'tag',
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+
+ store.load();
+ },
+
+}, function() {
+ Ext.define('pve-sdn-vnet', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'alias',
+ 'tag',
+ 'type',
+ 'vnet',
+ 'zone',
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/vnets",
+ },
+ idProperty: 'vnet',
+ });
+});
+Ext.define('PVE.form.SDNIpamSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveSDNIpamSelector'],
+
+ allowBlank: false,
+ valueField: 'ipam',
+ displayField: 'ipam',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-sdn-ipam',
+ sorters: {
+ property: 'ipam',
+ direction: 'ASC',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ autoSelect: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Ipam'),
+ sortable: true,
+ dataIndex: 'ipam',
+ flex: 1,
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+
+ store.load();
+ },
+
+}, function() {
+ Ext.define('pve-sdn-ipam', {
+ extend: 'Ext.data.Model',
+ fields: ['ipam'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/ipams",
+ },
+ idProperty: 'ipam',
+ });
+});
+Ext.define('PVE.form.SDNDnsSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveSDNDnsSelector'],
+
+ allowBlank: false,
+ valueField: 'dns',
+ displayField: 'dns',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-sdn-dns',
+ sorters: {
+ property: 'dns',
+ direction: 'ASC',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ autoSelect: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('dns'),
+ sortable: true,
+ dataIndex: 'dns',
+ flex: 1,
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+
+ store.load();
+ },
+
+}, function() {
+ Ext.define('pve-sdn-dns', {
+ extend: 'Ext.data.Model',
+ fields: ['dns'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/dns",
+ },
+ idProperty: 'dns',
+ });
+});
+Ext.define('PVE.form.ScsiHwSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveScsiHwSelector'],
+ comboItems: [
+ ['__default__', PVE.Utils.render_scsihw('')],
+ ['lsi', PVE.Utils.render_scsihw('lsi')],
+ ['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')],
+ ['megasas', PVE.Utils.render_scsihw('megasas')],
+ ['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')],
+ ['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')],
+ ['pvscsi', PVE.Utils.render_scsihw('pvscsi')],
+ ],
+});
+Ext.define('PVE.form.SecurityGroupsSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveSecurityGroupsSelector'],
+
+ valueField: 'group',
+ displayField: 'group',
+ initComponent: function() {
+ var me = this;
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ fields: ['group', 'comment'],
+ idProperty: 'group',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/firewall/groups",
+ },
+ sorters: {
+ property: 'group',
+ direction: 'ASC',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Security Group'),
+ dataIndex: 'group',
+ hideable: false,
+ width: 100,
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.form.SnapshotSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.PVE.form.SnapshotSelector'],
+
+ valueField: 'name',
+ displayField: 'name',
+
+ loadStore: function(nodename, vmid) {
+ var me = this;
+
+ if (!nodename) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ if (!vmid) {
+ return;
+ }
+
+ me.vmid = vmid;
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid +'/snapshot',
+ });
+
+ me.store.load();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.guestType) {
+ throw "no guest type specified";
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: ['name'],
+ filterOnLoad: true,
+ });
+
+ Ext.apply(me, {
+ store: store,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Snapshot'),
+ dataIndex: 'name',
+ hideable: false,
+ flex: 1,
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+
+ me.loadStore(me.nodename, me.vmid);
+ },
+});
+Ext.define('PVE.form.SpiceEnhancementSelector', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveSpiceEnhancementSelector',
+
+ viewModel: {},
+
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ itemId: 'foldersharing',
+ name: 'foldersharing',
+ reference: 'foldersharing',
+ fieldLabel: 'Folder Sharing',
+ uncheckedValue: 0,
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ itemId: 'videostreaming',
+ name: 'videostreaming',
+ value: 'off',
+ fieldLabel: 'Video Streaming',
+ comboItems: [
+ ['off', 'off'],
+ ['all', 'all'],
+ ['filter', 'filter'],
+ ],
+ },
+ {
+ xtype: 'displayfield',
+ itemId: 'spicehint',
+ userCls: 'pmx-hint',
+ value: gettext('To use these features set the display to SPICE in the hardware settings of the VM.'),
+ hidden: true,
+ },
+ {
+ xtype: 'displayfield',
+ itemId: 'spicefolderhint',
+ userCls: 'pmx-hint',
+ value: gettext('Make sure the SPICE WebDav daemon is installed in the VM.'),
+ bind: {
+ hidden: '{!foldersharing.checked}',
+ },
+ },
+ ],
+
+ onGetValues: function(values) {
+ var ret = {};
+
+ if (values.videostreaming !== "off") {
+ ret.videostreaming = values.videostreaming;
+ }
+ if (values.foldersharing) {
+ ret.foldersharing = 1;
+ }
+ if (Ext.Object.isEmpty(ret)) {
+ return { 'delete': 'spice_enhancements' };
+ }
+ var enhancements = PVE.Parser.printPropertyString(ret);
+ return { spice_enhancements: enhancements };
+ },
+
+ setValues: function(values) {
+ var vga = PVE.Parser.parsePropertyString(values.vga, 'type');
+ if (!/^qxl\d?$/.test(vga.type)) {
+ this.down('#spicehint').setVisible(true);
+ }
+ if (values.spice_enhancements) {
+ var enhancements = PVE.Parser.parsePropertyString(values.spice_enhancements);
+ enhancements.foldersharing = PVE.Parser.parseBoolean(enhancements.foldersharing, 0);
+ this.callParent([enhancements]);
+ }
+ },
+});
+Ext.define('PVE.form.StorageScanNodeSelector', {
+ extend: 'PVE.form.NodeSelector',
+ xtype: 'pveStorageScanNodeSelector',
+
+ name: 'storageScanNode',
+ itemId: 'pveStorageScanNodeSelector',
+ fieldLabel: gettext('Scan node'),
+ allowBlank: true,
+ disallowedNodes: undefined,
+ autoSelect: false,
+ submitValue: false,
+ value: null,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Scan for available storages on the selected node'),
+ },
+ triggers: {
+ clear: {
+ handler: function() {
+ let me = this;
+ me.setValue(null);
+ },
+ },
+ },
+
+ emptyText: Proxmox.NodeName,
+
+ setValue: function(value) {
+ let me = this;
+ me.callParent([value]);
+ me.triggers.clear.setVisible(!!value);
+ },
+});
+Ext.define('PVE.form.StorageSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.pveStorageSelector',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ cbindData: {
+ clusterView: false,
+ },
+
+ allowBlank: false,
+ valueField: 'storage',
+ displayField: 'storage',
+ listConfig: {
+ cbind: {
+ clusterView: '{clusterView}',
+ },
+ width: 450,
+ columns: [
+ {
+ header: gettext('Name'),
+ dataIndex: 'storage',
+ hideable: false,
+ flex: 1,
+ },
+ {
+ header: gettext('Type'),
+ width: 75,
+ dataIndex: 'type',
+ },
+ {
+ header: gettext('Avail'),
+ width: 90,
+ dataIndex: 'avail',
+ renderer: Proxmox.Utils.format_size,
+ cbind: {
+ hidden: '{clusterView}',
+ },
+ },
+ {
+ header: gettext('Capacity'),
+ width: 90,
+ dataIndex: 'total',
+ renderer: Proxmox.Utils.format_size,
+ cbind: {
+ hidden: '{clusterView}',
+ },
+ },
+ {
+ header: gettext('Nodes'),
+ width: 120,
+ dataIndex: 'nodes',
+ renderer: (value) => value ? value : '-- ' + gettext('All') + ' --',
+ cbind: {
+ hidden: '{!clusterView}',
+ },
+ },
+ {
+ header: gettext('Shared'),
+ width: 70,
+ dataIndex: 'shared',
+ renderer: Proxmox.Utils.format_boolean,
+ cbind: {
+ hidden: '{!clusterView}',
+ },
+ },
+ ],
+ },
+
+ reloadStorageList: function() {
+ let me = this;
+
+ if (me.clusterView) {
+ me.getStore().setProxy({
+ type: 'proxmox',
+ url: `/api2/json/storage`,
+ });
+
+ // filter here, back-end does not support it currently
+ let filters = [(storage) => !storage.data.disable];
+
+ if (me.storageContent) {
+ filters.push(
+ (storage) => storage.data.content.split(',').includes(me.storageContent),
+ );
+ }
+
+ if (me.nodename) {
+ filters.push(
+ (storage) => !storage.data.nodes || storage.data.nodes.includes(me.nodename),
+ );
+ }
+
+ me.getStore().clearFilter();
+ me.getStore().setFilters(filters);
+ } else {
+ if (!me.nodename) {
+ return;
+ }
+
+ let params = {
+ format: 1,
+ };
+ if (me.storageContent) {
+ params.content = me.storageContent;
+ }
+ if (me.targetNode) {
+ params.target = me.targetNode;
+ params.enabled = 1; // skip disabled storages
+ }
+ me.store.setProxy({
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodename}/storage`,
+ extraParams: params,
+ });
+ }
+
+ me.store.load(() => me.validate());
+ },
+
+ setTargetNode: function(targetNode) {
+ var me = this;
+
+ if (!targetNode || me.targetNode === targetNode) {
+ return;
+ }
+
+ if (me.clusterView) {
+ throw "setting targetNode with clusterView is not implemented";
+ }
+
+ me.targetNode = targetNode;
+
+ me.reloadStorageList();
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ nodename = nodename || '';
+
+ if (me.nodename === nodename) {
+ return;
+ }
+
+ me.nodename = nodename;
+
+ me.reloadStorageList();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ let nodename = me.nodename;
+ me.nodename = undefined;
+
+ var store = Ext.create('Ext.data.Store', {
+ model: 'pve-storage-status',
+ sorters: {
+ property: 'storage',
+ direction: 'ASC',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+
+ me.setNodename(nodename);
+ },
+}, function() {
+ Ext.define('pve-storage-status', {
+ extend: 'Ext.data.Model',
+ fields: ['storage', 'active', 'type', 'avail', 'total', 'nodes', 'shared'],
+ idProperty: 'storage',
+ });
+});
+Ext.define('PVE.form.TFASelector', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveTFASelector',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ deleteEmpty: true,
+
+ viewModel: {
+ data: {
+ type: '__default__',
+ step: null,
+ digits: null,
+ id: null,
+ key: null,
+ url: null,
+ },
+
+ formulas: {
+ isOath: (get) => get('type') === 'oath',
+ isYubico: (get) => get('type') === 'yubico',
+ tfavalue: {
+ get: function(get) {
+ let val = {
+ type: get('type'),
+ };
+ if (get('isOath')) {
+ let step = get('step');
+ let digits = get('digits');
+ if (step) {
+ val.step = step;
+ }
+ if (digits) {
+ val.digits = digits;
+ }
+ } else if (get('isYubico')) {
+ let id = get('id');
+ let key = get('key');
+ let url = get('url');
+ val.id = id;
+ val.key = key;
+ if (url) {
+ val.url = url;
+ }
+ } else if (val.type === '__default__') {
+ return "";
+ }
+
+ return PVE.Parser.printPropertyString(val);
+ },
+ set: function(value) {
+ let val = PVE.Parser.parseTfaConfig(value);
+ this.set(val);
+ this.notify();
+ // we need to reset the original values, so that
+ // we can reliably track the state of the form
+ let form = this.getView().up('form');
+ if (form.trackResetOnLoad) {
+ let fields = this.getView().query('field[name!="tfa"]');
+ fields.forEach((field) => field.resetOriginalValue());
+ }
+ },
+ },
+ },
+ },
+
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'tfa',
+ hidden: true,
+ submitValue: true,
+ cbind: {
+ deleteEmpty: '{deleteEmpty}',
+ },
+ bind: {
+ value: "{tfavalue}",
+ },
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ value: '__default__',
+ deleteEmpty: false,
+ submitValue: false,
+ fieldLabel: gettext('Require TFA'),
+ comboItems: [
+ ['__default__', Proxmox.Utils.noneText],
+ ['oath', 'OATH/TOTP'],
+ ['yubico', 'Yubico'],
+ ],
+ bind: {
+ value: "{type}",
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ hidden: true,
+ minValue: 10,
+ submitValue: false,
+ emptyText: Proxmox.Utils.defaultText + ' (30)',
+ fieldLabel: gettext('Time Step'),
+ bind: {
+ value: "{step}",
+ hidden: "{!isOath}",
+ disabled: "{!isOath}",
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ hidden: true,
+ submitValue: false,
+ fieldLabel: gettext('Secret Length'),
+ minValue: 6,
+ maxValue: 8,
+ emptyText: Proxmox.Utils.defaultText + ' (6)',
+ bind: {
+ value: "{digits}",
+ hidden: "{!isOath}",
+ disabled: "{!isOath}",
+ },
+ },
+ {
+ xtype: 'textfield',
+ hidden: true,
+ submitValue: false,
+ allowBlank: false,
+ fieldLabel: 'Yubico API Id',
+ bind: {
+ value: "{id}",
+ hidden: "{!isYubico}",
+ disabled: "{!isYubico}",
+ },
+ },
+ {
+ xtype: 'textfield',
+ hidden: true,
+ submitValue: false,
+ allowBlank: false,
+ fieldLabel: 'Yubico API Key',
+ bind: {
+ value: "{key}",
+ hidden: "{!isYubico}",
+ disabled: "{!isYubico}",
+ },
+ },
+ {
+ xtype: 'textfield',
+ hidden: true,
+ submitValue: false,
+ fieldLabel: 'Yubico URL',
+ bind: {
+ value: "{url}",
+ hidden: "{!isYubico}",
+ disabled: "{!isYubico}",
+ },
+ },
+ ],
+});
+Ext.define('PVE.form.TokenSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveTokenSelector'],
+
+ allowBlank: false,
+ autoSelect: false,
+ displayField: 'id',
+
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+
+ store: {
+ model: 'pve-tokens',
+ autoLoad: true,
+ proxy: {
+ type: 'proxmox',
+ url: 'api2/json/access/users',
+ extraParams: { 'full': 1 },
+ },
+ sorters: 'id',
+ listeners: {
+ load: function(store, records, success) {
+ let tokens = [];
+ for (const { data: user } of records) {
+ if (!user.tokens || user.tokens.length === 0) {
+ continue;
+ }
+ for (const token of user.tokens) {
+ tokens.push({
+ id: `${user.userid}!${token.tokenid}`,
+ comment: token.comment,
+ });
+ }
+ }
+ store.loadData(tokens);
+ },
+ },
+ },
+
+ listConfig: {
+ columns: [
+ {
+ header: gettext('API Token'),
+ sortable: true,
+ dataIndex: 'id',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+ },
+}, function() {
+ Ext.define('pve-tokens', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id', 'userid', 'tokenid', 'comment',
+ { type: 'boolean', name: 'privsep' },
+ { type: 'date', dateFormat: 'timestamp', name: 'expire' },
+ ],
+ idProperty: 'id',
+ });
+});
+Ext.define('PVE.form.USBSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: ['widget.pveUSBSelector'],
+
+ allowBlank: false,
+ autoSelect: false,
+ anyMatch: true,
+ displayField: 'product_and_id',
+ valueField: 'usbid',
+ editable: true,
+
+ validator: function(value) {
+ var me = this;
+ if (!value) {
+ return true; // handled later by allowEmpty in the getErrors call chain
+ }
+ value = me.getValue(); // as the valueField is not the displayfield
+ if (me.type === 'device') {
+ return (/^[a-f0-9]{4}:[a-f0-9]{4}$/i).test(value);
+ } else if (me.type === 'port') {
+ return (/^[0-9]+-[0-9]+(\.[0-9]+)*$/).test(value);
+ }
+ return gettext("Invalid Value");
+ },
+
+ 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";
+ }
+
+ let store = new Ext.data.Store({
+ model: `pve-usb-${me.type}`,
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${nodename}/hardware/usb`,
+ },
+ filters: [
+ ({ data }) => !!data.usbpath && !!data.prodid && String(data.class) !== "9",
+ ],
+ });
+ let emptyText = '';
+ if (me.type === 'device') {
+ emptyText = gettext('Passthrough a specific device');
+ } else {
+ emptyText = gettext('Passthrough a full port');
+ }
+
+ Ext.apply(me, {
+ store: store,
+ emptyText: emptyText,
+ listConfig: {
+ width: 520,
+ columns: [
+ {
+ header: me.type === 'device'?gettext('Device'):gettext('Port'),
+ sortable: true,
+ dataIndex: 'usbid',
+ width: 80,
+ },
+ {
+ header: gettext('Manufacturer'),
+ sortable: true,
+ dataIndex: 'manufacturer',
+ width: 150,
+ },
+ {
+ header: gettext('Product'),
+ sortable: true,
+ dataIndex: 'product',
+ flex: 1,
+ },
+ {
+ header: gettext('Speed'),
+ width: 75,
+ sortable: true,
+ dataIndex: 'speed',
+ renderer: function(value) {
+ let speed2Class = {
+ "10000": "USB 3.1",
+ "5000": "USB 3.0",
+ "480": "USB 2.0",
+ "12": "USB 1.x",
+ "1.5": "USB 1.x",
+ };
+ return speed2Class[value] || value + " Mbps";
+ },
+ },
+ ],
+ },
+ });
+
+ me.callParent();
+
+ 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' },
+ {
+ name: 'product_and_id',
+ type: 'string',
+ convert: (v, rec) => {
+ let res = rec.data.product || gettext('Unkown');
+ res += " (" + rec.data.usbid + ")";
+ return res;
+ },
+ },
+ ],
+ });
+
+ Ext.define('pve-usb-port', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {
+ name: 'usbid',
+ convert: function(val, data) {
+ if (val) {
+ return val;
+ }
+ return data.get('busnum') + '-' + data.get('usbpath');
+ },
+ },
+ 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
+ { name: 'port', type: 'number' },
+ { name: 'level', type: 'number' },
+ { name: 'class', type: 'number' },
+ { name: 'devnum', type: 'number' },
+ { name: 'busnum', type: 'number' },
+ {
+ name: 'product_and_id',
+ type: 'string',
+ convert: (v, rec) => {
+ let res = rec.data.product || gettext('Unplugged');
+ res += " (" + rec.data.usbid + ")";
+ return res;
+ },
+ },
+ ],
+ });
+});
+Ext.define('pmx-users', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'userid', 'firstname', 'lastname', 'email', 'comment',
+ { type: 'boolean', name: 'enable' },
+ { type: 'date', dateFormat: 'timestamp', name: 'expire' },
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/users",
+ },
+ idProperty: 'userid',
+});
+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();
+ },
+});
+Ext.define('PVE.form.VMCPUFlagSelector', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.vmcpuflagselector',
+
+ mixins: {
+ field: 'Ext.form.field.Field',
+ },
+
+ disableSelection: true,
+ columnLines: false,
+ selectable: false,
+ hideHeaders: true,
+
+ scrollable: 'y',
+ height: 200,
+
+ unkownFlags: [],
+
+ store: {
+ type: 'store',
+ fields: ['flag', { name: 'state', defaultValue: '=' }, 'desc'],
+ data: [
+ // FIXME: let qemu-server host this and autogenerate or get from API call??
+ { flag: 'md-clear', desc: 'Required to let the guest OS know if MDS is mitigated correctly' },
+ { flag: 'pcid', desc: 'Meltdown fix cost reduction on Westmere, Sandy-, and IvyBridge Intel CPUs' },
+ { flag: 'spec-ctrl', desc: 'Allows improved Spectre mitigation with Intel CPUs' },
+ { flag: 'ssbd', desc: 'Protection for "Speculative Store Bypass" for Intel models' },
+ { flag: 'ibpb', desc: 'Allows improved Spectre mitigation with AMD CPUs' },
+ { flag: 'virt-ssbd', desc: 'Basis for "Speculative Store Bypass" protection for AMD models' },
+ { flag: 'amd-ssbd', desc: 'Improves Spectre mitigation performance with AMD CPUs, best used with "virt-ssbd"' },
+ { flag: 'amd-no-ssb', desc: 'Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs' },
+ { flag: 'pdpe1gb', desc: 'Allow guest OS to use 1GB size pages, if host HW supports it' },
+ { flag: 'hv-tlbflush', desc: 'Improve performance in overcommitted Windows guests. May lead to guest bluescreens on old CPUs.' },
+ { flag: 'hv-evmcs', desc: 'Improve performance for nested virtualization. Only supported on Intel CPUs.' },
+ { flag: 'aes', desc: 'Activate AES instruction set for HW acceleration.' },
+ ],
+ listeners: {
+ update: function() {
+ this.commitChanges();
+ },
+ },
+ },
+
+ getValue: function() {
+ var me = this;
+ var store = me.getStore();
+ var flags = '';
+
+ // ExtJS does not has a nice getAllRecords interface for stores :/
+ store.queryBy(Ext.returnTrue).each(function(rec) {
+ var s = rec.get('state');
+ if (s && s !== '=') {
+ var f = rec.get('flag');
+ if (flags === '') {
+ flags = s + f;
+ } else {
+ flags += ';' + s + f;
+ }
+ }
+ });
+
+ flags += me.unkownFlags.join(';');
+
+ return flags;
+ },
+
+ setValue: function(value) {
+ var me = this;
+ var store = me.getStore();
+
+ me.value = value || '';
+
+ me.unkownFlags = [];
+
+ me.getStore().queryBy(Ext.returnTrue).each(function(rec) {
+ rec.set('state', '=');
+ });
+
+ var flags = value ? value.split(';') : [];
+ flags.forEach(function(flag) {
+ var sign = flag.substr(0, 1);
+ flag = flag.substr(1);
+
+ var rec = store.findRecord('flag', flag, 0, false, true, true);
+ if (rec !== null) {
+ rec.set('state', sign);
+ } else {
+ me.unkownFlags.push(flag);
+ }
+ });
+ store.reload();
+
+ var res = me.mixins.field.setValue.call(me, value);
+
+ return res;
+ },
+ columns: [
+ {
+ dataIndex: 'state',
+ renderer: function(v) {
+ switch (v) {
+ case '=': return 'Default';
+ case '-': return 'Off';
+ case '+': return 'On';
+ default: return 'Unknown';
+ }
+ },
+ width: 65,
+ },
+ {
+ xtype: 'widgetcolumn',
+ dataIndex: 'state',
+ width: 95,
+ onWidgetAttach: function(column, widget, record) {
+ var val = record.get('state') || '=';
+ widget.down('[inputValue=' + val + ']').setValue(true);
+ // TODO: disable if selected CPU model and flag are incompatible
+ },
+ widget: {
+ xtype: 'radiogroup',
+ hideLabel: true,
+ layout: 'hbox',
+ validateOnChange: false,
+ value: '=',
+ listeners: {
+ change: function(f, value) {
+ var v = Object.values(value)[0];
+ f.getWidgetRecord().set('state', v);
+
+ var view = this.up('grid');
+ view.dirty = view.getValue() !== view.originalValue;
+ view.checkDirty();
+ //view.checkChange();
+ },
+ },
+ items: [
+ {
+ boxLabel: '-',
+ boxLabelAlign: 'before',
+ inputValue: '-',
+ isFormField: false,
+ },
+ {
+ checked: true,
+ inputValue: '=',
+ isFormField: false,
+ },
+ {
+ boxLabel: '+',
+ inputValue: '+',
+ isFormField: false,
+ },
+ ],
+ },
+ },
+ {
+ dataIndex: 'flag',
+ width: 100,
+ },
+ {
+ dataIndex: 'desc',
+ cellWrap: true,
+ flex: 1,
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ // static class store, thus gets not recreated, so ensure defaults are set!
+ me.getStore().data.forEach(function(v) {
+ v.state = '=';
+ });
+
+ me.value = me.originalValue = '';
+
+ me.callParent(arguments);
+ },
+});
+/* filter is a javascript builtin, but extjs calls it also filter */
+Ext.define('PVE.form.VMSelector', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.vmselector',
+
+ mixins: {
+ field: 'Ext.form.field.Field',
+ },
+
+ allowBlank: true,
+ selectAll: false,
+ isFormField: true,
+
+ plugins: 'gridfilters',
+
+ store: {
+ model: 'PVEResources',
+ sorters: 'vmid',
+ },
+
+ columnsDeclaration: [
+ {
+ header: 'ID',
+ dataIndex: 'vmid',
+ width: 80,
+ filter: {
+ type: 'number',
+ },
+ },
+ {
+ header: gettext('Node'),
+ dataIndex: 'node',
+ },
+ {
+ header: gettext('Status'),
+ dataIndex: 'status',
+ filter: {
+ type: 'list',
+ },
+ },
+ {
+ header: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ filter: {
+ type: 'string',
+ },
+ },
+ {
+ header: gettext('Pool'),
+ dataIndex: 'pool',
+ filter: {
+ type: 'list',
+ },
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ width: 120,
+ renderer: function(value) {
+ if (value === 'qemu') {
+ return gettext('Virtual Machine');
+ } else if (value === 'lxc') {
+ return gettext('LXC Container');
+ }
+
+ return '';
+ },
+ filter: {
+ type: 'list',
+ store: {
+ data: [
+ { id: 'qemu', text: gettext('Virtual Machine') },
+ { id: 'lxc', text: gettext('LXC Container') },
+ ],
+ un: function() {
+ // Due to EXTJS-18711. we have to do a static list via a store but to avoid
+ // creating an object, we have to have an empty pseudo un function
+ },
+ },
+ },
+ },
+ {
+ header: 'HA ' + gettext('Status'),
+ dataIndex: 'hastate',
+ flex: 1,
+ filter: {
+ type: 'list',
+ },
+ },
+ ],
+
+ // should be a list of 'dataIndex' values, if 'undefined' all declared columns will be included
+ columnSelection: undefined,
+
+ selModel: {
+ selType: 'checkboxmodel',
+ mode: 'SIMPLE',
+ },
+
+ checkChangeEvents: [
+ 'selectionchange',
+ 'change',
+ ],
+
+ listeners: {
+ selectionchange: function() {
+ // to trigger validity and error checks
+ this.checkChange();
+ },
+ },
+
+ getValue: function() {
+ var me = this;
+ if (me.savedValue !== undefined) {
+ return me.savedValue;
+ }
+ var sm = me.getSelectionModel();
+ var selection = sm.getSelection();
+ var values = [];
+ var store = me.getStore();
+ selection.forEach(function(item) {
+ // only add if not filtered
+ if (store.findExact('vmid', item.data.vmid) !== -1) {
+ values.push(item.data.vmid);
+ }
+ });
+ return values;
+ },
+
+ setValueSelection: function(value) {
+ let me = this;
+
+ let store = me.getStore();
+ let notFound = [];
+ let selection = value.map(item => {
+ let found = store.findRecord('vmid', item, 0, false, true, true);
+ if (!found) {
+ notFound.push(item);
+ }
+ return found;
+ }).filter(r => r);
+
+ for (const vmid of notFound) {
+ let rec = store.add({
+ vmid,
+ node: 'unknown',
+ });
+ selection.push(rec[0]);
+ }
+
+ let sm = me.getSelectionModel();
+ if (selection.length) {
+ sm.select(selection);
+ } else {
+ sm.deselectAll();
+ }
+ // to correctly trigger invalid class
+ me.getErrors();
+ },
+
+ setValue: function(value) {
+ let me = this;
+ if (!Ext.isArray(value)) {
+ value = value.split(',');
+ }
+
+ let store = me.getStore();
+ if (!store.isLoaded()) {
+ me.savedValue = value;
+ store.on('load', function() {
+ me.setValueSelection(value);
+ delete me.savedValue;
+ }, { single: true });
+ } else {
+ me.setValueSelection(value);
+ }
+ return me.mixins.field.setValue.call(me, value);
+ },
+
+ getErrors: function(value) {
+ let me = this;
+ if (!me.isDisabled() && me.allowBlank === false &&
+ me.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 [];
+ },
+
+ setDisabled: function(disabled) {
+ let me = this;
+ let res = me.callParent([disabled]);
+ me.getErrors();
+ return res;
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let columns = me.columnsDeclaration.filter((column) =>
+ me.columnSelection ? me.columnSelection.indexOf(column.dataIndex) !== -1 : true,
+ ).map((x) => x);
+
+ me.columns = columns;
+
+ me.callParent();
+
+ me.getStore().load({ params: { type: 'vm' } });
+
+ if (me.nodename) {
+ me.getStore().addFilter({
+ property: 'node',
+ exactMatch: true,
+ value: me.nodename,
+ });
+ }
+
+ // only show the relevant guests by default
+ if (me.action) {
+ var statusfilter = '';
+ switch (me.action) {
+ case 'startall':
+ statusfilter = 'stopped';
+ break;
+ case 'stopall':
+ statusfilter = 'running';
+ break;
+ }
+ if (statusfilter !== '') {
+ me.getStore().addFilter([{
+ property: 'template',
+ value: 0,
+ }, {
+ id: 'x-gridfilter-status',
+ operator: 'in',
+ property: 'status',
+ value: [statusfilter],
+ }]);
+ }
+ }
+
+ if (me.selectAll) {
+ me.mon(me.getStore(), 'load', function() {
+ me.getSelectionModel().selectAll(false);
+ });
+ }
+ },
+});
+
+
+Ext.define('PVE.form.VMComboSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.vmComboSelector',
+
+ valueField: 'vmid',
+ displayField: 'vmid',
+
+ autoSelect: false,
+ editable: true,
+ anyMatch: true,
+ forceSelection: true,
+
+ store: {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [{
+ property: 'type',
+ value: /lxc|qemu/,
+ }],
+ },
+
+ listConfig: {
+ width: 600,
+ plugins: 'gridfilters',
+ columns: [
+ {
+ header: 'ID',
+ dataIndex: 'vmid',
+ width: 80,
+ filter: {
+ type: 'number',
+ },
+ },
+ {
+ header: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ filter: {
+ type: 'string',
+ },
+ },
+ {
+ header: gettext('Node'),
+ dataIndex: 'node',
+ },
+ {
+ header: gettext('Status'),
+ dataIndex: 'status',
+ filter: {
+ type: 'list',
+ },
+ },
+ {
+ header: gettext('Pool'),
+ dataIndex: 'pool',
+ hidden: true,
+ filter: {
+ type: 'list',
+ },
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ width: 120,
+ renderer: function(value) {
+ if (value === 'qemu') {
+ return gettext('Virtual Machine');
+ } else if (value === 'lxc') {
+ return gettext('LXC Container');
+ }
+
+ return '';
+ },
+ filter: {
+ type: 'list',
+ store: {
+ data: [
+ { id: 'qemu', text: gettext('Virtual Machine') },
+ { id: 'lxc', text: gettext('LXC Container') },
+ ],
+ un: function() { /* due to EXTJS-18711 */ },
+ },
+ },
+ },
+ {
+ header: 'HA ' + gettext('Status'),
+ dataIndex: 'hastate',
+ hidden: true,
+ flex: 1,
+ filter: {
+ type: 'list',
+ },
+ },
+ ],
+ },
+});
+Ext.define('PVE.form.VNCKeyboardSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.VNCKeyboardSelector'],
+ comboItems: Object.entries(PVE.Utils.kvm_keymaps),
+});
+/*
+ * Top left combobox, used to select a view of the underneath RessourceTree
+ */
+Ext.define('PVE.form.ViewSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: ['widget.pveViewSelector'],
+
+ editable: false,
+ allowBlank: false,
+ forceSelection: true,
+ autoSelect: false,
+ valueField: 'key',
+ displayField: 'value',
+ hideLabel: true,
+ queryMode: 'local',
+
+ initComponent: function() {
+ let me = this;
+
+ let default_views = {
+ server: {
+ text: gettext('Server View'),
+ groups: ['node'],
+ },
+ folder: {
+ text: gettext('Folder View'),
+ groups: ['type'],
+ },
+ pool: {
+ text: gettext('Pool View'),
+ groups: ['pool'],
+ // Pool View only lists VMs and Containers
+ filterfn: ({ data }) => data.type === 'qemu' || data.type === 'lxc' || data.type === 'pool',
+ },
+ };
+ let groupdef = Object.entries(default_views).map(([name, config]) => [name, config.text]);
+
+ let store = Ext.create('Ext.data.Store', {
+ model: 'KeyValue',
+ proxy: {
+ type: 'memory',
+ reader: 'array',
+ },
+ data: groupdef,
+ autoload: true,
+ });
+
+ Ext.apply(me, {
+ store: store,
+ value: groupdef[0][0],
+ getViewFilter: function() {
+ let view = me.getValue();
+ return Ext.apply({ id: view }, default_views[view] || default_views.server);
+ },
+ getState: function() {
+ return { value: me.getValue() };
+ },
+ applyState: function(state, doSelect) {
+ let view = me.getValue();
+ if (state && state.value && view !== state.value) {
+ let record = store.findRecord('key', state.value, 0, false, true, true);
+ if (record) {
+ me.setValue(state.value, true);
+ if (doSelect) {
+ me.fireEvent('select', me, [record]);
+ }
+ }
+ }
+ },
+ stateEvents: ['select'],
+ stateful: true,
+ stateId: 'pveview',
+ id: 'view',
+ });
+
+ me.callParent();
+
+ let statechange = function(sp, key, value) {
+ if (key === me.id) {
+ me.applyState(value, true);
+ }
+ };
+ let sp = Ext.state.Manager.getProvider();
+ me.mon(sp, 'statechange', statechange, me);
+ },
+});
+Ext.define('PVE.form.iScsiProviderSelector', {
+ extend: 'Proxmox.form.KVComboBox',
+ alias: ['widget.pveiScsiProviderSelector'],
+ comboItems: [
+ ['comstar', 'Comstar'],
+ ['istgt', 'istgt'],
+ ['iet', 'IET'],
+ ['LIO', 'LIO'],
+ ],
+});
+Ext.define('PVE.form.ColorPicker', {
+ extend: 'Ext.form.FieldContainer',
+ alias: 'widget.pveColorPicker',
+
+ defaultBindProperty: 'value',
+
+ config: {
+ value: null,
+ },
+
+ height: 24,
+
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+
+ getValue: function() {
+ return this.realvalue.slice(1);
+ },
+
+ setValue: function(value) {
+ let me = this;
+ me.setColor(value);
+ if (value && value.length === 6) {
+ me.picker.value = value[0] !== '#' ? `#${value}` : value;
+ }
+ },
+
+ setColor: function(value) {
+ let me = this;
+ let oldValue = me.realvalue;
+ me.realvalue = value;
+ let color = value.length === 6 ? `#${value}` : undefined;
+ me.down('#picker').setStyle('background-color', color);
+ me.down('#text').setValue(value ?? "");
+ me.fireEvent('change', me, me.realvalue, oldValue);
+ },
+
+ initComponent: function() {
+ let me = this;
+ me.picker = document.createElement('input');
+ me.picker.type = 'color';
+ me.picker.style = `opacity: 0; border: 0px; width: 100%; height: ${me.height}px`;
+ me.picker.value = `${me.value}`;
+
+ me.items = [
+ {
+ xtype: 'textfield',
+ itemId: 'text',
+ minLength: !me.allowBlank ? 6 : undefined,
+ maxLength: 6,
+ enforceMaxLength: true,
+ allowBlank: me.allowBlank,
+ emptyText: me.allowBlank ? gettext('Automatic') : undefined,
+ maskRe: /[a-f0-9]/i,
+ regex: /^[a-f0-9]{6}$/i,
+ flex: 1,
+ listeners: {
+ change: function(field, value) {
+ me.setValue(value);
+ },
+ },
+ },
+ {
+ xtype: 'box',
+ style: {
+ 'margin-left': '1px',
+ border: '1px solid #cfcfcf',
+ },
+ itemId: 'picker',
+ width: 24,
+ contentEl: me.picker,
+ },
+ ];
+
+ me.callParent();
+ me.picker.oninput = function() {
+ me.setColor(me.picker.value.slice(1));
+ };
+ },
+});
+
+Ext.define('PVE.form.TagColorGrid', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveTagColorGrid',
+
+ mixins: [
+ 'Ext.form.field.Field',
+ ],
+
+ allowBlank: true,
+ selectAll: false,
+ isFormField: true,
+ deleteEmpty: false,
+ selModel: 'checkboxmodel',
+
+ config: {
+ deleteEmpty: false,
+ },
+
+ emptyText: gettext('No Overrides'),
+ viewConfig: {
+ deferEmptyText: false,
+ },
+
+ setValue: function(value) {
+ let me = this;
+ let colors;
+ if (Ext.isObject(value)) {
+ colors = value.colors;
+ } else {
+ colors = value;
+ }
+ if (!colors) {
+ me.getStore().removeAll();
+ me.checkChange();
+ return me;
+ }
+ let entries = (colors.split(';') || []).map((entry) => {
+ let [tag, bg, fg] = entry.split(':');
+ fg = fg || "";
+ return {
+ tag,
+ color: bg,
+ text: fg,
+ };
+ });
+ me.getStore().setData(entries);
+ me.checkChange();
+ return me;
+ },
+
+ getValue: function() {
+ let me = this;
+ let values = [];
+ me.getStore().each((rec) => {
+ if (rec.data.tag) {
+ let val = `${rec.data.tag}:${rec.data.color}`;
+ if (rec.data.text) {
+ val += `:${rec.data.text}`;
+ }
+ values.push(val);
+ }
+ });
+ return values.join(';');
+ },
+
+ getErrors: function(value) {
+ let me = this;
+ let emptyTag = false;
+ let notValidColor = false;
+ let colorRegex = new RegExp(/^[0-9a-f]{6}$/i);
+ me.getStore().each((rec) => {
+ if (!rec.data.tag) {
+ emptyTag = true;
+ }
+ if (!rec.data.color?.match(colorRegex)) {
+ notValidColor = true;
+ }
+ if (rec.data.text && !rec.data.text?.match(colorRegex)) {
+ notValidColor = true;
+ }
+ });
+ let errors = [];
+ if (emptyTag) {
+ errors.push(gettext('Tag must not be empty.'));
+ }
+ if (notValidColor) {
+ errors.push(gettext('Not a valid color.'));
+ }
+ return errors;
+ },
+
+ // override framework function to implement deleteEmpty behaviour
+ getSubmitData: function() {
+ let me = this,
+ data = null,
+ val;
+ if (!me.disabled && me.submitValue) {
+ val = me.getValue();
+ if (val !== null && val !== '') {
+ data = {};
+ data[me.getName()] = val;
+ } else if (me.getDeleteEmpty()) {
+ data = {};
+ data.delete = me.getName();
+ }
+ }
+ return data;
+ },
+
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ addLine: function() {
+ let me = this;
+ me.getView().getStore().add({
+ tag: '',
+ color: '',
+ text: '',
+ });
+ },
+
+ removeSelection: function() {
+ let me = this;
+ let view = me.getView();
+ let selection = view.getSelection();
+ if (selection === undefined) {
+ return;
+ }
+
+ selection.forEach((sel) => {
+ view.getStore().remove(sel);
+ });
+ view.checkChange();
+ },
+
+ tagChange: function(field, newValue, oldValue) {
+ let me = this;
+ let rec = field.getWidgetRecord();
+ if (!rec) {
+ return;
+ }
+ if (newValue && newValue !== oldValue) {
+ let newrgb = Proxmox.Utils.stringToRGB(newValue);
+ let newvalue = Proxmox.Utils.rgbToHex(newrgb);
+ if (!rec.get('color')) {
+ rec.set('color', newvalue);
+ } else if (oldValue) {
+ let oldrgb = Proxmox.Utils.stringToRGB(oldValue);
+ let oldvalue = Proxmox.Utils.rgbToHex(oldrgb);
+ if (rec.get('color') === oldvalue) {
+ rec.set('color', newvalue);
+ }
+ }
+ }
+ me.fieldChange(field, newValue, oldValue);
+ },
+
+ backgroundChange: function(field, newValue, oldValue) {
+ let me = this;
+ let rec = field.getWidgetRecord();
+ if (!rec) {
+ return;
+ }
+ if (newValue && newValue !== oldValue) {
+ let newrgb = Proxmox.Utils.hexToRGB(newValue);
+ let newcls = Proxmox.Utils.getTextContrastClass(newrgb);
+ let hexvalue = newcls === 'dark' ? '000000' : 'FFFFFF';
+ if (!rec.get('text')) {
+ rec.set('text', hexvalue);
+ } else if (oldValue) {
+ let oldrgb = Proxmox.Utils.hexToRGB(oldValue);
+ let oldcls = Proxmox.Utils.getTextContrastClass(oldrgb);
+ let oldvalue = oldcls === 'dark' ? '000000' : 'FFFFFF';
+ if (rec.get('text') === oldvalue) {
+ rec.set('text', hexvalue);
+ }
+ }
+ }
+ me.fieldChange(field, newValue, oldValue);
+ },
+
+ fieldChange: function(field, newValue, oldValue) {
+ let me = this;
+ let view = me.getView();
+ let rec = field.getWidgetRecord();
+ if (!rec) {
+ return;
+ }
+ let column = field.getWidgetColumn();
+ rec.set(column.dataIndex, newValue);
+ view.checkChange();
+ },
+ },
+
+ tbar: [
+ {
+ text: gettext('Add'),
+ handler: 'addLine',
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Remove'),
+ handler: 'removeSelection',
+ disabled: true,
+ },
+ ],
+
+ columns: [
+ {
+ header: 'Tag',
+ dataIndex: 'tag',
+ xtype: 'widgetcolumn',
+ onWidgetAttach: function(col, widget, rec) {
+ widget.getStore().setData(PVE.UIOptions.tagList.map(v => ({ tag: v })));
+ },
+ widget: {
+ xtype: 'combobox',
+ isFormField: false,
+ maskRe: PVE.Utils.tagCharRegex,
+ allowBlank: false,
+ queryMode: 'local',
+ displayField: 'tag',
+ valueField: 'tag',
+ store: {},
+ listeners: {
+ change: 'tagChange',
+ },
+ },
+ flex: 1,
+ },
+ {
+ header: gettext('Background'),
+ xtype: 'widgetcolumn',
+ flex: 1,
+ dataIndex: 'color',
+ widget: {
+ xtype: 'pveColorPicker',
+ isFormField: false,
+ listeners: {
+ change: 'backgroundChange',
+ },
+ },
+ },
+ {
+ header: gettext('Text'),
+ xtype: 'widgetcolumn',
+ flex: 1,
+ dataIndex: 'text',
+ widget: {
+ xtype: 'pveColorPicker',
+ allowBlank: true,
+ isFormField: false,
+ listeners: {
+ change: 'fieldChange',
+ },
+ },
+ },
+ ],
+
+ store: {
+ listeners: {
+ update: function() {
+ this.commitChanges();
+ },
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+ me.callParent();
+ me.initField();
+ },
+});
+Ext.define('PVE.form.ListField', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveListField',
+
+ mixins: [
+ 'Ext.form.field.Field',
+ ],
+
+ // override for column header
+ fieldTitle: gettext('Item'),
+
+ // will be applied to the textfields
+ maskRe: undefined,
+
+ allowBlank: true,
+ selectAll: false,
+ isFormField: true,
+ deleteEmpty: false,
+ config: {
+ deleteEmpty: false,
+ },
+
+ setValue: function(list) {
+ let me = this;
+ list = Ext.isArray(list) ? list : (list ?? '').split(';').filter(t => t !== '');
+
+ let store = me.lookup('grid').getStore();
+ if (list.length > 0) {
+ store.setData(list.map(item => ({ item })));
+ } else {
+ store.removeAll();
+ }
+ me.checkChange();
+ return me;
+ },
+
+ getValue: function() {
+ let me = this;
+ let values = [];
+ me.lookup('grid').getStore().each((rec) => {
+ if (rec.data.item) {
+ values.push(rec.data.item);
+ }
+ });
+ return values.join(';');
+ },
+
+ getErrors: function(value) {
+ let me = this;
+ let empty = false;
+ me.lookup('grid').getStore().each((rec) => {
+ if (!rec.data.item) {
+ empty = true;
+ }
+ });
+ if (empty) {
+ return [gettext('Tag must not be empty.')];
+ }
+ return [];
+ },
+
+ // override framework function to implement deleteEmpty behaviour
+ getSubmitData: function() {
+ let me = this,
+ data = null,
+ val;
+ if (!me.disabled && me.submitValue) {
+ val = me.getValue();
+ if (val !== null && val !== '') {
+ data = {};
+ data[me.getName()] = val;
+ } else if (me.getDeleteEmpty()) {
+ data = {};
+ data.delete = me.getName();
+ }
+ }
+ return data;
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ addLine: function() {
+ let me = this;
+ me.lookup('grid').getStore().add({
+ item: '',
+ });
+ },
+
+ removeSelection: function(field) {
+ let me = this;
+ let view = me.getView();
+ let grid = me.lookup('grid');
+
+ let record = field.getWidgetRecord();
+ if (record === undefined) {
+ // this is sometimes called before a record/column is initialized
+ return;
+ }
+
+ grid.getStore().remove(record);
+ view.checkChange();
+ view.validate();
+ },
+
+ itemChange: function(field, newValue) {
+ let rec = field.getWidgetRecord();
+ if (!rec) {
+ return;
+ }
+ let column = field.getWidgetColumn();
+ rec.set(column.dataIndex, newValue);
+ let list = field.up('pveListField');
+ list.checkChange();
+ list.validate();
+ },
+
+ control: {
+ 'grid button': {
+ click: 'removeSelection',
+ },
+ },
+ },
+
+ items: [
+ {
+ xtype: 'grid',
+ reference: 'grid',
+
+ viewConfig: {
+ deferEmptyText: false,
+ },
+
+ store: {
+ listeners: {
+ update: function() {
+ this.commitChanges();
+ },
+ },
+ },
+ },
+ {
+ xtype: 'button',
+ text: gettext('Add'),
+ iconCls: 'fa fa-plus-circle',
+ handler: 'addLine',
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ for (const [key, value] of Object.entries(me.gridConfig ?? {})) {
+ me.items[0][key] = value;
+ }
+
+ me.items[0].columns = [
+ {
+ header: me.fieldTtitle,
+ dataIndex: 'item',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'textfield',
+ isFormField: false,
+ maskRe: me.maskRe,
+ allowBlank: false,
+ queryMode: 'local',
+ listeners: {
+ change: 'itemChange',
+ },
+ },
+ flex: 1,
+ },
+ {
+ xtype: 'widgetcolumn',
+ width: 40,
+ widget: {
+ xtype: 'button',
+ iconCls: 'fa fa-trash-o',
+ },
+ },
+ ];
+
+ me.callParent();
+ me.initField();
+ },
+});
+Ext.define('Proxmox.form.Tag', {
+ extend: 'Ext.Component',
+ alias: 'widget.pveTag',
+
+ mode: 'editable',
+
+ tag: '',
+ cls: 'pve-edit-tag',
+
+ tpl: [
+ '',
+ '{tag}',
+ '',
+ ],
+
+ // contains tags not to show in the picker and not allowing to set
+ filter: [],
+
+ updateFilter: function(tags) {
+ this.filter = tags;
+ },
+
+ onClick: function(event) {
+ let me = this;
+ if (event.target.tagName === 'I' && !event.target.classList.contains('handle')) {
+ if (me.mode === 'editable') {
+ me.destroy();
+ return;
+ }
+ } else if (event.target.tagName !== 'SPAN' || me.mode !== 'editable') {
+ return;
+ }
+ me.selectText();
+ },
+
+ selectText: function(collapseToEnd) {
+ let me = this;
+ let tagEl = me.tagEl();
+ tagEl.contentEditable = true;
+ let range = document.createRange();
+ range.selectNodeContents(tagEl);
+ if (collapseToEnd) {
+ range.collapse(false);
+ }
+ let sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+
+ me.showPicker();
+ },
+
+ showPicker: function() {
+ let me = this;
+ if (!me.picker) {
+ me.picker = Ext.widget({
+ xtype: 'boundlist',
+ minWidth: 70,
+ scrollable: true,
+ floating: true,
+ hidden: true,
+ userCls: 'proxmox-tags-full',
+ displayField: 'tag',
+ itemTpl: [
+ '{[Proxmox.Utils.getTagElement(values.tag, PVE.UIOptions.tagOverrides)]}',
+ ],
+ store: [],
+ listeners: {
+ select: function(picker, rec) {
+ me.tagEl().innerHTML = rec.data.tag;
+ me.setTag(rec.data.tag, true);
+ me.selectText(true);
+ me.setColor(rec.data.tag);
+ me.picker.hide();
+ },
+ },
+ });
+ }
+ me.picker.getStore()?.clearFilter();
+ let taglist = PVE.UIOptions.tagList.filter(v => !me.filter.includes(v)).map(v => ({ tag: v }));
+ if (taglist.length < 1) {
+ return;
+ }
+ me.picker.getStore().setData(taglist);
+ me.picker.showBy(me, 'tl-bl');
+ me.picker.setMaxHeight(200);
+ },
+
+ setMode: function(mode) {
+ let me = this;
+ let tagEl = me.tagEl();
+ if (tagEl) {
+ tagEl.contentEditable = mode === 'editable';
+ }
+ me.removeCls(me.mode);
+ me.addCls(mode);
+ me.mode = mode;
+ if (me.mode !== 'editable') {
+ me.picker?.hide();
+ }
+ },
+
+ onKeyPress: function(event) {
+ let me = this;
+ let key = event.browserEvent.key;
+ switch (key) {
+ case 'Enter':
+ case 'Escape':
+ me.fireEvent('keypress', key);
+ break;
+ case 'ArrowLeft':
+ case 'ArrowRight':
+ case 'Backspace':
+ case 'Delete':
+ return;
+ default:
+ if (key.match(PVE.Utils.tagCharRegex)) {
+ return;
+ }
+ me.setTag(me.tagEl().innerHTML);
+ }
+ event.browserEvent.preventDefault();
+ event.browserEvent.stopPropagation();
+ },
+
+ // for pasting text
+ beforeInput: function(event) {
+ let me = this;
+ me.updateLayout();
+ let tag = event.event.data ?? event.event.dataTransfer?.getData('text/plain');
+ if (!tag) {
+ return;
+ }
+ if (tag.match(PVE.Utils.tagCharRegex) === null) {
+ event.event.preventDefault();
+ event.event.stopPropagation();
+ }
+ },
+
+ onInput: function(event) {
+ let me = this;
+ me.picker.getStore().filter({
+ property: 'tag',
+ value: me.tagEl().innerHTML,
+ anyMatch: true,
+ });
+ me.setTag(me.tagEl().innerHTML);
+ },
+
+ lostFocus: function(list, event) {
+ let me = this;
+ me.picker?.hide();
+ window.getSelection().removeAllRanges();
+ },
+
+ setColor: function(tag) {
+ let me = this;
+ let rgb = PVE.UIOptions.tagOverrides[tag] ?? Proxmox.Utils.stringToRGB(tag);
+
+ let cls = Proxmox.Utils.getTextContrastClass(rgb);
+ let color = Proxmox.Utils.rgbToCss(rgb);
+ me.setUserCls(`proxmox-tag-${cls}`);
+ me.setStyle('background-color', color);
+ if (rgb.length > 3) {
+ let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]);
+
+ me.setStyle('color', fgcolor);
+ } else {
+ me.setStyle('color');
+ }
+ },
+
+ setTag: function(tag) {
+ let me = this;
+ let oldtag = me.tag;
+ me.tag = tag;
+
+ clearTimeout(me.colorTimeout);
+ me.colorTimeout = setTimeout(() => me.setColor(tag), 200);
+
+ me.updateLayout();
+ if (oldtag !== tag) {
+ me.fireEvent('change', me, tag, oldtag);
+ }
+ },
+
+ tagEl: function() {
+ return this.el?.dom?.getElementsByTagName('span')?.[0];
+ },
+
+ listeners: {
+ click: 'onClick',
+ focusleave: 'lostFocus',
+ keydown: 'onKeyPress',
+ beforeInput: 'beforeInput',
+ input: 'onInput',
+ element: 'el',
+ scope: 'this',
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.data = {
+ tag: me.tag,
+ };
+
+ me.setTag(me.tag);
+ me.setColor(me.tag);
+ me.setMode(me.mode ?? 'normal');
+ me.callParent();
+ },
+
+ destroy: function() {
+ let me = this;
+ if (me.picker) {
+ Ext.destroy(me.picker);
+ }
+ clearTimeout(me.colorTimeout);
+ me.callParent();
+ },
+});
+Ext.define('PVE.panel.TagEditContainer', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveTagEditContainer',
+
+ layout: {
+ type: 'hbox',
+ align: 'middle',
+ },
+
+ // set to false to hide the 'no tags' field and the edit button
+ canEdit: true,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ loadTags: function(tagstring = '', force = false) {
+ let me = this;
+ let view = me.getView();
+
+ if (me.oldTags === tagstring && !force) {
+ return;
+ }
+
+ view.suspendLayout = true;
+ me.forEachTag((tag) => {
+ view.remove(tag);
+ });
+ me.getViewModel().set('tagCount', 0);
+ let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || [];
+ newtags.forEach((tag) => {
+ me.addTag(tag);
+ });
+ view.suspendLayout = false;
+ view.updateLayout();
+ if (!force) {
+ me.oldTags = tagstring;
+ }
+ me.tagsChanged();
+ },
+
+ onRender: function(v) {
+ let me = this;
+ let view = me.getView();
+ view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
+
+ view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), {
+ getDragData: function(e) {
+ let source = e.getTarget('.handle');
+ if (!source) {
+ return undefined;
+ }
+ let sourceId = source.parentNode.id;
+ let cmp = Ext.getCmp(sourceId);
+ let ddel = document.createElement('div');
+ ddel.classList.add('proxmox-tags-full');
+ ddel.innerHTML = Proxmox.Utils.getTagElement(cmp.tag, PVE.UIOptions.tagOverrides);
+ let repairXY = Ext.fly(source).getXY();
+ cmp.setDisabled(true);
+ ddel.id = Ext.id();
+ return {
+ ddel,
+ repairXY,
+ sourceId,
+ };
+ },
+ onMouseUp: function(target, e, id) {
+ let cmp = Ext.getCmp(this.dragData.sourceId);
+ if (cmp && !cmp.isDestroyed) {
+ cmp.setDisabled(false);
+ }
+ },
+ getRepairXY: function() {
+ return this.dragData.repairXY;
+ },
+ beforeInvalidDrop: function(target, e, id) {
+ let cmp = Ext.getCmp(this.dragData.sourceId);
+ if (cmp && !cmp.isDestroyed) {
+ cmp.setDisabled(false);
+ }
+ },
+ });
+ view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), {
+ getTargetFromEvent: function(e) {
+ return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light');
+ },
+ getIndicator: function() {
+ if (!view.indicator) {
+ view.indicator = Ext.create('Ext.Component', {
+ floating: true,
+ html: '',
+ hidden: true,
+ shadow: false,
+ });
+ }
+ return view.indicator;
+ },
+ onContainerOver: function() {
+ this.getIndicator().setVisible(false);
+ },
+ notifyOut: function() {
+ this.getIndicator().setVisible(false);
+ },
+ onNodeOver: function(target, dd, e, data) {
+ let indicator = this.getIndicator();
+ indicator.setVisible(true);
+ indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]);
+ return this.dropAllowed;
+ },
+ onNodeDrop: function(target, dd, e, data) {
+ this.getIndicator().setVisible(false);
+ let sourceCmp = Ext.getCmp(data.sourceId);
+ if (!sourceCmp) {
+ return;
+ }
+ sourceCmp.setDisabled(false);
+ let targetCmp = Ext.getCmp(target.id);
+ view.remove(sourceCmp, { destroy: false });
+ view.insert(view.items.indexOf(targetCmp), sourceCmp);
+ me.tagsChanged();
+ },
+ });
+ },
+
+ forEachTag: function(func) {
+ let me = this;
+ let view = me.getView();
+ view.items.each((field) => {
+ if (field.getXType() === 'pveTag') {
+ func(field);
+ }
+ return true;
+ });
+ },
+
+ toggleEdit: function(cancel) {
+ let me = this;
+ let vm = me.getViewModel();
+ let view = me.getView();
+ let editMode = !vm.get('editMode');
+ vm.set('editMode', editMode);
+
+ // get a current tag list for editing
+ if (editMode) {
+ PVE.UIOptions.update();
+ }
+
+ me.forEachTag((tag) => {
+ tag.setMode(editMode ? 'editable' : 'normal');
+ });
+
+ if (!vm.get('editMode')) {
+ let tags = [];
+ if (cancel) {
+ me.loadTags(me.oldTags, true);
+ } else {
+ let toRemove = [];
+ me.forEachTag((cmp) => {
+ if (cmp.isVisible() && cmp.tag) {
+ tags.push(cmp.tag);
+ } else {
+ toRemove.push(cmp);
+ }
+ });
+ toRemove.forEach(cmp => view.remove(cmp));
+ tags = tags.join(',');
+ if (me.oldTags !== tags) {
+ me.oldTags = tags;
+ me.loadTags(tags, true);
+ me.getView().fireEvent('change', tags);
+ }
+ }
+ }
+ me.getView().updateLayout();
+ },
+
+ tagsChanged: function() {
+ let me = this;
+ let tags = [];
+ me.forEachTag(cmp => {
+ if (cmp.tag) {
+ tags.push(cmp.tag);
+ }
+ });
+ me.getViewModel().set('isDirty', me.oldTags !== tags.join(','));
+ me.forEachTag(cmp => {
+ cmp.updateFilter(tags);
+ });
+ },
+
+ addTag: function(tag, isNew) {
+ let me = this;
+ let view = me.getView();
+ let vm = me.getViewModel();
+ let index = view.items.length - 5;
+ if (PVE.UIOptions.shouldSortTags() && !isNew) {
+ index = view.items.findIndexBy(tagField => {
+ if (tagField.reference === 'noTagsField') {
+ return false;
+ }
+ if (tagField.xtype !== 'pveTag') {
+ return true;
+ }
+ let a = tagField.tag.toLowerCase();
+ let b = tag.toLowerCase();
+ return a > b ? true : a < b ? false : tagField.tag.localeCompare(tag) > 0;
+ }, 1);
+ }
+ let tagField = view.insert(index, {
+ xtype: 'pveTag',
+ tag,
+ mode: vm.get('editMode') ? 'editable' : 'normal',
+ listeners: {
+ change: 'tagsChanged',
+ destroy: function() {
+ vm.set('tagCount', vm.get('tagCount') - 1);
+ me.tagsChanged();
+ },
+ keypress: function(key) {
+ if (key === 'Enter') {
+ me.editClick();
+ } else if (key === 'Escape') {
+ me.cancelClick();
+ }
+ },
+ },
+ });
+
+ if (isNew) {
+ me.tagsChanged();
+ tagField.selectText();
+ }
+
+ vm.set('tagCount', vm.get('tagCount') + 1);
+ },
+
+ addTagClick: function(event) {
+ let me = this;
+ me.lookup('noTagsField').setVisible(false);
+ me.addTag('', true);
+ },
+
+ cancelClick: function() {
+ this.toggleEdit(true);
+ },
+
+ editClick: function() {
+ this.toggleEdit(false);
+ },
+
+ init: function(view) {
+ let me = this;
+ if (view.tags) {
+ me.loadTags(view.tags);
+ }
+ me.getViewModel().set('canEdit', view.canEdit);
+
+ me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
+ view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
+ me.loadTags(me.oldTags, true); // refresh tag colors and order
+ });
+ },
+ },
+
+ viewModel: {
+ data: {
+ tagCount: 0,
+ editMode: false,
+ canEdit: true,
+ isDirty: false,
+ },
+
+ formulas: {
+ hideNoTags: function(get) {
+ return get('tagCount') !== 0 || !get('canEdit');
+ },
+ hideEditBtn: function(get) {
+ return get('editMode') || !get('canEdit');
+ },
+ },
+ },
+
+ loadTags: function() {
+ return this.getController().loadTags(...arguments);
+ },
+
+ items: [
+ {
+ xtype: 'box',
+ reference: 'noTagsField',
+ bind: {
+ hidden: '{hideNoTags}',
+ },
+ html: gettext('No Tags'),
+ style: {
+ opacity: 0.5,
+ },
+ },
+ {
+ xtype: 'button',
+ iconCls: 'fa fa-plus',
+ tooltip: gettext('Add Tag'),
+ bind: {
+ hidden: '{!editMode}',
+ },
+ hidden: true,
+ margin: '0 8 0 5',
+ ui: 'default-toolbar',
+ handler: 'addTagClick',
+ },
+ {
+ xtype: 'tbseparator',
+ ui: 'horizontal',
+ bind: {
+ hidden: '{!editMode}',
+ },
+ hidden: true,
+ },
+ {
+ xtype: 'button',
+ iconCls: 'fa fa-times',
+ tooltip: gettext('Cancel Edit'),
+ bind: {
+ hidden: '{!editMode}',
+ },
+ hidden: true,
+ margin: '0 5 0 0',
+ ui: 'default-toolbar',
+ handler: 'cancelClick',
+ },
+ {
+ xtype: 'button',
+ iconCls: 'fa fa-check',
+ tooltip: gettext('Finish Edit'),
+ bind: {
+ hidden: '{!editMode}',
+ disabled: '{!isDirty}',
+ },
+ hidden: true,
+ handler: 'editClick',
+ },
+ {
+ xtype: 'box',
+ cls: 'pve-tag-inline-button',
+ html: ``,
+ bind: {
+ hidden: '{hideEditBtn}',
+ },
+ listeners: {
+ click: 'editClick',
+ element: 'el',
+ },
+ },
+ ],
+
+ listeners: {
+ render: 'onRender',
+ },
+
+ destroy: function() {
+ let me = this;
+ Ext.destroy(me.dragzone);
+ Ext.destroy(me.dropzone);
+ Ext.destroy(me.indicator);
+ me.callParent();
+ },
+});
+Ext.define('PVE.grid.BackupView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveBackupView'],
+
+ onlineHelp: 'chapter_vzdump',
+
+ stateful: true,
+ stateId: 'grid-guest-backup',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var vmtype = me.pveSelNode.data.type;
+ if (!vmtype) {
+ throw "no VM type specified";
+ }
+
+ var vmtypeFilter;
+ if (vmtype === 'lxc' || vmtype === 'openvz') {
+ vmtypeFilter = function(item) {
+ return PVE.Utils.volume_is_lxc_backup(item.data.volid, item.data.format);
+ };
+ } else if (vmtype === 'qemu') {
+ vmtypeFilter = function(item) {
+ return PVE.Utils.volume_is_qemu_backup(item.data.volid, item.data.format);
+ };
+ } else {
+ throw "unsupported VM type '" + vmtype + "'";
+ }
+
+ var searchFilter = {
+ property: 'volid',
+ value: '',
+ anyMatch: true,
+ caseSensitive: false,
+ };
+
+ var vmidFilter = {
+ property: 'vmid',
+ value: vmid,
+ exactMatch: true,
+ };
+
+ me.store = Ext.create('Ext.data.Store', {
+ model: 'pve-storage-content',
+ sorters: [
+ {
+ property: 'vmid',
+ direction: 'ASC',
+ },
+ {
+ property: 'vdate',
+ direction: 'DESC',
+ },
+ ],
+ filters: [
+ vmtypeFilter,
+ searchFilter,
+ vmidFilter,
+ ],
+ });
+
+ let updateFilter = function() {
+ me.store.filter([
+ vmtypeFilter,
+ searchFilter,
+ vmidFilter,
+ ]);
+ };
+
+ const reload = Ext.Function.createBuffered((options) => {
+ if (me.store) {
+ me.store.load(options);
+ }
+ }, 100);
+
+ let isPBS = false;
+ var setStorage = function(storage) {
+ var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content';
+ url += '?content=backup';
+
+ me.store.setProxy({
+ type: 'proxmox',
+ url: url,
+ });
+
+ Proxmox.Utils.monStoreErrors(me.view, me.store, true);
+
+ reload();
+ };
+
+ let file_restore_btn;
+
+ var storagesel = Ext.create('PVE.form.StorageSelector', {
+ nodename: nodename,
+ fieldLabel: gettext('Storage'),
+ labelAlign: 'right',
+ storageContent: 'backup',
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ let storage = f.getStore().findRecord('storage', value, 0, false, true, true);
+ if (storage) {
+ isPBS = storage.data.type === 'pbs';
+ me.getColumns().forEach((column) => {
+ let id = column.dataIndex;
+ if (id === 'verification' || id === 'encrypted') {
+ column.setHidden(!isPBS);
+ }
+ });
+ } else {
+ isPBS = false;
+ }
+ setStorage(value);
+ if (file_restore_btn) {
+ file_restore_btn.setHidden(!isPBS);
+ }
+ },
+ },
+ });
+
+ var storagefilter = Ext.create('Ext.form.field.Text', {
+ fieldLabel: gettext('Search'),
+ labelWidth: 50,
+ labelAlign: 'right',
+ enableKeyEvents: true,
+ value: searchFilter.value,
+ listeners: {
+ buffer: 500,
+ keyup: function(field) {
+ me.store.clearFilter(true);
+ searchFilter.value = field.getValue();
+ updateFilter();
+ },
+ },
+ });
+
+ var vmidfilterCB = Ext.create('Ext.form.field.Checkbox', {
+ boxLabel: gettext('Filter VMID'),
+ value: '1',
+ listeners: {
+ change: function(cb, value) {
+ vmidFilter.value = value ? vmid : '';
+ vmidFilter.exactMatch = !!value;
+ updateFilter();
+ },
+ },
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var backup_btn = Ext.create('Ext.button.Button', {
+ text: gettext('Backup now'),
+ handler: function() {
+ var win = Ext.create('PVE.window.Backup', {
+ nodename: nodename,
+ vmid: vmid,
+ vmtype: vmtype,
+ storage: storagesel.getValue(),
+ listeners: {
+ close: function() {
+ reload();
+ },
+ },
+ });
+ win.show();
+ },
+ });
+
+ var restore_btn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Restore'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ return !!rec;
+ },
+ handler: function(b, e, rec) {
+ let win = Ext.create('PVE.window.Restore', {
+ nodename: nodename,
+ vmid: vmid,
+ volid: rec.data.volid,
+ volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
+ vmtype: vmtype,
+ isPBS: isPBS,
+ });
+ win.show();
+ win.on('destroy', reload);
+ },
+ });
+
+ let delete_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ dangerous: true,
+ delay: 5,
+ enableFn: rec => !rec?.data?.protected,
+ confirmMsg: ({ data }) => {
+ let msg = Ext.String.format(
+ gettext('Are you sure you want to remove entry {0}'), `'${data.volid}'`);
+ return msg + " " + gettext('This will permanently erase all data.');
+ },
+ getUrl: ({ data }) => `/nodes/${nodename}/storage/${storagesel.getValue()}/content/${data.volid}`,
+ callback: () => reload(),
+ });
+
+ let config_btn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Show Configuration'),
+ disabled: true,
+ selModel: sm,
+ enableFn: rec => !!rec,
+ handler: function(b, e, rec) {
+ let storage = storagesel.getValue();
+ if (!storage) {
+ return;
+ }
+ Ext.create('PVE.window.BackupConfig', {
+ volume: rec.data.volid,
+ pveSelNode: me.pveSelNode,
+ autoShow: true,
+ });
+ },
+ });
+
+ // declared above so that the storage selector can change this buttons hidden state
+ file_restore_btn = Ext.create('Proxmox.button.Button', {
+ text: gettext('File Restore'),
+ disabled: true,
+ selModel: sm,
+ enableFn: rec => !!rec && isPBS,
+ hidden: !isPBS,
+ handler: function(b, e, rec) {
+ let storage = storagesel.getValue();
+ let isVMArchive = PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format);
+ Ext.create('Proxmox.window.FileBrowser', {
+ title: gettext('File Restore') + " - " + rec.data.text,
+ listURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/list`,
+ downloadURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/download`,
+ extraParams: {
+ volume: rec.data.volid,
+ },
+ archive: isVMArchive ? 'all' : undefined,
+ autoShow: true,
+ });
+ },
+ });
+
+ Ext.apply(me, {
+ selModel: sm,
+ tbar: {
+ overflowHandler: 'scroller',
+ items: [
+ backup_btn,
+ '-',
+ restore_btn,
+ file_restore_btn,
+ config_btn,
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit Notes'),
+ disabled: true,
+ handler: function() {
+ let volid = sm.getSelection()[0].data.volid;
+ var storage = storagesel.getValue();
+ Ext.create('Proxmox.window.Edit', {
+ autoLoad: true,
+ width: 600,
+ height: 400,
+ resizable: true,
+ title: gettext('Notes'),
+ url: `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`,
+ layout: 'fit',
+ items: [
+ {
+ xtype: 'textarea',
+ layout: 'fit',
+ name: 'notes',
+ height: '100%',
+ },
+ ],
+ listeners: {
+ destroy: () => reload(),
+ },
+ }).show();
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Change Protection'),
+ disabled: true,
+ handler: function(button, event, record) {
+ let volid = record.data.volid, storage = storagesel.getValue();
+ let url = `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`;
+ Proxmox.Utils.API2Request({
+ url: url,
+ method: 'PUT',
+ waitMsgTarget: me,
+ params: {
+ 'protected': record.data.protected ? 0 : 1,
+ },
+ failure: (response) => Ext.Msg.alert('Error', response.htmlStatus),
+ success: () => {
+ reload({
+ callback: () => sm.fireEvent('selectionchange', sm, [record]),
+ });
+ },
+ });
+ },
+ },
+ '-',
+ delete_btn,
+ '->',
+ storagesel,
+ '-',
+ vmidfilterCB,
+ storagefilter,
+ ],
+ },
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 2,
+ sortable: true,
+ renderer: PVE.Utils.render_storage_content,
+ dataIndex: 'volid',
+ },
+ {
+ header: gettext('Notes'),
+ dataIndex: 'notes',
+ flex: 1,
+ renderer: Ext.htmlEncode,
+ },
+ {
+ header: ``,
+ tooltip: gettext('Protected'),
+ width: 30,
+ renderer: v => v ? `` : '',
+ sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0),
+ dataIndex: 'protected',
+ },
+ {
+ header: gettext('Date'),
+ width: 150,
+ dataIndex: 'vdate',
+ },
+ {
+ header: gettext('Format'),
+ width: 100,
+ dataIndex: 'format',
+ },
+ {
+ header: gettext('Size'),
+ width: 100,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'size',
+ },
+ {
+ header: gettext('VMID'),
+ dataIndex: 'vmid',
+ hidden: true,
+ },
+ {
+ header: gettext('Encrypted'),
+ dataIndex: 'encrypted',
+ renderer: PVE.Utils.render_backup_encryption,
+ },
+ {
+ header: gettext('Verify State'),
+ dataIndex: 'verification',
+ renderer: PVE.Utils.render_backup_verification,
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.FirewallAliasEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ base_url: undefined,
+
+ alias_name: undefined,
+
+ width: 400,
+
+ initComponent: function() {
+ let me = this;
+
+ me.isCreate = me.alias_name === undefined;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name;
+ me.method = 'PUT';
+ }
+
+ let ipanel = Ext.create('Proxmox.panel.InputPanel', {
+ isCreate: me.isCreate,
+ items: [
+ {
+ xtype: 'textfield',
+ name: me.isCreate ? 'name' : 'rename',
+ fieldLabel: gettext('Name'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'cidr',
+ fieldLabel: gettext('IP/CIDR'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ fieldLabel: gettext('Comment'),
+ },
+ ],
+ });
+
+ Ext.apply(me, {
+ subject: gettext('Alias'),
+ isAdd: true,
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ let values = response.result.data;
+ values.rename = values.name;
+ ipanel.setValues(values);
+ },
+ });
+ }
+ },
+});
+
+Ext.define('pve-fw-aliases', {
+ extend: 'Ext.data.Model',
+
+ fields: ['name', 'cidr', 'comment', 'digest'],
+ idProperty: 'name',
+});
+
+Ext.define('PVE.FirewallAliases', {
+ extend: 'Ext.grid.Panel',
+ alias: ['widget.pveFirewallAliases'],
+
+ onlineHelp: 'pve_firewall_ip_aliases',
+
+ stateful: true,
+ stateId: 'grid-firewall-aliases',
+
+ base_url: undefined,
+
+ title: gettext('Alias'),
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.base_url) {
+ throw "missing base_url configuration";
+ }
+
+ let store = new Ext.data.Store({
+ model: 'pve-fw-aliases',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json" + me.base_url,
+ },
+ sorters: {
+ property: 'name',
+ direction: 'ASC',
+ },
+ });
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let reload = function() {
+ let oldrec = sm.getSelection()[0];
+ store.load(function(records, operation, success) {
+ if (oldrec) {
+ var rec = store.findRecord('name', oldrec.data.name, 0, false, true, true);
+ if (rec) {
+ sm.select(rec);
+ }
+ }
+ });
+ };
+
+ let run_editor = function() {
+ let rec = me.getSelectionModel().getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ let win = Ext.create('PVE.FirewallAliasEdit', {
+ base_url: me.base_url,
+ alias_name: rec.data.name,
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ me.editBtn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ 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',
+ flex: 1,
+ },
+ {
+ header: gettext('IP/CIDR'),
+ dataIndex: 'cidr',
+ flex: 1,
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 3,
+ },
+ ],
+ listeners: {
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ me.on('activate', reload);
+ },
+});
+Ext.define('PVE.FirewallOptions', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveFirewallOptions'],
+
+ fwtype: undefined, // 'dc', 'node' or 'vm'
+
+ base_url: undefined,
+
+ initComponent: function() {
+ 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: gettext('Default') + ' (enable=1,rate1/second,burst=5)',
+ editor: {
+ xtype: 'pveFirewallLograteEdit',
+ defaultValue: 'enable=1',
+ },
+ };
+ }
+
+ 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']],
+});
+Ext.define('PVE.form.FWMacroSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.pveFWMacroSelector',
+ allowBlank: true,
+ autoSelect: false,
+ valueField: 'macro',
+ displayField: 'macro',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Macro'),
+ dataIndex: 'macro',
+ hideable: false,
+ width: 100,
+ },
+ {
+ header: gettext('Description'),
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ dataIndex: 'descr',
+ },
+ ],
+ },
+ initComponent: function() {
+ var me = this;
+
+ var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ fields: ['macro', 'descr'],
+ idProperty: 'macro',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/firewall/macros",
+ },
+ sorters: {
+ property: 'macro',
+ direction: 'ASC',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.form.ICMPTypeSelector', {
+ extend: 'Proxmox.form.ComboGrid',
+ alias: 'widget.pveICMPTypeSelector',
+ allowBlank: true,
+ autoSelect: false,
+ valueField: 'name',
+ displayField: 'name',
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ hideable: false,
+ sortable: false,
+ width: 50,
+ },
+ {
+ header: gettext('Name'),
+ dataIndex: 'name',
+ hideable: false,
+ sortable: false,
+ flex: 1,
+ },
+ ],
+ },
+ setName: function(value) {
+ this.name = value;
+ },
+});
+
+let ICMP_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', {
+ field: ['type', 'name'],
+ data: [
+ { type: 'any', name: 'any' },
+ { type: '0', name: 'echo-reply' },
+ { type: '3', name: 'destination-unreachable' },
+ { type: '3/0', name: 'network-unreachable' },
+ { type: '3/1', name: 'host-unreachable' },
+ { type: '3/2', name: 'protocol-unreachable' },
+ { type: '3/3', name: 'port-unreachable' },
+ { type: '3/4', name: 'fragmentation-needed' },
+ { type: '3/5', name: 'source-route-failed' },
+ { type: '3/6', name: 'network-unknown' },
+ { type: '3/7', name: 'host-unknown' },
+ { type: '3/9', name: 'network-prohibited' },
+ { type: '3/10', name: 'host-prohibited' },
+ { type: '3/11', name: 'TOS-network-unreachable' },
+ { type: '3/12', name: 'TOS-host-unreachable' },
+ { type: '3/13', name: 'communication-prohibited' },
+ { type: '3/14', name: 'host-precedence-violation' },
+ { type: '3/15', name: 'precedence-cutoff' },
+ { type: '4', name: 'source-quench' },
+ { type: '5', name: 'redirect' },
+ { type: '5/0', name: 'network-redirect' },
+ { type: '5/1', name: 'host-redirect' },
+ { type: '5/2', name: 'TOS-network-redirect' },
+ { type: '5/3', name: 'TOS-host-redirect' },
+ { type: '8', name: 'echo-request' },
+ { type: '9', name: 'router-advertisement' },
+ { type: '10', name: 'router-solicitation' },
+ { type: '11', name: 'time-exceeded' },
+ { type: '11/0', name: 'ttl-zero-during-transit' },
+ { type: '11/1', name: 'ttl-zero-during-reassembly' },
+ { type: '12', name: 'parameter-problem' },
+ { type: '12/0', name: 'ip-header-bad' },
+ { type: '12/1', name: 'required-option-missing' },
+ { type: '13', name: 'timestamp-request' },
+ { type: '14', name: 'timestamp-reply' },
+ { type: '17', name: 'address-mask-request' },
+ { type: '18', name: 'address-mask-reply' },
+ ],
+});
+let ICMPV6_TYPE_NAMES_STORE = Ext.create('Ext.data.Store', {
+ field: ['type', 'name'],
+ data: [
+ { type: '1', name: 'destination-unreachable' },
+ { type: '1/0', name: 'no-route' },
+ { type: '1/1', name: 'communication-prohibited' },
+ { type: '1/2', name: 'beyond-scope' },
+ { type: '1/3', name: 'address-unreachable' },
+ { type: '1/4', name: 'port-unreachable' },
+ { type: '1/5', name: 'failed-policy' },
+ { type: '1/6', name: 'reject-route' },
+ { type: '2', name: 'packet-too-big' },
+ { type: '3', name: 'time-exceeded' },
+ { type: '3/0', name: 'ttl-zero-during-transit' },
+ { type: '3/1', name: 'ttl-zero-during-reassembly' },
+ { type: '4', name: 'parameter-problem' },
+ { type: '4/0', name: 'bad-header' },
+ { type: '4/1', name: 'unknown-header-type' },
+ { type: '4/2', name: 'unknown-option' },
+ { type: '128', name: 'echo-request' },
+ { type: '129', name: 'echo-reply' },
+ { type: '133', name: 'router-solicitation' },
+ { type: '134', name: 'router-advertisement' },
+ { type: '135', name: 'neighbour-solicitation' },
+ { type: '136', name: 'neighbour-advertisement' },
+ { type: '137', name: 'redirect' },
+ ],
+});
+
+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', 'icmp-type', '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'),
+ maxLength: 512,
+ maxLengthText: gettext('Too long, consider using IP sets.'),
+ },
+ {
+ xtype: 'pveIPRefSelector',
+ name: 'dest',
+ autoSelect: false,
+ editable: true,
+ base_url: me.list_refs_url,
+ value: '',
+ fieldLabel: gettext('Destination'),
+ maxLength: 512,
+ maxLengthText: gettext('Too long, consider using IP sets.'),
+ },
+ );
+
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'enable',
+ checked: false,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Enable'),
+ },
+ {
+ xtype: 'pveFWMacroSelector',
+ name: 'macro',
+ fieldLabel: gettext('Macro'),
+ editable: true,
+ allowBlank: true,
+ listeners: {
+ change: function(f, value) {
+ if (value === null) {
+ me.down('field[name=proto]').setDisabled(false);
+ me.down('field[name=sport]').setDisabled(false);
+ me.down('field[name=dport]').setDisabled(false);
+ } else {
+ me.down('field[name=proto]').setDisabled(true);
+ me.down('field[name=proto]').setValue('');
+ me.down('field[name=sport]').setDisabled(true);
+ me.down('field[name=sport]').setValue('');
+ me.down('field[name=dport]').setDisabled(true);
+ me.down('field[name=dport]').setValue('');
+ }
+ },
+ },
+ },
+ {
+ xtype: 'pveIPProtocolSelector',
+ name: 'proto',
+ autoSelect: false,
+ editable: true,
+ value: '',
+ fieldLabel: gettext('Protocol'),
+ listeners: {
+ change: function(f, value) {
+ if (value === 'icmp' || value === 'icmpv6' || value === 'ipv6-icmp') {
+ me.down('field[name=dport]').setHidden(true);
+ me.down('field[name=dport]').setDisabled(true);
+ if (value === 'icmp') {
+ me.down('#icmpv4-type').setHidden(false);
+ me.down('#icmpv4-type').setDisabled(false);
+ me.down('#icmpv6-type').setHidden(true);
+ me.down('#icmpv6-type').setDisabled(true);
+ } else {
+ me.down('#icmpv6-type').setHidden(false);
+ me.down('#icmpv6-type').setDisabled(false);
+ me.down('#icmpv4-type').setHidden(true);
+ me.down('#icmpv4-type').setDisabled(true);
+ }
+ } else {
+ me.down('#icmpv4-type').setHidden(true);
+ me.down('#icmpv4-type').setDisabled(true);
+ me.down('#icmpv6-type').setHidden(true);
+ me.down('#icmpv6-type').setDisabled(true);
+ me.down('field[name=dport]').setHidden(false);
+ me.down('field[name=dport]').setDisabled(false);
+ }
+ },
+ },
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: '',
+ height: 7,
+ value: '',
+ },
+ {
+ xtype: 'textfield',
+ name: 'sport',
+ value: '',
+ fieldLabel: gettext('Source port'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'dport',
+ value: '',
+ fieldLabel: gettext('Dest. port'),
+ },
+ {
+ xtype: 'pveICMPTypeSelector',
+ name: 'icmp-type',
+ id: 'icmpv4-type',
+ autoSelect: false,
+ editable: true,
+ hidden: true,
+ disabled: true,
+ value: '',
+ fieldLabel: gettext('ICMP type'),
+ store: ICMP_TYPE_NAMES_STORE,
+ },
+ {
+ xtype: 'pveICMPTypeSelector',
+ name: 'icmp-type',
+ id: 'icmpv6-type',
+ autoSelect: false,
+ editable: true,
+ hidden: true,
+ disabled: true,
+ value: '',
+ fieldLabel: gettext('ICMP type'),
+ store: ICMPV6_TYPE_NAMES_STORE,
+ },
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'pveFirewallLogLevels',
+ },
+ ];
+
+ me.columnB = [
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: '',
+ fieldLabel: gettext('Comment'),
+ },
+ ];
+
+ me.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);
+ // set icmp-type again after protocol has been set
+ if (values["icmp-type"] !== undefined) {
+ ipanel.setValues({ "icmp-type": values["icmp-type"] });
+ }
+ 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() {
+ 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() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ let type = rec.data.type;
+ if (!(type === 'in' || type === 'out')) {
+ return;
+ }
+
+ let 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: ({ data }) => data.type === 'in' || 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();
+ },
+ });
+
+ let tbar = me.tbar_prefix ? [me.tbar_prefix] : [];
+ tbar.push(me.addBtn, me.copyBtn);
+ if (me.groupBtn) {
+ tbar.push(me.groupBtn);
+ }
+ tbar.push(me.removeBtn, me.editBtn);
+
+ let render_errors = function(name, value, metaData, record) {
+ let errors = record.data.errors;
+ if (errors && errors[name]) {
+ metaData.tdCls = 'proxmox-invalid-row';
+ let html = '' + Ext.htmlEncode(errors[name]) + '
';
+ metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html + '"';
+ }
+ return value;
+ };
+
+ let columns = [
+ {
+ // similar to xtype: 'rownumberer',
+ dataIndex: 'pos',
+ resizable: false,
+ minWidth: 65,
+ maxWidth: 83,
+ flex: 1,
+ sortable: false,
+ hideable: false,
+ menuDisabled: true,
+ renderer: function(value, metaData, record, rowIdx, colIdx) {
+ metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
+ let dragHandle = "";
+ if (value >= 0) {
+ return dragHandle + value;
+ }
+ return dragHandle;
+ },
+ },
+ {
+ xtype: 'checkcolumn',
+ header: gettext('On'),
+ dataIndex: 'enable',
+ listeners: {
+ checkchange: function(column, recordIndex, checked) {
+ var record = me.getStore().getData().items[recordIndex];
+ record.commit();
+ var data = {};
+ Ext.Array.forEach(record.getFields(), function(field) {
+ data[field.name] = record.get(field.name);
+ });
+ if (!me.allow_iface || !data.iface) {
+ delete data.iface;
+ }
+ me.updateRule(data);
+ },
+ },
+ width: 40,
+ },
+ {
+ header: gettext('Type'),
+ dataIndex: 'type',
+ renderer: function(value, metaData, record) {
+ return render_errors('type', value, metaData, record);
+ },
+ minWidth: 60,
+ maxWidth: 80,
+ flex: 2,
+ },
+ {
+ header: gettext('Action'),
+ dataIndex: 'action',
+ renderer: function(value, metaData, record) {
+ return render_errors('action', value, metaData, record);
+ },
+ minWidth: 80,
+ maxWidth: 200,
+ flex: 2,
+ },
+ {
+ header: gettext('Macro'),
+ dataIndex: 'macro',
+ renderer: function(value, metaData, record) {
+ return render_errors('macro', value, metaData, record);
+ },
+ minWidth: 80,
+ flex: 2,
+ },
+ ];
+
+ if (me.allow_iface) {
+ columns.push({
+ header: gettext('Interface'),
+ dataIndex: 'iface',
+ renderer: function(value, metaData, record) {
+ return render_errors('iface', value, metaData, record);
+ },
+ minWidth: 80,
+ flex: 2,
+ });
+ }
+
+ columns.push(
+ {
+ header: gettext('Protocol'),
+ dataIndex: 'proto',
+ renderer: function(value, metaData, record) {
+ return render_errors('proto', value, metaData, record);
+ },
+ width: 75,
+ },
+ {
+ header: gettext('Source'),
+ dataIndex: 'source',
+ renderer: function(value, metaData, record) {
+ return render_errors('source', value, metaData, record);
+ },
+ minWidth: 100,
+ flex: 2,
+ },
+ {
+ header: gettext('S.Port'),
+ dataIndex: 'sport',
+ renderer: function(value, metaData, record) {
+ return render_errors('sport', value, metaData, record);
+ },
+ width: 75,
+ },
+ {
+ header: gettext('Destination'),
+ dataIndex: 'dest',
+ renderer: function(value, metaData, record) {
+ return render_errors('dest', value, metaData, record);
+ },
+ minWidth: 100,
+ flex: 2,
+ },
+ {
+ header: gettext('D.Port'),
+ dataIndex: 'dport',
+ renderer: function(value, metaData, record) {
+ return render_errors('dport', value, metaData, record);
+ },
+ width: 75,
+ },
+ {
+ header: gettext('Log level'),
+ dataIndex: 'log',
+ renderer: function(value, metaData, record) {
+ return render_errors('log', value, metaData, record);
+ },
+ width: 100,
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ flex: 10,
+ minWidth: 75,
+ renderer: function(value, metaData, record) {
+ let comment = render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record) || '';
+ if (comment.length * 12 > metaData.column.cellWidth) {
+ comment = `${comment}`;
+ }
+ return comment;
+ },
+ },
+ );
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: tbar,
+ viewConfig: {
+ plugins: [
+ {
+ ptype: 'gridviewdragdrop',
+ dragGroup: 'FWRuleDDGroup',
+ dropGroup: 'FWRuleDDGroup',
+ },
+ ],
+ listeners: {
+ beforedrop: function(node, data, dropRec, dropPosition) {
+ if (!dropRec) {
+ return false; // empty view
+ }
+ let moveto = dropRec.get('pos');
+ if (dropPosition === 'after') {
+ moveto++;
+ }
+ let pos = data.records[0].get('pos');
+ me.moveRule(pos, moveto);
+ return 0;
+ },
+ itemdblclick: run_editor,
+ },
+ },
+ sortableColumns: false,
+ columns: columns,
+ });
+
+ me.callParent();
+
+ if (me.base_url) {
+ me.setBaseUrl(me.base_url); // load
+ }
+ },
+}, function() {
+ Ext.define('pve-fw-rule', {
+ extend: 'Ext.data.Model',
+ fields: [
+ { name: 'enable', type: 'boolean' },
+ 'type',
+ 'action',
+ 'macro',
+ 'source',
+ 'dest',
+ 'proto',
+ 'iface',
+ 'dport',
+ 'sport',
+ 'comment',
+ 'pos',
+ 'digest',
+ 'errors',
+ ],
+ idProperty: 'pos',
+ });
+});
+Ext.define('PVE.pool.AddVM', {
+ extend: 'Proxmox.window.Edit',
+ width: 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',
+ direction: '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().filter((c) =>
+ c.dataIndex !== 'tags' && c.dataIndex !== 'lock',
+ );
+
+ 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.window.ReplicaEdit', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveReplicaEdit',
+
+ subject: gettext('Replication Job'),
+
+
+ url: '/cluster/replication',
+ method: 'POST',
+
+ initComponent: function() {
+ var me = this;
+
+ var vmid = me.pveSelNode.data.vmid;
+ var nodename = me.pveSelNode.data.node;
+
+ var items = [];
+
+ items.push({
+ xtype: me.isCreate && !vmid?'pveGuestIDSelector':'displayfield',
+ name: 'guest',
+ fieldLabel: 'CT/VM ID',
+ value: vmid || '',
+ });
+
+ items.push(
+ {
+ xtype: me.isCreate ? 'pveNodeSelector':'displayfield',
+ name: 'target',
+ disallowedNodes: [nodename],
+ allowBlank: false,
+ onlineValidator: true,
+ fieldLabel: gettext("Target"),
+ },
+ {
+ xtype: 'pveCalendarEvent',
+ fieldLabel: gettext('Schedule'),
+ emptyText: '*/15 - ' + Ext.String.format(gettext('Every {0} minutes'), 15),
+ name: 'schedule',
+ },
+ {
+ xtype: 'numberfield',
+ fieldLabel: gettext('Rate limit') + ' (MB/s)',
+ step: 1,
+ minValue: 1,
+ emptyText: gettext('unlimited'),
+ name: 'rate',
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Comment'),
+ name: 'comment',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'enabled',
+ defaultValue: 'on',
+ checked: true,
+ fieldLabel: gettext('Enabled'),
+ },
+ );
+
+ me.items = [
+ {
+ xtype: 'inputpanel',
+ itemId: 'ipanel',
+ onlineHelp: 'pvesr_schedule_time_format',
+
+ onGetValues: function(values) {
+ let win = this.up('window');
+
+ values.disable = values.enabled ? 0 : 1;
+ delete values.enabled;
+
+ PVE.Utils.delete_if_default(values, 'rate', '', win.isCreate);
+ PVE.Utils.delete_if_default(values, 'disable', 0, win.isCreate);
+ PVE.Utils.delete_if_default(values, 'schedule', '*/15', win.isCreate);
+ PVE.Utils.delete_if_default(values, 'comment', '', win.isCreate);
+
+ if (win.isCreate) {
+ values.type = 'local';
+ let vm = vmid || values.guest;
+ let id = -1;
+ if (win.highestids[vm] !== undefined) {
+ id = win.highestids[vm];
+ }
+ id++;
+ values.id = vm + '-' + id.toString();
+ delete values.guest;
+ }
+ return values;
+ },
+ items: items,
+ },
+ ];
+
+ me.callParent();
+
+ if (me.isCreate) {
+ me.load({
+ success: function(response) {
+ var jobs = response.result.data;
+ var highestids = {};
+ Ext.Array.forEach(jobs, function(job) {
+ var match = /^([0-9]+)-([0-9]+)$/.exec(job.id);
+ if (match) {
+ let jobVMID = parseInt(match[1], 10);
+ let id = parseInt(match[2], 10);
+ if (highestids[jobVMID] === undefined || highestids[jobVMID] < id) {
+ highestids[jobVMID] = id;
+ }
+ }
+ });
+ me.highestids = highestids;
+ },
+ });
+ } else {
+ me.load({
+ success: function(response, options) {
+ response.result.data.enabled = !response.result.data.disable;
+ me.setValues(response.result.data);
+ me.digest = response.result.data.digest;
+ },
+ });
+ }
+ },
+});
+
+/* callback is a function and string */
+Ext.define('PVE.grid.ReplicaView', {
+ extend: 'Ext.grid.Panel',
+ xtype: 'pveReplicaView',
+
+ onlineHelp: 'chapter_pvesr',
+
+ stateful: true,
+ stateId: 'grid-pve-replication-status',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ addJob: function(button, event, rec) {
+ let me = this;
+ let view = me.getView();
+ Ext.create('PVE.window.ReplicaEdit', {
+ isCreate: true,
+ method: 'POST',
+ pveSelNode: view.pveSelNode,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ autoShow: true,
+ });
+ },
+
+ editJob: function(button, event, { data }) {
+ let me = this;
+ let view = me.getView();
+ Ext.create('PVE.window.ReplicaEdit', {
+ url: `/cluster/replication/${data.id}`,
+ method: 'PUT',
+ pveSelNode: view.pveSelNode,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ autoShow: true,
+ });
+ },
+
+ scheduleJobNow: function(button, event, rec) {
+ let me = this;
+ let view = me.getView();
+ Proxmox.Utils.API2Request({
+ url: `/api2/extjs/nodes/${view.nodename}/replication/${rec.data.id}/schedule_now`,
+ method: 'POST',
+ waitMsgTarget: view,
+ callback: () => me.reload(),
+ failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ },
+
+ showLog: function(button, event, rec) {
+ let me = this;
+ let view = this.getView();
+
+ let logView = Ext.create('Proxmox.panel.LogView', {
+ border: false,
+ url: `/api2/extjs/nodes/${view.nodename}/replication/${rec.data.id}/log`,
+ });
+ let task = Ext.TaskManager.newTask({
+ run: () => logView.requestUpdate(),
+ interval: 1000,
+ });
+ let win = Ext.create('Ext.window.Window', {
+ items: [logView],
+ layout: 'fit',
+ width: 800,
+ height: 400,
+ modal: true,
+ title: gettext("Replication Log"),
+ listeners: {
+ destroy: function() {
+ task.stop();
+ me.reload();
+ },
+ },
+ });
+ task.start();
+ win.show();
+ },
+
+ reload: function() {
+ this.getView().rstore.load();
+ },
+
+ dblClick: function(grid, record, item) {
+ this.editJob(undefined, undefined, record);
+ },
+
+ // currently replication is for cluster only, so disable the whole component for non-cluster
+ checkPrerequisites: function() {
+ let view = this.getView();
+ if (PVE.data.ResourceStore.getNodes().length < 2) {
+ view.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']);
+ }
+ },
+
+ control: {
+ '#': {
+ itemdblclick: 'dblClick',
+ afterlayout: 'checkPrerequisites',
+ },
+ },
+ },
+
+ tbar: [
+ {
+ text: gettext('Add'),
+ itemId: 'addButton',
+ handler: 'addJob',
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ itemId: 'editButton',
+ handler: 'editJob',
+ disabled: true,
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ itemId: 'removeButton',
+ baseurl: '/api2/extjs/cluster/replication/',
+ dangerous: true,
+ callback: 'reload',
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Log'),
+ itemId: 'logButton',
+ handler: 'showLog',
+ disabled: true,
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Schedule now'),
+ itemId: 'scheduleNowButton',
+ handler: 'scheduleJobNow',
+ disabled: true,
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+ var mode = '';
+ var url = '/cluster/replication';
+
+ me.nodename = me.pveSelNode.data.node;
+ me.vmid = me.pveSelNode.data.vmid;
+
+ me.columns = [
+ {
+ header: gettext('Enabled'),
+ width: 80,
+ dataIndex: 'enabled',
+ align: 'center',
+ // TODO: switch to Proxmox.Utils.renderEnabledIcon once available
+ renderer: enabled => ``,
+ sortable: true,
+ },
+ {
+ text: 'ID',
+ dataIndex: 'id',
+ width: 60,
+ hidden: true,
+ },
+ {
+ text: gettext('Guest'),
+ dataIndex: 'guest',
+ width: 75,
+ },
+ {
+ text: gettext('Job'),
+ dataIndex: 'jobnum',
+ width: 60,
+ },
+ {
+ text: gettext('Target'),
+ dataIndex: 'target',
+ },
+ ];
+
+ if (!me.nodename) {
+ mode = 'dc';
+ me.stateId = 'grid-pve-replication-dc';
+ } else if (!me.vmid) {
+ mode = 'node';
+ url = `/nodes/${me.nodename}/replication`;
+ } else {
+ mode = 'vm';
+ url = `/nodes/${me.nodename}/replication?guest=${me.vmid}`;
+ }
+
+ if (mode !== 'dc') {
+ me.columns.push(
+ {
+ text: gettext('Status'),
+ dataIndex: 'state',
+ minWidth: 160,
+ flex: 1,
+ renderer: function(value, metadata, record) {
+ if (record.data.pid) {
+ metadata.tdCls = 'x-grid-row-loading';
+ return '';
+ }
+
+ let icons = [], states = [];
+
+ if (record.data.remove_job) {
+ icons.push('');
+ states.push(gettext("Removal Scheduled"));
+ }
+ if (record.data.error) {
+ icons.push('');
+ states.push(record.data.error);
+ }
+ if (icons.length === 0) {
+ icons.push('');
+ states.push(gettext('OK'));
+ }
+
+ return icons.join(',') + ' ' + states.join(',');
+ },
+ },
+ {
+ text: gettext('Last Sync'),
+ dataIndex: 'last_sync',
+ width: 150,
+ renderer: function(value, metadata, record) {
+ if (!value) {
+ return '-';
+ }
+ if (record.data.pid) {
+ return gettext('syncing');
+ }
+ return Proxmox.Utils.render_timestamp(value);
+ },
+ },
+ {
+ text: gettext('Duration'),
+ dataIndex: 'duration',
+ width: 60,
+ renderer: Proxmox.Utils.render_duration,
+ },
+ {
+ text: gettext('Next Sync'),
+ dataIndex: 'next_sync',
+ width: 150,
+ renderer: function(value) {
+ if (!value) {
+ return '-';
+ }
+
+ let now = new Date(), next = new Date(value * 1000);
+ if (next < now) {
+ return gettext('pending');
+ }
+ return Proxmox.Utils.render_timestamp(value);
+ },
+ },
+ );
+ }
+
+ me.columns.push(
+ {
+ text: gettext('Schedule'),
+ width: 75,
+ dataIndex: 'schedule',
+ },
+ {
+ text: gettext('Rate limit'),
+ dataIndex: 'rate',
+ renderer: function(value) {
+ if (!value) {
+ return gettext('unlimited');
+ }
+
+ return value.toString() + ' MB/s';
+ },
+ hidden: true,
+ },
+ {
+ text: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.htmlEncode,
+ },
+ );
+
+ me.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'pve-replica-' + me.nodename + me.vmid,
+ model: mode === 'dc'? 'pve-replication' : 'pve-replication-state',
+ interval: 3000,
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json" + url,
+ },
+ });
+
+ me.store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: me.rstore,
+ sorters: [
+ {
+ property: 'guest',
+ },
+ {
+ property: 'jobnum',
+ },
+ ],
+ });
+
+ me.callParent();
+
+ // we cannot access the log and scheduleNow button
+ // in the datacenter, because
+ // we do not know where/if the jobs runs
+ if (mode === 'dc') {
+ me.down('#logButton').setHidden(true);
+ me.down('#scheduleNowButton').setHidden(true);
+ }
+
+ // if we set the warning mask, we do not want to load
+ // or set the mask on store errors
+ if (PVE.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.grid.ResourceGrid', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveResourceGrid'],
+
+ border: false,
+ defaultSorter: {
+ property: 'type',
+ direction: 'ASC',
+ },
+ userCls: 'proxmox-tags-full',
+ initComponent: function() {
+ let me = this;
+
+ let rstore = PVE.data.ResourceStore;
+
+ let store = Ext.create('Ext.data.Store', {
+ model: 'PVEResources',
+ sorters: me.defaultSorter,
+ proxy: {
+ type: 'memory',
+ },
+ });
+
+ let textfilter = '';
+ let textfilterMatch = function(item) {
+ for (const field of ['name', 'storage', 'node', 'type', 'text']) {
+ let v = item.data[field];
+ if (v && v.toLowerCase().indexOf(textfilter) >= 0) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ let updateGrid = function() {
+ var filterfn = me.viewFilter ? me.viewFilter.filterfn : null;
+
+ store.suspendEvents();
+
+ let nodeidx = {};
+ let gather_child_nodes;
+ gather_child_nodes = function(node) {
+ if (!node || !node.childNodes) {
+ return;
+ }
+ for (let child of node.childNodes) {
+ let orgNode = rstore.data.get(child.data.id);
+ if (orgNode) {
+ if ((!filterfn || filterfn(child)) && (!textfilter || textfilterMatch(child))) {
+ nodeidx[child.data.id] = orgNode;
+ }
+ }
+ gather_child_nodes(child);
+ }
+ };
+ gather_child_nodes(me.pveSelNode);
+
+ // remove vanished items
+ let rmlist = [];
+ store.each(olditem => {
+ if (!nodeidx[olditem.data.id]) {
+ rmlist.push(olditem);
+ }
+ });
+ if (rmlist.length) {
+ store.remove(rmlist);
+ }
+
+ // add new items
+ let addlist = [];
+ for (const [_key, item] of Object.entries(nodeidx)) {
+ // getById() use find(), which is slow (ExtJS4 DP5)
+ let olditem = store.data.get(item.data.id);
+ if (!olditem) {
+ addlist.push(item);
+ continue;
+ }
+ let changes = false;
+ for (let field of PVE.data.ResourceStore.fieldNames) {
+ if (field !== 'id' && item.data[field] !== olditem.data[field]) {
+ changes = true;
+ olditem.beginEdit();
+ olditem.set(field, item.data[field]);
+ }
+ }
+ if (changes) {
+ olditem.endEdit(true);
+ olditem.commit(true);
+ }
+ }
+ if (addlist.length) {
+ store.add(addlist);
+ }
+ store.sort();
+ store.resumeEvents();
+ store.fireEvent('refresh', store);
+ };
+
+ Ext.apply(me, {
+ store: store,
+ stateful: true,
+ stateId: 'grid-resource',
+ tbar: [
+ '->',
+ gettext('Search') + ':', ' ',
+ {
+ xtype: 'textfield',
+ width: 200,
+ value: textfilter,
+ enableKeyEvents: true,
+ listeners: {
+ buffer: 500,
+ keyup: function(field, e) {
+ textfilter = field.getValue().toLowerCase();
+ updateGrid();
+ },
+ },
+ },
+ ],
+ viewConfig: {
+ stripeRows: true,
+ },
+ listeners: {
+ itemcontextmenu: PVE.Utils.createCmdMenu,
+ itemdblclick: function(v, record) {
+ var ws = me.up('pveStdWorkspace');
+ ws.selectById(record.data.id);
+ },
+ afterrender: function() {
+ updateGrid();
+ },
+ },
+ columns: rstore.defaultColumns(),
+ });
+ me.callParent();
+ me.mon(rstore, 'load', () => updateGrid());
+ },
+});
+/*
+ * Base class for all the multitab config panels
+ *
+ * How to use this:
+ *
+ * You create a subclass of this, and then define your wanted tabs
+ * as items like this:
+ *
+ * items: [{
+ * title: "myTitle",
+ * xytpe: "somextype",
+ * iconCls: 'fa fa-icon',
+ * groups: ['somegroup'],
+ * expandedOnInit: true,
+ * itemId: 'someId'
+ * }]
+ *
+ * this has to be in the declarative syntax, else we
+ * cannot save them for later
+ * (so no Ext.create or Ext.apply of an item in the subclass)
+ *
+ * the groups array expects the itemids of the items
+ * which are the parents, which have to come before they
+ * are used
+ *
+ * if you want following the tree:
+ *
+ * Option1
+ * Option2
+ * -> SubOption1
+ * -> SubSubOption1
+ *
+ * the suboption1 group array has to look like this:
+ * groups: ['itemid-of-option2']
+ *
+ * and of subsuboption1:
+ * groups: ['itemid-of-option2', 'itemid-of-suboption1']
+ *
+ * setting the expandedOnInit determines if the item/group is expanded
+ * initially (false by default)
+ */
+Ext.define('PVE.panel.Config', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pvePanelConfig',
+
+ showSearch: true, // add a resource grid with a search button as first tab
+ viewFilter: undefined, // a filter to pass to that resource grid
+
+ tbarSpacing: true, // if true, adds a spacer after the title in tbar
+
+ dockedItems: [{
+ // this is needed for the overflow handler
+ xtype: 'toolbar',
+ overflowHandler: 'scroller',
+ dock: 'left',
+ style: {
+ padding: 0,
+ margin: 0,
+ },
+ cls: 'pve-toolbar-bg',
+ items: {
+ xtype: 'treelist',
+ itemId: 'menu',
+ ui: 'pve-nav',
+ expanderOnly: true,
+ expanderFirst: false,
+ animation: false,
+ singleExpand: false,
+ listeners: {
+ selectionchange: function(treeList, selection) {
+ if (!selection) {
+ return;
+ }
+ let view = this.up('panel');
+ view.suspendLayout = true;
+ view.activateCard(selection.data.id);
+ view.suspendLayout = false;
+ view.updateLayout();
+ },
+ itemclick: function(treelist, info) {
+ var olditem = treelist.getSelection();
+ var newitem = info.node;
+
+ // when clicking on the expand arrow, we don't select items, but still want the original behaviour
+ if (info.select === false) {
+ return;
+ }
+
+ // click on a different, open item then leave it open, else toggle the clicked item
+ if (olditem.data.id !== newitem.data.id &&
+ newitem.data.expanded === true) {
+ info.toggle = false;
+ } else {
+ info.toggle = true;
+ }
+ },
+ },
+ },
+ },
+ {
+ xtype: 'toolbar',
+ itemId: 'toolbar',
+ dock: 'top',
+ height: 36,
+ overflowHandler: 'scroller',
+ }],
+
+ firstItem: '',
+ layout: 'card',
+ border: 0,
+
+ // used for automated test
+ selectById: function(cardid) {
+ var me = this;
+
+ var root = me.store.getRoot();
+ var selection = root.findChild('id', cardid, true);
+
+ if (selection) {
+ selection.expand();
+ var menu = me.down('#menu');
+ menu.setSelection(selection);
+ return cardid;
+ }
+ return '';
+ },
+
+ 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) {
+ let state = me.sp.get(stateid);
+ if (state && state.value) {
+ // if this tab does not exist, it chooses the first
+ activeTab = state.value;
+ }
+ }
+
+ // get title
+ var title = me.title || me.pveSelNode.data.text;
+ me.title = undefined;
+
+ // create toolbar
+ var tbar = me.tbar || [];
+ me.tbar = undefined;
+
+ if (!me.onlineHelp) {
+ // use the onlineHelp property indirection to enforce checking reference validity
+ let typeToOnlineHelp = {
+ 'type/lxc': { onlineHelp: 'chapter_pct' },
+ 'type/node': { onlineHelp: 'chapter_system_administration' },
+ 'type/pool': { onlineHelp: 'pveum_pools' },
+ 'type/qemu': { onlineHelp: 'chapter_virtual_machines' },
+ 'type/sdn': { onlineHelp: 'chapter_pvesdn' },
+ 'type/storage': { onlineHelp: 'chapter_storage' },
+ };
+ me.onlineHelp = typeToOnlineHelp[me.pveSelNode.data.id]?.onlineHelp;
+ }
+
+ if (me.tbarSpacing) {
+ tbar.unshift('->');
+ }
+ tbar.unshift({
+ xtype: 'tbtext',
+ text: title,
+ baseCls: 'x-panel-header-text',
+ });
+
+ me.helpButton = Ext.create('Proxmox.button.Help', {
+ hidden: false,
+ listenToGlobalEvent: false,
+ onlineHelp: me.onlineHelp || undefined,
+ });
+
+ tbar.push(me.helpButton);
+
+ me.dockedItems[1].items = tbar;
+
+ // include search tab
+ me.items = me.items || [];
+ if (me.showSearch) {
+ me.items.unshift({
+ xtype: 'pveResourceGrid',
+ itemId: 'search',
+ title: gettext('Search'),
+ iconCls: 'fa fa-search',
+ pveSelNode: me.pveSelNode,
+ });
+ }
+
+ me.savedItems = {};
+ if (me.items[0]) {
+ me.firstItem = me.items[0].itemId;
+ }
+
+ me.store = Ext.create('Ext.data.TreeStore', {
+ root: {
+ expanded: true,
+ },
+ });
+ var root = me.store.getRoot();
+ me.insertNodes(me.items);
+
+ delete me.items;
+ me.defaults = me.defaults || {};
+ Ext.apply(me.defaults, {
+ pveSelNode: me.pveSelNode,
+ viewFilter: me.viewFilter,
+ workspace: me.workspace,
+ border: 0,
+ });
+
+ me.callParent();
+
+ var menu = me.down('#menu');
+ var selection = root.findChild('id', activeTab, true) || root.firstChild;
+ var node = selection;
+ while (node !== root) {
+ node.expand();
+ node = node.parentNode;
+ }
+ menu.setStore(me.store);
+ menu.setSelection(selection);
+
+ // on a state change,
+ // select the new item
+ var statechange = function(sp, key, state) {
+ // it the state change is for this panel
+ if (stateid && key === stateid && state) {
+ // get active item
+ 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);
+ }
+ },
+
+ insertNodes: function(items) {
+ var me = this;
+ var root = me.store.getRoot();
+
+ items.forEach(function(item) {
+ var treeitem = Ext.create('Ext.data.TreeModel', {
+ id: item.itemId,
+ text: item.title,
+ iconCls: item.iconCls,
+ leaf: true,
+ expanded: item.expandedOnInit,
+ });
+ item.header = false;
+ if (me.savedItems[item.itemId] !== undefined) {
+ throw "itemId already exists, please use another";
+ }
+ me.savedItems[item.itemId] = item;
+
+ var group;
+ var curnode = root;
+
+ // get/create the group items
+ while (Ext.isArray(item.groups) && item.groups.length > 0) {
+ group = item.groups.shift();
+
+ 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";
+ }
+ });
+ },
+});
+/*
+ * Input panel for prune settings with a keep-all option intended to be used as
+ * part of an edit/create window.
+ */
+Ext.define('PVE.panel.BackupJobPrune', {
+ extend: 'Proxmox.panel.PruneInputPanel',
+ xtype: 'pveBackupJobPrunePanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'vzdump_retention',
+
+ onGetValues: function(formValues) {
+ if (this.needMask) { // isMasked() may not yet be true if not rendered once
+ return {};
+ } else if (this.isCreate && !this.rendered) {
+ return this.keepAllDefaultForCreate ? { 'prune-backups': 'keep-all=1' } : {};
+ }
+
+ let options = { 'delete': [] };
+
+ if ('max-protected-backups' in formValues) {
+ options['max-protected-backups'] = formValues['max-protected-backups'];
+ } else if (this.hasMaxProtected) {
+ options.delete.push('max-protected-backups');
+ }
+
+ delete formValues['max-protected-backups'];
+ delete formValues.delete;
+
+ let retention = PVE.Parser.printPropertyString(formValues);
+ if (retention === '') {
+ options.delete.push('prune-backups');
+ } else {
+ options['prune-backups'] = retention;
+ }
+
+ if (!this.isCreate) {
+ // always delete old 'maxfiles' on edit, we map it to keep-last on window load
+ options.delete.push('maxfiles');
+ } else {
+ delete options.delete;
+ }
+
+ return options;
+ },
+
+ updateComponents: function() {
+ let me = this;
+
+ let keepAll = me.down('proxmoxcheckbox[name=keep-all]').getValue();
+ let anyValue = false;
+ me.query('pmxPruneKeepField').forEach(field => {
+ anyValue = anyValue || field.getValue() !== null;
+ field.setDisabled(keepAll);
+ });
+ me.down('component[name=no-keeps-hint]').setHidden(anyValue || keepAll);
+ },
+
+ listeners: {
+ afterrender: function(panel) {
+ if (panel.needMask) {
+ panel.down('component[name=no-keeps-hint]').setHtml('');
+ panel.mask(
+ gettext('Backup content type not available for this storage.'),
+ );
+ } else if (panel.isCreate && panel.keepAllDefaultForCreate) {
+ panel.down('proxmoxcheckbox[name=keep-all]').setValue(true);
+ }
+ panel.down('component[name=pbs-hint]').setHidden(!panel.showPBSHint);
+
+ let maxProtected = panel.down('proxmoxintegerfield[name=max-protected-backups]');
+ maxProtected.setDisabled(!panel.hasMaxProtected);
+ maxProtected.setHidden(!panel.hasMaxProtected);
+
+ panel.query('pmxPruneKeepField').forEach(field => {
+ field.on('change', panel.updateComponents, panel);
+ });
+ panel.updateComponents();
+ },
+ },
+
+ columnT: {
+ xtype: 'proxmoxcheckbox',
+ name: 'keep-all',
+ boxLabel: gettext('Keep all backups'),
+ listeners: {
+ change: function(field, newValue) {
+ let panel = field.up('pveBackupJobPrunePanel');
+ panel.updateComponents();
+ },
+ },
+ },
+
+ columnB: [
+ {
+ xtype: 'component',
+ userCls: 'pmx-hint',
+ name: 'no-keeps-hint',
+ hidden: true,
+ padding: '5 1',
+ cbind: {
+ html: '{fallbackHintHtml}',
+ },
+ },
+ {
+ xtype: 'component',
+ userCls: 'pmx-hint',
+ name: 'pbs-hint',
+ hidden: true,
+ padding: '5 1',
+ html: gettext("It's preferred to configure backup retention directly on the Proxmox Backup Server."),
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'max-protected-backups',
+ fieldLabel: gettext('Maximum Protected'),
+ minValue: -1,
+ hidden: true,
+ disabled: true,
+ emptyText: 'unlimited with Datastore.Allocate privilege, 5 otherwise',
+ deleteEmpty: true,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': Ext.String.format(gettext('Use {0} for unlimited'), -1),
+ },
+ },
+ ],
+});
+Ext.define('PVE.widget.HealthWidget', {
+ extend: 'Ext.Component',
+ alias: 'widget.pveHealthWidget',
+
+ data: {
+ iconCls: PVE.Utils.get_health_icon(undefined, true),
+ text: '',
+ title: '',
+ },
+
+ style: {
+ 'text-align': 'center',
+ },
+
+ tpl: [
+ '{title}
',
+ '',
+ '
',
+ '{text}',
+ ],
+
+ updateHealth: function(data) {
+ var me = this;
+ me.update(Ext.apply(me.data, data));
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.title) {
+ me.config.data.title = me.title;
+ }
+
+ me.callParent();
+ },
+
+});
+Ext.define('pve-fw-ipsets', {
+ extend: 'Ext.data.Model',
+ fields: ['name', 'comment', 'digest'],
+ idProperty: 'name',
+});
+
+Ext.define('PVE.IPSetList', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveIPSetList',
+
+ stateful: true,
+ stateId: 'grid-firewall-ipsetlist',
+
+ ipset_panel: undefined,
+
+ base_url: undefined,
+
+ addBtn: undefined,
+ removeBtn: undefined,
+ editBtn: undefined,
+
+ initComponent: function() {
+ var me = this;
+
+ if (typeof me.ipset_panel === 'undefined') {
+ throw "no rule panel specified";
+ }
+
+ if (typeof me.ipset_panel === 'undefined') {
+ throw "no base_url specified";
+ }
+
+ var store = new Ext.data.Store({
+ model: 'pve-fw-ipsets',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json" + me.base_url,
+ },
+ sorters: {
+ property: 'name',
+ direction: 'ASC',
+ },
+ });
+
+ var 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, 0, false, true, true);
+ 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(_, 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() {
+ 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();
+ },
+});
+/*
+ * This is a running chart widget you add time datapoints to it, and we only
+ * show the last x of it used for ceph performance charts
+ */
+Ext.define('PVE.widget.RunningChart', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveRunningChart',
+
+ layout: {
+ type: 'hbox',
+ align: 'center',
+ },
+ items: [
+ {
+ width: 80,
+ xtype: 'box',
+ itemId: 'title',
+ data: {
+ title: '',
+ },
+ tpl: '{title}:
',
+ },
+ {
+ flex: 1,
+ xtype: 'cartesian',
+ height: '100%',
+ itemId: 'chart',
+ border: false,
+ axes: [
+ {
+ type: 'numeric',
+ position: 'left',
+ hidden: true,
+ minimum: 0,
+ },
+ {
+ type: 'numeric',
+ position: 'bottom',
+ hidden: true,
+ },
+ ],
+
+ store: {
+ trackRemoved: false,
+ data: {},
+ },
+
+ sprites: [{
+ id: 'valueSprite',
+ type: 'text',
+ text: '0 B/s',
+ textAlign: 'end',
+ textBaseline: 'middle',
+ fontSize: 14,
+ }],
+
+ series: [{
+ type: 'line',
+ xField: 'time',
+ yField: 'val',
+ fill: 'true',
+ colors: ['#cfcfcf'],
+ tooltip: {
+ trackMouse: true,
+ renderer: function(tooltip, record, ctx) {
+ if (!record || !record.data) return;
+ const view = this.getChart();
+ const date = new Date(record.data.time);
+ const value = view.up().renderer(record.data.val);
+ const line1 = `${view.up().title}: ${value}`;
+ const line2 = Ext.Date.format(date, 'H:i:s');
+ tooltip.setHtml(`${line1}
${line2}`);
+ },
+ },
+ style: {
+ lineWidth: 1.5,
+ opacity: 0.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,
+
+ checkThemeColors: function() {
+ let me = this;
+ let rootStyle = getComputedStyle(document.documentElement);
+
+ // get color
+ let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
+ let text = rootStyle.getPropertyValue("--pwt-text-color").trim() || "#000000";
+
+ // set the colors
+ me.chart.setBackground(background);
+ me.chart.valuesprite.setAttributes({ fillStyle: text }, true);
+ me.chart.redraw();
+ },
+
+ addDataPoint: function(value, time) {
+ let view = this.chart;
+ let panel = view.up();
+ let now = new Date().getTime();
+ let begin = new Date(now - 1000 * panel.timeFrame).getTime();
+
+ view.store.add({
+ time: time || now,
+ val: value || 0,
+ });
+
+ // delete all old records when we have 20 times more datapoints
+ // than seconds in our timeframe (so even a subsecond graph does
+ // not trigger this often)
+ //
+ // records in the store do not take much space, but like this,
+ // we prevent a memory leak when someone has the site open for a long time
+ // with minimal graphical glitches
+ if (view.store.count() > panel.timeFrame * 20) {
+ var oldData = view.store.getData().createFiltered(function(item) {
+ return item.data.time < begin;
+ });
+
+ view.store.remove(oldData.getRange());
+ }
+
+ view.timeaxis.setMinimum(begin);
+ view.timeaxis.setMaximum(now);
+ view.valuesprite.setText(panel.renderer(value || 0).toString());
+ view.valuesprite.setAttributes({
+ x: view.getWidth() - 15,
+ y: view.getHeight()/2,
+ }, true);
+ view.redraw();
+ },
+
+ setTitle: function(title) {
+ this.title = title;
+ let titlebox = this.getComponent('title');
+ titlebox.update({ title: title });
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+
+ if (me.title) {
+ me.getComponent('title').update({ title: me.title });
+ }
+ me.chart = me.getComponent('chart');
+ me.chart.timeaxis = me.chart.getAxes()[1];
+ me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite');
+ if (me.color) {
+ me.chart.series[0].setStyle({
+ fill: me.color,
+ stroke: me.color,
+ });
+ }
+
+ me.checkThemeColors();
+
+ // switch colors on media query changes
+ me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
+ me.themeListener = (e) => { me.checkThemeColors(); };
+ me.mediaQueryList.addEventListener("change", me.themeListener);
+ },
+
+ doDestroy: function() {
+ let me = this;
+
+ me.mediaQueryList.removeEventListener("change", me.themeListener);
+
+ me.callParent();
+ },
+});
+/*
+ * This class describes the bottom panel
+ */
+Ext.define('PVE.panel.StatusPanel', {
+ extend: 'Ext.tab.Panel',
+ alias: 'widget.pveStatusPanel',
+
+
+ //title: "Logs",
+ //tabPosition: 'bottom',
+
+ initComponent: function() {
+ var me = this;
+
+ var stateid = 'ltab';
+ var sp = Ext.state.Manager.getProvider();
+
+ var state = sp.get(stateid);
+ if (state && state.value) {
+ me.activeTab = state.value;
+ }
+
+ Ext.apply(me, {
+ listeners: {
+ tabchange: function() {
+ var atab = me.getActiveTab().itemId;
+ let tabstate = { value: atab };
+ sp.set(stateid, tabstate);
+ },
+ },
+ items: [
+ {
+ itemId: 'tasks',
+ title: gettext('Tasks'),
+ xtype: 'pveClusterTasks',
+ },
+ {
+ itemId: 'clog',
+ title: gettext('Cluster log'),
+ xtype: 'pveClusterLog',
+ },
+ ],
+ });
+
+ me.callParent();
+
+ me.items.get(0).fireEvent('show', me.items.get(0));
+
+ var statechange = function(_, key, newstate) {
+ if (key === stateid) {
+ var atab = me.getActiveTab().itemId;
+ let ntab = newstate.value;
+ if (newstate && ntab && atab !== ntab) {
+ me.setActiveTab(ntab);
+ }
+ }
+ };
+
+ sp.on('statechange', statechange);
+ me.on('destroy', function() {
+ sp.un('statechange', statechange);
+ });
+ },
+});
+Ext.define('PVE.panel.GuestStatusView', {
+ extend: 'Proxmox.panel.StatusView',
+ alias: 'widget.pveGuestStatusView',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ cbindData: function(initialConfig) {
+ var me = this;
+ return {
+ isQemu: me.pveSelNode.data.type === 'qemu',
+ isLxc: me.pveSelNode.data.type === 'lxc',
+ };
+ },
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+
+ defaults: {
+ xtype: 'pmxInfoWidget',
+ padding: '2 25',
+ },
+ items: [
+ {
+ xtype: 'box',
+ height: 20,
+ },
+ {
+ itemId: 'status',
+ title: gettext('Status'),
+ iconCls: 'fa fa-info fa-fw',
+ printBar: false,
+ multiField: true,
+ renderer: function(record) {
+ var me = this;
+ var text = record.data.status;
+ var qmpstatus = record.data.qmpstatus;
+ if (qmpstatus && qmpstatus !== record.data.status) {
+ text += ' (' + qmpstatus + ')';
+ }
+ return text;
+ },
+ },
+ {
+ itemId: 'hamanaged',
+ iconCls: 'fa fa-heartbeat fa-fw',
+ title: gettext('HA State'),
+ printBar: false,
+ textField: 'ha',
+ renderer: PVE.Utils.format_ha,
+ },
+ {
+ itemId: 'node',
+ iconCls: 'fa fa-building fa-fw',
+ title: gettext('Node'),
+ cbind: {
+ text: '{pveSelNode.data.node}',
+ },
+ printBar: false,
+ },
+ {
+ xtype: 'box',
+ height: 15,
+ },
+ {
+ itemId: 'cpu',
+ iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
+ title: gettext('CPU usage'),
+ valueField: 'cpu',
+ maxField: 'cpus',
+ renderer: Proxmox.Utils.render_cpu_usage,
+ // in this specific api call
+ // we already have the correct value for the usage
+ calculate: Ext.identityFn,
+ },
+ {
+ itemId: 'memory',
+ iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
+ title: gettext('Memory usage'),
+ valueField: 'mem',
+ maxField: 'maxmem',
+ },
+ {
+ itemId: 'swap',
+ iconCls: 'fa fa-refresh fa-fw',
+ title: gettext('SWAP usage'),
+ valueField: 'swap',
+ maxField: 'maxswap',
+ cbind: {
+ hidden: '{isQemu}',
+ disabled: '{isQemu}',
+ },
+ },
+ {
+ itemId: 'rootfs',
+ iconCls: 'fa fa-hdd-o fa-fw',
+ title: gettext('Bootdisk size'),
+ valueField: 'disk',
+ maxField: 'maxdisk',
+ printBar: false,
+ renderer: function(used, max) {
+ var me = this;
+ me.setPrintBar(used > 0);
+ if (used === 0) {
+ return Proxmox.Utils.render_size(max);
+ } else {
+ return Proxmox.Utils.render_size_usage(used, max);
+ }
+ },
+ },
+ {
+ xtype: 'box',
+ height: 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);
+ },
+});
+Ext.define('PVE.guest.Summary', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveGuestSummary',
+
+ scrollable: true,
+ bodyPadding: 5,
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.workspace) {
+ throw "no workspace specified";
+ }
+
+ if (!me.statusStore) {
+ throw "no status storage specified";
+ }
+
+ var type = me.pveSelNode.data.type;
+ var template = !!me.pveSelNode.data.template;
+ var rstore = me.statusStore;
+
+ var items = [
+ {
+ xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
+ flex: 1,
+ padding: template ? '5' : '0 5 0 0',
+ itemId: 'gueststatus',
+ pveSelNode: me.pveSelNode,
+ rstore: rstore,
+ },
+ {
+ xtype: 'pmxNotesView',
+ flex: 1,
+ padding: template ? '5' : '0 0 0 5',
+ itemId: 'notesview',
+ pveSelNode: me.pveSelNode,
+ },
+ ];
+
+ var rrdstore;
+ if (!template) {
+ // in non-template mode put the two panels always together
+ items = [
+ {
+ xtype: 'container',
+ height: 300,
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+ items: items,
+ },
+ ];
+
+ rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: `/api2/json/nodes/${nodename}/${type}/${vmid}/rrddata`,
+ model: 'pve-rrd-guest',
+ });
+
+ items.push(
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('CPU usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['cpu'],
+ fieldTitles: [gettext('CPU usage')],
+ unit: 'percent',
+ store: rrdstore,
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Memory usage'),
+ pveSelNode: me.pveSelNode,
+ fields: ['maxmem', 'mem'],
+ fieldTitles: [gettext('Total'), gettext('RAM usage')],
+ unit: 'bytes',
+ powerOfTwo: true,
+ 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',
+ itemId: 'itemcontainer',
+ layout: {
+ type: 'column',
+ },
+ minWidth: 700,
+ defaults: {
+ minHeight: 330,
+ padding: 5,
+ },
+ items: items,
+ listeners: {
+ resize: function(container) {
+ Proxmox.Utils.updateColumns(container);
+ },
+ },
+ },
+ ],
+ });
+
+ me.callParent();
+ if (!template) {
+ rrdstore.startUpdate();
+ me.on('destroy', rrdstore.stopUpdate);
+ }
+ let sp = Ext.state.Manager.getProvider();
+ me.mon(sp, 'statechange', function(provider, key, value) {
+ if (key !== 'summarycolumns') {
+ return;
+ }
+ Proxmox.Utils.updateColumns(me.getComponent('itemcontainer'));
+ });
+ },
+});
+Ext.define('PVE.panel.TemplateStatusView', {
+ extend: 'Proxmox.panel.StatusView',
+ alias: 'widget.pveTemplateStatusView',
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+
+ defaults: {
+ xtype: 'pmxInfoWidget',
+ printBar: false,
+ padding: '2 25',
+ },
+ items: [
+ {
+ xtype: 'box',
+ height: 20,
+ },
+ {
+ itemId: 'hamanaged',
+ iconCls: 'fa fa-heartbeat fa-fw',
+ title: gettext('HA State'),
+ printBar: false,
+ textField: 'ha',
+ renderer: PVE.Utils.format_ha,
+ },
+ {
+ itemId: 'node',
+ iconCls: 'fa fa-fw fa-building',
+ title: gettext('Node'),
+ },
+ {
+ xtype: 'box',
+ height: 20,
+ },
+ {
+ itemId: 'cpus',
+ iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
+ title: gettext('Processors'),
+ textField: 'cpus',
+ },
+ {
+ itemId: 'memory',
+ iconCls: 'fa fa-fw pmx-itype-icon-memory pmx-icon',
+ title: gettext('Memory'),
+ textField: 'maxmem',
+ renderer: Proxmox.Utils.render_size,
+ },
+ {
+ itemId: 'swap',
+ iconCls: 'fa fa-refresh fa-fw',
+ title: gettext('Swap'),
+ textField: 'maxswap',
+ renderer: Proxmox.Utils.render_size,
+ },
+ {
+ itemId: 'disk',
+ iconCls: 'fa fa-hdd-o fa-fw',
+ title: gettext('Bootdisk size'),
+ textField: 'maxdisk',
+ renderer: Proxmox.Utils.render_size,
+ },
+ {
+ xtype: 'box',
+ height: 20,
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ var name = me.pveSelNode.data.name;
+ if (!name) {
+ throw "no name specified";
+ }
+
+ me.title = name;
+
+ me.callParent();
+ if (me.pveSelNode.data.type !== 'lxc') {
+ me.remove(me.getComponent('swap'));
+ }
+ me.getComponent('node').updateValue(me.pveSelNode.data.node);
+ },
+});
+Ext.define('PVE.panel.MultiDiskPanel', {
+ extend: 'Ext.panel.Panel',
+
+ setNodename: function(nodename) {
+ this.items.each((panel) => panel.setNodename(nodename));
+ },
+
+ border: false,
+ bodyBorder: false,
+
+ layout: 'card',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ vmconfig: {},
+
+ onAdd: function() {
+ let me = this;
+ me.lookup('addButton').setDisabled(true);
+ me.addDisk();
+ let count = me.lookup('grid').getStore().getCount() + 1; // +1 is from ide2
+ me.lookup('addButton').setDisabled(count >= me.maxCount);
+ },
+
+ getNextFreeDisk: function(vmconfig) {
+ throw "implement in subclass";
+ },
+
+ addPanel: function(itemId, vmconfig, nextFreeDisk) {
+ throw "implement in subclass";
+ },
+
+ // define in subclass
+ diskSorter: undefined,
+
+ addDisk: function() {
+ let me = this;
+ let grid = me.lookup('grid');
+ let store = grid.getStore();
+
+ // get free disk id
+ let vmconfig = me.getVMConfig(true);
+ let nextFreeDisk = me.getNextFreeDisk(vmconfig);
+ if (!nextFreeDisk) {
+ return;
+ }
+
+ // add store entry + panel
+ let itemId = 'disk-card-' + ++Ext.idSeed;
+ let rec = store.add({
+ name: nextFreeDisk.confid,
+ itemId,
+ })[0];
+
+ let panel = me.addPanel(itemId, vmconfig, nextFreeDisk);
+ panel.updateVMConfig(vmconfig);
+
+ // we need to setup a validitychange handler, so that we can show
+ // that a disk has invalid fields
+ let fields = panel.query('field');
+ fields.forEach((el) => el.on('validitychange', () => {
+ let valid = fields.every((field) => field.isValid());
+ rec.set('valid', valid);
+ me.checkValidity();
+ }));
+
+ store.sort(me.diskSorter);
+
+ // select if the panel added is the only one
+ if (store.getCount() === 1) {
+ grid.getSelectionModel().select(0, false);
+ }
+ },
+
+ getBaseVMConfig: function() {
+ throw "implement in subclass";
+ },
+
+ getVMConfig: function(all) {
+ let me = this;
+
+ let vmconfig = me.getBaseVMConfig();
+
+ me.lookup('grid').getStore().each((rec) => {
+ if (all || rec.get('valid')) {
+ vmconfig[rec.get('name')] = rec.get('itemId');
+ }
+ });
+
+ return vmconfig;
+ },
+
+ checkValidity: function() {
+ let me = this;
+ let valid = me.lookup('grid').getStore().findExact('valid', false) === -1;
+ me.lookup('validationfield').setValue(valid);
+ },
+
+ updateVMConfig: function() {
+ let me = this;
+ let view = me.getView();
+ let grid = me.lookup('grid');
+ let store = grid.getStore();
+
+ let vmconfig = me.getVMConfig();
+
+ let valid = true;
+
+ store.each((rec) => {
+ let itemId = rec.get('itemId');
+ let name = rec.get('name');
+ let panel = view.getComponent(itemId);
+ if (!panel) {
+ throw "unexpected missing panel";
+ }
+
+ // copy config for each panel and remote its own id
+ let panel_vmconfig = Ext.apply({}, vmconfig);
+ if (panel_vmconfig[name] === itemId) {
+ delete panel_vmconfig[name];
+ }
+
+ if (!rec.get('valid')) {
+ valid = false;
+ }
+
+ panel.updateVMConfig(panel_vmconfig);
+ });
+
+ me.lookup('validationfield').setValue(valid);
+
+ return vmconfig;
+ },
+
+ onChange: function(panel, newVal) {
+ let me = this;
+ let store = me.lookup('grid').getStore();
+
+ let el = store.findRecord('itemId', panel.itemId, 0, false, true, true);
+ if (el.get('name') === newVal) {
+ // do not update if there was no change
+ return;
+ }
+
+ el.set('name', newVal);
+ el.commit();
+
+ store.sort(me.diskSorter);
+
+ // so that it happens after the layouting
+ setTimeout(function() {
+ me.updateVMConfig();
+ }, 10);
+ },
+
+ onRemove: function(tableview, rowIndex, colIndex, item, event, record) {
+ let me = this;
+ let grid = me.lookup('grid');
+ let store = grid.getStore();
+ let removed_idx = store.indexOf(record);
+
+ let selection = grid.getSelection()[0];
+ let selected_idx = store.indexOf(selection);
+
+ if (selected_idx === removed_idx) {
+ let newidx = store.getCount() > removed_idx + 1 ? removed_idx + 1: removed_idx - 1;
+ grid.getSelectionModel().select(newidx, false);
+ }
+
+ store.remove(record);
+ me.getView().remove(record.get('itemId'));
+ me.lookup('addButton').setDisabled(false);
+ me.updateVMConfig();
+ me.checkValidity();
+ },
+
+ onSelectionChange: function(grid, selection) {
+ let me = this;
+ if (!selection || selection.length < 1) {
+ return;
+ }
+
+ me.getView().setActiveItem(selection[0].data.itemId);
+ },
+
+ control: {
+ 'inputpanel': {
+ diskidchange: 'onChange',
+ },
+ 'grid[reference=grid]': {
+ selectionchange: 'onSelectionChange',
+ },
+ },
+
+ init: function(view) {
+ let me = this;
+ me.onAdd();
+ me.lookup('grid').getSelectionModel().select(0, false);
+ },
+ },
+
+ dockedItems: [
+ {
+ xtype: 'container',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ dock: 'left',
+ border: false,
+ width: 130,
+ items: [
+ {
+ xtype: 'grid',
+ hideHeaders: true,
+ reference: 'grid',
+ flex: 1,
+ emptyText: gettext('No Disks'),
+ margin: '0 0 5 0',
+ store: {
+ fields: ['name', 'itemId', 'valid'],
+ data: [],
+ },
+ columns: [
+ {
+ dataIndex: 'name',
+ renderer: function(val, md, rec) {
+ let warn = '';
+ if (!rec.get('valid')) {
+ warn = ' ';
+ }
+ return val + warn;
+ },
+ flex: 1,
+ },
+ {
+ xtype: 'actioncolumn',
+ width: 30,
+ align: 'center',
+ menuDisabled: true,
+ items: [
+ {
+ iconCls: 'x-fa fa-trash critical',
+ tooltip: 'Delete',
+ handler: 'onRemove',
+ isActionDisabled: 'deleteDisabled',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ xtype: 'button',
+ reference: 'addButton',
+ text: gettext('Add'),
+ iconCls: 'fa fa-plus-circle',
+ handler: 'onAdd',
+ },
+ {
+ // dummy field to control wizard validation
+ xtype: 'textfield',
+ hidden: true,
+ reference: 'validationfield',
+ submitValue: false,
+ value: true,
+ validator: (val) => !!val,
+ },
+ ],
+ },
+ ],
+});
+/*
+ * Left Treepanel, containing all the resources we manage in this datacenter: server nodes, server storages, VMs and Containers
+ */
+Ext.define('PVE.tree.ResourceTree', {
+ extend: 'Ext.tree.TreePanel',
+ alias: ['widget.pveResourceTree'],
+
+ userCls: 'proxmox-tags-circle',
+
+ statics: {
+ typeDefaults: {
+ node: {
+ iconCls: 'fa fa-building',
+ text: gettext('Nodes'),
+ },
+ pool: {
+ iconCls: 'fa fa-tags',
+ text: gettext('Resource Pool'),
+ },
+ storage: {
+ iconCls: 'fa fa-database',
+ text: gettext('Storage'),
+ },
+ sdn: {
+ iconCls: 'fa fa-th',
+ text: gettext('SDN'),
+ },
+ 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) {
+ let me = this;
+ let n1 = node1.data, n2 = node2.data;
+
+ if (!n1.groupbyid === !n2.groupbyid) {
+ let n1IsGuest = n1.type === 'qemu' || n1.type === 'lxc';
+ let n2IsGuest = n2.type === 'qemu' || n2.type === 'lxc';
+ if (me['group-guest-types'] || !n1IsGuest || !n2IsGuest) {
+ // first sort (group) by type
+ if (n1.type > n2.type) {
+ return 1;
+ } else if (n1.type < n2.type) {
+ return -1;
+ }
+ }
+
+ // then sort (group) by ID
+ if (n1IsGuest) {
+ if (me['group-templates'] && (!n1.template !== !n2.template)) {
+ return n1.template ? 1 : -1; // sort templates after regular VMs
+ }
+ if (me['sort-field'] === 'vmid') {
+ if (n1.vmid > n2.vmid) { // prefer VMID as metric for guests
+ return 1;
+ } else if (n1.vmid < n2.vmid) {
+ return -1;
+ }
+ } else {
+ return n1.name.localeCompare(n2.name);
+ }
+ }
+ // same types but not a guest
+ return n1.id > n2.id ? 1 : n1.id < n2.id ? -1 : 0;
+ } else if (n1.groupbyid) {
+ return -1;
+ } else if (n2.groupbyid) {
+ return 1;
+ }
+ return 0; // should not happen
+ },
+
+ // private: fast binary search
+ findInsertIndex: function(node, child, start, end) {
+ let me = this;
+
+ let diff = end - start;
+ if (diff <= 0) {
+ return start;
+ }
+ let mid = start + (diff >> 1);
+
+ let res = me.nodeSortFn(child, node.childNodes[mid]);
+ if (res <= 0) {
+ return me.findInsertIndex(node, child, start, mid);
+ } else {
+ return me.findInsertIndex(node, child, mid + 1, end);
+ }
+ },
+
+ setIconCls: function(info) {
+ let cls = PVE.Utils.get_object_icon_class(info.type, info);
+ if (cls !== '') {
+ info.iconCls = cls;
+ }
+ },
+
+ // add additional elements to text. Currently only the usage indicator for storages
+ setText: function(info) {
+ let me = this;
+
+ let status = '';
+ if (info.type === 'storage') {
+ let usage = info.disk / info.maxdisk;
+ if (usage >= 0.0 && usage <= 1.0) {
+ let barHeight = (usage * 100).toFixed(0);
+ let remainingHeight = (100 - barHeight).toFixed(0);
+ status = '';
+ status += `
`;
+ status += `
`;
+ status += '
';
+ }
+ }
+ if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
+ if (PVE.UIOptions.getTreeSortingValue('sort-field') !== 'vmid') {
+ info.text = `${info.name} (${String(info.vmid)})`;
+ }
+ }
+
+ info.text += PVE.Utils.renderTags(info.tags, PVE.UIOptions.tagOverrides);
+
+ info.text = status + info.text;
+ },
+
+ setToolTip: function(info) {
+ if (info.type === 'pool' || info.groupbyid !== undefined) {
+ return;
+ }
+
+ let qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)];
+ if (info.lock) {
+ qtips.push(Ext.String.format(gettext('Config locked ({0})'), info.lock));
+ }
+ if (info.hastate !== 'unmanaged') {
+ qtips.push(gettext('HA State') + ": " + info.hastate);
+ }
+
+ info.qtip = qtips.join(', ');
+ },
+
+ // private
+ addChildSorted: function(node, info) {
+ let me = this;
+
+ me.setIconCls(info);
+ me.setText(info);
+ me.setToolTip(info);
+
+ if (info.groupbyid) {
+ info.text = info.groupbyid;
+ if (info.type === 'type') {
+ let defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid];
+ if (defaults && defaults.text) {
+ info.text = defaults.text;
+ }
+ }
+ }
+ let child = Ext.create('PVETree', info);
+
+ if (node.childNodes) {
+ let pos = me.findInsertIndex(node, child, 0, node.childNodes.length);
+ node.insertBefore(child, node.childNodes[pos]);
+ } else {
+ node.insertBefore(child);
+ }
+
+ return child;
+ },
+
+ // private
+ groupChild: function(node, info, groups, level) {
+ let me = this;
+
+ let groupBy = groups[level];
+ let v = info[groupBy];
+
+ if (v) {
+ let group = node.findChild('groupbyid', v);
+ if (!group) {
+ let groupinfo;
+ if (info.type === groupBy) {
+ groupinfo = info;
+ } else {
+ groupinfo = {
+ type: groupBy,
+ id: groupBy + "/" + v,
+ };
+ if (groupBy !== 'type') {
+ groupinfo[groupBy] = v;
+ }
+ }
+ groupinfo.leaf = false;
+ groupinfo.groupbyid = v;
+ group = me.addChildSorted(node, groupinfo);
+ }
+ if (info.type === groupBy) {
+ return group;
+ }
+ if (group) {
+ return me.groupChild(group, info, groups, level + 1);
+ }
+ }
+
+ return me.addChildSorted(node, info);
+ },
+
+ saveSortingOptions: function() {
+ let me = this;
+ let changed = false;
+ for (const key of ['sort-field', 'group-templates', 'group-guest-types']) {
+ let newValue = PVE.UIOptions.getTreeSortingValue(key);
+ if (me[key] !== newValue) {
+ me[key] = newValue;
+ changed = true;
+ }
+ }
+ return changed;
+ },
+
+ initComponent: function() {
+ let me = this;
+ me.saveSortingOptions();
+
+ let rstore = PVE.data.ResourceStore;
+ let sp = Ext.state.Manager.getProvider();
+
+ if (!me.viewFilter) {
+ me.viewFilter = {};
+ }
+
+ let pdata = {
+ dataIndex: {},
+ updateCount: 0,
+ };
+
+ let store = Ext.create('Ext.data.TreeStore', {
+ model: 'PVETree',
+ root: {
+ expanded: true,
+ id: 'root',
+ text: gettext('Datacenter'),
+ iconCls: 'fa fa-server',
+ },
+ });
+
+ let stateid = 'rid';
+
+ const changedFields = [
+ 'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags',
+ ];
+
+ let updateTree = function() {
+ store.suspendEvents();
+
+ let rootnode = me.store.getRootNode();
+ // remember selected node (and all parents)
+ let sm = me.getSelectionModel();
+ let lastsel = sm.getSelection()[0];
+ let parents = [];
+ let sorting_changed = me.saveSortingOptions();
+ for (let node = lastsel; node; node = node.parentNode) {
+ parents.push(node);
+ }
+
+ let groups = me.viewFilter.groups || [];
+ // explicitly check for node/template, as those are not always grouping attributes
+ // also check for name for when the tree is sorted by name
+ let moveCheckAttrs = groups.concat(['node', 'template', 'name']);
+ let filterfn = me.viewFilter.filterfn;
+
+ let reselect = false; // for disappeared nodes
+ let index = pdata.dataIndex;
+ // remove vanished or moved items and update changed items in-place
+ for (const [key, olditem] of Object.entries(index)) {
+ // getById() use find(), which is slow (ExtJS4 DP5)
+ let item = rstore.data.get(olditem.data.id);
+
+ let changed = sorting_changed, moved = sorting_changed;
+ if (item) {
+ // test if any grouping attributes changed, catches migrated tree-nodes in server view too
+ for (const attr of moveCheckAttrs) {
+ if (item.data[attr] !== olditem.data[attr]) {
+ moved = true;
+ break;
+ }
+ }
+
+ // tree item has been updated
+ for (const field of changedFields) {
+ if (item.data[field] !== olditem.data[field]) {
+ changed = true;
+ break;
+ }
+ }
+ // FIXME: also test filterfn()?
+ }
+
+ if (changed) {
+ olditem.beginEdit();
+ let info = olditem.data;
+ Ext.apply(info, item.data);
+ me.setIconCls(info);
+ me.setText(info);
+ me.setToolTip(info);
+ olditem.commit();
+ }
+ if ((!item || moved) && olditem.isLeaf()) {
+ delete index[key];
+ let parentNode = olditem.parentNode;
+ // a selected item moved (migration) or disappeared (destroyed), so deselect that
+ // node now and try to reselect the moved (or its parent) node later
+ if (lastsel && olditem.data.id === lastsel.data.id) {
+ reselect = true;
+ sm.deselect(olditem);
+ }
+ // store events are suspended, so remove the item manually
+ store.remove(olditem);
+ parentNode.removeChild(olditem, true);
+ }
+ }
+
+ rstore.each(function(item) { // add new items
+ let olditem = index[item.data.id];
+ if (olditem) {
+ return;
+ }
+ if (filterfn && !filterfn(item)) {
+ return;
+ }
+ let info = Ext.apply({ leaf: true }, item.data);
+
+ let child = me.groupChild(rootnode, info, groups, 0);
+ if (child) {
+ index[item.data.id] = child;
+ }
+ });
+
+ store.resumeEvents();
+ store.fireEvent('refresh', store);
+
+ // select parent node if original selected node vanished
+ if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) {
+ lastsel = rootnode;
+ for (const node of parents) {
+ if (rootnode.findChild('id', node.data.id, true)) {
+ lastsel = node;
+ break;
+ }
+ }
+ me.selectById(lastsel.data.id);
+ } else if (lastsel && reselect) {
+ me.selectById(lastsel.data.id);
+ }
+
+ // on first tree load set the selection from the stateful provider
+ if (!pdata.updateCount) {
+ rootnode.expand();
+ me.applyState(sp.get(stateid));
+ }
+
+ pdata.updateCount++;
+ };
+
+ sp.on('statechange', (_sp, key, value) => {
+ if (key === stateid) {
+ me.applyState(value);
+ }
+ });
+
+ Ext.apply(me, {
+ allowSelection: true,
+ store: store,
+ viewConfig: {
+ animate: false, // note: animate cause problems with applyState
+ },
+ listeners: {
+ itemcontextmenu: PVE.Utils.createCmdMenu,
+ destroy: function() {
+ rstore.un("load", updateTree);
+ },
+ beforecellmousedown: function(tree, td, cellIndex, record, tr, rowIndex, ev) {
+ let sm = me.getSelectionModel();
+ // disable selection when right clicking except if the record is already selected
+ me.allowSelection = ev.button !== 2 || sm.isSelected(record);
+ },
+ beforeselect: function(tree, record, index, eopts) {
+ let allow = me.allowSelection;
+ me.allowSelection = true;
+ return allow;
+ },
+ itemdblclick: PVE.Utils.openTreeConsole,
+ },
+ setViewFilter: function(view) {
+ me.viewFilter = view;
+ me.clearTree();
+ updateTree();
+ },
+ setDatacenterText: function(clustername) {
+ let rootnode = me.store.getRootNode();
+
+ let rnodeText = gettext('Datacenter');
+ if (clustername !== undefined) {
+ rnodeText += ' (' + clustername + ')';
+ }
+
+ rootnode.beginEdit();
+ rootnode.data.text = rnodeText;
+ rootnode.commit();
+ },
+ clearTree: function() {
+ pdata.updateCount = 0;
+ let rootnode = me.store.getRootNode();
+ rootnode.collapse();
+ rootnode.removeAll();
+ pdata.dataIndex = {};
+ me.getSelectionModel().deselectAll();
+ },
+ selectExpand: function(node) {
+ let sm = me.getSelectionModel();
+ if (!sm.isSelected(node)) {
+ sm.select(node);
+ for (let iter = node; iter; iter = iter.parentNode) {
+ if (!iter.isExpanded()) {
+ iter.expand();
+ }
+ }
+ me.getView().focusRow(node);
+ }
+ },
+ selectById: function(nodeid) {
+ let rootnode = me.store.getRootNode();
+ let node;
+ if (nodeid === 'root') {
+ node = rootnode;
+ } else {
+ node = rootnode.findChild('id', nodeid, true);
+ }
+ if (node) {
+ me.selectExpand(node);
+ }
+ return node;
+ },
+ applyState: function(state) {
+ if (state && state.value) {
+ me.selectById(state.value);
+ } else {
+ me.getSelectionModel().deselectAll();
+ }
+ },
+ });
+
+ me.callParent();
+
+ me.getSelectionModel().on('select', (_sm, n) => sp.set(stateid, { value: n.data.id }));
+
+ rstore.on("load", updateTree);
+ rstore.startUpdate();
+ },
+
+});
+Ext.define('PVE.guest.SnapshotTree', {
+ extend: 'Ext.tree.Panel',
+ xtype: 'pveGuestSnapshotTree',
+
+ stateful: true,
+ stateId: 'grid-snapshots',
+
+ viewModel: {
+ data: {
+ // should be 'qemu' or 'lxc'
+ type: undefined,
+ nodename: undefined,
+ vmid: undefined,
+ snapshotAllowed: false,
+ rollbackAllowed: false,
+ snapshotFeature: false,
+ running: false,
+ selected: '',
+ load_delay: 3000,
+ },
+ formulas: {
+ canSnapshot: (get) => get('snapshotAllowed') && get('snapshotFeature'),
+ canRollback: (get) => get('rollbackAllowed') && get('isSnapshot'),
+ canRemove: (get) => get('snapshotAllowed') && get('isSnapshot'),
+ isSnapshot: (get) => get('selected') && get('selected') !== 'current',
+ buttonText: (get) => get('snapshotAllowed') ? gettext('Edit') : gettext('View'),
+ showMemory: (get) => get('type') === 'qemu',
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ newSnapshot: function() {
+ this.run_editor(false);
+ },
+
+ editSnapshot: function() {
+ this.run_editor(true);
+ },
+
+ run_editor: function(edit) {
+ let me = this;
+ let vm = me.getViewModel();
+ let snapname;
+ if (edit) {
+ snapname = vm.get('selected');
+ if (!snapname || snapname === 'current') { return; }
+ }
+ let win = Ext.create('PVE.window.Snapshot', {
+ nodename: vm.get('nodename'),
+ vmid: vm.get('vmid'),
+ viewonly: !vm.get('snapshotAllowed'),
+ type: vm.get('type'),
+ isCreate: !edit,
+ submitText: !edit ? gettext('Take Snapshot') : undefined,
+ snapname: snapname,
+ running: vm.get('running'),
+ });
+ win.show();
+ me.mon(win, 'destroy', me.reload, me);
+ },
+
+ snapshotAction: function(action, method) {
+ let me = this;
+ let view = me.getView();
+ let vm = me.getViewModel();
+ let snapname = vm.get('selected');
+ if (!snapname) { return; }
+
+ let nodename = vm.get('nodename');
+ let type = vm.get('type');
+ let vmid = vm.get('vmid');
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${nodename}/${type}/${vmid}/snapshot/${snapname}/${action}`,
+ method: method,
+ waitMsgTarget: view,
+ callback: function() {
+ me.reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
+ win.show();
+ },
+ });
+ },
+
+ rollback: function() {
+ this.snapshotAction('rollback', 'POST');
+ },
+ remove: function() {
+ this.snapshotAction('', 'DELETE');
+ },
+ cancel: function() {
+ this.load_task.cancel();
+ },
+
+ reload: function() {
+ let me = this;
+ let view = me.getView();
+ let vm = me.getViewModel();
+ let nodename = vm.get('nodename');
+ let vmid = vm.get('vmid');
+ let type = vm.get('type');
+ let load_delay = vm.get('load_delay');
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${nodename}/${type}/${vmid}/snapshot`,
+ method: 'GET',
+ failure: function(response, opts) {
+ if (me.destroyed) return;
+ Proxmox.Utils.setErrorMask(view, response.htmlStatus);
+ me.load_task.delay(load_delay);
+ },
+ success: function(response, opts) {
+ if (me.destroyed) {
+ // this is in a delayed task, avoid dragons if view has
+ // been destroyed already and go home.
+ return;
+ }
+ Proxmox.Utils.setErrorMask(view, false);
+ var digest = 'invalid';
+ var idhash = {};
+ var root = { name: '__root', expanded: true, children: [] };
+ Ext.Array.each(response.result.data, function(item) {
+ item.leaf = true;
+ item.children = [];
+ if (item.name === 'current') {
+ vm.set('running', !!item.running);
+ digest = item.digest + item.running;
+ item.iconCls = PVE.Utils.get_object_icon_class(vm.get('type'), item);
+ } else {
+ item.iconCls = 'fa fa-fw fa-history x-fa-tree';
+ }
+ idhash[item.name] = item;
+ });
+
+ if (digest !== me.old_digest) {
+ me.old_digest = digest;
+
+ Ext.Array.each(response.result.data, function(item) {
+ if (item.parent && idhash[item.parent]) {
+ 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.getView().setRootNode(root);
+ }
+
+ me.load_task.delay(load_delay);
+ },
+ });
+
+ // if we do not have the permissions, we don't have to check
+ // if we can create a snapshot, since the butten stays disabled
+ if (!vm.get('snapshotAllowed')) {
+ return;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${nodename}/${type}/${vmid}/feature`,
+ params: { feature: 'snapshot' },
+ method: 'GET',
+ success: function(response, options) {
+ if (me.destroyed) {
+ // this is in a delayed task, the current view could been
+ // destroyed already; then we mustn't do viemodel set
+ return;
+ }
+ let res = response.result.data;
+ vm.set('snapshotFeature', !!res.hasFeature);
+ },
+ });
+ },
+
+ select: function(grid, val) {
+ let vm = this.getViewModel();
+ if (val.length < 1) {
+ vm.set('selected', '');
+ return;
+ }
+ vm.set('selected', val[0].data.name);
+ },
+
+ init: function(view) {
+ let me = this;
+ let vm = me.getViewModel();
+ me.load_task = new Ext.util.DelayedTask(me.reload, me);
+
+ if (!view.type) {
+ throw 'guest type not set';
+ }
+ vm.set('type', view.type);
+
+ if (!view.pveSelNode.data.node) {
+ throw "no node name specified";
+ }
+ vm.set('nodename', view.pveSelNode.data.node);
+
+ if (!view.pveSelNode.data.vmid) {
+ throw "no VM ID specified";
+ }
+ vm.set('vmid', view.pveSelNode.data.vmid);
+
+ let caps = Ext.state.Manager.get('GuiCap');
+ vm.set('snapshotAllowed', !!caps.vms['VM.Snapshot']);
+ vm.set('rollbackAllowed', !!caps.vms['VM.Snapshot.Rollback']);
+
+ view.getStore().sorters.add({
+ property: 'order',
+ direction: 'ASC',
+ });
+
+ me.reload();
+ },
+ },
+
+ listeners: {
+ selectionchange: 'select',
+ itemdblclick: 'editSnapshot',
+ beforedestroy: 'cancel',
+ },
+
+ layout: 'fit',
+ rootVisible: false,
+ animate: false,
+ sortableColumns: false,
+
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Take Snapshot'),
+ disabled: true,
+ bind: {
+ disabled: "{!canSnapshot}",
+ },
+ handler: 'newSnapshot',
+ },
+ '-',
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Rollback'),
+ disabled: true,
+ bind: {
+ disabled: '{!canRollback}',
+ },
+ confirmMsg: function() {
+ let view = this.up('treepanel');
+ let rec = view.getSelection()[0];
+ let vmid = view.getViewModel().get('vmid');
+ return Proxmox.Utils.format_task_description('qmrollback', vmid) +
+ ` '${rec.data.name}'? ${gettext("Current state will be lost.")}`;
+ },
+ handler: 'rollback',
+ },
+ '-',
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ bind: {
+ text: '{buttonText}',
+ disabled: '{!isSnapshot}',
+ },
+ disabled: true,
+ edit: true,
+ handler: 'editSnapshot',
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Remove'),
+ disabled: true,
+ dangerous: true,
+ bind: {
+ disabled: '{!canRemove}',
+ },
+ confirmMsg: function() {
+ let view = this.up('treepanel');
+ let { data } = view.getSelection()[0];
+ return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${data.name}'`);
+ },
+ handler: 'remove',
+ },
+ {
+ xtype: 'label',
+ text: gettext("The current guest configuration does not support taking new snapshots"),
+ hidden: true,
+ bind: {
+ hidden: "{canSnapshot}",
+ },
+ },
+ ],
+
+ columnLines: true,
+
+ fields: [
+ 'name',
+ 'description',
+ 'snapstate',
+ 'vmstate',
+ 'running',
+ { name: 'snaptime', type: 'date', dateFormat: 'timestamp' },
+ {
+ name: 'order',
+ calculate: function(data) {
+ return data.snaptime || (data.name === 'current' ? 'ZZZ' : data.snapstate);
+ },
+ },
+ ],
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: gettext('Name'),
+ dataIndex: 'name',
+ width: 200,
+ renderer: (value, _, { data }) => data.name !== 'current' ? value : gettext('NOW'),
+ },
+ {
+ text: gettext('RAM'),
+ hidden: true,
+ bind: {
+ hidden: '{!showMemory}',
+ },
+ align: 'center',
+ resizable: false,
+ dataIndex: 'vmstate',
+ width: 50,
+ renderer: (value, _, { data }) => data.name !== 'current' ? Proxmox.Utils.format_boolean(value) : '',
+ },
+ {
+ text: gettext('Date') + "/" + gettext("Status"),
+ dataIndex: 'snaptime',
+ width: 150,
+ renderer: function(value, metaData, record) {
+ if (record.data.snapstate) {
+ return record.data.snapstate;
+ } else if (value) {
+ return Ext.Date.format(value, 'Y-m-d H:i:s');
+ }
+ return '';
+ },
+ },
+ {
+ text: gettext('Description'),
+ dataIndex: 'description',
+ flex: 1,
+ renderer: function(value, metaData, record) {
+ if (record.data.name === 'current') {
+ return gettext("You are here!");
+ } else {
+ return Ext.String.htmlEncode(value);
+ }
+ },
+ },
+ ],
+
+});
+Ext.define('PVE.window.Backup', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.vmtype) {
+ throw "no VM type specified";
+ }
+
+ let compressionSelector = Ext.create('PVE.form.CompressionSelector', {
+ name: 'compress',
+ value: 'zstd',
+ fieldLabel: gettext('Compression'),
+ });
+
+ let modeSelector = Ext.create('PVE.form.BackupModeSelector', {
+ fieldLabel: gettext('Mode'),
+ value: 'snapshot',
+ name: 'mode',
+ });
+
+ let mailtoField = Ext.create('Ext.form.field.Text', {
+ fieldLabel: gettext('Send email to'),
+ name: 'mailto',
+ emptyText: Proxmox.Utils.noneText,
+ });
+
+ const keepNames = [
+ ['keep-last', gettext('Keep Last')],
+ ['keep-hourly', gettext('Keep Hourly')],
+ ['keep-daily', gettext('Keep Daily')],
+ ['keep-weekly', gettext('Keep Weekly')],
+ ['keep-monthly', gettext('Keep Monthly')],
+ ['keep-yearly', gettext('Keep Yearly')],
+ ];
+
+ let pruneSettings = keepNames.map(
+ name => Ext.create('Ext.form.field.Display', {
+ name: name[0],
+ fieldLabel: name[1],
+ hidden: true,
+ }),
+ );
+
+ let removeCheckbox = Ext.create('Proxmox.form.Checkbox', {
+ name: 'remove',
+ checked: false,
+ hidden: true,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Prune'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Prune older backups afterwards'),
+ },
+ handler: function(checkbox, value) {
+ pruneSettings.forEach(field => field.setHidden(!value));
+ me.down('label[name="pruneLabel"]').setHidden(!value);
+ },
+ });
+
+ let initialDefaults = false;
+
+ var storagesel = Ext.create('PVE.form.StorageSelector', {
+ nodename: me.nodename,
+ name: 'storage',
+ fieldLabel: gettext('Storage'),
+ storageContent: 'backup',
+ allowBlank: false,
+ listeners: {
+ change: function(f, v) {
+ if (!initialDefaults) {
+ me.setLoading(false);
+ }
+
+ if (v === null || v === undefined || v === '') {
+ return;
+ }
+
+ let store = f.getStore();
+ let rec = store.findRecord('storage', v, 0, false, true, true);
+
+ if (rec && rec.data && rec.data.type === 'pbs') {
+ compressionSelector.setValue('zstd');
+ compressionSelector.setDisabled(true);
+ } else if (!compressionSelector.getEditable()) {
+ compressionSelector.setDisabled(false);
+ }
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${me.nodename}/vzdump/defaults`,
+ method: 'GET',
+ params: {
+ storage: v,
+ },
+ waitMsgTarget: me,
+ success: function(response, opts) {
+ const data = response.result.data;
+
+ if (!initialDefaults && data.mailto !== undefined) {
+ mailtoField.setValue(data.mailto);
+ }
+ if (!initialDefaults && data.mode !== undefined) {
+ modeSelector.setValue(data.mode);
+ }
+ if (!initialDefaults && (data['notes-template'] ?? false)) {
+ me.down('field[name=notes-template]').setValue(
+ PVE.Utils.unEscapeNotesTemplate(data['notes-template']),
+ );
+ }
+
+ initialDefaults = true;
+
+ // always update storage dependent properties
+ if (data['prune-backups'] !== undefined) {
+ const keepParams = PVE.Parser.parsePropertyString(
+ data["prune-backups"],
+ );
+ if (!keepParams['keep-all']) {
+ removeCheckbox.setHidden(false);
+ pruneSettings.forEach(function(field) {
+ const keep = keepParams[field.name];
+ if (keep) {
+ field.setValue(keep);
+ } else {
+ field.reset();
+ }
+ });
+ return;
+ }
+ }
+
+ // no defaults or keep-all=1
+ removeCheckbox.setHidden(true);
+ removeCheckbox.setValue(false);
+ pruneSettings.forEach(field => field.reset());
+ },
+ failure: function(response, opts) {
+ initialDefaults = true;
+
+ removeCheckbox.setHidden(true);
+ removeCheckbox.setValue(false);
+ pruneSettings.forEach(field => field.reset());
+
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+ },
+ });
+
+ let protectedCheckbox = Ext.create('Proxmox.form.Checkbox', {
+ name: 'protected',
+ checked: false,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Protected'),
+ });
+
+ me.formPanel = Ext.create('Proxmox.panel.InputPanel', {
+ bodyPadding: 10,
+ border: false,
+ column1: [
+ storagesel,
+ modeSelector,
+ protectedCheckbox,
+ ],
+ column2: [
+ compressionSelector,
+ mailtoField,
+ removeCheckbox,
+ ],
+ columnB: [
+ {
+ xtype: 'textareafield',
+ name: 'notes-template',
+ fieldLabel: gettext('Notes'),
+ anchor: '100%',
+ value: '{{guestname}}',
+ },
+ {
+ xtype: 'box',
+ style: {
+ margin: '8px 0px',
+ 'line-height': '1.5em',
+ },
+ html: Ext.String.format(
+ gettext('Possible template variables are: {0}'),
+ PVE.Utils.notesTemplateVars.map(v => `{{${v}}}`).join(', '),
+ ),
+ },
+ {
+ xtype: 'label',
+ name: 'pruneLabel',
+ text: gettext('Storage Retention Configuration') + ':',
+ hidden: true,
+ },
+ {
+ layout: 'hbox',
+ border: false,
+ defaults: {
+ border: false,
+ layout: 'anchor',
+ flex: 1,
+ },
+ items: [
+ {
+ padding: '0 10 0 0',
+ defaults: {
+ labelWidth: 110,
+ },
+ items: [
+ pruneSettings[0],
+ pruneSettings[2],
+ pruneSettings[4],
+ ],
+ },
+ {
+ padding: '0 0 0 10',
+ defaults: {
+ labelWidth: 110,
+ },
+ items: [
+ pruneSettings[1],
+ pruneSettings[3],
+ pruneSettings[5],
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ var submitBtn = Ext.create('Ext.Button', {
+ text: gettext('Backup'),
+ handler: function() {
+ var storage = storagesel.getValue();
+ let values = me.formPanel.getValues();
+ var params = {
+ storage: storage,
+ vmid: me.vmid,
+ mode: values.mode,
+ remove: values.remove,
+ };
+
+ if (values.mailto) {
+ params.mailto = values.mailto;
+ }
+
+ if (values.compress) {
+ params.compress = values.compress;
+ }
+
+ if (values.protected) {
+ params.protected = values.protected;
+ }
+
+ if (values['notes-template']) {
+ params['notes-template'] = PVE.Utils.escapeNotesTemplate(
+ values['notes-template']);
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/vzdump',
+ params: params,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ // close later so we reload the grid
+ // after the task has completed
+ me.hide();
+
+ var upid = response.result.data;
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid,
+ listeners: {
+ close: function() {
+ me.close();
+ },
+ },
+ });
+ win.show();
+ },
+ });
+ },
+ });
+
+ var helpBtn = Ext.create('Proxmox.button.Help', {
+ onlineHelp: 'chapter_vzdump',
+ listenToGlobalEvent: false,
+ hidden: false,
+ });
+
+ var title = gettext('Backup') + " " +
+ (me.vmtype === 'lxc' ? "CT" : "VM") +
+ " " + me.vmid;
+
+ Ext.apply(me, {
+ title: title,
+ modal: true,
+ layout: 'auto',
+ border: false,
+ width: 600,
+ items: [me.formPanel],
+ buttons: [helpBtn, '->', submitBtn],
+ listeners: {
+ afterrender: function() {
+ /// cleared within the storage selector's change listener
+ me.setLoading(gettext('Please wait...'));
+ storagesel.setValue(me.storage);
+ },
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.window.BackupConfig', {
+ extend: 'Ext.window.Window',
+ title: gettext('Configuration'),
+ width: 600,
+ height: 400,
+ layout: 'fit',
+ modal: true,
+ items: {
+ xtype: 'component',
+ itemId: 'configtext',
+ autoScroll: true,
+ style: {
+ 'white-space': 'pre',
+ 'font-family': 'monospace',
+ padding: '5px',
+ },
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.volume) {
+ throw "no volume specified";
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.callParent();
+
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + nodename + "/vzdump/extractconfig",
+ method: 'GET',
+ params: {
+ volume: me.volume,
+ },
+ failure: function(response, opts) {
+ me.close();
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, options) {
+ me.show();
+ me.down('#configtext').update(Ext.htmlEncode(response.result.data));
+ },
+ });
+ },
+});
+Ext.define('PVE.window.BulkAction', {
+ extend: 'Ext.window.Window',
+
+ resizable: true,
+ width: 800,
+ height: 600,
+ modal: true,
+ layout: {
+ type: 'fit',
+ },
+ border: false,
+
+ // the action to set, currently there are: `startall`, `migrateall`, `stopall`
+ action: undefined,
+
+ submit: function(params) {
+ let me = this;
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: `/nodes/${me.nodename}/${me.action}`,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: response => Ext.Msg.alert('Error', response.htmlStatus),
+ success: function({ result }, options) {
+ Ext.create('Proxmox.window.TaskViewer', {
+ autoShow: true,
+ upid: result.data,
+ listeners: {
+ destroy: () => me.close(),
+ },
+ });
+ me.hide();
+ },
+ });
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.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";
+ }
+
+ let items = [];
+ if (me.action === 'migrateall') {
+ 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,
+ },
+ {
+ xtype: 'fieldcontainer',
+ fieldLabel: gettext('Allow local disk migration'),
+ layout: 'hbox',
+ items: [{
+ xtype: 'proxmoxcheckbox',
+ name: 'with-local-disks',
+ checked: true,
+ uncheckedValue: 0,
+ listeners: {
+ change: (cb, val) => me.down('#localdiskwarning').setVisible(val),
+ },
+ },
+ {
+ itemId: 'localdiskwarning',
+ xtype: 'displayfield',
+ flex: 1,
+ padding: '0 0 0 10',
+ userCls: 'pmx-hint',
+ value: 'Note: Migration with local disks might take long.',
+ }],
+ },
+ {
+ itemId: 'lxcwarning',
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: 'Warning: Running CTs will be migrated in Restart Mode.',
+ hidden: true, // only visible if running container chosen
+ },
+ );
+ } else if (me.action === 'startall') {
+ items.push({
+ xtype: 'hiddenfield',
+ name: 'force',
+ value: 1,
+ });
+ } else if (me.action === 'stopall') {
+ items.push(
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'force-stop',
+ fieldLabel: gettext('Force Stop'),
+ boxLabel: gettext('Force stop guest if shutdown times out.'),
+ checked: true,
+ uncheckedValue: 0,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'timeout',
+ fieldLabel: gettext('Timeout (s)'),
+ emptyText: '180',
+ minValue: 0,
+ maxValue: 7200,
+ allowBlank: true,
+ },
+ );
+ }
+
+ 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') {
+ let showWarning = records.some(
+ item => 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: me.action === 'migrateall' ? 300 : 120,
+ anchor: '100%',
+ },
+ items: items,
+ });
+
+ let form = me.formPanel.getForm();
+
+ let submitBtn = Ext.create('Ext.Button', {
+ text: me.btnText,
+ handler: function() {
+ form.isValid();
+ me.submit(form.getValues());
+ },
+ });
+
+ Ext.apply(me, {
+ items: [me.formPanel],
+ buttons: [submitBtn],
+ });
+
+ me.callParent();
+
+ form.on('validitychange', function() {
+ let valid = form.isValid();
+ submitBtn.setDisabled(!valid);
+ });
+ form.isValid();
+ },
+});
+Ext.define('PVE.ceph.Install', {
+ extend: 'Ext.window.Window',
+ xtype: 'pveCephInstallWindow',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 220,
+ header: false,
+ resizable: false,
+ draggable: false,
+ modal: true,
+ nodename: undefined,
+ shadow: false,
+ border: false,
+ bodyBorder: false,
+ closable: false,
+ cls: 'install-mask',
+ bodyCls: 'install-mask',
+ layout: {
+ align: 'stretch',
+ pack: 'center',
+ type: 'vbox',
+ },
+ viewModel: {
+ data: {
+ isInstalled: false,
+ },
+ formulas: {
+ buttonText: function(get) {
+ if (get('isInstalled')) {
+ return gettext('Configure Ceph');
+ } else {
+ return gettext('Install Ceph');
+ }
+ },
+ windowText: function(get) {
+ if (get('isInstalled')) {
+ return `
+ ${Ext.String.format(gettext('{0} is not initialized.'), 'Ceph')}
+ ${gettext('You need to create an 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() {
+ let view = this.up('pveCephInstallWindow');
+ let wizzard = Ext.create('PVE.ceph.CephInstallWizard', {
+ nodename: view.nodename,
+ });
+ wizzard.getViewModel().set('isInstalled', this.getViewModel().get('isInstalled'));
+ wizzard.show();
+ view.mon(wizzard, 'beforeClose', function() {
+ view.fireEvent("cephInstallWindowClosed");
+ view.close();
+ });
+ },
+ },
+ ],
+});
+Ext.define('PVE.window.Clone', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ isTemplate: false,
+
+ onlineHelp: 'qm_copy_and_clone',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'panel[reference=cloneform]': {
+ validitychange: 'disableSubmit',
+ },
+ },
+ disableSubmit: function(form) {
+ this.lookupReference('submitBtn').setDisabled(!form.isValid());
+ },
+ },
+
+ statics: {
+ // display a snapshot selector only if needed
+ wrap: function(nodename, vmid, isTemplate, guestType) {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var snapshotList = response.result.data;
+ var hasSnapshots = !(snapshotList.length === 1 &&
+ snapshotList[0].name === 'current');
+
+ Ext.create('PVE.window.Clone', {
+ nodename: nodename,
+ guestType: guestType,
+ vmid: vmid,
+ 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),
+ disabled: false,
+ allowBlank: false,
+ value: me.snapname,
+ listeners: {
+ change: function(f, value) {
+ me.verifyFeature();
+ },
+ },
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ reference: 'diskselector',
+ nodename: me.nodename,
+ autoSelect: false,
+ hideSize: true,
+ hideSelection: true,
+ storageLabel: gettext('Target Storage'),
+ allowBlank: true,
+ storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir',
+ emptyText: gettext('Same as source'),
+ disabled: !!me.isTemplate, // because default mode is clone for templates
+ });
+
+ var formPanel = Ext.create('Ext.form.Panel', {
+ bodyPadding: 10,
+ reference: 'cloneform',
+ border: false,
+ layout: 'hbox',
+ defaultType: 'container',
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%',
+ },
+ items: [
+ {
+ flex: 1,
+ padding: '0 10 0 0',
+ layout: 'anchor',
+ items: col1,
+ },
+ {
+ flex: 1,
+ padding: '0 0 0 10',
+ layout: 'anchor',
+ items: col2,
+ },
+ ],
+ });
+
+ Ext.apply(me, {
+ modal: true,
+ width: 600,
+ height: 250,
+ border: false,
+ layout: 'fit',
+ buttons: [{
+ xtype: 'proxmoxHelpButton',
+ listenToGlobalEvent: false,
+ hidden: false,
+ onlineHelp: me.onlineHelp,
+ },
+ '->',
+ {
+ reference: 'submitBtn',
+ text: gettext('Clone'),
+ disabled: true,
+ handler: function() {
+ var cloneForm = me.lookupReference('cloneform');
+ if (cloneForm.isValid()) {
+ me.create_clone(cloneForm.getValues());
+ }
+ },
+ }],
+ items: [formPanel],
+ });
+
+ me.callParent();
+
+ me.verifyFeature();
+ },
+});
+Ext.define('PVE.FirewallEnableEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pveFirewallEnableEdit'],
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ subject: gettext('Firewall'),
+ cbindData: {
+ defaultValue: 0,
+ },
+ width: 350,
+
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'enable',
+ uncheckedValue: 0,
+ cbind: {
+ defaultValue: '{defaultValue}',
+ checked: '{defaultValue}',
+ },
+ deleteDefaultValue: false,
+ fieldLabel: gettext('Firewall'),
+ },
+ {
+ xtype: 'displayfield',
+ name: 'warning',
+ userCls: 'pmx-hint',
+ value: gettext('Warning: Firewall still disabled at datacenter level!'),
+ hidden: true,
+ },
+ ],
+
+ beforeShow: function() {
+ var me = this;
+
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/cluster/firewall/options',
+ method: 'GET',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, opts) {
+ if (!response.result.data.enable) {
+ me.down('displayfield[name=warning]').setVisible(true);
+ }
+ },
+ });
+ },
+});
+Ext.define('PVE.FirewallLograteInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveFirewallLograteInputPanel',
+
+ viewModel: {},
+
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'enable',
+ reference: 'enable',
+ fieldLabel: gettext('Enable'),
+ value: true,
+ },
+ {
+ layout: 'hbox',
+ border: false,
+ items: [
+ {
+ xtype: 'numberfield',
+ name: 'rate',
+ fieldLabel: gettext('Log rate limit'),
+ minValue: 1,
+ maxValue: 99,
+ allowBlank: false,
+ flex: 2,
+ value: 1,
+ },
+ {
+ xtype: 'box',
+ html: '/
',
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'unit',
+ comboItems: [
+ ['second', 'second'],
+ ['minute', 'minute'],
+ ['hour', 'hour'],
+ ['day', 'day'],
+ ],
+ allowBlank: false,
+ flex: 1,
+ value: 'second',
+ },
+ ],
+ },
+ {
+ xtype: 'numberfield',
+ name: 'burst',
+ fieldLabel: gettext('Log burst limit'),
+ minValue: 1,
+ maxValue: 99,
+ value: 5,
+ },
+ ],
+
+ onGetValues: function(values) {
+ let me = this;
+
+ let cfg = {
+ enable: values.enable !== undefined ? 1 : 0,
+ rate: values.rate + '/' + values.unit,
+ burst: values.burst,
+ };
+ let properties = PVE.Parser.printPropertyString(cfg, undefined);
+ if (properties === '') {
+ return { 'delete': 'log_ratelimit' };
+ }
+ return { log_ratelimit: properties };
+ },
+
+ setValues: function(values) {
+ let me = this;
+
+ let properties = {};
+ if (values.log_ratelimit !== undefined) {
+ properties = PVE.Parser.parsePropertyString(values.log_ratelimit, 'enable');
+ if (properties.rate) {
+ 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,
+});
+/*global u2f*/
+Ext.define('PVE.window.LoginWindow', {
+ extend: 'Ext.window.Window',
+
+ viewModel: {
+ data: {
+ openid: false,
+ },
+ formulas: {
+ button_text: function(get) {
+ if (get("openid") === true) {
+ return gettext("Login (OpenID redirect)");
+ } else {
+ return gettext("Login");
+ }
+ },
+ },
+ },
+
+ controller: {
+
+ xclass: 'Ext.app.ViewController',
+
+ onLogon: async function() {
+ var me = this;
+
+ var form = this.lookupReference('loginForm');
+ var unField = this.lookupReference('usernameField');
+ var saveunField = this.lookupReference('saveunField');
+ var view = this.getView();
+
+ if (!form.isValid()) {
+ return;
+ }
+
+ let creds = form.getValues();
+
+ if (this.getViewModel().data.openid === true) {
+ const redirectURL = location.origin;
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/access/openid/auth-url',
+ params: {
+ realm: creds.realm,
+ "redirect-url": redirectURL,
+ },
+ method: 'POST',
+ success: function(resp, opts) {
+ window.location = resp.result.data;
+ },
+ failure: function(resp, opts) {
+ Proxmox.Utils.authClear();
+ form.unmask();
+ Ext.MessageBox.alert(
+ gettext('Error'),
+ gettext('OpenID redirect failed.') + `
${resp.htmlStatus}`,
+ );
+ },
+ });
+ return;
+ }
+
+ view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+
+ // set or clear username
+ var sp = Ext.state.Manager.getProvider();
+ if (saveunField.getValue() === true) {
+ sp.set(unField.getStateId(), unField.getValue());
+ } else {
+ sp.clear(unField.getStateId());
+ }
+ sp.set(saveunField.getStateId(), saveunField.getValue());
+
+ try {
+ // Request updated authentication mechanism:
+ creds['new-format'] = 1;
+
+ let resp = await Proxmox.Async.api2({
+ url: '/api2/extjs/access/ticket',
+ params: creds,
+ method: 'POST',
+ });
+
+ let data = resp.result.data;
+ if (data.ticket.startsWith("PVE:!tfa!")) {
+ // Store first factor login information first:
+ data.LoggedOut = true;
+ Proxmox.Utils.setAuthData(data);
+
+ data = await me.performTFAChallenge(data);
+
+ // Fill in what we copy over from the 1st factor:
+ data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
+ data.username = Proxmox.UserName;
+ me.success(data);
+ } else if (Ext.isDefined(data.NeedTFA)) {
+ // Store first factor login information first:
+ data.LoggedOut = true;
+ Proxmox.Utils.setAuthData(data);
+
+ if (Ext.isDefined(data.U2FChallenge)) {
+ me.perform_u2f(data);
+ } else {
+ me.perform_otp();
+ }
+ } else {
+ me.success(data);
+ }
+ } catch (error) {
+ me.failure(error);
+ }
+ },
+
+ /* START NEW TFA CODE (pbs copy) */
+ performTFAChallenge: async function(data) {
+ let me = this;
+
+ let userid = data.username;
+ let ticket = data.ticket;
+ let challenge = JSON.parse(decodeURIComponent(
+ ticket.split(':')[1].slice("!tfa!".length),
+ ));
+
+ let resp = await new Promise((resolve, reject) => {
+ Ext.create('Proxmox.window.TfaLoginWindow', {
+ userid,
+ ticket,
+ challenge,
+ onResolve: value => resolve(value),
+ onReject: reject,
+ }).show();
+ });
+
+ return resp.result.data;
+ },
+ /* END NEW TFA CODE (pbs copy) */
+
+ failure: function(resp) {
+ var me = this;
+ var view = me.getView();
+ view.el.unmask();
+ var handler = function() {
+ var uf = me.lookupReference('usernameField');
+ uf.focus(true, true);
+ };
+
+ let emsg = gettext("Login failed. Please try again");
+
+ if (resp.failureType === "connect") {
+ emsg = gettext("Connection failure. Network error or Proxmox VE services not running?");
+ }
+
+ Ext.MessageBox.alert(gettext('Error'), emsg, handler);
+ },
+ success: function(data) {
+ var me = this;
+ var view = me.getView();
+ var handler = view.handler || Ext.emptyFn;
+ handler.call(me, data);
+ view.close();
+ },
+
+ perform_otp: function() {
+ var me = this;
+ var win = Ext.create('PVE.window.TFALoginWindow', {
+ onLogin: function(value) {
+ me.finish_tfa(value);
+ },
+ onCancel: function() {
+ Proxmox.LoggedOut = false;
+ Proxmox.Utils.authClear();
+ me.getView().show();
+ },
+ });
+ win.show();
+ },
+
+ perform_u2f: function(data) {
+ var me = this;
+ // Show the message:
+ var msg = Ext.Msg.show({
+ title: 'U2F: '+gettext('Verification'),
+ message: gettext('Please press the button on your U2F Device'),
+ buttons: [],
+ });
+ var chlg = data.U2FChallenge;
+ var key = {
+ version: chlg.version,
+ keyHandle: chlg.keyHandle,
+ };
+ u2f.sign(chlg.appId, chlg.challenge, [key], function(res) {
+ msg.close();
+ if (res.errorCode) {
+ Proxmox.Utils.authClear();
+ Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode));
+ return;
+ }
+ delete res.errorCode;
+ me.finish_tfa(JSON.stringify(res));
+ });
+ },
+ finish_tfa: function(res) {
+ var me = this;
+ var view = me.getView();
+ view.el.mask(gettext('Please wait...'), 'x-mask-loading');
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/access/tfa',
+ params: {
+ response: res,
+ },
+ method: 'POST',
+ timeout: 5000, // it'll delay both success & failure
+ success: function(resp, opts) {
+ view.el.unmask();
+ // Fill in what we copy over from the 1st factor:
+ var data = resp.result.data;
+ data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
+ data.username = Proxmox.UserName;
+ // Finish logging in:
+ me.success(data);
+ },
+ failure: function(resp, opts) {
+ Proxmox.Utils.authClear();
+ me.failure(resp);
+ },
+ });
+ },
+
+ control: {
+ 'field[name=username]': {
+ specialkey: function(f, e) {
+ if (e.getKey() === e.ENTER) {
+ 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();
+ },
+ },
+ 'field[name=realm]': {
+ change: function(f, value) {
+ let record = f.store.getById(value);
+ if (record === undefined) return;
+ let data = record.data;
+ this.getViewModel().set("openid", data.type === "openid");
+ },
+ },
+ 'button[reference=loginButton]': {
+ click: 'onLogon',
+ },
+ '#': {
+ show: function() {
+ var me = this;
+
+ var sp = Ext.state.Manager.getProvider();
+ var checkboxField = this.lookupReference('saveunField');
+ var unField = this.lookupReference('usernameField');
+
+ var checked = sp.get(checkboxField.getStateId());
+ checkboxField.setValue(checked);
+
+ if (checked === true) {
+ var username = sp.get(unField.getStateId());
+ unField.setValue(username);
+ var pwField = this.lookupReference('passwordField');
+ pwField.focus();
+ }
+
+ let auth = Proxmox.Utils.getOpenIDRedirectionAuthorization();
+ if (auth !== undefined) {
+ Proxmox.Utils.authClear();
+
+ let loginForm = this.lookupReference('loginForm');
+ loginForm.mask(gettext('OpenID login - please wait...'), 'x-mask-loading');
+
+ const redirectURL = location.origin;
+
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/access/openid/login',
+ params: {
+ state: auth.state,
+ code: auth.code,
+ "redirect-url": redirectURL,
+ },
+ method: 'POST',
+ failure: function(response) {
+ loginForm.unmask();
+ let error = response.htmlStatus;
+ Ext.MessageBox.alert(
+ gettext('Error'),
+ gettext('OpenID login failed, please try again') + `
${error}`,
+ () => { window.location = redirectURL; },
+ );
+ },
+ success: function(response, options) {
+ loginForm.unmask();
+ let data = response.result.data;
+ history.replaceState(null, '', redirectURL);
+ me.success(data);
+ },
+ });
+ }
+ },
+ },
+ },
+ },
+
+ width: 400,
+ modal: true,
+ border: false,
+ draggable: true,
+ closable: false,
+ resizable: false,
+ layout: 'auto',
+
+ title: gettext('Proxmox VE Login'),
+
+ defaultFocus: 'usernameField',
+ defaultButton: 'loginButton',
+
+ items: [{
+ xtype: 'form',
+ layout: 'form',
+ url: '/api2/extjs/access/ticket',
+ reference: 'loginForm',
+
+ fieldDefaults: {
+ labelAlign: 'right',
+ allowBlank: false,
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('User name'),
+ name: 'username',
+ itemId: 'usernameField',
+ reference: 'usernameField',
+ stateId: 'login-username',
+ bind: {
+ visible: "{!openid}",
+ disabled: "{openid}",
+ },
+ },
+ {
+ xtype: 'textfield',
+ inputType: 'password',
+ fieldLabel: gettext('Password'),
+ name: 'password',
+ reference: 'passwordField',
+ bind: {
+ visible: "{!openid}",
+ disabled: "{openid}",
+ },
+ },
+ {
+ xtype: 'pmxRealmComboBox',
+ 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: 250,
+ labelAlign: 'right',
+ submitValue: false,
+ bind: {
+ visible: "{!openid}",
+ },
+ },
+ {
+ bind: {
+ text: "{button_text}",
+ },
+ reference: 'loginButton',
+ },
+ ],
+ }],
+ });
+Ext.define('PVE.window.Migrate', {
+ extend: 'Ext.window.Window',
+
+ vmtype: undefined,
+ nodename: undefined,
+ vmid: undefined,
+ maxHeight: 450,
+
+ viewModel: {
+ data: {
+ vmid: undefined,
+ nodename: undefined,
+ vmtype: undefined,
+ running: false,
+ qemu: {
+ onlineHelp: 'qm_migration',
+ commonName: 'VM',
+ },
+ lxc: {
+ onlineHelp: 'pct_migration',
+ commonName: 'CT',
+ },
+ migration: {
+ possible: true,
+ preconditions: [],
+ 'with-local-disks': 0,
+ mode: undefined,
+ allowedNodes: undefined,
+ overwriteLocalResourceCheck: false,
+ hasLocalResources: false,
+ },
+
+ },
+
+ formulas: {
+ setMigrationMode: function(get) {
+ if (get('running')) {
+ if (get('vmtype') === 'qemu') {
+ return gettext('Online');
+ } else {
+ return gettext('Restart Mode');
+ }
+ } else {
+ return gettext('Offline');
+ }
+ },
+ setStorageselectorHidden: function(get) {
+ if (get('migration.with-local-disks') && get('running')) {
+ return false;
+ } else {
+ return true;
+ }
+ },
+ setLocalResourceCheckboxHidden: function(get) {
+ if (get('running') || !get('migration.hasLocalResources') ||
+ Proxmox.UserName !== 'root@pam') {
+ return true;
+ } else {
+ return false;
+ }
+ },
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'panel[reference=formPanel]': {
+ validityChange: function(panel, isValid) {
+ this.getViewModel().set('migration.possible', isValid);
+ this.checkMigratePreconditions();
+ },
+ },
+ },
+
+ init: function(view) {
+ var me = this,
+ vm = view.getViewModel();
+
+ if (!view.nodename) {
+ throw "missing custom view config: nodename";
+ }
+ vm.set('nodename', view.nodename);
+
+ if (!view.vmid) {
+ throw "missing custom view config: vmid";
+ }
+ vm.set('vmid', view.vmid);
+
+ if (!view.vmtype) {
+ throw "missing custom view config: vmtype";
+ }
+ vm.set('vmtype', view.vmtype);
+
+ view.setTitle(
+ Ext.String.format('{0} {1} {2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid),
+ );
+ me.lookup('proxmoxHelpButton').setHelpConfig({
+ onlineHelp: vm.get(view.vmtype).onlineHelp,
+ });
+ me.lookup('formPanel').isValid();
+ },
+
+ onTargetChange: function(nodeSelector) {
+ // Always display the storages of the currently seleceted migration target
+ this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
+ this.checkMigratePreconditions();
+ },
+
+ startMigration: function() {
+ var me = this,
+ view = me.getView(),
+ vm = me.getViewModel();
+
+ var values = me.lookup('formPanel').getValues();
+ var params = {
+ target: values.target,
+ };
+
+ if (vm.get('migration.mode')) {
+ params[vm.get('migration.mode')] = 1;
+ }
+ if (vm.get('migration.with-local-disks')) {
+ params['with-local-disks'] = 1;
+ }
+ //offline migration to a different storage currently might fail at a late stage
+ //(i.e. after some disks have been moved), so don't expose it yet in the GUI
+ if (vm.get('migration.with-local-disks') && vm.get('running') && values.targetstorage) {
+ params.targetstorage = values.targetstorage;
+ }
+
+ if (vm.get('migration.overwriteLocalResourceCheck')) {
+ params.force = 1;
+ }
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
+ waitMsgTarget: view,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+ var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target);
+
+ Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid,
+ extraTitle: extraTitle,
+ }).show();
+
+ view.close();
+ },
+ });
+ },
+
+ checkMigratePreconditions: function(resetMigrationPossible) {
+ var me = this,
+ vm = me.getViewModel();
+
+ var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'),
+ 0, false, false, true);
+ if (vmrec && vmrec.data && vmrec.data.running) {
+ vm.set('running', true);
+ }
+
+ if (vm.get('vmtype') === 'qemu') {
+ me.checkQemuPreconditions(resetMigrationPossible);
+ } else {
+ me.checkLxcPreconditions(resetMigrationPossible);
+ }
+ me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
+
+ // Only allow nodes where the local storage is available in case of offline migration
+ // where storage migration is not possible
+ me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes');
+
+ me.lookup('formPanel').isValid();
+ },
+
+ checkQemuPreconditions: async function(resetMigrationPossible) {
+ let me = this,
+ vm = me.getViewModel(),
+ migrateStats;
+
+ if (vm.get('running')) {
+ vm.set('migration.mode', 'online');
+ }
+
+ try {
+ if (me.fetchingNodeMigrateInfo && me.fetchingNodeMigrateInfo === vm.get('nodename')) {
+ return;
+ }
+ me.fetchingNodeMigrateInfo = vm.get('nodename');
+ let { result } = await Proxmox.Async.api2({
+ url: `/nodes/${vm.get('nodename')}/${vm.get('vmtype')}/${vm.get('vmid')}/migrate`,
+ method: 'GET',
+ });
+ migrateStats = result.data;
+ me.fetchingNodeMigrateInfo = false;
+ } catch (error) {
+ Ext.Msg.alert(gettext('Error'), error.htmlStatus);
+ return;
+ }
+
+ if (migrateStats.running) {
+ vm.set('running', true);
+ }
+ // Get migration object from viewmodel to prevent to many bind callbacks
+ let migration = vm.get('migration');
+ if (resetMigrationPossible) {
+ migration.possible = true;
+ }
+ migration.preconditions = [];
+
+ if (migrateStats.allowed_nodes) {
+ migration.allowedNodes = migrateStats.allowed_nodes;
+ let target = me.lookup('pveNodeSelector').value;
+ if (target.length && !migrateStats.allowed_nodes.includes(target)) {
+ let disallowed = migrateStats.not_allowed_nodes[target];
+ let missingStorages = disallowed.unavailable_storages.join(', ');
+
+ migration.possible = false;
+ migration.preconditions.push({
+ text: 'Storage (' + missingStorages + ') not available on selected target. ' +
+ 'Start VM to use live storage migration or select other target node',
+ severity: 'error',
+ });
+ }
+ }
+
+ if (migrateStats.local_resources.length) {
+ migration.hasLocalResources = true;
+ if (!migration.overwriteLocalResourceCheck || vm.get('running')) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: Ext.String.format('Can\'t migrate VM with local resources: {0}',
+ migrateStats.local_resources.join(', ')),
+ severity: 'error',
+ });
+ } else {
+ migration.preconditions.push({
+ text: Ext.String.format('Migrate VM with local resources: {0}. ' +
+ 'This might fail if resources aren\'t available on the target node.',
+ migrateStats.local_resources.join(', ')),
+ severity: 'warning',
+ });
+ }
+ }
+
+ if (migrateStats.local_disks.length) {
+ migrateStats.local_disks.forEach(function(disk) {
+ if (disk.cdrom && disk.cdrom === 1) {
+ if (!disk.volid.includes('vm-' + vm.get('vmid') + '-cloudinit')) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: "Can't migrate VM with local CD/DVD",
+ severity: 'error',
+ });
+ }
+ } else {
+ let size = disk.size ? '(' + Proxmox.Utils.render_size(disk.size) + ')' : '';
+ migration['with-local-disks'] = 1;
+ migration.preconditions.push({
+ text: Ext.String.format('Migration with local disk might take long: {0} {1}', disk.volid, size),
+ severity: 'warning',
+ });
+ }
+ });
+ }
+
+ vm.set('migration', migration);
+ },
+ checkLxcPreconditions: function(resetMigrationPossible) {
+ let vm = this.getViewModel();
+ if (vm.get('running')) {
+ vm.set('migration.mode', 'restart');
+ }
+ },
+ },
+
+ width: 600,
+ modal: true,
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ border: false,
+ items: [
+ {
+ xtype: 'form',
+ reference: 'formPanel',
+ bodyPadding: 10,
+ border: false,
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'container',
+ flex: 1,
+ items: [{
+ xtype: 'displayfield',
+ name: 'source',
+ fieldLabel: gettext('Source node'),
+ bind: {
+ value: '{nodename}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'migrationMode',
+ fieldLabel: gettext('Mode'),
+ bind: {
+ value: '{setMigrationMode}',
+ },
+ }],
+ },
+ {
+ xtype: 'container',
+ flex: 1,
+ items: [{
+ xtype: 'pveNodeSelector',
+ reference: 'pveNodeSelector',
+ name: 'target',
+ fieldLabel: gettext('Target node'),
+ allowBlank: false,
+ disallowedNodes: undefined,
+ onlineValidator: true,
+ listeners: {
+ change: 'onTargetChange',
+ },
+ },
+ {
+ xtype: 'pveStorageSelector',
+ reference: 'pveDiskStorageSelector',
+ name: 'targetstorage',
+ fieldLabel: gettext('Target storage'),
+ storageContent: 'images',
+ allowBlank: true,
+ autoSelect: false,
+ emptyText: gettext('Current layout'),
+ bind: {
+ hidden: '{setStorageselectorHidden}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'overwriteLocalResourceCheck',
+ fieldLabel: gettext('Force'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': 'Overwrite local resources unavailable check',
+ },
+ bind: {
+ hidden: '{setLocalResourceCheckboxHidden}',
+ value: '{migration.overwriteLocalResourceCheck}',
+ },
+ listeners: {
+ change: {
+ fn: 'checkMigratePreconditions',
+ extraArg: true,
+ },
+ },
+ }],
+ },
+ ],
+ },
+ {
+ xtype: 'gridpanel',
+ reference: 'preconditionGrid',
+ selectable: false,
+ flex: 1,
+ columns: [{
+ text: '',
+ dataIndex: 'severity',
+ renderer: function(v) {
+ switch (v) {
+ case 'warning':
+ return ' ';
+ case 'error':
+ return '';
+ default:
+ return v;
+ }
+ },
+ width: 35,
+ },
+ {
+ text: 'Info',
+ dataIndex: 'text',
+ cellWrap: true,
+ flex: 1,
+ }],
+ bind: {
+ hidden: '{!migration.preconditions.length}',
+ store: {
+ fields: ['severity', 'text'],
+ data: '{migration.preconditions}',
+ sorters: 'text',
+ },
+ },
+ },
+
+ ],
+ buttons: [
+ {
+ xtype: 'proxmoxHelpButton',
+ reference: 'proxmoxHelpButton',
+ onlineHelp: 'pct_migration',
+ listenToGlobalEvent: false,
+ hidden: false,
+ },
+ '->',
+ {
+ xtype: 'button',
+ reference: 'submitButton',
+ text: gettext('Migrate'),
+ handler: 'startMigration',
+ bind: {
+ disabled: '{!migration.possible}',
+ },
+ },
+ ],
+});
+Ext.define('pve-prune-list', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'type',
+ 'vmid',
+ {
+ name: 'ctime',
+ type: 'date',
+ dateFormat: 'timestamp',
+ },
+ ],
+});
+
+Ext.define('PVE.PruneInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pvePruneInputPanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onGetValues: function(values) {
+ let me = this;
+
+ // the API expects a single prune-backups property string
+ let pruneBackups = PVE.Parser.printPropertyString(values);
+ values = {
+ 'prune-backups': pruneBackups,
+ 'type': me.backup_type,
+ 'vmid': me.backup_id,
+ };
+
+ return values;
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ if (!view.url) {
+ throw "no url specified";
+ }
+ if (!view.backup_type) {
+ throw "no backup_type specified";
+ }
+ if (!view.backup_id) {
+ throw "no backup_id specified";
+ }
+
+ this.reload(); // initial load
+ },
+
+ reload: function() {
+ let view = this.getView();
+
+ // helper to allow showing why a backup is kept
+ let addKeepReasons = function(backups, params) {
+ const rules = [
+ 'keep-last',
+ 'keep-hourly',
+ 'keep-daily',
+ 'keep-weekly',
+ 'keep-monthly',
+ 'keep-yearly',
+ 'keep-all', // when all keep options are not set
+ ];
+ let counter = {};
+
+ backups.sort((a, b) => b.ctime - a.ctime);
+
+ let ruleIndex = -1;
+ let nextRule = function() {
+ let rule;
+ do {
+ ruleIndex++;
+ rule = rules[ruleIndex];
+ } while (!params[rule] && rule !== 'keep-all');
+ counter[rule] = 0;
+ return rule;
+ };
+
+ let rule = nextRule();
+ for (let backup of backups) {
+ if (backup.mark === 'keep') {
+ counter[rule]++;
+ if (rule !== 'keep-all') {
+ backup.keepReason = rule + ': ' + counter[rule];
+ if (counter[rule] >= params[rule]) {
+ rule = nextRule();
+ }
+ } else {
+ backup.keepReason = rule;
+ }
+ }
+ }
+ };
+
+ let params = view.getValues();
+ let keepParams = PVE.Parser.parsePropertyString(params["prune-backups"]);
+
+ Proxmox.Utils.API2Request({
+ url: view.url,
+ method: "GET",
+ params: params,
+ callback: function() {
+ // for easy breakpoint setting
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var data = response.result.data;
+ addKeepReasons(data, keepParams);
+ view.pruneStore.setData(data);
+ },
+ });
+ },
+
+ control: {
+ field: { change: 'reload' },
+ },
+ },
+
+ column1: [
+ {
+ xtype: 'pmxPruneKeepField',
+ name: 'keep-last',
+ fieldLabel: gettext('keep-last'),
+ },
+ {
+ xtype: 'pmxPruneKeepField',
+ name: 'keep-hourly',
+ fieldLabel: gettext('keep-hourly'),
+ },
+ {
+ xtype: 'pmxPruneKeepField',
+ name: 'keep-daily',
+ fieldLabel: gettext('keep-daily'),
+ },
+ {
+ xtype: 'pmxPruneKeepField',
+ name: 'keep-weekly',
+ fieldLabel: gettext('keep-weekly'),
+ },
+ {
+ xtype: 'pmxPruneKeepField',
+ name: 'keep-monthly',
+ fieldLabel: gettext('keep-monthly'),
+ },
+ {
+ xtype: 'pmxPruneKeepField',
+ name: 'keep-yearly',
+ fieldLabel: gettext('keep-yearly'),
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ me.pruneStore = Ext.create('Ext.data.Store', {
+ model: 'pve-prune-list',
+ sorters: { property: 'ctime', direction: 'DESC' },
+ });
+
+ me.column2 = [
+ {
+ xtype: 'grid',
+ height: 200,
+ store: me.pruneStore,
+ columns: [
+ {
+ header: gettext('Backup Time'),
+ sortable: true,
+ dataIndex: 'ctime',
+ renderer: function(value, metaData, record) {
+ let text = Ext.Date.format(value, 'Y-m-d H:i:s');
+ if (record.data.mark === 'remove') {
+ return ''+ text +'
';
+ } else {
+ return text;
+ }
+ },
+ flex: 1,
+ },
+ {
+ text: 'Keep (reason)',
+ dataIndex: 'mark',
+ renderer: function(value, metaData, record) {
+ if (record.data.mark === 'keep') {
+ return 'true (' + record.data.keepReason + ')';
+ } else if (record.data.mark === 'protected') {
+ return 'true (protected)';
+ } else if (record.data.mark === 'renamed') {
+ return 'true (renamed)';
+ } else {
+ return 'false';
+ }
+ },
+ flex: 1,
+ },
+ ],
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.window.Prune', {
+ extend: 'Proxmox.window.Edit',
+
+ method: 'DELETE',
+ submitText: gettext("Prune"),
+
+ fieldDefaults: { labelWidth: 130 },
+
+ isCreate: true,
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no nodename specified";
+ }
+ if (!me.storage) {
+ throw "no storage specified";
+ }
+ if (!me.backup_type) {
+ throw "no backup_type specified";
+ }
+ if (me.backup_type !== 'qemu' && me.backup_type !== 'lxc') {
+ throw "unknown backup type: " + me.backup_type;
+ }
+ if (!me.backup_id) {
+ throw "no backup_id specified";
+ }
+
+ let title = Ext.String.format(
+ gettext("Prune Backups for '{0}' on Storage '{1}'"),
+ me.backup_type + '/' + me.backup_id,
+ me.storage,
+ );
+
+ Ext.apply(me, {
+ url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups",
+ title: title,
+ items: [
+ {
+ xtype: 'pvePruneInputPanel',
+ url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups",
+ backup_type: me.backup_type,
+ backup_id: me.backup_id,
+ storage: me.storage,
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.window.Restore', {
+ extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit?
+
+ resizable: false,
+ width: 500,
+ modal: true,
+ layout: 'auto',
+ border: false,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ '#liveRestore': {
+ change: function(el, newVal) {
+ let liveWarning = this.lookupReference('liveWarning');
+ liveWarning.setHidden(!newVal);
+ let start = this.lookupReference('start');
+ start.setDisabled(newVal);
+ },
+ },
+ 'form': {
+ validitychange: function(f, valid) {
+ this.lookupReference('doRestoreBtn').setDisabled(!valid);
+ },
+ },
+ },
+
+ doRestore: function() {
+ let me = this;
+ let view = me.getView();
+
+ let values = view.down('form').getForm().getValues();
+
+ let params = {
+ vmid: view.vmid || values.vmid,
+ force: view.vmid ? 1 : 0,
+ };
+ if (values.unique) {
+ params.unique = 1;
+ }
+ if (values.start && !values['live-restore']) {
+ params.start = 1;
+ }
+ if (values['live-restore']) {
+ params['live-restore'] = 1;
+ }
+ if (values.storage) {
+ params.storage = values.storage;
+ }
+
+ ['bwlimit', 'cores', 'name', 'memory', 'sockets'].forEach(opt => {
+ if ((values[opt] ?? '') !== '') {
+ params[opt] = values[opt];
+ }
+ });
+
+ if (params.name && view.vmtype === 'lxc') {
+ params.hostname = params.name;
+ delete params.name;
+ }
+
+ let confirmMsg;
+ if (view.vmtype === 'lxc') {
+ params.ostemplate = view.volid;
+ params.restore = 1;
+ if (values.unprivileged !== 'keep') {
+ params.unprivileged = values.unprivileged;
+ }
+ confirmMsg = Proxmox.Utils.format_task_description('vzrestore', params.vmid);
+ } else if (view.vmtype === 'qemu') {
+ params.archive = view.volid;
+ confirmMsg = Proxmox.Utils.format_task_description('qmrestore', params.vmid);
+ } else {
+ throw 'unknown VM type';
+ }
+
+ let executeRestore = () => {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${view.nodename}/${view.vmtype}`,
+ params: params,
+ method: 'POST',
+ waitMsgTarget: view,
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ success: function(response, options) {
+ Ext.create('Proxmox.window.TaskViewer', {
+ autoShow: true,
+ upid: response.result.data,
+ });
+ view.close();
+ },
+ });
+ };
+
+ if (view.vmid) {
+ confirmMsg += `. ${Ext.String.format(
+ gettext('This will permanently erase current {0} data.'),
+ view.vmtype === 'lxc' ? 'CT' : 'VM',
+ )}`;
+ if (view.vmtype === 'lxc') {
+ confirmMsg += `
${gettext('Mount point volumes are also erased.')}`;
+ }
+ Ext.Msg.confirm(gettext('Confirm'), confirmMsg, function(btn) {
+ if (btn === 'yes') {
+ executeRestore();
+ }
+ });
+ } else {
+ executeRestore();
+ }
+ },
+
+ afterRender: function() {
+ let view = this.getView();
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${view.nodename}/vzdump/extractconfig`,
+ method: 'GET',
+ waitMsgTarget: view,
+ params: {
+ volume: view.volid,
+ },
+ failure: response => Ext.Msg.alert('Error', response.htmlStatus),
+ success: function(response, options) {
+ let allStoragesAvailable = true;
+
+ response.result.data.split('\n').forEach(line => {
+ let [_, key, value] = line.match(/^([^:]+):\s*(\S+)\s*$/) ?? [];
+
+ if (!key) {
+ return;
+ }
+
+ if (key === '#qmdump#map') {
+ let match = value.match(/^(\S+):(\S+):(\S*):(\S*):$/) ?? [];
+ // if a /dev/XYZ disk was backed up, ther is no storage hint
+ allStoragesAvailable &&= !!match[3] && !!PVE.data.ResourceStore.getById(
+ `storage/${view.nodename}/${match[3]}`);
+ } else if (key === 'name' || key === 'hostname') {
+ view.lookupReference('nameField').setEmptyText(value);
+ } else if (key === 'memory' || key === 'cores' || key === 'sockets') {
+ view.lookupReference(`${key}Field`).setEmptyText(value);
+ }
+ });
+
+ if (!allStoragesAvailable) {
+ let storagesel = view.down('pveStorageSelector[name=storage]');
+ storagesel.allowBlank = false;
+ storagesel.setEmptyText('');
+ }
+ },
+ });
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ if (!me.volid) {
+ throw "no volume ID specified";
+ }
+ if (!me.vmtype) {
+ throw "no vmtype specified";
+ }
+
+ let storagesel = Ext.create('PVE.form.StorageSelector', {
+ nodename: me.nodename,
+ name: 'storage',
+ value: '',
+ fieldLabel: gettext('Storage'),
+ storageContent: me.vmtype === 'lxc' ? 'rootdir' : 'images',
+ // when restoring a container without specifying a storage, the backend defaults
+ // to 'local', which is unintuitive and 'rootdir' might not even be allowed on it
+ allowBlank: me.vmtype !== 'lxc',
+ emptyText: me.vmtype === 'lxc' ? '' : gettext('From backup configuration'),
+ autoSelect: me.vmtype === 'lxc',
+ });
+
+ let items = [
+ {
+ xtype: 'displayfield',
+ value: me.volidText || me.volid,
+ fieldLabel: gettext('Source'),
+ },
+ storagesel,
+ {
+ xtype: 'pmxDisplayEditField',
+ name: 'vmid',
+ fieldLabel: me.vmtype === 'lxc' ? 'CT' : 'VM',
+ value: me.vmid,
+ editable: !me.vmid,
+ editConfig: {
+ xtype: 'pveGuestIDSelector',
+ guestType: me.vmtype,
+ loadNextFreeID: true,
+ validateExists: false,
+ },
+ },
+ {
+ xtype: 'pveBandwidthField',
+ name: 'bwlimit',
+ backendUnit: 'KiB',
+ allowZero: true,
+ fieldLabel: gettext('Bandwidth Limit'),
+ emptyText: gettext('Defaults to target storage restore limit'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext("Use '0' to disable all bandwidth limits."),
+ },
+ },
+ {
+ xtype: 'fieldcontainer',
+ layout: 'hbox',
+ items: [{
+ xtype: 'proxmoxcheckbox',
+ name: 'unique',
+ fieldLabel: gettext('Unique'),
+ flex: 1,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses'),
+ },
+ checked: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'start',
+ reference: 'start',
+ flex: 1,
+ fieldLabel: gettext('Start after restore'),
+ labelWidth: 105,
+ checked: false,
+ }],
+ },
+ ];
+
+ if (me.vmtype === 'lxc') {
+ items.push(
+ {
+ xtype: 'radiogroup',
+ fieldLabel: gettext('Privilege Level'),
+ reference: 'noVNCScalingGroup',
+ height: '15px', // renders faster with value assigned
+ layout: {
+ type: 'hbox',
+ algin: 'stretch',
+ },
+ autoEl: {
+ tag: 'div',
+ 'data-qtip':
+ gettext('Choose if you want to keep or override the privilege level of the restored Container.'),
+ },
+ items: [
+ {
+ xtype: 'radiofield',
+ name: 'unprivileged',
+ inputValue: 'keep',
+ boxLabel: gettext('From Backup'),
+ flex: 1,
+ checked: true,
+ },
+ {
+ xtype: 'radiofield',
+ name: 'unprivileged',
+ inputValue: '1',
+ boxLabel: gettext('Unprivileged'),
+ flex: 1,
+ },
+ {
+ xtype: 'radiofield',
+ name: 'unprivileged',
+ inputValue: '0',
+ boxLabel: gettext('Privileged'),
+ flex: 1,
+ //margin: '0 0 0 10',
+ },
+ ],
+ },
+ );
+ } else if (me.vmtype === 'qemu') {
+ items.push({
+ xtype: 'proxmoxcheckbox',
+ name: 'live-restore',
+ itemId: 'liveRestore',
+ flex: 1,
+ fieldLabel: gettext('Live restore'),
+ checked: false,
+ hidden: !me.isPBS,
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'liveWarning',
+ // TODO: Remove once more tested/stable?
+ value: gettext('Note: If anything goes wrong during the live-restore, new data written by the VM may be lost.'),
+ userCls: 'pmx-hint',
+ hidden: true,
+ });
+ }
+
+ items.push({
+ xtype: 'fieldset',
+ title: `${gettext('Override Settings')}:`,
+ layout: 'hbox',
+ defaults: {
+ border: false,
+ layout: 'anchor',
+ flex: 1,
+ },
+ items: [
+ {
+ padding: '0 10 0 0',
+ items: [{
+ xtype: 'textfield',
+ fieldLabel: me.vmtype === 'lxc' ? gettext('Hostname') : gettext('Name'),
+ name: 'name',
+ reference: 'nameField',
+ allowBlank: true,
+ }, {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Cores'),
+ name: 'cores',
+ reference: 'coresField',
+ minValue: 1,
+ maxValue: 128,
+ allowBlank: true,
+ }],
+ },
+ {
+ padding: '0 0 0 10',
+ items: [
+ {
+ xtype: 'pveMemoryField',
+ fieldLabel: gettext('Memory'),
+ name: 'memory',
+ reference: 'memoryField',
+ value: '',
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Sockets'),
+ name: 'sockets',
+ reference: 'socketsField',
+ minValue: 1,
+ maxValue: 4,
+ allowBlank: true,
+ hidden: me.vmtype !== 'qemu',
+ disabled: me.vmtype !== 'qemu',
+ }],
+ },
+ ],
+ });
+
+ let title = gettext('Restore') + ": " + (me.vmtype === 'lxc' ? 'CT' : 'VM');
+ if (me.vmid) {
+ title = `${gettext('Overwrite')} ${title} ${me.vmid}`;
+ }
+
+ Ext.apply(me, {
+ title: title,
+ items: [
+ {
+ xtype: 'form',
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%',
+ },
+ items: items,
+ },
+ ],
+ buttons: [
+ {
+ text: gettext('Restore'),
+ reference: 'doRestoreBtn',
+ handler: 'doRestore',
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+/*
+ * SafeDestroy window with additional checkboxes for removing guests
+ */
+Ext.define('PVE.window.SafeDestroyGuest', {
+ extend: 'Proxmox.window.SafeDestroy',
+ alias: 'widget.pveSafeDestroyGuest',
+
+ additionalItems: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'purge',
+ reference: 'purgeCheckbox',
+ boxLabel: gettext('Purge from job configurations'),
+ checked: false,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Remove from replication, HA and backup jobs'),
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'destroyUnreferenced',
+ reference: 'destroyUnreferencedCheckbox',
+ boxLabel: gettext('Destroy unreferenced disks owned by guest'),
+ checked: false,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Scan all enabled storages for unreferenced disks and delete them.'),
+ },
+ },
+ ],
+
+ note: gettext('Referenced disks will always be destroyed.'),
+
+ getParams: function() {
+ let me = this;
+
+ const purgeCheckbox = me.lookupReference('purgeCheckbox');
+ me.params.purge = purgeCheckbox.checked ? 1 : 0;
+
+ const destroyUnreferencedCheckbox = me.lookupReference('destroyUnreferencedCheckbox');
+ me.params["destroy-unreferenced-disks"] = destroyUnreferencedCheckbox.checked ? 1 : 0;
+
+ return me.callParent();
+ },
+});
+/*
+ * SafeDestroy window with additional checkboxes for removing a storage on the disk level.
+ */
+Ext.define('PVE.window.SafeDestroyStorage', {
+ extend: 'Proxmox.window.SafeDestroy',
+ alias: 'widget.pveSafeDestroyStorage',
+
+ showProgress: true,
+
+ additionalItems: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'wipeDisks',
+ reference: 'wipeDisksCheckbox',
+ boxLabel: gettext('Cleanup Disks'),
+ checked: true,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Wipe labels and other left-overs'),
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'cleanupConfig',
+ reference: 'cleanupConfigCheckbox',
+ boxLabel: gettext('Cleanup Storage Configuration'),
+ checked: true,
+ },
+ ],
+
+ getParams: function() {
+ let me = this;
+
+ me.params['cleanup-disks'] = me.lookupReference('wipeDisksCheckbox').checked ? 1 : 0;
+ me.params['cleanup-config'] = me.lookupReference('cleanupConfigCheckbox').checked ? 1 : 0;
+
+ return me.callParent();
+ },
+});
+Ext.define('PVE.window.Settings', {
+ extend: 'Ext.window.Window',
+
+ width: '800px',
+ title: gettext('My Settings'),
+ iconCls: 'fa fa-gear',
+ modal: true,
+ bodyPadding: 10,
+ resizable: false,
+
+ buttons: [
+ {
+ xtype: 'proxmoxHelpButton',
+ onlineHelp: 'gui_my_settings',
+ hidden: false,
+ },
+ '->',
+ {
+ text: gettext('Close'),
+ handler: function() {
+ this.up('window').close();
+ },
+ },
+ ],
+
+ layout: 'hbox',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ var me = this;
+ var sp = Ext.state.Manager.getProvider();
+
+ var username = sp.get('login-username') || Proxmox.Utils.noneText;
+ me.lookupReference('savedUserName').setValue(Ext.String.htmlEncode(username));
+ var vncMode = sp.get('novnc-scaling') || 'auto';
+ me.lookupReference('noVNCScalingGroup').setValue({ noVNCScalingField: vncMode });
+
+ let summarycolumns = sp.get('summarycolumns', 'auto');
+ me.lookup('summarycolumns').setValue(summarycolumns);
+
+ me.lookup('guestNotesCollapse').setValue(sp.get('guest-notes-collapse', 'never'));
+
+ 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() {
+ let me = this;
+ let form = me.lookup('xtermform');
+
+ let valid = form.isValid(), dirty = form.isDirty();
+ let hasValues = Object.values(form.getValues()).some(v => !!v);
+
+ me.lookup('xtermsave').setDisabled(!dirty || !valid);
+ me.lookup('xtermreset').setDisabled(!hasValues);
+ },
+
+ control: {
+ '#xtermjs form': {
+ dirtychange: 'set_button_status',
+ validitychange: 'set_button_status',
+ },
+ '#xtermjs button': {
+ click: function(button) {
+ var me = this;
+ var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
+ settings.forEach(function(setting) {
+ var field = me.lookup(setting);
+ if (button.reference === 'xtermsave') {
+ 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() {
+ let blacklist = ['GuiCap', 'login-username', 'dash-storages'];
+ let sp = Ext.state.Manager.getProvider();
+ for (const state of Object.keys(sp.state)) {
+ if (!blacklist.includes(state)) {
+ sp.clear(state);
+ }
+ }
+ window.location.reload();
+ },
+ },
+ 'button[name=clear-username]': {
+ click: function() {
+ let me = this;
+ me.lookupReference('savedUserName').setValue(Proxmox.Utils.noneText);
+ Ext.state.Manager.getProvider().clear('login-username');
+ },
+ },
+ 'grid[reference=dashboard-storages]': {
+ selectionchange: function(grid, selected) {
+ var me = this;
+ var sp = Ext.state.Manager.getProvider();
+
+ // saves the selected storageids as "id1,id2,id3,..." or clears the variable
+ if (selected.length > 0) {
+ sp.set('dash-storages', Ext.Array.pluck(selected, 'id').join(','));
+ } else {
+ sp.clear('dash-storages');
+ }
+ },
+ afterrender: function(grid) {
+ let store = grid.getStore();
+ let storages = Ext.state.Manager.getProvider().get('dash-storages') || '';
+
+ let items = [];
+ storages.split(',').forEach(storage => {
+ if (storage !== '') { // we have to get the records to be able to select them
+ let item = store.getById(storage);
+ if (item) {
+ items.push(item);
+ }
+ }
+ });
+ grid.suspendEvent('selectionchange');
+ grid.getSelectionModel().select(items);
+ grid.resumeEvent('selectionchange');
+ },
+ },
+ 'field[reference=summarycolumns]': {
+ change: (el, newValue) => Ext.state.Manager.getProvider().set('summarycolumns', newValue),
+ },
+ 'field[reference=guestNotesCollapse]': {
+ change: (e, v) => Ext.state.Manager.getProvider().set('guest-notes-collapse', v),
+ },
+ },
+ },
+
+ items: [{
+ xtype: 'fieldset',
+ flex: 1,
+ title: gettext('Webinterface Settings'),
+ margin: '5',
+ layout: {
+ type: 'vbox',
+ align: 'left',
+ },
+ defaults: {
+ width: '100%',
+ margin: '0 0 10 0',
+ },
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Dashboard Storages'),
+ labelAlign: 'left',
+ labelWidth: '50%',
+ },
+ {
+ xtype: 'grid',
+ maxHeight: 150,
+ reference: 'dashboard-storages',
+ selModel: {
+ selType: 'checkboxmodel',
+ },
+ columns: [{
+ header: gettext('Name'),
+ dataIndex: 'storage',
+ flex: 1,
+ }, {
+ header: gettext('Node'),
+ dataIndex: 'node',
+ flex: 1,
+ }],
+ store: {
+ type: 'diff',
+ field: ['type', 'storage', 'id', 'node'],
+ rstore: PVE.data.ResourceStore,
+ filters: [{
+ property: 'type',
+ value: 'storage',
+ }],
+ sorters: ['node', 'storage'],
+ },
+ },
+ {
+ xtype: 'box',
+ autoEl: { tag: 'hr' },
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Saved User Name') + ':',
+ labelWidth: 150,
+ stateId: 'login-username',
+ reference: 'savedUserName',
+ flex: 1,
+ value: '',
+ },
+ {
+ xtype: 'button',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ text: gettext('Reset'),
+ name: 'clear-username',
+ },
+ ],
+ },
+ {
+ xtype: 'box',
+ autoEl: { tag: 'hr' },
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Layout') + ':',
+ flex: 1,
+ },
+ {
+ xtype: 'button',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ text: gettext('Reset'),
+ tooltip: gettext('Reset all layout changes (for example, column widths)'),
+ name: 'reset',
+ },
+ ],
+ },
+ {
+ xtype: 'box',
+ autoEl: { tag: 'hr' },
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Summary columns') + ':',
+ labelWidth: 150,
+ stateId: 'summarycolumns',
+ reference: 'summarycolumns',
+ comboItems: [
+ ['auto', 'auto'],
+ ['1', '1'],
+ ['2', '2'],
+ ['3', '3'],
+ ],
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Guest Notes') + ':',
+ labelWidth: 150,
+ stateId: 'guest-notes-collapse',
+ reference: 'guestNotesCollapse',
+ comboItems: [
+ ['never', 'Show by default'],
+ ['always', 'Collapse by default'],
+ ['auto', 'auto (Collapse if empty)'],
+ ],
+ },
+ ],
+ },
+ {
+ xtype: 'container',
+ layout: 'vbox',
+ flex: 1,
+ margin: '5',
+ defaults: {
+ width: '100%',
+ // right margin ensures that the right border of the fieldsets
+ // is shown
+ margin: '0 2 10 0',
+ },
+ items: [
+ {
+ xtype: 'fieldset',
+ itemId: 'xtermjs',
+ title: gettext('xterm.js Settings'),
+ items: [{
+ xtype: 'form',
+ reference: 'xtermform',
+ border: false,
+ layout: {
+ type: 'vbox',
+ algin: 'left',
+ },
+ defaults: {
+ width: '100%',
+ margin: '0 0 10 0',
+ },
+ items: [
+ {
+ xtype: 'textfield',
+ name: 'fontFamily',
+ reference: 'fontFamily',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Font-Family'),
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ emptyText: Proxmox.Utils.defaultText,
+ name: 'fontSize',
+ reference: 'fontSize',
+ minValue: 1,
+ fieldLabel: gettext('Font-Size'),
+ },
+ {
+ xtype: 'numberfield',
+ name: 'letterSpacing',
+ reference: 'letterSpacing',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Letter Spacing'),
+ },
+ {
+ xtype: 'numberfield',
+ name: 'lineHeight',
+ minValue: 0.1,
+ reference: 'lineHeight',
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('Line Height'),
+ },
+ {
+ xtype: 'container',
+ layout: {
+ type: 'hbox',
+ pack: 'end',
+ },
+ defaults: {
+ margin: '0 0 0 5',
+ },
+ items: [
+ {
+ xtype: 'button',
+ reference: 'xtermreset',
+ disabled: true,
+ text: gettext('Reset'),
+ },
+ {
+ xtype: 'button',
+ reference: 'xtermsave',
+ disabled: true,
+ text: gettext('Save'),
+ },
+ ],
+ },
+ ],
+ }],
+ }, {
+ xtype: 'fieldset',
+ title: gettext('noVNC Settings'),
+ items: [
+ {
+ xtype: 'radiogroup',
+ fieldLabel: gettext('Scaling mode'),
+ reference: 'noVNCScalingGroup',
+ height: '15px', // renders faster with value assigned
+ layout: {
+ type: 'hbox',
+ },
+ items: [
+ {
+ xtype: 'radiofield',
+ name: 'noVNCScalingField',
+ inputValue: 'auto',
+ boxLabel: 'Auto',
+ },
+ {
+ xtype: 'radiofield',
+ name: 'noVNCScalingField',
+ inputValue: 'scale',
+ boxLabel: 'Local Scaling',
+ margin: '0 0 0 10',
+ }, {
+ xtype: 'radiofield',
+ name: 'noVNCScalingField',
+ inputValue: 'off',
+ boxLabel: 'Off',
+ margin: '0 0 0 10',
+ },
+ ],
+ listeners: {
+ change: function(el, { noVNCScalingField }) {
+ let provider = Ext.state.Manager.getProvider();
+ if (noVNCScalingField === 'auto') {
+ provider.clear('novnc-scaling');
+ } else {
+ provider.set('novnc-scaling', noVNCScalingField);
+ }
+ },
+ },
+ },
+ ],
+ },
+ ],
+ }],
+});
+Ext.define('PVE.window.Snapshot', {
+ extend: 'Proxmox.window.Edit',
+
+ viewModel: {
+ data: {
+ type: undefined,
+ isCreate: undefined,
+ running: false,
+ guestAgentEnabled: false,
+ },
+ formulas: {
+ runningWithoutGuestAgent: (get) => get('type') === 'qemu' && get('running') && !get('guestAgentEnabled'),
+ shouldWarnAboutFS: (get) => get('isCreate') && get('runningWithoutGuestAgent') && get('!vmstate.checked'),
+ },
+ },
+
+ onGetValues: function(values) {
+ let me = this;
+
+ if (me.type === 'lxc') {
+ delete values.vmstate;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+ var vm = me.getViewModel();
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.type) {
+ throw "no type specified";
+ }
+
+ vm.set('type', me.type);
+ vm.set('running', me.running);
+ vm.set('isCreate', me.isCreate);
+
+ if (me.type === 'qemu' && me.isCreate) {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${me.nodename}/${me.type}/${me.vmid}/config`,
+ params: { 'current': '1' },
+ method: 'GET',
+ success: function(response, options) {
+ let res = response.result.data;
+ let enabled = PVE.Parser.parsePropertyString(res.agent, 'enabled');
+ vm.set('guestAgentEnabled', !!PVE.Parser.parseBoolean(enabled.enabled));
+ },
+ });
+ }
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'snapname',
+ value: me.snapname,
+ fieldLabel: gettext('Name'),
+ vtype: 'ConfigId',
+ allowBlank: false,
+ },
+ {
+ xtype: 'displayfield',
+ hidden: me.isCreate,
+ disabled: me.isCreate,
+ name: 'snaptime',
+ renderer: PVE.Utils.render_timestamp_human_readable,
+ fieldLabel: gettext('Timestamp'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ hidden: me.type !== 'qemu' || !me.isCreate || !me.running,
+ disabled: me.type !== 'qemu' || !me.isCreate || !me.running,
+ name: 'vmstate',
+ reference: 'vmstate',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ checked: 1,
+ fieldLabel: gettext('Include RAM'),
+ },
+ {
+ xtype: 'textareafield',
+ grow: true,
+ editable: !me.viewonly,
+ name: 'description',
+ fieldLabel: gettext('Description'),
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ name: 'fswarning',
+ hidden: true,
+ value: gettext('It is recommended to either include the RAM or use the QEMU Guest Agent when taking a snapshot of a running VM to avoid inconsistencies.'),
+ bind: {
+ hidden: '{!shouldWarnAboutFS}',
+ },
+ },
+ {
+ title: gettext('Settings'),
+ hidden: me.isCreate,
+ xtype: 'grid',
+ itemId: 'summary',
+ border: true,
+ height: 200,
+ store: {
+ model: 'KeyValue',
+ sorters: [
+ {
+ property: 'key',
+ direction: 'ASC',
+ },
+ ],
+ },
+ columns: [
+ {
+ header: gettext('Key'),
+ width: 150,
+ dataIndex: 'key',
+ },
+ {
+ header: gettext('Value'),
+ flex: 1,
+ dataIndex: 'value',
+ },
+ ],
+ },
+ ];
+
+ me.url = `/nodes/${me.nodename}/${me.type}/${me.vmid}/snapshot`;
+
+ let subject;
+ if (me.isCreate) {
+ subject = (me.type === 'qemu' ? 'VM' : 'CT') + me.vmid + ' ' + gettext('Snapshot');
+ me.method = 'POST';
+ me.showTaskViewer = true;
+ } else {
+ subject = `${gettext('Snapshot')} ${me.snapname}`;
+ me.url += `/${me.snapname}/config`;
+ }
+
+ Ext.apply(me, {
+ subject: subject,
+ width: me.isCreate ? 450 : 620,
+ height: me.isCreate ? undefined : 420,
+ });
+
+ me.callParent();
+
+ if (!me.snapname) {
+ return;
+ }
+
+ me.load({
+ success: function(response) {
+ let kvarray = [];
+ Ext.Object.each(response.result.data, function(key, value) {
+ if (key === 'description' || key === 'snaptime') {
+ return;
+ }
+ kvarray.push({ key: key, value: value });
+ });
+
+ let summarystore = me.down('#summary').getStore();
+ summarystore.suspendEvents();
+ summarystore.add(kvarray);
+ summarystore.sort();
+ summarystore.resumeEvents();
+ summarystore.fireEvent('refresh', summarystore);
+
+ me.setValues(response.result.data);
+ },
+ });
+ },
+});
+Ext.define('PVE.panel.StartupInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ onlineHelp: 'qm_startup_and_shutdown',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var res = PVE.Parser.printStartup(values);
+
+ if (res === undefined || res === '') {
+ return { 'delete': 'startup' };
+ }
+
+ return { startup: res };
+ },
+
+ setStartup: function(value) {
+ var me = this;
+
+ var startup = PVE.Parser.parseStartup(value);
+ if (startup) {
+ me.setValues(startup);
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: 'textfield',
+ name: 'order',
+ defaultValue: '',
+ emptyText: 'any',
+ fieldLabel: gettext('Start/Shutdown order'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'up',
+ defaultValue: '',
+ emptyText: 'default',
+ fieldLabel: gettext('Startup delay'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'down',
+ defaultValue: '',
+ emptyText: 'default',
+ fieldLabel: gettext('Shutdown timeout'),
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.window.StartupEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveWindowStartupEdit',
+ onlineHelp: undefined,
+
+ initComponent: function() {
+ let me = this;
+
+ let ipanelConfig = me.onlineHelp ? { onlineHelp: me.onlineHelp } : {};
+ let ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig);
+
+ Ext.applyIf(me, {
+ subject: gettext('Start/Shutdown order'),
+ fieldDefaults: {
+ labelWidth: 120,
+ },
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ me.vmconfig = response.result.data;
+ ipanel.setStartup(me.vmconfig.startup);
+ },
+ });
+ },
+});
+Ext.define('PVE.window.DownloadUrlToStorage', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveStorageDownloadUrl',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ isCreate: true,
+
+ method: 'POST',
+
+ showTaskViewer: true,
+
+ title: gettext('Download from URL'),
+ submitText: gettext('Download'),
+
+ cbindData: function(initialConfig) {
+ var me = this;
+ return {
+ nodename: me.nodename,
+ storage: me.storage,
+ content: me.content,
+ };
+ },
+
+ cbind: {
+ url: '/nodes/{nodename}/storage/{storage}/download-url',
+ },
+
+
+ viewModel: {
+ data: {
+ size: '-',
+ mimetype: '-',
+ enableQuery: true,
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ urlChange: function(field) {
+ this.resetMetaInfo();
+ this.setQueryEnabled();
+ },
+ setQueryEnabled: function() {
+ this.getViewModel().set('enableQuery', true);
+ },
+ resetMetaInfo: function() {
+ let vm = this.getViewModel();
+ vm.set('size', '-');
+ vm.set('mimetype', '-');
+ },
+
+ urlCheck: function(field) {
+ let me = this;
+ let view = me.getView();
+
+ const queryParam = view.getValues();
+
+ me.getViewModel().set('enableQuery', false);
+ me.resetMetaInfo();
+ let urlField = view.down('[name=url]');
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${view.nodename}/query-url-metadata`,
+ method: 'GET',
+ params: {
+ url: queryParam.url,
+ 'verify-certificates': queryParam['verify-certificates'],
+ },
+ waitMsgTarget: view,
+ failure: res => {
+ urlField.setValidation(res.result.message);
+ urlField.validate();
+ Ext.MessageBox.alert(gettext('Error'), res.htmlStatus);
+ // re-enable so one can directly requery, e.g., if it was just a network hiccup
+ me.setQueryEnabled();
+ },
+ success: function(res, opt) {
+ urlField.setValidation();
+ urlField.validate();
+
+ let data = res.result.data;
+ view.setValues({
+ filename: data.filename || "",
+ size: (data.size && Proxmox.Utils.format_size(data.size)) || gettext("Unknown"),
+ mimetype: data.mimetype || gettext("Unknown"),
+ });
+ },
+ });
+ },
+
+ hashChange: function(field) {
+ let checksum = Ext.getCmp('downloadUrlChecksum');
+ if (field.getValue() === '__default__') {
+ checksum.setDisabled(true);
+ checksum.setValue("");
+ checksum.allowBlank = true;
+ } else {
+ checksum.setDisabled(false);
+ checksum.allowBlank = false;
+ }
+ },
+ },
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ border: false,
+ onGetValues: function(values) {
+ if (typeof values.checksum === 'string') {
+ values.checksum = values.checksum.trim();
+ }
+ return values;
+ },
+ columnT: [
+ {
+ xtype: 'fieldcontainer',
+ layout: 'hbox',
+ fieldLabel: gettext('URL'),
+ items: [
+ {
+ xtype: 'textfield',
+ name: 'url',
+ emptyText: gettext("Enter URL to download"),
+ allowBlank: false,
+ flex: 1,
+ listeners: {
+ change: 'urlChange',
+ },
+ },
+ {
+ xtype: 'button',
+ name: 'check',
+ text: gettext('Query URL'),
+ margin: '0 0 0 5',
+ bind: {
+ disabled: '{!enableQuery}',
+ },
+ listeners: {
+ click: 'urlCheck',
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'textfield',
+ name: 'filename',
+ allowBlank: false,
+ fieldLabel: gettext('File name'),
+ emptyText: gettext("Please (re-)query URL to get meta information"),
+ },
+ ],
+ column1: [
+ {
+ xtype: 'displayfield',
+ name: 'size',
+ fieldLabel: gettext('File size'),
+ bind: {
+ value: '{size}',
+ },
+ },
+ ],
+ column2: [
+ {
+ xtype: 'displayfield',
+ name: 'mimetype',
+ fieldLabel: gettext('MIME type'),
+ bind: {
+ value: '{mimetype}',
+ },
+ },
+ ],
+ advancedColumn1: [
+ {
+ xtype: 'pveHashAlgorithmSelector',
+ name: 'checksum-algorithm',
+ fieldLabel: gettext('Hash algorithm'),
+ allowBlank: true,
+ hasNoneOption: true,
+ value: '__default__',
+ listeners: {
+ change: 'hashChange',
+ },
+ },
+ {
+ xtype: 'textfield',
+ name: 'checksum',
+ fieldLabel: gettext('Checksum'),
+ allowBlank: true,
+ disabled: true,
+ emptyText: gettext('none'),
+ id: 'downloadUrlChecksum',
+ },
+ ],
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'verify-certificates',
+ fieldLabel: gettext('Verify certificates'),
+ uncheckedValue: 0,
+ checked: true,
+ listeners: {
+ change: 'setQueryEnabled',
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'hiddenfield',
+ name: 'content',
+ cbind: {
+ value: '{content}',
+ },
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ if (!me.storage) {
+ throw "no storage ID specified";
+ }
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.window.UploadToStorage', {
+ extend: 'Ext.window.Window',
+ alias: 'widget.pveStorageUpload',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ resizable: false,
+ modal: true,
+
+ title: gettext('Upload'),
+
+ acceptedExtensions: {
+ iso: ['.img', '.iso'],
+ vztmpl: ['.tar.gz', '.tar.xz', '.tar.zst'],
+ },
+
+ cbindData: function(initialConfig) {
+ const me = this;
+ const ext = me.acceptedExtensions[me.content] || [];
+
+ me.url = `/nodes/${me.nodename}/storage/${me.storage}/upload`;
+
+ return {
+ extensions: ext.join(', '),
+ filenameRegex: RegExp('^.*(?:' + ext.join('|').replaceAll('.', '\\.') + ')$', 'i'),
+ };
+ },
+
+ viewModel: {
+ data: {
+ size: '-',
+ mimetype: '-',
+ filename: '',
+ },
+ },
+
+ controller: {
+ submit: function(button) {
+ const view = this.getView();
+ const form = this.lookup('formPanel').getForm();
+ const abortBtn = this.lookup('abortBtn');
+ const pbar = this.lookup('progressBar');
+
+ const updateProgress = function(per, bytes) {
+ let text = (per * 100).toFixed(2) + '%';
+ if (bytes) {
+ text += " (" + Proxmox.Utils.format_size(bytes) + ')';
+ }
+ pbar.updateProgress(per, text);
+ };
+
+ const fd = new FormData();
+
+ button.setDisabled(true);
+ abortBtn.setDisabled(false);
+
+ fd.append("content", view.content);
+
+ const fileField = form.findField('file');
+ const file = fileField.fileInputEl.dom.files[0];
+ fileField.setDisabled(true);
+
+ const filenameField = form.findField('filename');
+ const filename = filenameField.getValue();
+ filenameField.setDisabled(true);
+
+ const algorithmField = form.findField('checksum-algorithm');
+ algorithmField.setDisabled(true);
+ if (algorithmField.getValue() !== '__default__') {
+ fd.append("checksum-algorithm", algorithmField.getValue());
+
+ const checksumField = form.findField('checksum');
+ fd.append("checksum", checksumField.getValue()?.trim());
+ checksumField.setDisabled(true);
+ }
+
+ fd.append("filename", file, filename);
+
+ pbar.setVisible(true);
+ updateProgress(0);
+
+ const xhr = new XMLHttpRequest();
+ view.xhr = xhr;
+
+ xhr.addEventListener("load", function(e) {
+ if (xhr.status === 200) {
+ view.hide();
+
+ const result = JSON.parse(xhr.response);
+ const upid = result.data;
+ Ext.create('Proxmox.window.TaskViewer', {
+ autoShow: true,
+ upid: upid,
+ taskDone: view.taskDone,
+ listeners: {
+ destroy: function() {
+ view.close();
+ },
+ },
+ });
+
+ return;
+ }
+ const err = Ext.htmlEncode(xhr.statusText);
+ let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`;
+ if (xhr.responseText !== "") {
+ const result = Ext.decode(xhr.responseText);
+ result.message = msg;
+ msg = Proxmox.Utils.extractRequestError(result, true);
+ }
+ Ext.Msg.alert(gettext('Error'), msg, btn => view.close());
+ }, false);
+
+ xhr.addEventListener("error", function(e) {
+ const err = e.target.status.toString();
+ const msg = `Error '${err}' occurred while receiving the document.`;
+ Ext.Msg.alert(gettext('Error'), msg, btn => view.close());
+ });
+
+ xhr.upload.addEventListener("progress", function(evt) {
+ if (evt.lengthComputable) {
+ const percentComplete = evt.loaded / evt.total;
+ updateProgress(percentComplete, evt.loaded);
+ }
+ }, false);
+
+ xhr.open("POST", `/api2/json${view.url}`, true);
+ xhr.send(fd);
+ },
+
+ validitychange: function(f, valid) {
+ const submitBtn = this.lookup('submitBtn');
+ submitBtn.setDisabled(!valid);
+ },
+
+ fileChange: function(input) {
+ const vm = this.getViewModel();
+ const name = input.value.replace(/^.*(\/|\\)/, '');
+ const fileInput = input.fileInputEl.dom;
+ vm.set('filename', name);
+ vm.set('size', (fileInput.files[0] && Proxmox.Utils.format_size(fileInput.files[0].size)) || '-');
+ vm.set('mimetype', (fileInput.files[0] && fileInput.files[0].type) || '-');
+ },
+
+ hashChange: function(field, value) {
+ const checksum = this.lookup('downloadUrlChecksum');
+ if (value === '__default__') {
+ checksum.setDisabled(true);
+ checksum.setValue("");
+ } else {
+ checksum.setDisabled(false);
+ }
+ },
+ },
+
+ items: [
+ {
+ xtype: 'form',
+ reference: 'formPanel',
+ method: 'POST',
+ waitMsgTarget: true,
+ bodyPadding: 10,
+ border: false,
+ width: 400,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%',
+ },
+ items: [
+ {
+ xtype: 'filefield',
+ name: 'file',
+ buttonText: gettext('Select File'),
+ allowBlank: false,
+ fieldLabel: gettext('File'),
+ cbind: {
+ accept: '{extensions}',
+ },
+ listeners: {
+ change: 'fileChange',
+ },
+ },
+ {
+ xtype: 'textfield',
+ name: 'filename',
+ allowBlank: false,
+ fieldLabel: gettext('File name'),
+ bind: {
+ value: '{filename}',
+ },
+ cbind: {
+ regex: '{filenameRegex}',
+ },
+ regexText: gettext('Wrong file extension'),
+ },
+ {
+ xtype: 'displayfield',
+ name: 'size',
+ fieldLabel: gettext('File size'),
+ bind: {
+ value: '{size}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ name: 'mimetype',
+ fieldLabel: gettext('MIME type'),
+ bind: {
+ value: '{mimetype}',
+ },
+ },
+ {
+ xtype: 'pveHashAlgorithmSelector',
+ name: 'checksum-algorithm',
+ fieldLabel: gettext('Hash algorithm'),
+ allowBlank: true,
+ hasNoneOption: true,
+ value: '__default__',
+ listeners: {
+ change: 'hashChange',
+ },
+ },
+ {
+ xtype: 'textfield',
+ name: 'checksum',
+ fieldLabel: gettext('Checksum'),
+ allowBlank: false,
+ disabled: true,
+ emptyText: gettext('none'),
+ reference: 'downloadUrlChecksum',
+ },
+ {
+ xtype: 'progressbar',
+ text: 'Ready',
+ hidden: true,
+ reference: 'progressBar',
+ },
+ {
+ xtype: 'hiddenfield',
+ name: 'content',
+ cbind: {
+ value: '{content}',
+ },
+ },
+ ],
+ listeners: {
+ validitychange: 'validitychange',
+ },
+ },
+ ],
+
+ buttons: [
+ {
+ xtype: 'button',
+ text: gettext('Abort'),
+ reference: 'abortBtn',
+ disabled: true,
+ handler: function() {
+ const me = this;
+ me.up('pveStorageUpload').close();
+ },
+ },
+ {
+ text: gettext('Upload'),
+ reference: 'submitBtn',
+ disabled: true,
+ handler: 'submit',
+ },
+ ],
+
+ listeners: {
+ close: function() {
+ const me = this;
+ if (me.xhr) {
+ me.xhr.abort();
+ delete me.xhr;
+ }
+ },
+ },
+
+ initComponent: function() {
+ const me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ if (!me.storage) {
+ throw "no storage ID specified";
+ }
+ if (!me.acceptedExtensions[me.content]) {
+ throw "content type not supported";
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.window.ScheduleSimulator', {
+ extend: 'Ext.window.Window',
+
+ title: gettext('Job Schedule Simulator'),
+
+ viewModel: {
+ data: {
+ simulatedOnce: false,
+ },
+ formulas: {
+ gridEmptyText: get => get('simulatedOnce') ? Proxmox.Utils.NoneText : gettext('No simulation done'),
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ close: function() {
+ this.getView().close();
+ },
+ simulate: function() {
+ let me = this;
+ let schedule = me.lookup('schedule').getValue();
+ if (!schedule) {
+ return;
+ }
+ let iterations = me.lookup('iterations').getValue() || 10;
+ Proxmox.Utils.API2Request({
+ url: '/cluster/jobs/schedule-analyze',
+ method: 'GET',
+ params: {
+ schedule,
+ iterations,
+ },
+ failure: response => {
+ me.getViewModel().set('simulatedOnce', true);
+ me.lookup('grid').getStore().setData([]);
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response) {
+ let schedules = response.result.data;
+ me.lookup('grid').getStore().setData(schedules);
+ me.getViewModel().set('simulatedOnce', true);
+ },
+ });
+ },
+
+ scheduleChanged: function(field, value) {
+ this.lookup('simulateBtn').setDisabled(!value);
+ },
+
+ renderDate: function(value) {
+ let date = new Date(value*1000);
+ return date.toLocaleDateString();
+ },
+
+ renderTime: function(value) {
+ let date = new Date(value*1000);
+ return date.toLocaleTimeString();
+ },
+
+ init: function(view) {
+ let me = this;
+ if (view.schedule) {
+ me.lookup('schedule').setValue(view.schedule);
+ }
+ },
+ },
+
+ bodyPadding: 10,
+ modal: true,
+ resizable: false,
+ width: 600,
+
+ layout: 'fit',
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ column1: [
+ {
+ xtype: 'pveCalendarEvent',
+ reference: 'schedule',
+ fieldLabel: gettext('Schedule'),
+ listeners: {
+ change: 'scheduleChanged',
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ reference: 'iterations',
+ fieldLabel: gettext('Iterations'),
+ minValue: 1,
+ maxValue: 100,
+ value: 10,
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'box',
+ flex: 1,
+ },
+ {
+ xtype: 'button',
+ reference: 'simulateBtn',
+ text: gettext('Simulate'),
+ handler: 'simulate',
+ disabled: true,
+ },
+ ],
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'grid',
+ reference: 'grid',
+ bind: {
+ emptyText: '{gridEmptyText}',
+ },
+ scrollable: true,
+ height: 300,
+ columns: [
+ {
+ text: gettext('Date'),
+ renderer: 'renderDate',
+ dataIndex: 'timestamp',
+ flex: 1,
+ },
+ {
+ text: gettext('Time'),
+ renderer: 'renderTime',
+ dataIndex: 'timestamp',
+ align: 'right',
+ flex: 1,
+ },
+ ],
+ store: {
+ fields: ['timestamp'],
+ data: [],
+ sorter: 'timestamp',
+ },
+ },
+ ],
+ },
+ ],
+
+ buttons: [
+ {
+ text: gettext('Done'),
+ handler: 'close',
+ },
+ ],
+});
+Ext.define('PVE.window.Wizard', {
+ extend: 'Ext.window.Window',
+
+ activeTitle: '', // used for automated testing
+
+ width: 720,
+ height: 510,
+
+ modal: true,
+ border: false,
+
+ draggable: true,
+ closable: true,
+ resizable: false,
+
+ layout: 'border',
+
+ getValues: function(dirtyOnly) {
+ let me = this;
+
+ let values = {};
+
+ me.down('form').getForm().getFields().each(field => {
+ if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
+ Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
+ }
+ });
+
+ me.query('inputpanel').forEach(panel => {
+ Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
+ });
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var tabs = me.items || [];
+ delete me.items;
+
+ /*
+ * Items may have the following functions:
+ * validator(): per tab custom validation
+ * onSubmit(): submit handler
+ * onGetValues(): overwrite getValues results
+ */
+
+ Ext.Array.each(tabs, function(tab) {
+ tab.disabled = true;
+ });
+ tabs[0].disabled = false;
+
+ let maxidx = 0, curidx = 0;
+
+ let check_card = function(card) {
+ let fields = card.query('field, fieldcontainer');
+ if (card.isXType('fieldcontainer')) {
+ fields.unshift(card);
+ }
+ let valid = true;
+ for (const field of fields) {
+ // Note: not all fielcontainer have isValid()
+ if (Ext.isFunction(field.isValid) && !field.isValid()) {
+ valid = false;
+ }
+ }
+ if (Ext.isFunction(card.validator)) {
+ return card.validator();
+ }
+ return valid;
+ };
+
+ let disableTab = function(card) {
+ let tp = me.down('#wizcontent');
+ for (let idx = tp.items.indexOf(card); idx < tp.items.getCount(); idx++) {
+ let tab = tp.items.getAt(idx);
+ if (tab) {
+ tab.disable();
+ }
+ }
+ };
+
+ let tabchange = function(tp, newcard, oldcard) {
+ if (newcard.onSubmit) {
+ me.down('#next').setVisible(false);
+ me.down('#submit').setVisible(true);
+ } else {
+ me.down('#next').setVisible(true);
+ me.down('#submit').setVisible(false);
+ }
+ let valid = check_card(newcard);
+ me.down('#next').setDisabled(!valid);
+ me.down('#submit').setDisabled(!valid);
+ me.down('#back').setDisabled(tp.items.indexOf(newcard) === 0);
+
+ let idx = tp.items.indexOf(newcard);
+ if (idx > maxidx) {
+ maxidx = idx;
+ }
+ curidx = idx;
+
+ let ntab = tp.items.getAt(idx + 1);
+ if (valid && ntab && !newcard.onSubmit) {
+ ntab.enable();
+ }
+ };
+
+ if (me.subject && !me.title) {
+ me.title = Proxmox.Utils.dialog_title(me.subject, true, false);
+ }
+
+ let sp = Ext.state.Manager.getProvider();
+ let advancedOn = sp.get('proxmox-advanced-cb');
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'form',
+ region: 'center',
+ layout: 'fit',
+ border: false,
+ margins: '5 5 0 5',
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%',
+ },
+ items: [{
+ itemId: 'wizcontent',
+ xtype: 'tabpanel',
+ activeItem: 0,
+ bodyPadding: 0,
+ listeners: {
+ afterrender: function(tp) {
+ tabchange(tp, this.getActiveTab());
+ },
+ tabchange: function(tp, newcard, oldcard) {
+ tabchange(tp, newcard, oldcard);
+ },
+ },
+ defaults: {
+ padding: 10,
+ },
+ items: tabs,
+ }],
+ },
+ ],
+ fbar: [
+ {
+ xtype: 'proxmoxHelpButton',
+ itemId: 'help',
+ },
+ '->',
+ {
+ xtype: 'proxmoxcheckbox',
+ boxLabelAlign: 'before',
+ boxLabel: gettext('Advanced'),
+ value: advancedOn,
+ listeners: {
+ change: function(_, value) {
+ let tp = me.down('#wizcontent');
+ tp.query('inputpanel').forEach(function(ip) {
+ ip.setAdvancedVisible(value);
+ });
+ sp.set('proxmox-advanced-cb', value);
+ },
+ },
+ },
+ {
+ text: gettext('Back'),
+ disabled: true,
+ itemId: 'back',
+ minWidth: 60,
+ handler: function() {
+ let tp = me.down('#wizcontent');
+ let prev = tp.items.indexOf(tp.getActiveTab()) - 1;
+ if (prev < 0) {
+ return;
+ }
+ let ntab = tp.items.getAt(prev);
+ if (ntab) {
+ tp.setActiveTab(ntab);
+ }
+ },
+ },
+ {
+ text: gettext('Next'),
+ disabled: true,
+ itemId: 'next',
+ minWidth: 60,
+ handler: function() {
+ let tp = me.down('#wizcontent');
+ let activeTab = tp.getActiveTab();
+ if (!check_card(activeTab)) {
+ return;
+ }
+ let next = tp.items.indexOf(activeTab) + 1;
+ let ntab = tp.items.getAt(next);
+ if (ntab) {
+ ntab.enable();
+ tp.setActiveTab(ntab);
+ }
+ },
+ },
+ {
+ text: gettext('Finish'),
+ minWidth: 60,
+ hidden: true,
+ itemId: 'submit',
+ handler: function() {
+ let tp = me.down('#wizcontent');
+ tp.getActiveTab().onSubmit();
+ },
+ },
+ ],
+ });
+ me.callParent();
+
+ Ext.Array.each(me.query('inputpanel'), function(panel) {
+ panel.setAdvancedVisible(advancedOn);
+ });
+
+ Ext.Array.each(me.query('field'), function(field) {
+ let validcheck = function() {
+ let tp = me.down('#wizcontent');
+
+ // check validity for current to last enabled tab, as local change may affect validity of a later one
+ for (let i = curidx; i <= maxidx && i < tp.items.getCount(); i++) {
+ let tab = tp.items.getAt(i);
+ let valid = check_card(tab);
+
+ // only set the buttons on the current panel
+ if (i === curidx) {
+ me.down('#next').setDisabled(!valid);
+ me.down('#submit').setDisabled(!valid);
+ }
+ // if a panel is invalid, then disable all following, else enable the next tab
+ let nextTab = tp.items.getAt(i + 1);
+ if (!valid) {
+ disableTab(nextTab);
+ return;
+ } else if (nextTab && !tab.onSubmit) {
+ nextTab.enable();
+ }
+ }
+ };
+ field.on('change', validcheck);
+ field.on('validitychange', validcheck);
+ });
+ },
+});
+Ext.define('PVE.window.GuestDiskReassign', {
+ extend: 'Proxmox.window.Edit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ resizable: false,
+ modal: true,
+ width: 350,
+ border: false,
+ layout: 'fit',
+ showReset: false,
+ showProgress: true,
+ method: 'POST',
+
+ viewModel: {
+ data: {
+ mpType: '',
+ },
+ formulas: {
+ mpMaxCount: get => get('mpType') === 'mp'
+ ? PVE.Utils.mp_counts.mps - 1
+ : PVE.Utils.mp_counts.unused - 1,
+ },
+ },
+
+ cbindData: function() {
+ let me = this;
+ return {
+ vmid: me.vmid,
+ disk: me.disk,
+ isQemu: me.type === 'qemu',
+ nodename: me.nodename,
+ url: () => {
+ let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume';
+ return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`;
+ },
+ };
+ },
+
+ cbind: {
+ title: get => get('isQemu') ? gettext('Reassign Disk') : gettext('Reassign Volume'),
+ submitText: get => get('title'),
+ qemu: '{isQemu}',
+ url: '{url}',
+ },
+
+ getValues: function() {
+ let me = this;
+ let values = me.formPanel.getForm().getValues();
+
+ let params = {
+ vmid: me.vmid,
+ 'target-vmid': values.targetVmid,
+ };
+
+ params[me.qemu ? 'disk' : 'volume'] = me.disk;
+
+ if (me.qemu) {
+ params['target-disk'] = `${values.controller}${values.deviceid}`;
+ } else {
+ params['target-volume'] = `${values.mpType}${values.mpId}`;
+ }
+ return params;
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ initViewModel: function(model) {
+ let view = this.getView();
+ let mpTypeValue = view.disk.match(/^unused\d+/) ? 'unused' : 'mp';
+ model.set('mpType', mpTypeValue);
+ },
+
+ onMpTypeChange: function(value) {
+ let view = this.getView();
+ view.getViewModel().set('mpType', value.getValue());
+ view.lookup('mpIdSelector').validate();
+ },
+
+ onTargetVMChange: function(f, vmid) {
+ let me = this;
+ let view = me.getView();
+ let diskSelector = view.lookup('diskSelector');
+ if (!vmid) {
+ diskSelector.setVMConfig(null);
+ me.VMConfig = null;
+ return;
+ }
+
+ let url = `/nodes/${view.nodename}/${view.type}/${vmid}/config`;
+ Proxmox.Utils.API2Request({
+ url: url,
+ method: 'GET',
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ success: function({ result }, options) {
+ if (view.qemu) {
+ diskSelector.setVMConfig(result.data);
+ diskSelector.setDisabled(false);
+ } else {
+ let mpIdSelector = view.lookup('mpIdSelector');
+ let mpType = view.lookup('mpType');
+
+ view.VMConfig = result.data;
+
+ mpIdSelector.setValue(
+ PVE.Utils.nextFreeMP(
+ view.getViewModel().get('mpType'),
+ view.VMConfig,
+ ).id,
+ );
+
+ mpType.setDisabled(false);
+ mpIdSelector.setDisabled(false);
+ mpIdSelector.validate();
+ }
+ },
+ });
+ },
+ },
+
+ defaultFocus: 'sourceDisk',
+ items: [
+ {
+ xtype: 'displayfield',
+ name: 'sourceDisk',
+ fieldLabel: gettext('Source'),
+ cbind: {
+ name: get => get('isQemu') ? 'disk' : 'volume',
+ value: '{disk}',
+ },
+ allowBlank: false,
+ },
+ {
+ xtype: 'vmComboSelector',
+ name: 'targetVmid',
+ allowBlank: false,
+ fieldLabel: gettext('Target Guest'),
+ store: {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ cbind: {}, // for nested cbinds
+ filters: [
+ {
+ property: 'type',
+ cbind: { value: '{type}' },
+ },
+ {
+ property: 'node',
+ cbind: { value: '{nodename}' },
+ },
+ // FIXME: remove, artificial restriction that doesn't gains us anything..
+ {
+ property: 'vmid',
+ operator: '!=',
+ cbind: { value: '{vmid}' },
+ },
+ {
+ property: 'template',
+ value: 0,
+ },
+ ],
+ },
+ listeners: { change: 'onTargetVMChange' },
+ },
+ {
+ xtype: 'pveControllerSelector',
+ reference: 'diskSelector',
+ withUnused: true,
+ disabled: true,
+ cbind: {
+ hidden: '{!isQemu}',
+ },
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ cbind: {
+ hidden: '{isQemu}',
+ disabled: '{isQemu}',
+ },
+ items: [
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: get => !get('disk').match(/^unused\d+/),
+ value: get => get('disk').match(/^unused\d+/) ? 'unused' : 'mp',
+ },
+ disabled: true,
+ name: 'mpType',
+ reference: 'mpType',
+ fieldLabel: gettext('Add as'),
+ submitValue: true,
+ flex: 4,
+ editConfig: {
+ xtype: 'proxmoxKVComboBox',
+ name: 'mpTypeCombo',
+ deleteEmpty: false,
+ cbind: {
+ hidden: '{isQemu}',
+ },
+ comboItems: [
+ ['mp', gettext('Mount Point')],
+ ['unused', gettext('Unused')],
+ ],
+ listeners: { change: 'onMpTypeChange' },
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mpId',
+ reference: 'mpIdSelector',
+ minValue: 0,
+ flex: 1,
+ allowBlank: false,
+ validateOnChange: true,
+ disabled: true,
+ bind: {
+ maxValue: '{mpMaxCount}',
+ },
+ validator: function(value) {
+ let view = this.up('window');
+ let type = view.getViewModel().get('mpType');
+ if (Ext.isDefined(view.VMConfig[`${type}${value}`])) {
+ return "Mount point is already in use.";
+ }
+ return true;
+ },
+ },
+ ],
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.type) {
+ throw "no type specified";
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.window.TreeSettingsEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveTreeSettingsEdit',
+
+ title: gettext('Tree Settings'),
+ isCreate: false,
+
+ url: '#', // ignored as submit() gets overriden here, but the parent class requires it
+
+ width: 450,
+ fieldDefaults: {
+ labelWidth: 150,
+ },
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ items: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'sort-field',
+ fieldLabel: gettext('Sort Key'),
+ comboItems: [
+ ['__default__', `${Proxmox.Utils.defaultText} (VMID)`],
+ ['vmid', 'VMID'],
+ ['name', gettext('Name')],
+ ],
+ defaultValue: '__default__',
+ value: '__default__',
+ deleteEmpty: false,
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'group-templates',
+ fieldLabel: gettext('Group Templates'),
+ comboItems: [
+ ['__default__', `${Proxmox.Utils.defaultText} (${gettext("Yes")})`],
+ [1, gettext('Yes')],
+ [0, gettext('No')],
+ ],
+ defaultValue: '__default__',
+ value: '__default__',
+ deleteEmpty: false,
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'group-guest-types',
+ fieldLabel: gettext('Group Guest Types'),
+ comboItems: [
+ ['__default__', `${Proxmox.Utils.defaultText} (${gettext("Yes")})`],
+ [1, gettext('Yes')],
+ [0, gettext('No')],
+ ],
+ defaultValue: '__default__',
+ value: '__default__',
+ deleteEmpty: false,
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext('Settings are saved in the local storage of the browser'),
+ },
+ ],
+ },
+ ],
+
+ submit: function() {
+ let me = this;
+
+ let localStorage = Ext.state.Manager.getProvider();
+ localStorage.set('pve-tree-sorting', me.down('inputpanel').getValues() || null);
+
+ me.apiCallDone();
+ me.close();
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.callParent();
+
+ let localStorage = Ext.state.Manager.getProvider();
+ me.down('inputpanel').setValues(localStorage.get('pve-tree-sorting'));
+ },
+
+});
+Ext.define('PVE.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.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;
+
+ let update_nodefield, update_node_selection;
+
+ let sm = Ext.create('Ext.selection.CheckboxModel', {
+ mode: 'SIMPLE',
+ listeners: {
+ selectionchange: function(model, selected) {
+ update_nodefield(selected);
+ },
+ },
+ });
+
+ let store = Ext.create('Ext.data.Store', {
+ fields: ['node', 'mem', 'cpu', 'priority'],
+ data: PVE.data.ResourceStore.getNodes(), // use already cached data to avoid an API call
+ proxy: {
+ type: 'memory',
+ reader: { type: 'json' },
+ },
+ sorters: [
+ {
+ property: 'node',
+ direction: 'ASC',
+ },
+ ],
+ });
+
+ var nodegrid = Ext.createWidget('grid', {
+ store: store,
+ border: true,
+ height: 300,
+ selModel: sm,
+ columns: [
+ {
+ header: gettext('Node'),
+ flex: 1,
+ dataIndex: 'node',
+ },
+ {
+ header: gettext('Memory usage') + " %",
+ renderer: PVE.Utils.render_mem_usage_percent,
+ sortable: true,
+ width: 150,
+ dataIndex: 'mem',
+ },
+ {
+ header: gettext('CPU usage'),
+ renderer: Proxmox.Utils.render_cpu,
+ sortable: true,
+ width: 150,
+ dataIndex: 'cpu',
+ },
+ {
+ header: 'Priority',
+ xtype: 'widgetcolumn',
+ dataIndex: 'priority',
+ sortable: true,
+ stopSelection: true,
+ widget: {
+ xtype: 'proxmoxintegerfield',
+ minValue: 0,
+ maxValue: 1000,
+ isFormField: false,
+ listeners: {
+ change: function(numberfield, value, old_value) {
+ let record = numberfield.getWidgetRecord();
+ record.set('priority', value);
+ update_nodefield(sm.getSelection());
+ record.commit();
+ },
+ },
+ },
+ },
+ ],
+ });
+
+ let nodefield = Ext.create('Ext.form.field.Hidden', {
+ name: 'nodes',
+ value: '',
+ listeners: {
+ change: function(field, value) {
+ update_node_selection(value);
+ },
+ },
+ isValid: function() {
+ let value = this.getValue();
+ return value && value.length !== 0;
+ },
+ });
+
+ update_node_selection = function(string) {
+ sm.deselectAll(true);
+
+ string.split(',').forEach(function(e, idx, array) {
+ let [node, priority] = e.split(':');
+ store.each(function(record) {
+ if (record.get('node') === node) {
+ sm.select(record, true);
+ record.set('priority', priority);
+ record.commit();
+ }
+ });
+ });
+ nodegrid.reconfigure(store);
+ };
+
+ update_nodefield = function(selected) {
+ let nodes = selected
+ .map(({ data }) => data.node + (data.priority ? `:${data.priority}` : ''))
+ .join(',');
+
+ // nodefield change listener calls us again, which results in a
+ // endless recursion, suspend the event temporary to avoid this
+ nodefield.suspendEvent('change');
+ nodefield.setValue(nodes);
+ nodefield.resumeEvent('change');
+ };
+
+ me.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.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',
+ direction: 'ASC',
+ },
+ },
+
+ 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.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',
+ direction: 'ASC',
+ },
+ });
+
+ var reload = function() {
+ store.load();
+ };
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ Ext.create('PVE.ha.GroupEdit', {
+ groupId: rec.data.group,
+ listeners: {
+ destroy: () => store.load(),
+ },
+ autoShow: true,
+ });
+ };
+
+ let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/cluster/ha/groups/',
+ callback: () => store.load(),
+ });
+ let 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() {
+ Ext.create('PVE.ha.GroupEdit', {
+ listeners: {
+ destroy: () => store.load(),
+ },
+ autoShow: true,
+ });
+ },
+ },
+ 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: (grid, record, index, eOpts) => caps.nodes['Sys.Console'],
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ },
+});
+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: 'pmx-hint',
+ value: 'Disabling the resource will stop the guest system. ' +
+ 'See the online help for details.',
+ hidden: true,
+ });
+
+ var fewVotesHint = Ext.createWidget({
+ itemId: 'fewVotesHint',
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: 'At least three quorum votes are recommended for reliable HA.',
+ hidden: true,
+ });
+
+ Proxmox.Utils.API2Request({
+ url: '/cluster/config/nodes',
+ method: 'GET',
+ failure: function(response) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response) {
+ var nodes = response.result.data;
+ var votes = 0;
+ Ext.Array.forEach(nodes, function(node) {
+ var vote = parseInt(node.quorum_votes, 10); // parse as base 10
+ votes += vote || 0; // parseInt might return NaN, which is false
+ });
+
+ if (votes < MIN_QUORUM_VOTES) {
+ fewVotesHint.setVisible(true);
+ }
+ },
+ });
+
+ var vmidStore = me.vmid ? {} : {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ filters: [
+ {
+ property: 'type',
+ value: /lxc|qemu/,
+ },
+ {
+ property: 'hastate',
+ value: /unmanaged/,
+ },
+ ],
+ };
+
+ // value is a string above, but a number below
+ me.column1 = [
+ {
+ xtype: me.vmid ? 'displayfield' : 'vmComboSelector',
+ submitValue: me.isCreate,
+ name: 'vmid',
+ fieldLabel: me.vmid && me.guestType === 'ct' ? 'CT' : 'VM',
+ value: me.vmid,
+ store: vmidStore,
+ validateExists: true,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'max_restart',
+ fieldLabel: gettext('Max. Restart'),
+ value: 1,
+ minValue: 0,
+ maxValue: 10,
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'max_relocate',
+ fieldLabel: gettext('Max. Relocate'),
+ value: 1,
+ minValue: 0,
+ maxValue: 10,
+ allowBlank: false,
+ },
+ ];
+
+ me.column2 = [
+ {
+ xtype: '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() {
+ let me = this;
+
+ if (!me.rstore) {
+ throw "no store given";
+ }
+
+ Proxmox.Utils.monStoreErrors(me, me.rstore);
+ let store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: me.rstore,
+ filters: {
+ property: 'type',
+ value: 'service',
+ },
+ });
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ let sid = rec.data.sid;
+
+ let res = sid.match(/^(\S+):(\S+)$/);
+ if (!res || (res[1] !== 'vm' && res[1] !== 'ct')) {
+ console.warn(`unknown HA service ID type ${sid}`);
+ return;
+ }
+ let [, guestType, vmid] = res;
+ Ext.create('PVE.ha.VMResourceEdit', {
+ guestType: guestType,
+ vmid: vmid,
+ listeners: {
+ destroy: () => me.rstore.load(),
+ },
+ autoShow: true,
+ });
+ };
+
+ let caps = Ext.state.Manager.get('GuiCap');
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ tbar: [
+ {
+ text: gettext('Add'),
+ disabled: !caps.nodes['Sys.Console'],
+ handler: function() {
+ Ext.create('PVE.ha.VMResourceEdit', {
+ listeners: {
+ destroy: () => me.rstore.load(),
+ },
+ autoShow: true,
+ });
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ selModel: sm,
+ getUrl: function(rec) {
+ return `/cluster/ha/resources/${rec.get('sid')}`;
+ },
+ callback: () => me.rstore.load(),
+ },
+ ],
+ columns: [
+ {
+ header: 'ID',
+ width: 100,
+ sortable: true,
+ dataIndex: 'sid',
+ },
+ {
+ header: gettext('State'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'state',
+ },
+ {
+ header: gettext('Node'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'node',
+ },
+ {
+ header: gettext('Request State'),
+ width: 100,
+ hidden: true,
+ sortable: true,
+ renderer: v => v || 'started',
+ dataIndex: 'request_state',
+ },
+ {
+ header: gettext('CRM State'),
+ width: 100,
+ hidden: true,
+ sortable: true,
+ dataIndex: 'crm_state',
+ },
+ {
+ header: gettext('Name'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'vname',
+ },
+ {
+ header: gettext('Max. Restart'),
+ width: 100,
+ sortable: true,
+ renderer: (v) => v === undefined ? '1' : v,
+ dataIndex: 'max_restart',
+ },
+ {
+ header: gettext('Max. Relocate'),
+ width: 100,
+ sortable: true,
+ renderer: (v) => v === undefined ? '1' : v,
+ dataIndex: 'max_relocate',
+ },
+ {
+ header: gettext('Group'),
+ width: 200,
+ sortable: true,
+ renderer: function(value, metaData, { data }) {
+ if (data.errors && data.errors.group) {
+ metaData.tdCls = 'proxmox-invalid-row';
+ let html = `${Ext.htmlEncode(data.errors.group)}
`;
+ metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + html + '"';
+ }
+ return value;
+ },
+ dataIndex: 'group',
+ },
+ {
+ header: gettext('Description'),
+ flex: 1,
+ renderer: Ext.String.htmlEncode,
+ dataIndex: 'comment',
+ },
+ ],
+ listeners: {
+ beforeselect: (grid, record, index, eOpts) => caps.nodes['Sys.Console'],
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.ha.Status', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveHAStatus',
+
+ onlineHelp: 'chapter_ha_manager',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.rstore = Ext.create('Proxmox.data.ObjectStore', {
+ interval: me.interval,
+ model: 'pve-ha-status',
+ storeid: 'pve-store-' + ++Ext.idSeed,
+ groupField: 'type',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/ha/status/current',
+ },
+ });
+
+ me.items = [{
+ xtype: 'pveHAStatusView',
+ title: gettext('Status'),
+ rstore: me.rstore,
+ border: 0,
+ collapsible: true,
+ padding: '0 0 20 0',
+ }, {
+ xtype: 'pveHAResourcesView',
+ flex: 1,
+ collapsible: true,
+ title: gettext('Resources'),
+ border: 0,
+ rstore: me.rstore,
+ }];
+
+ me.callParent();
+ me.on('activate', me.rstore.startUpdate);
+ },
+});
+Ext.define('PVE.ha.StatusView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveHAStatusView'],
+
+ onlineHelp: 'chapter_ha_manager',
+
+ sortPriority: {
+ quorum: 1,
+ master: 2,
+ lrm: 3,
+ service: 4,
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.rstore) {
+ throw "no rstore given";
+ }
+
+ Proxmox.Utils.monStoreErrors(me, me.rstore);
+
+ var store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: me.rstore,
+ sortAfterUpdate: true,
+ sorters: [{
+ sorterFn: function(rec1, rec2) {
+ var p1 = me.sortPriority[rec1.data.type];
+ var p2 = me.sortPriority[rec2.data.type];
+ return p1 !== p2 ? p1 > p2 ? 1 : -1 : 0;
+ },
+ }],
+ filters: {
+ property: 'type',
+ value: 'service',
+ operator: '!=',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ stateful: false,
+ viewConfig: {
+ trackOver: false,
+ },
+ columns: [
+ {
+ header: gettext('Type'),
+ width: 80,
+ dataIndex: 'type',
+ },
+ {
+ header: gettext('Status'),
+ width: 80,
+ flex: 1,
+ dataIndex: 'status',
+ },
+ ],
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ },
+}, function() {
+ Ext.define('pve-ha-status', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id', 'type', 'node', 'status', 'sid',
+ 'state', 'group', 'comment',
+ 'max_restart', 'max_relocate', 'type',
+ 'crm_state', 'request_state',
+ {
+ name: 'vname',
+ convert: function(value, record) {
+ let sid = record.data.sid;
+ if (!sid) return '';
+
+ let res = sid.match(/^(\S+):(\S+)$/);
+ if (res[1] !== 'vm' && res[1] !== 'ct') {
+ return '-';
+ }
+ let vmid = res[2];
+ return PVE.data.ResourceStore.guestName(vmid);
+ },
+ },
+ ],
+ idProperty: 'id',
+ });
+});
+Ext.define('PVE.dc.ACLAdd', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pveACLAdd'],
+
+ url: '/access/acl',
+ method: 'PUT',
+ isAdd: true,
+ isCreate: true,
+
+ width: 400,
+
+ initComponent: function() {
+ let me = this;
+
+ let items = [
+ {
+ xtype: me.path ? 'hiddenfield' : 'pvePermPathSelector',
+ name: 'path',
+ value: me.path,
+ allowBlank: false,
+ fieldLabel: gettext('Path'),
+ },
+ ];
+
+ if (me.aclType === 'group') {
+ me.subject = gettext("Group Permission");
+ items.push({
+ xtype: 'pveGroupSelector',
+ name: 'groups',
+ fieldLabel: gettext('Group'),
+ });
+ } else if (me.aclType === 'user') {
+ me.subject = gettext("User Permission");
+ items.push({
+ xtype: 'pmxUserSelector',
+ name: 'users',
+ fieldLabel: gettext('User'),
+ });
+ } else if (me.aclType === 'token') {
+ me.subject = gettext("API Token Permission");
+ items.push({
+ xtype: 'pveTokenSelector',
+ name: 'tokens',
+ fieldLabel: gettext('API Token'),
+ });
+ } else {
+ throw "unknown ACL type";
+ }
+
+ items.push({
+ xtype: 'pmxRoleSelector',
+ name: 'roles',
+ value: 'NoAccess',
+ fieldLabel: gettext('Role'),
+ });
+
+ if (!me.path) {
+ items.push({
+ xtype: 'proxmoxcheckbox',
+ name: 'propagate',
+ checked: true,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Propagate'),
+ });
+ }
+
+ let ipanel = Ext.create('Proxmox.panel.InputPanel', {
+ items: items,
+ onlineHelp: 'pveum_permission_management',
+ });
+
+ Ext.apply(me, {
+ items: [ipanel],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.dc.ACLView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveACLView'],
+
+ onlineHelp: 'chapter_user_management',
+
+ stateful: true,
+ stateId: 'grid-acls',
+
+ // use fixed path
+ path: undefined,
+
+ initComponent: function() {
+ let me = this;
+
+ let store = Ext.create('Ext.data.Store', {
+ model: 'pve-acl',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/access/acl",
+ },
+ sorters: {
+ property: 'path',
+ direction: 'ASC',
+ },
+ });
+
+ if (me.path) {
+ store.addFilter(Ext.create('Ext.util.Filter', {
+ filterFn: item => item.data.path === me.path,
+ }));
+ }
+
+ let render_ugid = function(ugid, metaData, record) {
+ if (record.data.type === 'group') {
+ return '@' + ugid;
+ }
+
+ return Ext.String.htmlEncode(ugid);
+ };
+
+ let columns = [
+ {
+ header: gettext('User') + '/' + gettext('Group') + '/' + gettext('API Token'),
+ flex: 1,
+ sortable: true,
+ renderer: render_ugid,
+ dataIndex: 'ugid',
+ },
+ {
+ header: gettext('Role'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'roleid',
+ },
+ ];
+
+ if (!me.path) {
+ columns.unshift({
+ header: gettext('Path'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'path',
+ });
+ columns.push({
+ header: gettext('Propagate'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'propagate',
+ });
+ }
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ disabled: true,
+ selModel: sm,
+ confirmMsg: gettext('Are you sure you want to remove this entry'),
+ handler: function(btn, event, rec) {
+ var params = {
+ 'delete': 1,
+ path: rec.data.path,
+ roles: rec.data.roleid,
+ };
+ if (rec.data.type === 'group') {
+ params.groups = rec.data.ugid;
+ } else if (rec.data.type === 'user') {
+ params.users = rec.data.ugid;
+ } else if (rec.data.type === 'token') {
+ params.tokens = rec.data.ugid;
+ } else {
+ throw 'unknown data type';
+ }
+
+ Proxmox.Utils.API2Request({
+ url: '/access/acl',
+ params: params,
+ method: 'PUT',
+ waitMsgTarget: me,
+ callback: () => store.load(),
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ },
+ });
+
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: {
+ xtype: 'menu',
+ items: [
+ {
+ text: gettext('Group Permission'),
+ iconCls: 'fa fa-fw fa-group',
+ handler: function() {
+ var win = Ext.create('PVE.dc.ACLAdd', {
+ aclType: 'group',
+ path: me.path,
+ });
+ win.on('destroy', () => store.load());
+ win.show();
+ },
+ },
+ {
+ text: gettext('User Permission'),
+ iconCls: 'fa fa-fw fa-user',
+ handler: function() {
+ var win = Ext.create('PVE.dc.ACLAdd', {
+ aclType: 'user',
+ path: me.path,
+ });
+ win.on('destroy', () => store.load());
+ win.show();
+ },
+ },
+ {
+ text: gettext('API Token Permission'),
+ iconCls: 'fa fa-fw fa-user-o',
+ handler: function() {
+ let win = Ext.create('PVE.dc.ACLAdd', {
+ aclType: 'token',
+ path: me.path,
+ });
+ win.on('destroy', () => store.load());
+ win.show();
+ },
+ },
+ ],
+ },
+ },
+ remove_btn,
+ ],
+ viewConfig: {
+ trackOver: false,
+ },
+ columns: columns,
+ listeners: {
+ activate: () => store.load(),
+ },
+ });
+
+ me.callParent();
+ },
+}, function() {
+ Ext.define('pve-acl', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'path', 'type', 'ugid', 'roleid',
+ {
+ name: 'propagate',
+ type: 'boolean',
+ },
+ ],
+ });
+});
+Ext.define('pve-acme-accounts', {
+ extend: 'Ext.data.Model',
+ fields: ['name'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/acme/account",
+ },
+ idProperty: 'name',
+});
+
+Ext.define('pve-acme-plugins', {
+ extend: 'Ext.data.Model',
+ fields: ['type', 'plugin', 'api'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/acme/plugins",
+ },
+ idProperty: 'plugin',
+});
+
+Ext.define('PVE.dc.ACMEAccountView', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveACMEAccountView',
+
+ title: gettext('Accounts'),
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ addAccount: function() {
+ let me = this;
+ let view = me.getView();
+ let defaultExists = view.getStore().findExact('name', 'default') !== -1;
+ Ext.create('PVE.node.ACMEAccountCreate', {
+ defaultExists,
+ taskDone: function() {
+ me.reload();
+ },
+ }).show();
+ },
+
+ viewAccount: function() {
+ let me = this;
+ let view = me.getView();
+ let selection = view.getSelection();
+ if (selection.length < 1) return;
+ Ext.create('PVE.node.ACMEAccountView', {
+ accountname: selection[0].data.name,
+ }).show();
+ },
+
+ reload: function() {
+ let me = this;
+ let view = me.getView();
+ view.getStore().rstore.load();
+ },
+
+ showTaskAndReload: function(options, success, response) {
+ let me = this;
+ if (!success) return;
+
+ let upid = response.result.data;
+ Ext.create('Proxmox.window.TaskProgress', {
+ upid,
+ taskDone: function() {
+ me.reload();
+ },
+ }).show();
+ },
+ },
+
+ minHeight: 150,
+ emptyText: gettext('No Accounts configured'),
+
+ columns: [
+ {
+ dataIndex: 'name',
+ text: gettext('Name'),
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Add'),
+ selModel: false,
+ handler: 'addAccount',
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('View'),
+ handler: 'viewAccount',
+ disabled: true,
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ baseurl: '/cluster/acme/account',
+ callback: 'showTaskAndReload',
+ },
+ ],
+
+ listeners: {
+ itemdblclick: 'viewAccount',
+ },
+
+ store: {
+ type: 'diff',
+ autoDestroy: true,
+ autoDestroyRstore: true,
+ rstore: {
+ type: 'update',
+ storeid: 'pve-acme-accounts',
+ model: 'pve-acme-accounts',
+ autoStart: true,
+ },
+ sorters: 'name',
+ },
+});
+
+Ext.define('PVE.dc.ACMEPluginView', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveACMEPluginView',
+
+ title: gettext('Challenge Plugins'),
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ addPlugin: function() {
+ let me = this;
+ Ext.create('PVE.dc.ACMEPluginEditor', {
+ isCreate: true,
+ apiCallDone: function() {
+ me.reload();
+ },
+ }).show();
+ },
+
+ editPlugin: function() {
+ let me = this;
+ let view = me.getView();
+ let selection = view.getSelection();
+ if (selection.length < 1) return;
+ let plugin = selection[0].data.plugin;
+ Ext.create('PVE.dc.ACMEPluginEditor', {
+ url: `/cluster/acme/plugins/${plugin}`,
+ apiCallDone: function() {
+ me.reload();
+ },
+ }).show();
+ },
+
+ reload: function() {
+ let me = this;
+ let view = me.getView();
+ view.getStore().rstore.load();
+ },
+ },
+
+ minHeight: 150,
+ emptyText: gettext('No Plugins configured'),
+
+ columns: [
+ {
+ dataIndex: 'plugin',
+ text: gettext('Plugin'),
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ {
+ dataIndex: 'api',
+ text: 'API',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Add'),
+ handler: 'addPlugin',
+ selModel: false,
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ handler: 'editPlugin',
+ disabled: true,
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ baseurl: '/cluster/acme/plugins',
+ callback: 'reload',
+ },
+ ],
+
+ listeners: {
+ itemdblclick: 'editPlugin',
+ },
+
+ store: {
+ type: 'diff',
+ autoDestroy: true,
+ autoDestroyRstore: true,
+ rstore: {
+ type: 'update',
+ storeid: 'pve-acme-plugins',
+ model: 'pve-acme-plugins',
+ autoStart: true,
+ filters: item => !!item.data.api,
+ },
+ sorters: 'plugin',
+ },
+});
+
+Ext.define('PVE.dc.ACMEClusterView', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveACMEClusterView',
+
+ onlineHelp: 'sysadmin_certificate_management',
+
+ items: [
+ {
+ region: 'north',
+ border: false,
+ xtype: 'pveACMEAccountView',
+ },
+ {
+ region: 'center',
+ border: false,
+ xtype: 'pveACMEPluginView',
+ },
+ ],
+});
+Ext.define('PVE.dc.ACMEPluginEditor', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveACMEPluginEditor',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'sysadmin_certs_acme_plugins',
+
+ isAdd: true,
+ isCreate: false,
+
+ width: 550,
+ url: '/cluster/acme/plugins/',
+
+ subject: 'ACME DNS Plugin',
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ // we dynamically create fields from the given schema
+ // things we have to do here:
+ // * save which fields we created to remove them again
+ // * split the data from the generic 'data' field into the boxes
+ // * on deletion collect those values again
+ // * save the original values of the data field
+ createdFields: {},
+ createdInitially: false,
+ originalValues: {},
+ createSchemaFields: function(schema) {
+ let me = this;
+ // we know where to add because we define it right below
+ let container = me.down('container');
+ let datafield = me.down('field[name=data]');
+ let hintfield = me.down('field[name=hint]');
+ if (!me.createdInitially) {
+ [me.originalValues] = PVE.Parser.parseACMEPluginData(datafield.getValue());
+ }
+
+ // collect values from custom fields and add it to 'data'',
+ // then remove the custom fields
+ let data = [];
+ for (const [name, field] of Object.entries(me.createdFields)) {
+ let value = field.getValue();
+ if (value !== undefined && value !== null && value !== '') {
+ data.push(`${name}=${value}`);
+ }
+ container.remove(field);
+ }
+ let datavalue = datafield.getValue();
+ if (datavalue !== undefined && datavalue !== null && datavalue !== '') {
+ data.push(datavalue);
+ }
+ datafield.setValue(data.join('\n'));
+
+ me.createdFields = {};
+
+ if (typeof schema.fields !== 'object') {
+ schema.fields = {};
+ }
+ // create custom fields according to schema
+ let gotSchemaField = false;
+ let cmp = (a, b) => a[0].localeCompare(b[0]);
+ for (const [name, definition] of Object.entries(schema.fields).sort(cmp)) {
+ let xtype;
+ switch (definition.type) {
+ case 'string':
+ xtype = 'proxmoxtextfield';
+ break;
+ case 'integer':
+ xtype = 'proxmoxintegerfield';
+ break;
+ case 'number':
+ xtype = 'numberfield';
+ break;
+ default:
+ console.warn(`unknown type '${definition.type}'`);
+ xtype = 'proxmoxtextfield';
+ break;
+ }
+
+ let label = name;
+ if (typeof definition.name === "string") {
+ label = definition.name;
+ }
+
+ let field = Ext.create({
+ xtype,
+ name: `custom_${name}`,
+ fieldLabel: label,
+ width: '100%',
+ labelWidth: 150,
+ labelSeparator: '=',
+ emptyText: definition.default || '',
+ autoEl: definition.description ? {
+ tag: 'div',
+ 'data-qtip': definition.description,
+ } : undefined,
+ });
+
+ me.createdFields[name] = field;
+ container.add(field);
+ gotSchemaField = true;
+ }
+ datafield.setHidden(gotSchemaField); // prefer schema-fields
+
+ if (schema.description) {
+ hintfield.setValue(schema.description);
+ hintfield.setHidden(false);
+ } else {
+ hintfield.setValue('');
+ hintfield.setHidden(true);
+ }
+
+ // parse data from field and set it to the custom ones
+ let extradata = [];
+ [data, extradata] = PVE.Parser.parseACMEPluginData(datafield.getValue());
+ for (const [key, value] of Object.entries(data)) {
+ if (me.createdFields[key]) {
+ me.createdFields[key].setValue(value);
+ me.createdFields[key].originalValue = me.originalValues[key];
+ } else {
+ extradata.push(`${key}=${value}`);
+ }
+ }
+ datafield.setValue(extradata.join('\n'));
+ if (!me.createdInitially) {
+ datafield.resetOriginalValue();
+ me.createdInitially = true; // save that we initally set that
+ }
+ },
+ onGetValues: function(values) {
+ let me = this;
+ let win = me.up('pveACMEPluginEditor');
+ if (win.isCreate) {
+ values.id = values.plugin;
+ values.type = 'dns'; // the only one for now
+ }
+ delete values.plugin;
+
+ PVE.Utils.delete_if_default(values, 'validation-delay', '30', win.isCreate);
+
+ let data = '';
+ for (const [name, field] of Object.entries(me.createdFields)) {
+ let value = field.getValue();
+ if (value !== null && value !== undefined && value !== '') {
+ data += `${name}=${value}\n`;
+ }
+ delete values[`custom_${name}`];
+ }
+ values.data = Ext.util.Base64.encode(data + values.data);
+ return values;
+ },
+ items: [
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: (get) => get('isCreate'),
+ submitValue: (get) => get('isCreate'),
+ },
+ editConfig: {
+ flex: 1,
+ xtype: 'proxmoxtextfield',
+ allowBlank: false,
+ },
+ name: 'plugin',
+ labelWidth: 150,
+ fieldLabel: gettext('Plugin ID'),
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'validation-delay',
+ labelWidth: 150,
+ fieldLabel: gettext('Validation Delay'),
+ emptyText: 30,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ minValue: 0,
+ maxValue: 48*60*60,
+ },
+ {
+ xtype: 'pveACMEApiSelector',
+ name: 'api',
+ labelWidth: 150,
+ listeners: {
+ change: function(selector) {
+ let schema = selector.getSchema();
+ selector.up('inputpanel').createSchemaFields(schema);
+ },
+ },
+ },
+ {
+ xtype: 'textarea',
+ fieldLabel: gettext('API Data'),
+ labelWidth: 150,
+ name: 'data',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Hint'),
+ labelWidth: 150,
+ name: 'hint',
+ hidden: true,
+ },
+ ],
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, opts) {
+ me.setValues(response.result.data);
+ },
+ });
+ } else {
+ me.method = 'POST';
+ }
+ },
+});
+Ext.define('PVE.panel.AuthBase', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveAuthBasePanel',
+
+ type: '',
+
+ onGetValues: function(values) {
+ let me = this;
+
+ if (!values.port) {
+ if (!me.isCreate) {
+ Proxmox.Utils.assemble_field_data(values, { 'delete': 'port' });
+ }
+ delete values.port;
+ }
+
+ if (me.isCreate) {
+ values.type = me.type;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let options = PVE.Utils.authSchema[me.type];
+
+ if (!me.column1) { me.column1 = []; }
+ if (!me.column2) { me.column2 = []; }
+ if (!me.columnB) { me.columnB = []; }
+
+ // first field is name
+ me.column1.unshift({
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'realm',
+ fieldLabel: gettext('Realm'),
+ value: me.realm,
+ allowBlank: false,
+ });
+
+ // last field is default'
+ me.column1.push({
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Default'),
+ name: 'default',
+ uncheckedValue: 0,
+ });
+
+ if (options.tfa) {
+ // last field of column2is tfa
+ me.column2.push({
+ xtype: 'pveTFASelector',
+ deleteEmpty: !me.isCreate,
+ });
+ }
+
+ me.columnB.push({
+ xtype: 'textfield',
+ name: 'comment',
+ fieldLabel: gettext('Comment'),
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.dc.AuthEditBase', {
+ extend: 'Proxmox.window.Edit',
+
+ onlineHelp: 'pveum_authentication_realms',
+
+ isAdd: true,
+
+ fieldDefaults: {
+ labelWidth: 120,
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = !me.realm;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/access/domains';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/access/domains/' + me.realm;
+ me.method = 'PUT';
+ }
+
+ let authConfig = PVE.Utils.authSchema[me.authType];
+ if (!authConfig) {
+ throw 'unknown auth type';
+ } else if (!authConfig.add && me.isCreate) {
+ throw 'trying to add non addable realm';
+ }
+
+ me.subject = authConfig.name;
+
+ let items;
+ let bodyPadding;
+ if (authConfig.syncipanel) {
+ bodyPadding = 0;
+ items = {
+ xtype: 'tabpanel',
+ region: 'center',
+ layout: 'fit',
+ bodyPadding: 10,
+ items: [
+ {
+ title: gettext('General'),
+ realm: me.realm,
+ xtype: authConfig.ipanel,
+ isCreate: me.isCreate,
+ type: me.authType,
+ },
+ {
+ title: gettext('Sync Options'),
+ realm: me.realm,
+ xtype: authConfig.syncipanel,
+ isCreate: me.isCreate,
+ type: me.authType,
+ },
+ ],
+ };
+ } else {
+ items = [{
+ realm: me.realm,
+ xtype: authConfig.ipanel,
+ isCreate: me.isCreate,
+ type: me.authType,
+ }];
+ }
+
+ Ext.apply(me, {
+ items,
+ bodyPadding,
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data || {};
+ // just to be sure (should not happen)
+ if (data.type !== me.authType) {
+ me.close();
+ throw "got wrong auth type";
+ }
+ me.setValues(data);
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.panel.ADInputPanel', {
+ extend: 'PVE.panel.AuthBase',
+ xtype: 'pveAuthADPanel',
+
+ initComponent: function() {
+ let me = this;
+
+ if (me.type !== 'ad') {
+ throw 'invalid type';
+ }
+
+ me.column1 = [
+ {
+ xtype: 'textfield',
+ name: 'domain',
+ fieldLabel: gettext('Domain'),
+ emptyText: 'company.net',
+ allowBlank: false,
+ },
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Server'),
+ name: 'server1',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Fallback Server'),
+ deleteEmpty: !me.isCreate,
+ name: 'server2',
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'port',
+ fieldLabel: gettext('Port'),
+ minValue: 1,
+ maxValue: 65535,
+ emptyText: gettext('Default'),
+ submitEmptyText: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: 'SSL',
+ name: 'secure',
+ uncheckedValue: 0,
+ listeners: {
+ change: function(field, newValue) {
+ let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]');
+ if (newValue === true) {
+ verifyCheckbox.enable();
+ } else {
+ verifyCheckbox.disable();
+ verifyCheckbox.setValue(0);
+ }
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Verify Certificate'),
+ name: 'verify',
+ unceckedValue: 0,
+ disabled: true,
+ checked: false,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Verify SSL certificate of the server'),
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+ onGetValues: function(values) {
+ let me = this;
+
+ if (!values.verify) {
+ if (!me.isCreate) {
+ Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' });
+ }
+ delete values.verify;
+ }
+
+ return me.callParent([values]);
+ },
+});
+Ext.define('PVE.panel.LDAPInputPanel', {
+ extend: 'PVE.panel.AuthBase',
+ xtype: 'pveAuthLDAPPanel',
+
+ initComponent: function() {
+ let me = this;
+
+ if (me.type !== 'ldap') {
+ throw 'invalid type';
+ }
+
+ me.column1 = [
+ {
+ xtype: 'textfield',
+ name: 'base_dn',
+ fieldLabel: gettext('Base Domain Name'),
+ emptyText: 'CN=Users,DC=Company,DC=net',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'user_attr',
+ emptyText: 'uid / sAMAccountName',
+ fieldLabel: gettext('User Attribute Name'),
+ allowBlank: false,
+ },
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Server'),
+ name: 'server1',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Fallback Server'),
+ deleteEmpty: !me.isCreate,
+ name: 'server2',
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'port',
+ fieldLabel: gettext('Port'),
+ minValue: 1,
+ maxValue: 65535,
+ emptyText: gettext('Default'),
+ submitEmptyText: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: 'SSL',
+ name: 'secure',
+ uncheckedValue: 0,
+ listeners: {
+ change: function(field, newValue) {
+ let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]');
+ if (newValue === true) {
+ verifyCheckbox.enable();
+ } else {
+ verifyCheckbox.disable();
+ verifyCheckbox.setValue(0);
+ }
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Verify Certificate'),
+ name: 'verify',
+ unceckedValue: 0,
+ disabled: true,
+ checked: false,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Verify SSL certificate of the server'),
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+ onGetValues: function(values) {
+ let me = this;
+
+ if (!values.verify) {
+ if (!me.isCreate) {
+ Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' });
+ }
+ delete values.verify;
+ }
+
+ return me.callParent([values]);
+ },
+});
+
+Ext.define('PVE.panel.LDAPSyncInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveAuthLDAPSyncPanel',
+
+ editableAttributes: ['email'],
+ editableDefaults: ['scope', 'enable-new'],
+ default_opts: {},
+ sync_attributes: {},
+
+ // (de)construct the sync-attributes from the list above,
+ // not touching all others
+ onGetValues: function(values) {
+ let me = this;
+ me.editableDefaults.forEach((attr) => {
+ if (values[attr]) {
+ me.default_opts[attr] = values[attr];
+ delete values[attr];
+ } else {
+ delete me.default_opts[attr];
+ }
+ });
+ let vanished_opts = [];
+ ['acl', 'entry', 'properties'].forEach((prop) => {
+ if (values[`remove-vanished-${prop}`]) {
+ vanished_opts.push(prop);
+ }
+ delete values[`remove-vanished-${prop}`];
+ });
+ me.default_opts['remove-vanished'] = vanished_opts.join(';');
+
+ values['sync-defaults-options'] = PVE.Parser.printPropertyString(me.default_opts);
+ me.editableAttributes.forEach((attr) => {
+ if (values[attr]) {
+ me.sync_attributes[attr] = values[attr];
+ delete values[attr];
+ } else {
+ delete me.sync_attributes[attr];
+ }
+ });
+ values.sync_attributes = PVE.Parser.printPropertyString(me.sync_attributes);
+
+ PVE.Utils.delete_if_default(values, 'sync-defaults-options');
+ PVE.Utils.delete_if_default(values, 'sync_attributes');
+
+ // Force values.delete to be an array
+ if (typeof values.delete === 'string') {
+ values.delete = values.delete.split(',');
+ }
+
+ if (me.isCreate) {
+ delete values.delete; // on create we cannot delete values
+ }
+
+ return values;
+ },
+
+ setValues: function(values) {
+ let me = this;
+ if (values.sync_attributes) {
+ me.sync_attributes = PVE.Parser.parsePropertyString(values.sync_attributes);
+ delete values.sync_attributes;
+ me.editableAttributes.forEach((attr) => {
+ if (me.sync_attributes[attr]) {
+ values[attr] = me.sync_attributes[attr];
+ }
+ });
+ }
+ if (values['sync-defaults-options']) {
+ me.default_opts = PVE.Parser.parsePropertyString(values['sync-defaults-options']);
+ delete values.default_opts;
+ me.editableDefaults.forEach((attr) => {
+ if (me.default_opts[attr]) {
+ values[attr] = me.default_opts[attr];
+ }
+ });
+
+ if (me.default_opts['remove-vanished']) {
+ let opts = me.default_opts['remove-vanished'].split(';');
+ for (const opt of opts) {
+ values[`remove-vanished-${opt}`] = 1;
+ }
+ }
+ }
+ return me.callParent([values]);
+ },
+
+ column1: [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'bind_dn',
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.noneText,
+ fieldLabel: gettext('Bind User'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ inputType: 'password',
+ name: 'password',
+ emptyText: gettext('Unchanged'),
+ fieldLabel: gettext('Bind Password'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'email',
+ fieldLabel: gettext('E-Mail attribute'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'group_name_attr',
+ deleteEmpty: true,
+ fieldLabel: gettext('Groupname attr.'),
+ },
+ {
+ xtype: 'displayfield',
+ value: gettext('Default Sync Options'),
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'scope',
+ emptyText: Proxmox.Utils.NoneText,
+ fieldLabel: gettext('Scope'),
+ value: '__default__',
+ deleteEmpty: false,
+ comboItems: [
+ ['__default__', Proxmox.Utils.NoneText],
+ ['users', gettext('Users')],
+ ['groups', gettext('Groups')],
+ ['both', gettext('Users and Groups')],
+ ],
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'user_classes',
+ fieldLabel: gettext('User classes'),
+ deleteEmpty: true,
+ emptyText: 'inetorgperson, posixaccount, person, user',
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'group_classes',
+ fieldLabel: gettext('Group classes'),
+ deleteEmpty: true,
+ emptyText: 'groupOfNames, group, univentionGroup, ipausergroup',
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'filter',
+ fieldLabel: gettext('User Filter'),
+ deleteEmpty: true,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'group_filter',
+ fieldLabel: gettext('Group Filter'),
+ deleteEmpty: true,
+ },
+ {
+ // fake for spacing
+ xtype: 'displayfield',
+ value: ' ',
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ value: '__default__',
+ deleteEmpty: false,
+ comboItems: [
+ [
+ '__default__',
+ Ext.String.format(
+ gettext("{0} ({1})"),
+ Proxmox.Utils.yesText,
+ Proxmox.Utils.defaultText,
+ ),
+ ],
+ ['1', Proxmox.Utils.yesText],
+ ['0', Proxmox.Utils.noText],
+ ],
+ name: 'enable-new',
+ fieldLabel: gettext('Enable new users'),
+ },
+ ],
+
+ columnB: [
+ {
+ xtype: 'fieldset',
+ title: gettext('Remove Vanished Options'),
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('ACL'),
+ name: 'remove-vanished-acl',
+ boxLabel: gettext('Remove ACLs of vanished users and groups.'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Entry'),
+ name: 'remove-vanished-entry',
+ boxLabel: gettext('Remove vanished user and group entries.'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Properties'),
+ name: 'remove-vanished-properties',
+ boxLabel: gettext('Remove vanished properties from synced users.'),
+ },
+ ],
+ },
+ ],
+});
+Ext.define('PVE.panel.OpenIDInputPanel', {
+ extend: 'PVE.panel.AuthBase',
+ xtype: 'pveAuthOpenIDPanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onGetValues: function(values) {
+ let me = this;
+
+ if (!values.verify) {
+ if (!me.isCreate) {
+ Proxmox.Utils.assemble_field_data(values, { 'delete': 'verify' });
+ }
+ delete values.verify;
+ }
+
+ return me.callParent([values]);
+ },
+
+ columnT: [
+ {
+ xtype: 'textfield',
+ name: 'issuer-url',
+ fieldLabel: gettext('Issuer URL'),
+ allowBlank: false,
+ },
+ ],
+
+ column1: [
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Client ID'),
+ name: 'client-id',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Client Key'),
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ name: 'client-key',
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Autocreate Users'),
+ name: 'autocreate',
+ value: 0,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'pmxDisplayEditField',
+ name: 'username-claim',
+ fieldLabel: gettext('Username Claim'),
+ editConfig: {
+ xtype: 'proxmoxKVComboBox',
+ editable: true,
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText],
+ ['subject', 'subject'],
+ ['username', 'username'],
+ ['email', 'email'],
+ ],
+ },
+ cbind: {
+ value: get => get('isCreate') ? '__default__' : Proxmox.Utils.defaultText,
+ deleteEmpty: '{!isCreate}',
+ editable: '{isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'scopes',
+ fieldLabel: gettext('Scopes'),
+ emptyText: `${Proxmox.Utils.defaultText} (email profile)`,
+ submitEmpty: false,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'prompt',
+ fieldLabel: gettext('Prompt'),
+ editable: true,
+ emptyText: gettext('Auth-Provider Default'),
+ comboItems: [
+ ['__default__', gettext('Auth-Provider Default')],
+ ['none', 'none'],
+ ['login', 'login'],
+ ['consent', 'consent'],
+ ['select_account', 'select_account'],
+ ],
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+
+ advancedColumnB: [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'acr-values',
+ fieldLabel: gettext('ACR Values'),
+ submitEmpty: false,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (me.type !== 'openid') {
+ throw 'invalid type';
+ }
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.dc.AuthView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveAuthView'],
+
+ onlineHelp: 'pveum_authentication_realms',
+
+ stateful: true,
+ stateId: 'grid-authrealms',
+
+ viewConfig: {
+ trackOver: false,
+ },
+
+ columns: [
+ {
+ header: gettext('Realm'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'realm',
+ },
+ {
+ header: gettext('Type'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'type',
+ },
+ {
+ header: gettext('TFA'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'tfa',
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+
+ store: {
+ model: 'pmx-domains',
+ sorters: {
+ property: 'realm',
+ direction: 'ASC',
+ },
+ },
+
+ openEditWindow: function(authType, realm) {
+ let me = this;
+ Ext.create('PVE.dc.AuthEditBase', {
+ authType,
+ realm,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ }).show();
+ },
+
+ reload: function() {
+ let me = this;
+ me.getStore().load();
+ },
+
+ run_editor: function() {
+ let me = this;
+ let rec = me.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ me.openEditWindow(rec.data.type, rec.data.realm);
+ },
+
+ open_sync_window: function() {
+ let me = this;
+ let rec = me.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ Ext.create('PVE.dc.SyncWindow', {
+ realm: rec.data.realm,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ }).show();
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ let items = [];
+ for (const [authType, config] of Object.entries(PVE.Utils.authSchema)) {
+ if (!config.add) { continue; }
+ items.push({
+ text: config.name,
+ iconCls: 'fa fa-fw ' + (config.iconCls || 'fa-address-book-o'),
+ handler: () => me.openEditWindow(authType),
+ });
+ }
+
+ Ext.apply(me, {
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: {
+ items: items,
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ disabled: true,
+ handler: () => me.run_editor(),
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ baseurl: '/access/domains/',
+ enableFn: (rec) => PVE.Utils.authSchema[rec.data.type].add,
+ callback: () => me.reload(),
+ },
+ '-',
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Sync'),
+ disabled: true,
+ enableFn: (rec) => Boolean(PVE.Utils.authSchema[rec.data.type].syncipanel),
+ handler: () => me.open_sync_window(),
+ },
+ ],
+ listeners: {
+ activate: () => me.reload(),
+ itemdblclick: () => me.run_editor(),
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.dc.BackupDiskTree', {
+ extend: 'Ext.tree.Panel',
+ alias: 'widget.pveBackupDiskTree',
+
+ folderSort: true,
+ rootVisible: false,
+
+ store: {
+ sorters: 'id',
+ data: {},
+ },
+
+ tools: [
+ {
+ type: 'expand',
+ tooltip: gettext('Expand All'),
+ callback: panel => panel.expandAll(),
+ },
+ {
+ type: 'collapse',
+ tooltip: gettext('Collapse All'),
+ callback: panel => panel.collapseAll(),
+ },
+ ],
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: gettext('Guest Image'),
+ renderer: function(value, meta, record) {
+ if (record.data.type) {
+ // guest level
+ let ret = value;
+ if (record.data.name) {
+ ret += " (" + record.data.name + ")";
+ }
+ return ret;
+ } else {
+ // extJS needs unique IDs but we only want to show the volumes key from "vmid:key"
+ return value.split(':')[1] + " - " + record.data.name;
+ }
+ },
+ dataIndex: 'id',
+ flex: 6,
+ },
+ {
+ text: gettext('Type'),
+ dataIndex: 'type',
+ flex: 1,
+ },
+ {
+ text: gettext('Backup Job'),
+ renderer: PVE.Utils.render_backup_status,
+ dataIndex: 'included',
+ flex: 3,
+ },
+ ],
+
+ reload: function() {
+ let me = this;
+ let sm = me.getSelectionModel();
+
+ Proxmox.Utils.API2Request({
+ url: `/cluster/backup/${me.jobid}/included_volumes`,
+ waitMsgTarget: me,
+ method: 'GET',
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, response.htmlStatus);
+ },
+ success: function(response, opts) {
+ sm.deselectAll();
+ me.setRootNode(response.result.data);
+ me.expandAll();
+ },
+ });
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.jobid) {
+ throw "no job id specified";
+ }
+
+ var sm = Ext.create('Ext.selection.TreeModel', {});
+
+ Ext.apply(me, {
+ selModel: sm,
+ fields: ['id', 'type',
+ {
+ type: 'string',
+ name: 'iconCls',
+ calculate: function(data) {
+ var txt = 'fa x-fa-tree fa-';
+ if (data.leaf && !data.type) {
+ return txt + 'hdd-o';
+ } else if (data.type === 'qemu') {
+ return txt + 'desktop';
+ } else if (data.type === 'lxc') {
+ return txt + 'cube';
+ } else {
+ return txt + 'question-circle';
+ }
+ },
+ },
+ ],
+ header: {
+ items: [{
+ xtype: 'textfield',
+ fieldLabel: gettext('Search'),
+ labelWidth: 50,
+ emptyText: 'Name, VMID, Type',
+ width: 200,
+ padding: '0 5 0 0',
+ enableKeyEvents: true,
+ listeners: {
+ buffer: 500,
+ keyup: function(field) {
+ let searchValue = field.getValue().toLowerCase();
+ me.store.clearFilter(true);
+ me.store.filterBy(function(record) {
+ let data = {};
+ if (record.data.depth === 0) {
+ return true;
+ } else if (record.data.depth === 1) {
+ data = record.data;
+ } else if (record.data.depth === 2) {
+ data = record.parentNode.data;
+ }
+
+ for (const property of ['name', 'id', 'type']) {
+ if (!data[property]) {
+ continue;
+ }
+ let v = data[property].toString();
+ if (v !== undefined) {
+ v = v.toLowerCase();
+ if (v.includes(searchValue)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ });
+ },
+ },
+ }],
+ },
+ });
+
+ me.callParent();
+
+ me.reload();
+ },
+});
+
+Ext.define('PVE.dc.BackupInfo', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveBackupInfo',
+
+ viewModel: {
+ data: {
+ retentionType: 'none',
+ },
+ formulas: {
+ hasRetention: (get) => get('retentionType') !== 'none',
+ retentionKeepAll: (get) => get('retentionType') === 'all',
+ },
+ },
+
+ padding: '5 0 5 10',
+
+ column1: [
+ {
+ xtype: 'displayfield',
+ name: 'node',
+ fieldLabel: gettext('Node'),
+ renderer: value => value || `-- ${gettext('All')} --`,
+ },
+ {
+ xtype: 'displayfield',
+ name: 'storage',
+ fieldLabel: gettext('Storage'),
+ },
+ {
+ xtype: 'displayfield',
+ name: 'schedule',
+ fieldLabel: gettext('Schedule'),
+ },
+ {
+ xtype: 'displayfield',
+ name: 'next-run',
+ fieldLabel: gettext('Next Run'),
+ renderer: PVE.Utils.render_next_event,
+ },
+ {
+ xtype: 'displayfield',
+ name: 'selMode',
+ fieldLabel: gettext('Selection mode'),
+ },
+ ],
+ column2: [
+ {
+ xtype: 'displayfield',
+ name: 'mailnotification',
+ fieldLabel: gettext('Notification'),
+ renderer: function(value) {
+ let mailto = this.up('pveBackupInfo')?.record?.mailto || 'root@localhost';
+ let when = gettext('Always');
+ if (value === 'failure') {
+ when = gettext('On failure only');
+ }
+ return `${when} (${mailto})`;
+ },
+ },
+ {
+ xtype: 'displayfield',
+ name: 'compress',
+ fieldLabel: gettext('Compression'),
+ },
+ {
+ xtype: 'displayfield',
+ name: 'mode',
+ fieldLabel: gettext('Mode'),
+ renderer: function(value) {
+ const modeToDisplay = {
+ snapshot: gettext('Snapshot'),
+ stop: gettext('Stop'),
+ suspend: gettext('Snapshot'),
+ };
+ return modeToDisplay[value] ?? gettext('Unknown');
+ },
+ },
+ {
+ xtype: 'displayfield',
+ name: 'enabled',
+ fieldLabel: gettext('Enabled'),
+ renderer: v => PVE.Parser.parseBoolean(v.toString()) ? gettext('Yes') : gettext('No'),
+ },
+ {
+ xtype: 'displayfield',
+ name: 'pool',
+ fieldLabel: gettext('Pool to backup'),
+ },
+ ],
+
+ columnB: [
+ {
+ xtype: 'displayfield',
+ name: 'comment',
+ fieldLabel: gettext('Comment'),
+ },
+ {
+ xtype: 'fieldset',
+ title: gettext('Retention Configuration'),
+ layout: 'hbox',
+ collapsible: true,
+ defaults: {
+ border: false,
+ layout: 'anchor',
+ flex: 1,
+ },
+ bind: {
+ hidden: '{!hasRetention}',
+ },
+ items: [
+ {
+ padding: '0 10 0 0',
+ defaults: {
+ labelWidth: 110,
+ },
+ items: [{
+ xtype: 'displayfield',
+ name: 'keep-all',
+ fieldLabel: gettext('Keep All'),
+ renderer: Proxmox.Utils.format_boolean,
+ bind: {
+ hidden: '{!retentionKeepAll}',
+ },
+ }].concat(
+ [
+ ['keep-last', gettext('Keep Last')],
+ ['keep-hourly', gettext('Keep Hourly')],
+ ].map(
+ name => ({
+ xtype: 'displayfield',
+ name: name[0],
+ fieldLabel: name[1],
+ bind: {
+ hidden: '{!hasRetention || retentionKeepAll}',
+ },
+ }),
+ ),
+ ),
+ },
+ {
+ padding: '0 0 0 10',
+ defaults: {
+ labelWidth: 110,
+ },
+ items: [
+ ['keep-daily', gettext('Keep Daily')],
+ ['keep-weekly', gettext('Keep Weekly')],
+ ].map(
+ name => ({
+ xtype: 'displayfield',
+ name: name[0],
+ fieldLabel: name[1],
+ bind: {
+ hidden: '{!hasRetention || retentionKeepAll}',
+ },
+ }),
+ ),
+ },
+ {
+ padding: '0 0 0 10',
+ defaults: {
+ labelWidth: 110,
+ },
+ items: [
+ ['keep-monthly', gettext('Keep Monthly')],
+ ['keep-yearly', gettext('Keep Yearly')],
+ ].map(
+ name => ({
+ xtype: 'displayfield',
+ name: name[0],
+ fieldLabel: name[1],
+ bind: {
+ hidden: '{!hasRetention || retentionKeepAll}',
+ },
+ }),
+ ),
+ },
+ ],
+ },
+ ],
+
+ setValues: function(values) {
+ var me = this;
+ let vm = me.getViewModel();
+
+ Ext.iterate(values, function(fieldId, val) {
+ let field = me.query('[isFormField][name=' + fieldId + ']')[0];
+ if (field) {
+ field.setValue(val);
+ }
+ });
+
+ if (values['prune-backups'] || values.maxfiles !== undefined) {
+ let keepValues;
+ if (values['prune-backups']) {
+ keepValues = values['prune-backups'];
+ } else if (values.maxfiles > 0) {
+ keepValues = { 'keep-last': values.maxfiles };
+ } else {
+ keepValues = { 'keep-all': 1 };
+ }
+
+ vm.set('retentionType', keepValues['keep-all'] ? 'all' : 'other');
+
+ // set values of all keep-X fields
+ ['all', 'last', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'].forEach(time => {
+ let name = `keep-${time}`;
+ me.query(`[isFormField][name=${name}]`)[0]?.setValue(keepValues[name]);
+ });
+ } else {
+ vm.set('retentionType', 'none');
+ }
+
+ // selection Mode depends on the presence/absence of several keys
+ let selModeField = me.query('[isFormField][name=selMode]')[0];
+ let selMode = 'none';
+ if (values.vmid) {
+ selMode = gettext('Include selected VMs');
+ }
+ if (values.all) {
+ selMode = gettext('All');
+ }
+ if (values.exclude) {
+ selMode = gettext('Exclude selected VMs');
+ }
+ if (values.pool) {
+ selMode = gettext('Pool based');
+ }
+ selModeField.setValue(selMode);
+
+ if (!values.pool) {
+ let poolField = me.query('[isFormField][name=pool]')[0];
+ poolField.setVisible(0);
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.record) {
+ throw "no data provided";
+ }
+ me.callParent();
+
+ me.setValues(me.record);
+ },
+});
+
+
+Ext.define('PVE.dc.BackedGuests', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveBackedGuests',
+
+ stateful: true,
+ stateId: 'grid-dc-backed-guests',
+
+ textfilter: '',
+
+ columns: [
+ {
+ header: gettext('Type'),
+ dataIndex: "type",
+ renderer: PVE.Utils.render_resource_type,
+ flex: 1,
+ sortable: true,
+ },
+ {
+ header: gettext('VMID'),
+ dataIndex: 'vmid',
+ flex: 1,
+ sortable: true,
+ },
+ {
+ header: gettext('Name'),
+ dataIndex: 'name',
+ flex: 2,
+ sortable: true,
+ },
+ ],
+ viewConfig: {
+ stripeRows: true,
+ trackOver: false,
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.store.clearFilter(true);
+
+ Ext.apply(me, {
+ tbar: [
+ '->',
+ gettext('Search') + ':',
+ ' ',
+ {
+ xtype: 'textfield',
+ width: 200,
+ emptyText: 'Name, VMID, Type',
+ enableKeyEvents: true,
+ listeners: {
+ buffer: 500,
+ keyup: function(field) {
+ let searchValue = field.getValue().toLowerCase();
+ me.store.clearFilter(true);
+ me.store.filterBy(function(record) {
+ let data = record.data;
+ for (const property of ['name', 'vmid', 'type']) {
+ if (data[property] === null) {
+ continue;
+ }
+ let v = data[property].toString();
+ if (v !== undefined) {
+ if (v.toLowerCase().includes(searchValue)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ });
+ },
+ },
+ },
+ ],
+ });
+ me.callParent();
+ },
+});
+Ext.define('PVE.dc.BackupEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pveDcBackupEdit'],
+
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ defaultFocus: undefined,
+
+ subject: gettext("Backup Job"),
+ bodyPadding: 0,
+
+ url: '/api2/extjs/cluster/backup',
+ method: 'POST',
+ isCreate: true,
+
+ cbindData: function() {
+ let me = this;
+ if (me.jobid) {
+ me.isCreate = false;
+ me.method = 'PUT';
+ me.url += `/${me.jobid}`;
+ }
+ return {};
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ onGetValues: function(values) {
+ let me = this;
+ let isCreate = me.getView().isCreate;
+ if (!values.node) {
+ if (!isCreate) {
+ Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
+ }
+ delete values.node;
+ }
+
+ if (!values.id && isCreate) {
+ values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
+ }
+
+ let selMode = values.selMode;
+ delete values.selMode;
+
+ if (selMode === 'all') {
+ values.all = 1;
+ values.exclude = '';
+ delete values.vmid;
+ } else if (selMode === 'exclude') {
+ values.all = 1;
+ values.exclude = values.vmid;
+ delete values.vmid;
+ } else if (selMode === 'pool') {
+ delete values.vmid;
+ }
+
+ if (selMode !== 'pool') {
+ delete values.pool;
+ }
+ return values;
+ },
+
+ nodeChange: function(f, value) {
+ let me = this;
+ me.lookup('storageSelector').setNodename(value);
+ let vmgrid = me.lookup('vmgrid');
+ let store = vmgrid.getStore();
+
+ store.clearFilter();
+ store.filterBy(function(rec) {
+ return !value || rec.get('node') === value;
+ });
+
+ let mode = me.lookup('modeSelector').getValue();
+ if (mode === 'all') {
+ vmgrid.selModel.selectAll(true);
+ }
+ if (mode === 'pool') {
+ me.selectPoolMembers();
+ }
+ },
+
+ storageChange: function(f, v) {
+ let me = this;
+ let rec = f.getStore().findRecord('storage', v, 0, false, true, true);
+ let compressionSelector = me.lookup('compressionSelector');
+
+ if (rec?.data?.type === 'pbs') {
+ compressionSelector.setValue('zstd');
+ compressionSelector.setDisabled(true);
+ } else if (!compressionSelector.getEditable()) {
+ compressionSelector.setDisabled(false);
+ }
+ },
+
+ selectPoolMembers: function() {
+ let me = this;
+ let vmgrid = me.lookup('vmgrid');
+ let poolid = me.lookup('poolSelector').getValue();
+
+ vmgrid.getSelectionModel().deselectAll(true);
+ if (!poolid) {
+ return;
+ }
+ vmgrid.getStore().filter([
+ {
+ id: 'poolFilter',
+ property: 'pool',
+ value: poolid,
+ },
+ ]);
+ vmgrid.selModel.selectAll(true);
+ },
+
+ modeChange: function(f, value, oldValue) {
+ let me = this;
+ let vmgrid = me.lookup('vmgrid');
+ vmgrid.getStore().removeFilter('poolFilter');
+
+ if (oldValue === 'all' && value !== 'all') {
+ vmgrid.getSelectionModel().deselectAll(true);
+ }
+
+ if (value === 'all') {
+ vmgrid.getSelectionModel().selectAll(true);
+ }
+
+ if (value === 'pool') {
+ me.selectPoolMembers();
+ }
+ },
+
+ init: function(view) {
+ let me = this;
+ if (view.isCreate) {
+ me.lookup('modeSelector').setValue('include');
+ } else {
+ view.load({
+ success: function(response, _options) {
+ let data = response.result.data;
+
+ if (data.exclude) {
+ data.vmid = data.exclude;
+ data.selMode = 'exclude';
+ } else if (data.all) {
+ data.vmid = '';
+ data.selMode = 'all';
+ } else if (data.pool) {
+ data.selMode = 'pool';
+ data.selPool = data.pool;
+ } else {
+ data.selMode = 'include';
+ }
+
+ me.getViewModel().set('selMode', data.selMode);
+
+ if (data['prune-backups']) {
+ Object.assign(data, data['prune-backups']);
+ delete data['prune-backups'];
+ } else if (data.maxfiles !== undefined) {
+ if (data.maxfiles > 0) {
+ data['keep-last'] = data.maxfiles;
+ } else {
+ data['keep-all'] = 1;
+ }
+ delete data.maxfiles;
+ }
+
+ if (data['notes-template']) {
+ data['notes-template'] =
+ PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
+ }
+
+ view.setValues(data);
+ },
+ });
+ }
+ },
+ },
+
+ viewModel: {
+ data: {
+ selMode: 'include',
+ },
+
+ formulas: {
+ poolMode: (get) => get('selMode') === 'pool',
+ disableVMSelection: (get) => get('selMode') !== 'include' && get('selMode') !== 'exclude',
+ },
+ },
+
+ items: [
+ {
+ xtype: 'tabpanel',
+ region: 'center',
+ layout: 'fit',
+ bodyPadding: 10,
+ items: [
+ {
+ xtype: 'container',
+ title: gettext('General'),
+ region: 'center',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ items: [
+ {
+ xtype: 'inputpanel',
+ onlineHelp: 'chapter_vzdump',
+ column1: [
+ {
+ xtype: 'pveNodeSelector',
+ name: 'node',
+ fieldLabel: gettext('Node'),
+ allowBlank: true,
+ editable: true,
+ autoSelect: false,
+ emptyText: '-- ' + gettext('All') + ' --',
+ listeners: {
+ change: 'nodeChange',
+ },
+ },
+ {
+ xtype: 'pveStorageSelector',
+ reference: 'storageSelector',
+ fieldLabel: gettext('Storage'),
+ clusterView: true,
+ storageContent: 'backup',
+ allowBlank: false,
+ name: 'storage',
+ listeners: {
+ change: 'storageChange',
+ },
+ },
+ {
+ xtype: 'pveCalendarEvent',
+ fieldLabel: gettext('Schedule'),
+ allowBlank: false,
+ name: 'schedule',
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ reference: 'modeSelector',
+ comboItems: [
+ ['include', gettext('Include selected VMs')],
+ ['all', gettext('All')],
+ ['exclude', gettext('Exclude selected VMs')],
+ ['pool', gettext('Pool based')],
+ ],
+ fieldLabel: gettext('Selection mode'),
+ name: 'selMode',
+ value: '',
+ bind: {
+ value: '{selMode}',
+ },
+ listeners: {
+ change: 'modeChange',
+ },
+ },
+ {
+ xtype: 'pvePoolSelector',
+ reference: 'poolSelector',
+ fieldLabel: gettext('Pool to backup'),
+ hidden: true,
+ allowBlank: false,
+ name: 'pool',
+ listeners: {
+ change: 'selectPoolMembers',
+ },
+ bind: {
+ hidden: '{!poolMode}',
+ disabled: '{!poolMode}',
+ },
+ },
+ ],
+ column2: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Send email to'),
+ name: 'mailto',
+ },
+ {
+ xtype: 'pveEmailNotificationSelector',
+ fieldLabel: gettext('Email'),
+ name: 'mailnotification',
+ cbind: {
+ value: (get) => get('isCreate') ? 'always' : '',
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'pveCompressionSelector',
+ reference: 'compressionSelector',
+ fieldLabel: gettext('Compression'),
+ name: 'compress',
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ value: 'zstd',
+ },
+ {
+ xtype: 'pveBackupModeSelector',
+ fieldLabel: gettext('Mode'),
+ value: 'snapshot',
+ name: 'mode',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Enable'),
+ name: 'enabled',
+ uncheckedValue: 0,
+ defaultValue: 1,
+ checked: true,
+ },
+ ],
+ columnB: [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'comment',
+ fieldLabel: gettext('Job Comment'),
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Description of the job'),
+ },
+ },
+ {
+ xtype: 'vmselector',
+ reference: 'vmgrid',
+ height: 300,
+ name: 'vmid',
+ disabled: true,
+ allowBlank: false,
+ columnSelection: ['vmid', 'node', 'status', 'name', 'type'],
+ bind: {
+ disabled: '{disableVMSelection}',
+ },
+ },
+ ],
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Repeat missed'),
+ name: 'repeat-missed',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ cbind: {
+ deleteDefaultValue: '{!isCreate}',
+ },
+ },
+ ],
+ onGetValues: function(values) {
+ return this.up('window').getController().onGetValues(values);
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'pveBackupJobPrunePanel',
+ title: gettext('Retention'),
+ cbind: {
+ isCreate: '{isCreate}',
+ },
+ keepAllDefaultForCreate: false,
+ showPBSHint: false,
+ fallbackHintHtml: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
+ },
+ {
+ xtype: 'inputpanel',
+ title: gettext('Note Template'),
+ region: 'center',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ onGetValues: function(values) {
+ if (values['notes-template']) {
+ values['notes-template'] =
+ PVE.Utils.escapeNotesTemplate(values['notes-template']);
+ }
+ return values;
+ },
+ items: [
+ {
+ xtype: 'textarea',
+ name: 'notes-template',
+ fieldLabel: gettext('Backup Notes'),
+ height: 100,
+ maxLength: 512,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ value: (get) => get('isCreate') ? '{{guestname}}' : undefined,
+ },
+ },
+ {
+ xtype: 'box',
+ style: {
+ margin: '8px 0px',
+ 'line-height': '1.5em',
+ },
+ html: gettext('The notes are added to each backup created by this job.')
+ + '
'
+ + Ext.String.format(
+ gettext('Possible template variables are: {0}'),
+ PVE.Utils.notesTemplateVars.map(v => `{{${v}}}`).join(', '),
+ ),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+});
+
+Ext.define('PVE.dc.BackupView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveDcBackupView'],
+
+ onlineHelp: 'chapter_vzdump',
+
+ allText: '-- ' + gettext('All') + ' --',
+
+ initComponent: function() {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'pve-cluster-backup',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/backup",
+ },
+ });
+
+ let not_backed_store = new Ext.data.Store({
+ sorters: 'vmid',
+ proxy: {
+ type: 'proxmox',
+ url: 'api2/json/cluster/backup-info/not-backed-up',
+ },
+ });
+
+ let noBackupJobInfoButton;
+ let reload = function() {
+ store.load();
+ not_backed_store.load({
+ callback: records => noBackupJobInfoButton.setVisible(records.length > 0),
+ });
+ };
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ let win = Ext.create('PVE.dc.BackupEdit', {
+ jobid: rec.data.id,
+ });
+ win.on('destroy', reload);
+ win.show();
+ };
+
+ let run_detail = function() {
+ let record = sm.getSelection()[0];
+ if (!record) {
+ return;
+ }
+ Ext.create('Ext.window.Window', {
+ modal: true,
+ width: 800,
+ height: Ext.getBody().getViewSize().height > 1000 ? 800 : 600, // factor out as common infra?
+ resizable: true,
+ layout: 'fit',
+ title: gettext('Backup Details'),
+ items: [
+ {
+ xtype: 'panel',
+ region: 'center',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ items: [
+ {
+ xtype: 'pveBackupInfo',
+ flex: 0,
+ layout: 'fit',
+ record: record.data,
+ },
+ {
+ xtype: 'pveBackupDiskTree',
+ title: gettext('Included disks'),
+ flex: 1,
+ jobid: record.data.id,
+ },
+ ],
+ },
+ ],
+ }).show();
+ };
+
+ let run_backup_now = function(job) {
+ job = Ext.clone(job);
+
+ let jobNode = job.node;
+ // Remove properties related to scheduling
+ delete job.enabled;
+ delete job.starttime;
+ delete job.dow;
+ delete job.id;
+ delete job.schedule;
+ delete job.type;
+ delete job.node;
+ delete job.comment;
+ delete job['next-run'];
+ delete job['repeat-missed'];
+ job.all = job.all === true ? 1 : 0;
+
+ ['performance', 'prune-backups'].forEach(key => {
+ if (job[key]) {
+ job[key] = PVE.Parser.printPropertyString(job[key]);
+ }
+ });
+
+ let allNodes = PVE.data.ResourceStore.getNodes();
+ let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
+ let errors = [];
+
+ if (jobNode !== undefined) {
+ if (!nodes.includes(jobNode)) {
+ Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
+ return;
+ }
+ nodes = [jobNode];
+ } else {
+ let unkownNodes = allNodes.filter(node => node.status !== 'online');
+ if (unkownNodes.length > 0) {errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));}
+ }
+ let jobTotalCount = nodes.length, jobsStarted = 0;
+
+ Ext.Msg.show({
+ title: gettext('Please wait...'),
+ closable: false,
+ progress: true,
+ progressText: '0/' + jobTotalCount,
+ });
+
+ let postRequest = function() {
+ jobsStarted++;
+ Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
+
+ if (jobsStarted === jobTotalCount) {
+ Ext.Msg.hide();
+ if (errors.length > 0) {
+ Ext.Msg.alert('Error', 'Some errors have been encountered:
' + errors.join('
'));
+ }
+ }
+ };
+
+ nodes.forEach(node => Proxmox.Utils.API2Request({
+ url: '/nodes/' + node + '/vzdump',
+ method: 'POST',
+ params: job,
+ failure: function(response, opts) {
+ errors.push(node + ': ' + response.htmlStatus);
+ postRequest();
+ },
+ success: postRequest,
+ }));
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ var run_btn = new Proxmox.button.Button({
+ text: gettext('Run now'),
+ disabled: true,
+ selModel: sm,
+ handler: function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: Ext.Msg.QUESTION,
+ msg: gettext('Start the selected backup job now?'),
+ buttons: Ext.Msg.YESNO,
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ run_backup_now(rec.data);
+ },
+ });
+ },
+ });
+
+ var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/cluster/backup',
+ callback: function() {
+ reload();
+ },
+ });
+
+ var detail_btn = new Proxmox.button.Button({
+ text: gettext('Job Detail'),
+ disabled: true,
+ tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'),
+ selModel: sm,
+ handler: run_detail,
+ });
+
+ noBackupJobInfoButton = new Proxmox.button.Button({
+ text: `${gettext('Show')}: ${gettext('Guests Without Backup Job')}`,
+ tooltip: gettext('Some guests are not covered by any backup job.'),
+ iconCls: 'fa fa-fw fa-exclamation-circle',
+ hidden: true,
+ handler: () => {
+ Ext.create('Ext.window.Window', {
+ autoShow: true,
+ modal: true,
+ width: 600,
+ height: 500,
+ resizable: true,
+ layout: 'fit',
+ title: gettext('Guests Without Backup Job'),
+ items: [
+ {
+ xtype: 'panel',
+ region: 'center',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ items: [
+ {
+ xtype: 'pveBackedGuests',
+ flex: 1,
+ layout: 'fit',
+ store: not_backed_store,
+ },
+ ],
+ },
+ ],
+ });
+ },
+ });
+
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ stateful: true,
+ stateId: 'grid-dc-backup',
+ viewConfig: {
+ trackOver: false,
+ },
+ dockedItems: [{
+ xtype: 'toolbar',
+ overflowHandler: 'scroller',
+ dock: 'top',
+ items: [
+ {
+ text: gettext('Add'),
+ handler: function() {
+ var win = Ext.create('PVE.dc.BackupEdit', {});
+ win.on('destroy', reload);
+ win.show();
+ },
+ },
+ '-',
+ remove_btn,
+ edit_btn,
+ detail_btn,
+ '-',
+ run_btn,
+ '->',
+ noBackupJobInfoButton,
+ '-',
+ {
+ xtype: 'proxmoxButton',
+ selModel: null,
+ text: gettext('Schedule Simulator'),
+ handler: () => {
+ let record = sm.getSelection()[0];
+ let schedule;
+ if (record) {
+ schedule = record.data.schedule;
+ }
+ Ext.create('PVE.window.ScheduleSimulator', {
+ autoShow: true,
+ schedule,
+ });
+ },
+ },
+ ],
+ }],
+ columns: [
+ {
+ header: gettext('Enabled'),
+ width: 80,
+ dataIndex: 'enabled',
+ align: 'center',
+ // TODO: switch to Proxmox.Utils.renderEnabledIcon once available
+ renderer: enabled => ``,
+ sortable: true,
+ },
+ {
+ header: gettext('ID'),
+ dataIndex: 'id',
+ hidden: true,
+ },
+ {
+ header: gettext('Node'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'node',
+ renderer: function(value) {
+ if (value) {
+ return value;
+ }
+ return me.allText;
+ },
+ },
+ {
+ header: gettext('Schedule'),
+ width: 150,
+ dataIndex: 'schedule',
+ },
+ {
+ text: gettext('Next Run'),
+ dataIndex: 'next-run',
+ width: 150,
+ renderer: PVE.Utils.render_next_event,
+ },
+ {
+ header: gettext('Storage'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'storage',
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.htmlEncode,
+ sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''),
+ flex: 1,
+ },
+ {
+ header: gettext('Retention'),
+ dataIndex: 'prune-backups',
+ renderer: v => v ? PVE.Parser.printPropertyString(v) : gettext('Fallback from storage config'),
+ flex: 2,
+ },
+ {
+ header: gettext('Selection'),
+ flex: 4,
+ sortable: false,
+ dataIndex: 'vmid',
+ renderer: PVE.Utils.render_backup_selection,
+ },
+ ],
+ listeners: {
+ activate: reload,
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ },
+}, function() {
+ Ext.define('pve-cluster-backup', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id',
+ 'compress',
+ 'dow',
+ 'exclude',
+ 'mailto',
+ 'mode',
+ 'node',
+ 'pool',
+ 'prune-backups',
+ 'starttime',
+ 'storage',
+ 'vmid',
+ { name: 'enabled', type: 'boolean' },
+ { name: 'all', type: 'boolean' },
+ ],
+ });
+});
+Ext.define('pve-cluster-nodes', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr',
+ { type: 'integer', name: 'quorum_votes' },
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/config/nodes",
+ },
+ idProperty: 'nodeid',
+});
+
+Ext.define('pve-cluster-info', {
+ extend: 'Ext.data.Model',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/config/join",
+ },
+});
+
+Ext.define('PVE.ClusterAdministration', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveClusterAdministration',
+
+ title: gettext('Cluster Administration'),
+ onlineHelp: 'chapter_pvecm',
+
+ border: false,
+ defaults: { border: false },
+
+ viewModel: {
+ parent: null,
+ data: {
+ totem: {},
+ nodelist: [],
+ preferred_node: {
+ name: '',
+ fp: '',
+ addr: '',
+ },
+ isInCluster: false,
+ nodecount: 0,
+ },
+ },
+
+ items: [
+ {
+ xtype: 'panel',
+ title: gettext('Cluster Information'),
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.store = Ext.create('Proxmox.data.UpdateStore', {
+ autoStart: true,
+ interval: 15 * 1000,
+ storeid: 'pve-cluster-info',
+ model: 'pve-cluster-info',
+ });
+ view.store.on('load', this.onLoad, this);
+ view.on('destroy', view.store.stopUpdate);
+ },
+
+ onLoad: function(store, records, success, operation) {
+ let vm = this.getViewModel();
+
+ let data = records?.[0]?.data;
+ if (!success || !data || !data.nodelist?.length) {
+ let error = operation.getError();
+ if (error) {
+ let msg = Proxmox.Utils.getResponseErrorMessage(error);
+ if (error.status !== 424 && !msg.match(/node is not in a cluster/i)) {
+ // an actual error, not just the "not in a cluster one", so show it!
+ Proxmox.Utils.setErrorMask(this.getView(), msg);
+ }
+ }
+ vm.set('totem', {});
+ vm.set('isInCluster', false);
+ vm.set('nodelist', []);
+ vm.set('preferred_node', {
+ name: '',
+ addr: '',
+ fp: '',
+ });
+ return;
+ }
+ vm.set('totem', data.totem);
+ vm.set('isInCluster', !!data.totem.cluster_name);
+ vm.set('nodelist', data.nodelist);
+
+ let nodeinfo = data.nodelist.find(el => el.name === data.preferred_node);
+
+ let links = {};
+ let ring_addr = [];
+ PVE.Utils.forEachCorosyncLink(nodeinfo, (num, link) => {
+ links[num] = link;
+ ring_addr.push(link);
+ });
+
+ vm.set('preferred_node', {
+ name: data.preferred_node,
+ addr: nodeinfo.pve_addr,
+ peerLinks: links,
+ ring_addr: ring_addr,
+ fp: nodeinfo.pve_fp,
+ });
+ },
+
+ onCreate: function() {
+ let view = this.getView();
+ view.store.stopUpdate();
+ Ext.create('PVE.ClusterCreateWindow', {
+ autoShow: true,
+ listeners: {
+ destroy: function() {
+ view.store.startUpdate();
+ },
+ },
+ });
+ },
+
+ onClusterInfo: function() {
+ let vm = this.getViewModel();
+ Ext.create('PVE.ClusterInfoWindow', {
+ autoShow: true,
+ joinInfo: {
+ ipAddress: vm.get('preferred_node.addr'),
+ fingerprint: vm.get('preferred_node.fp'),
+ peerLinks: vm.get('preferred_node.peerLinks'),
+ ring_addr: vm.get('preferred_node.ring_addr'),
+ totem: vm.get('totem'),
+ },
+ });
+ },
+
+ onJoin: function() {
+ let view = this.getView();
+ view.store.stopUpdate();
+ Ext.create('PVE.ClusterJoinNodeWindow', {
+ autoShow: true,
+ listeners: {
+ destroy: function() {
+ view.store.startUpdate();
+ },
+ },
+ });
+ },
+ },
+ tbar: [
+ {
+ text: gettext('Create Cluster'),
+ reference: 'createButton',
+ handler: 'onCreate',
+ bind: {
+ disabled: '{isInCluster}',
+ },
+ },
+ {
+ text: gettext('Join Information'),
+ reference: 'addButton',
+ handler: 'onClusterInfo',
+ bind: {
+ disabled: '{!isInCluster}',
+ },
+ },
+ {
+ text: gettext('Join Cluster'),
+ reference: 'joinButton',
+ handler: 'onJoin',
+ bind: {
+ disabled: '{isInCluster}',
+ },
+ },
+ ],
+ layout: 'hbox',
+ bodyPadding: 5,
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Cluster Name'),
+ bind: {
+ value: '{totem.cluster_name}',
+ hidden: '{!isInCluster}',
+ },
+ flex: 1,
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Config Version'),
+ bind: {
+ value: '{totem.config_version}',
+ hidden: '{!isInCluster}',
+ },
+ flex: 1,
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Number of Nodes'),
+ labelWidth: 120,
+ bind: {
+ value: '{nodecount}',
+ hidden: '{!isInCluster}',
+ },
+ flex: 1,
+ },
+ {
+ xtype: 'displayfield',
+ value: gettext('Standalone node - no cluster defined'),
+ bind: {
+ hidden: '{isInCluster}',
+ },
+ flex: 1,
+ },
+ ],
+ },
+ {
+ xtype: 'grid',
+ title: gettext('Cluster Nodes'),
+ autoScroll: true,
+ enableColumnHide: false,
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ autoLoad: true,
+ xtype: 'update',
+ interval: 5 * 1000,
+ autoStart: true,
+ storeid: 'pve-cluster-nodes',
+ model: 'pve-cluster-nodes',
+ });
+ view.setStore(Ext.create('Proxmox.data.DiffStore', {
+ rstore: view.rstore,
+ sorters: {
+ property: 'nodeid',
+ direction: 'ASC',
+ },
+ }));
+ Proxmox.Utils.monStoreErrors(view, view.rstore);
+ view.rstore.on('load', this.onLoad, this);
+ view.on('destroy', view.rstore.stopUpdate);
+ },
+
+ onLoad: function(store, records, success) {
+ let view = this.getView();
+ let vm = this.getViewModel();
+
+ if (!success || !records || !records.length) {
+ vm.set('nodecount', 0);
+ return;
+ }
+ vm.set('nodecount', records.length);
+
+ // show/hide columns according to used links
+ let linkIndex = view.columns.length;
+ Ext.each(view.columns, (col, i) => {
+ if (col.linkNumber !== undefined) {
+ col.setHidden(true);
+ // save offset at which link columns start, so we can address them directly below
+ if (i < linkIndex) {
+ linkIndex = i;
+ }
+ }
+ });
+
+ PVE.Utils.forEachCorosyncLink(records[0].data,
+ (linknum, val) => {
+ if (linknum > 7) {
+ return;
+ }
+ view.columns[linkIndex + linknum].setHidden(false);
+ },
+ );
+ },
+ },
+ columns: {
+ items: [
+ {
+ header: gettext('Nodename'),
+ hidden: false,
+ dataIndex: 'name',
+ },
+ {
+ header: gettext('ID'),
+ minWidth: 100,
+ width: 100,
+ flex: 0,
+ hidden: false,
+ dataIndex: 'nodeid',
+ },
+ {
+ header: gettext('Votes'),
+ minWidth: 100,
+ width: 100,
+ flex: 0,
+ hidden: false,
+ dataIndex: 'quorum_votes',
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 0),
+ dataIndex: 'ring0_addr',
+ linkNumber: 0,
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 1),
+ dataIndex: 'ring1_addr',
+ linkNumber: 1,
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 2),
+ dataIndex: 'ring2_addr',
+ linkNumber: 2,
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 3),
+ dataIndex: 'ring3_addr',
+ linkNumber: 3,
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 4),
+ dataIndex: 'ring4_addr',
+ linkNumber: 4,
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 5),
+ dataIndex: 'ring5_addr',
+ linkNumber: 5,
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 6),
+ dataIndex: 'ring6_addr',
+ linkNumber: 6,
+ },
+ {
+ header: Ext.String.format(gettext('Link {0}'), 7),
+ dataIndex: 'ring7_addr',
+ linkNumber: 7,
+ },
+ ],
+ defaults: {
+ flex: 1,
+ hidden: true,
+ minWidth: 150,
+ },
+ },
+ },
+ ],
+});
+Ext.define('PVE.ClusterCreateWindow', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveClusterCreateWindow',
+
+ title: gettext('Create Cluster'),
+ width: 600,
+
+ method: 'POST',
+ url: '/cluster/config',
+
+ isCreate: true,
+ subject: gettext('Cluster'),
+ showTaskViewer: true,
+
+ onlineHelp: 'pvecm_create_cluster',
+
+ items: {
+ xtype: 'inputpanel',
+ items: [{
+ xtype: 'textfield',
+ fieldLabel: gettext('Cluster Name'),
+ allowBlank: false,
+ maxLength: 15,
+ name: 'clustername',
+ },
+ {
+ xtype: 'fieldcontainer',
+ fieldLabel: gettext("Cluster Network"),
+ items: [
+ {
+ xtype: 'pveCorosyncLinkEditor',
+ infoText: gettext("Multiple links are used as failover, lower numbers have higher priority."),
+ name: 'links',
+ },
+ ],
+ }],
+ },
+});
+
+Ext.define('PVE.ClusterInfoWindow', {
+ extend: 'Ext.window.Window',
+ xtype: 'pveClusterInfoWindow',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 800,
+ modal: true,
+ resizable: false,
+ title: gettext('Cluster Join Information'),
+
+ joinInfo: {
+ ipAddress: undefined,
+ fingerprint: undefined,
+ totem: {},
+ },
+
+ items: [
+ {
+ xtype: 'component',
+ border: false,
+ padding: '10 10 10 10',
+ html: gettext("Copy the Join Information here and use it on the node you want to add."),
+ },
+ {
+ xtype: 'container',
+ layout: 'form',
+ border: false,
+ padding: '0 10 10 10',
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('IP Address'),
+ cbind: {
+ value: '{joinInfo.ipAddress}',
+ },
+ editable: false,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Fingerprint'),
+ cbind: {
+ value: '{joinInfo.fingerprint}',
+ },
+ editable: false,
+ },
+ {
+ xtype: 'textarea',
+ inputId: 'pveSerializedClusterInfo',
+ fieldLabel: gettext('Join Information'),
+ grow: true,
+ cbind: {
+ joinInfo: '{joinInfo}',
+ },
+ editable: false,
+ listeners: {
+ afterrender: function(field) {
+ if (!field.joinInfo) {
+ return;
+ }
+ var jsons = Ext.JSON.encode(field.joinInfo);
+ var base64s = Ext.util.Base64.encode(jsons);
+ field.setValue(base64s);
+ },
+ },
+ },
+ ],
+ },
+ ],
+ dockedItems: [{
+ dock: 'bottom',
+ xtype: 'toolbar',
+ items: [{
+ xtype: 'button',
+ handler: function(b) {
+ var el = document.getElementById('pveSerializedClusterInfo');
+ el.select();
+ document.execCommand("copy");
+ },
+ text: gettext('Copy Information'),
+ }],
+ }],
+});
+
+Ext.define('PVE.ClusterJoinNodeWindow', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveClusterJoinNodeWindow',
+
+ title: gettext('Cluster Join'),
+ width: 800,
+
+ method: 'POST',
+ url: '/cluster/config/join',
+
+ defaultFocus: 'textarea[name=serializedinfo]',
+ isCreate: true,
+ bind: {
+ submitText: '{submittxt}',
+ },
+ showTaskViewer: true,
+
+ onlineHelp: 'pvecm_join_node_to_cluster',
+
+ viewModel: {
+ parent: null,
+ data: {
+ info: {
+ fp: '',
+ ip: '',
+ clusterName: '',
+ },
+ hasAssistedInfo: false,
+ },
+ formulas: {
+ submittxt: function(get) {
+ let cn = get('info.clusterName');
+ if (cn) {
+ return Ext.String.format(gettext('Join {0}'), `'${cn}'`);
+ }
+ return gettext('Join');
+ },
+ showClusterFields: (get) => {
+ let manualMode = !get('assistedEntry.checked');
+ return get('hasAssistedInfo') || manualMode;
+ },
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ '#': {
+ close: function() {
+ delete PVE.Utils.silenceAuthFailures;
+ },
+ },
+ 'proxmoxcheckbox[name=assistedEntry]': {
+ change: 'onInputTypeChange',
+ },
+ 'textarea[name=serializedinfo]': {
+ change: 'recomputeSerializedInfo',
+ enable: 'resetField',
+ },
+ 'textfield': {
+ disable: 'resetField',
+ },
+ },
+ resetField: function(field) {
+ field.reset();
+ },
+ onInputTypeChange: function(field, assistedInput) {
+ let linkEditor = this.lookup('linkEditor');
+
+ // this also clears all links
+ linkEditor.setAllowNumberEdit(!assistedInput);
+
+ if (!assistedInput) {
+ linkEditor.setInfoText();
+ linkEditor.setDefaultLinks();
+ }
+ },
+ recomputeSerializedInfo: function(field, value) {
+ let vm = this.getViewModel();
+
+ let assistedEntryBox = this.lookup('assistedEntry');
+
+ if (!assistedEntryBox.getValue()) {
+ // not in assisted entry mode, nothing to do
+ vm.set('hasAssistedInfo', false);
+ return;
+ }
+
+ let linkEditor = this.lookup('linkEditor');
+
+ let jsons = Ext.util.Base64.decode(value);
+ let joinInfo = Ext.JSON.decode(jsons, true);
+
+ let info = {
+ fp: '',
+ ip: '',
+ clusterName: '',
+ };
+
+ if (!(joinInfo && joinInfo.totem)) {
+ field.valid = false;
+ linkEditor.setLinks([]);
+ linkEditor.setInfoText();
+ vm.set('hasAssistedInfo', false);
+ } else {
+ let interfaces = joinInfo.totem.interface;
+ let links = Object.values(interfaces).map(iface => {
+ let linkNumber = iface.linknumber;
+ let peerLink;
+ if (joinInfo.peerLinks) {
+ peerLink = joinInfo.peerLinks[linkNumber];
+ }
+ return {
+ number: linkNumber,
+ value: '',
+ text: peerLink ? Ext.String.format(gettext("peer's link address: {0}"), peerLink) : '',
+ allowBlank: false,
+ };
+ });
+
+ linkEditor.setInfoText();
+ if (links.length === 1 && joinInfo.ring_addr !== undefined &&
+ joinInfo.ring_addr[0] === joinInfo.ipAddress
+ ) {
+ links[0].allowBlank = true;
+ links[0].emptyText = gettext("IP resolved by node's hostname");
+ }
+
+ linkEditor.setLinks(links);
+
+ info = {
+ ip: joinInfo.ipAddress,
+ fp: joinInfo.fingerprint,
+ clusterName: joinInfo.totem.cluster_name,
+ };
+ field.valid = true;
+ vm.set('hasAssistedInfo', true);
+ }
+ vm.set('info', info);
+ },
+ },
+
+ submit: function() {
+ // joining may produce temporarily auth failures, ignore as long the task runs
+ PVE.Utils.silenceAuthFailures = true;
+ this.callParent();
+ },
+
+ taskDone: function(success) {
+ delete PVE.Utils.silenceAuthFailures;
+ if (success) {
+ // reload always (if user wasn't faster), but wait a bit for pveproxy
+ Ext.defer(function() {
+ window.location.reload(true);
+ }, 5000);
+ let txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!');
+ // ensure user cannot do harm
+ Ext.getBody().mask(txt, ['pve-static-mask']);
+ // TaskView may hide above mask, so tell him directly
+ Ext.Msg.show({
+ title: gettext('Join Task Finished'),
+ icon: Ext.Msg.INFO,
+ msg: txt,
+ });
+ }
+ },
+
+ items: [{
+ xtype: 'proxmoxcheckbox',
+ reference: 'assistedEntry',
+ name: 'assistedEntry',
+ itemId: 'assistedEntry',
+ submitValue: false,
+ value: true,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering'),
+ },
+ boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.'),
+ },
+ {
+ xtype: 'textarea',
+ name: 'serializedinfo',
+ submitValue: false,
+ allowBlank: false,
+ fieldLabel: gettext('Information'),
+ emptyText: gettext('Paste encoded Cluster Information here'),
+ validator: function(val) {
+ return val === '' || this.valid || gettext('Does not seem like a valid encoded Cluster Information!');
+ },
+ bind: {
+ disabled: '{!assistedEntry.checked}',
+ hidden: '{!assistedEntry.checked}',
+ },
+ value: '',
+ },
+ {
+ xtype: 'panel',
+ width: 776,
+ layout: {
+ type: 'hbox',
+ align: 'center',
+ },
+ bind: {
+ hidden: '{!showClusterFields}',
+ },
+ items: [
+ {
+ xtype: 'textfield',
+ flex: 1,
+ margin: '0 5px 0 0',
+ fieldLabel: gettext('Peer Address'),
+ allowBlank: false,
+ bind: {
+ value: '{info.ip}',
+ readOnly: '{assistedEntry.checked}',
+ },
+ name: 'hostname',
+ },
+ {
+ xtype: 'textfield',
+ flex: 1,
+ margin: '0 0 10px 5px',
+ inputType: 'password',
+ emptyText: gettext("Peer's root password"),
+ fieldLabel: gettext('Password'),
+ allowBlank: false,
+ name: 'password',
+ },
+ ],
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Fingerprint'),
+ allowBlank: false,
+ bind: {
+ value: '{info.fp}',
+ readOnly: '{assistedEntry.checked}',
+ hidden: '{!showClusterFields}',
+ },
+ name: 'fingerprint',
+ },
+ {
+ xtype: 'fieldcontainer',
+ fieldLabel: gettext("Cluster Network"),
+ bind: {
+ hidden: '{!showClusterFields}',
+ },
+ items: [
+ {
+ xtype: 'pveCorosyncLinkEditor',
+ itemId: 'linkEditor',
+ reference: 'linkEditor',
+ allowNumberEdit: false,
+ },
+ ],
+ }],
+});
+/*
+ * 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',
+ },
+ {
+ xtype: 'pmxNotesView',
+ title: gettext('Notes'),
+ iconCls: 'fa fa-sticky-note-o',
+ itemId: 'notes',
+ },
+ {
+ title: gettext('Cluster'),
+ xtype: 'pveClusterAdministration',
+ iconCls: 'fa fa-server',
+ itemId: 'cluster',
+ },
+ {
+ title: 'Ceph',
+ itemId: 'ceph',
+ iconCls: 'fa fa-ceph',
+ xtype: 'pveNodeCephStatus',
+ },
+ {
+ xtype: 'pveDcOptionView',
+ title: gettext('Options'),
+ iconCls: 'fa fa-gear',
+ itemId: 'options',
+ });
+ }
+
+ if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveStorageView',
+ title: gettext('Storage'),
+ iconCls: 'fa fa-database',
+ itemId: 'storage',
+ });
+ }
+
+
+ if (caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveDcBackupView',
+ iconCls: 'fa fa-floppy-o',
+ title: gettext('Backup'),
+ itemId: 'backup',
+ },
+ {
+ xtype: 'pveReplicaView',
+ iconCls: 'fa fa-retweet',
+ title: gettext('Replication'),
+ itemId: 'replication',
+ },
+ {
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ expandedOnInit: true,
+ });
+ }
+
+ me.items.push({
+ xtype: 'pveUserView',
+ groups: ['permissions'],
+ iconCls: 'fa fa-user',
+ title: gettext('Users'),
+ itemId: 'users',
+ });
+
+ me.items.push({
+ xtype: 'pveTokenView',
+ groups: ['permissions'],
+ iconCls: 'fa fa-user-o',
+ title: gettext('API Tokens'),
+ itemId: 'apitokens',
+ });
+
+ me.items.push({
+ xtype: 'pmxTfaView',
+ title: gettext('Two Factor'),
+ groups: ['permissions'],
+ iconCls: 'fa fa-key',
+ itemId: 'tfa',
+ yubicoEnabled: true,
+ issuerName: `Proxmox VE - ${PVE.ClusterName || Proxmox.NodeName}`,
+ });
+
+ if (caps.dc['Sys.Audit']) {
+ me.items.push({
+ xtype: 'pveGroupView',
+ title: gettext('Groups'),
+ iconCls: 'fa fa-users',
+ groups: ['permissions'],
+ itemId: 'groups',
+ },
+ {
+ xtype: 'pvePoolView',
+ title: gettext('Pools'),
+ iconCls: 'fa fa-tags',
+ groups: ['permissions'],
+ itemId: 'pools',
+ },
+ {
+ xtype: 'pveRoleView',
+ title: gettext('Roles'),
+ iconCls: 'fa fa-male',
+ groups: ['permissions'],
+ itemId: 'roles',
+ },
+ {
+ xtype: 'pveAuthView',
+ title: gettext('Realms'),
+ groups: ['permissions'],
+ iconCls: 'fa fa-address-book-o',
+ 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',
+ });
+ // always show on initial load, will be hiddea later if the SDN API calls don't exist,
+ // else it won't be shown at first if the user initially loads with DC selected
+ if (PVE.SDNInfo || PVE.SDNInfo === undefined) {
+ me.items.push({
+ xtype: 'pveSDNStatus',
+ title: gettext('SDN'),
+ iconCls: 'fa fa-sdn',
+ hidden: true,
+ itemId: 'sdn',
+ expandedOnInit: true,
+ },
+ {
+ xtype: 'pveSDNZoneView',
+ groups: ['sdn'],
+ title: gettext('Zones'),
+ hidden: true,
+ iconCls: 'fa fa-th',
+ itemId: 'sdnzone',
+ },
+ {
+ xtype: 'pveSDNVnet',
+ groups: ['sdn'],
+ title: gettext('Vnets'),
+ hidden: true,
+ iconCls: 'fa fa-network-wired',
+ itemId: 'sdnvnet',
+ },
+ {
+ xtype: 'pveSDNOptions',
+ groups: ['sdn'],
+ title: gettext('Options'),
+ hidden: true,
+ iconCls: 'fa fa-gear',
+ itemId: 'sdnoptions',
+ });
+ }
+
+ if (Proxmox.UserName === 'root@pam') {
+ me.items.push({
+ xtype: 'pveACMEClusterView',
+ title: 'ACME',
+ iconCls: 'fa fa-certificate',
+ itemId: 'acme',
+ });
+ }
+
+ me.items.push({
+ xtype: 'pveFirewallRules',
+ title: gettext('Firewall'),
+ allow_iface: true,
+ base_url: '/cluster/firewall/rules',
+ list_refs_url: '/cluster/firewall/refs',
+ iconCls: 'fa fa-shield',
+ itemId: 'firewall',
+ },
+ {
+ xtype: 'pveFirewallOptions',
+ title: gettext('Options'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-gear',
+ base_url: '/cluster/firewall/options',
+ onlineHelp: 'pve_firewall_cluster_wide_setup',
+ fwtype: 'dc',
+ itemId: 'firewall-options',
+ },
+ {
+ xtype: 'pveSecurityGroups',
+ title: gettext('Security Group'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-group',
+ itemId: 'firewall-sg',
+ },
+ {
+ xtype: 'pveFirewallAliases',
+ title: gettext('Alias'),
+ groups: ['firewall'],
+ iconCls: 'fa fa-external-link',
+ base_url: '/cluster/firewall/aliases',
+ itemId: 'firewall-aliases',
+ },
+ {
+ xtype: 'pveIPSet',
+ title: 'IPSet',
+ groups: ['firewall'],
+ iconCls: 'fa fa-list-ol',
+ base_url: '/cluster/firewall/ipset',
+ list_refs_url: '/cluster/firewall/refs',
+ itemId: 'firewall-ipset',
+ },
+ {
+ xtype: 'pveMetricServerView',
+ title: gettext('Metric Server'),
+ iconCls: 'fa fa-bar-chart',
+ itemId: 'metricservers',
+ onlineHelp: 'external_metric_server',
+ },
+ {
+ xtype: 'pveDcSupport',
+ title: gettext('Support'),
+ itemId: 'support',
+ iconCls: 'fa fa-comments-o',
+ });
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.form.CorosyncLinkEditorController', {
+ extend: 'Ext.app.ViewController',
+ alias: 'controller.pveCorosyncLinkEditorController',
+
+ addLinkIfEmpty: function() {
+ let view = this.getView();
+ if (view.items || view.items.length === 0) {
+ this.addLink();
+ }
+ },
+
+ addEmptyLink: function() {
+ this.addLink(); // discard parameters to allow being called from 'handler'
+ },
+
+ addLink: function(link) {
+ let me = this;
+ let view = me.getView();
+ let vm = view.getViewModel();
+
+ let linkCount = vm.get('linkCount');
+ if (linkCount >= vm.get('maxLinkCount')) {
+ return;
+ }
+
+ link = link || {};
+
+ if (link.number === undefined) {
+ link.number = me.getNextFreeNumber();
+ }
+ if (link.value === undefined) {
+ link.value = me.getNextFreeNetwork();
+ }
+
+ let linkSelector = Ext.create('PVE.form.CorosyncLinkSelector', {
+ maxLinkNumber: vm.get('maxLinkCount') - 1,
+ allowNumberEdit: vm.get('allowNumberEdit'),
+ allowBlankNetwork: link.allowBlank,
+ initNumber: link.number,
+ initNetwork: link.value,
+ text: link.text,
+ emptyText: link.emptyText,
+
+ // needs to be set here, because we need to update the viewmodel
+ removeBtnHandler: function() {
+ let curLinkCount = vm.get('linkCount');
+
+ if (curLinkCount <= 1) {
+ return;
+ }
+
+ vm.set('linkCount', curLinkCount - 1);
+
+ // 'this' is the linkSelector here
+ view.remove(this);
+
+ me.updateDeleteButtonState();
+ },
+ });
+
+ view.add(linkSelector);
+
+ linkCount++;
+ vm.set('linkCount', linkCount);
+
+ me.updateDeleteButtonState();
+ },
+
+ // ExtJS trips on binding this for some reason, so do it manually
+ updateDeleteButtonState: function() {
+ let view = this.getView();
+ let vm = view.getViewModel();
+
+ let disabled = vm.get('linkCount') <= 1;
+
+ let deleteButtons = view.query('button[cls=removeLinkBtn]');
+ Ext.Array.each(deleteButtons, btn => {
+ btn.setDisabled(disabled);
+ });
+ },
+
+ getNextFreeNetwork: function() {
+ let view = this.getView();
+ let vm = view.getViewModel();
+
+ let networksInUse = view.query('proxmoxNetworkSelector').map(selector => selector.value);
+
+ for (const network of vm.get('networks')) {
+ if (!networksInUse.includes(network)) {
+ return network;
+ }
+ }
+ return undefined; // default to empty field, user has to set up link manually
+ },
+
+ getNextFreeNumber: function() {
+ let view = this.getView();
+ let vm = view.getViewModel();
+
+ let numbersInUse = view.query('numberfield').map(field => field.value);
+
+ for (let i = 0; i < vm.get('maxLinkCount'); i++) {
+ if (!numbersInUse.includes(i)) {
+ return i;
+ }
+ }
+ // all numbers in use, this should never happen since add button is disabled automatically
+ return 0;
+ },
+});
+
+Ext.define('PVE.form.CorosyncLinkSelector', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveCorosyncLinkSelector',
+
+ mixins: ['Proxmox.Mixin.CBind'],
+ cbindData: [],
+
+ // config
+ maxLinkNumber: 7,
+ allowNumberEdit: true,
+ allowBlankNetwork: false,
+ removeBtnHandler: undefined,
+ emptyText: '',
+
+ // values
+ initNumber: 0,
+ initNetwork: '',
+ text: '',
+
+ layout: 'hbox',
+ bodyPadding: 5,
+ border: 0,
+
+ items: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: 'Link',
+ cbind: {
+ hidden: '{allowNumberEdit}',
+ value: '{initNumber}',
+ },
+ width: 45,
+ labelWidth: 30,
+ allowBlank: false,
+ },
+ {
+ xtype: 'numberfield',
+ fieldLabel: 'Link',
+ cbind: {
+ maxValue: '{maxLinkNumber}',
+ hidden: '{!allowNumberEdit}',
+ value: '{initNumber}',
+ },
+ width: 80,
+ labelWidth: 30,
+ minValue: 0,
+ submitValue: false, // see getSubmitValue of network selector
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ cbind: {
+ allowBlank: '{allowBlankNetwork}',
+ value: '{initNetwork}',
+ emptyText: '{emptyText}',
+ },
+ autoSelect: false,
+ valueField: 'address',
+ displayField: 'address',
+ width: 220,
+ margin: '0 5px 0 5px',
+ getSubmitValue: function() {
+ let me = this;
+ // link number is encoded into key, so we need to set field name before value retrieval
+ let linkNumber = me.prev('numberfield').getValue(); // always the correct one
+ me.name = 'link' + linkNumber;
+ return me.getValue();
+ },
+ },
+ {
+ xtype: 'button',
+ iconCls: 'fa fa-trash-o',
+ cls: 'removeLinkBtn',
+ cbind: {
+ hidden: '{!allowNumberEdit}',
+ },
+ handler: function() {
+ let me = this;
+ let parent = me.up('pveCorosyncLinkSelector');
+ if (parent.removeBtnHandler !== undefined) {
+ parent.removeBtnHandler();
+ }
+ },
+ },
+ {
+ xtype: 'label',
+ margin: '-1px 0 0 5px',
+
+ // for muted effect
+ cls: 'x-form-item-label-default',
+
+ cbind: {
+ text: '{text}',
+ },
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ me.callParent();
+
+ let numSelect = me.down('numberfield');
+ let netSelect = me.down('proxmoxNetworkSelector');
+
+ numSelect.validator = me.createNoDuplicatesValidator(
+ 'numberfield',
+ gettext("Duplicate link number not allowed."),
+ );
+
+ netSelect.validator = me.createNoDuplicatesValidator(
+ 'proxmoxNetworkSelector',
+ gettext("Duplicate link address not allowed."),
+ );
+ },
+
+ createNoDuplicatesValidator: function(queryString, errorMsg) { // linkSelector generator
+ let view = this; // eslint-disable-line consistent-this
+ /** @this is the field itself, as the validator this is called from scopes it that way */
+ return function(val) {
+ let me = this;
+ let form = view.up('form');
+ let linkEditor = view.up('pveCorosyncLinkEditor');
+
+ if (!form.validating) {
+ // avoid recursion/double validation by setting temporary states
+ me.validating = true;
+ form.validating = true;
+
+ // validate all other fields as well, to always mark both
+ // parties involved in a 'duplicate' error
+ form.isValid();
+
+ form.validating = false;
+ me.validating = false;
+ } else if (me.validating) {
+ // we'll be validated by the original call in the other if-branch, avoid double work
+ return true;
+ }
+
+ if (val === undefined || (val instanceof String && val.length === 0)) {
+ return true; // let this be caught by allowBlank, if at all
+ }
+
+ let allFields = linkEditor.query(queryString);
+ for (const field of allFields) {
+ if (field !== me && String(field.getValue()) === String(val)) {
+ return errorMsg;
+ }
+ }
+ return true;
+ };
+ },
+});
+
+Ext.define('PVE.form.CorosyncLinkEditor', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveCorosyncLinkEditor',
+
+ controller: 'pveCorosyncLinkEditorController',
+
+ // only initial config, use setter otherwise
+ allowNumberEdit: true,
+
+ viewModel: {
+ data: {
+ linkCount: 0,
+ maxLinkCount: 8,
+ networks: null,
+ allowNumberEdit: true,
+ infoText: '',
+ },
+ formulas: {
+ addDisabled: function(get) {
+ return !get('allowNumberEdit') ||
+ get('linkCount') >= get('maxLinkCount');
+ },
+ dockHidden: function(get) {
+ return !(get('allowNumberEdit') || get('infoText'));
+ },
+ },
+ },
+
+ dockedItems: [{
+ xtype: 'toolbar',
+ dock: 'bottom',
+ defaultButtonUI: 'default',
+ border: false,
+ padding: '6 0 6 0',
+ bind: {
+ hidden: '{dockHidden}',
+ },
+ items: [
+ {
+ xtype: 'button',
+ text: gettext('Add'),
+ bind: {
+ disabled: '{addDisabled}',
+ hidden: '{!allowNumberEdit}',
+ },
+ handler: 'addEmptyLink',
+ },
+ {
+ xtype: 'label',
+ bind: {
+ text: '{infoText}',
+ },
+ },
+ ],
+ }],
+
+ setInfoText: function(text) {
+ let me = this;
+ let vm = me.getViewModel();
+
+ vm.set('infoText', text || '');
+ },
+
+ setLinks: function(links) {
+ let me = this;
+ let controller = me.getController();
+ let vm = me.getViewModel();
+
+ me.removeAll();
+ vm.set('linkCount', 0);
+
+ Ext.Array.each(links, link => controller.addLink(link));
+ },
+
+ setDefaultLinks: function() {
+ let me = this;
+ let controller = me.getController();
+ let vm = me.getViewModel();
+
+ me.removeAll();
+ vm.set('linkCount', 0);
+ controller.addLink();
+ },
+
+ // clears all links
+ setAllowNumberEdit: function(allow) {
+ let me = this;
+ let vm = me.getViewModel();
+ vm.set('allowNumberEdit', allow);
+ me.removeAll();
+ vm.set('linkCount', 0);
+ },
+
+ items: [{
+ // No links is never a valid scenario, but can occur during a slow load
+ xtype: 'hiddenfield',
+ submitValue: false,
+ isValid: function() {
+ let me = this;
+ let vm = me.up('pveCorosyncLinkEditor').getViewModel();
+ return vm.get('linkCount') > 0;
+ },
+ }],
+
+ initComponent: function() {
+ let me = this;
+ let vm = me.getViewModel();
+ let controller = me.getController();
+
+ vm.set('allowNumberEdit', me.allowNumberEdit);
+ vm.set('infoText', me.infoText || '');
+
+ me.callParent();
+
+ // Request local node networks to pre-populate first link.
+ Proxmox.Utils.API2Request({
+ url: '/nodes/localhost/network',
+ method: 'GET',
+ waitMsgTarget: me,
+ success: response => {
+ let data = response.result.data;
+ if (data.length > 0) {
+ data.sort((a, b) => a.iface.localeCompare(b.iface));
+ let addresses = [];
+ for (let net of data) {
+ if (net.address) {
+ addresses.push(net.address);
+ }
+ if (net.address6) {
+ addresses.push(net.address6);
+ }
+ }
+
+ vm.set('networks', addresses);
+ }
+
+ // Always have at least one link, but account for delay in API,
+ // someone might have called 'setLinks' in the meantime -
+ // except if 'allowNumberEdit' is false, in which case we're
+ // probably waiting for the user to input the join info
+ if (vm.get('allowNumberEdit')) {
+ controller.addLinkIfEmpty();
+ }
+ },
+ failure: () => {
+ if (vm.get('allowNumberEdit')) {
+ controller.addLinkIfEmpty();
+ }
+ },
+ });
+ },
+});
+
+Ext.define('PVE.dc.GroupEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pveDcGroupEdit'],
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = !me.groupid;
+
+ var url;
+ var method;
+
+ if (me.isCreate) {
+ url = '/api2/extjs/access/groups';
+ method = 'POST';
+ } else {
+ url = '/api2/extjs/access/groups/' + me.groupid;
+ method = 'PUT';
+ }
+
+ Ext.applyIf(me, {
+ subject: gettext('Group'),
+ url: url,
+ method: method,
+ items: [
+ {
+ xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
+ fieldLabel: gettext('Name'),
+ name: 'groupid',
+ value: me.groupid,
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Comment'),
+ name: 'comment',
+ allowBlank: true,
+ },
+ ],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load();
+ }
+ },
+});
+Ext.define('PVE.dc.GroupView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveGroupView'],
+
+ onlineHelp: 'pveum_groups',
+
+ stateful: true,
+ stateId: 'grid-groups',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-groups',
+ sorters: {
+ property: 'groupid',
+ direction: 'ASC',
+ },
+ });
+
+ var reload = function() {
+ store.load();
+ };
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ callback: function() {
+ reload();
+ },
+ baseurl: '/access/groups/',
+ });
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.dc.GroupEdit', {
+ groupid: rec.data.groupid,
+ });
+ win.on('destroy', reload);
+ win.show();
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ var tbar = [
+ {
+ text: gettext('Create'),
+ handler: function() {
+ var win = Ext.create('PVE.dc.GroupEdit', {});
+ win.on('destroy', reload);
+ win.show();
+ },
+ },
+ edit_btn, remove_btn,
+ ];
+
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: tbar,
+ viewConfig: {
+ trackOver: false,
+ },
+ columns: [
+ {
+ header: gettext('Name'),
+ width: 200,
+ sortable: true,
+ dataIndex: 'groupid',
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ renderer: Ext.String.htmlEncode,
+ dataIndex: 'comment',
+ flex: 1,
+ },
+ {
+ header: gettext('Users'),
+ sortable: false,
+ dataIndex: 'users',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+ listeners: {
+ activate: reload,
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.dc.Guests', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveDcGuests',
+
+
+ title: gettext('Guests'),
+ height: 250,
+ layout: {
+ type: 'table',
+ columns: 2,
+ tableAttrs: {
+ style: {
+ width: '100%',
+ },
+ },
+ },
+ bodyPadding: '0 20 20 20',
+
+ defaults: {
+ xtype: 'box',
+ padding: '0 50 0 50',
+ style: {
+ 'text-align': 'center',
+ 'line-height': '1.5em',
+ 'font-size': '14px',
+ },
+ },
+ items: [
+ {
+ itemId: 'qemu',
+ data: {
+ running: 0,
+ paused: 0,
+ stopped: 0,
+ template: 0,
+ },
+ cls: 'centered-flex-column',
+ tpl: [
+ '' + gettext("Virtual Machines") + '
',
+ '',
+ '
',
+ ' ',
+ gettext('Running'),
+ '
',
+ '
{running}
',
+ '
',
+ '',
+ '',
+ '
',
+ ' ',
+ gettext('Paused'),
+ '
',
+ '
{paused}
',
+ '
',
+ '',
+ '',
+ '
',
+ ' ',
+ gettext('Stopped'),
+ '
',
+ '
{stopped}
',
+ '
',
+ '',
+ '',
+ '
',
+ ' ',
+ gettext('Templates'),
+ '
',
+ '
{template}
',
+ '
',
+ '',
+ ],
+ },
+ {
+ itemId: 'lxc',
+ data: {
+ running: 0,
+ paused: 0,
+ stopped: 0,
+ template: 0,
+ },
+ cls: 'centered-flex-column',
+ tpl: [
+ '' + gettext("LXC Container") + '
',
+ '',
+ '
',
+ ' ',
+ gettext('Running'),
+ '
',
+ '
{running}
',
+ '
',
+ '',
+ '',
+ '
',
+ ' ',
+ gettext('Paused'),
+ '
',
+ '
{paused}
',
+ '
',
+ '',
+ '',
+ '
',
+ ' ',
+ gettext('Stopped'),
+ '
',
+ '
{stopped}
',
+ '
',
+ '',
+ '',
+ '
',
+ ' ',
+ gettext('Templates'),
+ '
',
+ '
{template}
',
+ '
',
+ '',
+ ],
+ },
+ {
+ itemId: 'error',
+ colspan: 2,
+ data: {
+ num: 0,
+ },
+ columnWidth: 1,
+ padding: '10 250 0 250',
+ tpl: [
+ '',
+ '',
+ ' ',
+ gettext('Error'),
+ '
',
+ '{num}
',
+ '',
+ ],
+ },
+ ],
+
+ updateValues: function(qemu, lxc, error) {
+ let me = this;
+
+ let lazyUpdate = (query, newData) => {
+ let el = me.getComponent(query);
+ let currentData = el.data;
+
+ let keys = Object.keys(newData);
+ if (keys.length === Object.keys(currentData).length) {
+ if (keys.every(k => newData[k] === currentData[k])) {
+ return; // all stayed the same here, return early to avoid bogus regeneration
+ }
+ }
+ el.update(newData);
+ };
+ lazyUpdate('qemu', qemu);
+ lazyUpdate('lxc', lxc);
+ lazyUpdate('error', { num: error });
+ },
+});
+Ext.define('PVE.dc.Health', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveDcHealth',
+
+ title: gettext('Health'),
+
+ bodyPadding: 10,
+ height: 250,
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+
+ defaults: {
+ flex: 1,
+ xtype: 'box',
+ style: {
+ 'text-align': 'center',
+ },
+ },
+
+ nodeList: [],
+ nodeIndex: 0,
+
+ updateStatus: function(store, records, success) {
+ let me = this;
+ if (!success) {
+ return;
+ }
+
+ let cluster = {
+ iconCls: PVE.Utils.get_health_icon('good', true),
+ text: gettext("Standalone node - no cluster defined"),
+ };
+ let nodes = {
+ online: 0,
+ offline: 0,
+ };
+ let numNodes = 1; // by default we have one node
+ for (const { data } of records) {
+ if (data.type === 'node') {
+ nodes[data.online === 1 ? 'online':'offline']++;
+ } else if (data.type === 'cluster') {
+ cluster.text = `${gettext("Cluster")}: ${data.name}, ${gettext("Quorate")}: `;
+ cluster.text += Proxmox.Utils.format_boolean(data.quorate);
+ if (data.quorate !== 1) {
+ cluster.iconCls = PVE.Utils.get_health_icon('critical', true);
+ }
+ numNodes = data.nodes;
+ }
+ }
+
+ if (numNodes !== nodes.online + nodes.offline) {
+ nodes.offline = numNodes - nodes.online;
+ }
+
+ me.getComponent('clusterstatus').updateHealth(cluster);
+ me.getComponent('nodestatus').update(nodes);
+ },
+
+ updateCeph: function(store, records, success) {
+ let me = this;
+ let cephstatus = me.getComponent('ceph');
+ if (!success || records.length < 1) {
+ if (cephstatus.isVisible()) {
+ return; // if ceph status is already visible don't stop to update
+ }
+ // try all nodes until we either get a successful api call, or we tried all nodes
+ if (++me.nodeIndex >= me.nodeList.length) {
+ me.cephstore.stopUpdate();
+ } else {
+ store.getProxy().setUrl(`/api2/json/nodes/${me.nodeList[me.nodeIndex].node}/ceph/status`);
+ }
+ return;
+ }
+
+ let state = PVE.Utils.render_ceph_health(records[0].data.health || {});
+ cephstatus.updateHealth(state);
+ cephstatus.setVisible(true);
+ },
+
+ listeners: {
+ destroy: function() {
+ let me = this;
+ me.cephstore.stopUpdate();
+ },
+ },
+
+ items: [
+ {
+ itemId: 'clusterstatus',
+ xtype: 'pveHealthWidget',
+ title: gettext('Status'),
+ },
+ {
+ itemId: 'nodestatus',
+ data: {
+ online: 0,
+ offline: 0,
+ },
+ tpl: [
+ '' + gettext('Nodes') + '
',
+ '',
+ '
',
+ ' ',
+ gettext('Online'),
+ '
',
+ '
{online}
',
+ '
',
+ '
',
+ ' ',
+ gettext('Offline'),
+ '
',
+ '
{offline}
',
+ '
',
+ ],
+ },
+ {
+ itemId: 'ceph',
+ width: 250,
+ columnWidth: undefined,
+ userCls: 'pointer',
+ title: 'Ceph',
+ xtype: 'pveHealthWidget',
+ hidden: true,
+ listeners: {
+ element: 'el',
+ click: function() {
+ Ext.state.Manager.getProvider().set('dctab', { value: 'ceph' }, true);
+ },
+ },
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ me.nodeList = PVE.data.ResourceStore.getNodes();
+ me.nodeIndex = 0;
+ me.cephstore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 3000,
+ storeid: 'pve-cluster-ceph',
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodeList[me.nodeIndex].node}/ceph/status`,
+ },
+ });
+ me.callParent();
+ me.mon(me.cephstore, 'load', me.updateCeph, me);
+ me.cephstore.startUpdate();
+ },
+});
+/* This class defines the "Cluster log" tab of the bottom status panel
+ * A log entry is a timestamp associated with an action on a cluster
+ */
+
+Ext.define('PVE.dc.Log', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveClusterLog'],
+
+ initComponent: function() {
+ let me = this;
+
+ let logstore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'pve-cluster-log',
+ model: 'proxmox-cluster-log',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/log',
+ },
+ });
+ let store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: logstore,
+ appendAtStart: true,
+ });
+
+ Ext.apply(me, {
+ store: store,
+ stateful: false,
+
+ viewConfig: {
+ trackOver: false,
+ stripeRows: true,
+ getRowClass: function(record, index) {
+ let pri = record.get('pri');
+ if (pri && pri <= 3) {
+ return "proxmox-invalid-row";
+ }
+ return undefined;
+ },
+ },
+ sortableColumns: false,
+ columns: [
+ {
+ header: gettext("Time"),
+ dataIndex: 'time',
+ width: 150,
+ renderer: function(value) {
+ return Ext.Date.format(value, "M d H:i:s");
+ },
+ },
+ {
+ header: gettext("Node"),
+ dataIndex: 'node',
+ width: 150,
+ },
+ {
+ header: gettext("Service"),
+ dataIndex: 'tag',
+ width: 100,
+ },
+ {
+ header: "PID",
+ dataIndex: 'pid',
+ width: 100,
+ },
+ {
+ header: gettext("User name"),
+ dataIndex: 'user',
+ renderer: Ext.String.htmlEncode,
+ width: 150,
+ },
+ {
+ header: gettext("Severity"),
+ dataIndex: 'pri',
+ renderer: PVE.Utils.render_serverity,
+ width: 100,
+ },
+ {
+ header: gettext("Message"),
+ dataIndex: 'msg',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+ listeners: {
+ activate: () => logstore.startUpdate(),
+ deactivate: () => logstore.stopUpdate(),
+ destroy: () => logstore.stopUpdate(),
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.dc.NodeView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveDcNodeView',
+
+ title: gettext('Nodes'),
+ disableSelection: true,
+ scrollable: true,
+
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'name',
+ },
+ {
+ header: 'ID',
+ width: 40,
+ sortable: true,
+ dataIndex: 'nodeid',
+ },
+ {
+ header: gettext('Online'),
+ width: 60,
+ sortable: true,
+ dataIndex: 'online',
+ renderer: function(value) {
+ var cls = value?'good':'critical';
+ return '';
+ },
+ },
+ {
+ header: gettext('Support'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'level',
+ renderer: PVE.Utils.render_support_level,
+ },
+ {
+ header: gettext('Server Address'),
+ width: 115,
+ sortable: true,
+ dataIndex: 'ip',
+ },
+ {
+ header: gettext('CPU usage'),
+ sortable: true,
+ width: 110,
+ dataIndex: 'cpuusage',
+ tdCls: 'x-progressbar-default-cell',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar',
+ },
+ },
+ {
+ header: gettext('Memory usage'),
+ width: 110,
+ sortable: true,
+ tdCls: 'x-progressbar-default-cell',
+ dataIndex: 'memoryusage',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar',
+ },
+ },
+ {
+ header: gettext('Uptime'),
+ sortable: true,
+ dataIndex: 'uptime',
+ align: 'right',
+ renderer: Proxmox.Utils.render_uptime,
+ },
+ ],
+
+ stateful: true,
+ stateId: 'grid-cluster-nodes',
+ tools: [
+ {
+ type: 'up',
+ handler: function() {
+ let view = this.up('grid');
+ view.setHeight(Math.max(view.getHeight() - 50, 250));
+ },
+ },
+ {
+ type: 'down',
+ handler: function() {
+ let view = this.up('grid');
+ view.setHeight(view.getHeight() + 50);
+ },
+ },
+ ],
+}, function() {
+ Ext.define('pve-dc-nodes', {
+ extend: 'Ext.data.Model',
+ fields: ['id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'],
+ idProperty: 'id',
+ });
+});
+
+Ext.define('PVE.widget.ProgressBar', {
+ extend: 'Ext.Progress',
+ alias: 'widget.pveProgressBar',
+
+ animate: true,
+ textTpl: [
+ '{percent}%',
+ ],
+
+ setValue: function(value) {
+ let me = this;
+
+ me.callParent([value]);
+
+ me.removeCls(['warning', 'critical']);
+
+ if (value > 0.89) {
+ me.addCls('critical');
+ } else if (value > 0.75) {
+ me.addCls('warning');
+ }
+ },
+});
+Ext.define('PVE.dc.OptionView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveDcOptionView'],
+
+ onlineHelp: 'datacenter_configuration_file',
+
+ monStoreErrors: true,
+ userCls: 'proxmox-tags-full',
+
+ add_inputpanel_row: function(name, text, opts) {
+ var me = this;
+
+ opts = opts || {};
+ me.rows = me.rows || {};
+
+ let canEdit = !Object.prototype.hasOwnProperty.call(opts, 'caps') || opts.caps;
+ me.rows[name] = {
+ required: true,
+ defaultValue: opts.defaultValue,
+ header: text,
+ renderer: opts.renderer,
+ editor: canEdit ? {
+ xtype: 'proxmoxWindowEdit',
+ width: opts.width || 350,
+ subject: text,
+ onlineHelp: opts.onlineHelp,
+ fieldDefaults: {
+ labelWidth: opts.labelWidth || 100,
+ },
+ setValues: function(values) {
+ var edit_value = values[name];
+
+ if (opts.parseBeforeSet) {
+ edit_value = PVE.Parser.parsePropertyString(edit_value);
+ }
+
+ Ext.Array.each(this.query('inputpanel'), function(panel) {
+ panel.setValues(edit_value);
+ });
+ },
+ url: opts.url,
+ items: [{
+ xtype: 'inputpanel',
+ onGetValues: function(values) {
+ if (values === undefined || Object.keys(values).length === 0) {
+ return { 'delete': name };
+ }
+ var ret_val = {};
+ ret_val[name] = PVE.Parser.printPropertyString(values);
+ return ret_val;
+ },
+ items: opts.items,
+ }],
+ } : undefined,
+ };
+ },
+
+ render_bwlimits: function(value) {
+ if (!value) {
+ return gettext("None");
+ }
+
+ let parsed = PVE.Parser.parsePropertyString(value);
+ return Object.entries(parsed)
+ .map(([k, v]) => k + ": " + Proxmox.Utils.format_size(v * 1024) + "/s")
+ .join(',');
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.add_combobox_row('keyboard', gettext('Keyboard Layout'), {
+ renderer: PVE.Utils.render_kvm_language,
+ comboItems: Object.entries(PVE.Utils.kvm_keymaps),
+ defaultValue: '__default__',
+ deleteEmpty: true,
+ });
+ me.add_text_row('http_proxy', gettext('HTTP proxy'), {
+ defaultValue: Proxmox.Utils.noneText,
+ vtype: 'HttpProxy',
+ deleteEmpty: true,
+ });
+ me.add_combobox_row('console', gettext('Console Viewer'), {
+ renderer: PVE.Utils.render_console_viewer,
+ comboItems: Object.entries(PVE.Utils.console_map),
+ defaultValue: '__default__',
+ deleteEmpty: true,
+ });
+ me.add_text_row('email_from', gettext('Email from address'), {
+ deleteEmpty: true,
+ vtype: 'proxmoxMail',
+ defaultValue: 'root@$hostname',
+ });
+ me.add_inputpanel_row('notify', gettext('Notify'), {
+ renderer: v => !v ? 'package-updates=auto' : PVE.Parser.printPropertyString(v),
+ labelWidth: 120,
+ url: "/api2/extjs/cluster/options",
+ //onlineHelp: 'ha_manager_shutdown_policy',
+ items: [{
+ xtype: 'proxmoxKVComboBox',
+ name: 'package-updates',
+ fieldLabel: gettext('Package Updates'),
+ deleteEmpty: false,
+ value: '__default__',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + ' (auto)'],
+ ['auto', gettext('Automatically')],
+ ['always', gettext('Always')],
+ ['never', gettext('Never')],
+ ],
+ defaultValue: '__default__',
+ }],
+ });
+ 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_as_property_string,
+ labelWidth: 120,
+ url: "/api2/extjs/cluster/options",
+ defaultKey: 'type',
+ items: [{
+ xtype: 'displayfield',
+ name: 'type',
+ fieldLabel: gettext('Type'),
+ value: 'secure',
+ submitValue: true,
+ }, {
+ xtype: 'proxmoxNetworkSelector',
+ name: 'network',
+ fieldLabel: gettext('Network'),
+ value: null,
+ emptyText: Proxmox.Utils.defaultText,
+ autoSelect: false,
+ skipEmptyText: true,
+ }],
+ });
+ me.add_inputpanel_row('ha', gettext('HA Settings'), {
+ renderer: PVE.Utils.render_dc_ha_opts,
+ labelWidth: 120,
+ url: "/api2/extjs/cluster/options",
+ onlineHelp: 'ha_manager_shutdown_policy',
+ items: [{
+ xtype: 'proxmoxKVComboBox',
+ name: 'shutdown_policy',
+ fieldLabel: gettext('Shutdown Policy'),
+ deleteEmpty: false,
+ value: '__default__',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + ' (conditional)'],
+ ['freeze', 'freeze'],
+ ['failover', 'failover'],
+ ['migrate', 'migrate'],
+ ['conditional', 'conditional'],
+ ],
+ defaultValue: '__default__',
+ }],
+ });
+ me.add_inputpanel_row('crs', gettext('Cluster Resource Scheduling'), {
+ renderer: PVE.Utils.render_as_property_string,
+ width: 450,
+ labelWidth: 120,
+ url: "/api2/extjs/cluster/options",
+ onlineHelp: 'ha_manager_crs',
+ items: [{
+ xtype: 'proxmoxKVComboBox',
+ name: 'ha',
+ fieldLabel: gettext('HA Scheduling'),
+ deleteEmpty: false,
+ value: '__default__',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + ' (basic)'],
+ ['basic', 'Basic (Resource Count)'],
+ ['static', 'Static Load'],
+ ],
+ defaultValue: '__default__',
+ }, {
+ xtype: 'proxmoxcheckbox',
+ name: 'ha-rebalance-on-start',
+ fieldLabel: gettext('Rebalance on Start'),
+ boxLabel: gettext('Use CRS to select the least loaded node when starting an HA service'),
+ value: 0,
+ }],
+ });
+ me.add_inputpanel_row('u2f', gettext('U2F Settings'), {
+ renderer: v => !v ? Proxmox.Utils.NoneText : PVE.Parser.printPropertyString(v),
+ width: 450,
+ url: "/api2/extjs/cluster/options",
+ onlineHelp: 'pveum_configure_u2f',
+ items: [{
+ xtype: 'textfield',
+ name: 'appid',
+ fieldLabel: gettext('U2F AppID URL'),
+ emptyText: gettext('Defaults to origin'),
+ value: '',
+ deleteEmpty: true,
+ skipEmptyText: true,
+ submitEmptyText: false,
+ }, {
+ xtype: 'textfield',
+ name: 'origin',
+ fieldLabel: gettext('U2F Origin'),
+ emptyText: gettext('Defaults to requesting host URI'),
+ value: '',
+ deleteEmpty: true,
+ skipEmptyText: true,
+ submitEmptyText: false,
+ },
+ {
+ xtype: 'box',
+ height: 25,
+ html: `${gettext('Note:')} `
+ + Ext.String.format(gettext('{0} is deprecated, use {1}'), 'U2F', 'WebAuthn'),
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext('NOTE: Changing an AppID breaks existing U2F registrations!'),
+ }],
+ });
+ me.add_inputpanel_row('webauthn', gettext('WebAuthn Settings'), {
+ renderer: v => !v ? Proxmox.Utils.NoneText : PVE.Parser.printPropertyString(v),
+ width: 450,
+ url: "/api2/extjs/cluster/options",
+ onlineHelp: 'pveum_configure_webauthn',
+ items: [{
+ xtype: 'textfield',
+ fieldLabel: gettext('Name'),
+ name: 'rp', // NOTE: relying party consists of name and id, this is the name
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Origin'),
+ emptyText: Ext.String.format(gettext("Domain Lockdown (e.g., {0})"), document.location.origin),
+ name: 'origin',
+ allowBlank: true,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: 'ID',
+ name: 'id',
+ allowBlank: false,
+ listeners: {
+ dirtychange: (f, isDirty) =>
+ f.up('panel').down('box[id=idChangeWarning]').setHidden(!f.originalValue || !isDirty),
+ },
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'box',
+ flex: 1,
+ },
+ {
+ xtype: 'button',
+ text: gettext('Auto-fill'),
+ iconCls: 'fa fa-fw fa-pencil-square-o',
+ handler: function(button, ev) {
+ let panel = this.up('panel');
+ let fqdn = document.location.hostname;
+
+ panel.down('field[name=rp]').setValue(fqdn);
+
+ let idField = panel.down('field[name=id]');
+ let currentID = idField.getValue();
+ if (!currentID || currentID.length === 0) {
+ idField.setValue(fqdn);
+ }
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'box',
+ height: 25,
+ html: `${gettext('Note:')} `
+ + gettext('WebAuthn requires using a trusted certificate.'),
+ },
+ {
+ xtype: 'box',
+ id: 'idChangeWarning',
+ hidden: true,
+ padding: '5 0 0 0',
+ html: ' '
+ + gettext('Changing the ID breaks existing WebAuthn TFA entries.'),
+ }],
+ });
+ me.add_inputpanel_row('bwlimit', gettext('Bandwidth Limits'), {
+ renderer: me.render_bwlimits,
+ width: 450,
+ url: "/api2/extjs/cluster/options",
+ parseBeforeSet: true,
+ labelWidth: 120,
+ items: [{
+ xtype: 'pveBandwidthField',
+ name: 'default',
+ fieldLabel: gettext('Default'),
+ emptyText: gettext('none'),
+ backendUnit: "KiB",
+ },
+ {
+ xtype: 'pveBandwidthField',
+ name: 'restore',
+ fieldLabel: gettext('Backup Restore'),
+ emptyText: gettext('default'),
+ backendUnit: "KiB",
+ },
+ {
+ xtype: 'pveBandwidthField',
+ name: 'migration',
+ fieldLabel: gettext('Migration'),
+ emptyText: gettext('default'),
+ backendUnit: "KiB",
+ },
+ {
+ xtype: 'pveBandwidthField',
+ name: 'clone',
+ fieldLabel: gettext('Clone'),
+ emptyText: gettext('default'),
+ backendUnit: "KiB",
+ },
+ {
+ xtype: 'pveBandwidthField',
+ name: 'move',
+ fieldLabel: gettext('Disk Move'),
+ emptyText: gettext('default'),
+ backendUnit: "KiB",
+ }],
+ });
+ me.add_integer_row('max_workers', gettext('Maximal Workers/bulk-action'), {
+ deleteEmpty: true,
+ defaultValue: 4,
+ minValue: 1,
+ maxValue: 64, // arbitrary but generous limit as limits are good
+ });
+ me.add_inputpanel_row('next-id', gettext('Next Free VMID Range'), {
+ renderer: PVE.Utils.render_as_property_string,
+ url: "/api2/extjs/cluster/options",
+ items: [{
+ xtype: 'proxmoxintegerfield',
+ name: 'lower',
+ fieldLabel: gettext('Lower'),
+ emptyText: '100',
+ minValue: 100,
+ maxValue: 1000 * 1000 * 1000 - 1,
+ submitValue: true,
+ }, {
+ xtype: 'proxmoxintegerfield',
+ name: 'upper',
+ fieldLabel: gettext('Upper'),
+ emptyText: '1.000.000',
+ minValue: 100,
+ maxValue: 1000 * 1000 * 1000 - 1,
+ submitValue: true,
+ }],
+ });
+ me.rows['tag-style'] = {
+ required: true,
+ renderer: (value) => {
+ if (value === undefined) {
+ return gettext('No Overrides');
+ }
+ let colors = PVE.UIOptions.parseTagOverrides(value?.['color-map']);
+ let shape = value.shape;
+ let shapeText = PVE.UIOptions.tagTreeStyles[shape ?? '__default__'];
+ let txt = Ext.String.format(gettext("Tree Shape: {0}"), shapeText);
+ let orderText = PVE.UIOptions.tagOrderOptions[value.ordering ?? '__default__'];
+ txt += `, ${Ext.String.format(gettext("Ordering: {0}"), orderText)}`;
+ if (value['case-sensitive']) {
+ txt += `, ${gettext('Case-Sensitive')}`;
+ }
+ if (Object.keys(colors).length > 0) {
+ txt += `, ${gettext('Color Overrides')}: `;
+ for (const tag of Object.keys(colors)) {
+ txt += Proxmox.Utils.getTagElement(tag, colors);
+ }
+ }
+ return txt;
+ },
+ header: gettext('Tag Style Override'),
+ editor: {
+ xtype: 'proxmoxWindowEdit',
+ width: 800,
+ subject: gettext('Tag Color Override'),
+ onlineHelp: 'datacenter_configuration_file',
+ fieldDefaults: {
+ labelWidth: 100,
+ },
+ url: '/api2/extjs/cluster/options',
+ items: [
+ {
+ xtype: 'inputpanel',
+ setValues: function(values) {
+ if (values === undefined) {
+ return undefined;
+ }
+ values = values?.['tag-style'] ?? {};
+ values.shape = values.shape || '__default__';
+ values.colors = values['color-map'];
+ return Proxmox.panel.InputPanel.prototype.setValues.call(this, values);
+ },
+ onGetValues: function(values) {
+ let style = {};
+ if (values.colors) {
+ style['color-map'] = values.colors;
+ }
+ if (values.shape && values.shape !== '__default__') {
+ style.shape = values.shape;
+ }
+ if (values.ordering) {
+ style.ordering = values.ordering;
+ }
+ if (values['case-sensitive']) {
+ style['case-sensitive'] = 1;
+ }
+ let value = PVE.Parser.printPropertyString(style);
+ if (value === '') {
+ return {
+ 'delete': 'tag-style',
+ };
+ }
+ return {
+ 'tag-style': value,
+ };
+ },
+ items: [
+ {
+
+ name: 'shape',
+ xtype: 'proxmoxComboGrid',
+ fieldLabel: gettext('Tree Shape'),
+ valueField: 'value',
+ displayField: 'display',
+ allowBlank: false,
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Option'),
+ dataIndex: 'display',
+ flex: 1,
+ },
+ {
+ header: gettext('Preview'),
+ dataIndex: 'value',
+ renderer: function(value) {
+ let cls = value ?? '__default__';
+ if (value === '__default__') {
+ cls = 'circle';
+ }
+ let tags = PVE.Utils.renderTags('preview');
+ return `${tags}
`;
+ },
+ flex: 1,
+ },
+ ],
+ },
+ store: {
+ data: Object.entries(PVE.UIOptions.tagTreeStyles).map(v => ({
+ value: v[0],
+ display: v[1],
+ })),
+ },
+ deleteDefault: true,
+ defaultValue: '__default__',
+ deleteEmpty: true,
+ },
+ {
+ name: 'ordering',
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Ordering'),
+ comboItems: Object.entries(PVE.UIOptions.tagOrderOptions),
+ defaultValue: '__default__',
+ value: '__default__',
+ deleteEmpty: true,
+ },
+ {
+ name: 'case-sensitive',
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Case-Sensitive'),
+ boxLabel: gettext('Applies to new edits'),
+ value: 0,
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Color Overrides'),
+ },
+ {
+ name: 'colors',
+ xtype: 'pveTagColorGrid',
+ deleteEmpty: true,
+ height: 300,
+ },
+ ],
+ },
+ ],
+ },
+ };
+
+ me.rows['user-tag-access'] = {
+ required: true,
+ renderer: (value) => {
+ if (value === undefined) {
+ return Ext.String.format(gettext('Mode: {0}'), 'free');
+ }
+ let mode = value?.['user-allow'] ?? 'free';
+ let list = value?.['user-allow-list']?.join(',') ?? '';
+ let modeTxt = Ext.String.format(gettext('Mode: {0}'), mode);
+ let overrides = PVE.UIOptions.tagOverrides;
+ let tags = PVE.Utils.renderTags(list, overrides);
+ let listTxt = tags !== '' ? `, ${gettext('Pre-defined:')} ${tags}` : '';
+ return `${modeTxt}${listTxt}`;
+ },
+ header: gettext('User Tag Access'),
+ editor: {
+ xtype: 'pveUserTagAccessEdit',
+ },
+ };
+
+ me.rows['registered-tags'] = {
+ required: true,
+ renderer: (value) => {
+ if (value === undefined) {
+ return gettext('No Registered Tags');
+ }
+ let overrides = PVE.UIOptions.tagOverrides;
+ return PVE.Utils.renderTags(value.join(','), overrides);
+ },
+ header: gettext('Registered Tags'),
+ editor: {
+ xtype: 'pveRegisteredTagEdit',
+ },
+ };
+
+ me.selModel = Ext.create('Ext.selection.RowModel', {});
+
+ Ext.apply(me, {
+ tbar: [{
+ text: gettext('Edit'),
+ xtype: 'proxmoxButton',
+ disabled: true,
+ handler: function() { me.run_editor(); },
+ selModel: me.selModel,
+ }],
+ url: "/api2/json/cluster/options",
+ editorConfig: {
+ url: "/api2/extjs/cluster/options",
+ },
+ interval: 5000,
+ cwidth1: 200,
+ listeners: {
+ itemdblclick: me.run_editor,
+ },
+ });
+
+ me.callParent();
+
+ // set the new value for the default console
+ me.mon(me.rstore, 'load', function(store, records, success) {
+ if (!success) {
+ return;
+ }
+
+ var rec = store.getById('console');
+ PVE.UIOptions.options.console = rec.data.value;
+ if (rec.data.value === '__default__') {
+ delete PVE.UIOptions.options.console;
+ }
+
+ PVE.UIOptions.options['tag-style'] = store.getById('tag-style')?.data?.value;
+ PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']);
+ PVE.UIOptions.fireUIConfigChanged();
+ });
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+ },
+});
+Ext.define('pve-permissions', {
+ extend: 'Ext.data.TreeModel',
+ fields: [
+ 'text', 'type',
+ {
+ type: 'boolean', name: 'propagate',
+ },
+ ],
+});
+
+Ext.define('PVE.dc.PermissionGridPanel', {
+ extend: 'Ext.tree.Panel',
+ alias: 'widget.pveUserPermissionGrid',
+
+ onlineHelp: 'chapter_user_management',
+
+ scrollable: true,
+ layout: 'fit',
+ rootVisible: false,
+ animate: false,
+ sortableColumns: false,
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ header: gettext('Path') + '/' + gettext('Permission'),
+ dataIndex: 'text',
+ flex: 6,
+ },
+ {
+ header: gettext('Propagate'),
+ dataIndex: 'propagate',
+ flex: 1,
+ renderer: function(value) {
+ if (Ext.isDefined(value)) {
+ return Proxmox.Utils.format_boolean(value);
+ }
+ return '';
+ },
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ Proxmox.Utils.API2Request({
+ url: '/access/permissions?userid=' + me.userid,
+ method: 'GET',
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, response.htmlStatus);
+ me.load_task.delay(me.load_delay);
+ },
+ success: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, false);
+ let result = Ext.decode(response.responseText);
+ let data = result.data || {};
+
+ let root = {
+ name: '__root',
+ expanded: true,
+ children: [],
+ };
+ let idhash = {
+ '/': {
+ children: [],
+ text: '/',
+ type: 'path',
+ },
+ };
+ Ext.Object.each(data, function(path, perms) {
+ let path_item = {
+ text: path,
+ type: 'path',
+ children: [],
+ };
+ Ext.Object.each(perms, function(perm, propagate) {
+ let perm_item = {
+ text: perm,
+ type: 'perm',
+ propagate: propagate === 1,
+ iconCls: 'fa fa-fw fa-unlock',
+ leaf: true,
+ };
+ path_item.children.push(perm_item);
+ path_item.expandable = true;
+ });
+ idhash[path] = path_item;
+ });
+
+ Ext.Object.each(idhash, function(path, item) {
+ let parent_item = idhash['/'];
+ if (path === '/') {
+ parent_item = root;
+ item.expanded = true;
+ } else {
+ let split_path = path.split('/');
+ while (split_path.pop()) {
+ let parent_path = split_path.join('/');
+ if (idhash[parent_path]) {
+ parent_item = idhash[parent_path];
+ break;
+ }
+ }
+ }
+ parent_item.children.push(item);
+ });
+
+ me.setRootNode(root);
+ },
+ });
+
+ me.callParent();
+
+ me.store.sorters.add(new Ext.util.Sorter({
+ sorterFn: function(rec1, rec2) {
+ let v1 = rec1.data.text,
+ v2 = rec2.data.text;
+ if (rec1.data.type !== rec2.data.type) {
+ v2 = rec1.data.type;
+ v1 = rec2.data.type;
+ }
+ if (v1 > v2) {
+ return 1;
+ } else if (v1 < v2) {
+ return -1;
+ }
+ return 0;
+ },
+ }));
+ },
+});
+
+Ext.define('PVE.dc.PermissionView', {
+ extend: 'Ext.window.Window',
+ alias: 'widget.userShowPermissionWindow',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ scrollable: true,
+ width: 800,
+ height: 600,
+ layout: 'fit',
+ cbind: {
+ title: (get) => Ext.String.htmlEncode(get('userid')) +
+ ` - ${gettext('Granted Permissions')}`,
+ },
+ items: [{
+ xtype: 'pveUserPermissionGrid',
+ cbind: {
+ userid: '{userid}',
+ },
+ }],
+});
+Ext.define('PVE.dc.PoolEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pveDcPoolEdit'],
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ subject: gettext('Pool'),
+
+ cbindData: {
+ poolid: '',
+ isCreate: (cfg) => !cfg.poolid,
+ },
+
+ cbind: {
+ autoLoad: get => !get('isCreate'),
+ url: get => `/api2/extjs/pools/${get('poolid')}`,
+ method: get => get('isCreate') ? 'POST' : 'PUT',
+ },
+
+ items: [
+ {
+ xtype: 'pmxDisplayEditField',
+ fieldLabel: gettext('Name'),
+ cbind: {
+ editable: '{isCreate}',
+ value: '{poolid}',
+ },
+ name: 'poolid',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Comment'),
+ name: 'comment',
+ allowBlank: true,
+ },
+ ],
+});
+Ext.define('PVE.dc.PoolView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pvePoolView'],
+
+ onlineHelp: 'pveum_pools',
+
+ stateful: true,
+ stateId: 'grid-pools',
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-pools',
+ sorters: {
+ property: 'poolid',
+ direction: 'ASC',
+ },
+ });
+
+ var reload = function() {
+ store.load();
+ };
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/pools/',
+ callback: function() {
+ reload();
+ },
+ });
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.dc.PoolEdit', {
+ poolid: rec.data.poolid,
+ });
+ win.on('destroy', reload);
+ win.show();
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ var tbar = [
+ {
+ text: gettext('Create'),
+ handler: function() {
+ var win = Ext.create('PVE.dc.PoolEdit', {});
+ win.on('destroy', reload);
+ win.show();
+ },
+ },
+ edit_btn, remove_btn,
+ ];
+
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: tbar,
+ viewConfig: {
+ trackOver: false,
+ },
+ columns: [
+ {
+ header: gettext('Name'),
+ width: 200,
+ sortable: true,
+ dataIndex: 'poolid',
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ renderer: Ext.String.htmlEncode,
+ dataIndex: 'comment',
+ flex: 1,
+ },
+ ],
+ listeners: {
+ activate: reload,
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.dc.RoleEdit', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveDcRoleEdit',
+
+ width: 400,
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = !me.roleid;
+
+ var url;
+ var method;
+
+ if (me.isCreate) {
+ url = '/api2/extjs/access/roles';
+ method = 'POST';
+ } else {
+ url = '/api2/extjs/access/roles/' + me.roleid;
+ method = 'PUT';
+ }
+
+ Ext.applyIf(me, {
+ subject: gettext('Role'),
+ url: url,
+ method: method,
+ items: [
+ {
+ xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
+ name: 'roleid',
+ value: me.roleid,
+ allowBlank: false,
+ fieldLabel: gettext('Name'),
+ },
+ {
+ xtype: 'pvePrivilegesSelector',
+ name: 'privs',
+ value: me.privs,
+ allowBlank: false,
+ fieldLabel: gettext('Privileges'),
+ },
+ ],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response) {
+ var data = response.result.data;
+ var keys = Ext.Object.getKeys(data);
+
+ me.setValues({
+ privs: keys,
+ roleid: me.roleid,
+ });
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.dc.RoleView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveRoleView'],
+
+ onlineHelp: 'pveum_roles',
+
+ stateful: true,
+ stateId: 'grid-roles',
+
+ initComponent: function() {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'pmx-roles',
+ sorters: {
+ property: 'roleid',
+ direction: 'ASC',
+ },
+ });
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ if (rec.data.special) {
+ return;
+ }
+ Ext.create('PVE.dc.RoleEdit', {
+ roleid: rec.data.roleid,
+ privs: rec.data.privs,
+ listeners: {
+ destroy: () => store.load(),
+ },
+ autoShow: true,
+ });
+ };
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+
+ viewConfig: {
+ trackOver: false,
+ },
+ columns: [
+ {
+ header: gettext('Built-In'),
+ width: 65,
+ sortable: true,
+ dataIndex: 'special',
+ renderer: Proxmox.Utils.format_boolean,
+ },
+ {
+ header: gettext('Name'),
+ width: 150,
+ sortable: true,
+ dataIndex: 'roleid',
+ },
+ {
+ itemid: 'privs',
+ header: gettext('Privileges'),
+ sortable: false,
+ renderer: (value, metaData) => {
+ if (!value) {
+ return '-';
+ }
+ metaData.style = 'white-space:normal;'; // allow word wrap
+ return value.replace(/,/g, ' ');
+ },
+ variableRowHeight: true,
+ dataIndex: 'privs',
+ flex: 1,
+ },
+ ],
+ listeners: {
+ activate: function() {
+ store.load();
+ },
+ itemdblclick: run_editor,
+ },
+ tbar: [
+ {
+ text: gettext('Create'),
+ handler: function() {
+ Ext.create('PVE.dc.RoleEdit', {
+ listeners: {
+ destroy: () => store.load(),
+ },
+ autoShow: true,
+ });
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ enableFn: (rec) => !rec.data.special,
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ selModel: sm,
+ callback: () => store.load(),
+ baseurl: '/access/roles/',
+ enableFn: (rec) => !rec.data.special,
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('pve-security-groups', {
+ extend: 'Ext.data.Model',
+
+ fields: ['group', 'comment', 'digest'],
+ idProperty: 'group',
+});
+
+Ext.define('PVE.SecurityGroupEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ base_url: "/cluster/firewall/groups",
+
+ allow_iface: false,
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = me.group_name === undefined;
+
+ var subject;
+
+ me.url = '/api2/extjs' + me.base_url;
+ me.method = 'POST';
+
+ var items = [
+ {
+ xtype: 'textfield',
+ name: 'group',
+ value: me.group_name || '',
+ fieldLabel: gettext('Name'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ value: me.group_comment || '',
+ fieldLabel: gettext('Comment'),
+ },
+ ];
+
+ if (me.isCreate) {
+ subject = gettext('Security Group');
+ } else {
+ subject = gettext('Security Group') + " '" + me.group_name + "'";
+ items.push({
+ xtype: 'hiddenfield',
+ name: 'rename',
+ value: me.group_name,
+ });
+ }
+
+ var ipanel = Ext.create('Proxmox.panel.InputPanel', {
+ // InputPanel does not have a 'create' property, does it need a 'isCreate'
+ isCreate: me.isCreate,
+ items: items,
+ });
+
+
+ Ext.apply(me, {
+ subject: subject,
+ items: [ipanel],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.SecurityGroupList', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveSecurityGroupList',
+
+ stateful: true,
+ stateId: 'grid-securitygroups',
+
+ rulePanel: undefined,
+
+ addBtn: undefined,
+ removeBtn: undefined,
+ editBtn: undefined,
+
+ base_url: "/cluster/firewall/groups",
+
+ initComponent: function() {
+ let me = this;
+ if (!me.base_url) {
+ throw "no base_url specified";
+ }
+
+ let store = new Ext.data.Store({
+ model: 'pve-security-groups',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json' + me.base_url,
+ },
+ sorters: {
+ property: 'group',
+ direction: 'ASC',
+ },
+ });
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let reload = function() {
+ let oldrec = sm.getSelection()[0];
+ store.load((records, operation, success) => {
+ if (oldrec) {
+ let rec = store.findRecord('group', oldrec.data.group, 0, false, true, true);
+ if (rec) {
+ sm.select(rec);
+ }
+ }
+ });
+ };
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ Ext.create('PVE.SecurityGroupEdit', {
+ digest: rec.data.digest,
+ group_name: rec.data.group,
+ group_comment: rec.data.comment,
+ listeners: {
+ destroy: () => reload(),
+ },
+ autoShow: true,
+ });
+ };
+
+ me.editBtn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ 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: () => reload(),
+ });
+
+ Ext.apply(me, {
+ store: store,
+ tbar: ['' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn],
+ selModel: sm,
+ columns: [
+ {
+ header: gettext('Group'),
+ dataIndex: 'group',
+ width: '100',
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 1,
+ },
+ ],
+ listeners: {
+ itemdblclick: run_editor,
+ select: function(_sm, rec) {
+ if (!me.rulePanel) {
+ me.rulePanel = me.up('panel').down('pveFirewallRules');
+ }
+ me.rulePanel.setBaseUrl(`/cluster/firewall/groups/${rec.data.group}`);
+ },
+ deselect: function() {
+ if (!me.rulePanel) {
+ me.rulePanel = me.up('panel').down('pveFirewallRules');
+ }
+ me.rulePanel.setBaseUrl(undefined);
+ },
+ show: reload,
+ },
+ });
+
+ me.callParent();
+
+ store.load();
+ },
+});
+
+Ext.define('PVE.SecurityGroups', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSecurityGroups',
+
+ title: 'Security Groups',
+ onlineHelp: 'pve_firewall_security_groups',
+
+ layout: 'border',
+
+ items: [
+ {
+ xtype: 'pveFirewallRules',
+ region: 'center',
+ allow_groups: false,
+ list_refs_url: '/cluster/firewall/refs',
+ tbar_prefix: '' + gettext('Rules') + ':',
+ border: false,
+ },
+ {
+ xtype: 'pveSecurityGroupList',
+ region: 'west',
+ width: '25%',
+ border: false,
+ split: true,
+ },
+ ],
+ listeners: {
+ show: function() {
+ let sglist = this.down('pveSecurityGroupList');
+ sglist.fireEvent('show', sglist);
+ },
+ },
+});
+Ext.define('PVE.dc.StorageView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveStorageView'],
+
+ onlineHelp: 'chapter_storage',
+
+ stateful: true,
+ stateId: 'grid-dc-storage',
+
+ createStorageEditWindow: function(type, sid) {
+ let schema = PVE.Utils.storageSchema[type];
+ if (!schema || !schema.ipanel) {
+ throw "no editor registered for storage type: " + type;
+ }
+
+ Ext.create('PVE.storage.BaseEdit', {
+ paneltype: 'PVE.storage.' + schema.ipanel,
+ type: type,
+ storageId: sid,
+ canDoBackups: schema.backups,
+ autoShow: true,
+ listeners: {
+ destroy: this.reloadStore,
+ },
+ });
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'pve-storage',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/storage",
+ },
+ sorters: {
+ property: 'storage',
+ direction: 'ASC',
+ },
+ });
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ let { type, storage } = rec.data;
+ me.createStorageEditWindow(type, storage);
+ };
+
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+ let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/storage/',
+ callback: () => store.load(),
+ });
+
+ // else we cannot dynamically generate the add menu handlers
+ let addHandleGenerator = function(type) {
+ return function() { me.createStorageEditWindow(type); };
+ };
+ let addMenuItems = [];
+ for (const [type, storage] of Object.entries(PVE.Utils.storageSchema)) {
+ if (storage.hideAdd) {
+ continue;
+ }
+ addMenuItems.push({
+ text: PVE.Utils.format_storage_type(type),
+ iconCls: 'fa fa-fw fa-' + storage.faIcon,
+ handler: addHandleGenerator(type),
+ });
+ }
+
+ Ext.apply(me, {
+ store: store,
+ reloadStore: () => store.load(),
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: addMenuItems,
+ }),
+ },
+ remove_btn,
+ edit_btn,
+ ],
+ columns: [
+ {
+ header: 'ID',
+ flex: 2,
+ sortable: true,
+ dataIndex: 'storage',
+ },
+ {
+ header: gettext('Type'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'type',
+ renderer: PVE.Utils.format_storage_type,
+ },
+ {
+ header: gettext('Content'),
+ flex: 3,
+ sortable: true,
+ dataIndex: 'content',
+ renderer: PVE.Utils.format_content_types,
+ },
+ {
+ header: gettext('Path') + '/' + gettext('Target'),
+ flex: 2,
+ sortable: true,
+ dataIndex: 'path',
+ renderer: function(value, metaData, record) {
+ if (record.data.target) {
+ return record.data.target;
+ }
+ return value;
+ },
+ },
+ {
+ header: gettext('Shared'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'shared',
+ renderer: Proxmox.Utils.format_boolean,
+ },
+ {
+ header: gettext('Enabled'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'disable',
+ renderer: Proxmox.Utils.format_neg_boolean,
+ },
+ {
+ header: gettext('Bandwidth Limit'),
+ flex: 2,
+ sortable: true,
+ dataIndex: 'bwlimit',
+ },
+ ],
+ listeners: {
+ activate: () => store.load(),
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ },
+}, function() {
+ Ext.define('pve-storage', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'path', 'type', 'content', 'server', 'portal', 'target', 'export', 'storage',
+ { name: 'shared', type: 'boolean' },
+ { name: 'disable', type: 'boolean' },
+ ],
+ idProperty: 'storage',
+ });
+});
+Ext.define('PVE.dc.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveDcSummary',
+
+ scrollable: true,
+
+ bodyPadding: 5,
+
+ layout: 'column',
+
+ defaults: {
+ padding: 5,
+ columnWidth: 1,
+ },
+
+ items: [
+ {
+ itemId: 'dcHealth',
+ xtype: 'pveDcHealth',
+ },
+ {
+ itemId: 'dcGuests',
+ xtype: 'pveDcGuests',
+ },
+ {
+ title: gettext('Resources'),
+ xtype: 'panel',
+ minHeight: 250,
+ bodyPadding: 5,
+ layout: 'hbox',
+ defaults: {
+ xtype: 'proxmoxGauge',
+ flex: 1,
+ },
+ items: [
+ {
+ title: gettext('CPU'),
+ itemId: 'cpu',
+ },
+ {
+ title: gettext('Memory'),
+ itemId: 'memory',
+ },
+ {
+ 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');
+ }
+ },
+ },
+ },
+ ],
+ },
+ ],
+
+ listeners: {
+ resize: function(panel) {
+ Proxmox.Utils.updateColumns(panel);
+ },
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var rstore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 3000,
+ storeid: 'pve-cluster-status',
+ model: 'pve-dc-nodes',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/status",
+ },
+ });
+
+ var gridstore = Ext.create('Proxmox.data.DiffStore', {
+ rstore: rstore,
+ filters: {
+ property: 'type',
+ value: 'node',
+ },
+ sorters: {
+ property: 'id',
+ direction: 'ASC',
+ },
+ });
+
+ me.callParent();
+
+ me.getComponent('nodeview').setStore(gridstore);
+
+ var gueststatus = me.getComponent('dcGuests');
+
+ var cpustat = me.down('#cpu');
+ var memorystat = me.down('#memory');
+ var storagestat = me.down('#storage');
+ var sp = Ext.state.Manager.getProvider();
+
+ me.mon(PVE.data.ResourceStore, 'load', function(curstore, results) {
+ me.suspendLayout = true;
+
+ let cpu = 0, maxcpu = 0;
+ let memory = 0, maxmem = 0;
+
+ let used = 0, total = 0;
+ let countedStorage = {}, usableStorages = {};
+ let storages = sp.get('dash-storages') || '';
+ storages.split(',').filter(v => v !== '').forEach(storage => {
+ usableStorages[storage] = true;
+ });
+
+ let qemu = {
+ running: 0,
+ paused: 0,
+ stopped: 0,
+ template: 0,
+ };
+ let lxc = {
+ running: 0,
+ paused: 0,
+ stopped: 0,
+ template: 0,
+ };
+ let error = 0;
+
+ for (const { data } of results) {
+ switch (data.type) {
+ case 'node':
+ cpu += data.cpu * data.maxcpu;
+ maxcpu += data.maxcpu || 0;
+ memory += data.mem || 0;
+ maxmem += data.maxmem || 0;
+
+ if (gridstore.getById(data.id)) {
+ let griditem = gridstore.getById(data.id);
+ griditem.set('cpuusage', data.cpu);
+ let max = data.maxmem || 1;
+ let val = data.mem || 0;
+ griditem.set('memoryusage', val / max);
+ griditem.set('uptime', data.uptime);
+ griditem.commit(); // else the store marks the field as dirty
+ }
+ break;
+ case 'storage': {
+ let sid = !data.shared || data.storage === 'local' ? data.id : data.storage;
+ if (!Ext.Object.isEmpty(usableStorages)) {
+ if (usableStorages[data.id] !== true) {
+ break;
+ }
+ sid = data.id;
+ } else if (countedStorage[sid]) {
+ break;
+ }
+ used += data.disk;
+ total += data.maxdisk;
+ countedStorage[sid] = true;
+ break;
+ }
+ case 'qemu':
+ qemu[data.template ? 'template' : data.status]++;
+ if (data.hastate === 'error') {
+ error++;
+ }
+ break;
+ case 'lxc':
+ lxc[data.template ? 'template' : data.status]++;
+ if (data.hastate === 'error') {
+ error++;
+ }
+ break;
+ default: break;
+ }
+ }
+
+ let text = Ext.String.format(gettext('of {0} CPU(s)'), maxcpu);
+ cpustat.updateValue(cpu/maxcpu, text);
+
+ text = Ext.String.format(gettext('{0} of {1}'), Proxmox.Utils.render_size(memory), Proxmox.Utils.render_size(maxmem));
+ memorystat.updateValue(memory/maxmem, text);
+
+ text = Ext.String.format(gettext('{0} of {1}'), Proxmox.Utils.render_size(used), Proxmox.Utils.render_size(total));
+ storagestat.updateValue(used/total, text);
+
+ gueststatus.updateValues(qemu, lxc, error);
+
+ me.suspendLayout = false;
+ me.updateLayout(true);
+ });
+
+ let dcHealth = me.getComponent('dcHealth');
+ me.mon(rstore, 'load', dcHealth.updateStatus, dcHealth);
+
+ let subs = me.down('#subscriptions');
+ me.mon(rstore, 'load', function(store, records, success) {
+ var level;
+ var mixed = false;
+ for (let i = 0; i < records.length; i++) {
+ let node = records[i];
+ if (node.get('type') !== 'node' || node.get('status') === 'offline') {
+ continue;
+ }
+
+ let curlevel = node.get('level');
+ if (curlevel === '') { // no subscription beats all, set it and break the loop
+ level = '';
+ break;
+ }
+
+ if (level === undefined) { // save level
+ level = curlevel;
+ } else if (level !== curlevel) { // detect different levels
+ mixed = true;
+ }
+ }
+
+ let data = {
+ title: Proxmox.Utils.unknownText,
+ text: Proxmox.Utils.unknownText,
+ iconCls: PVE.Utils.get_health_icon(undefined, true),
+ };
+ if (level === '') {
+ data = {
+ title: gettext('No Subscription'),
+ iconCls: PVE.Utils.get_health_icon('critical', true),
+ text: gettext('You have at least one node without subscription.'),
+ };
+ subs.setUserCls('pointer');
+ } else if (mixed) {
+ data = {
+ title: gettext('Mixed Subscriptions'),
+ iconCls: PVE.Utils.get_health_icon('warning', true),
+ text: gettext('Warning: Your subscription levels are not the same.'),
+ };
+ subs.setUserCls('pointer');
+ } else if (level) {
+ data = {
+ title: PVE.Utils.render_support_level(level),
+ iconCls: PVE.Utils.get_health_icon('good', true),
+ text: gettext('Your subscription status is valid.'),
+ };
+ subs.setUserCls('');
+ }
+
+ subs.setData(data);
+ });
+
+ me.on('destroy', function() {
+ rstore.stopUpdate();
+ });
+
+ me.mon(sp, 'statechange', function(provider, key, value) {
+ if (key !== 'summarycolumns') {
+ return;
+ }
+ Proxmox.Utils.updateColumns(me);
+ });
+
+ rstore.startUpdate();
+ },
+
+});
+Ext.define('PVE.dc.Support', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveDcSupport',
+ pveGuidePath: '/pve-docs/index.html',
+ onlineHelp: 'getting_help',
+
+ invalidHtml: 'No valid subscription
' + PVE.Utils.noSubKeyHtml,
+
+ communityHtml: 'Please use the public community forum for any questions.',
+
+ activeHtml: 'Please use our support portal for any questions. You can also use the public community forum to get additional information.',
+
+ bugzillaHtml: 'Bug Tracking
Our bug tracking system is available here.',
+
+ docuHtml: function() {
+ var me = this;
+ var guideUrl = window.location.origin + me.pveGuidePath;
+ var text = Ext.String.format('Documentation
'
+ + 'The official Proxmox VE Administration Guide'
+ + ' is included with this installation and can be browsed at '
+ + '{0}', guideUrl);
+ return text;
+ },
+
+ updateActive: function(data) {
+ var me = this;
+
+ var html = '' + data.productname + '
' + me.activeHtml;
+ html += '
' + me.docuHtml();
+ html += '
' + me.bugzillaHtml;
+
+ me.update(html);
+ },
+
+ updateCommunity: function(data) {
+ var me = this;
+
+ var html = '' + data.productname + '
' + me.communityHtml;
+ html += '
' + me.docuHtml();
+ html += '
' + me.bugzillaHtml;
+
+ me.update(html);
+ },
+
+ updateInactive: function(data) {
+ var me = this;
+ me.update(me.invalidHtml);
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let reload = function() {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/localhost/subscription',
+ method: 'GET',
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ me.update(`${gettext('Unable to load subscription status')}: ${response.htmlStatus}`);
+ },
+ success: function(response, opts) {
+ let data = response.result.data;
+ if (data?.status.toLowerCase() === 'active') {
+ if (data.level === 'c') {
+ me.updateCommunity(data);
+ } else {
+ me.updateActive(data);
+ }
+ } else {
+ me.updateInactive(data);
+ }
+ },
+ });
+ };
+
+ Ext.apply(me, {
+ autoScroll: true,
+ bodyStyle: 'padding:10px',
+ listeners: {
+ activate: reload,
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.dc.SyncWindow', {
+ extend: 'Ext.window.Window',
+
+ title: gettext('Realm Sync'),
+
+ width: 600,
+ bodyPadding: 10,
+ modal: true,
+ resizable: false,
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ control: {
+ 'form': {
+ validitychange: function(field, valid) {
+ let me = this;
+ me.lookup('preview_btn').setDisabled(!valid);
+ me.lookup('sync_btn').setDisabled(!valid);
+ },
+ },
+ 'button': {
+ click: function(btn) {
+ if (btn.reference === 'help_btn') return;
+ this.sync_realm(btn.reference === 'preview_btn');
+ },
+ },
+ },
+
+ sync_realm: function(is_preview) {
+ let me = this;
+ let view = me.getView();
+ let ipanel = me.lookup('ipanel');
+ let params = ipanel.getValues();
+
+ let vanished_opts = [];
+ ['acl', 'entry', 'properties'].forEach((prop) => {
+ if (params[`remove-vanished-${prop}`]) {
+ vanished_opts.push(prop);
+ }
+ delete params[`remove-vanished-${prop}`];
+ });
+ if (vanished_opts.length > 0) {
+ params['remove-vanished'] = vanished_opts.join(';');
+ } else {
+ params['remove-vanished'] = 'none';
+ }
+
+ params['dry-run'] = is_preview ? 1 : 0;
+ Proxmox.Utils.API2Request({
+ url: `/access/domains/${view.realm}/sync`,
+ waitMsgTarget: view,
+ method: 'POST',
+ params,
+ failure: function(response) {
+ view.show();
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response) {
+ view.hide();
+ Ext.create('Proxmox.window.TaskViewer', {
+ upid: response.result.data,
+ listeners: {
+ destroy: function() {
+ if (is_preview) {
+ view.show();
+ } else {
+ view.close();
+ }
+ },
+ },
+ }).show();
+ },
+ });
+ },
+ },
+
+ items: [
+ {
+ xtype: 'form',
+ reference: 'form',
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%',
+ },
+ items: [{
+ xtype: 'inputpanel',
+ reference: 'ipanel',
+ column1: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'scope',
+ fieldLabel: gettext('Scope'),
+ value: '',
+ emptyText: gettext('No default available'),
+ deleteEmpty: false,
+ allowBlank: false,
+ comboItems: [
+ ['users', gettext('Users')],
+ ['groups', gettext('Groups')],
+ ['both', gettext('Users and Groups')],
+ ],
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ value: '1',
+ deleteEmpty: false,
+ allowBlank: false,
+ comboItems: [
+ ['1', Proxmox.Utils.yesText],
+ ['0', Proxmox.Utils.noText],
+ ],
+ name: 'enable-new',
+ fieldLabel: gettext('Enable new'),
+ },
+ ],
+
+ columnB: [
+ {
+ xtype: 'fieldset',
+ title: gettext('Remove Vanished Options'),
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('ACL'),
+ name: 'remove-vanished-acl',
+ boxLabel: gettext('Remove ACLs of vanished users and groups.'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Entry'),
+ name: 'remove-vanished-entry',
+ boxLabel: gettext('Remove vanished user and group entries.'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Properties'),
+ name: 'remove-vanished-properties',
+ boxLabel: gettext('Remove vanished properties from synced users.'),
+ },
+ ],
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'defaulthint',
+ value: gettext('Default sync options can be set by editing the realm.'),
+ userCls: 'pmx-hint',
+ hidden: true,
+ },
+ ],
+ }],
+ },
+ ],
+
+ buttons: [
+ {
+ xtype: 'proxmoxHelpButton',
+ reference: 'help_btn',
+ onlineHelp: 'pveum_ldap_sync',
+ hidden: false,
+ },
+ '->',
+ {
+ text: gettext('Preview'),
+ reference: 'preview_btn',
+ },
+ {
+ text: gettext('Sync'),
+ reference: 'sync_btn',
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.realm) {
+ throw "no realm defined";
+ }
+
+ me.callParent();
+
+ Proxmox.Utils.API2Request({
+ url: `/access/domains/${me.realm}`,
+ waitMsgTarget: me,
+ method: 'GET',
+ failure: function(response) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ me.close();
+ },
+ success: function(response) {
+ let default_options = response.result.data['sync-defaults-options'];
+ if (default_options) {
+ let options = PVE.Parser.parsePropertyString(default_options);
+ if (options['remove-vanished']) {
+ let opts = options['remove-vanished'].split(';');
+ for (const opt of opts) {
+ options[`remove-vanished-${opt}`] = 1;
+ }
+ }
+ let ipanel = me.lookup('ipanel');
+ ipanel.setValues(options);
+ } else {
+ me.lookup('defaulthint').setVisible(true);
+ }
+
+ // check validity for button state
+ me.lookup('form').isValid();
+ },
+ });
+ },
+});
+/* This class defines the "Tasks" tab of the bottom status panel
+ * Tasks are jobs with a start, end and log output
+ */
+
+Ext.define('PVE.dc.Tasks', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveClusterTasks'],
+
+ initComponent: function() {
+ let me = this;
+
+ let taskstore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'pve-cluster-tasks',
+ model: 'proxmox-tasks',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/tasks',
+ },
+ });
+ let store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: taskstore,
+ sortAfterUpdate: true,
+ appendAtStart: true,
+ sorters: [
+ {
+ property: 'pid',
+ direction: 'DESC',
+ },
+ {
+ property: 'starttime',
+ direction: 'DESC',
+ },
+ ],
+
+ });
+
+ let run_task_viewer = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: rec.data.upid,
+ endtime: rec.data.endtime,
+ });
+ win.show();
+ };
+
+ Ext.apply(me, {
+ store: store,
+ stateful: false,
+ viewConfig: {
+ trackOver: false,
+ stripeRows: true, // does not work with getRowClass()
+ getRowClass: function(record, index) {
+ let taskState = record.get('status');
+ if (taskState) {
+ let parsed = Proxmox.Utils.parse_task_status(taskState);
+ if (parsed === 'warning') {
+ return "proxmox-warning-row";
+ } else if (parsed !== 'ok') {
+ return "proxmox-invalid-row";
+ }
+ }
+ return '';
+ },
+ },
+ sortableColumns: false,
+ columns: [
+ {
+ header: gettext("Start Time"),
+ dataIndex: 'starttime',
+ width: 150,
+ renderer: function(value) {
+ return Ext.Date.format(value, "M d H:i:s");
+ },
+ },
+ {
+ header: gettext("End Time"),
+ dataIndex: 'endtime',
+ width: 150,
+ 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',
+ renderer: Ext.String.htmlEncode,
+ width: 150,
+ },
+ {
+ header: gettext("Description"),
+ dataIndex: 'upid',
+ flex: 1,
+ renderer: Proxmox.Utils.render_upid,
+ },
+ {
+ header: gettext("Status"),
+ dataIndex: 'status',
+ width: 200,
+ renderer: function(value, metaData, record) {
+ if (record.data.pid) {
+ if (record.data.type !== "vncproxy") {
+ metaData.tdCls = "x-grid-row-loading";
+ }
+ return "";
+ }
+ return Proxmox.Utils.format_task_status(value);
+ },
+ },
+ ],
+ listeners: {
+ itemdblclick: run_task_viewer,
+ show: () => taskstore.startUpdate(),
+ destroy: () => taskstore.stopUpdate(),
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.dc.TokenEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pveDcTokenEdit'],
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ subject: gettext('Token'),
+ onlineHelp: 'pveum_tokens',
+
+ isAdd: true,
+ isCreate: false,
+ fixedUser: false,
+
+ method: 'POST',
+ url: '/api2/extjs/access/users/',
+
+ defaultFocus: 'field[disabled=false][hidden=false][name=tokenid]',
+
+ items: {
+ xtype: 'inputpanel',
+ onGetValues: function(values) {
+ let me = this;
+ let win = me.up('pveDcTokenEdit');
+ win.url = '/api2/extjs/access/users/';
+ let uid = encodeURIComponent(values.userid);
+ let tid = encodeURIComponent(values.tokenid);
+ delete values.userid;
+ delete values.tokenid;
+
+ win.url += `${uid}/token/${tid}`;
+ return values;
+ },
+ column1: [
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: (get) => get('isCreate') && !get('fixedUser'),
+ },
+ submitValue: true,
+ editConfig: {
+ xtype: 'pmxUserSelector',
+ allowBlank: false,
+ },
+ name: 'userid',
+ value: Proxmox.UserName,
+ renderer: Ext.String.htmlEncode,
+ fieldLabel: gettext('User'),
+ },
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: '{isCreate}',
+ },
+ name: 'tokenid',
+ fieldLabel: gettext('Token ID'),
+ submitValue: true,
+ minLength: 2,
+ allowBlank: false,
+ },
+ ],
+ column2: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'privsep',
+ checked: true,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Privilege Separation'),
+ },
+ {
+ xtype: 'pmxExpireDate',
+ name: 'expire',
+ },
+ ],
+ columnB: [
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ fieldLabel: gettext('Comment'),
+ },
+ ],
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ me.setValues(response.result.data);
+ },
+ });
+ }
+ },
+ apiCallDone: function(success, response, options) {
+ let res = response.result.data;
+ if (!success || !res.value) {
+ return;
+ }
+
+ Ext.create('PVE.dc.TokenShow', {
+ autoShow: true,
+ tokenid: res['full-tokenid'],
+ secret: res.value,
+ });
+ },
+});
+
+Ext.define('PVE.dc.TokenShow', {
+ extend: 'Ext.window.Window',
+ alias: ['widget.pveTokenShow'],
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 600,
+ modal: true,
+ resizable: false,
+ title: gettext('Token Secret'),
+
+ items: [
+ {
+ xtype: 'container',
+ layout: 'form',
+ bodyPadding: 10,
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%',
+ },
+ padding: '0 10 10 10',
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Token ID'),
+ cbind: {
+ value: '{tokenid}',
+ },
+ editable: false,
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Secret'),
+ inputId: 'token-secret-value',
+ cbind: {
+ value: '{secret}',
+ },
+ editable: false,
+ },
+ ],
+ },
+ {
+ xtype: 'component',
+ border: false,
+ padding: '10 10 10 10',
+ userCls: 'pmx-hint',
+ html: gettext('Please record the API token secret - it will only be displayed now'),
+ },
+ ],
+ buttons: [
+ {
+ handler: function(b) {
+ document.getElementById('token-secret-value').select();
+ document.execCommand("copy");
+ },
+ text: gettext('Copy Secret Value'),
+ iconCls: 'fa fa-clipboard',
+ },
+ ],
+});
+Ext.define('PVE.dc.TokenView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveTokenView'],
+
+ onlineHelp: 'chapter_user_management',
+
+ stateful: true,
+ stateId: 'grid-tokens',
+
+ // use fixed user
+ fixedUser: undefined,
+
+ initComponent: function() {
+ let me = this;
+
+ let caps = Ext.state.Manager.get('GuiCap');
+
+ let store = new Ext.data.Store({
+ id: "tokens",
+ model: 'pve-tokens',
+ sorters: 'id',
+ });
+
+ let reload = function() {
+ if (me.fixedUser) {
+ Proxmox.Utils.API2Request({
+ url: `/access/users/${encodeURIComponent(me.fixedUser)}/token`,
+ method: 'GET',
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, response.htmlStatus);
+ me.load_task.delay(me.load_delay);
+ },
+ success: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, false);
+ let result = Ext.decode(response.responseText);
+ let data = result.data || [];
+ let records = [];
+ Ext.Array.each(data, function(token) {
+ let r = {};
+ r.id = me.fixedUser + '!' + token.tokenid;
+ r.userid = me.fixedUser;
+ r.tokenid = token.tokenid;
+ r.comment = token.comment;
+ r.expire = token.expire;
+ r.privsep = token.privsep === 1;
+ records.push(r);
+ });
+ store.loadData(records);
+ },
+ });
+ return;
+ }
+ Proxmox.Utils.API2Request({
+ url: '/access/users/?full=1',
+ method: 'GET',
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, response.htmlStatus);
+ me.load_task.delay(me.load_delay);
+ },
+ success: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, false);
+ let result = Ext.decode(response.responseText);
+ let data = result.data || [];
+ let records = [];
+ Ext.Array.each(data, function(user) {
+ let tokens = user.tokens || [];
+ Ext.Array.each(tokens, function(token) {
+ let r = {};
+ r.id = user.userid + '!' + token.tokenid;
+ r.userid = user.userid;
+ r.tokenid = token.tokenid;
+ r.comment = token.comment;
+ r.expire = token.expire;
+ r.privsep = token.privsep === 1;
+ records.push(r);
+ });
+ });
+ store.loadData(records);
+ },
+ });
+ };
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let urlFromRecord = (rec) => {
+ let uid = encodeURIComponent(rec.data.userid);
+ let tid = encodeURIComponent(rec.data.tokenid);
+ return `/access/users/${uid}/token/${tid}`;
+ };
+
+ let run_editor = function(rec) {
+ if (!caps.access['User.Modify']) {
+ return;
+ }
+
+ let win = Ext.create('PVE.dc.TokenEdit', {
+ method: 'PUT',
+ url: urlFromRecord(rec),
+ });
+ win.setValues(rec.data);
+ win.on('destroy', reload);
+ win.show();
+ };
+
+ let tbar = [
+ {
+ text: gettext('Add'),
+ disabled: !caps.access['User.Modify'],
+ handler: function(btn, e, rec) {
+ let data = {};
+ if (me.fixedUser) {
+ data.userid = me.fixedUser;
+ data.fixedUser = true;
+ } else if (rec && rec.data) {
+ data.userid = rec.data.userid;
+ }
+ let win = Ext.create('PVE.dc.TokenEdit', {
+ isCreate: true,
+ fixedUser: me.fixedUser,
+ });
+ win.setValues(data);
+ win.on('destroy', reload);
+ win.show();
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ disabled: true,
+ enableFn: (rec) => !!caps.access['User.Modify'],
+ selModel: sm,
+ handler: (btn, e, rec) => run_editor(rec),
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ selModel: sm,
+ enableFn: (rec) => !!caps.access['User.Modify'],
+ callback: reload,
+ getUrl: urlFromRecord,
+ },
+ '-',
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Show Permissions'),
+ disabled: true,
+ selModel: sm,
+ handler: function(btn, event, rec) {
+ Ext.create('PVE.dc.PermissionView', {
+ autoShow: true,
+ userid: rec.data.id,
+ });
+ },
+ },
+ ];
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: tbar,
+ viewConfig: {
+ trackOver: false,
+ },
+ columns: [
+ {
+ header: gettext('User name'),
+ dataIndex: 'userid',
+ renderer: (uid) => {
+ let realmIndex = uid.lastIndexOf('@');
+ let user = Ext.String.htmlEncode(uid.substr(0, realmIndex));
+ let realm = Ext.String.htmlEncode(uid.substr(realmIndex));
+ return `${user} ${realm}`;
+ },
+ hidden: !!me.fixedUser,
+ flex: 2,
+ },
+ {
+ header: gettext('Token Name'),
+ dataIndex: 'tokenid',
+ hideable: false,
+ flex: 1,
+ },
+ {
+ header: gettext('Expire'),
+ dataIndex: 'expire',
+ hideable: false,
+ renderer: Proxmox.Utils.format_expire,
+ flex: 1,
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ renderer: Ext.String.htmlEncode,
+ flex: 3,
+ },
+ {
+ header: gettext('Privilege Separation'),
+ dataIndex: 'privsep',
+ hideable: false,
+ renderer: Proxmox.Utils.format_boolean,
+ flex: 1,
+ },
+ ],
+ listeners: {
+ activate: reload,
+ itemdblclick: (view, rec) => run_editor(rec),
+ },
+ });
+
+ if (me.fixedUser) {
+ reload();
+ }
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.window.TokenView', {
+ extend: 'Ext.window.Window',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ modal: true,
+ subject: gettext('API Tokens'),
+ scrollable: true,
+ layout: 'fit',
+ width: 800,
+ height: 400,
+ cbind: {
+ title: gettext('API Tokens') + ' - {userid}',
+ },
+ items: [{
+ xtype: 'pveTokenView',
+ cbind: {
+ fixedUser: '{userid}',
+ },
+ }],
+});
+Ext.define('PVE.dc.UserEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pveDcUserEdit'],
+
+ isAdd: true,
+
+ initComponent: function() {
+ let me = this;
+
+ me.isCreate = !me.userid;
+
+ let url = '/api2/extjs/access/users';
+ let method = 'POST';
+ if (!me.isCreate) {
+ url += '/' + encodeURIComponent(me.userid);
+ method = 'PUT';
+ }
+
+ let verifypw, pwfield;
+ let validate_pw = function() {
+ if (verifypw.getValue() !== pwfield.getValue()) {
+ return gettext("Passwords do not match");
+ }
+ return true;
+ };
+ verifypw = Ext.createWidget('textfield', {
+ inputType: 'password',
+ fieldLabel: gettext('Confirm password'),
+ name: 'verifypassword',
+ submitValue: false,
+ disabled: true,
+ hidden: true,
+ validator: validate_pw,
+ });
+
+ pwfield = Ext.createWidget('textfield', {
+ inputType: 'password',
+ fieldLabel: gettext('Password'),
+ minLength: 5,
+ name: 'password',
+ disabled: true,
+ hidden: true,
+ validator: validate_pw,
+ });
+
+ let column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'userid',
+ fieldLabel: gettext('User name'),
+ value: me.userid,
+ renderer: Ext.String.htmlEncode,
+ allowBlank: false,
+ submitValue: !!me.isCreate,
+ },
+ pwfield,
+ verifypw,
+ {
+ xtype: 'pveGroupSelector',
+ name: 'groups',
+ multiSelect: true,
+ allowBlank: true,
+ fieldLabel: gettext('Group'),
+ },
+ {
+ xtype: 'pmxExpireDate',
+ name: 'expire',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Enabled'),
+ name: 'enable',
+ uncheckedValue: 0,
+ defaultValue: 1,
+ checked: true,
+ },
+ ];
+
+ let column2 = [
+ {
+ xtype: 'textfield',
+ name: 'firstname',
+ fieldLabel: gettext('First Name'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'lastname',
+ fieldLabel: gettext('Last Name'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'email',
+ fieldLabel: gettext('E-Mail'),
+ vtype: 'proxmoxMail',
+ },
+ ];
+
+ if (me.isCreate) {
+ column1.splice(1, 0, {
+ xtype: 'pmxRealmComboBox',
+ name: 'realm',
+ fieldLabel: gettext('Realm'),
+ allowBlank: false,
+ matchFieldWidth: false,
+ listConfig: { width: 300 },
+ listeners: {
+ change: function(combo, realm) {
+ me.realm = realm;
+ pwfield.setVisible(realm === 'pve');
+ pwfield.setDisabled(realm !== 'pve');
+ verifypw.setVisible(realm === 'pve');
+ verifypw.setDisabled(realm !== 'pve');
+ },
+ },
+ submitValue: false,
+ });
+ }
+
+ var ipanel = Ext.create('Proxmox.panel.InputPanel', {
+ column1: column1,
+ column2: column2,
+ columnB: [
+ {
+ xtype: 'textfield',
+ name: 'comment',
+ fieldLabel: gettext('Comment'),
+ },
+ ],
+ advancedItems: [
+ {
+ xtype: 'textfield',
+ name: 'keys',
+ fieldLabel: gettext('Key IDs'),
+ },
+ ],
+ onGetValues: function(values) {
+ if (me.realm) {
+ values.userid = values.userid + '@' + me.realm;
+ }
+ if (!values.password) {
+ delete values.password;
+ }
+ return values;
+ },
+ });
+
+ Ext.applyIf(me, {
+ subject: gettext('User'),
+ url: url,
+ method: method,
+ fieldDefaults: {
+ labelWidth: 110, // some translation are quite long (e.g., Spanish)
+ },
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+ me.setValues(data);
+ if (data.keys) {
+ if (data.keys === 'x!oath' || data.keys === 'x!u2f') {
+ me.down('[name="keys"]').setDisabled(1);
+ }
+ }
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.dc.UserView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: ['widget.pveUserView'],
+
+ onlineHelp: 'pveum_users',
+
+ stateful: true,
+ stateId: 'grid-users',
+
+ initComponent: function() {
+ var me = this;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var store = new Ext.data.Store({
+ id: "users",
+ model: 'pmx-users',
+ sorters: {
+ property: 'userid',
+ direction: 'ASC',
+ },
+ });
+ let reload = () => store.load();
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/access/users/',
+ dangerous: true,
+ enableFn: rec => caps.access['User.Modify'] && rec.data.userid !== 'root@pam',
+ callback: () => reload(),
+ });
+ let run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec || !caps.access['User.Modify']) {
+ return;
+ }
+ Ext.create('PVE.dc.UserEdit', {
+ userid: rec.data.userid,
+ autoShow: true,
+ listeners: {
+ destroy: () => reload(),
+ },
+ });
+ };
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ enableFn: function(rec) {
+ return !!caps.access['User.Modify'];
+ },
+ selModel: sm,
+ handler: run_editor,
+ });
+ let pwchange_btn = new Proxmox.button.Button({
+ text: gettext('Password'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(record) {
+ let type = record.data['realm-type'];
+ if (type) {
+ if (PVE.Utils.authSchema[type]) {
+ return !!PVE.Utils.authSchema[type].pwchange;
+ }
+ }
+ return false;
+ },
+ handler: function(btn, event, rec) {
+ Ext.create('Proxmox.window.PasswordEdit', {
+ userid: rec.data.userid,
+ autoShow: true,
+ listeners: {
+ destroy: () => reload(),
+ },
+ });
+ },
+ });
+
+ var perm_btn = new Proxmox.button.Button({
+ text: gettext('Permissions'),
+ disabled: true,
+ selModel: sm,
+ handler: function(btn, event, rec) {
+ Ext.create('PVE.dc.PermissionView', {
+ userid: rec.data.userid,
+ autoShow: true,
+ listeners: {
+ destroy: () => reload(),
+ },
+ });
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ {
+ text: gettext('Add'),
+ disabled: !caps.access['User.Modify'],
+ handler: function() {
+ Ext.create('PVE.dc.UserEdit', {
+ autoShow: true,
+ listeners: {
+ destroy: () => reload(),
+ },
+ });
+ },
+ },
+ '-',
+ edit_btn,
+ remove_btn,
+ '-',
+ pwchange_btn,
+ '-',
+ perm_btn,
+ ],
+ viewConfig: {
+ trackOver: false,
+ },
+ columns: [
+ {
+ header: gettext('User name'),
+ width: 200,
+ sortable: true,
+ renderer: Proxmox.Utils.render_username,
+ dataIndex: 'userid',
+ },
+ {
+ header: gettext('Realm'),
+ width: 100,
+ sortable: true,
+ renderer: Proxmox.Utils.render_realm,
+ dataIndex: 'userid',
+ },
+ {
+ header: gettext('Enabled'),
+ width: 80,
+ sortable: true,
+ renderer: Proxmox.Utils.format_boolean,
+ dataIndex: 'enable',
+ },
+ {
+ header: gettext('Expire'),
+ width: 80,
+ sortable: true,
+ renderer: Proxmox.Utils.format_expire,
+ dataIndex: 'expire',
+ },
+ {
+ header: gettext('Name'),
+ width: 150,
+ sortable: true,
+ renderer: PVE.Utils.render_full_name,
+ dataIndex: 'firstname',
+ },
+ {
+ header: 'TFA',
+ width: 50,
+ sortable: true,
+ renderer: function(v) {
+ let tfa_type = PVE.Parser.parseTfaType(v);
+ if (tfa_type === undefined) {
+ return Proxmox.Utils.noText;
+ } else if (tfa_type === 1) {
+ return Proxmox.Utils.yesText;
+ } else {
+ return tfa_type;
+ }
+ },
+ dataIndex: 'keys',
+ },
+ {
+ header: gettext('Comment'),
+ sortable: false,
+ renderer: Ext.String.htmlEncode,
+ dataIndex: 'comment',
+ flex: 1,
+ },
+ ],
+ listeners: {
+ activate: reload,
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, store);
+ },
+});
+Ext.define('PVE.dc.MetricServerView', {
+ extend: 'Ext.grid.Panel',
+ alias: ['widget.pveMetricServerView'],
+
+ stateful: true,
+ stateId: 'grid-metricserver',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ render_type: function(value) {
+ switch (value) {
+ case 'influxdb': return "InfluxDB";
+ case 'graphite': return "Graphite";
+ default: return Proxmox.Utils.unknownText;
+ }
+ },
+
+ editWindow: function(xtype, id) {
+ let me = this;
+ Ext.create(`PVE.dc.${xtype}Edit`, {
+ serverid: id,
+ autoShow: true,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+
+ addServer: function(button) {
+ this.editWindow(button.text);
+ },
+
+ editServer: function() {
+ let me = this;
+ let view = me.getView();
+ let selection = view.getSelection();
+ if (!selection || selection.length < 1) {
+ return;
+ }
+
+ let cfg = selection[0].data;
+
+ let xtype = me.render_type(cfg.type);
+ me.editWindow(xtype, cfg.id);
+ },
+
+ reload: function() {
+ this.getView().getStore().load();
+ },
+ },
+
+ store: {
+ autoLoad: true,
+ id: 'metricservers',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/metrics/server',
+ },
+ },
+
+ columns: [
+ {
+ text: gettext('Name'),
+ flex: 2,
+ dataIndex: 'id',
+ },
+ {
+ text: gettext('Type'),
+ flex: 1,
+ dataIndex: 'type',
+ renderer: 'render_type',
+ },
+ {
+ text: gettext('Enabled'),
+ dataIndex: 'disable',
+ width: 100,
+ renderer: Proxmox.Utils.format_neg_boolean,
+ },
+ {
+ text: gettext('Server'),
+ width: 200,
+ dataIndex: 'server',
+ },
+ {
+ text: gettext('Port'),
+ width: 100,
+ dataIndex: 'port',
+ },
+ ],
+
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: [
+ {
+ text: 'Graphite',
+ iconCls: 'fa fa-fw fa-bar-chart',
+ handler: 'addServer',
+ },
+ {
+ text: 'InfluxDB',
+ iconCls: 'fa fa-fw fa-bar-chart',
+ handler: 'addServer',
+ },
+ ],
+ },
+ {
+ text: gettext('Edit'),
+ xtype: 'proxmoxButton',
+ handler: 'editServer',
+ disabled: true,
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ baseurl: `/api2/extjs/cluster/metrics/server`,
+ callback: 'reload',
+ },
+ ],
+
+ listeners: {
+ itemdblclick: 'editServer',
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.getStore());
+ },
+});
+
+Ext.define('PVE.dc.MetricServerBaseEdit', {
+ extend: 'Proxmox.window.Edit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ cbindData: function() {
+ let me = this;
+ me.isCreate = !me.serverid;
+ me.serverid = me.serverid || "";
+ me.url = `/api2/extjs/cluster/metrics/server/${me.serverid}`;
+ me.method = me.isCreate ? 'POST' : 'PUT';
+ if (!me.isCreate) {
+ me.subject = `${me.subject}: ${me.serverid}`;
+ }
+ return {};
+ },
+
+ submitUrl: function(url, values) {
+ return this.isCreate ? `${url}/${values.id}` : url;
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.callParent();
+
+ if (me.serverid) {
+ me.load({
+ success: function(response, options) {
+ let values = response.result.data;
+ values.enable = !values.disable;
+ me.down('inputpanel').setValues(values);
+ },
+ });
+ }
+ },
+});
+
+Ext.define('PVE.dc.InfluxDBEdit', {
+ extend: 'PVE.dc.MetricServerBaseEdit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'metric_server_influxdb',
+
+ subject: 'InfluxDB',
+
+ cbindData: function() {
+ let me = this;
+ me.callParent();
+ me.tokenEmptyText = me.isCreate ? '' : gettext('unchanged');
+ return {};
+ },
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ cbind: {
+ isCreate: '{isCreate}',
+ },
+ onGetValues: function(values) {
+ let me = this;
+ values.disable = values.enable ? 0 : 1;
+ delete values.enable;
+ PVE.Utils.delete_if_default(values, 'verify-certificate', '1', me.isCreate);
+ return values;
+ },
+
+ column1: [
+ {
+ xtype: 'hidden',
+ name: 'type',
+ value: 'influxdb',
+ cbind: {
+ submitValue: '{isCreate}',
+ },
+ },
+ {
+ xtype: 'pmxDisplayEditField',
+ name: 'id',
+ fieldLabel: gettext('Name'),
+ allowBlank: false,
+ cbind: {
+ editable: '{isCreate}',
+ value: '{serverid}',
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'server',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'port',
+ fieldLabel: gettext('Port'),
+ value: 8089,
+ minValue: 1,
+ maximum: 65536,
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'influxdbproto',
+ fieldLabel: gettext('Protocol'),
+ value: '__default__',
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ comboItems: [
+ ['__default__', 'UDP'],
+ ['http', 'HTTP'],
+ ['https', 'HTTPS'],
+ ],
+ listeners: {
+ change: function(field, value) {
+ let me = this;
+ let view = me.up('inputpanel');
+ let isUdp = value !== 'http' && value !== 'https';
+ view.down('field[name=organization]').setDisabled(isUdp);
+ view.down('field[name=bucket]').setDisabled(isUdp);
+ view.down('field[name=token]').setDisabled(isUdp);
+ view.down('field[name=api-path-prefix]').setDisabled(isUdp);
+ view.down('field[name=mtu]').setDisabled(!isUdp);
+ view.down('field[name=timeout]').setDisabled(isUdp);
+ view.down('field[name=max-body-size]').setDisabled(isUdp);
+ view.down('field[name=verify-certificate]').setDisabled(value !== 'https');
+ },
+ },
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'checkbox',
+ name: 'enable',
+ fieldLabel: gettext('Enabled'),
+ inputValue: 1,
+ uncheckedValue: 0,
+ checked: true,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'organization',
+ fieldLabel: gettext('Organization'),
+ emptyText: 'proxmox',
+ disabled: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'bucket',
+ fieldLabel: gettext('Bucket'),
+ emptyText: 'proxmox',
+ disabled: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'token',
+ fieldLabel: gettext('Token'),
+ disabled: true,
+ allowBlank: true,
+ deleteEmpty: false,
+ submitEmpty: false,
+ cbind: {
+ disabled: '{!isCreate}',
+ emptyText: '{tokenEmptyText}',
+ },
+ },
+ ],
+
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'api-path-prefix',
+ fieldLabel: gettext('API Path Prefix'),
+ allowBlank: true,
+ disabled: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'timeout',
+ fieldLabel: gettext('Timeout (s)'),
+ disabled: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ minValue: 1,
+ emptyText: 1,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'verify-certificate',
+ fieldLabel: gettext('Verify Certificate'),
+ value: 1,
+ uncheckedValue: 0,
+ disabled: true,
+ },
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'max-body-size',
+ fieldLabel: gettext('Batch Size (b)'),
+ minValue: 1,
+ emptyText: '25000000',
+ submitEmpty: false,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mtu',
+ fieldLabel: 'MTU',
+ minValue: 1,
+ emptyText: '1500',
+ submitEmpty: false,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+ },
+ ],
+});
+
+Ext.define('PVE.dc.GraphiteEdit', {
+ extend: 'PVE.dc.MetricServerBaseEdit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'metric_server_graphite',
+
+ subject: 'Graphite',
+
+ items: [
+ {
+ xtype: 'inputpanel',
+
+ onGetValues: function(values) {
+ values.disable = values.enable ? 0 : 1;
+ delete values.enable;
+ return values;
+ },
+
+ column1: [
+ {
+ xtype: 'hidden',
+ name: 'type',
+ value: 'graphite',
+ cbind: {
+ submitValue: '{isCreate}',
+ },
+ },
+ {
+ xtype: 'pmxDisplayEditField',
+ name: 'id',
+ fieldLabel: gettext('Name'),
+ allowBlank: false,
+ cbind: {
+ editable: '{isCreate}',
+ value: '{serverid}',
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'server',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'checkbox',
+ name: 'enable',
+ fieldLabel: gettext('Enabled'),
+ inputValue: 1,
+ uncheckedValue: 0,
+ checked: true,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'port',
+ fieldLabel: gettext('Port'),
+ value: 2003,
+ minimum: 1,
+ maximum: 65536,
+ allowBlank: false,
+ },
+ {
+ fieldLabel: gettext('Path'),
+ xtype: 'proxmoxtextfield',
+ emptyText: 'proxmox',
+ name: 'path',
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ ],
+
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'proto',
+ fieldLabel: gettext('Protocol'),
+ value: '__default__',
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ comboItems: [
+ ['__default__', 'UDP'],
+ ['tcp', 'TCP'],
+ ],
+ listeners: {
+ change: function(field, value) {
+ let me = this;
+ me.up('inputpanel').down('field[name=timeout]').setDisabled(value !== 'tcp');
+ me.up('inputpanel').down('field[name=mtu]').setDisabled(value === 'tcp');
+ },
+ },
+ },
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mtu',
+ fieldLabel: 'MTU',
+ minimum: 1,
+ emptyText: '1500',
+ submitEmpty: false,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'timeout',
+ fieldLabel: gettext('TCP Timeout'),
+ disabled: true,
+ cbind: {
+ deleteEmpty: '{!isCreate}',
+ },
+ minValue: 1,
+ emptyText: 1,
+ },
+ ],
+ },
+ ],
+});
+Ext.define('PVE.dc.UserTagAccessEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveUserTagAccessEdit',
+
+ subject: gettext('User Tag Access'),
+ onlineHelp: 'datacenter_configuration_file',
+
+ url: '/api2/extjs/cluster/options',
+
+ hintText: gettext('NOTE: The following tags are also defined as registered tags.'),
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ tagChange: function(field, value) {
+ let me = this;
+ let view = me.getView();
+ let also_registered = [];
+ value = Ext.isArray(value) ? value : value.split(';');
+ value.forEach(tag => {
+ if (view.registered_tags.indexOf(tag) !== -1) {
+ also_registered.push(tag);
+ }
+ });
+ let hint_field = me.lookup('hintField');
+ hint_field.setVisible(also_registered.length > 0);
+ if (also_registered.length > 0) {
+ hint_field.setValue(`${view.hintText} ${also_registered.join(', ')}`);
+ }
+ },
+ },
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ setValues: function(values) {
+ this.up('pveUserTagAccessEdit').registered_tags = values?.['registered-tags'] ?? [];
+ let data = values?.['user-tag-access'] ?? {};
+ return Proxmox.panel.InputPanel.prototype.setValues.call(this, data);
+ },
+ onGetValues: function(values) {
+ if (values === undefined || Object.keys(values).length === 0) {
+ return { 'delete': 'user-tag-access' };
+ }
+ return {
+ 'user-tag-access': PVE.Parser.printPropertyString(values),
+ };
+ },
+ items: [
+ {
+ name: 'user-allow',
+ fieldLabel: gettext('Mode'),
+ xtype: 'proxmoxKVComboBox',
+ deleteEmpty: false,
+ value: '__default__',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + ' (free)'],
+ ['free', 'free'],
+ ['existing', 'existing'],
+ ['list', 'list'],
+ ['none', 'none'],
+ ],
+ defaultValue: '__default__',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Predefined Tags'),
+ },
+ {
+ name: 'user-allow-list',
+ xtype: 'pveListField',
+ emptyText: gettext('No Tags defined'),
+ fieldTitle: gettext('Tag'),
+ maskRe: PVE.Utils.tagCharRegex,
+ gridConfig: {
+ height: 200,
+ scrollable: true,
+ },
+ listeners: {
+ change: 'tagChange',
+ },
+ },
+ {
+ hidden: true,
+ xtype: 'displayfield',
+ reference: 'hintField',
+ userCls: 'pmx-hint',
+ },
+ ],
+ },
+ ],
+});
+Ext.define('PVE.dc.RegisteredTagsEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveRegisteredTagEdit',
+
+ subject: gettext('Registered Tags'),
+ onlineHelp: 'datacenter_configuration_file',
+
+ url: '/api2/extjs/cluster/options',
+
+ hintText: gettext('NOTE: The following tags are also defined in the user allow list.'),
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ tagChange: function(field, value) {
+ let me = this;
+ let view = me.getView();
+ let also_allowed = [];
+ value = Ext.isArray(value) ? value : value.split(';');
+ value.forEach(tag => {
+ if (view.allowed_tags.indexOf(tag) !== -1) {
+ also_allowed.push(tag);
+ }
+ });
+ let hint_field = me.lookup('hintField');
+ hint_field.setVisible(also_allowed.length > 0);
+ if (also_allowed.length > 0) {
+ hint_field.setValue(`${view.hintText} ${also_allowed.join(', ')}`);
+ }
+ },
+ },
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ setValues: function(values) {
+ let allowed_tags = values?.['user-tag-access']?.['user-allow-list'] ?? [];
+ this.up('pveRegisteredTagEdit').allowed_tags = allowed_tags;
+ let tags = values?.['registered-tags'];
+ return Proxmox.panel.InputPanel.prototype.setValues.call(this, { tags });
+ },
+ onGetValues: function(values) {
+ if (!values.tags) {
+ return {
+ 'delete': 'registered-tags',
+ };
+ } else {
+ return {
+ 'registered-tags': values.tags,
+ };
+ }
+ },
+ items: [
+ {
+ name: 'tags',
+ xtype: 'pveListField',
+ maskRe: PVE.Utils.tagCharRegex,
+ gridConfig: {
+ height: 200,
+ scrollable: true,
+ emptyText: gettext('No Tags defined'),
+ },
+ listeners: {
+ change: 'tagChange',
+ },
+ },
+ {
+ hidden: true,
+ xtype: 'displayfield',
+ reference: 'hintField',
+ userCls: 'pmx-hint',
+ },
+ ],
+ },
+ ],
+});
+Ext.define('PVE.lxc.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+
+ showSeparator: false,
+ initComponent: function() {
+ let me = this;
+
+ let info = me.pveSelNode.data;
+ if (!info.node) {
+ throw "no node name specified";
+ }
+ if (!info.vmid) {
+ throw "no CT ID specified";
+ }
+
+ let vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: `/nodes/${info.node}/${info.type}/${info.vmid}/status/${cmd}`,
+ method: 'POST',
+ failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ };
+ let confirmedVMCommand = (cmd, params) => {
+ let msg = Proxmox.Utils.format_task_description(`vz${cmd}`, info.vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, btn => {
+ if (btn === 'yes') {
+ vm_command(cmd, params);
+ }
+ });
+ };
+
+ let caps = Ext.state.Manager.get('GuiCap');
+ let standalone = PVE.data.ResourceStore.getNodes().length < 2;
+
+ let running = false, stopped = true, suspended = false;
+ switch (info.status) {
+ case 'running':
+ running = true;
+ stopped = false;
+ break;
+ case 'paused':
+ stopped = false;
+ suspended = true;
+ break;
+ default: break;
+ }
+
+ me.title = 'CT ' + info.vmid;
+
+ me.items = [
+ {
+ text: gettext('Start'),
+ iconCls: 'fa fa-fw fa-play',
+ disabled: running,
+ handler: () => vm_command('start'),
+ },
+ {
+ text: gettext('Shutdown'),
+ iconCls: 'fa fa-fw fa-power-off',
+ disabled: stopped || suspended,
+ handler: () => confirmedVMCommand('shutdown'),
+ },
+ {
+ text: gettext('Stop'),
+ iconCls: 'fa fa-fw fa-stop',
+ disabled: stopped,
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
+ handler: () => confirmedVMCommand('stop'),
+ },
+ {
+ text: gettext('Reboot'),
+ iconCls: 'fa fa-fw fa-refresh',
+ disabled: stopped,
+ tooltip: Ext.String.format(gettext('Reboot {0}'), 'CT'),
+ handler: () => confirmedVMCommand('reboot'),
+ },
+ {
+ xtype: 'menuseparator',
+ hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone'],
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: !caps.vms['VM.Clone'],
+ handler: () => PVE.window.Clone.wrap(info.node, info.vmid, me.isTemplate, 'lxc'),
+ },
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ hidden: standalone || !caps.vms['VM.Migrate'],
+ handler: function() {
+ Ext.create('PVE.window.Migrate', {
+ vmtype: 'lxc',
+ nodename: info.node,
+ vmid: info.vmid,
+ autoShow: true,
+ });
+ },
+ },
+ {
+ text: gettext('Convert to template'),
+ iconCls: 'fa fa-fw fa-file-o',
+ handler: function() {
+ let msg = Proxmox.Utils.format_task_description('vztemplate', info.vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
+ if (btn === 'yes') {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${info.node}/lxc/${info.vmid}/template`,
+ method: 'POST',
+ failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus),
+ });
+ }
+ });
+ },
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Console'),
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: () =>
+ PVE.Utils.openDefaultConsoleWindow(true, 'lxc', info.vmid, info.node, info.vmname),
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.lxc.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.pveLXCConfig',
+
+ onlineHelp: 'chapter_pct',
+
+ userCls: 'proxmox-tags-full',
+
+ initComponent: function() {
+ var me = this;
+ var vm = me.pveSelNode.data;
+
+ var nodename = vm.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = vm.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var template = !!vm.template;
+
+ var running = !!vm.uptime;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var base_url = '/nodes/' + nodename + '/lxc/' + vmid;
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json' + base_url + '/status/current',
+ interval: 1000,
+ });
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: base_url + "/status/" + cmd,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ });
+ };
+
+ var startBtn = Ext.create('Ext.Button', {
+ text: gettext('Start'),
+ disabled: !caps.vms['VM.PowerMgmt'] || running,
+ hidden: template,
+ handler: function() {
+ vm_command('start');
+ },
+ iconCls: 'fa fa-play',
+ });
+
+ var shutdownBtn = Ext.create('PVE.button.Split', {
+ text: gettext('Shutdown'),
+ disabled: !caps.vms['VM.PowerMgmt'] || !running,
+ hidden: template,
+ confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid),
+ handler: function() {
+ vm_command('shutdown');
+ },
+ menu: {
+ items: [{
+ text: gettext('Reboot'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ confirmMsg: Proxmox.Utils.format_task_description('vzreboot', vmid),
+ tooltip: Ext.String.format(gettext('Reboot {0}'), 'CT'),
+ handler: function() {
+ vm_command("reboot");
+ },
+ iconCls: 'fa fa-refresh',
+ },
+ {
+ text: gettext('Stop'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ 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',
+ }],
+ },
+ 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'],
+ 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'],
+ 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 = vm.hastate;
+ Ext.create('PVE.ha.VMResourceEdit', {
+ vmid: vmid,
+ guestType: 'ct',
+ isCreate: !ha || ha === 'unmanaged',
+ }).show();
+ },
+ },
+ {
+ text: gettext('Remove'),
+ disabled: !caps.vms['VM.Allocate'],
+ itemId: 'removeBtn',
+ handler: function() {
+ Ext.create('PVE.window.SafeDestroyGuest', {
+ url: base_url,
+ item: { type: 'CT', id: vmid },
+ taskName: 'vzdestroy',
+ }).show();
+ },
+ iconCls: 'fa fa-trash-o',
+ },
+ ],
+},
+ });
+
+ var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.vms['VM.Console'],
+ consoleType: 'lxc',
+ consoleName: vm.name,
+ hidden: template,
+ nodename: nodename,
+ vmid: vmid,
+ });
+
+ var statusTxt = Ext.create('Ext.toolbar.TextItem', {
+ data: {
+ lock: undefined,
+ },
+ tpl: [
+ '',
+ ' ({lock})',
+ '',
+ ],
+ });
+
+ let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
+ tags: vm.tags,
+ canEdit: !!caps.vms['VM.Config.Options'],
+ listeners: {
+ change: function(tags) {
+ Proxmox.Utils.API2Request({
+ url: base_url + '/config',
+ method: 'PUT',
+ params: {
+ tags,
+ },
+ success: function() {
+ me.statusStore.load();
+ },
+ failure: function(response) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ me.statusStore.load();
+ },
+ });
+ },
+ },
+ });
+
+ let vm_text = `${vm.vmid} (${vm.name})`;
+
+ Ext.apply(me, {
+ title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm_text, nodename),
+ hstateid: 'lxctab',
+ tbarSpacing: false,
+ tbar: [statusTxt, tagsContainer, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
+ defaults: { statusStore: me.statusStore },
+ items: [
+ {
+ title: gettext('Summary'),
+ xtype: 'pveGuestSummary',
+ iconCls: 'fa fa-book',
+ itemId: 'summary',
+ },
+ ],
+ });
+
+ if (caps.vms['VM.Console'] && !template) {
+ me.items.push(
+ {
+ title: gettext('Console'),
+ itemId: 'consolejs',
+ iconCls: 'fa fa-terminal',
+ xtype: 'pveNoVncConsole',
+ vmid: vmid,
+ consoleType: 'lxc',
+ xtermjs: true,
+ nodename: nodename,
+ },
+ );
+ }
+
+ me.items.push(
+ {
+ title: gettext('Resources'),
+ itemId: 'resources',
+ expandedOnInit: true,
+ iconCls: 'fa fa-cube',
+ xtype: 'pveLxcRessourceView',
+ },
+ {
+ title: gettext('Network'),
+ iconCls: 'fa fa-exchange',
+ itemId: 'network',
+ xtype: 'pveLxcNetworkView',
+ },
+ {
+ title: gettext('DNS'),
+ iconCls: 'fa fa-globe',
+ itemId: 'dns',
+ xtype: 'pveLxcDNS',
+ },
+ {
+ title: gettext('Options'),
+ itemId: 'options',
+ iconCls: 'fa fa-gear',
+ xtype: 'pveLxcOptions',
+ },
+ {
+ title: gettext('Task History'),
+ itemId: 'tasks',
+ iconCls: 'fa fa-list-alt',
+ xtype: 'proxmoxNodeTasks',
+ nodename: nodename,
+ preFilter: {
+ vmid,
+ },
+ },
+ );
+
+ if (caps.vms['VM.Backup']) {
+ me.items.push({
+ title: gettext('Backup'),
+ iconCls: 'fa fa-floppy-o',
+ xtype: 'pveBackupView',
+ itemId: 'backup',
+ },
+ {
+ title: gettext('Replication'),
+ iconCls: 'fa fa-retweet',
+ xtype: 'pveReplicaView',
+ itemId: 'replication',
+ });
+ }
+
+ if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback'] ||
+ caps.vms['VM.Audit']) && !template) {
+ me.items.push({
+ title: gettext('Snapshots'),
+ iconCls: 'fa fa-history',
+ xtype: 'pveGuestSnapshotTree',
+ type: 'lxc',
+ itemId: 'snapshot',
+ });
+ }
+
+ if (caps.vms['VM.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();
+
+ var prevStatus = 'unknown';
+ me.mon(me.statusStore, 'load', function(s, records, success) {
+ var status;
+ var lock;
+ var rec;
+
+ if (!success) {
+ status = 'unknown';
+ } else {
+ rec = s.data.get('status');
+ status = rec ? rec.data.value : 'unknown';
+ rec = s.data.get('template');
+ template = rec ? rec.data.value : false;
+ rec = s.data.get('lock');
+ lock = rec ? rec.data.value : undefined;
+ }
+
+ statusTxt.update({ lock: lock });
+
+ rec = s.data.get('tags');
+ tagsContainer.loadTags(rec?.data?.value);
+
+ startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template);
+ shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
+ me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
+ consoleBtn.setDisabled(template);
+
+ if (prevStatus === 'stopped' && status === 'running') {
+ let con = me.down('#consolejs');
+ if (con) {
+ con.reload();
+ }
+ }
+
+ prevStatus = status;
+ });
+
+ me.on('afterrender', function() {
+ me.statusStore.startUpdate();
+ });
+
+ me.on('destroy', function() {
+ me.statusStore.stopUpdate();
+ });
+ },
+});
+Ext.define('PVE.lxc.CreateWizard', {
+ extend: 'PVE.window.Wizard',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ viewModel: {
+ data: {
+ nodename: '',
+ storage: '',
+ unprivileged: true,
+ },
+ formulas: {
+ cgroupMode: function(get) {
+ const nodeInfo = PVE.data.ResourceStore.getNodes().find(
+ node => node.node === get('nodename'),
+ );
+ return nodeInfo ? nodeInfo['cgroup-mode'] : 2;
+ },
+ },
+ },
+
+ cbindData: {
+ nodename: undefined,
+ },
+
+ subject: gettext('LXC Container'),
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ title: gettext('General'),
+ onlineHelp: 'pct_general',
+ column1: [
+ {
+ xtype: 'pveNodeSelector',
+ name: 'nodename',
+ cbind: {
+ selectCurNode: '{!nodename}',
+ preferredValue: '{nodename}',
+ },
+ bind: {
+ value: '{nodename}',
+ },
+ fieldLabel: gettext('Node'),
+ allowBlank: false,
+ onlineValidator: true,
+ },
+ {
+ xtype: 'pveGuestIDSelector',
+ name: 'vmid', // backend only knows vmid
+ guestType: 'lxc',
+ value: '',
+ loadNextFreeID: true,
+ validateExists: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'hostname',
+ vtype: 'DnsName',
+ value: '',
+ fieldLabel: gettext('Hostname'),
+ skipEmptyText: true,
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'unprivileged',
+ value: true,
+ bind: {
+ value: '{unprivileged}',
+ },
+ fieldLabel: gettext('Unprivileged container'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'features',
+ inputValue: 'nesting=1',
+ value: true,
+ bind: {
+ disabled: '{!unprivileged}',
+ },
+ fieldLabel: gettext('Nesting'),
+ },
+ ],
+ 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) {
+ let pwfield = this.up().down('field[name=password]');
+ if (value.length) {
+ let 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) {
+ return; // No FileReader support in this browser
+ }
+ let cancelEvent = ev => {
+ ev = ev.event;
+ if (ev.preventDefault) {
+ ev.preventDefault();
+ }
+ };
+ this.inputEl.on('dragover', cancelEvent);
+ this.inputEl.on('dragenter', cancelEvent);
+ this.inputEl.on('drop', ev => {
+ cancelEvent(ev);
+ let files = ev.event.dataTransfer.files;
+ PVE.Utils.loadSSHKeyFromFile(files[0], v => this.setValue(v));
+ });
+ },
+ },
+ {
+ xtype: 'filebutton',
+ name: 'file',
+ hidden: !window.FileReader,
+ text: gettext('Load SSH Key File'),
+ listeners: {
+ change: function(btn, e, value) {
+ e = e.event;
+ let field = this.up().down('proxmoxtextfield[name=ssh-public-keys]');
+ PVE.Utils.loadSSHKeyFromFile(e.target.files[0], 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: 'pveMultiMPPanel',
+ title: gettext('Disks'),
+ insideWizard: true,
+ isCreate: true,
+ unused: false,
+ confid: 'rootfs',
+ },
+ {
+ xtype: 'pveLxcCPUInputPanel',
+ title: gettext('CPU'),
+ insideWizard: true,
+ },
+ {
+ xtype: 'pveLxcMemoryInputPanel',
+ title: gettext('Memory'),
+ insideWizard: true,
+ },
+ {
+ xtype: 'pveLxcNetworkInputPanel',
+ title: gettext('Network'),
+ insideWizard: true,
+ bind: {
+ nodename: '{nodename}',
+ },
+ isCreate: true,
+ },
+ {
+ xtype: 'pveLxcDNSInputPanel',
+ title: gettext('DNS'),
+ insideWizard: true,
+ },
+ {
+ title: gettext('Confirm'),
+ layout: 'fit',
+ items: [
+ {
+ xtype: 'grid',
+ store: {
+ model: 'KeyValue',
+ sorters: [{
+ property: 'key',
+ direction: 'ASC',
+ }],
+ },
+ columns: [
+ { header: 'Key', width: 150, dataIndex: 'key' },
+ { header: 'Value', flex: 1, dataIndex: 'value' },
+ ],
+ },
+ ],
+ dockedItems: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'start',
+ dock: 'bottom',
+ margin: '5 0 0 0',
+ boxLabel: gettext('Start after created'),
+ },
+ ],
+ listeners: {
+ show: function(panel) {
+ let wizard = this.up('window');
+ let kv = wizard.getValues();
+ let data = [];
+ Ext.Object.each(kv, function(key, value) {
+ if (key === 'delete' || key === 'tmplstorage') { // ignore
+ return;
+ }
+ if (key === 'password') { // don't show pw
+ return;
+ }
+ data.push({ key: key, value: value });
+ });
+
+ let summaryStore = panel.down('grid').getStore();
+ summaryStore.suspendEvents();
+ summaryStore.removeAll();
+ summaryStore.add(data);
+ summaryStore.sort();
+ summaryStore.resumeEvents();
+ summaryStore.fireEvent('refresh');
+ },
+ },
+ onSubmit: function() {
+ let wizard = this.up('window');
+ let kv = wizard.getValues();
+ delete kv.delete;
+
+ let nodename = kv.nodename;
+ delete kv.nodename;
+ delete kv.tmplstorage;
+
+ if (!kv.pool.length) {
+ delete kv.pool;
+ }
+ if (!kv.password.length && kv['ssh-public-keys']) {
+ delete kv.password;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${nodename}/lxc`,
+ waitMsgTarget: wizard,
+ method: 'POST',
+ params: kv,
+ success: function(response, opts) {
+ Ext.create('Proxmox.window.TaskViewer', {
+ autoShow: true,
+ upid: response.result.data,
+ });
+ wizard.close();
+ },
+ failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ },
+ },
+ ],
+});
+Ext.define('PVE.lxc.DNSInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveLxcDNSInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var deletes = [];
+ if (!values.searchdomain && !me.insideWizard) {
+ deletes.push('searchdomain');
+ }
+
+ if (values.nameserver) {
+ let list = values.nameserver.split(/[ ,;]+/);
+ values.nameserver = list.join(' ');
+ } else if (!me.insideWizard) {
+ deletes.push('nameserver');
+ }
+
+ if (deletes.length) {
+ values.delete = deletes.join(',');
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var items = [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'searchdomain',
+ skipEmptyText: true,
+ fieldLabel: gettext('DNS domain'),
+ emptyText: gettext('use host settings'),
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('DNS servers'),
+ vtype: 'IP64AddressWithSuffixList',
+ allowBlank: true,
+ emptyText: gettext('use host settings'),
+ name: 'nameserver',
+ itemId: 'nameserver',
+ },
+ ];
+
+ if (me.insideWizard) {
+ me.column1 = items;
+ } else {
+ me.items = items;
+ }
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.lxc.DNSEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.lxc.DNSInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('Resources'),
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+
+ if (values.nameserver) {
+ values.nameserver.replace(/[,;]/, ' ');
+ values.nameserver.replace(/^\s+/, '');
+ }
+
+ ipanel.setValues(values);
+ },
+ });
+ }
+ },
+});
+
+Ext.define('PVE.lxc.DNS', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.pveLxcDNS'],
+
+ onlineHelp: 'pct_container_network',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ hostname: {
+ required: true,
+ defaultValue: me.pveSelNode.data.name,
+ header: gettext('Hostname'),
+ editor: caps.vms['VM.Config.Network'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Hostname'),
+ items: {
+ xtype: 'inputpanel',
+ items: {
+ fieldLabel: gettext('Hostname'),
+ xtype: 'textfield',
+ name: 'hostname',
+ vtype: 'DnsName',
+ allowBlank: true,
+ emptyText: 'CT' + vmid.toString(),
+ },
+ onGetValues: function(values) {
+ var params = values;
+ if (values.hostname === undefined ||
+ values.hostname === null ||
+ values.hostname === '') {
+ params = { hostname: 'CT'+vmid.toString() };
+ }
+ return params;
+ },
+ },
+ } : undefined,
+ },
+ searchdomain: {
+ header: gettext('DNS domain'),
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ renderer: function(value) {
+ return value || gettext('use host settings');
+ },
+ },
+ nameserver: {
+ header: gettext('DNS server'),
+ defaultValue: '',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
+ renderer: function(value) {
+ return value || gettext('use host settings');
+ },
+ },
+ };
+
+ var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
+
+ var reload = function() {
+ me.rstore.load();
+ };
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var run_editor = function() {
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var rowdef = rows[rec.data.key];
+ if (!rowdef.editor) {
+ return;
+ }
+
+ var win;
+ if (Ext.isString(rowdef.editor)) {
+ win = Ext.create(rowdef.editor, {
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config',
+ });
+ } else {
+ var config = Ext.apply({
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config',
+ }, rowdef.editor);
+ win = Ext.createWidget(rowdef.editor.xtype, config);
+ win.load();
+ }
+ //win.load();
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ var rowdef = rows[rec.data.key];
+ return !!rowdef.editor;
+ },
+ handler: run_editor,
+ });
+
+ var revert_btn = new PVE.button.PendingRevert();
+
+ var set_button_status = function() {
+ let button_sm = me.getSelectionModel();
+ let rec = button_sm.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ return;
+ }
+ let key = rec.data.key;
+
+ let rowdef = rows[key];
+ edit_btn.setDisabled(!rowdef.editor);
+
+ let pending = rec.data.delete || me.hasPendingChanges(key);
+ revert_btn.setDisabled(!pending);
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/pending",
+ selModel: sm,
+ cwidth1: 150,
+ interval: 5000,
+ run_editor: run_editor,
+ tbar: [edit_btn, revert_btn],
+ rows: rows,
+ editorConfig: {
+ url: "/api2/extjs/" + baseurl,
+ },
+ listeners: {
+ itemdblclick: run_editor,
+ selectionchange: set_button_status,
+ activate: reload,
+ },
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ me.mon(me.getStore(), 'datachanged', function() {
+ set_button_status();
+ });
+ },
+});
+Ext.define('PVE.lxc.FeaturesInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveLxcFeaturesInputPanel',
+
+ // used to save the mounts fstypes until sending
+ mounts: [],
+
+ fstypes: ['nfs', 'cifs'],
+
+ viewModel: {
+ parent: null,
+ data: {
+ unprivileged: false,
+ },
+ formulas: {
+ privilegedOnly: function(get) {
+ return get('unprivileged') ? gettext('privileged only') : '';
+ },
+ unprivilegedOnly: function(get) {
+ return !get('unprivileged') ? gettext('unprivileged only') : '';
+ },
+ },
+ },
+
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('keyctl'),
+ name: 'keyctl',
+ bind: {
+ disabled: '{!unprivileged}',
+ boxLabel: '{unprivilegedOnly}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Nesting'),
+ name: 'nesting',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'nfs',
+ fieldLabel: 'NFS',
+ bind: {
+ disabled: '{unprivileged}',
+ boxLabel: '{privilegedOnly}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'cifs',
+ fieldLabel: 'SMB/CIFS',
+ bind: {
+ disabled: '{unprivileged}',
+ boxLabel: '{privilegedOnly}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'fuse',
+ fieldLabel: 'FUSE',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'mknod',
+ fieldLabel: gettext('Create Device Nodes'),
+ boxLabel: gettext('Experimental'),
+ },
+ ],
+
+ onGetValues: function(values) {
+ var me = this;
+ var mounts = me.mounts;
+ me.fstypes.forEach(function(fs) {
+ if (values[fs]) {
+ mounts.push(fs);
+ }
+ delete values[fs];
+ });
+
+ if (mounts.length) {
+ values.mount = mounts.join(';');
+ }
+
+ var featuresstring = PVE.Parser.printPropertyString(values, undefined);
+ if (featuresstring === '') {
+ return { 'delete': 'features' };
+ }
+ return { features: featuresstring };
+ },
+
+ setValues: function(values) {
+ var me = this;
+
+ me.viewModel.set('unprivileged', values.unprivileged);
+
+ if (values.features) {
+ 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]);
+ }
+ },
+
+ initComponent: function() {
+ let me = this;
+ me.mounts = []; // reset state
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.lxc.FeaturesEdit', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveLxcFeaturesEdit',
+
+ subject: gettext('Features'),
+ autoLoad: true,
+ width: 350,
+
+ items: [{
+ xtype: 'pveLxcFeaturesInputPanel',
+ }],
+});
+Ext.define('PVE.lxc.MountPointInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveLxcMountPointInputPanel',
+
+ onlineHelp: 'pct_container_storage',
+
+ insideWizard: false,
+
+ unused: false, // add unused disk imaged
+ unprivileged: false,
+
+ vmconfig: {}, // used to select unused disks
+
+ setUnprivileged: function(unprivileged) {
+ var me = this;
+ var vm = me.getViewModel();
+ me.unprivileged = unprivileged;
+ vm.set('unpriv', unprivileged);
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var confid = me.confid || "mp"+values.mpid;
+ me.mp.file = me.down('field[name=file]').getValue();
+
+ if (me.unused) {
+ confid = "mp"+values.mpid;
+ } else if (me.isCreate) {
+ me.mp.file = values.hdstorage + ':' + values.disksize;
+ }
+
+ // delete unnecessary fields
+ delete values.mpid;
+ delete values.hdstorage;
+ delete values.disksize;
+ delete values.diskformat;
+
+ let setMPOpt = (k, src, v) => PVE.Utils.propertyStringSet(me.mp, src, k, v);
+
+ setMPOpt('mp', values.mp);
+ let mountOpts = (values.mountoptions || []).join(';');
+ setMPOpt('mountoptions', values.mountoptions, mountOpts);
+ setMPOpt('mp', values.mp);
+ setMPOpt('backup', values.backup);
+ setMPOpt('quota', values.quota);
+ setMPOpt('ro', values.ro);
+ setMPOpt('acl', values.acl);
+ setMPOpt('replicate', values.replicate);
+
+ let res = {};
+ res[confid] = PVE.Parser.printLxcMountPoint(me.mp);
+ return res;
+ },
+
+ setMountPoint: function(mp) {
+ let me = this;
+ let vm = me.getViewModel();
+ vm.set('mptype', mp.type);
+ if (mp.mountoptions) {
+ mp.mountoptions = mp.mountoptions.split(';');
+ }
+ me.mp = mp;
+ me.filterMountOptions();
+ me.setValues(mp);
+ },
+
+ filterMountOptions: function() {
+ let me = this;
+ if (me.confid === 'rootfs') {
+ let field = me.down('field[name=mountoptions]');
+ let exclude = ['nodev', 'noexec'];
+ let filtered = field.comboItems.filter(v => !exclude.includes(v[0]));
+ field.setComboItems(filtered);
+ }
+ },
+
+ updateVMConfig: function(vmconfig) {
+ let me = this;
+ let vm = me.getViewModel();
+ me.vmconfig = vmconfig;
+ vm.set('unpriv', vmconfig.unprivileged);
+ me.down('field[name=mpid]').validate();
+ },
+
+ setVMConfig: function(vmconfig) {
+ let me = this;
+
+ me.updateVMConfig(vmconfig);
+ PVE.Utils.forEachMP((bus, i) => {
+ let name = "mp" + i.toString();
+ if (!Ext.isDefined(vmconfig[name])) {
+ me.down('field[name=mpid]').setValue(i);
+ return false;
+ }
+ return undefined;
+ });
+ },
+
+ setNodename: function(nodename) {
+ let me = this;
+ let vm = me.getViewModel();
+ vm.set('node', nodename);
+ me.down('#diskstorage').setNodename(nodename);
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ control: {
+ 'field[name=mpid]': {
+ change: function(field, value) {
+ let me = this;
+ let view = this.getView();
+ if (view.confid !== 'rootfs') {
+ view.fireEvent('diskidchange', view, `mp${value}`);
+ }
+ field.validate();
+ },
+ },
+ '#hdstorage': {
+ change: function(field, newValue) {
+ let me = this;
+ if (!newValue) {
+ return;
+ }
+
+ let rec = field.store.getById(newValue);
+ if (!rec) {
+ return;
+ }
+ me.getViewModel().set('type', rec.data.type);
+ },
+ },
+ },
+ init: function(view) {
+ let me = this;
+ let vm = this.getViewModel();
+ view.mp = {};
+ vm.set('confid', view.confid);
+ vm.set('unused', view.unused);
+ vm.set('node', view.nodename);
+ vm.set('unpriv', view.unprivileged);
+ vm.set('hideStorSelector', view.unused || !view.isCreate);
+
+ if (view.isCreate) { // can be array if created from unused disk
+ vm.set('isIncludedInBackup', true);
+ if (view.insideWizard) {
+ view.filterMountOptions();
+ }
+ }
+ if (view.selectFree) {
+ view.setVMConfig(view.vmconfig);
+ }
+ },
+ },
+
+ viewModel: {
+ data: {
+ unpriv: false,
+ unused: false,
+ showStorageSelector: false,
+ mptype: '',
+ type: '',
+ confid: '',
+ node: '',
+ },
+
+ formulas: {
+ quota: function(get) {
+ return !(get('type') === 'zfs' ||
+ get('type') === 'zfspool' ||
+ get('unpriv') ||
+ get('isBind'));
+ },
+ hasMP: function(get) {
+ return !!get('confid') && !get('unused');
+ },
+ isRoot: function(get) {
+ return get('confid') === 'rootfs';
+ },
+ isBind: function(get) {
+ return get('mptype') === 'bind';
+ },
+ isBindOrRoot: function(get) {
+ return get('isBind') || get('isRoot');
+ },
+ },
+ },
+
+ column1: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mpid',
+ fieldLabel: gettext('Mount Point ID'),
+ minValue: 0,
+ maxValue: PVE.Utils.mp_counts.mp - 1,
+ hidden: true,
+ allowBlank: false,
+ disabled: true,
+ bind: {
+ hidden: '{hasMP}',
+ disabled: '{hasMP}',
+ },
+ validator: function(value) {
+ let view = this.up('inputpanel');
+ if (!view.rendered) {
+ return undefined;
+ }
+ if (Ext.isDefined(view.vmconfig["mp"+value])) {
+ return "Mount point is already in use.";
+ }
+ return true;
+ },
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ itemId: 'diskstorage',
+ storageContent: 'rootdir',
+ hidden: true,
+ autoSelect: true,
+ selectformat: false,
+ defaultSize: 8,
+ bind: {
+ hidden: '{hideStorSelector}',
+ disabled: '{hideStorSelector}',
+ nodename: '{node}',
+ },
+ },
+ {
+ xtype: 'textfield',
+ disabled: true,
+ submitValue: false,
+ fieldLabel: gettext('Disk image'),
+ name: 'file',
+ bind: {
+ hidden: '{!hideStorSelector}',
+ },
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'textfield',
+ name: 'mp',
+ value: '',
+ emptyText: gettext('/some/path'),
+ allowBlank: false,
+ disabled: true,
+ fieldLabel: gettext('Path'),
+ bind: {
+ hidden: '{isRoot}',
+ disabled: '{isRoot}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'backup',
+ fieldLabel: gettext('Backup'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Include volume in backup job'),
+ },
+ bind: {
+ hidden: '{isRoot}',
+ disabled: '{isBindOrRoot}',
+ value: '{isIncludedInBackup}',
+ },
+ },
+ ],
+
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'quota',
+ defaultValue: 0,
+ bind: {
+ disabled: '{!quota}',
+ },
+ fieldLabel: gettext('Enable quota'),
+ listeners: {
+ disable: function() {
+ this.reset();
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'ro',
+ defaultValue: 0,
+ bind: {
+ hidden: '{isRoot}',
+ disabled: '{isRoot}',
+ },
+ fieldLabel: gettext('Read-only'),
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'mountoptions',
+ fieldLabel: gettext('Mount options'),
+ deleteEmpty: false,
+ comboItems: [
+ ['lazytime', 'lazytime'],
+ ['noatime', 'noatime'],
+ ['nodev', 'nodev'],
+ ['noexec', 'noexec'],
+ ['nosuid', 'nosuid'],
+ ],
+ multiSelect: true,
+ value: [],
+ allowBlank: true,
+ },
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'acl',
+ fieldLabel: 'ACLs',
+ deleteEmpty: false,
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText],
+ ['1', Proxmox.Utils.enabledText],
+ ['0', Proxmox.Utils.disabledText],
+ ],
+ value: '__default__',
+ bind: {
+ disabled: '{isBind}',
+ },
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ inputValue: '0', // reverses the logic
+ name: 'replicate',
+ fieldLabel: gettext('Skip replication'),
+ },
+ ],
+});
+
+Ext.define('PVE.lxc.MountPointEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ unprivileged: false,
+
+ initComponent: function() {
+ let me = this;
+
+ let nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ let unused = me.confid && me.confid.match(/^unused\d+$/);
+
+ me.isCreate = me.confid ? unused : true;
+
+ let ipanel = Ext.create('PVE.lxc.MountPointInputPanel', {
+ confid: me.confid,
+ nodename: nodename,
+ unused: unused,
+ unprivileged: me.unprivileged,
+ isCreate: me.isCreate,
+ });
+
+ let subject;
+ if (unused) {
+ subject = gettext('Unused Disk');
+ } else if (me.isCreate) {
+ subject = gettext('Mount Point');
+ } else {
+ subject = gettext('Mount Point') + ' (' + me.confid + ')';
+ }
+
+ Ext.apply(me, {
+ subject: subject,
+ defaultFocus: me.confid !== 'rootfs' ? 'textfield[name=mp]' : 'tool',
+ items: ipanel,
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ ipanel.setVMConfig(response.result.data);
+
+ if (me.confid) {
+ let value = response.result.data[me.confid];
+ let mp = PVE.Parser.parseLxcMountPoint(value);
+ if (!mp) {
+ Ext.Msg.alert(gettext('Error'), 'Unable to parse mount point options');
+ me.close();
+ return;
+ }
+ ipanel.setMountPoint(mp);
+ me.isValid(); // trigger validation
+ }
+ },
+ });
+ },
+});
+Ext.define('PVE.window.MPResize', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ resize_disk: function(disk, size) {
+ var me = this;
+ var params = { disk: disk, size: '+' + size + 'G' };
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/resize',
+ waitMsgTarget: me,
+ method: 'PUT',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, opts) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid });
+ win.show();
+ me.close();
+ },
+ });
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ var items = [
+ {
+ xtype: 'displayfield',
+ name: 'disk',
+ value: me.disk,
+ fieldLabel: gettext('Disk'),
+ vtype: 'StorageId',
+ allowBlank: false,
+ },
+ ];
+
+ me.hdsizesel = Ext.createWidget('numberfield', {
+ name: 'size',
+ minValue: 0,
+ maxValue: 128*1024,
+ decimalPrecision: 3,
+ value: '0',
+ fieldLabel: gettext('Size Increment') + ' (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();
+ },
+});
+Ext.define('PVE.lxc.NetworkInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveLxcNetworkInputPanel',
+
+ insideWizard: false,
+
+ onlineHelp: 'pct_container_network',
+
+ setNodename: function(nodename) {
+ let me = this;
+
+ if (!nodename || me.nodename === nodename) {
+ return;
+ }
+ me.nodename = nodename;
+
+ let bridgeSelector = me.query("[isFormField][name=bridge]")[0];
+ bridgeSelector.setNodename(nodename);
+ },
+
+ onGetValues: function(values) {
+ let me = this;
+
+ let id;
+ if (me.isCreate) {
+ id = values.id;
+ delete values.id;
+ } else {
+ id = me.ifname;
+ }
+ let newdata = {};
+ if (id) {
+ if (values.ipv6mode !== 'static') {
+ values.ip6 = values.ipv6mode;
+ }
+ if (values.ipv4mode !== 'static') {
+ values.ip = values.ipv4mode;
+ }
+ newdata[id] = PVE.Parser.printLxcNetwork(values);
+ }
+ return newdata;
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let cdata = {};
+ if (me.insideWizard) {
+ me.ifname = 'net0';
+ cdata.name = 'eth0';
+ me.dataCache = {};
+ }
+ cdata.firewall = me.insideWizard || me.isCreate;
+
+ if (!me.dataCache) {
+ throw "no dataCache specified";
+ }
+
+ if (!me.isCreate) {
+ if (!me.ifname) {
+ throw "no interface name specified";
+ }
+ if (!me.dataCache[me.ifname]) {
+ throw "no such interface '" + me.ifname + "'";
+ }
+ cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]);
+ }
+
+ for (let i = 0; i < 32; i++) {
+ let ifname = 'net' + i.toString();
+ if (me.isCreate && !me.dataCache[ifname]) {
+ me.ifname = ifname;
+ break;
+ }
+ }
+
+ me.column1 = [
+ {
+ xtype: 'hidden',
+ name: 'id',
+ value: me.ifname,
+ },
+ {
+ xtype: 'textfield',
+ name: 'name',
+ fieldLabel: gettext('Name'),
+ emptyText: '(e.g., eth0)',
+ allowBlank: false,
+ value: cdata.name,
+ validator: function(value) {
+ for (const [key, netRaw] of Object.entries(me.dataCache)) {
+ if (!key.match(/^net\d+/) || key === me.ifname) {
+ continue;
+ }
+ let net = PVE.Parser.parseLxcNetwork(netRaw);
+ if (net.name === value) {
+ return "interface name already in use";
+ }
+ }
+ return true;
+ },
+ },
+ {
+ xtype: 'textfield',
+ name: 'hwaddr',
+ fieldLabel: gettext('MAC address'),
+ vtype: 'MacAddress',
+ value: cdata.hwaddr,
+ allowBlank: true,
+ emptyText: 'auto',
+ },
+ {
+ xtype: 'PVE.form.BridgeSelector',
+ name: 'bridge',
+ nodename: me.nodename,
+ fieldLabel: gettext('Bridge'),
+ value: cdata.bridge,
+ allowBlank: false,
+ },
+ {
+ xtype: 'pveVlanField',
+ name: 'tag',
+ value: cdata.tag,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Firewall'),
+ name: 'firewall',
+ value: cdata.firewall,
+ },
+ ];
+
+ let dhcp4 = cdata.ip === 'dhcp';
+ if (dhcp4) {
+ cdata.ip = '';
+ cdata.gw = '';
+ }
+
+ let auto6 = cdata.ip6 === 'auto';
+ let dhcp6 = cdata.ip6 === 'dhcp';
+ if (auto6 || dhcp6) {
+ cdata.ip6 = '';
+ cdata.gw6 = '';
+ }
+
+ me.column2 = [
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle',
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: 'IPv4:', // do not localize
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv4mode',
+ inputValue: 'static',
+ checked: !dhcp4,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip]').setEmptyText(
+ value ? Proxmox.Utils.NoneText : "",
+ );
+ me.down('field[name=ip]').setDisabled(!value);
+ me.down('field[name=gw]').setDisabled(!value);
+ },
+ },
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'DHCP', // do not localize
+ name: 'ipv4mode',
+ inputValue: 'dhcp',
+ checked: dhcp4,
+ margin: '0 0 0 10',
+ },
+ ],
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip',
+ vtype: 'IPCIDRAddress',
+ value: cdata.ip,
+ emptyText: dhcp4 ? '' : Proxmox.Utils.NoneText,
+ disabled: dhcp4,
+ fieldLabel: 'IPv4/CIDR', // do not localize
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw',
+ value: cdata.gw,
+ vtype: 'IPAddress',
+ disabled: dhcp4,
+ fieldLabel: gettext('Gateway') + ' (IPv4)',
+ margin: '0 0 3 0', // override bottom margin to account for the menuseparator
+ },
+ {
+ xtype: 'menuseparator',
+ height: '3',
+ margin: '0',
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle',
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: 'IPv6:', // do not localize
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv6mode',
+ inputValue: 'static',
+ checked: !(auto6 || dhcp6),
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip6]').setEmptyText(
+ value ? Proxmox.Utils.NoneText : "",
+ );
+ me.down('field[name=ip6]').setDisabled(!value);
+ me.down('field[name=gw6]').setDisabled(!value);
+ },
+ },
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'DHCP', // do not localize
+ name: 'ipv6mode',
+ inputValue: 'dhcp',
+ checked: dhcp6,
+ margin: '0 0 0 10',
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: 'SLAAC', // do not localize
+ name: 'ipv6mode',
+ inputValue: 'auto',
+ checked: auto6,
+ margin: '0 0 0 10',
+ },
+ ],
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip6',
+ value: cdata.ip6,
+ emptyText: dhcp6 || auto6 ? '' : Proxmox.Utils.NoneText,
+ vtype: 'IP6CIDRAddress',
+ disabled: dhcp6 || auto6,
+ fieldLabel: 'IPv6/CIDR', // do not localize
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw6',
+ vtype: 'IP6Address',
+ value: cdata.gw6,
+ disabled: dhcp6 || auto6,
+ fieldLabel: gettext('Gateway') + ' (IPv6)',
+ },
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Disconnect'),
+ name: 'link_down',
+ value: cdata.link_down,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: 'MTU',
+ emptyText: gettext('Same as bridge'),
+ name: 'mtu',
+ value: cdata.mtu,
+ minValue: 576,
+ maxValue: 65535,
+ },
+ ];
+
+ me.advancedColumn2 = [
+ {
+ xtype: 'numberfield',
+ name: 'rate',
+ fieldLabel: gettext('Rate limit') + ' (MB/s)',
+ minValue: 0,
+ maxValue: 10*1024,
+ value: cdata.rate,
+ emptyText: 'unlimited',
+ allowBlank: true,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.lxc.NetworkEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.dataCache) {
+ throw "no dataCache specified";
+ }
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ subject: gettext('Network Device') + ' (veth)',
+ digest: me.dataCache.digest,
+ items: [
+ {
+ xtype: 'pveLxcNetworkInputPanel',
+ ifname: me.ifname,
+ nodename: me.nodename,
+ dataCache: me.dataCache,
+ isCreate: me.isCreate,
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.lxc.NetworkView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveLxcNetworkView',
+
+ onlineHelp: 'pct_container_network',
+
+ dataCache: {}, // used to store result of last load
+
+ stateful: true,
+ stateId: 'grid-lxc-network',
+
+ load: function() {
+ let 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);
+ let result = Ext.decode(response.responseText);
+ me.dataCache = result.data || {};
+ let records = [];
+ for (const [key, value] of Object.entries(me.dataCache)) {
+ if (key.match(/^net\d+/)) {
+ let net = PVE.Parser.parseLxcNetwork(value);
+ net.id = key;
+ records.push(net);
+ }
+ }
+ me.store.loadData(records);
+ me.down('button[name=addButton]').setDisabled(records.length >= 32);
+ },
+ });
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ let vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ let caps = Ext.state.Manager.get('GuiCap');
+
+ me.url = `/nodes/${nodename}/lxc/${vmid}/config`;
+
+ let store = new Ext.data.Store({
+ model: 'pve-lxc-network',
+ sorters: [
+ {
+ property: 'id',
+ direction: 'ASC',
+ },
+ ],
+ });
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec || !caps.vms['VM.Config.Network']) {
+ return false; // disable default-propagation when triggered by grid dblclick
+ }
+ Ext.create('PVE.lxc.NetworkEdit', {
+ url: me.url,
+ nodename: nodename,
+ dataCache: me.dataCache,
+ ifname: rec.data.id,
+ listeners: {
+ destroy: () => me.load(),
+ },
+ autoShow: true,
+ });
+ return undefined; // make eslint happier
+ };
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ {
+ text: gettext('Add'),
+ name: 'addButton',
+ disabled: !caps.vms['VM.Config.Network'],
+ handler: function() {
+ Ext.create('PVE.lxc.NetworkEdit', {
+ url: me.url,
+ nodename: nodename,
+ isCreate: true,
+ dataCache: me.dataCache,
+ listeners: {
+ destroy: () => me.load(),
+ },
+ autoShow: true,
+ });
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Remove'),
+ disabled: true,
+ selModel: sm,
+ enableFn: function(rec) {
+ return !!caps.vms['VM.Config.Network'];
+ },
+ confirmMsg: ({ data }) =>
+ Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${data.id}'`),
+ handler: function(btn, e, rec) {
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ waitMsgTarget: me,
+ method: 'PUT',
+ params: {
+ 'delete': rec.data.id,
+ digest: me.dataCache.digest,
+ },
+ callback: () => me.load(),
+ failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ selModel: sm,
+ disabled: true,
+ enableFn: rec => !!caps.vms['VM.Config.Network'],
+ handler: run_editor,
+ },
+ ],
+ columns: [
+ {
+ header: 'ID',
+ width: 50,
+ dataIndex: 'id',
+ },
+ {
+ header: gettext('Name'),
+ width: 80,
+ dataIndex: 'name',
+ },
+ {
+ header: gettext('Bridge'),
+ width: 80,
+ dataIndex: 'bridge',
+ },
+ {
+ header: gettext('Firewall'),
+ width: 80,
+ dataIndex: 'firewall',
+ renderer: Proxmox.Utils.format_boolean,
+ },
+ {
+ header: gettext('VLAN Tag'),
+ width: 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;
+ }
+ },
+ },
+ {
+ header: gettext('MTU'),
+ width: 80,
+ dataIndex: 'mtu',
+ },
+ {
+ header: gettext('Disconnected'),
+ width: 100,
+ dataIndex: 'link_down',
+ renderer: Proxmox.Utils.format_boolean,
+ },
+ ],
+ listeners: {
+ activate: me.load,
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ },
+}, function() {
+ Ext.define('pve-lxc-network', {
+ extend: "Ext.data.Model",
+ proxy: { type: 'memory' },
+ fields: [
+ 'id',
+ 'name',
+ 'hwaddr',
+ 'bridge',
+ 'ip',
+ 'gw',
+ 'ip6',
+ 'gw6',
+ 'tag',
+ 'firewall',
+ 'mtu',
+ 'link_down',
+ ],
+ });
+});
+
+Ext.define('PVE.lxc.Options', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.pveLxcOptions'],
+
+ onlineHelp: 'pct_options',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ onboot: {
+ header: gettext('Start at boot'),
+ defaultValue: '',
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Start at boot'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'onboot',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ fieldLabel: gettext('Start at boot'),
+ },
+ } : undefined,
+ },
+ startup: {
+ header: gettext('Start/Shutdown order'),
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_startup,
+ editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify']
+ ? {
+ xtype: 'pveWindowStartupEdit',
+ onlineHelp: 'pct_startup_and_shutdown',
+ } : undefined,
+ },
+ ostype: {
+ header: gettext('OS Type'),
+ defaultValue: Proxmox.Utils.unknownText,
+ },
+ arch: {
+ header: gettext('Architecture'),
+ defaultValue: Proxmox.Utils.unknownText,
+ },
+ console: {
+ header: '/dev/console',
+ defaultValue: 1,
+ renderer: Proxmox.Utils.format_enabled_toggle,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: '/dev/console',
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'console',
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ checked: true,
+ fieldLabel: '/dev/console',
+ },
+ } : undefined,
+ },
+ tty: {
+ header: gettext('TTY count'),
+ defaultValue: 2,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('TTY count'),
+ items: {
+ xtype: 'proxmoxintegerfield',
+ name: 'tty',
+ minValue: 0,
+ maxValue: 6,
+ 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: 'PVE.lxc.FeaturesEdit',
+ },
+ 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(); },
+ });
+
+ var revert_btn = new PVE.button.PendingRevert();
+
+ var set_button_status = function() {
+ let button_sm = me.getSelectionModel();
+ let rec = button_sm.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ return;
+ }
+
+ var key = rec.data.key;
+ var pending = rec.data.delete || me.hasPendingChanges(key);
+ var rowdef = rows[key];
+
+ if (key === 'features') {
+ let unprivileged = me.getStore().getById('unprivileged').data.value;
+ let root = Proxmox.UserName === 'root@pam';
+ let vmalloc = caps.vms['VM.Allocate'];
+ edit_btn.setDisabled(!(root || (vmalloc && unprivileged)));
+ } else {
+ edit_btn.setDisabled(!rowdef.editor);
+ }
+
+ revert_btn.setDisabled(!pending);
+ };
+
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/pending",
+ selModel: sm,
+ interval: 5000,
+ tbar: [edit_btn, revert_btn],
+ rows: rows,
+ editorConfig: {
+ url: '/api2/extjs/' + baseurl,
+ },
+ listeners: {
+ itemdblclick: me.run_editor,
+ selectionchange: set_button_status,
+ },
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ me.mon(me.getStore(), 'datachanged', function() {
+ set_button_status();
+ });
+ },
+});
+
+var labelWidth = 120;
+
+Ext.define('PVE.lxc.MemoryEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ Ext.apply(me, {
+ subject: gettext('Memory'),
+ items: Ext.create('PVE.lxc.MemoryInputPanel'),
+ });
+
+ me.callParent();
+
+ me.load();
+ },
+});
+
+
+Ext.define('PVE.lxc.CPUEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveLxcCPUEdit',
+
+ viewModel: {
+ data: {
+ cgroupMode: 2,
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+ me.getViewModel().set('cgroupMode', me.cgroupMode);
+
+ Ext.apply(me, {
+ subject: gettext('CPU'),
+ items: Ext.create('PVE.lxc.CPUInputPanel'),
+ });
+
+ me.callParent();
+
+ me.load();
+ },
+});
+
+// The view model of the parent shoul contain a 'cgroupMode' variable (or params for v2 are used).
+Ext.define('PVE.lxc.CPUInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveLxcCPUInputPanel',
+
+ onlineHelp: 'pct_cpu',
+
+ insideWizard: false,
+
+ viewModel: {
+ formulas: {
+ cpuunitsDefault: (get) => get('cgroupMode') === 1 ? 1024 : 100,
+ cpuunitsMax: (get) => get('cgroupMode') === 1 ? 500000 : 10000,
+ },
+ },
+
+ onGetValues: function(values) {
+ let me = this;
+ let cpuunitsDefault = me.getViewModel().get('cpuunitsDefault');
+
+ PVE.Utils.delete_if_default(values, 'cpulimit', '0', me.insideWizard);
+ PVE.Utils.delete_if_default(values, 'cpuunits', `${cpuunitsDefault}`, me.insideWizard);
+
+ return values;
+ },
+
+ advancedColumn1: [
+ {
+ xtype: 'numberfield',
+ name: 'cpulimit',
+ minValue: 0,
+ value: '',
+ step: 1,
+ fieldLabel: gettext('CPU limit'),
+ allowBlank: true,
+ emptyText: gettext('unlimited'),
+ },
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'cpuunits',
+ fieldLabel: gettext('CPU units'),
+ value: '',
+ minValue: 8,
+ maxValue: '10000',
+ emptyText: '100',
+ bind: {
+ emptyText: '{cpuunitsDefault}',
+ maxValue: '{cpuunitsMax}',
+ },
+ labelWidth: labelWidth,
+ deleteEmpty: true,
+ allowBlank: true,
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'cores',
+ minValue: 1,
+ maxValue: 8192,
+ value: me.insideWizard ? 1 : '',
+ fieldLabel: gettext('Cores'),
+ allowBlank: true,
+ deleteEmpty: true,
+ emptyText: gettext('unlimited'),
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.lxc.MemoryInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveLxcMemoryInputPanel',
+
+ onlineHelp: 'pct_memory',
+
+ insideWizard: false,
+
+ initComponent: function() {
+ var me = this;
+
+ var items = [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'memory',
+ minValue: 16,
+ value: '512',
+ step: 32,
+ fieldLabel: gettext('Memory') + ' (MiB)',
+ labelWidth: labelWidth,
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'swap',
+ minValue: 0,
+ value: '512',
+ step: 32,
+ fieldLabel: gettext('Swap') + ' (MiB)',
+ labelWidth: labelWidth,
+ allowBlank: false,
+ },
+ ];
+
+ if (me.insideWizard) {
+ me.column1 = items;
+ } else {
+ me.items = items;
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.lxc.RessourceView', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.pveLxcRessourceView'],
+
+ onlineHelp: 'pct_configuration',
+
+ renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
+ let me = this;
+ let rowdef = me.rows[key] || {};
+
+ let txt = rowdef.header || key;
+ let icon = '';
+
+ metaData.tdAttr = "valign=middle";
+ if (rowdef.tdCls) {
+ metaData.tdCls = rowdef.tdCls;
+ } else if (rowdef.iconCls) {
+ icon = ``;
+ metaData.tdCls += " pve-itype-fa";
+ }
+ // only return icons in grid but not remove dialog
+ if (rowIndex !== undefined) {
+ return icon + txt;
+ } else {
+ return txt;
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+ let 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;
+
+ const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename);
+ let cpuEditor = {
+ xtype: 'pveLxcCPUEdit',
+ cgroupMode: nodeInfo['cgroup-mode'],
+ };
+
+ var rows = {
+ memory: {
+ header: gettext('Memory'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
+ defaultValue: 512,
+ tdCls: 'pmx-itype-icon-memory',
+ group: 1,
+ renderer: function(value) {
+ return Proxmox.Utils.format_size(value*1024*1024);
+ },
+ },
+ swap: {
+ header: gettext('Swap'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
+ defaultValue: 512,
+ iconCls: 'refresh',
+ group: 2,
+ renderer: function(value) {
+ return Proxmox.Utils.format_size(value*1024*1024);
+ },
+ },
+ cores: {
+ header: gettext('Cores'),
+ editor: caps.vms['VM.Config.CPU'] ? cpuEditor : undefined,
+ defaultValue: '',
+ tdCls: 'pmx-itype-icon-processor',
+ group: 3,
+ renderer: function(value) {
+ var cpulimit = me.getObjectValue('cpulimit');
+ var cpuunits = me.getObjectValue('cpuunits');
+ var res;
+ if (value) {
+ res = value;
+ } else {
+ res = gettext('unlimited');
+ }
+
+ if (cpulimit) {
+ res += ' [cpulimit=' + cpulimit + ']';
+ }
+
+ if (cpuunits) {
+ res += ' [cpuunits=' + cpuunits + ']';
+ }
+ return res;
+ },
+ },
+ rootfs: {
+ header: gettext('Root Disk'),
+ defaultValue: Proxmox.Utils.noneText,
+ editor: mpeditor,
+ iconCls: 'hdd-o',
+ group: 4,
+ },
+ 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);
+ },
+ });
+ };
+
+ let run_move = function() {
+ let rec = me.selModel.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('PVE.window.HDMove', {
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid,
+ type: 'lxc',
+ });
+
+ win.show();
+
+ win.on('destroy', me.reload, me);
+ };
+
+ let run_reassign = function() {
+ let rec = me.selModel.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ Ext.create('PVE.window.GuestDiskReassign', {
+ disk: rec.data.key,
+ nodename: nodename,
+ autoShow: true,
+ vmid: vmid,
+ type: 'lxc',
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ };
+
+ var edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ selModel: me.selModel,
+ disabled: true,
+ enableFn: function(rec) {
+ if (!rec) {
+ return false;
+ }
+ var rowdef = rows[rec.data.key];
+ return !!rowdef.editor;
+ },
+ handler: function() { me.run_editor(); },
+ });
+
+ var remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ defaultText: gettext('Remove'),
+ altText: gettext('Detach'),
+ selModel: me.selModel,
+ disabled: true,
+ dangerous: true,
+ confirmMsg: function(rec) {
+ let warn = Ext.String.format(gettext('Are you sure you want to remove entry {0}'));
+ if (this.text === this.altText) {
+ warn = gettext('Are you sure you want to detach entry {0}');
+ }
+ let rendered = me.renderKey(rec.data.key, {}, rec);
+ let msg = Ext.String.format(warn, `'${rendered}'`);
+
+ if (rec.data.key.match(/^unused\d+$/)) {
+ msg += " " + gettext('This will permanently erase all data.');
+ }
+ return msg;
+ },
+ handler: run_remove,
+ listeners: {
+ render: function(btn) {
+ // hack: calculate the max button width on first display to prevent the whole
+ // toolbar to move when we switch between the "Remove" and "Detach" labels
+ let def = btn.getSize().width;
+
+ btn.setText(btn.altText);
+ let alt = btn.getSize().width;
+
+ btn.setText(btn.defaultText);
+
+ let optimal = alt > def ? alt : def;
+ btn.setSize({ width: optimal });
+ },
+ },
+ });
+
+ let move_menuitem = new Ext.menu.Item({
+ text: gettext('Move Storage'),
+ tooltip: gettext('Move volume to another storage'),
+ iconCls: 'fa fa-database',
+ selModel: me.selModel,
+ handler: run_move,
+ });
+
+ let reassign_menuitem = new Ext.menu.Item({
+ text: gettext('Reassign Owner'),
+ tooltip: gettext('Reassign volume to another CT'),
+ iconCls: 'fa fa-cube',
+ handler: run_reassign,
+ reference: 'reassing_item',
+ });
+
+ let resize_menuitem = new Ext.menu.Item({
+ text: gettext('Resize'),
+ iconCls: 'fa fa-plus',
+ selModel: me.selModel,
+ handler: run_resize,
+ });
+
+ let volumeaction_btn = new Proxmox.button.Button({
+ text: gettext('Volume Action'),
+ disabled: true,
+ menu: {
+ items: [
+ move_menuitem,
+ reassign_menuitem,
+ resize_menuitem,
+ ],
+ },
+ });
+
+ let revert_btn = new PVE.button.PendingRevert();
+
+ let set_button_status = function() {
+ let rec = me.selModel.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ remove_btn.disable();
+ volumeaction_btn.disable();
+ revert_btn.disable();
+ return;
+ }
+ let { key, value, 'delete': isDelete } = rec.data;
+ let rowdef = rows[key];
+
+ let pending = isDelete || me.hasPendingChanges(key);
+ let isRootFS = key === 'rootfs';
+ let isDisk = isRootFS || key.match(/^(mp|unused)\d+/);
+ let isUnusedDisk = key.match(/^unused\d+/);
+ let isUsedDisk = isDisk && !isUnusedDisk;
+
+ let noedit = isDelete || !rowdef.editor;
+ if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) {
+ let mp = PVE.Parser.parseLxcMountPoint(value);
+ if (mp.type !== 'volume') {
+ noedit = true;
+ }
+ }
+ edit_btn.setDisabled(noedit);
+
+ volumeaction_btn.setDisabled(!isDisk || !diskCap);
+ move_menuitem.setDisabled(isUnusedDisk);
+ reassign_menuitem.setDisabled(isRootFS);
+ resize_menuitem.setDisabled(isUnusedDisk);
+
+ remove_btn.setDisabled(!isDisk || isRootFS || !diskCap || pending);
+ revert_btn.setDisabled(!pending);
+
+ remove_btn.setText(isUsedDisk ? remove_btn.altText : remove_btn.defaultText);
+ };
+
+ let sorterFn = function(rec1, rec2) {
+ let v1 = rec1.data.key, v2 = rec2.data.key;
+
+ let g1 = rows[v1].group || 0, g2 = rows[v2].group || 0;
+ if (g1 - g2 !== 0) {
+ return g1 - g2;
+ }
+
+ let order1 = rows[v1].order || 0, order2 = rows[v2].order || 0;
+ if (order1 - order2 !== 0) {
+ return order1 - order2;
+ }
+
+ if (v1 > v2) {
+ return 1;
+ } else if (v1 < v2) {
+ return -1;
+ } else {
+ return 0;
+ }
+ };
+
+ Ext.apply(me, {
+ url: `/api2/json/nodes/${nodename}/lxc/${vmid}/pending`,
+ selModel: me.selModel,
+ interval: 2000,
+ cwidth1: 170,
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: [
+ {
+ text: gettext('Mount Point'),
+ iconCls: 'fa fa-fw fa-hdd-o black',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+ Ext.create('PVE.lxc.MountPointEdit', {
+ autoShow: true,
+ url: `/api2/extjs/${baseurl}`,
+ unprivileged: me.getObjectValue('unprivileged'),
+ pveSelNode: me.pveSelNode,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+ },
+ ],
+ }),
+ },
+ edit_btn,
+ remove_btn,
+ volumeaction_btn,
+ revert_btn,
+ ],
+ rows: rows,
+ sorterFn: sorterFn,
+ editorConfig: {
+ pveSelNode: me.pveSelNode,
+ url: '/api2/extjs/' + baseurl,
+ },
+ listeners: {
+ itemdblclick: me.run_editor,
+ selectionchange: set_button_status,
+ },
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ me.on('deactivate', me.rstore.stopUpdate);
+
+ me.mon(me.getStore(), 'datachanged', function() {
+ set_button_status();
+ });
+
+ Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') });
+ },
+});
+Ext.define('PVE.lxc.MultiMPPanel', {
+ extend: 'PVE.panel.MultiDiskPanel',
+ alias: 'widget.pveMultiMPPanel',
+
+ onlineHelp: 'pct_container_storage',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ // count of mps + rootfs
+ maxCount: PVE.Utils.mp_counts.mp + 1,
+
+ getNextFreeDisk: function(vmconfig) {
+ let nextFreeDisk;
+ if (!vmconfig.rootfs) {
+ return {
+ confid: 'rootfs',
+ };
+ } else {
+ for (let i = 0; i < PVE.Utils.mp_counts.mp; i++) {
+ let confid = `mp${i}`;
+ if (!vmconfig[confid]) {
+ nextFreeDisk = {
+ confid,
+ };
+ break;
+ }
+ }
+ }
+ return nextFreeDisk;
+ },
+
+ addPanel: function(itemId, vmconfig, nextFreeDisk) {
+ let me = this;
+ return me.getView().add({
+ vmconfig,
+ border: false,
+ showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'),
+ xtype: 'pveLxcMountPointInputPanel',
+ confid: nextFreeDisk.confid === 'rootfs' ? 'rootfs' : null,
+ bind: {
+ nodename: '{nodename}',
+ unprivileged: '{unprivileged}',
+ },
+ padding: '0 5 0 10',
+ itemId,
+ selectFree: true,
+ isCreate: true,
+ insideWizard: true,
+ });
+ },
+
+ getBaseVMConfig: function() {
+ let me = this;
+
+ return {
+ unprivileged: me.getViewModel().get('unprivileged'),
+ };
+ },
+
+ diskSorter: {
+ sorterFn: function(rec1, rec2) {
+ if (rec1.data.name === 'rootfs') {
+ return -1;
+ } else if (rec2.data.name === 'rootfs') {
+ return 1;
+ }
+
+ let mp_match = /^mp(\d+)$/;
+ let [, id1] = mp_match.exec(rec1.data.name);
+ let [, id2] = mp_match.exec(rec2.data.name);
+
+ return parseInt(id1, 10) - parseInt(id2, 10);
+ },
+ },
+
+ deleteDisabled: (view, rI, cI, item, rec) => rec.data.name === 'rootfs',
+ },
+});
+Ext.define('PVE.menu.Item', {
+ extend: 'Ext.menu.Item',
+ alias: 'widget.pveMenuItem',
+
+ // set to wrap the handler callback in a confirm dialog showing this text
+ confirmMsg: false,
+
+ // set to focus 'No' instead of 'Yes' button and show a warning symbol
+ dangerous: false,
+
+ initComponent: function() {
+ let me = this;
+ if (me.handler) {
+ me.setHandler(me.handler, me.scope);
+ }
+ me.callParent();
+ },
+
+ setHandler: function(fn, scope) {
+ let me = this;
+ me.scope = scope;
+ me.handler = function(button, e) {
+ if (me.confirmMsg) {
+ Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
+ msg: me.confirmMsg,
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: me.dangerous ? 'no' : 'yes',
+ callback: function(btn) {
+ if (btn === 'yes') {
+ Ext.callback(fn, me.scope, [me, e], 0, me);
+ }
+ },
+ });
+ } else {
+ Ext.callback(fn, me.scope, [me, e], 0, me);
+ }
+ };
+ },
+});
+Ext.define('PVE.menu.TemplateMenu', {
+ extend: 'Ext.menu.Menu',
+
+ initComponent: function() {
+ let me = this;
+
+ let info = me.pveSelNode.data;
+ if (!info.node) {
+ throw "no node name specified";
+ }
+ if (!info.vmid) {
+ throw "no VM ID specified";
+ }
+
+ let guestType = me.pveSelNode.data.type;
+ if (guestType !== 'qemu' && guestType !== 'lxc') {
+ throw `invalid guest type ${guestType}`;
+ }
+
+ let template = me.pveSelNode.data.template;
+
+ me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + info.vmid;
+
+ let caps = Ext.state.Manager.get('GuiCap');
+ let standaloneNode = PVE.data.ResourceStore.getNodes().length < 2;
+
+ me.items = [
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ hidden: standaloneNode || !caps.vms['VM.Migrate'],
+ handler: function() {
+ Ext.create('PVE.window.Migrate', {
+ vmtype: guestType,
+ nodename: info.node,
+ vmid: info.vmid,
+ autoShow: true,
+ });
+ },
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: !caps.vms['VM.Clone'],
+ handler: function() {
+ Ext.create('PVE.window.Clone', {
+ nodename: info.node,
+ guestType: guestType,
+ vmid: info.vmid,
+ isTemplate: template,
+ autoShow: true,
+ });
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.ceph.CephInstallWizardInfo', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveCephInstallWizardInfo',
+
+ html: `Ceph?
+ "Ceph is a unified,
+ distributed storage system, designed for excellent performance, reliability,
+ and scalability."
+
+ Ceph is currently not installed on this node. This wizard
+ will guide you through the installation. Click on the next button below
+ to begin. After the initial installation, the wizard will offer to create
+ an initial configuration. This configuration step is only
+ needed once per cluster and will be skipped if a config is already present.
+
+
+ Before starting the installation, please take a look at our documentation,
+ by clicking the help button below. If you want to gain deeper knowledge about
+ Ceph, visit ceph.com.
+
`,
+});
+
+Ext.define('PVE.ceph.CephVersionSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ xtype: 'pveCephVersionSelector',
+
+ fieldLabel: gettext('Ceph version to install'),
+
+ displayField: 'display',
+ valueField: 'release',
+
+ queryMode: 'local',
+ editable: false,
+ forceSelection: true,
+
+ store: {
+ fields: [
+ 'release',
+ 'version',
+ {
+ name: 'display',
+ calculate: d => `${d.release} (${d.version})`,
+ },
+ ],
+ proxy: {
+ type: 'memory',
+ reader: {
+ type: 'json',
+ },
+ },
+ data: [
+ { release: "octopus", version: "15.2" },
+ { release: "pacific", version: "16.2" },
+ { release: "quincy", version: "17.2" },
+ ],
+ },
+});
+
+Ext.define('PVE.ceph.CephHighestVersionDisplay', {
+ extend: 'Ext.form.field.Display',
+ xtype: 'pveCephHighestVersionDisplay',
+
+ fieldLabel: gettext('Ceph in the cluster'),
+
+ value: 'unknown',
+
+ // called on success with (release, versionTxt, versionParts)
+ gotNewestVersion: Ext.emptyFn,
+
+ initComponent: function() {
+ let me = this;
+
+ me.callParent(arguments);
+
+ Proxmox.Utils.API2Request({
+ method: 'GET',
+ url: '/cluster/ceph/metadata',
+ params: {
+ scope: 'versions',
+ },
+ waitMsgTarget: me,
+ success: (response) => {
+ let res = response.result;
+ if (!res || !res.data || !res.data.node) {
+ me.setValue(
+ gettext('Could not detect a ceph installation in the cluster'),
+ );
+ return;
+ }
+ let nodes = res.data.node;
+ if (me.nodename) {
+ // can happen on ceph purge, we do not yet cleanup old version data
+ delete nodes[me.nodename];
+ }
+
+ let maxversion = [];
+ let maxversiontext = "";
+ for (const [_nodename, data] of Object.entries(nodes)) {
+ let version = data.version.parts;
+ if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
+ maxversion = version;
+ maxversiontext = data.version.str;
+ }
+ }
+ // FIXME: get from version selector store
+ const major2release = {
+ 13: 'luminous',
+ 14: 'nautilus',
+ 15: 'octopus',
+ 16: 'pacific',
+ 17: 'quincy',
+ };
+ let release = major2release[maxversion[0]] || 'unknown';
+ let newestVersionTxt = `${Ext.String.capitalize(release)} (${maxversiontext})`;
+
+ if (release === 'unknown') {
+ me.setValue(
+ gettext('Could not detect a ceph installation in the cluster'),
+ );
+ } else {
+ me.setValue(Ext.String.format(
+ gettext('Newest ceph version in cluster is {0}'),
+ newestVersionTxt,
+ ));
+ }
+ me.gotNewestVersion(release, maxversiontext, maxversion);
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+});
+
+Ext.define('PVE.ceph.CephInstallWizard', {
+ extend: 'PVE.window.Wizard',
+ alias: 'widget.pveCephInstallWizard',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ resizable: false,
+ nodename: undefined,
+
+ viewModel: {
+ data: {
+ nodename: '',
+ cephRelease: 'quincy',
+ 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: [
+ {
+ xtype: 'panel',
+ title: gettext('Info'),
+ viewModel: {}, // needed to inherit parent viewModel data
+ border: false,
+ bodyBorder: false,
+ onlineHelp: 'chapter_pveceph',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ defaults: {
+ border: false,
+ bodyBorder: false,
+ },
+ items: [
+ {
+ xtype: 'pveCephInstallWizardInfo',
+ },
+ {
+ flex: 1,
+ },
+ {
+ xtype: 'pveCephHighestVersionDisplay',
+ labelWidth: 180,
+ cbind: {
+ nodename: '{nodename}',
+ },
+ gotNewestVersion: function(release, maxversiontext, maxversion) {
+ if (release === 'unknown') {
+ return;
+ }
+ let wizard = this.up('pveCephInstallWizard');
+ wizard.getViewModel().set('cephRelease', release);
+ },
+ },
+ {
+ xtype: 'pveCephVersionSelector',
+ labelWidth: 180,
+ submitValue: false,
+ bind: {
+ value: '{cephRelease}',
+ },
+ listeners: {
+ change: function(field, release) {
+ let wizard = this.up('pveCephInstallWizard');
+ wizard.down('#next').setText(
+ Ext.String.format(gettext('Start {0} installation'), release),
+ );
+ },
+ },
+ },
+ ],
+ listeners: {
+ activate: function() {
+ // notify owning container that it should display a help button
+ if (this.onlineHelp) {
+ Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
+ }
+ let wizard = this.up('pveCephInstallWizard');
+ let release = wizard.getViewModel().get('cephRelease');
+ wizard.down('#back').hide(true);
+ wizard.down('#next').setText(
+ Ext.String.format(gettext('Start {0} installation'), release),
+ );
+ },
+ deactivate: function() {
+ if (this.onlineHelp) {
+ Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
+ }
+ this.up('pveCephInstallWizard').down('#next').setText(gettext('Next'));
+ },
+ },
+ },
+ {
+ title: gettext('Installation'),
+ xtype: 'panel',
+ layout: 'fit',
+ cbind: {
+ nodename: '{nodename}',
+ },
+ viewModel: {}, // needed to inherit parent viewModel data
+ listeners: {
+ afterrender: function() {
+ var me = this;
+ if (this.getViewModel().get('isInstalled')) {
+ this.mask("Ceph is already installed, click next to create your configuration.", ['pve-static-mask']);
+ } else {
+ me.down('pveNoVncConsole').fireEvent('activate');
+ }
+ },
+ activate: function() {
+ let me = this;
+ const nodename = me.nodename;
+ me.updateStore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-status-' + nodename,
+ interval: 1000,
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + nodename + '/ceph/status',
+ },
+ listeners: {
+ load: function(rec, response, success, operation) {
+ if (success) {
+ me.updateStore.stopUpdate();
+ me.down('textfield').setValue('success');
+ } else if (operation.error.statusText.match("not initialized", "i")) {
+ me.updateStore.stopUpdate();
+ me.up('pveCephInstallWizard').getViewModel().set('configuration', false);
+ me.down('textfield').setValue('success');
+ } else if (operation.error.statusText.match("rados_connect failed", "i")) {
+ me.updateStore.stopUpdate();
+ me.up('pveCephInstallWizard').getViewModel().set('configuration', true);
+ me.down('textfield').setValue('success');
+ } else if (!operation.error.statusText.match("not installed", "i")) {
+ Proxmox.Utils.setErrorMask(me, operation.error.statusText);
+ }
+ },
+ },
+ });
+ me.updateStore.startUpdate();
+ },
+ destroy: function() {
+ var me = this;
+ if (me.updateStore) {
+ me.updateStore.stopUpdate();
+ }
+ },
+ },
+ items: [
+ {
+ xtype: 'pveNoVncConsole',
+ itemId: 'jsconsole',
+ consoleType: 'cmd',
+ xtermjs: true,
+ cbind: {
+ nodename: '{nodename}',
+ },
+ beforeLoad: function() {
+ let me = this;
+ let wizard = me.up('pveCephInstallWizard');
+ let release = wizard.getViewModel().get('cephRelease');
+ me.cmdOpts = `--version\0${release}`;
+ },
+ cmd: 'ceph_install',
+ },
+ {
+ xtype: 'textfield',
+ name: 'installSuccess',
+ value: '',
+ allowBlank: false,
+ submitValue: false,
+ hidden: true,
+ },
+ ],
+ },
+ {
+ xtype: 'inputpanel',
+ title: gettext('Configuration'),
+ onlineHelp: 'chapter_pveceph',
+ height: 300,
+ cbind: {
+ nodename: '{nodename}',
+ },
+ viewModel: {
+ data: {
+ replicas: undefined,
+ minreplicas: undefined,
+ },
+ },
+ listeners: {
+ activate: function() {
+ this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next'));
+ },
+ afterrender: function() {
+ if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
+ this.mask("Configuration already initialized", ['pve-static-mask']);
+ } else {
+ this.unmask();
+ }
+ },
+ deactivate: function() {
+ this.up('pveCephInstallWizard').down('#submit').setText(gettext('Finish'));
+ },
+ },
+ column1: [
+ {
+ xtype: 'displayfield',
+ value: gettext('Ceph cluster configuration') + ':',
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ name: 'network',
+ value: '',
+ fieldLabel: 'Public Network IP/CIDR',
+ autoSelect: false,
+ bind: {
+ allowBlank: '{configuration}',
+ },
+ cbind: {
+ nodename: '{nodename}',
+ },
+ },
+ {
+ xtype: 'proxmoxNetworkSelector',
+ name: 'cluster-network',
+ fieldLabel: 'Cluster Network IP/CIDR',
+ allowBlank: true,
+ autoSelect: false,
+ emptyText: gettext('Same as Public Network'),
+ cbind: {
+ nodename: '{nodename}',
+ },
+ },
+ // FIXME: add hint about cluster network and/or reference user to docs??
+ ],
+ column2: [
+ {
+ xtype: 'displayfield',
+ value: gettext('First Ceph monitor') + ':',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Monitor node'),
+ cbind: {
+ value: '{nodename}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ value: gettext('Additional monitors are recommended. They can be created at any time in the Monitor tab.'),
+ userCls: 'pmx-hint',
+ },
+ ],
+ advancedColumn1: [
+ {
+ xtype: 'numberfield',
+ name: 'size',
+ fieldLabel: 'Number of replicas',
+ bind: {
+ value: '{replicas}',
+ },
+ maxValue: 7,
+ minValue: 2,
+ emptyText: '3',
+ },
+ {
+ xtype: 'numberfield',
+ name: 'min_size',
+ fieldLabel: 'Minimum replicas',
+ bind: {
+ maxValue: '{replicas}',
+ value: '{minreplicas}',
+ },
+ minValue: 2,
+ maxValue: 3,
+ setMaxValue: function(value) {
+ this.maxValue = Ext.Number.from(value, 2);
+ // allow enough to avoid split brains with max 'size', but more makes simply no sense
+ if (this.maxValue > 4) {
+ this.maxValue = 4;
+ }
+ this.toggleSpinners();
+ this.validate();
+ },
+ emptyText: '2',
+ },
+ ],
+ onGetValues: function(values) {
+ ['cluster-network', 'size', 'min_size'].forEach(function(field) {
+ if (!values[field]) {
+ delete values[field];
+ }
+ });
+ return values;
+ },
+ onSubmit: function() {
+ var me = this;
+ if (!this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
+ var wizard = me.up('window');
+ var kv = wizard.getValues();
+ delete kv.delete;
+ 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/${nodename}/ceph/mon/${nodename}`,
+ waitMsgTarget: wizard,
+ method: 'POST',
+ success: function() {
+ me.up('pveCephInstallWizard').navigateNext();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ } else {
+ me.up('pveCephInstallWizard').navigateNext();
+ }
+ },
+ },
+ {
+ title: gettext('Success'),
+ xtype: 'panel',
+ border: false,
+ bodyBorder: false,
+ onlineHelp: 'pve_ceph_install',
+ html: 'Installation successful!
'+
+ 'The basic installation and configuration is complete. Depending on your setup, some of the following steps are required to start using Ceph:
'+
+ '- Install Ceph on other nodes
'+
+ '- Create additional Ceph Monitors
'+
+ '- Create Ceph OSDs
'+
+ '- Create Ceph Pools
'+
+ 'To learn more, click on the help button below.
',
+ listeners: {
+ activate: function() {
+ // notify owning container that it should display a help button
+ if (this.onlineHelp) {
+ Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
+ }
+
+ var tp = this.up('#wizcontent');
+ var idx = tp.items.indexOf(this)-1;
+ for (;idx >= 0; idx--) {
+ 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.CephConfigDb', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveNodeCephConfigDb',
+
+ border: false,
+ store: {
+ proxy: {
+ type: 'proxmox',
+ },
+ },
+
+ columns: [
+ {
+ dataIndex: 'section',
+ text: 'WHO',
+ width: 100,
+ },
+ {
+ dataIndex: 'mask',
+ text: 'MASK',
+ hidden: true,
+ width: 80,
+ },
+ {
+ dataIndex: 'level',
+ hidden: true,
+ text: 'LEVEL',
+ },
+ {
+ dataIndex: 'name',
+ flex: 1,
+ text: 'OPTION',
+ },
+ {
+ dataIndex: 'value',
+ flex: 1,
+ text: 'VALUE',
+ },
+ {
+ dataIndex: 'can_update_at_runtime',
+ text: 'Runtime Updatable',
+ hidden: true,
+ width: 80,
+ renderer: Proxmox.Utils.format_boolean,
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.store.proxy.url = '/api2/json/nodes/' + nodename + '/ceph/cfg/db';
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.getStore());
+ me.getStore().load();
+ },
+});
+Ext.define('PVE.node.CephConfig', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeCephConfig',
+
+ bodyStyle: 'white-space:pre',
+ bodyPadding: 5,
+ border: false,
+ scrollable: true,
+ load: function() {
+ var me = this;
+
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ me.update(gettext('Error') + " " + response.htmlStatus);
+ var msg = response.htmlStatus;
+ PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node,
+ function(win) {
+ me.mon(win, 'cephInstallWindowClosed', function() {
+ me.load();
+ });
+ },
+ );
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+ me.update(Ext.htmlEncode(data));
+ },
+ });
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ url: '/nodes/' + nodename + '/ceph/cfg/raw',
+ listeners: {
+ activate: function() {
+ me.load();
+ },
+ },
+ });
+
+ me.callParent();
+
+ me.load();
+ },
+});
+
+Ext.define('PVE.node.CephConfigCrush', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeCephConfigCrush',
+
+ onlineHelp: 'chapter_pveceph',
+
+ layout: 'border',
+ items: [{
+ title: gettext('Configuration'),
+ xtype: 'pveNodeCephConfig',
+ region: 'center',
+ },
+ {
+ title: 'Crush Map', // do not localize
+ xtype: 'pveNodeCephCrushMap',
+ region: 'east',
+ split: true,
+ width: '50%',
+ },
+ {
+ title: gettext('Configuration Database'),
+ xtype: 'pveNodeCephConfigDb',
+ region: 'south',
+ split: true,
+ weight: -30,
+ height: '50%',
+ }],
+
+ initComponent: function() {
+ var me = this;
+ me.defaults = {
+ pveSelNode: me.pveSelNode,
+ };
+ me.callParent();
+ },
+});
+Ext.define('PVE.node.CephCrushMap', {
+ extend: 'Ext.panel.Panel',
+ alias: ['widget.pveNodeCephCrushMap'],
+ bodyStyle: 'white-space:pre',
+ bodyPadding: 5,
+ border: false,
+ stateful: true,
+ stateId: 'layout-ceph-crush',
+ scrollable: true,
+ load: function() {
+ var me = this;
+
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ me.update(gettext('Error') + " " + response.htmlStatus);
+ var msg = response.htmlStatus;
+ PVE.Utils.showCephInstallOrMask(
+ me.ownerCt,
+ msg,
+ me.pveSelNode.data.node,
+ win => me.mon(win, 'cephInstallWindowClosed', () => me.load()),
+ );
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+ me.update(Ext.htmlEncode(data));
+ },
+ });
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ url: `/nodes/${nodename}/ceph/crush`,
+ listeners: {
+ activate: () => me.load(),
+ },
+ });
+
+ me.callParent();
+
+ me.load();
+ },
+});
+Ext.define('PVE.CephCreateFS', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveCephCreateFS',
+
+ showTaskViewer: true,
+ onlineHelp: 'pveceph_fs_create',
+
+ subject: 'Ceph FS',
+ isCreate: true,
+ method: 'POST',
+
+ setFSName: function(fsName) {
+ var me = this;
+
+ if (fsName === '' || fsName === undefined) {
+ fsName = 'cephfs';
+ }
+
+ me.url = "/nodes/" + me.nodename + "/ceph/fs/" + fsName;
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Name'),
+ name: 'name',
+ value: 'cephfs',
+ listeners: {
+ change: function(f, value) {
+ this.up('pveCephCreateFS').setFSName(value);
+ },
+ },
+ submitValue: false, // already encoded in apicall URL
+ emptyText: 'cephfs',
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: 'Placement Groups',
+ name: 'pg_num',
+ value: 128,
+ emptyText: 128,
+ minValue: 8,
+ maxValue: 32768,
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Add as Storage'),
+ value: true,
+ name: 'add-storage',
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Add the new CephFS to the cluster storage configuration.'),
+ },
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ me.setFSName();
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.NodeCephFSPanel', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pveNodeCephFSPanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ title: gettext('CephFS'),
+ onlineHelp: 'pveceph_fs',
+
+ border: false,
+ defaults: {
+ border: false,
+ cbind: {
+ nodename: '{nodename}',
+ },
+ },
+
+ viewModel: {
+ parent: null,
+ data: {
+ mdsCount: 0,
+ },
+ formulas: {
+ canCreateFS: function(get) {
+ return get('mdsCount') > 0;
+ },
+ },
+ },
+
+ items: [
+ {
+ xtype: 'grid',
+ emptyText: Ext.String.format(gettext('No {0} configured.'), 'CephFS'),
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ view.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ autoLoad: true,
+ xtype: 'update',
+ interval: 5 * 1000,
+ autoStart: true,
+ storeid: 'pve-ceph-fs',
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${view.nodename}/ceph/fs`,
+ },
+ model: 'pve-ceph-fs',
+ });
+ view.setStore(Ext.create('Proxmox.data.DiffStore', {
+ rstore: view.rstore,
+ sorters: {
+ property: 'name',
+ direction: 'ASC',
+ },
+ }));
+ // manages the "install ceph?" overlay
+ PVE.Utils.monitor_ceph_installed(view, view.rstore, view.nodename, true);
+ view.on('destroy', () => view.rstore.stopUpdate());
+ },
+
+ onCreate: function() {
+ let view = this.getView();
+ view.rstore.stopUpdate();
+ Ext.create('PVE.CephCreateFS', {
+ autoShow: true,
+ nodename: view.nodename,
+ listeners: {
+ destroy: () => view.rstore.startUpdate(),
+ },
+ });
+ },
+ },
+ tbar: [
+ {
+ text: gettext('Create CephFS'),
+ reference: 'createButton',
+ handler: 'onCreate',
+ bind: {
+ disabled: '{!canCreateFS}',
+ },
+ },
+ ],
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ dataIndex: 'name',
+ },
+ {
+ header: 'Data Pool',
+ flex: 1,
+ dataIndex: 'data_pool',
+ },
+ {
+ header: 'Metadata Pool',
+ flex: 1,
+ dataIndex: 'metadata_pool',
+ },
+ ],
+ cbind: {
+ nodename: '{nodename}',
+ },
+ },
+ {
+ xtype: 'pveNodeCephMDSList',
+ title: gettext('Metadata Servers'),
+ stateId: 'grid-ceph-mds',
+ type: 'mds',
+ storeLoadCallback: function(store, records, success) {
+ var vm = this.getViewModel();
+ if (!success || !records) {
+ vm.set('mdsCount', 0);
+ return;
+ }
+ let count = 0;
+ for (const mds of records) {
+ if (mds.data.state === 'up:standby') {
+ count++;
+ }
+ }
+ vm.set('mdsCount', count);
+ },
+ cbind: {
+ nodename: '{nodename}',
+ },
+ },
+ ],
+}, function() {
+ Ext.define('pve-ceph-fs', {
+ extend: 'Ext.data.Model',
+ fields: ['name', 'data_pool', 'metadata_pool'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/localhost/ceph/fs",
+ },
+ idProperty: 'name',
+ });
+});
+Ext.define('PVE.ceph.Log', {
+ extend: 'Proxmox.panel.LogView',
+ xtype: 'cephLogView',
+
+ nodename: undefined,
+
+ failCallback: function(response) {
+ var me = this;
+ var msg = response.htmlStatus;
+ var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename,
+ function(win) {
+ me.mon(win, 'cephInstallWindowClosed', function() {
+ me.loadTask.delay(200);
+ });
+ },
+ );
+ if (!windowShow) {
+ Proxmox.Utils.setErrorMask(me, msg);
+ }
+ },
+});
+Ext.define('PVE.node.CephMonMgrList', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveNodeCephMonMgr',
+
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'chapter_pveceph',
+
+ defaults: {
+ border: false,
+ onlineHelp: 'chapter_pveceph',
+ flex: 1,
+ },
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+
+ items: [
+ {
+ xtype: 'pveNodeCephServiceList',
+ cbind: { pveSelNode: '{pveSelNode}' },
+ type: 'mon',
+ additionalColumns: [
+ {
+ header: gettext('Quorum'),
+ width: 70,
+ sortable: true,
+ renderer: Proxmox.Utils.format_boolean,
+ dataIndex: 'quorum',
+ },
+ ],
+ stateId: 'grid-ceph-monitor',
+ showCephInstallMask: true,
+ title: gettext('Monitor'),
+ },
+ {
+ xtype: 'pveNodeCephServiceList',
+ type: 'mgr',
+ stateId: 'grid-ceph-manager',
+ cbind: { pveSelNode: '{pveSelNode}' },
+ title: gettext('Manager'),
+ },
+ ],
+});
+Ext.define('PVE.CephCreateOsd', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCephCreateOsd',
+
+ subject: 'Ceph OSD',
+
+ showProgress: true,
+
+ onlineHelp: 'pve_ceph_osds',
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = true;
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${me.nodename}/ceph/crush`,
+ method: 'GET',
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ success: function({ result: { data } }) {
+ let classes = [...new Set(
+ Array.from(
+ data.matchAll(/^device\s[0-9]*\sosd\.[0-9]*\sclass\s(.*)$/gim),
+ m => m[1],
+ ).filter(v => !['hdd', 'ssd', 'nvme'].includes(v)),
+ )].map(v => [v, v]);
+
+ if (classes.length) {
+ let kvField = me.down('field[name=crush-device-class]');
+ kvField.setComboItems([...kvField.comboItems, ...classes]);
+ }
+ },
+ });
+
+ Ext.applyIf(me, {
+ url: "/nodes/" + me.nodename + "/ceph/osd",
+ method: 'POST',
+ items: [
+ {
+ xtype: 'inputpanel',
+ onGetValues: function(values) {
+ Object.keys(values || {}).forEach(function(name) {
+ if (values[name] === '') {
+ delete values[name];
+ }
+ });
+
+ return values;
+ },
+ column1: [
+ {
+ xtype: 'pmxDiskSelector',
+ name: 'dev',
+ nodename: me.nodename,
+ diskType: 'unused',
+ includePartitions: true,
+ fieldLabel: gettext('Disk'),
+ allowBlank: false,
+ },
+ ],
+ column2: [
+ {
+ xtype: 'pmxDiskSelector',
+ name: 'db_dev',
+ nodename: me.nodename,
+ diskType: 'journal_disks',
+ includePartitions: true,
+ fieldLabel: gettext('DB Disk'),
+ value: '',
+ autoSelect: false,
+ allowBlank: true,
+ emptyText: 'use OSD disk',
+ listeners: {
+ change: function(field, val) {
+ me.down('field[name=db_dev_size]').setDisabled(!val);
+ },
+ },
+ },
+ {
+ xtype: 'numberfield',
+ name: 'db_dev_size',
+ fieldLabel: gettext('DB size') + ' (GiB)',
+ minValue: 1,
+ maxValue: 128*1024,
+ decimalPrecision: 2,
+ allowBlank: true,
+ disabled: true,
+ emptyText: gettext('Automatic'),
+ },
+ ],
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'encrypted',
+ fieldLabel: gettext('Encrypt OSD'),
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ comboItems: [
+ ['hdd', 'HDD'],
+ ['ssd', 'SSD'],
+ ['nvme', 'NVMe'],
+ ],
+ name: 'crush-device-class',
+ nodename: me.nodename,
+ fieldLabel: gettext('Device Class'),
+ value: '',
+ autoSelect: false,
+ allowBlank: true,
+ editable: true,
+ emptyText: 'auto detect',
+ deleteEmpty: !me.isCreate,
+ },
+ ],
+ advancedColumn2: [
+ {
+ xtype: 'pmxDiskSelector',
+ name: 'wal_dev',
+ nodename: me.nodename,
+ diskType: 'journal_disks',
+ includePartitions: true,
+ fieldLabel: gettext('WAL Disk'),
+ value: '',
+ autoSelect: false,
+ allowBlank: true,
+ emptyText: 'use OSD/DB disk',
+ listeners: {
+ change: function(field, val) {
+ me.down('field[name=wal_dev_size]').setDisabled(!val);
+ },
+ },
+ },
+ {
+ xtype: 'numberfield',
+ name: 'wal_dev_size',
+ fieldLabel: gettext('WAL size') + ' (GiB)',
+ minValue: 0.5,
+ maxValue: 128*1024,
+ decimalPrecision: 2,
+ allowBlank: true,
+ disabled: true,
+ emptyText: gettext('Automatic'),
+ },
+ ],
+ },
+ {
+ xtype: 'displayfield',
+ padding: '5 0 0 0',
+ userCls: 'pmx-hint',
+ value: 'Note: Ceph is not compatible with disks backed by a hardware ' +
+ 'RAID controller. For details see ' +
+ 'the reference documentation.',
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.CephRemoveOsd', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.pveCephRemoveOsd'],
+
+ isRemove: true,
+
+ showProgress: true,
+ method: 'DELETE',
+ items: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'cleanup',
+ checked: true,
+ labelWidth: 130,
+ fieldLabel: gettext('Cleanup Disks'),
+ },
+ {
+ xtype: 'displayfield',
+ name: 'osd-flag-hint',
+ userCls: 'pmx-hint',
+ value: gettext('Global flags limiting the self healing of Ceph are enabled.'),
+ hidden: true,
+ },
+ {
+ xtype: 'displayfield',
+ name: 'degraded-objects-hint',
+ userCls: 'pmx-hint',
+ value: gettext('Objects are degraded. Consider waiting until the cluster is healthy.'),
+ hidden: true,
+ },
+ ],
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ if (me.osdid === undefined || me.osdid < 0) {
+ throw "no osdid specified";
+ }
+
+ me.isCreate = true;
+
+ me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString();
+
+ Ext.applyIf(me, {
+ url: "/nodes/" + me.nodename + "/ceph/osd/" + me.osdid.toString(),
+ });
+
+ me.callParent();
+
+ if (me.warnings.flags) {
+ me.down('field[name=osd-flag-hint]').setHidden(false);
+ }
+ if (me.warnings.degraded) {
+ me.down('field[name=degraded-objects-hint]').setHidden(false);
+ }
+ },
+});
+
+Ext.define('PVE.CephSetFlags', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCephSetFlags',
+
+ showProgress: true,
+
+ width: 720,
+ layout: 'fit',
+
+ onlineHelp: 'pve_ceph_osds',
+ isCreate: true,
+ title: Ext.String.format(gettext('Manage {0}'), 'Global OSD Flags'),
+ submitText: gettext('Apply'),
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ onGetValues: function(values) {
+ let me = this;
+ let val = {};
+ me.down('#flaggrid').getStore().each((rec) => {
+ val[rec.data.name] = rec.data.value ? 1 : 0;
+ });
+
+ return val;
+ },
+ items: [
+ {
+ xtype: 'grid',
+ itemId: 'flaggrid',
+ store: {
+ listeners: {
+ update: function() {
+ this.commitChanges();
+ },
+ },
+ },
+
+ columns: [
+ {
+ text: gettext('Enable'),
+ xtype: 'checkcolumn',
+ width: 75,
+ dataIndex: 'value',
+ },
+ {
+ text: 'Name',
+ dataIndex: 'name',
+ },
+ {
+ text: 'Description',
+ flex: 1,
+ dataIndex: 'description',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.applyIf(me, {
+ url: "/cluster/ceph/flags",
+ method: 'PUT',
+ });
+
+ me.callParent();
+
+ let grid = me.down('#flaggrid');
+ me.load({
+ success: function(response, options) {
+ let data = response.result.data;
+ grid.getStore().setData(data);
+ // re-align after store load, else the window is not centered
+ me.alignTo(Ext.getBody(), 'c-c');
+ },
+ });
+ },
+});
+
+Ext.define('PVE.node.CephOsdTree', {
+ extend: 'Ext.tree.Panel',
+ alias: ['widget.pveNodeCephOsdTree'],
+ onlineHelp: 'chapter_pveceph',
+
+ viewModel: {
+ data: {
+ nodename: '',
+ flags: [],
+ maxversion: '0',
+ mixedversions: false,
+ versions: {},
+ isOsd: false,
+ downOsd: false,
+ upOsd: false,
+ inOsd: false,
+ outOsd: false,
+ osdid: '',
+ osdhost: '',
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ reload: function() {
+ let me = this;
+ let view = me.getView();
+ let vm = me.getViewModel();
+ let nodename = vm.get('nodename');
+ let sm = view.getSelectionModel();
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + nodename + "/ceph/osd",
+ waitMsgTarget: view,
+ method: 'GET',
+ failure: function(response, opts) {
+ let msg = response.htmlStatus;
+ PVE.Utils.showCephInstallOrMask(view, msg, nodename, win =>
+ view.mon(win, 'cephInstallWindowClosed', () => { me.reload(); }),
+ );
+ },
+ success: function(response, opts) {
+ let data = response.result.data;
+ let selected = view.getSelection();
+ let name;
+ if (selected.length) {
+ name = selected[0].data.name;
+ }
+ data.versions = data.versions || {};
+ vm.set('versions', data.versions);
+ // extract max version
+ let maxversion = "0";
+ let mixedversions = false;
+ let traverse;
+ traverse = function(node, fn) {
+ fn(node);
+ if (Array.isArray(node.children)) {
+ node.children.forEach(c => { traverse(c, fn); });
+ }
+ };
+ traverse(data.root, node => {
+ // compatibility for old api call
+ if (node.type === 'host' && !node.version) {
+ node.version = data.versions[node.name];
+ }
+
+ if (node.version === undefined) {
+ return;
+ }
+
+ if (PVE.Utils.compare_ceph_versions(node.version, maxversion) !== 0 && maxversion !== "0") {
+ mixedversions = true;
+ }
+
+ if (PVE.Utils.compare_ceph_versions(node.version, maxversion) > 0) {
+ maxversion = node.version;
+ }
+ });
+ vm.set('maxversion', maxversion);
+ vm.set('mixedversions', mixedversions);
+ sm.deselectAll();
+ view.setRootNode(data.root);
+ view.expandAll();
+ if (name) {
+ let node = view.getRootNode().findChild('name', name, true);
+ if (node) {
+ view.setSelection([node]);
+ }
+ }
+
+ let flags = data.flags.split(',');
+ vm.set('flags', flags);
+ },
+ });
+ },
+
+ osd_cmd: function(comp) {
+ let me = this;
+ let vm = this.getViewModel();
+ let cmd = comp.cmd;
+ let params = comp.params || {};
+ let osdid = vm.get('osdid');
+
+ let doRequest = function() {
+ let targetnode = vm.get('osdhost');
+ // cmds not node specific and need to work if the OSD node is down
+ if (['in', 'out'].includes(cmd)) {
+ targetnode = vm.get('nodename');
+ }
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${targetnode}/ceph/osd/${osdid}/${cmd}`,
+ waitMsgTarget: me.getView(),
+ method: 'POST',
+ params: params,
+ success: () => { me.reload(); },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ };
+
+ if (cmd === 'scrub') {
+ Ext.MessageBox.defaultButton = params.deep === 1 ? 2 : 1;
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: params.deep === 1 ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
+ msg: params.deep !== 1
+ ? Ext.String.format(gettext("Scrub OSD.{0}"), osdid)
+ : Ext.String.format(gettext("Deep Scrub OSD.{0}"), osdid) +
+ "
Caution: This can reduce performance while it is running.",
+ buttons: Ext.Msg.YESNO,
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ doRequest();
+ },
+ });
+ } else {
+ doRequest();
+ }
+ },
+
+ create_osd: function() {
+ let me = this;
+ let vm = this.getViewModel();
+ Ext.create('PVE.CephCreateOsd', {
+ nodename: vm.get('nodename'),
+ taskDone: () => { me.reload(); },
+ }).show();
+ },
+
+ destroy_osd: async function() {
+ let me = this;
+ let vm = this.getViewModel();
+
+ let warnings = {
+ flags: false,
+ degraded: false,
+ };
+
+ let flagsPromise = Proxmox.Async.api2({
+ url: `/cluster/ceph/flags`,
+ method: 'GET',
+ });
+
+ let statusPromise = Proxmox.Async.api2({
+ url: `/cluster/ceph/status`,
+ method: 'GET',
+ });
+
+ me.getView().mask(gettext('Loading...'));
+
+ try {
+ let result = await Promise.all([flagsPromise, statusPromise]);
+
+ let flagsData = result[0].result.data;
+ let statusData = result[1].result.data;
+
+ let flags = Array.from(
+ flagsData.filter(v => v.value),
+ v => v.name,
+ ).filter(v => ['norebalance', 'norecover', 'noout'].includes(v));
+
+ if (flags.length) {
+ warnings.flags = true;
+ }
+ if (Object.keys(statusData.pgmap).includes('degraded_objects')) {
+ warnings.degraded = true;
+ }
+ } catch (error) {
+ Ext.Msg.alert(gettext('Error'), error.htmlStatus);
+ me.getView().unmask();
+ return;
+ }
+
+ me.getView().unmask();
+ Ext.create('PVE.CephRemoveOsd', {
+ nodename: vm.get('osdhost'),
+ osdid: vm.get('osdid'),
+ warnings: warnings,
+ taskDone: () => { me.reload(); },
+ autoShow: true,
+ });
+ },
+
+ set_flags: function() {
+ let me = this;
+ let vm = this.getViewModel();
+ Ext.create('PVE.CephSetFlags', {
+ nodename: vm.get('nodename'),
+ taskDone: () => { me.reload(); },
+ }).show();
+ },
+
+ service_cmd: function(comp) {
+ let me = this;
+ let vm = this.getViewModel();
+ let cmd = comp.cmd || comp;
+
+ let doRequest = function() {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${vm.get('osdhost')}/ceph/${cmd}`,
+ params: { service: "osd." + vm.get('osdid') },
+ waitMsgTarget: me.getView(),
+ method: 'POST',
+ success: function(response, options) {
+ let upid = response.result.data;
+ let win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid,
+ taskDone: () => { me.reload(); },
+ });
+ win.show();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ };
+
+ if (cmd === "stop") {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${vm.get('osdhost')}/ceph/cmd-safety`,
+ params: {
+ service: 'osd',
+ id: vm.get('osdid'),
+ action: 'stop',
+ },
+ waitMsgTarget: me.getView(),
+ method: 'GET',
+ success: function({ result: { data } }) {
+ if (!data.safe) {
+ Ext.Msg.show({
+ title: gettext('Warning'),
+ message: data.status,
+ icon: Ext.Msg.WARNING,
+ buttons: Ext.Msg.OKCANCEL,
+ buttonText: { ok: gettext('Stop OSD') },
+ fn: function(selection) {
+ if (selection === 'ok') {
+ doRequest();
+ }
+ },
+ });
+ } else {
+ doRequest();
+ }
+ },
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ } else {
+ doRequest();
+ }
+ },
+
+ run_details: function(view, rec) {
+ if (rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0) {
+ this.details();
+ }
+ },
+
+ details: function() {
+ let vm = this.getViewModel();
+ Ext.create('PVE.CephOsdDetails', {
+ nodename: vm.get('osdhost'),
+ osdid: vm.get('osdid'),
+ }).show();
+ },
+
+ set_selection_status: function(tp, selection) {
+ if (selection.length < 1) {
+ return;
+ }
+ let rec = selection[0];
+ let vm = this.getViewModel();
+
+ let isOsd = rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0;
+
+ vm.set('isOsd', isOsd);
+ vm.set('downOsd', isOsd && rec.data.status === 'down');
+ vm.set('upOsd', isOsd && rec.data.status !== 'down');
+ vm.set('inOsd', isOsd && rec.data.in);
+ vm.set('outOsd', isOsd && !rec.data.in);
+ vm.set('osdid', isOsd ? rec.data.id : undefined);
+ vm.set('osdhost', isOsd ? rec.data.host : undefined);
+ },
+
+ render_status: function(value, metaData, rec) {
+ if (!value) {
+ return value;
+ }
+ let inout = rec.data.in ? 'in' : 'out';
+ let updownicon = value === 'up' ? 'good fa-arrow-circle-up'
+ : 'critical fa-arrow-circle-down';
+
+ let inouticon = rec.data.in ? 'good fa-circle'
+ : 'warning fa-circle-o';
+
+ let text = value + ' / ' +
+ inout + ' ';
+
+ return text;
+ },
+
+ render_wal: function(value, metaData, rec) {
+ if (!value &&
+ rec.data.osdtype === 'bluestore' &&
+ rec.data.type === 'osd') {
+ return 'N/A';
+ }
+ return value;
+ },
+
+ render_version: function(value, metadata, rec) {
+ let vm = this.getViewModel();
+ let versions = vm.get('versions');
+ let icon = "";
+ let version = value || "";
+ let maxversion = vm.get('maxversion');
+ if (value && PVE.Utils.compare_ceph_versions(value, maxversion) !== 0) {
+ let host_version = rec.parentNode?.data?.version || versions[rec.data.host] || "";
+ if (rec.data.type === 'host' || PVE.Utils.compare_ceph_versions(host_version, maxversion) !== 0) {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE');
+ } else {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
+ }
+ } else if (value && vm.get('mixedversions')) {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK');
+ }
+
+ return icon + version;
+ },
+
+ render_osd_val: function(value, metaData, rec) {
+ return rec.data.type === 'osd' ? value : '';
+ },
+ render_osd_weight: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ return Ext.util.Format.number(value, '0.00###');
+ },
+
+ render_osd_latency: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ let commit_ms = rec.data.commit_latency_ms,
+ apply_ms = rec.data.apply_latency_ms;
+ return apply_ms + ' / ' + commit_ms;
+ },
+
+ render_osd_size: function(value, metaData, rec) {
+ return this.render_osd_val(Proxmox.Utils.render_size(value), metaData, rec);
+ },
+
+ control: {
+ '#': {
+ selectionchange: 'set_selection_status',
+ },
+ },
+
+ init: function(view) {
+ let me = this;
+ let vm = this.getViewModel();
+
+ if (!view.pveSelNode.data.node) {
+ throw "no node name specified";
+ }
+
+ vm.set('nodename', view.pveSelNode.data.node);
+
+ me.callParent();
+ me.reload();
+ },
+ },
+
+ stateful: true,
+ stateId: 'grid-ceph-osd',
+ rootVisible: false,
+ useArrows: true,
+ listeners: {
+ itemdblclick: 'run_details',
+ },
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: 'Name',
+ dataIndex: 'name',
+ width: 150,
+ },
+ {
+ text: 'Type',
+ dataIndex: 'type',
+ hidden: true,
+ align: 'right',
+ width: 75,
+ },
+ {
+ text: gettext("Class"),
+ dataIndex: 'device_class',
+ align: 'right',
+ width: 75,
+ },
+ {
+ text: "OSD Type",
+ dataIndex: 'osdtype',
+ align: 'right',
+ width: 100,
+ },
+ {
+ text: "Bluestore Device",
+ dataIndex: 'blfsdev',
+ align: 'right',
+ width: 75,
+ hidden: true,
+ },
+ {
+ text: "DB Device",
+ dataIndex: 'dbdev',
+ align: 'right',
+ width: 75,
+ hidden: true,
+ },
+ {
+ text: "WAL Device",
+ dataIndex: 'waldev',
+ align: 'right',
+ renderer: 'render_wal',
+ width: 75,
+ hidden: true,
+ },
+ {
+ text: 'Status',
+ dataIndex: 'status',
+ align: 'right',
+ renderer: 'render_status',
+ width: 120,
+ },
+ {
+ text: gettext('Version'),
+ dataIndex: 'version',
+ align: 'right',
+ renderer: 'render_version',
+ },
+ {
+ text: 'weight',
+ dataIndex: 'crush_weight',
+ align: 'right',
+ renderer: 'render_osd_weight',
+ width: 90,
+ },
+ {
+ text: 'reweight',
+ dataIndex: 'reweight',
+ align: 'right',
+ renderer: 'render_osd_weight',
+ width: 90,
+ },
+ {
+ text: gettext('Used') + ' (%)',
+ dataIndex: 'percent_used',
+ align: 'right',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.type !== 'osd') {
+ return '';
+ }
+ return Ext.util.Format.number(value, '0.00');
+ },
+ width: 100,
+ },
+ {
+ text: gettext('Total'),
+ dataIndex: 'total_space',
+ align: 'right',
+ renderer: 'render_osd_size',
+ width: 100,
+ },
+ {
+ text: 'Apply/Commit
Latency (ms)',
+ dataIndex: 'apply_latency_ms',
+ align: 'right',
+ renderer: 'render_osd_latency',
+ width: 120,
+ },
+ {
+ text: 'PGs',
+ dataIndex: 'pgs',
+ align: 'right',
+ renderer: 'render_osd_val',
+ width: 90,
+ },
+ ],
+
+
+ tbar: {
+ items: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: 'reload',
+ },
+ '-',
+ {
+ text: gettext('Create') + ': OSD',
+ handler: 'create_osd',
+ },
+ {
+ text: Ext.String.format(gettext('Manage {0}'), 'Global Flags'),
+ handler: 'set_flags',
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ data: {
+ osd: undefined,
+ },
+ bind: {
+ data: {
+ osd: "{osdid}",
+ },
+ },
+ tpl: [
+ '',
+ 'osd.{osd}:',
+ '',
+ gettext('No OSD selected'),
+ '',
+ ],
+ },
+ {
+ text: gettext('Details'),
+ iconCls: 'fa fa-info-circle',
+ disabled: true,
+ bind: {
+ disabled: '{!isOsd}',
+ },
+ handler: 'details',
+ },
+ {
+ text: gettext('Start'),
+ iconCls: 'fa fa-play',
+ disabled: true,
+ bind: {
+ disabled: '{!downOsd}',
+ },
+ cmd: 'start',
+ handler: 'service_cmd',
+ },
+ {
+ text: gettext('Stop'),
+ iconCls: 'fa fa-stop',
+ disabled: true,
+ bind: {
+ disabled: '{!upOsd}',
+ },
+ cmd: 'stop',
+ handler: 'service_cmd',
+ },
+ {
+ text: gettext('Restart'),
+ iconCls: 'fa fa-refresh',
+ disabled: true,
+ bind: {
+ disabled: '{!upOsd}',
+ },
+ cmd: 'restart',
+ handler: 'service_cmd',
+ },
+ '-',
+ {
+ text: 'Out',
+ iconCls: 'fa fa-circle-o',
+ disabled: true,
+ bind: {
+ disabled: '{!inOsd}',
+ },
+ cmd: 'out',
+ handler: 'osd_cmd',
+ },
+ {
+ text: 'In',
+ iconCls: 'fa fa-circle',
+ disabled: true,
+ bind: {
+ disabled: '{!outOsd}',
+ },
+ cmd: 'in',
+ handler: 'osd_cmd',
+ },
+ '-',
+ {
+ text: gettext('More'),
+ iconCls: 'fa fa-bars',
+ disabled: true,
+ bind: {
+ disabled: '{!isOsd}',
+ },
+ menu: [
+ {
+ text: gettext('Scrub'),
+ iconCls: 'fa fa-shower',
+ cmd: 'scrub',
+ handler: 'osd_cmd',
+ },
+ {
+ text: gettext('Deep Scrub'),
+ iconCls: 'fa fa-bath',
+ cmd: 'scrub',
+ params: {
+ deep: 1,
+ },
+ handler: 'osd_cmd',
+ },
+ {
+ text: gettext('Destroy'),
+ itemId: 'remove',
+ iconCls: 'fa fa-fw fa-trash-o',
+ bind: {
+ disabled: '{!downOsd}',
+ },
+ handler: 'destroy_osd',
+ },
+ ],
+ },
+ ],
+ },
+
+ fields: [
+ 'name', 'type', 'status', 'host', 'in', 'id',
+ { type: 'number', name: 'reweight' },
+ { type: 'number', name: 'percent_used' },
+ { type: 'integer', name: 'bytes_used' },
+ { type: 'integer', name: 'total_space' },
+ { type: 'integer', name: 'apply_latency_ms' },
+ { type: 'integer', name: 'commit_latency_ms' },
+ { type: 'string', name: 'device_class' },
+ { type: 'string', name: 'osdtype' },
+ { type: 'string', name: 'blfsdev' },
+ { type: 'string', name: 'dbdev' },
+ { type: 'string', name: 'waldev' },
+ {
+ type: 'string', name: 'version', calculate: function(data) {
+ return PVE.Utils.parse_ceph_version(data);
+ },
+},
+ {
+ type: 'string', name: 'iconCls', calculate: function(data) {
+ let iconMap = {
+ host: 'fa-building',
+ osd: 'fa-hdd-o',
+ root: 'fa-server',
+ };
+ return `fa x-fa-tree ${iconMap[data.type] ?? 'fa-folder-o'}`;
+ },
+},
+ { type: 'number', name: 'crush_weight' },
+ ],
+});
+Ext.define('pve-osd-details-devices', {
+ extend: 'Ext.data.Model',
+ fields: ['device', 'type', 'physical_device', 'size', 'support_discard', 'dev_node'],
+ idProperty: 'device',
+});
+
+Ext.define('PVE.CephOsdDetails', {
+ extend: 'Ext.window.Window',
+ alias: ['widget.pveCephOsdDetails'],
+
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ cbindData: function() {
+ let me = this;
+ me.baseUrl = `/nodes/${me.nodename}/ceph/osd/${me.osdid}`;
+ return {
+ title: `${gettext('Details')}: OSD ${me.osdid}`,
+ };
+ },
+
+ viewModel: {
+ data: {
+ device: '',
+ },
+ },
+
+ modal: true,
+ width: 650,
+ minHeight: 250,
+ resizable: true,
+ cbind: {
+ title: '{title}',
+ },
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ defaults: {
+ layout: 'fit',
+ border: false,
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ reload: function() {
+ let view = this.getView();
+
+ Proxmox.Utils.API2Request({
+ url: `${view.baseUrl}/metadata`,
+ waitMsgTarget: view.lookup('detailsTabs'),
+ method: 'GET',
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(view.lookup('detailsTabs'), response.htmlStatus);
+ },
+ success: function(response, opts) {
+ let d = response.result.data;
+ let osdData = Object.keys(d.osd).sort().map(x => ({ key: x, value: d.osd[x] }));
+ view.osdStore.loadData(osdData);
+ let devices = view.lookup('devices');
+ let deviceStore = devices.getStore();
+ deviceStore.loadData(d.devices);
+
+ view.lookup('osdGeneral').rstore.fireEvent('load', view.osdStore, osdData, true);
+ view.lookup('osdNetwork').rstore.fireEvent('load', view.osdStore, osdData, true);
+
+ // select 'block' device automatically on first load
+ if (devices.getSelection().length === 0) {
+ devices.setSelection(deviceStore.findRecord('device', 'block'));
+ }
+ },
+ });
+ },
+
+ showDevInfo: function(grid, selected) {
+ let view = this.getView();
+ if (selected[0]) {
+ let device = selected[0].data.device;
+ this.getViewModel().set('device', device);
+
+ let detailStore = view.lookup('volumeDetails');
+ detailStore.rstore.getProxy().setUrl(`api2/json${view.baseUrl}/lv-info`);
+ detailStore.rstore.getProxy().setExtraParams({ 'type': device });
+ detailStore.setLoading();
+ detailStore.rstore.load({ callback: () => detailStore.setLoading(false) });
+ }
+ },
+
+ init: function() {
+ this.reload();
+ },
+
+ control: {
+ 'grid[reference=devices]': {
+ selectionchange: 'showDevInfo',
+ },
+ },
+ },
+ tbar: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: 'reload',
+ },
+ ],
+ initComponent: function() {
+ let me = this;
+
+ me.osdStore = Ext.create('Proxmox.data.ObjectStore');
+
+ Ext.applyIf(me, {
+ items: [
+ {
+ xtype: 'tabpanel',
+ reference: 'detailsTabs',
+ items: [
+ {
+ xtype: 'proxmoxObjectGrid',
+ reference: 'osdGeneral',
+ tooltip: gettext('Various information about the OSD'),
+ rstore: me.osdStore,
+ title: gettext('General'),
+ viewConfig: {
+ enableTextSelection: true,
+ },
+ gridRows: [
+ {
+ xtype: 'text',
+ name: 'version',
+ text: gettext('Version'),
+ },
+ {
+ xtype: 'text',
+ name: 'hostname',
+ text: gettext('Hostname'),
+ },
+ {
+ xtype: 'text',
+ name: 'osd_data',
+ text: gettext('OSD data path'),
+ },
+ {
+ xtype: 'text',
+ name: 'osd_objectstore',
+ text: gettext('OSD object store'),
+ },
+ {
+ xtype: 'text',
+ name: 'mem_usage',
+ text: gettext('Memory usage'),
+ renderer: Proxmox.Utils.render_size,
+ },
+ {
+ xtype: 'text',
+ name: 'pid',
+ text: `${gettext('Process ID')} (PID)`,
+ },
+ ],
+ },
+ {
+ xtype: 'proxmoxObjectGrid',
+ reference: 'osdNetwork',
+ tooltip: gettext('Addresses and ports used by the OSD service'),
+ rstore: me.osdStore,
+ title: gettext('Network'),
+ viewConfig: {
+ enableTextSelection: true,
+ },
+ gridRows: [
+ {
+ xtype: 'text',
+ name: 'front_addr',
+ text: `${gettext('Front Address')}
(Client & Monitor)`,
+ renderer: PVE.Utils.render_ceph_osd_addr,
+ },
+ {
+ xtype: 'text',
+ name: 'hb_front_addr',
+ text: gettext('Heartbeat Front Address'),
+ renderer: PVE.Utils.render_ceph_osd_addr,
+ },
+ {
+ xtype: 'text',
+ name: 'back_addr',
+ text: `${gettext('Back Address')}
(OSD)`,
+ renderer: PVE.Utils.render_ceph_osd_addr,
+ },
+ {
+ xtype: 'text',
+ name: 'hb_back_addr',
+ text: gettext('Heartbeat Back Address'),
+ renderer: PVE.Utils.render_ceph_osd_addr,
+ },
+ ],
+ },
+ {
+ xtype: 'panel',
+ title: 'Devices',
+ tooltip: gettext('Physical devices used by the OSD'),
+ items: [
+ {
+ xtype: 'grid',
+ border: false,
+ reference: 'devices',
+ store: {
+ model: 'pve-osd-details-devices',
+ },
+ columns: {
+ items: [
+ { text: gettext('Device'), dataIndex: 'device' },
+ { text: gettext('Type'), dataIndex: 'type' },
+ {
+ text: gettext('Physical Device'),
+ dataIndex: 'physical_device',
+ },
+ {
+ text: gettext('Size'),
+ dataIndex: 'size',
+ renderer: Proxmox.Utils.render_size,
+ },
+ {
+ text: 'Discard',
+ dataIndex: 'support_discard',
+ hidden: true,
+ },
+ {
+ text: gettext('Device node'),
+ dataIndex: 'dev_node',
+ hidden: true,
+ },
+ ],
+ defaults: {
+ tdCls: 'pointer',
+ flex: 1,
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxObjectGrid',
+ reference: 'volumeDetails',
+ maskOnLoad: true,
+ viewConfig: {
+ enableTextSelection: true,
+ },
+ bind: {
+ title: Ext.String.format(
+ gettext('Volume Details for {0}'),
+ '{device}',
+ ),
+ },
+ rows: {
+ creation_time: {
+ header: gettext('Creation time'),
+ },
+ lv_name: {
+ header: gettext('LV Name'),
+ },
+ lv_path: {
+ header: gettext('LV Path'),
+ },
+ lv_uuid: {
+ header: gettext('LV UUID'),
+ },
+ vg_name: {
+ header: gettext('VG Name'),
+ },
+ },
+ url: 'nodes/', //placeholder will be set when device is selected
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.CephPoolInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveCephPoolInputPanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ showProgress: true,
+ onlineHelp: 'pve_ceph_pools',
+
+ subject: 'Ceph Pool',
+ column1: [
+ {
+ xtype: 'pmxDisplayEditField',
+ fieldLabel: gettext('Name'),
+ cbind: {
+ editable: '{isCreate}',
+ value: '{pool_name}',
+ },
+ name: 'name',
+ allowBlank: false,
+ },
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: '{!isErasure}',
+ },
+ fieldLabel: gettext('Size'),
+ name: 'size',
+ editConfig: {
+ xtype: 'proxmoxintegerfield',
+ value: 3,
+ minValue: 2,
+ maxValue: 7,
+ allowBlank: false,
+ listeners: {
+ change: function(field, val) {
+ let size = Math.round(val / 2);
+ if (size > 1) {
+ field.up('inputpanel').down('field[name=min_size]').setValue(size);
+ }
+ },
+ },
+ },
+
+ },
+ ],
+ column2: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: 'PG Autoscale Mode',
+ name: 'pg_autoscale_mode',
+ comboItems: [
+ ['warn', 'warn'],
+ ['on', 'on'],
+ ['off', 'off'],
+ ],
+ value: 'on', // FIXME: check ceph version and only default to on on octopus and newer
+ allowBlank: false,
+ autoSelect: false,
+ labelWidth: 140,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Add as Storage'),
+ cbind: {
+ value: '{isCreate}',
+ hidden: '{!isCreate}',
+ },
+ name: 'add_storages',
+ labelWidth: 140,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Add the new pool to the cluster storage configuration.'),
+ },
+ },
+ ],
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('Min. Size'),
+ name: 'min_size',
+ value: 2,
+ cbind: {
+ minValue: (get) => get('isCreate') ? 2 : 1,
+ },
+ maxValue: 7,
+ allowBlank: false,
+ listeners: {
+ change: function(field, minSize) {
+ let panel = field.up('inputpanel');
+ let size = panel.down('field[name=size]').getValue();
+
+ let showWarning = minSize < (size / 2) && minSize !== size;
+
+ let fieldLabel = gettext('Min. Size');
+ if (showWarning) {
+ fieldLabel = gettext('Min. Size') + ' ';
+ }
+ panel.down('field[name=min_size-warning]').setHidden(!showWarning);
+ field.setFieldLabel(fieldLabel);
+ },
+ },
+ },
+ {
+ xtype: 'displayfield',
+ name: 'min_size-warning',
+ userCls: 'pmx-hint',
+ value: gettext('min_size < size/2 can lead to data loss, incomplete PGs or unfound objects.'),
+ hidden: true,
+ },
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: '{!isErasure}',
+ nodename: '{nodename}',
+ isCreate: '{isCreate}',
+ },
+ fieldLabel: 'Crush Rule', // do not localize
+ name: 'crush_rule',
+ editConfig: {
+ xtype: 'pveCephRuleSelector',
+ allowBlank: false,
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: '# of PGs',
+ name: 'pg_num',
+ value: 128,
+ minValue: 1,
+ maxValue: 32768,
+ allowBlank: false,
+ emptyText: 128,
+ },
+ ],
+ advancedColumn2: [
+ {
+ xtype: 'numberfield',
+ fieldLabel: gettext('Target Ratio'),
+ name: 'target_size_ratio',
+ minValue: 0,
+ decimalPrecision: 3,
+ allowBlank: true,
+ emptyText: '0.0',
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('The ratio of storage amount this pool will consume compared to other pools with ratios. Used for auto-scaling.'),
+ },
+ },
+ {
+ xtype: 'pveSizeField',
+ name: 'target_size',
+ fieldLabel: gettext('Target Size'),
+ unit: 'GiB',
+ minValue: 0,
+ allowBlank: true,
+ allowZero: true,
+ emptyText: '0',
+ emptyValue: 0,
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('The amount of data eventually stored in this pool. Used for auto-scaling.'),
+ },
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: Ext.String.format(gettext('{0} takes precedence.'), gettext('Target Ratio')), // FIXME: tooltip?
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: 'Min. # of PGs',
+ name: 'pg_num_min',
+ minValue: 0,
+ allowBlank: true,
+ emptyText: '0',
+ },
+ ],
+
+ onGetValues: function(values) {
+ Object.keys(values || {}).forEach(function(name) {
+ if (values[name] === '') {
+ delete values[name];
+ }
+ });
+
+ return values;
+ },
+});
+
+Ext.define('PVE.Ceph.PoolEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveCephPoolEdit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ cbindData: {
+ pool_name: '',
+ isCreate: (cfg) => !cfg.pool_name,
+ },
+
+ cbind: {
+ autoLoad: get => !get('isCreate'),
+ url: get => get('isCreate')
+ ? `/nodes/${get('nodename')}/ceph/pool`
+ : `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}`,
+ loadUrl: get => `/nodes/${get('nodename')}/ceph/pool/${get('pool_name')}/status`,
+ method: get => get('isCreate') ? 'POST' : 'PUT',
+ },
+
+ showProgress: true,
+
+ subject: gettext('Ceph Pool'),
+
+ items: [{
+ xtype: 'pveCephPoolInputPanel',
+ cbind: {
+ nodename: '{nodename}',
+ pool_name: '{pool_name}',
+ isErasure: '{isErasure}',
+ isCreate: '{isCreate}',
+ },
+ }],
+});
+
+Ext.define('PVE.node.Ceph.PoolList', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveNodeCephPoolList',
+
+ onlineHelp: 'chapter_pveceph',
+
+ stateful: true,
+ stateId: 'grid-ceph-pools',
+ bufferedRenderer: false,
+
+ features: [{ ftype: 'summary' }],
+
+ columns: [
+ {
+ text: gettext('Name'),
+ minWidth: 120,
+ flex: 2,
+ sortable: true,
+ dataIndex: 'pool_name',
+ },
+ {
+ text: gettext('Type'),
+ minWidth: 100,
+ flex: 1,
+ dataIndex: 'type',
+ hidden: true,
+ },
+ {
+ text: gettext('Size') + '/min',
+ minWidth: 100,
+ flex: 1,
+ align: 'right',
+ renderer: (v, meta, rec) => `${v}/${rec.data.min_size}`,
+ dataIndex: 'size',
+ },
+ {
+ text: '# of Placement Groups',
+ flex: 1,
+ minWidth: 100,
+ align: 'right',
+ dataIndex: 'pg_num',
+ },
+ {
+ text: gettext('Optimal # of PGs'),
+ flex: 1,
+ minWidth: 100,
+ align: 'right',
+ dataIndex: 'pg_num_final',
+ renderer: function(value, metaData) {
+ if (!value) {
+ value = ' n/a';
+ metaData.tdAttr = 'data-qtip="Needs pg_autoscaler module enabled."';
+ }
+ return value;
+ },
+ },
+ {
+ text: gettext('Min. # of PGs'),
+ flex: 1,
+ minWidth: 100,
+ align: 'right',
+ dataIndex: 'pg_num_min',
+ hidden: true,
+ },
+ {
+ text: gettext('Target Ratio'),
+ flex: 1,
+ minWidth: 100,
+ align: 'right',
+ dataIndex: 'target_size_ratio',
+ renderer: Ext.util.Format.numberRenderer('0.0000'),
+ hidden: true,
+ },
+ {
+ text: gettext('Target Size'),
+ flex: 1,
+ minWidth: 100,
+ align: 'right',
+ dataIndex: 'target_size',
+ hidden: true,
+ renderer: function(v, metaData, rec) {
+ let value = Proxmox.Utils.render_size(v);
+ if (rec.data.target_size_ratio > 0) {
+ value = ' ' + value;
+ metaData.tdAttr = 'data-qtip="Target Size Ratio takes precedence over Target Size."';
+ }
+ return value;
+ },
+ },
+ {
+ text: gettext('Autoscale Mode'),
+ flex: 1,
+ minWidth: 100,
+ align: 'right',
+ dataIndex: 'pg_autoscale_mode',
+ },
+ {
+ text: 'CRUSH Rule (ID)',
+ flex: 1,
+ align: 'right',
+ minWidth: 150,
+ renderer: (v, meta, rec) => `${v} (${rec.data.crush_rule})`,
+ dataIndex: 'crush_rule_name',
+ },
+ {
+ text: gettext('Used') + ' (%)',
+ flex: 1,
+ minWidth: 150,
+ sortable: true,
+ align: 'right',
+ dataIndex: 'bytes_used',
+ summaryType: 'sum',
+ summaryRenderer: Proxmox.Utils.render_size,
+ renderer: function(v, meta, rec) {
+ let percentage = Ext.util.Format.percent(rec.data.percent_used, '0.00');
+ let used = Proxmox.Utils.render_size(v);
+ return `${used} (${percentage})`;
+ },
+ },
+ ],
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var rstore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 3000,
+ storeid: 'ceph-pool-list' + nodename,
+ model: 'ceph-pool-list',
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${nodename}/ceph/pool`,
+ },
+ });
+ let store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore });
+
+ // manages the "install ceph?" overlay
+ PVE.Utils.monitor_ceph_installed(me, rstore, nodename);
+
+ var run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec || !rec.data.pool_name) {
+ return;
+ }
+ Ext.create('PVE.Ceph.PoolEdit', {
+ title: gettext('Edit') + ': Ceph Pool',
+ nodename: nodename,
+ pool_name: rec.data.pool_name,
+ isErasure: rec.data.type === 'erasure',
+ autoShow: true,
+ listeners: {
+ destroy: () => rstore.load(),
+ },
+ });
+ };
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ {
+ text: gettext('Create'),
+ handler: function() {
+ Ext.create('PVE.Ceph.PoolEdit', {
+ title: gettext('Create') + ': Ceph Pool',
+ isCreate: true,
+ isErasure: false,
+ nodename: nodename,
+ autoShow: true,
+ listeners: {
+ destroy: () => rstore.load(),
+ },
+ });
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ selModel: sm,
+ disabled: true,
+ handler: run_editor,
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Destroy'),
+ selModel: sm,
+ disabled: true,
+ handler: function() {
+ let rec = sm.getSelection()[0];
+ if (!rec || !rec.data.pool_name) {
+ return;
+ }
+ let poolName = rec.data.pool_name;
+ Ext.create('Proxmox.window.SafeDestroy', {
+ showProgress: true,
+ url: `/nodes/${nodename}/ceph/pool/${poolName}`,
+ params: {
+ remove_storages: 1,
+ },
+ item: {
+ type: 'CephPool',
+ id: poolName,
+ },
+ taskName: 'cephdestroypool',
+ autoShow: true,
+ listeners: {
+ destroy: () => rstore.load(),
+ },
+ });
+ },
+ },
+ ],
+ listeners: {
+ activate: () => rstore.startUpdate(),
+ destroy: () => rstore.stopUpdate(),
+ itemdblclick: run_editor,
+ },
+ });
+
+ me.callParent();
+ },
+}, function() {
+ Ext.define('ceph-pool-list', {
+ extend: 'Ext.data.Model',
+ fields: ['pool_name',
+ { name: 'pool', type: 'integer' },
+ { name: 'size', type: 'integer' },
+ { name: 'min_size', type: 'integer' },
+ { name: 'pg_num', type: 'integer' },
+ { name: 'pg_num_min', type: 'integer' },
+ { name: 'bytes_used', type: 'integer' },
+ { name: 'percent_used', type: 'number' },
+ { name: 'crush_rule', type: 'integer' },
+ { name: 'crush_rule_name', type: 'string' },
+ { name: 'pg_autoscale_mode', type: 'string' },
+ { name: 'pg_num_final', type: 'integer' },
+ { name: 'target_size_ratio', type: 'number' },
+ { name: 'target_size', type: 'integer' },
+ ],
+ idProperty: 'pool_name',
+ });
+});
+
+Ext.define('PVE.form.CephRuleSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveCephRuleSelector',
+
+ allowBlank: false,
+ valueField: 'name',
+ displayField: 'name',
+ editable: false,
+ queryMode: 'local',
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no nodename given";
+ }
+
+ me.originalAllowBlank = me.allowBlank;
+ me.allowBlank = true;
+
+ Ext.apply(me, {
+ store: {
+ fields: ['name'],
+ sorters: 'name',
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodename}/ceph/rules`,
+ },
+ autoLoad: {
+ callback: (records, op, success) => {
+ if (me.isCreate && success && records.length > 0) {
+ me.select(records[0]);
+ }
+
+ me.allowBlank = me.originalAllowBlank;
+ delete me.originalAllowBlank;
+ me.validate();
+ },
+ },
+ },
+ });
+
+ me.callParent();
+ },
+
+});
+Ext.define('PVE.CephCreateService', {
+ extend: 'Proxmox.window.Edit',
+ mixins: ['Proxmox.Mixin.CBind'],
+ xtype: 'pveCephCreateService',
+
+ method: 'POST',
+ isCreate: true,
+ showProgress: true,
+ width: 450,
+
+ setNode: function(node) {
+ let me = this;
+ me.nodename = node;
+ me.updateUrl();
+ },
+ setExtraID: function(extraID) {
+ let me = this;
+ me.extraID = me.type === 'mds' ? `-${extraID}` : '';
+ me.updateUrl();
+ },
+ updateUrl: function() {
+ let me = this;
+
+ let extraID = me.extraID ?? '';
+ let node = me.nodename;
+
+ me.url = `/nodes/${node}/ceph/${me.type}/${node}${extraID}`;
+ },
+
+ defaults: {
+ labelWidth: 75,
+ },
+ items: [
+ {
+ xtype: 'pveNodeSelector',
+ fieldLabel: gettext('Host'),
+ selectCurNode: true,
+ allowBlank: false,
+ submitValue: false,
+ listeners: {
+ change: function(f, value) {
+ let view = this.up('pveCephCreateService');
+ view.setNode(value);
+ },
+ },
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Extra ID'),
+ regex: /[a-zA-Z0-9]+/,
+ regexText: gettext('ID may only consist of alphanumeric characters'),
+ submitValue: false,
+ emptyText: Proxmox.Utils.NoneText,
+ cbind: {
+ disabled: get => get('type') !== 'mds',
+ hidden: get => get('type') !== 'mds',
+ },
+ listeners: {
+ change: function(f, value) {
+ let view = this.up('pveCephCreateService');
+ view.setExtraID(value);
+ },
+ },
+ },
+ {
+ xtype: 'component',
+ border: false,
+ padding: '5 2',
+ style: {
+ fontSize: '12px',
+ },
+ userCls: 'pmx-hint',
+ cbind: {
+ hidden: get => get('type') !== 'mds',
+ },
+ html: gettext('The Extra ID allows creating multiple MDS per node, which increases redundancy with more than one CephFS.'),
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ if (!me.type) {
+ throw "no type specified";
+ }
+ me.setNode(me.nodename);
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.node.CephServiceController', {
+ extend: 'Ext.app.ViewController',
+ alias: 'controller.CephServiceList',
+
+ render_status: (value, metadata, rec) => value,
+
+ render_version: function(value, metadata, rec) {
+ if (value === undefined) {
+ return '';
+ }
+ let view = this.getView();
+ let host = rec.data.host, nodev = [0];
+ if (view.nodeversions[host] !== undefined) {
+ nodev = view.nodeversions[host].version.parts;
+ }
+
+ let icon = '';
+ if (PVE.Utils.compare_ceph_versions(view.maxversion, nodev) > 0) {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE');
+ } else if (PVE.Utils.compare_ceph_versions(nodev, value) > 0) {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
+ } else if (view.mixedversions) {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK');
+ }
+ return icon + value;
+ },
+
+ getMaxVersions: function(store, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ let me = this;
+ let view = me.getView();
+
+ view.nodeversions = records[0].data.node;
+ view.maxversion = [];
+ view.mixedversions = false;
+ for (const [_nodename, data] of Object.entries(view.nodeversions)) {
+ let res = PVE.Utils.compare_ceph_versions(data.version.parts, view.maxversion);
+ if (res !== 0 && view.maxversion.length > 0) {
+ view.mixedversions = true;
+ }
+ if (res > 0) {
+ view.maxversion = data.version.parts;
+ }
+ }
+ },
+
+ init: function(view) {
+ if (view.pveSelNode) {
+ view.nodename = view.pveSelNode.data.node;
+ }
+ if (!view.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!view.type) {
+ throw "no type specified";
+ }
+
+ view.versionsstore = Ext.create('Proxmox.data.UpdateStore', {
+ autoStart: true,
+ interval: 10000,
+ storeid: `ceph-versions-${view.type}-list${view.nodename}`,
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/ceph/metadata?scope=versions",
+ },
+ });
+ view.versionsstore.on('load', this.getMaxVersions, this);
+ view.on('destroy', view.versionsstore.stopUpdate);
+
+ view.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ autoStart: true,
+ interval: 3000,
+ storeid: `ceph-${view.type}-list${view.nodename}`,
+ model: 'ceph-service-list',
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${view.nodename}/ceph/${view.type}`,
+ },
+ });
+
+ view.setStore(Ext.create('Proxmox.data.DiffStore', {
+ rstore: view.rstore,
+ sorters: [{ property: 'name' }],
+ }));
+
+ if (view.storeLoadCallback) {
+ view.rstore.on('load', view.storeLoadCallback, this);
+ }
+ view.on('destroy', view.rstore.stopUpdate);
+
+ if (view.showCephInstallMask) {
+ PVE.Utils.monitor_ceph_installed(view, view.rstore, view.nodename, true);
+ }
+ },
+
+ service_cmd: function(rec, cmd) {
+ let view = this.getView();
+ if (!rec.data.host) {
+ Ext.Msg.alert(gettext('Error'), "entry has no host");
+ return;
+ }
+ let doRequest = function() {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${rec.data.host}/ceph/${cmd}`,
+ method: 'POST',
+ params: { service: view.type + '.' + rec.data.name },
+ success: function(response, options) {
+ Ext.create('Proxmox.window.TaskProgress', {
+ autoShow: true,
+ upid: response.result.data,
+ taskDone: () => view.rstore.load(),
+ });
+ },
+ failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ };
+ if (cmd === "stop" && ['mon', 'mds'].includes(view.type)) {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${rec.data.host}/ceph/cmd-safety`,
+ params: {
+ service: view.type,
+ id: rec.data.name,
+ action: 'stop',
+ },
+ method: 'GET',
+ success: function({ result: { data } }) {
+ let stopText = {
+ mon: gettext('Stop MON'),
+ mds: gettext('Stop MDS'),
+ };
+ if (!data.safe) {
+ Ext.Msg.show({
+ title: gettext('Warning'),
+ message: data.status,
+ icon: Ext.Msg.WARNING,
+ buttons: Ext.Msg.OKCANCEL,
+ buttonText: { ok: stopText[view.type] },
+ fn: function(selection) {
+ if (selection === 'ok') {
+ doRequest();
+ }
+ },
+ });
+ } else {
+ doRequest();
+ }
+ },
+ failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ } else {
+ doRequest();
+ }
+ },
+ onChangeService: function(button) {
+ let me = this;
+ let record = me.getView().getSelection()[0];
+ me.service_cmd(record, button.action);
+ },
+
+ showSyslog: function() {
+ let view = this.getView();
+ let rec = view.getSelection()[0];
+ let service = `ceph-${view.type}@${rec.data.name}`;
+ Ext.create('Ext.window.Window', {
+ title: `${gettext('Syslog')}: ${service}`,
+ autoShow: true,
+ modal: true,
+ width: 800,
+ height: 400,
+ layout: 'fit',
+ items: [{
+ xtype: 'proxmoxLogView',
+ url: `/api2/extjs/nodes/${rec.data.host}/syslog?service=${encodeURIComponent(service)}`,
+ log_select_timespan: 1,
+ }],
+ });
+ },
+
+ onCreate: function() {
+ let view = this.getView();
+ Ext.create('PVE.CephCreateService', {
+ autoShow: true,
+ nodename: view.nodename,
+ subject: view.getTitle(),
+ type: view.type,
+ taskDone: () => view.rstore.load(),
+ });
+ },
+});
+
+Ext.define('PVE.node.CephServiceList', {
+ extend: 'Ext.grid.GridPanel',
+ xtype: 'pveNodeCephServiceList',
+
+ onlineHelp: 'chapter_pveceph',
+ emptyText: gettext('No such service configured.'),
+
+ stateful: true,
+
+ // will be called when the store loads
+ storeLoadCallback: Ext.emptyFn,
+
+ // if set to true, does shows the ceph install mask if needed
+ showCephInstallMask: false,
+
+ controller: 'CephServiceList',
+
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Start'),
+ iconCls: 'fa fa-play',
+ action: 'start',
+ disabled: true,
+ enableFn: rec => rec.data.state === 'stopped' || rec.data.state === 'unknown',
+ handler: 'onChangeService',
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Stop'),
+ iconCls: 'fa fa-stop',
+ action: 'stop',
+ enableFn: rec => rec.data.state !== 'stopped',
+ disabled: true,
+ handler: 'onChangeService',
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Restart'),
+ iconCls: 'fa fa-refresh',
+ action: 'restart',
+ disabled: true,
+ enableFn: rec => rec.data.state !== 'stopped',
+ handler: 'onChangeService',
+ },
+ '-',
+ {
+ text: gettext('Create'),
+ reference: 'createButton',
+ handler: 'onCreate',
+ },
+ {
+ text: gettext('Destroy'),
+ xtype: 'proxmoxStdRemoveButton',
+ getUrl: function(rec) {
+ let view = this.up('grid');
+ if (!rec.data.host) {
+ Ext.Msg.alert(gettext('Error'), "entry has no host, cannot build API url");
+ return '';
+ }
+ return `/nodes/${rec.data.host}/ceph/${view.type}/${rec.data.name}`;
+ },
+ callback: function(options, success, response) {
+ let view = this.up('grid');
+ if (!success) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ return;
+ }
+ Ext.create('Proxmox.window.TaskProgress', {
+ autoShow: true,
+ upid: response.result.data,
+ taskDone: () => view.rstore.load(),
+ });
+ },
+ handler: function(btn, event, rec) {
+ let me = this;
+ let view = me.up('grid');
+ let doRequest = function() {
+ Proxmox.button.StdRemoveButton.prototype.handler.call(me, btn, event, rec);
+ };
+ if (view.type === 'mon') {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${rec.data.host}/ceph/cmd-safety`,
+ params: {
+ service: view.type,
+ id: rec.data.name,
+ action: 'destroy',
+ },
+ method: 'GET',
+ success: function({ result: { data } }) {
+ if (!data.safe) {
+ Ext.Msg.show({
+ title: gettext('Warning'),
+ message: data.status,
+ icon: Ext.Msg.WARNING,
+ buttons: Ext.Msg.OKCANCEL,
+ buttonText: { ok: gettext('Destroy MON') },
+ fn: function(selection) {
+ if (selection === 'ok') {
+ doRequest();
+ }
+ },
+ });
+ } else {
+ doRequest();
+ }
+ },
+ failure: (response, _opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ } else {
+ doRequest();
+ }
+ },
+
+ },
+ '-',
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Syslog'),
+ disabled: true,
+ handler: 'showSyslog',
+ },
+ ],
+
+ columns: [
+ {
+ header: gettext('Name'),
+ flex: 1,
+ sortable: true,
+ renderer: function(v) {
+ return this.type + '.' + v;
+ },
+ dataIndex: 'name',
+ },
+ {
+ header: gettext('Host'),
+ flex: 1,
+ sortable: true,
+ renderer: function(v) {
+ return v || Proxmox.Utils.unknownText;
+ },
+ dataIndex: 'host',
+ },
+ {
+ header: gettext('Status'),
+ flex: 1,
+ sortable: false,
+ renderer: 'render_status',
+ dataIndex: 'state',
+ },
+ {
+ header: gettext('Address'),
+ flex: 3,
+ sortable: true,
+ renderer: function(v) {
+ return v || Proxmox.Utils.unknownText;
+ },
+ dataIndex: 'addr',
+ },
+ {
+ header: gettext('Version'),
+ flex: 3,
+ sortable: true,
+ dataIndex: 'version',
+ renderer: 'render_version',
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (me.additionalColumns) {
+ me.columns = me.columns.concat(me.additionalColumns);
+ }
+
+ me.callParent();
+ },
+
+}, function() {
+ Ext.define('ceph-service-list', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'addr',
+ 'name',
+ 'fs_name',
+ 'rank',
+ 'host',
+ 'quorum',
+ 'state',
+ 'ceph_version',
+ 'ceph_version_short',
+ {
+ type: 'string',
+ name: 'version',
+ calculate: data => PVE.Utils.parse_ceph_version(data),
+ },
+ ],
+ idProperty: 'name',
+ });
+});
+
+Ext.define('PVE.node.CephMDSServiceController', {
+ extend: 'PVE.node.CephServiceController',
+ alias: 'controller.CephServiceMDSList',
+
+ render_status: (value, mD, rec) => rec.data.fs_name ? `${value} (${rec.data.fs_name})` : value,
+});
+
+Ext.define('PVE.node.CephMDSList', {
+ extend: 'PVE.node.CephServiceList',
+ xtype: 'pveNodeCephMDSList',
+
+ controller: {
+ type: 'CephServiceMDSList',
+ },
+});
+
+Ext.define('PVE.ceph.Services', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveCephServices',
+
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+
+ bodyPadding: '0 5 20',
+ defaults: {
+ xtype: 'box',
+ style: {
+ 'text-align': 'center',
+ },
+ },
+
+ items: [
+ {
+ flex: 1,
+ xtype: 'pveCephServiceList',
+ itemId: 'mons',
+ title: gettext('Monitors'),
+ },
+ {
+ flex: 1,
+ xtype: 'pveCephServiceList',
+ itemId: 'mgrs',
+ title: gettext('Managers'),
+ },
+ {
+ flex: 1,
+ xtype: 'pveCephServiceList',
+ itemId: 'mdss',
+ title: gettext('Meta Data Servers'),
+ },
+ ],
+
+ updateAll: function(metadata, status) {
+ var me = this;
+
+ const healthstates = {
+ 'HEALTH_UNKNOWN': 0,
+ 'HEALTH_ERR': 1,
+ 'HEALTH_WARN': 2,
+ 'HEALTH_UPGRADE': 3,
+ 'HEALTH_OLD': 4,
+ 'HEALTH_OK': 5,
+ };
+ // order guarantee since es2020, but browsers did so before. Note, integers would break it.
+ const healthmap = Object.keys(healthstates);
+ let maxversion = "00.0.00";
+ Object.values(metadata.node || {}).forEach(function(node) {
+ if (PVE.Utils.compare_ceph_versions(node?.version?.parts, maxversion) > 0) {
+ maxversion = node?.version?.parts;
+ }
+ });
+ var quorummap = status && status.quorum_names ? status.quorum_names : [];
+ let monmessages = {}, mgrmessages = {}, mdsmessages = {};
+ if (status) {
+ if (status.health) {
+ Ext.Object.each(status.health.checks, function(key, value, _obj) {
+ if (!Ext.String.startsWith(key, "MON_")) {
+ return;
+ }
+ for (let i = 0; i < value.detail.length; i++) {
+ let match = value.detail[i].message.match(/mon.([a-zA-Z0-9\-.]+)/);
+ if (!match) {
+ continue;
+ }
+ let monid = match[1];
+ if (!monmessages[monid]) {
+ monmessages[monid] = {
+ worstSeverity: healthstates.HEALTH_OK,
+ messages: [],
+ };
+ }
+
+ let severityIcon = PVE.Utils.get_ceph_icon_html(value.severity, true);
+ let details = value.detail.reduce((acc, v) => `${acc}\n${v.message}`, '');
+ monmessages[monid].messages.push(severityIcon + details);
+
+ if (healthstates[value.severity] < monmessages[monid].worstSeverity) {
+ monmessages[monid].worstSeverity = healthstates[value.severity];
+ }
+ }
+ });
+ }
+
+ if (status.mgrmap) {
+ mgrmessages[status.mgrmap.active_name] = "active";
+ status.mgrmap.standbys.forEach(function(mgr) {
+ mgrmessages[mgr.name] = "standby";
+ });
+ }
+
+ if (status.fsmap) {
+ status.fsmap.by_rank.forEach(function(mds) {
+ mdsmessages[mds.name] = 'rank: ' + mds.rank + "; " + mds.status;
+ });
+ }
+ }
+
+ let checks = {
+ mon: function(mon) {
+ if (quorummap.indexOf(mon.name) !== -1) {
+ mon.health = healthstates.HEALTH_OK;
+ } else {
+ mon.health = healthstates.HEALTH_ERR;
+ }
+ if (monmessages[mon.name]) {
+ if (monmessages[mon.name].worstSeverity < mon.health) {
+ mon.health = monmessages[mon.name].worstSeverity;
+ }
+ Array.prototype.push.apply(mon.messages, monmessages[mon.name].messages);
+ }
+ return mon;
+ },
+ mgr: function(mgr) {
+ if (mgrmessages[mgr.name] === 'active') {
+ mgr.title = '' + mgr.title + '';
+ mgr.statuses.push(gettext('Status') + ': active');
+ } else if (mgrmessages[mgr.name] === 'standby') {
+ mgr.statuses.push(gettext('Status') + ': standby');
+ } else if (mgr.health > healthstates.HEALTH_WARN) {
+ mgr.health = healthstates.HEALTH_WARN;
+ }
+
+ return mgr;
+ },
+ mds: function(mds) {
+ if (mdsmessages[mds.name]) {
+ mds.title = '' + mds.title + '';
+ mds.statuses.push(gettext('Status') + ': ' + mdsmessages[mds.name]+"");
+ } else if (mds.addr !== Proxmox.Utils.unknownText) {
+ mds.statuses.push(gettext('Status') + ': standby');
+ }
+
+ return mds;
+ },
+ };
+
+ for (let type of ['mon', 'mgr', 'mds']) {
+ var ids = Object.keys(metadata[type] || {});
+ me[type] = {};
+
+ for (let id of ids) {
+ const [name, host] = id.split('@');
+ let result = {
+ id: id,
+ health: healthstates.HEALTH_OK,
+ statuses: [],
+ messages: [],
+ name: name,
+ title: metadata[type][id].name || name,
+ host: host,
+ version: PVE.Utils.parse_ceph_version(metadata[type][id]),
+ service: metadata[type][id].service,
+ addr: metadata[type][id].addr || metadata[type][id].addrs || Proxmox.Utils.unknownText,
+ };
+
+ result.statuses = [
+ gettext('Host') + ": " + host,
+ gettext('Address') + ": " + result.addr,
+ ];
+
+ if (checks[type]) {
+ result = checks[type](result);
+ }
+
+ if (result.service && !result.version) {
+ result.messages.push(
+ PVE.Utils.get_ceph_icon_html('HEALTH_UNKNOWN', true) +
+ gettext('Stopped'),
+ );
+ result.health = healthstates.HEALTH_UNKNOWN;
+ }
+
+ if (!result.version && result.addr === Proxmox.Utils.unknownText) {
+ result.health = healthstates.HEALTH_UNKNOWN;
+ }
+
+ if (result.version) {
+ result.statuses.push(gettext('Version') + ": " + result.version);
+
+ if (PVE.Utils.compare_ceph_versions(result.version, maxversion) !== 0) {
+ let host_version = metadata.node[host]?.version?.parts || metadata.version?.[host] || "";
+ if (PVE.Utils.compare_ceph_versions(host_version, maxversion) === 0) {
+ if (result.health > healthstates.HEALTH_OLD) {
+ result.health = healthstates.HEALTH_OLD;
+ }
+ result.messages.push(
+ PVE.Utils.get_ceph_icon_html('HEALTH_OLD', true) +
+ gettext('A newer version was installed but old version still running, please restart'),
+ );
+ } else {
+ if (result.health > healthstates.HEALTH_UPGRADE) {
+ result.health = healthstates.HEALTH_UPGRADE;
+ }
+ result.messages.push(
+ PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE', true) +
+ gettext('Other cluster members use a newer version of this service, please upgrade and restart'),
+ );
+ }
+ }
+ }
+
+ result.statuses.push(''); // empty line
+ result.text = result.statuses.concat(result.messages).join('
');
+
+ result.health = healthmap[result.health];
+
+ me[type][id] = result;
+ }
+ }
+
+ me.getComponent('mons').updateAll(Object.values(me.mon));
+ me.getComponent('mgrs').updateAll(Object.values(me.mgr));
+ me.getComponent('mdss').updateAll(Object.values(me.mds));
+ },
+});
+
+Ext.define('PVE.ceph.ServiceList', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveCephServiceList',
+
+ style: {
+ 'text-align': 'center',
+ },
+ defaults: {
+ xtype: 'box',
+ style: {
+ 'text-align': 'center',
+ },
+ },
+
+ items: [
+ {
+ itemId: 'title',
+ data: {
+ title: '',
+ },
+ tpl: '{title}
',
+ },
+ ],
+
+ updateAll: function(list) {
+ var me = this;
+ me.suspendLayout = true;
+
+ list.sort((a, b) => a.id > b.id ? 1 : a.id < b.id ? -1 : 0);
+ if (!me.ids) {
+ me.ids = [];
+ }
+ let pendingRemoval = {};
+ me.ids.forEach(id => { pendingRemoval[id] = true; }); // mark all as to-remove first here
+
+ for (let i = 0; i < list.length; i++) {
+ let service = me.getComponent(list[i].id);
+ if (!service) {
+ // services and list are sorted, so just insert at i + 1 (first el. is the title)
+ service = me.insert(i + 1, {
+ xtype: 'pveCephServiceWidget',
+ itemId: list[i].id,
+ });
+ me.ids.push(list[i].id);
+ } else {
+ delete pendingRemoval[list[i].id]; // drop exisiting from for-removal
+ }
+ service.updateService(list[i].title, list[i].text, list[i].health);
+ }
+ Object.keys(pendingRemoval).forEach(id => me.remove(id)); // GC
+
+ me.suspendLayout = false;
+ me.updateLayout();
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+ me.getComponent('title').update({
+ title: me.title,
+ });
+ },
+});
+
+Ext.define('PVE.ceph.ServiceWidget', {
+ extend: 'Ext.Component',
+ alias: 'widget.pveCephServiceWidget',
+
+ userCls: 'monitor inline-block',
+ data: {
+ title: '0',
+ health: 'HEALTH_ERR',
+ text: '',
+ iconCls: PVE.Utils.get_health_icon(),
+ },
+
+ tpl: [
+ '{title}: ',
+ '',
+ ],
+
+ updateService: function(title, text, health) {
+ var me = this;
+
+ me.update(Ext.apply(me.data, {
+ health: health,
+ text: text,
+ title: title,
+ iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health]),
+ }));
+
+ if (me.tooltip) {
+ me.tooltip.setHtml(text);
+ }
+ },
+
+ listeners: {
+ destroy: function() {
+ let me = this;
+ if (me.tooltip) {
+ me.tooltip.destroy();
+ delete me.tooltip;
+ }
+ },
+ mouseenter: {
+ element: 'el',
+ fn: function(events, element) {
+ let view = this.component;
+ if (!view) {
+ return;
+ }
+ if (!view.tooltip || view.data.text !== view.tooltip.html) {
+ view.tooltip = Ext.create('Ext.tip.ToolTip', {
+ target: view.el,
+ trackMouse: true,
+ dismissDelay: 0,
+ renderTo: Ext.getBody(),
+ html: view.data.text,
+ });
+ }
+ view.tooltip.show();
+ },
+ },
+ mouseleave: {
+ element: 'el',
+ fn: function(events, element) {
+ let view = this.component;
+ if (view.tooltip) {
+ view.tooltip.destroy();
+ delete view.tooltip;
+ }
+ },
+ },
+ },
+});
+Ext.define('PVE.node.CephStatus', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeCephStatus',
+
+ onlineHelp: 'chapter_pveceph',
+
+ scrollable: true,
+ bodyPadding: 5,
+ layout: {
+ type: 'column',
+ },
+
+ defaults: {
+ padding: 5,
+ },
+
+ items: [
+ {
+ xtype: 'panel',
+ title: gettext('Health'),
+ bodyPadding: 10,
+ plugins: 'responsive',
+ responsiveConfig: {
+ 'width < 1600': {
+ minHeight: 230,
+ columnWidth: 1,
+ },
+ 'width >= 1600': {
+ minHeight: 500,
+ columnWidth: 0.5,
+ },
+ },
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+ items: [
+ {
+ xtype: 'container',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ flex: 1,
+ items: [
+ {
+
+ xtype: 'pveHealthWidget',
+ itemId: 'overallhealth',
+ flex: 1,
+ title: gettext('Status'),
+ },
+ {
+ xtype: 'displayfield',
+ itemId: 'versioninfo',
+ fieldLabel: gettext('Ceph Version'),
+ value: "",
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('The newest version installed in the Cluster.'),
+ },
+ padding: '10 0 0 0',
+ style: {
+ 'text-align': 'center',
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'grid',
+ itemId: 'warnings',
+ flex: 2,
+ stateful: true,
+ stateId: 'ceph-status-warnings',
+ // we load the store manually, to show an emptyText specify an empty intermediate store
+ store: {
+ trackRemoved: false,
+ data: [],
+ },
+ updateHealth: function(health) {
+ let checks = health.checks || {};
+
+ let checkRecords = Object.keys(checks).sort().map(key => {
+ let check = checks[key];
+ return {
+ id: key,
+ summary: check.summary.message,
+ detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, ''),
+ severity: check.severity,
+ };
+ });
+
+ this.getStore().loadRawData(checkRecords, false);
+ },
+ emptyText: gettext('No Warnings/Errors'),
+ columns: [
+ {
+ dataIndex: 'severity',
+ header: gettext('Severity'),
+ align: 'center',
+ width: 70,
+ renderer: function(value) {
+ let health = PVE.Utils.map_ceph_health[value];
+ let icon = PVE.Utils.get_health_icon(health);
+ return ``;
+ },
+ sorter: {
+ sorterFn: function(a, b) {
+ let health = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK'];
+ return health.indexOf(b.data.severity) - health.indexOf(a.data.severity);
+ },
+ },
+ },
+ {
+ dataIndex: 'summary',
+ header: gettext('Summary'),
+ 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 < 1600': {
+ columnWidth: 1,
+ minHeight: 250,
+ },
+ 'width >= 1600': {
+ columnWidth: 0.5,
+ minHeight: 300,
+ },
+ },
+ title: gettext('Status'),
+ },
+ {
+ xtype: 'pveCephServices',
+ title: gettext('Services'),
+ itemId: 'services',
+ plugins: 'responsive',
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+ responsiveConfig: {
+ 'width < 1600': {
+ columnWidth: 1,
+ minHeight: 200,
+ },
+ 'width >= 1600': {
+ columnWidth: 0.5,
+ minHeight: 200,
+ },
+ },
+ },
+ {
+ xtype: 'panel',
+ title: gettext('Performance'),
+ columnWidth: 1,
+ bodyPadding: 5,
+ layout: {
+ type: 'hbox',
+ align: 'center',
+ },
+ items: [
+ {
+ xtype: 'container',
+ flex: 1,
+ items: [
+ {
+ xtype: 'proxmoxGauge',
+ itemId: 'space',
+ title: gettext('Usage'),
+ },
+ {
+ flex: 1,
+ border: false,
+ },
+ {
+ xtype: 'container',
+ itemId: 'recovery',
+ hidden: true,
+ padding: 25,
+ items: [
+ {
+ xtype: 'pveRunningChart',
+ itemId: 'recoverychart',
+ title: gettext('Recovery') +'/ '+ gettext('Rebalance'),
+ renderer: PVE.Utils.render_bandwidth,
+ height: 100,
+ },
+ {
+ xtype: 'progressbar',
+ itemId: 'recoveryprogress',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ xtype: 'container',
+ flex: 2,
+ defaults: {
+ padding: 0,
+ height: 100,
+ },
+ items: [
+ {
+ xtype: 'pveRunningChart',
+ itemId: 'reads',
+ title: gettext('Reads'),
+ renderer: PVE.Utils.render_bandwidth,
+ },
+ {
+ xtype: 'pveRunningChart',
+ itemId: 'writes',
+ title: gettext('Writes'),
+ renderer: PVE.Utils.render_bandwidth,
+ },
+ {
+ xtype: 'pveRunningChart',
+ itemId: 'readiops',
+ title: 'IOPS: ' + gettext('Reads'),
+ renderer: Ext.util.Format.numberRenderer('0,000'),
+ },
+ {
+ xtype: 'pveRunningChart',
+ itemId: 'writeiops',
+ title: 'IOPS: ' + gettext('Writes'),
+ renderer: Ext.util.Format.numberRenderer('0,000'),
+ },
+ ],
+ },
+ ],
+ },
+ ],
+
+ updateAll: function(store, records, success) {
+ if (!success || records.length === 0) {
+ return;
+ }
+
+ var me = this;
+ var rec = records[0];
+ me.status = rec.data;
+
+ // add health panel
+ me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
+ me.down('#warnings').updateHealth(rec.data.health || {}); // add errors to gridstore
+
+ me.getComponent('services').updateAll(me.metadata || {}, rec.data);
+
+ me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
+
+ // add performance data
+ let pgmap = rec.data.pgmap;
+ let used = pgmap.bytes_used;
+ let total = pgmap.bytes_total;
+
+ var text = Ext.String.format(gettext('{0} of {1}'),
+ Proxmox.Utils.render_size(used),
+ Proxmox.Utils.render_size(total),
+ );
+
+ // update the usage widget
+ me.down('#space').updateValue(used/total, text);
+
+ let readiops = pgmap.read_op_per_sec;
+ let writeiops = pgmap.write_op_per_sec;
+ let reads = pgmap.read_bytes_sec || 0;
+ let writes = pgmap.write_bytes_sec || 0;
+
+ // update the graphs
+ me.reads.addDataPoint(reads);
+ me.writes.addDataPoint(writes);
+ me.readiops.addDataPoint(readiops);
+ me.writeiops.addDataPoint(writeiops);
+
+ let degraded = pgmap.degraded_objects || 0;
+ let misplaced = pgmap.misplaced_objects || 0;
+ let unfound = pgmap.unfound_objects || 0;
+ let unhealthy = degraded + unfound + misplaced;
+ // update recovery
+ if (pgmap.recovering_objects_per_sec !== undefined || unhealthy > 0) {
+ let toRecoverObjects = pgmap.misplaced_total || pgmap.unfound_total || pgmap.degraded_total || 0;
+ if (toRecoverObjects === 0) {
+ return; // FIXME: unexpected return and leaves things possible visible when it shouldn't?
+ }
+ let recovered = toRecoverObjects - unhealthy || 0;
+ let speed = pgmap.recovering_bytes_per_sec || 0;
+
+ let recoveryRatio = recovered / toRecoverObjects;
+ let txt = `${(recoveryRatio * 100).toFixed(2)}%`;
+ if (speed > 0) {
+ let obj_per_sec = speed / (4 * 1024 * 1024); // 4 MiB per Object
+ let duration = Proxmox.Utils.format_duration_human(unhealthy/obj_per_sec);
+ let speedTxt = PVE.Utils.render_bandwidth(speed);
+ txt += ` (${speedTxt} - ${duration} left)`;
+ }
+
+ me.down('#recovery').setVisible(true);
+ me.down('#recoveryprogress').updateValue(recoveryRatio);
+ me.down('#recoveryprogress').updateText(txt);
+ me.down('#recoverychart').addDataPoint(speed);
+ } else {
+ me.down('#recovery').setVisible(false);
+ me.down('#recoverychart').addDataPoint(0);
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+
+ me.callParent();
+ var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
+ me.store = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-status-' + (nodename || 'cluster'),
+ interval: 5000,
+ proxy: {
+ type: 'proxmox',
+ url: baseurl + '/status',
+ },
+ });
+
+ me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'ceph-metadata-' + (nodename || 'cluster'),
+ interval: 15*1000,
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/ceph/metadata',
+ },
+ });
+
+ // save references for the updatefunction
+ me.iops = me.down('#iops');
+ me.readiops = me.down('#readiops');
+ me.writeiops = me.down('#writeiops');
+ me.reads = me.down('#reads');
+ me.writes = me.down('#writes');
+
+ // manages the "install ceph?" overlay
+ PVE.Utils.monitor_ceph_installed(me, me.store, nodename);
+
+ me.mon(me.store, 'load', me.updateAll, me);
+ me.mon(me.metadatastore, 'load', function(store, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ me.metadata = records[0].data;
+
+ // update services
+ me.getComponent('services').updateAll(me.metadata, me.status || {});
+
+ // update detailstatus panel
+ me.getComponent('statusdetail').updateAll(me.metadata, me.status || {});
+
+ let maxversion = [];
+ let maxversiontext = "";
+ for (const [_nodename, data] of Object.entries(me.metadata.node)) {
+ let version = data.version.parts;
+ if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
+ maxversion = version;
+ maxversiontext = data.version.str;
+ }
+ }
+ me.down('#versioninfo').setValue(maxversiontext);
+ }, me);
+
+ me.on('destroy', me.store.stopUpdate);
+ me.on('destroy', me.metadatastore.stopUpdate);
+ me.store.startUpdate();
+ me.metadatastore.startUpdate();
+ },
+
+});
+Ext.define('PVE.ceph.StatusDetail', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveCephStatusDetail',
+
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+
+ bodyPadding: '0 5',
+ defaults: {
+ xtype: 'box',
+ style: {
+ 'text-align': 'center',
+ },
+ },
+
+ items: [{
+ flex: 1,
+ itemId: 'osds',
+ maxHeight: 250,
+ scrollable: true,
+ padding: '0 10 5 10',
+ data: {
+ total: 0,
+ upin: 0,
+ upout: 0,
+ downin: 0,
+ downout: 0,
+ oldOSD: [],
+ ghostOSD: [],
+ },
+ tpl: [
+ 'OSDs
',
+ '',
+ ' | ',
+ '',
+ gettext('In'),
+ ' | ',
+ '',
+ gettext('Out'),
+ ' | ',
+ '
',
+ '',
+ '| ',
+ gettext('Up'),
+ ' | ',
+ '{upin} | ',
+ '{upout} | ',
+ '
',
+ '',
+ '| ',
+ gettext('Down'),
+ ' | ',
+ '{downin} | ',
+ '{downout} | ',
+ '
',
+ '
',
+ '
',
+ gettext('Total'),
+ ': {total}',
+ '
',
+ '',
+ ' ' + gettext('Outdated OSDs') + "
",
+ '',
+ '
',
+ 'osd.{id}:
',
+ '{version}
',
+ '',
+ '',
+ '
',
+ '',
+ '',
+ '',
+ '
',
+ ` ${gettext('Ghost OSDs')}
`,
+ ``,
+ '
',
+ 'osd.{id}
',
+ '',
+ '',
+ '
',
+ '',
+ ],
+ },
+ {
+ flex: 1,
+ border: false,
+ itemId: 'pgchart',
+ xtype: 'polar',
+ height: 184,
+ innerPadding: 5,
+ insetPadding: 5,
+ colors: [
+ '#CFCFCF',
+ '#21BF4B',
+ '#FFCC00',
+ '#FF6C59',
+ ],
+ store: { },
+ series: [
+ {
+ type: 'pie',
+ donut: 60,
+ angleField: 'count',
+ tooltip: {
+ trackMouse: true,
+ renderer: function(tooltip, record, ctx) {
+ var html = record.get('text');
+ html += '
';
+ record.get('states').forEach(function(state) {
+ html += '
' +
+ state.state_name + ': ' + state.count.toString();
+ });
+ tooltip.setHtml(html);
+ },
+ },
+ subStyle: {
+ strokeStyle: false,
+ },
+ },
+ ],
+ },
+ {
+ flex: 1.6,
+ itemId: 'pgs',
+ padding: '0 10',
+ maxHeight: 250,
+ scrollable: true,
+ data: {
+ states: [],
+ },
+ tpl: [
+ 'PGs
',
+ '',
+ ' {state_name}:
',
+ '{count}
',
+ '',
+ '',
+ ],
+ }],
+
+ // similar to mgr dashboard
+ pgstates: {
+ // clean
+ clean: 1,
+ active: 1,
+
+ // working
+ activating: 2,
+ backfill_wait: 2,
+ backfilling: 2,
+ creating: 2,
+ deep: 2,
+ degraded: 2,
+ forced_backfill: 2,
+ forced_recovery: 2,
+ peered: 2,
+ peering: 2,
+ recovering: 2,
+ recovery_wait: 2,
+ remapped: 2,
+ repair: 2,
+ scrubbing: 2,
+ snaptrim: 2,
+ snaptrim_wait: 2,
+
+ // error
+ backfill_toofull: 3,
+ backfill_unfound: 3,
+ down: 3,
+ incomplete: 3,
+ inconsistent: 3,
+ recovery_toofull: 3,
+ recovery_unfound: 3,
+ snaptrim_error: 3,
+ stale: 3,
+ undersized: 3,
+ },
+
+ statecategories: [
+ {
+ text: gettext('Unknown'),
+ count: 0,
+ states: [],
+ cls: 'faded',
+ },
+ {
+ text: gettext('Clean'),
+ cls: 'good',
+ },
+ {
+ text: gettext('Working'),
+ cls: 'warning',
+ },
+ {
+ text: gettext('Error'),
+ cls: 'critical',
+ },
+ ],
+
+ checkThemeColors: function() {
+ let me = this;
+ let rootStyle = getComputedStyle(document.documentElement);
+
+ // get color
+ let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
+
+ // set the colors
+ me.chart.setBackground(background);
+ me.chart.redraw();
+ },
+
+ updateAll: function(metadata, status) {
+ let me = this;
+ me.suspendLayout = true;
+
+ let maxversion = "0";
+ Object.values(metadata.node || {}).forEach(function(node) {
+ if (PVE.Utils.compare_ceph_versions(node?.version?.parts, maxversion) > 0) {
+ maxversion = node.version.parts;
+ }
+ });
+
+ let oldOSD = [], ghostOSD = [];
+ metadata.osd?.forEach(osd => {
+ let version = PVE.Utils.parse_ceph_version(osd);
+ if (version !== undefined) {
+ if (PVE.Utils.compare_ceph_versions(version, maxversion) !== 0) {
+ oldOSD.push({
+ id: osd.id,
+ version: version,
+ });
+ }
+ } else {
+ if (Object.keys(osd).length > 1) {
+ console.warn('got OSD entry with no valid version but other keys', osd);
+ }
+ ghostOSD.push({
+ id: osd.id,
+ });
+ }
+ });
+
+ // update PGs sorted
+ let pgmap = status.pgmap || {};
+ let pgs_by_state = pgmap.pgs_by_state || [];
+ pgs_by_state.sort(function(a, b) {
+ return a.state_name < b.state_name?-1:a.state_name === b.state_name?0:1;
+ });
+
+ me.statecategories.forEach(function(cat) {
+ cat.count = 0;
+ cat.states = [];
+ });
+
+ pgs_by_state.forEach(function(state) {
+ let states = state.state_name.split(/[^a-z]+/);
+ let result = 0;
+ for (let i = 0; i < states.length; i++) {
+ if (me.pgstates[states[i]] > result) {
+ result = me.pgstates[states[i]];
+ }
+ }
+ // for the list
+ state.cls = me.statecategories[result].cls;
+
+ me.statecategories[result].count += state.count;
+ me.statecategories[result].states.push(state);
+ });
+
+ me.chart.getStore().setData(me.statecategories);
+ me.getComponent('pgs').update({ states: pgs_by_state });
+
+ let health = status.health || {};
+ // we collect monitor/osd information from the checks
+ const downinregex = /(\d+) osds down/;
+ let downin_osds = 0;
+ Ext.Object.each(health.checks, function(key, value, obj) {
+ var found = null;
+ if (key === 'OSD_DOWN') {
+ found = value.summary.message.match(downinregex);
+ if (found !== null) {
+ downin_osds = parseInt(found[1], 10);
+ }
+ }
+ });
+
+ let osdmap = status.osdmap || {};
+ if (typeof osdmap.osdmap !== "undefined") {
+ osdmap = osdmap.osdmap;
+ }
+ // update OSDs counts
+ let total_osds = osdmap.num_osds || 0;
+ let in_osds = osdmap.num_in_osds || 0;
+ let up_osds = osdmap.num_up_osds || 0;
+ let down_osds = total_osds - up_osds;
+
+ let downout_osds = down_osds - downin_osds;
+ let upin_osds = in_osds - downin_osds;
+ let upout_osds = up_osds - upin_osds;
+
+ let osds = {
+ total: total_osds,
+ upin: upin_osds,
+ upout: upout_osds,
+ downin: downin_osds,
+ downout: downout_osds,
+ oldOSD: oldOSD,
+ ghostOSD,
+ };
+ let osdcomponent = me.getComponent('osds');
+ osdcomponent.update(Ext.apply(osdcomponent.data, osds));
+
+ me.suspendLayout = false;
+ me.updateLayout();
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+
+ me.chart = me.getComponent('pgchart');
+ me.checkThemeColors();
+
+ // switch colors on media query changes
+ me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
+ me.themeListener = (e) => { me.checkThemeColors(); };
+ me.mediaQueryList.addEventListener("change", me.themeListener);
+ },
+
+ doDestroy: function() {
+ let me = this;
+
+ me.mediaQueryList.removeEventListener("change", me.themeListener);
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.node.ACMEAccountCreate', {
+ extend: 'Proxmox.window.Edit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 450,
+ title: gettext('Register Account'),
+ isCreate: true,
+ method: 'POST',
+ submitText: gettext('Register'),
+ url: '/cluster/acme/account',
+ showTaskViewer: true,
+ defaultExists: false,
+
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ fieldLabel: gettext('Account Name'),
+ name: 'name',
+ cbind: {
+ emptyText: (get) => get('defaultExists') ? '' : 'default',
+ allowBlank: (get) => !get('defaultExists'),
+ },
+ },
+ {
+ xtype: 'textfield',
+ name: 'contact',
+ vtype: 'email',
+ allowBlank: false,
+ fieldLabel: gettext('E-Mail'),
+ },
+ {
+ xtype: 'proxmoxComboGrid',
+ 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',
+ direction: '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);
+ checkbox.setHidden(true);
+
+ Proxmox.Utils.API2Request({
+ url: '/cluster/acme/tos',
+ method: 'GET',
+ params: {
+ directory: value,
+ },
+ success: function(response, opt) {
+ field.setValue(response.result.data);
+ disp.setValue(response.result.data);
+ checkbox.setHidden(false);
+ },
+ failure: function(response, opt) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+ },
+ },
+ {
+ xtype: 'displayfield',
+ itemId: 'tos_url_display',
+ renderer: PVE.Utils.render_optional_url,
+ name: 'tos_url_display',
+ },
+ {
+ xtype: 'hidden',
+ itemId: 'tos_url',
+ name: 'tos_url',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ itemId: 'tos_checkbox',
+ boxLabel: gettext('Accept TOS'),
+ submitValue: false,
+ validateValue: function(value) {
+ if (value && this.checked) {
+ return true;
+ }
+ return false;
+ },
+ },
+ ],
+
+});
+
+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.ACMEDomainEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveACMEDomainEdit',
+
+ subject: gettext('Domain'),
+ isCreate: false,
+ width: 450,
+ onlineHelp: 'sysadmin_certificate_management',
+
+ items: [
+ {
+ xtype: 'inputpanel',
+ onGetValues: function(values) {
+ let me = this;
+ let win = me.up('pveACMEDomainEdit');
+ let nodeconfig = win.nodeconfig;
+ let olddomain = win.domain || {};
+
+ let params = {
+ digest: nodeconfig.digest,
+ };
+
+ let configkey = olddomain.configkey;
+ let acmeObj = PVE.Parser.parseACME(nodeconfig.acme);
+
+ if (values.type === 'dns') {
+ if (!olddomain.configkey || olddomain.configkey === 'acme') {
+ // look for first free slot
+ for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
+ if (nodeconfig[`acmedomain${i}`] === undefined) {
+ configkey = `acmedomain${i}`;
+ break;
+ }
+ }
+ if (olddomain.domain) {
+ // we have to remove the domain from the acme domainlist
+ PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain);
+ params.acme = PVE.Parser.printACME(acmeObj);
+ }
+ }
+
+ delete values.type;
+ params[configkey] = PVE.Parser.printPropertyString(values, 'domain');
+ } else {
+ if (olddomain.configkey && olddomain.configkey !== 'acme') {
+ // delete the old dns entry
+ params.delete = [olddomain.configkey];
+ }
+
+ // add new, remove old and make entries unique
+ PVE.Utils.add_domain_to_acme(acmeObj, values.domain);
+ PVE.Utils.remove_domain_from_acme(acmeObj, olddomain.domain);
+ params.acme = PVE.Parser.printACME(acmeObj);
+ }
+
+ return params;
+ },
+ items: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'type',
+ fieldLabel: gettext('Challenge Type'),
+ allowBlank: false,
+ value: 'standalone',
+ comboItems: [
+ ['standalone', 'HTTP'],
+ ['dns', 'DNS'],
+ ],
+ validator: function(value) {
+ let me = this;
+ let win = me.up('pveACMEDomainEdit');
+ let oldconfigkey = win.domain ? win.domain.configkey : undefined;
+ let val = me.getValue();
+ if (val === 'dns' && (!oldconfigkey || oldconfigkey === 'acme')) {
+ // we have to check if there is a 'acmedomain' slot left
+ let found = false;
+ for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
+ if (!win.nodeconfig[`acmedomain${i}`]) {
+ found = true;
+ }
+ }
+ if (!found) {
+ return gettext('Only 5 Domains with type DNS can be configured');
+ }
+ }
+
+ return true;
+ },
+ listeners: {
+ change: function(cb, value) {
+ let me = this;
+ let view = me.up('pveACMEDomainEdit');
+ let pluginField = view.down('field[name=plugin]');
+ pluginField.setDisabled(value !== 'dns');
+ pluginField.setHidden(value !== 'dns');
+ },
+ },
+ },
+ {
+ xtype: 'hidden',
+ name: 'alias',
+ },
+ {
+ xtype: 'pveACMEPluginSelector',
+ name: 'plugin',
+ disabled: true,
+ hidden: true,
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'domain',
+ allowBlank: false,
+ vtype: 'DnsName',
+ value: '',
+ fieldLabel: gettext('Domain'),
+ },
+ ],
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw 'no nodename given';
+ }
+
+ if (!me.nodeconfig) {
+ throw 'no nodeconfig given';
+ }
+
+ me.isCreate = !me.domain;
+ if (me.isCreate) {
+ me.domain = `${me.nodename}.`; // TODO: FQDN of node
+ }
+
+ me.url = `/api2/extjs/nodes/${me.nodename}/config`;
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.setValues(me.domain);
+ } else {
+ me.setValues({ domain: me.domain });
+ }
+ },
+});
+
+Ext.define('pve-acme-domains', {
+ extend: 'Ext.data.Model',
+ fields: ['domain', 'type', 'alias', 'plugin', 'configkey'],
+ idProperty: 'domain',
+});
+
+Ext.define('PVE.node.ACME', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveACMEView',
+
+ margin: '10 0 0 0',
+ title: 'ACME',
+
+ emptyText: gettext('No Domains configured'),
+
+ viewModel: {
+ data: {
+ domaincount: 0,
+ account: undefined, // the account we display
+ configaccount: undefined, // the account set in the config
+ accountEditable: false,
+ accountsAvailable: false,
+ },
+
+ formulas: {
+ canOrder: (get) => !!get('account') && get('domaincount') > 0,
+ editBtnIcon: (get) => 'fa black fa-' + (get('accountEditable') ? 'check' : 'pencil'),
+ editBtnText: (get) => get('accountEditable') ? gettext('Apply') : gettext('Edit'),
+ accountTextHidden: (get) => get('accountEditable') || !get('accountsAvailable'),
+ accountValueHidden: (get) => !get('accountEditable') || !get('accountsAvailable'),
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ let accountSelector = this.lookup('accountselector');
+ accountSelector.store.on('load', this.onAccountsLoad, this);
+ },
+
+ onAccountsLoad: function(store, records, success) {
+ let me = this;
+ let vm = me.getViewModel();
+ let configaccount = vm.get('configaccount');
+ vm.set('accountsAvailable', records.length > 0);
+ if (me.autoChangeAccount && records.length > 0) {
+ me.changeAccount(records[0].data.name, () => {
+ vm.set('accountEditable', false);
+ me.reload();
+ });
+ me.autoChangeAccount = false;
+ } else if (configaccount) {
+ if (store.findExact('name', configaccount) !== -1) {
+ vm.set('account', configaccount);
+ } else {
+ vm.set('account', null);
+ }
+ }
+ },
+
+ addDomain: function() {
+ let me = this;
+ let view = me.getView();
+
+ Ext.create('PVE.node.ACMEDomainEdit', {
+ nodename: view.nodename,
+ nodeconfig: view.nodeconfig,
+ apiCallDone: function() {
+ me.reload();
+ },
+ }).show();
+ },
+
+ editDomain: function() {
+ let me = this;
+ let view = me.getView();
+
+ let selection = view.getSelection();
+ if (selection.length < 1) return;
+
+ Ext.create('PVE.node.ACMEDomainEdit', {
+ nodename: view.nodename,
+ nodeconfig: view.nodeconfig,
+ domain: selection[0].data,
+ apiCallDone: function() {
+ me.reload();
+ },
+ }).show();
+ },
+
+ removeDomain: function() {
+ let me = this;
+ let view = me.getView();
+ let selection = view.getSelection();
+ if (selection.length < 1) return;
+
+ let rec = selection[0].data;
+ let params = {};
+ if (rec.configkey !== 'acme') {
+ params.delete = rec.configkey;
+ } else {
+ let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
+ PVE.Utils.remove_domain_from_acme(acme, rec.domain);
+ params.acme = PVE.Parser.printACME(acme);
+ }
+
+ Proxmox.Utils.API2Request({
+ method: 'PUT',
+ url: `/nodes/${view.nodename}/config`,
+ params,
+ success: function(response, opt) {
+ me.reload();
+ },
+ failure: function(response, opt) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+
+ toggleEditAccount: function() {
+ let me = this;
+ let vm = me.getViewModel();
+ let editable = vm.get('accountEditable');
+ if (editable) {
+ me.changeAccount(vm.get('account'), function() {
+ vm.set('accountEditable', false);
+ me.reload();
+ });
+ } else {
+ vm.set('accountEditable', true);
+ }
+ },
+
+ changeAccount: function(account, callback) {
+ let me = this;
+ let view = me.getView();
+ let params = {};
+
+ let acme = PVE.Parser.parseACME(view.nodeconfig.acme);
+ acme.account = account;
+ params.acme = PVE.Parser.printACME(acme);
+
+ Proxmox.Utils.API2Request({
+ method: 'PUT',
+ waitMsgTarget: view,
+ url: `/nodes/${view.nodename}/config`,
+ params,
+ success: function(response, opt) {
+ if (Ext.isFunction(callback)) {
+ callback();
+ }
+ },
+ failure: function(response, opt) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+
+ order: function() {
+ let me = this;
+ let view = me.getView();
+
+ Proxmox.Utils.API2Request({
+ method: 'POST',
+ params: {
+ force: 1,
+ },
+ url: `/nodes/${view.nodename}/certificates/acme/certificate`,
+ success: function(response, opt) {
+ Ext.create('Proxmox.window.TaskViewer', {
+ upid: response.result.data,
+ taskDone: function(success) {
+ me.orderFinished(success);
+ },
+ }).show();
+ },
+ failure: function(response, opt) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+
+ orderFinished: function(success) {
+ if (!success) return;
+ var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
+ Ext.getBody().mask(txt, ['pve-static-mask']);
+ // reload after 10 seconds automatically
+ Ext.defer(function() {
+ window.location.reload(true);
+ }, 10000);
+ },
+
+ reload: function() {
+ let me = this;
+ let view = me.getView();
+ view.rstore.load();
+ },
+
+ addAccount: function() {
+ let me = this;
+ Ext.create('PVE.node.ACMEAccountCreate', {
+ autoShow: true,
+ taskDone: function() {
+ me.reload();
+ let accountSelector = me.lookup('accountselector');
+ me.autoChangeAccount = true;
+ accountSelector.store.load();
+ },
+ });
+ },
+ },
+
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Add'),
+ handler: 'addDomain',
+ selModel: false,
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ disabled: true,
+ handler: 'editDomain',
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ handler: 'removeDomain',
+ },
+ '-',
+ {
+ xtype: 'button',
+ reference: 'order',
+ text: gettext('Order Certificates Now'),
+ bind: {
+ disabled: '{!canOrder}',
+ },
+ handler: 'order',
+ },
+ '-',
+ {
+ xtype: 'displayfield',
+ value: gettext('Using Account') + ':',
+ bind: {
+ hidden: '{!accountsAvailable}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'accounttext',
+ renderer: (val) => val || Proxmox.Utils.NoneText,
+ bind: {
+ value: '{account}',
+ hidden: '{accountTextHidden}',
+ },
+ },
+ {
+ xtype: 'pveACMEAccountSelector',
+ hidden: true,
+ reference: 'accountselector',
+ bind: {
+ value: '{account}',
+ hidden: '{accountValueHidden}',
+ },
+ },
+ {
+ xtype: 'button',
+ iconCls: 'fa black fa-pencil',
+ bind: {
+ iconCls: '{editBtnIcon}',
+ text: '{editBtnText}',
+ hidden: '{!accountsAvailable}',
+ },
+ handler: 'toggleEditAccount',
+ },
+ {
+ xtype: 'displayfield',
+ value: gettext('No Account available.'),
+ bind: {
+ hidden: '{accountsAvailable}',
+ },
+ },
+ {
+ xtype: 'button',
+ hidden: true,
+ reference: 'accountlink',
+ text: gettext('Add ACME Account'),
+ bind: {
+ hidden: '{accountsAvailable}',
+ },
+ handler: 'addAccount',
+ },
+ ],
+
+ updateStore: function(store, records, success) {
+ let me = this;
+ let data = [];
+ let rec;
+ if (success && records.length > 0) {
+ rec = records[0];
+ } else {
+ rec = {
+ data: {},
+ };
+ }
+
+ me.nodeconfig = rec.data; // save nodeconfig for updates
+
+ let account = 'default';
+
+ if (rec.data.acme) {
+ let obj = PVE.Parser.parseACME(rec.data.acme);
+ (obj.domains || []).forEach(domain => {
+ if (domain === '') return;
+ let record = {
+ domain,
+ type: 'standalone',
+ configkey: 'acme',
+ };
+ data.push(record);
+ });
+
+ if (obj.account) {
+ account = obj.account;
+ }
+ }
+
+ let vm = me.getViewModel();
+ let oldaccount = vm.get('account');
+
+ // account changed, and we do not edit currently, load again to verify
+ if (oldaccount !== account && !vm.get('accountEditable')) {
+ vm.set('configaccount', account);
+ me.lookup('accountselector').store.load();
+ }
+
+ for (let i = 0; i < PVE.Utils.acmedomain_count; i++) {
+ let acmedomain = rec.data[`acmedomain${i}`];
+ if (!acmedomain) continue;
+
+ let record = PVE.Parser.parsePropertyString(acmedomain, 'domain');
+ record.type = 'dns';
+ record.configkey = `acmedomain${i}`;
+ data.push(record);
+ }
+
+ vm.set('domaincount', data.length);
+ me.store.loadData(data, false);
+ },
+
+ listeners: {
+ itemdblclick: 'editDomain',
+ },
+
+ columns: [
+ {
+ dataIndex: 'domain',
+ flex: 5,
+ text: gettext('Domain'),
+ },
+ {
+ dataIndex: 'type',
+ flex: 1,
+ text: gettext('Type'),
+ },
+ {
+ dataIndex: 'plugin',
+ flex: 1,
+ text: gettext('Plugin'),
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no nodename given";
+ }
+
+ me.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 10 * 1000,
+ autoStart: true,
+ storeid: `pve-node-domains-${me.nodename}`,
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodename}/config`,
+ },
+ });
+
+ me.store = Ext.create('Ext.data.Store', {
+ model: 'pve-acme-domains',
+ sorters: 'domain',
+ });
+
+ me.callParent();
+ me.mon(me.rstore, 'load', 'updateStore', me);
+ Proxmox.Utils.monStoreErrors(me, me.rstore);
+ me.on('destroy', me.rstore.stopUpdate, me.rstore);
+ },
+});
+Ext.define('PVE.node.CertificateView', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveCertificatesView',
+
+ onlineHelp: 'sysadmin_certificate_management',
+
+ mixins: ['Proxmox.Mixin.CBind'],
+ scrollable: 'y',
+
+ items: [
+ {
+ xtype: 'pveCertView',
+ border: 0,
+ cbind: {
+ nodename: '{nodename}',
+ },
+ },
+ {
+ xtype: 'pveACMEView',
+ border: 0,
+ cbind: {
+ nodename: '{nodename}',
+ },
+ },
+ ],
+
+});
+
+Ext.define('PVE.node.CertificateViewer', {
+ extend: 'Proxmox.window.Edit',
+
+ title: gettext('Certificate'),
+
+ fieldDefaults: {
+ labelWidth: 120,
+ },
+ width: 800,
+
+ items: {
+ xtype: 'inputpanel',
+ maxHeight: 900,
+ scrollable: 'y',
+ columnT: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Name'),
+ name: 'filename',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Fingerprint'),
+ name: 'fingerprint',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Issuer'),
+ name: 'issuer',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Subject'),
+ name: 'subject',
+ },
+ ],
+ column1: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Public Key Type'),
+ name: 'public-key-type',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Public Key Size'),
+ name: 'public-key-bits',
+ },
+ ],
+ column2: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Valid Since'),
+ renderer: Proxmox.Utils.render_timestamp,
+ name: 'notbefore',
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Expires'),
+ renderer: Proxmox.Utils.render_timestamp,
+ name: 'notafter',
+ },
+ ],
+ columnB: [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Subject Alternative Names'),
+ name: 'san',
+ renderer: PVE.Utils.render_san,
+ },
+ {
+ xtype: 'fieldset',
+ title: gettext('Raw Certificate'),
+ collapsible: true,
+ collapsed: true,
+ items: [{
+ xtype: 'textarea',
+ name: 'pem',
+ editable: false,
+ grow: true,
+ growMax: 350,
+ fieldStyle: {
+ 'white-space': 'pre-wrap',
+ 'font-family': 'monospace',
+ },
+ }],
+ },
+ ],
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.cert) {
+ throw "no cert given";
+ }
+ if (!me.nodename) {
+ throw "no nodename given";
+ }
+
+ me.url = `/nodes/${me.nodename}/certificates/info`;
+ me.callParent();
+
+ // hide OK/Reset button, because we just want to show data
+ me.down('toolbar[dock=bottom]').setVisible(false);
+
+ me.load({
+ success: function(response) {
+ if (Ext.isArray(response.result.data)) {
+ for (const item of response.result.data) {
+ if (item.filename === me.cert) {
+ me.setValues(item);
+ return;
+ }
+ }
+ }
+ },
+ });
+ },
+});
+
+Ext.define('PVE.node.CertUpload', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCertUpload',
+
+ title: gettext('Upload Custom Certificate'),
+ resizable: false,
+ isCreate: true,
+ submitText: gettext('Upload'),
+ method: 'POST',
+ width: 600,
+
+ apiCallDone: function(success, response, options) {
+ if (!success) {
+ return;
+ }
+ let txt = gettext('API server will be restarted to use new certificates, please reload web-interface!');
+ Ext.getBody().mask(txt, ['pve-static-mask']);
+ Ext.defer(() => window.location.reload(true), 10000); // reload after 10 seconds automatically
+ },
+
+ items: {
+ xtype: 'inputpanel',
+ onGetValues: function(values) {
+ values.restart = 1;
+ values.force = 1;
+ if (!values.key) {
+ delete values.key;
+ }
+ return values;
+ },
+ items: [
+ {
+ fieldLabel: gettext('Private Key (Optional)'),
+ labelAlign: 'top',
+ emptyText: gettext('No change'),
+ name: 'key',
+ xtype: 'textarea',
+ },
+ {
+ xtype: 'filebutton',
+ text: gettext('From File'),
+ listeners: {
+ change: function(btn, e, value) {
+ let form = this.up('form');
+ for (const file of e.event.target.files) {
+ PVE.Utils.loadFile(file, res => form.down('field[name=key]').setValue(res));
+ }
+ btn.reset();
+ },
+ },
+ },
+ {
+ fieldLabel: gettext('Certificate Chain'),
+ labelAlign: 'top',
+ allowBlank: false,
+ name: 'certificates',
+ xtype: 'textarea',
+ },
+ {
+ xtype: 'filebutton',
+ text: gettext('From File'),
+ listeners: {
+ change: function(btn, e, value) {
+ let form = this.up('form');
+ for (const file of e.event.target.files) {
+ PVE.Utils.loadFile(file, res => form.down('field[name=certificates]').setValue(res));
+ }
+ btn.reset();
+ },
+ },
+ },
+ ],
+ },
+
+ initComponent: function() {
+ let me = this;
+ if (!me.nodename) {
+ throw "no nodename given";
+ }
+ me.url = `/nodes/${me.nodename}/certificates/custom`;
+
+ me.callParent();
+ },
+});
+
+Ext.define('pve-certificate', {
+ extend: 'Ext.data.Model',
+ fields: ['filename', 'fingerprint', 'issuer', 'notafter', 'notbefore', 'subject', 'san', 'public-key-bits', 'public-key-type'],
+ idProperty: 'filename',
+});
+
+Ext.define('PVE.node.Certificates', {
+ extend: 'Ext.grid.Panel',
+ xtype: 'pveCertView',
+
+ tbar: [
+ {
+ xtype: 'button',
+ text: gettext('Upload Custom Certificate'),
+ handler: function() {
+ let view = this.up('grid');
+ Ext.create('PVE.node.CertUpload', {
+ nodename: view.nodename,
+ listeners: {
+ destroy: () => view.reload(),
+ },
+ autoShow: true,
+ });
+ },
+ },
+ {
+ xtype: 'proxmoxStdRemoveButton',
+ itemId: 'deletebtn',
+ text: gettext('Delete Custom Certificate'),
+ dangerous: true,
+ selModel: false,
+ getUrl: function(rec) {
+ let view = this.up('grid');
+ return `/nodes/${view.nodename}/certificates/custom?restart=1`;
+ },
+ confirmMsg: gettext('Delete custom certificate and switch to generated one?'),
+ callback: function(options, success, response) {
+ if (success) {
+ let txt = gettext('API server will be restarted to use new certificates, please reload web-interface!');
+ Ext.getBody().mask(txt, ['pve-static-mask']);
+ // reload after 10 seconds automatically
+ Ext.defer(() => window.location.reload(true), 10000);
+ }
+ },
+ },
+ '-',
+ {
+ xtype: 'proxmoxButton',
+ itemId: 'viewbtn',
+ disabled: true,
+ text: gettext('View Certificate'),
+ handler: function() {
+ this.up('grid').viewCertificate();
+ },
+ },
+ ],
+
+ columns: [
+ {
+ header: gettext('File'),
+ width: 150,
+ dataIndex: 'filename',
+ },
+ {
+ header: gettext('Issuer'),
+ flex: 1,
+ dataIndex: 'issuer',
+ },
+ {
+ header: gettext('Subject'),
+ flex: 1,
+ dataIndex: 'subject',
+ },
+ {
+ header: gettext('Public Key Alogrithm'),
+ flex: 1,
+ dataIndex: 'public-key-type',
+ hidden: true,
+ },
+ {
+ header: gettext('Public Key Size'),
+ flex: 1,
+ dataIndex: 'public-key-bits',
+ hidden: true,
+ },
+ {
+ header: gettext('Valid Since'),
+ width: 150,
+ dataIndex: 'notbefore',
+ renderer: Proxmox.Utils.render_timestamp,
+ },
+ {
+ header: gettext('Expires'),
+ width: 150,
+ dataIndex: 'notafter',
+ renderer: Proxmox.Utils.render_timestamp,
+ },
+ {
+ header: gettext('Subject Alternative Names'),
+ flex: 1,
+ dataIndex: 'san',
+ renderer: PVE.Utils.render_san,
+ },
+ {
+ header: gettext('Fingerprint'),
+ dataIndex: 'fingerprint',
+ hidden: true,
+ },
+ {
+ header: gettext('PEM'),
+ dataIndex: 'pem',
+ hidden: true,
+ },
+ ],
+
+ reload: function() {
+ this.rstore.load();
+ },
+
+ viewCertificate: function() {
+ let me = this;
+ let selection = me.getSelection();
+ if (!selection || selection.length < 1) {
+ return;
+ }
+ var win = Ext.create('PVE.node.CertificateViewer', {
+ cert: selection[0].data.filename,
+ nodename: me.nodename,
+ });
+ win.show();
+ },
+
+ listeners: {
+ itemdblclick: 'viewCertificate',
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no nodename given";
+ }
+
+ me.rstore = Ext.create('Proxmox.data.UpdateStore', {
+ storeid: 'certs-' + me.nodename,
+ model: 'pve-certificate',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/certificates/info',
+ },
+ });
+
+ me.store = {
+ type: 'diff',
+ rstore: me.rstore,
+ };
+
+ me.callParent();
+
+ me.mon(me.rstore, 'load', store => me.down('#deletebtn').setDisabled(!store.getById('pveproxy-ssl.pem')));
+ me.rstore.startUpdate();
+ me.on('destroy', me.rstore.stopUpdate, me.rstore);
+ },
+});
+Ext.define('PVE.node.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+ xtype: 'nodeCmdMenu',
+
+ showSeparator: false,
+
+ items: [
+ {
+ text: gettext('Create VM'),
+ itemId: 'createvm',
+ iconCls: 'fa fa-desktop',
+ handler: function() {
+ Ext.create('PVE.qemu.CreateWizard', {
+ nodename: this.up('menu').nodename,
+ autoShow: true,
+ });
+ },
+ },
+ {
+ text: gettext('Create CT'),
+ itemId: 'createct',
+ iconCls: 'fa fa-cube',
+ handler: function() {
+ Ext.create('PVE.lxc.CreateWizard', {
+ nodename: this.up('menu').nodename,
+ autoShow: true,
+ });
+ },
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Bulk Start'),
+ itemId: 'bulkstart',
+ iconCls: 'fa fa-fw fa-play',
+ handler: function() {
+ Ext.create('PVE.window.BulkAction', {
+ nodename: this.up('menu').nodename,
+ title: gettext('Bulk Start'),
+ btnText: gettext('Start'),
+ action: 'startall',
+ autoShow: true,
+ });
+ },
+ },
+ {
+ text: gettext('Bulk Shutdown'),
+ itemId: 'bulkstop',
+ iconCls: 'fa fa-fw fa-stop',
+ handler: function() {
+ Ext.create('PVE.window.BulkAction', {
+ nodename: this.up('menu').nodename,
+ title: gettext('Bulk Shutdown'),
+ btnText: gettext('Shutdown'),
+ action: 'stopall',
+ autoShow: true,
+ });
+ },
+ },
+ {
+ text: gettext('Bulk Migrate'),
+ itemId: 'bulkmigrate',
+ iconCls: 'fa fa-fw fa-send-o',
+ handler: function() {
+ Ext.create('PVE.window.BulkAction', {
+ nodename: this.up('menu').nodename,
+ title: gettext('Bulk Migrate'),
+ btnText: gettext('Migrate'),
+ action: 'migrateall',
+ autoShow: true,
+ });
+ },
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Shell'),
+ itemId: 'shell',
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: function() {
+ let nodename = this.up('menu').nodename;
+ PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, nodename, undefined);
+ },
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Wake-on-LAN'),
+ itemId: 'wakeonlan',
+ iconCls: 'fa fa-fw fa-power-off',
+ handler: function() {
+ let nodename = this.up('menu').nodename;
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${nodename}/wakeonlan`,
+ method: 'POST',
+ failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ success: function(response, opts) {
+ Ext.Msg.show({
+ title: 'Success',
+ icon: Ext.Msg.INFO,
+ msg: Ext.String.format(
+ gettext("Wake on LAN packet send for '{0}': '{1}'"),
+ nodename,
+ response.result.data,
+ ),
+ });
+ },
+ });
+ },
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw 'no nodename specified';
+ }
+
+ me.title = gettext('Node') + " '" + me.nodename + "'";
+ me.callParent();
+
+ let caps = Ext.state.Manager.get('GuiCap');
+
+ if (!caps.vms['VM.Allocate']) {
+ me.getComponent('createct').setDisabled(true);
+ me.getComponent('createvm').setDisabled(true);
+ }
+ if (!caps.vms['VM.Migrate']) {
+ me.getComponent('bulkmigrate').setDisabled(true);
+ }
+ if (!caps.vms['VM.PowerMgmt']) {
+ me.getComponent('bulkstart').setDisabled(true);
+ me.getComponent('bulkstop').setDisabled(true);
+ }
+ if (!caps.nodes['Sys.PowerMgmt']) {
+ me.getComponent('wakeonlan').setDisabled(true);
+ }
+ if (!caps.nodes['Sys.Console']) {
+ me.getComponent('shell').setDisabled(true);
+ }
+ if (me.pveSelNode.data.running) {
+ me.getComponent('wakeonlan').setDisabled(true);
+ }
+ },
+});
+Ext.define('PVE.node.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.node.Config',
+
+ onlineHelp: 'chapter_system_administration',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: "/api2/json/nodes/" + nodename + "/status",
+ interval: 5000,
+ });
+
+ var node_command = function(cmd) {
+ Proxmox.Utils.API2Request({
+ params: { command: cmd },
+ url: '/nodes/' + nodename + '/status',
+ method: 'POST',
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ };
+
+ var actionBtn = Ext.create('Ext.Button', {
+ text: gettext('Bulk Actions'),
+ iconCls: 'fa fa-fw fa-ellipsis-v',
+ disabled: !caps.vms['VM.PowerMgmt'] && !caps.vms['VM.Migrate'],
+ menu: new Ext.menu.Menu({
+ items: [
+ {
+ text: gettext('Bulk Start'),
+ iconCls: 'fa fa-fw fa-play',
+ disabled: !caps.vms['VM.PowerMgmt'],
+ handler: function() {
+ Ext.create('PVE.window.BulkAction', {
+ autoShow: true,
+ nodename: nodename,
+ title: gettext('Bulk Start'),
+ btnText: gettext('Start'),
+ action: 'startall',
+ });
+ },
+ },
+ {
+ text: gettext('Bulk Shutdown'),
+ iconCls: 'fa fa-fw fa-stop',
+ disabled: !caps.vms['VM.PowerMgmt'],
+ handler: function() {
+ Ext.create('PVE.window.BulkAction', {
+ autoShow: true,
+ nodename: nodename,
+ title: gettext('Bulk Shutdown'),
+ btnText: gettext('Shutdown'),
+ action: 'stopall',
+ });
+ },
+ },
+ {
+ text: gettext('Bulk Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ disabled: !caps.vms['VM.Migrate'],
+ handler: function() {
+ Ext.create('PVE.window.BulkAction', {
+ autoShow: true,
+ nodename: nodename,
+ title: gettext('Bulk Migrate'),
+ btnText: gettext('Migrate'),
+ action: 'migrateall',
+ });
+ },
+ },
+ ],
+ }),
+ });
+
+ let restartBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Reboot'),
+ disabled: !caps.nodes['Sys.PowerMgmt'],
+ dangerous: true,
+ confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename),
+ handler: function() {
+ node_command('reboot');
+ },
+ iconCls: 'fa fa-undo',
+ });
+
+ var shutdownBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Shutdown'),
+ disabled: !caps.nodes['Sys.PowerMgmt'],
+ dangerous: true,
+ confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename),
+ handler: function() {
+ node_command('shutdown');
+ },
+ iconCls: 'fa fa-power-off',
+ });
+
+ var shellBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.nodes['Sys.Console'],
+ text: gettext('Shell'),
+ consoleType: 'shell',
+ nodename: nodename,
+ });
+
+ me.items = [];
+
+ Ext.apply(me, {
+ title: gettext('Node') + " '" + nodename + "'",
+ hstateid: 'nodetab',
+ defaults: {
+ statusStore: me.statusStore,
+ },
+ tbar: [restartBtn, shutdownBtn, shellBtn, actionBtn],
+ });
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ xtype: 'pveNodeSummary',
+ title: gettext('Summary'),
+ iconCls: 'fa fa-book',
+ itemId: 'summary',
+ },
+ {
+ xtype: 'pmxNotesView',
+ title: gettext('Notes'),
+ iconCls: 'fa fa-sticky-note-o',
+ itemId: 'notes',
+ },
+ );
+ }
+
+ if (caps.nodes['Sys.Console']) {
+ me.items.push(
+ {
+ xtype: 'pveNoVncConsole',
+ title: gettext('Shell'),
+ iconCls: 'fa fa-terminal',
+ itemId: 'jsconsole',
+ consoleType: 'shell',
+ xtermjs: true,
+ nodename: nodename,
+ },
+ );
+ }
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ xtype: 'proxmoxNodeServiceView',
+ title: gettext('System'),
+ iconCls: 'fa fa-cogs',
+ itemId: 'services',
+ expandedOnInit: true,
+ restartCommand: 'reload', // avoid disruptions
+ startOnlyServices: {
+ 'pveproxy': true,
+ 'pvedaemon': true,
+ 'pve-cluster': true,
+ },
+ nodename: nodename,
+ onlineHelp: 'pve_service_daemons',
+ },
+ {
+ xtype: 'proxmoxNodeNetworkView',
+ title: gettext('Network'),
+ iconCls: 'fa fa-exchange',
+ itemId: 'network',
+ showApplyBtn: true,
+ groups: ['services'],
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ },
+ {
+ xtype: 'pveCertificatesView',
+ title: gettext('Certificates'),
+ iconCls: 'fa fa-certificate',
+ itemId: 'certificates',
+ groups: ['services'],
+ nodename: nodename,
+ },
+ {
+ xtype: 'proxmoxNodeDNSView',
+ title: gettext('DNS'),
+ iconCls: 'fa fa-globe',
+ groups: ['services'],
+ itemId: 'dns',
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ },
+ {
+ xtype: 'proxmoxNodeHostsView',
+ title: gettext('Hosts'),
+ iconCls: 'fa fa-globe',
+ groups: ['services'],
+ itemId: 'hosts',
+ nodename: nodename,
+ onlineHelp: 'sysadmin_network_configuration',
+ },
+ {
+ xtype: 'proxmoxNodeOptionsView',
+ title: gettext('Options'),
+ iconCls: 'fa fa-gear',
+ groups: ['services'],
+ itemId: 'options',
+ nodename: nodename,
+ onlineHelp: 'proxmox_node_management',
+ },
+ {
+ xtype: 'proxmoxNodeTimeView',
+ title: gettext('Time'),
+ itemId: 'time',
+ groups: ['services'],
+ nodename: nodename,
+ iconCls: 'fa fa-clock-o',
+ });
+ }
+
+ if (caps.nodes['Sys.Syslog']) {
+ me.items.push({
+ xtype: 'proxmoxJournalView',
+ title: 'Syslog',
+ iconCls: 'fa fa-list',
+ groups: ['services'],
+ disabled: !caps.nodes['Sys.Syslog'],
+ itemId: 'syslog',
+ url: "/api2/extjs/nodes/" + nodename + "/journal",
+ });
+
+ if (caps.nodes['Sys.Modify']) {
+ me.items.push({
+ xtype: 'proxmoxNodeAPT',
+ title: gettext('Updates'),
+ iconCls: 'fa fa-refresh',
+ expandedOnInit: true,
+ disabled: !caps.nodes['Sys.Console'],
+ // do we want to link to system updates instead?
+ itemId: 'apt',
+ upgradeBtn: {
+ xtype: 'pveConsoleButton',
+ disabled: Proxmox.UserName !== 'root@pam',
+ text: gettext('Upgrade'),
+ consoleType: 'upgrade',
+ nodename: nodename,
+ },
+ nodename: nodename,
+ });
+
+ me.items.push({
+ xtype: 'proxmoxNodeAPTRepositories',
+ title: gettext('Repositories'),
+ iconCls: 'fa fa-files-o',
+ itemId: 'aptrepositories',
+ nodename: nodename,
+ onlineHelp: 'sysadmin_package_repositories',
+ groups: ['apt'],
+ });
+ }
+ }
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ xtype: 'pveFirewallRules',
+ iconCls: 'fa fa-shield',
+ title: gettext('Firewall'),
+ allow_iface: true,
+ base_url: '/nodes/' + nodename + '/firewall/rules',
+ list_refs_url: '/cluster/firewall/refs',
+ itemId: 'firewall',
+ },
+ {
+ xtype: 'pveFirewallOptions',
+ title: gettext('Options'),
+ iconCls: 'fa fa-gear',
+ onlineHelp: 'pve_firewall_host_specific_configuration',
+ groups: ['firewall'],
+ base_url: '/nodes/' + nodename + '/firewall/options',
+ fwtype: 'node',
+ itemId: 'firewall-options',
+ });
+ }
+
+
+ if (caps.nodes['Sys.Audit']) {
+ me.items.push(
+ {
+ xtype: 'pmxDiskList',
+ title: gettext('Disks'),
+ itemId: 'storage',
+ expandedOnInit: true,
+ iconCls: 'fa fa-hdd-o',
+ nodename: nodename,
+ includePartitions: true,
+ supportsWipeDisk: true,
+ },
+ {
+ xtype: 'pveLVMList',
+ title: 'LVM',
+ itemId: 'lvm',
+ onlineHelp: 'chapter_lvm',
+ iconCls: 'fa fa-square',
+ groups: ['storage'],
+ },
+ {
+ xtype: 'pveLVMThinList',
+ title: 'LVM-Thin',
+ itemId: 'lvmthin',
+ onlineHelp: 'chapter_lvm',
+ iconCls: 'fa fa-square-o',
+ groups: ['storage'],
+ },
+ {
+ xtype: 'pveDirectoryList',
+ title: Proxmox.Utils.directoryText,
+ itemId: 'directory',
+ onlineHelp: 'chapter_storage',
+ iconCls: 'fa fa-folder',
+ groups: ['storage'],
+ },
+ {
+ title: 'ZFS',
+ itemId: 'zfs',
+ onlineHelp: 'chapter_zfs',
+ iconCls: 'fa fa-th-large',
+ groups: ['storage'],
+ xtype: 'pveZFSList',
+ },
+ {
+ xtype: 'pveNodeCephStatus',
+ title: 'Ceph',
+ itemId: 'ceph',
+ iconCls: 'fa fa-ceph',
+ },
+ {
+ xtype: 'pveNodeCephConfigCrush',
+ title: gettext('Configuration'),
+ iconCls: 'fa fa-gear',
+ groups: ['ceph'],
+ itemId: 'ceph-config',
+ },
+ {
+ xtype: 'pveNodeCephMonMgr',
+ title: gettext('Monitor'),
+ iconCls: 'fa fa-tv',
+ groups: ['ceph'],
+ itemId: 'ceph-monlist',
+ },
+ {
+ xtype: 'pveNodeCephOsdTree',
+ title: 'OSD',
+ iconCls: 'fa fa-hdd-o',
+ groups: ['ceph'],
+ itemId: 'ceph-osdtree',
+ },
+ {
+ xtype: 'pveNodeCephFSPanel',
+ title: 'CephFS',
+ iconCls: 'fa fa-folder',
+ groups: ['ceph'],
+ nodename: nodename,
+ itemId: 'ceph-cephfspanel',
+ },
+ {
+ xtype: 'pveNodeCephPoolList',
+ title: 'Pools',
+ iconCls: 'fa fa-sitemap',
+ groups: ['ceph'],
+ itemId: 'ceph-pools',
+ },
+ {
+ xtype: 'pveReplicaView',
+ iconCls: 'fa fa-retweet',
+ title: gettext('Replication'),
+ itemId: 'replication',
+ },
+ );
+ }
+
+ if (caps.nodes['Sys.Syslog']) {
+ me.items.push(
+ {
+ xtype: 'proxmoxLogView',
+ title: gettext('Log'),
+ iconCls: 'fa fa-list',
+ groups: ['firewall'],
+ onlineHelp: 'chapter_pve_firewall',
+ url: '/api2/extjs/nodes/' + nodename + '/firewall/log',
+ itemId: 'firewall-fwlog',
+ },
+ {
+ xtype: 'cephLogView',
+ title: gettext('Log'),
+ itemId: 'ceph-log',
+ iconCls: 'fa fa-list',
+ groups: ['ceph'],
+ onlineHelp: 'chapter_pveceph',
+ url: "/api2/extjs/nodes/" + nodename + "/ceph/log",
+ nodename: nodename,
+ });
+ }
+
+ me.items.push(
+ {
+ title: gettext('Task History'),
+ iconCls: 'fa fa-list-alt',
+ itemId: 'tasks',
+ nodename: nodename,
+ xtype: 'proxmoxNodeTasks',
+ extraFilter: [
+ {
+ xtype: 'pveGuestIDSelector',
+ fieldLabel: gettext('VMID'),
+ allowBlank: true,
+ name: 'vmid',
+ },
+ ],
+ },
+ {
+ title: gettext('Subscription'),
+ iconCls: 'fa fa-support',
+ itemId: 'support',
+ xtype: 'pveNodeSubscription',
+ nodename: nodename,
+ },
+ );
+
+ me.callParent();
+
+ me.mon(me.statusStore, 'load', function(store, records, success) {
+ let uptimerec = store.data.get('uptime');
+ let powermgmt = caps.nodes['Sys.PowerMgmt'] && uptimerec && uptimerec.data.value;
+
+ restartBtn.setDisabled(!powermgmt);
+ shutdownBtn.setDisabled(!powermgmt);
+ shellBtn.setDisabled(!powermgmt);
+ });
+
+ me.on('afterrender', function() {
+ me.statusStore.startUpdate();
+ });
+
+ me.on('destroy', function() {
+ me.statusStore.stopUpdate();
+ });
+ },
+});
+Ext.define('PVE.node.CreateDirectory', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCreateDirectory',
+
+ subject: Proxmox.Utils.directoryText,
+
+ showProgress: true,
+
+ onlineHelp: 'chapter_storage',
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = true;
+
+ Ext.applyIf(me, {
+ url: "/nodes/" + me.nodename + "/disks/directory",
+ method: 'POST',
+ items: [
+ {
+ xtype: 'pmxDiskSelector',
+ name: 'device',
+ nodename: me.nodename,
+ diskType: 'unused',
+ includePartitions: true,
+ fieldLabel: gettext('Disk'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ comboItems: [
+ ['ext4', 'ext4'],
+ ['xfs', 'xfs'],
+ ],
+ fieldLabel: gettext('Filesystem'),
+ name: 'filesystem',
+ value: '',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'name',
+ fieldLabel: gettext('Name'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'add_storage',
+ fieldLabel: gettext('Add Storage'),
+ value: '1',
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.node.Directorylist', {
+ extend: 'Ext.grid.Panel',
+ xtype: 'pveDirectoryList',
+
+ viewModel: {
+ data: {
+ path: '',
+ },
+ formulas: {
+ dirName: (get) => get('path')?.replace('/mnt/pve/', '') || undefined,
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ destroyDirectory: function() {
+ let me = this;
+ let vm = me.getViewModel();
+ let view = me.getView();
+
+ const dirName = vm.get('dirName');
+
+ if (!view.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!dirName) {
+ throw "no directory name specified";
+ }
+
+ Ext.create('PVE.window.SafeDestroyStorage', {
+ url: `/nodes/${view.nodename}/disks/directory/${dirName}`,
+ item: { id: dirName },
+ taskName: 'dirremove',
+ taskDone: () => { view.reload(); },
+ }).show();
+ },
+ },
+
+ stateful: true,
+ stateId: 'grid-node-directory',
+ columns: [
+ {
+ text: gettext('Path'),
+ dataIndex: 'path',
+ flex: 1,
+ },
+ {
+ header: gettext('Device'),
+ flex: 1,
+ dataIndex: 'device',
+ },
+ {
+ header: gettext('Type'),
+ width: 100,
+ dataIndex: 'type',
+ },
+ {
+ header: gettext('Options'),
+ width: 100,
+ dataIndex: 'options',
+ },
+ {
+ header: gettext('Unit File'),
+ hidden: true,
+ dataIndex: 'unitfile',
+ },
+ ],
+
+ rootVisible: false,
+ useArrows: true,
+
+ tbar: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: function() {
+ this.up('panel').reload();
+ },
+ },
+ {
+ text: `${gettext('Create')}: ${gettext('Directory')}`,
+ handler: function() {
+ let view = this.up('panel');
+ Ext.create('PVE.node.CreateDirectory', {
+ nodename: view.nodename,
+ listeners: {
+ destroy: () => view.reload(),
+ },
+ autoShow: true,
+ });
+ },
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ data: {
+ dirName: undefined,
+ },
+ bind: {
+ data: {
+ dirName: "{dirName}",
+ },
+ },
+ tpl: [
+ '',
+ gettext('Directory') + ' {dirName}:',
+ '',
+ Ext.String.format(gettext('No {0} selected'), gettext('directory')),
+ '',
+ ],
+ },
+ {
+ text: gettext('More'),
+ iconCls: 'fa fa-bars',
+ disabled: true,
+ bind: {
+ disabled: '{!dirName}',
+ },
+ menu: [
+ {
+ text: gettext('Destroy'),
+ itemId: 'remove',
+ iconCls: 'fa fa-fw fa-trash-o',
+ handler: 'destroyDirectory',
+ disabled: true,
+ bind: {
+ disabled: '{!dirName}',
+ },
+ },
+ ],
+ },
+ ],
+
+ reload: function() {
+ let me = this;
+ me.store.load();
+ me.store.sort();
+ },
+
+ listeners: {
+ activate: function() {
+ this.reload();
+ },
+ selectionchange: function(model, selected) {
+ let me = this;
+ let vm = me.getViewModel();
+
+ vm.set('path', selected[0]?.data.path || '');
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.nodename = me.pveSelNode.data.node;
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ store: {
+ fields: ['path', 'device', 'type', 'options', 'unitfile'],
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodename}/disks/directory`,
+ },
+ sorters: 'path',
+ },
+ });
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
+ me.reload();
+ },
+});
+
+Ext.define('PVE.node.CreateLVM', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCreateLVM',
+
+ onlineHelp: 'chapter_lvm',
+ subject: 'LVM Volume Group',
+
+ showProgress: true,
+ isCreate: true,
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = true;
+
+ Ext.applyIf(me, {
+ url: `/nodes/${me.nodename}/disks/lvm`,
+ method: 'POST',
+ items: [
+ {
+ xtype: 'pmxDiskSelector',
+ name: 'device',
+ nodename: me.nodename,
+ diskType: 'unused',
+ includePartitions: true,
+ fieldLabel: gettext('Disk'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'name',
+ fieldLabel: gettext('Name'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'add_storage',
+ fieldLabel: gettext('Add Storage'),
+ value: '1',
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.node.LVMList', {
+ extend: 'Ext.tree.Panel',
+ xtype: 'pveLVMList',
+
+ viewModel: {
+ data: {
+ volumeGroup: '',
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ destroyVolumeGroup: function() {
+ let me = this;
+ let vm = me.getViewModel();
+ let view = me.getView();
+
+ const volumeGroup = vm.get('volumeGroup');
+
+ if (!view.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!volumeGroup) {
+ throw "no volume group specified";
+ }
+
+ Ext.create('PVE.window.SafeDestroyStorage', {
+ url: `/nodes/${view.nodename}/disks/lvm/${volumeGroup}`,
+ item: { id: volumeGroup },
+ taskName: 'lvmremove',
+ taskDone: () => { view.reload(); },
+ }).show();
+ },
+ },
+
+ emptyText: PVE.Utils.renderNotFound('VGs'),
+
+ stateful: true,
+ stateId: 'grid-node-lvm',
+
+ rootVisible: false,
+ useArrows: true,
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ text: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ },
+ {
+ text: gettext('Number of LVs'),
+ dataIndex: 'lvcount',
+ width: 150,
+ align: 'right',
+ },
+ {
+ header: gettext('Assigned to LVs'),
+ width: 130,
+ dataIndex: 'usage',
+ tdCls: 'x-progressbar-default-cell',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar',
+ },
+ },
+ {
+ header: gettext('Size'),
+ width: 100,
+ align: 'right',
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'size',
+ },
+ {
+ header: gettext('Free'),
+ width: 100,
+ align: 'right',
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'free',
+ },
+ ],
+
+ tbar: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: function() {
+ this.up('panel').reload();
+ },
+ },
+ {
+ text: gettext('Create') + ': Volume Group',
+ handler: function() {
+ let view = this.up('panel');
+ Ext.create('PVE.node.CreateLVM', {
+ nodename: view.nodename,
+ taskDone: () => view.reload(),
+ autoShow: true,
+ });
+ },
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ data: {
+ volumeGroup: undefined,
+ },
+ bind: {
+ data: {
+ volumeGroup: "{volumeGroup}",
+ },
+ },
+ tpl: [
+ '',
+ 'Volume group {volumeGroup}:',
+ '',
+ Ext.String.format(gettext('No {0} selected'), 'volume group'),
+ '',
+ ],
+ },
+ {
+ text: gettext('More'),
+ iconCls: 'fa fa-bars',
+ disabled: true,
+ bind: {
+ disabled: '{!volumeGroup}',
+ },
+ menu: [
+ {
+ text: gettext('Destroy'),
+ itemId: 'remove',
+ iconCls: 'fa fa-fw fa-trash-o',
+ handler: 'destroyVolumeGroup',
+ disabled: true,
+ bind: {
+ disabled: '{!volumeGroup}',
+ },
+ },
+ ],
+ },
+ ],
+
+ reload: function() {
+ let me = this;
+ let sm = me.getSelectionModel();
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${me.nodename}/disks/lvm`,
+ waitMsgTarget: me,
+ method: 'GET',
+ failure: (response, opts) => Proxmox.Utils.setErrorMask(me, response.htmlStatus),
+ success: function(response, opts) {
+ sm.deselectAll();
+ me.setRootNode(response.result.data);
+ me.expandAll();
+ },
+ });
+ },
+
+ listeners: {
+ activate: function() {
+ this.reload();
+ },
+ selectionchange: function(model, selected) {
+ let me = this;
+ let vm = me.getViewModel();
+
+ if (selected.length < 1 || selected[0].data.parentId !== 'root') {
+ vm.set('volumeGroup', '');
+ } else {
+ vm.set('volumeGroup', selected[0].data.name);
+ }
+ },
+ },
+
+ selModel: 'treemodel',
+ fields: [
+ 'name',
+ 'size',
+ 'free',
+ {
+ type: 'string',
+ name: 'iconCls',
+ calculate: data => `fa x-fa-tree fa-${data.leaf ? 'hdd-o' : 'object-group'}`,
+ },
+ {
+ type: 'number',
+ name: 'usage',
+ calculate: data => (data.size - data.free) / data.size,
+ },
+ ],
+ sorters: 'name',
+
+ initComponent: function() {
+ let me = this;
+
+ me.nodename = me.pveSelNode.data.node;
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ me.callParent();
+
+ me.reload();
+ },
+});
+
+Ext.define('PVE.node.CreateLVMThin', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCreateLVMThin',
+
+ onlineHelp: 'chapter_lvm',
+ subject: 'LVM Thinpool',
+
+ showProgress: true,
+ isCreate: true,
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.applyIf(me, {
+ url: `/nodes/${me.nodename}/disks/lvmthin`,
+ method: 'POST',
+ items: [
+ {
+ xtype: 'pmxDiskSelector',
+ name: 'device',
+ nodename: me.nodename,
+ diskType: 'unused',
+ includePartitions: true,
+ fieldLabel: gettext('Disk'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'name',
+ fieldLabel: gettext('Name'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'add_storage',
+ fieldLabel: gettext('Add Storage'),
+ value: '1',
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.node.LVMThinList', {
+ extend: 'Ext.grid.Panel',
+ xtype: 'pveLVMThinList',
+
+ viewModel: {
+ data: {
+ thinPool: '',
+ volumeGroup: '',
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ destroyThinPool: function() {
+ let me = this;
+ let vm = me.getViewModel();
+ let view = me.getView();
+
+ const thinPool = vm.get('thinPool');
+ const volumeGroup = vm.get('volumeGroup');
+
+ if (!view.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!thinPool) {
+ throw "no thin pool specified";
+ }
+
+ if (!volumeGroup) {
+ throw "no volume group specified";
+ }
+
+ Ext.create('PVE.window.SafeDestroyStorage', {
+ url: `/nodes/${view.nodename}/disks/lvmthin/${thinPool}`,
+ params: { 'volume-group': volumeGroup },
+ item: { id: `${volumeGroup}/${thinPool}` },
+ taskName: 'lvmthinremove',
+ taskDone: () => { view.reload(); },
+ }).show();
+ },
+ },
+
+ emptyText: PVE.Utils.renderNotFound('Thin-Pool'),
+
+ stateful: true,
+ stateId: 'grid-node-lvmthin',
+
+ rootVisible: false,
+ useArrows: true,
+
+ columns: [
+ {
+ text: gettext('Name'),
+ dataIndex: 'lv',
+ flex: 1,
+ },
+ {
+ header: 'Volume Group',
+ width: 110,
+ dataIndex: 'vg',
+ },
+ {
+ header: gettext('Usage'),
+ width: 110,
+ dataIndex: 'usage',
+ tdCls: 'x-progressbar-default-cell',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar',
+ },
+ },
+ {
+ header: gettext('Size'),
+ width: 100,
+ align: 'right',
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'lv_size',
+ },
+ {
+ header: gettext('Used'),
+ width: 100,
+ align: 'right',
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'used',
+ },
+ {
+ header: gettext('Metadata Usage'),
+ width: 120,
+ dataIndex: 'metadata_usage',
+ tdCls: 'x-progressbar-default-cell',
+ xtype: 'widgetcolumn',
+ widget: {
+ xtype: 'pveProgressBar',
+ },
+ },
+ {
+ header: gettext('Metadata Size'),
+ width: 120,
+ align: 'right',
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'metadata_size',
+ },
+ {
+ header: gettext('Metadata Used'),
+ width: 125,
+ align: 'right',
+ sortable: true,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'metadata_used',
+ },
+ ],
+
+ tbar: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: function() {
+ this.up('panel').reload();
+ },
+ },
+ {
+ text: gettext('Create') + ': Thinpool',
+ handler: function() {
+ var view = this.up('panel');
+ Ext.create('PVE.node.CreateLVMThin', {
+ nodename: view.nodename,
+ taskDone: () => view.reload(),
+ autoShow: true,
+ });
+ },
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ data: {
+ thinPool: undefined,
+ volumeGroup: undefined,
+ },
+ bind: {
+ data: {
+ thinPool: "{thinPool}",
+ volumeGroup: "{volumeGroup}",
+ },
+ },
+ tpl: [
+ '',
+ '',
+ 'Thinpool {volumeGroup}/{thinPool}:',
+ '', // volumeGroup
+ 'Missing volume group (node running old version?)',
+ '',
+ '', // thinPool
+ Ext.String.format(gettext('No {0} selected'), 'thinpool'),
+ '',
+ ],
+ },
+ {
+ text: gettext('More'),
+ iconCls: 'fa fa-bars',
+ disabled: true,
+ bind: {
+ disabled: '{!volumeGroup || !thinPool}',
+ },
+ menu: [
+ {
+ text: gettext('Destroy'),
+ itemId: 'remove',
+ iconCls: 'fa fa-fw fa-trash-o',
+ handler: 'destroyThinPool',
+ disabled: true,
+ bind: {
+ disabled: '{!volumeGroup || !thinPool}',
+ },
+ },
+ ],
+ },
+ ],
+
+ reload: function() {
+ let me = this;
+ me.store.load();
+ me.store.sort();
+ },
+
+ listeners: {
+ activate: function() {
+ this.reload();
+ },
+ selectionchange: function(model, selected) {
+ let me = this;
+ let vm = me.getViewModel();
+
+ vm.set('volumeGroup', selected[0]?.data.vg || '');
+ vm.set('thinPool', selected[0]?.data.lv || '');
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.nodename = me.pveSelNode.data.node;
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ store: {
+ fields: [
+ 'lv',
+ 'lv_size',
+ 'used',
+ 'metadata_size',
+ 'metadata_used',
+ {
+ type: 'number',
+ name: 'usage',
+ calculate: data => data.used / data.lv_size,
+ },
+ {
+ type: 'number',
+ name: 'metadata_usage',
+ calculate: data => data.metadata_used / data.metadata_size,
+ },
+ ],
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodename}/disks/lvmthin`,
+ },
+ sorters: 'lv',
+ },
+ });
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
+ me.reload();
+ },
+});
+
+Ext.define('PVE.node.StatusView', {
+ extend: 'Proxmox.panel.StatusView',
+ alias: 'widget.pveNodeStatus',
+
+ height: 300,
+ bodyPadding: '15 5 15 5',
+
+ layout: {
+ type: 'table',
+ columns: 2,
+ tableAttrs: {
+ style: {
+ width: '100%',
+ },
+ },
+ },
+
+ defaults: {
+ xtype: 'pmxInfoWidget',
+ padding: '0 10 5 10',
+ },
+
+ items: [
+ {
+ itemId: 'cpu',
+ iconCls: 'fa fa-fw pmx-itype-icon-processor pmx-icon',
+ title: gettext('CPU usage'),
+ valueField: 'cpu',
+ maxField: 'cpuinfo',
+ renderer: Proxmox.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 pmx-itype-icon-memory pmx-icon',
+ itemId: 'memory',
+ title: gettext('RAM usage'),
+ valueField: 'memory',
+ maxField: 'memory',
+ renderer: Proxmox.Utils.render_node_size_usage,
+ },
+ {
+ itemId: 'ksm',
+ printBar: false,
+ title: gettext('KSM sharing'),
+ textField: 'ksm',
+ renderer: function(record) {
+ return Proxmox.Utils.render_size(record.shared);
+ },
+ padding: '0 10 10 10',
+ },
+ {
+ iconCls: 'fa fa-fw fa-hdd-o',
+ itemId: 'rootfs',
+ title: '/ ' + gettext('HD space'),
+ valueField: 'rootfs',
+ maxField: 'rootfs',
+ renderer: Proxmox.Utils.render_node_size_usage,
+ },
+ {
+ iconCls: 'fa fa-fw fa-refresh',
+ itemId: 'swap',
+ printSize: true,
+ title: gettext('SWAP usage'),
+ valueField: 'swap',
+ maxField: 'swap',
+ renderer: Proxmox.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: Proxmox.Utils.render_cpu_model,
+ 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 + ')');
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let stateProvider = Ext.state.Manager.getProvider();
+ let repoLink = stateProvider.encodeHToken({
+ view: "server",
+ rid: `node/${me.pveSelNode.data.node}`,
+ ltab: "tasks",
+ nodetab: "aptrepositories",
+ });
+
+ me.items.push({
+ xtype: 'pmxNodeInfoRepoStatus',
+ itemId: 'repositoryStatus',
+ product: 'Proxmox VE',
+ repoLink: `#${repoLink}`,
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.node.SubscriptionKeyEdit', {
+ extend: 'Proxmox.window.Edit',
+ title: gettext('Upload Subscription Key'),
+ width: 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 getReportFileName = function() {
+ var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i');
+ return `${me.nodename}-pve-report-${now}.txt`;
+ };
+
+ var view = Ext.createWidget('component', {
+ itemId: 'system-report-view',
+ scrollable: true,
+ style: {
+ 'white-space': 'pre',
+ 'font-family': 'monospace',
+ padding: '5px',
+ },
+ });
+
+ var reportWindow = Ext.create('Ext.window.Window', {
+ title: gettext('System Report'),
+ width: 1024,
+ height: 600,
+ layout: 'fit',
+ modal: true,
+ buttons: [
+ '->',
+ {
+ text: gettext('Download'),
+ handler: function() {
+ var fileContent = Ext.String.htmlDecode(reportWindow.getComponent('system-report-view').html);
+ var fileName = getReportFileName();
+
+ // Internet Explorer
+ if (window.navigator.msSaveOrOpenBlob) {
+ navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName);
+ } else {
+ 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'),
+ },
+ signature: {
+ header: gettext('Signed/Offline'),
+ renderer: (value) => {
+ if (value) {
+ return gettext('Yes');
+ } else {
+ return gettext('No');
+ }
+ },
+ },
+ };
+
+ 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('Remove Subscription'),
+ xtype: 'proxmoxStdRemoveButton',
+ confirmMsg: gettext('Are you sure you want to remove the subscription key?'),
+ baseurl: baseurl,
+ dangerous: true,
+ selModel: false,
+ 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.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveNodeSummary',
+
+ scrollable: true,
+ bodyPadding: 5,
+
+ showVersions: function() {
+ var me = this;
+
+ // Note: we use simply text/html here, because ExtJS grid has problems
+ // with cut&paste
+
+ var nodename = me.pveSelNode.data.node;
+
+ var view = Ext.createWidget('component', {
+ autoScroll: true,
+ id: 'pkgversions',
+ padding: 5,
+ style: {
+ 'white-space': 'pre',
+ 'font-family': 'monospace',
+ },
+ });
+
+ var win = Ext.create('Ext.window.Window', {
+ title: gettext('Package versions'),
+ width: 600,
+ height: 600,
+ layout: 'fit',
+ modal: true,
+ items: [view],
+ buttons: [
+ {
+ xtype: 'button',
+ iconCls: 'fa fa-clipboard',
+ handler: function(button) {
+ window.getSelection().selectAllChildren(
+ document.getElementById('pkgversions'),
+ );
+ document.execCommand("copy");
+ },
+ text: gettext('Copy'),
+ },
+ {
+ text: gettext('Ok'),
+ handler: function() {
+ this.up('window').close();
+ },
+ },
+ ],
+ });
+
+ Proxmox.Utils.API2Request({
+ waitMsgTarget: me,
+ url: `/nodes/${nodename}/apt/versions`,
+ method: 'GET',
+ failure: function(response, opts) {
+ win.close();
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, opts) {
+ win.show();
+ let text = '';
+ Ext.Array.each(response.result.data, function(rec) {
+ let version = "not correctly installed";
+ let pkg = rec.Package;
+ if (rec.OldVersion && rec.CurrentState === 'Installed') {
+ version = rec.OldVersion;
+ }
+ if (rec.RunningKernel) {
+ text += `${pkg}: ${version} (running kernel: ${rec.RunningKernel})\n`;
+ } else if (rec.ManagerVersion) {
+ text += `${pkg}: ${version} (running version: ${rec.ManagerVersion})\n`;
+ } else {
+ text += `${pkg}: ${version}\n`;
+ }
+ });
+
+ view.update(Ext.htmlEncode(text));
+ },
+ });
+ },
+
+ updateRepositoryStatus: function() {
+ let me = this;
+ let repoStatus = me.nodeStatus.down('#repositoryStatus');
+
+ let nodename = me.pveSelNode.data.node;
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${nodename}/apt/repositories`,
+ method: 'GET',
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ success: response => repoStatus.setRepositoryInfo(response.result.data['standard-repos']),
+ });
+
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${nodename}/subscription`,
+ method: 'GET',
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ success: function(response, opts) {
+ const res = response.result;
+ const subscription = res?.data?.status.toLowerCase() === 'active';
+ repoStatus.setSubscriptionStatus(subscription);
+ },
+ });
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.statusStore) {
+ throw "no status storage specified";
+ }
+
+ var rstore = me.statusStore;
+
+ var version_btn = new Ext.Button({
+ text: gettext('Package versions'),
+ handler: function() {
+ Proxmox.Utils.checked_command(function() { me.showVersions(); });
+ },
+ });
+
+ var rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: "/api2/json/nodes/" + nodename + "/rrddata",
+ model: 'pve-rrd-node',
+ });
+
+ let nodeStatus = Ext.create('PVE.node.StatusView', {
+ xtype: 'pveNodeStatus',
+ rstore: rstore,
+ width: 770,
+ pveSelNode: me.pveSelNode,
+ });
+
+ Ext.apply(me, {
+ tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' }],
+ nodeStatus: nodeStatus,
+ items: [
+ {
+ xtype: 'container',
+ itemId: 'itemcontainer',
+ layout: 'column',
+ minWidth: 700,
+ defaults: {
+ minHeight: 325,
+ padding: 5,
+ columnWidth: 1,
+ },
+ items: [
+ nodeStatus,
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('CPU usage'),
+ fields: ['cpu', 'iowait'],
+ fieldTitles: [gettext('CPU usage'), gettext('IO delay')],
+ unit: 'percent',
+ store: rrdstore,
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Server load'),
+ fields: ['loadavg'],
+ fieldTitles: [gettext('Load average')],
+ store: rrdstore,
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Memory usage'),
+ fields: ['memtotal', 'memused'],
+ fieldTitles: [gettext('Total'), gettext('RAM usage')],
+ unit: 'bytes',
+ powerOfTwo: true,
+ store: rrdstore,
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Network traffic'),
+ fields: ['netin', 'netout'],
+ store: rrdstore,
+ },
+ ],
+ listeners: {
+ resize: function(panel) {
+ Proxmox.Utils.updateColumns(panel);
+ },
+ },
+ },
+ ],
+ listeners: {
+ activate: function() {
+ rstore.setInterval(1000);
+ rstore.startUpdate(); // just to be sure
+ rrdstore.startUpdate();
+ },
+ destroy: function() {
+ rstore.setInterval(5000); // don't stop it, it's not ours!
+ rrdstore.stopUpdate();
+ },
+ },
+ });
+
+ me.updateRepositoryStatus();
+
+ me.callParent();
+
+ let sp = Ext.state.Manager.getProvider();
+ me.mon(sp, 'statechange', function(provider, key, value) {
+ if (key !== 'summarycolumns') {
+ return;
+ }
+ Proxmox.Utils.updateColumns(me.getComponent('itemcontainer'));
+ });
+ },
+});
+Ext.define('PVE.node.CreateZFS', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCreateZFS',
+
+ onlineHelp: 'chapter_zfs',
+ subject: 'ZFS',
+
+ showProgress: true,
+ isCreate: true,
+ width: 800,
+
+ viewModel: {
+ data: {
+ raidLevel: 'single',
+ },
+ formulas: {
+ isDraid: get => get('raidLevel')?.startsWith("draid"),
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ url: `/nodes/${me.nodename}/disks/zfs`,
+ method: 'POST',
+ items: [
+ {
+ xtype: 'inputpanel',
+ onGetValues: function(values) {
+ if (values.draidData || values.draidSpares) {
+ let opt = { data: values.draidData, spares: values.draidSpares };
+ values['draid-config'] = PVE.Parser.printPropertyString(opt);
+ }
+ delete values.draidData;
+ delete values.draidSpares;
+ return values;
+ },
+ column1: [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'name',
+ fieldLabel: gettext('Name'),
+ allowBlank: false,
+ maxLength: 128, // ZFS_MAX_DATASET_NAME_LEN is (256 - some edge case)
+ validator: v => {
+ // see zpool_name_valid function in libzfs_zpool.c
+ if (v.match(/^(mirror|raidz|draid|spare)/) || v === 'log') {
+ return gettext('Cannot use reserved pool name');
+ } else if (!v.match(/^[a-zA-Z][a-zA-Z0-9\-_.]*$/)) {
+ // note: zfs would support also : and whitespace, but we don't
+ return gettext("Invalid characters in pool name");
+ }
+ return true;
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'add_storage',
+ fieldLabel: gettext('Add Storage'),
+ value: '1',
+ },
+ ],
+ column2: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('RAID Level'),
+ name: 'raidlevel',
+ value: 'single',
+ comboItems: [
+ ['single', gettext('Single Disk')],
+ ['mirror', 'Mirror'],
+ ['raid10', 'RAID10'],
+ ['raidz', 'RAIDZ'],
+ ['raidz2', 'RAIDZ2'],
+ ['raidz3', 'RAIDZ3'],
+ ['draid', 'dRAID'],
+ ['draid2', 'dRAID2'],
+ ['draid3', 'dRAID3'],
+ ],
+ bind: {
+ value: '{raidLevel}',
+ },
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Compression'),
+ name: 'compression',
+ value: 'on',
+ comboItems: [
+ ['on', 'on'],
+ ['off', 'off'],
+ ['gzip', 'gzip'],
+ ['lz4', 'lz4'],
+ ['lzjb', 'lzjb'],
+ ['zle', 'zle'],
+ ['zstd', 'zstd'],
+ ],
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ fieldLabel: gettext('ashift'),
+ minValue: 9,
+ maxValue: 16,
+ value: '12',
+ name: 'ashift',
+ },
+ ],
+ columnB: [
+ {
+ xtype: 'fieldset',
+ title: gettext('dRAID Config'),
+ collapsible: false,
+ bind: {
+ hidden: '{!isDraid}',
+ },
+ layout: 'hbox',
+ padding: '5px 10px',
+ defaults: {
+ flex: 1,
+ layout: 'anchor',
+ },
+ items: [{
+ xtype: 'proxmoxintegerfield',
+ name: 'draidData',
+ fieldLabel: gettext('Data Devs'),
+ minValue: 1,
+ allowBlank: false,
+ disabled: true,
+ hidden: true,
+ bind: {
+ disabled: '{!isDraid}',
+ hidden: '{!isDraid}',
+ },
+ padding: '0 10 0 0',
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'draidSpares',
+ fieldLabel: gettext('Spares'),
+ minValue: 0,
+ allowBlank: false,
+ disabled: true,
+ hidden: true,
+ bind: {
+ disabled: '{!isDraid}',
+ hidden: '{!isDraid}',
+ },
+ padding: '0 0 0 10',
+ }],
+ },
+ {
+ xtype: 'pmxMultiDiskSelector',
+ name: 'devices',
+ nodename: me.nodename,
+ diskType: 'unused',
+ includePartitions: true,
+ height: 200,
+ emptyText: gettext('No Disks unused'),
+ itemId: 'disklist',
+ },
+ ],
+ },
+ {
+ xtype: 'displayfield',
+ padding: '5 0 0 0',
+ userCls: 'pmx-hint',
+ value: 'Note: ZFS is not compatible with disks backed by a hardware ' +
+ 'RAID controller. For details see the reference documentation.',
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+
+});
+
+Ext.define('PVE.node.ZFSList', {
+ extend: 'Ext.grid.Panel',
+ xtype: 'pveZFSList',
+
+ viewModel: {
+ data: {
+ pool: '',
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ destroyPool: function() {
+ let me = this;
+ let vm = me.getViewModel();
+ let view = me.getView();
+
+ const pool = vm.get('pool');
+
+ if (!view.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!pool) {
+ throw "no pool specified";
+ }
+
+ Ext.create('PVE.window.SafeDestroyStorage', {
+ url: `/nodes/${view.nodename}/disks/zfs/${pool}`,
+ item: { id: pool },
+ taskName: 'zfsremove',
+ taskDone: () => { view.reload(); },
+ }).show();
+ },
+ },
+
+ stateful: true,
+ stateId: 'grid-node-zfs',
+ columns: [
+ {
+ text: gettext('Name'),
+ dataIndex: 'name',
+ flex: 1,
+ },
+ {
+ header: gettext('Size'),
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'size',
+ },
+ {
+ header: gettext('Free'),
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'free',
+ },
+ {
+ header: gettext('Allocated'),
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'alloc',
+ },
+ {
+ header: gettext('Fragmentation'),
+ renderer: function(value) {
+ return value.toString() + '%';
+ },
+ dataIndex: 'frag',
+ },
+ {
+ header: gettext('Health'),
+ renderer: PVE.Utils.render_zfs_health,
+ dataIndex: 'health',
+ },
+ {
+ header: gettext('Deduplication'),
+ hidden: true,
+ renderer: function(value) {
+ return value.toFixed(2).toString() + 'x';
+ },
+ dataIndex: 'dedup',
+ },
+ ],
+
+ rootVisible: false,
+ useArrows: true,
+
+ tbar: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: function() {
+ this.up('panel').reload();
+ },
+ },
+ {
+ text: gettext('Create') + ': ZFS',
+ handler: function() {
+ let view = this.up('panel');
+ Ext.create('PVE.node.CreateZFS', {
+ nodename: view.nodename,
+ listeners: {
+ destroy: () => view.reload(),
+ },
+ autoShow: true,
+ });
+ },
+ },
+ {
+ text: gettext('Detail'),
+ itemId: 'detailbtn',
+ disabled: true,
+ handler: function() {
+ let view = this.up('panel');
+ let selection = view.getSelection();
+ if (selection.length) {
+ view.show_detail(selection[0].get('name'));
+ }
+ },
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ data: {
+ pool: undefined,
+ },
+ bind: {
+ data: {
+ pool: "{pool}",
+ },
+ },
+ tpl: [
+ '',
+ 'Pool {pool}:',
+ '',
+ Ext.String.format(gettext('No {0} selected'), 'pool'),
+ '',
+ ],
+ },
+ {
+ text: gettext('More'),
+ iconCls: 'fa fa-bars',
+ disabled: true,
+ bind: {
+ disabled: '{!pool}',
+ },
+ menu: [
+ {
+ text: gettext('Destroy'),
+ itemId: 'remove',
+ iconCls: 'fa fa-fw fa-trash-o',
+ handler: 'destroyPool',
+ disabled: true,
+ bind: {
+ disabled: '{!pool}',
+ },
+ },
+ ],
+ },
+ ],
+
+ show_detail: function(zpool) {
+ let me = this;
+
+ Ext.create('Proxmox.window.ZFSDetail', {
+ zpool,
+ nodename: me.nodename,
+ }).show();
+ },
+
+ set_button_status: function() {
+ var me = this;
+ },
+
+ reload: function() {
+ var me = this;
+ me.store.load();
+ me.store.sort();
+ },
+
+ listeners: {
+ activate: function() {
+ this.reload();
+ },
+ selectionchange: function(model, selected) {
+ let me = this;
+ let vm = me.getViewModel();
+
+ me.down('#detailbtn').setDisabled(selected.length === 0);
+ vm.set('pool', selected[0]?.data.name || '');
+ },
+ itemdblclick: function(grid, record) {
+ this.show_detail(record.get('name'));
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.nodename = me.pveSelNode.data.node;
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ Ext.apply(me, {
+ store: {
+ fields: ['name', 'size', 'free', 'alloc', 'dedup', 'frag', 'health'],
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/nodes/${me.nodename}/disks/zfs`,
+ },
+ sorters: 'name',
+ },
+ });
+
+ me.callParent();
+
+ Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
+ me.reload();
+ },
+});
+
+Ext.define('Proxmox.node.NodeOptionsView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.proxmoxNodeOptionsView'],
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ cbindData: function(_initialconfig) {
+ let me = this;
+
+ let baseUrl = `/nodes/${me.nodename}/config`;
+ me.url = `/api2/json${baseUrl}`;
+ me.editorConfig = {
+ url: `/api2/extjs/${baseUrl}`,
+ };
+
+ return {};
+ },
+
+ listeners: {
+ itemdblclick: function() { this.run_editor(); },
+ activate: function() { this.rstore.startUpdate(); },
+ destroy: function() { this.rstore.stopUpdate(); },
+ deactivate: function() { this.rstore.stopUpdate(); },
+ },
+
+ tbar: [
+ {
+ text: gettext('Edit'),
+ xtype: 'proxmoxButton',
+ disabled: true,
+ handler: btn => btn.up('grid').run_editor(),
+ },
+ ],
+
+ gridRows: [
+ {
+ xtype: 'integer',
+ name: 'startall-onboot-delay',
+ text: gettext('Start on boot delay'),
+ minValue: 0,
+ maxValue: 300,
+ labelWidth: 130,
+ deleteEmpty: true,
+ renderer: function(value) {
+ if (value === undefined) {
+ return Proxmox.Utils.defaultText;
+ }
+
+ let secString = value === '1' ? gettext('Second') : gettext('Seconds');
+ return `${value} ${secString}`;
+ },
+ },
+ {
+ xtype: 'text',
+ name: 'wakeonlan',
+ text: gettext('MAC address for Wake on LAN'),
+ vtype: 'MacAddress',
+ labelWidth: 150,
+ deleteEmpty: true,
+ renderer: function(value) {
+ if (value === undefined) {
+ return Proxmox.Utils.NoneText;
+ }
+
+ return value;
+ },
+ },
+ ],
+});
+Ext.define('PVE.pool.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.pvePoolConfig',
+
+ onlineHelp: 'pveum_pools',
+
+ initComponent: function() {
+ var me = this;
+
+ var pool = me.pveSelNode.data.pool;
+ if (!pool) {
+ throw "no pool specified";
+ }
+
+ Ext.apply(me, {
+ title: Ext.String.format(gettext("Resource Pool") + ': ' + pool),
+ hstateid: 'pooltab',
+ items: [
+ {
+ title: gettext('Summary'),
+ iconCls: 'fa fa-book',
+ xtype: 'pvePoolSummary',
+ itemId: 'summary',
+ },
+ {
+ title: gettext('Members'),
+ xtype: 'pvePoolMembers',
+ iconCls: 'fa fa-th',
+ pool: pool,
+ itemId: 'members',
+ },
+ {
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ path: '/pool/' + pool,
+ },
+ ],
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.pool.StatusView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pvePoolStatusView'],
+ disabled: true,
+
+ title: gettext('Status'),
+ cwidth1: 150,
+ interval: 30000,
+ //height: 195,
+ initComponent: function() {
+ var me = this;
+
+ var pool = me.pveSelNode.data.pool;
+ if (!pool) {
+ throw "no pool specified";
+ }
+
+ var rows = {
+ comment: {
+ header: gettext('Comment'),
+ renderer: Ext.String.htmlEncode,
+ required: true,
+ },
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/pools/" + 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.window.IPInfo', {
+ extend: 'Ext.window.Window',
+ width: 600,
+ title: gettext('Guest Agent Network Information'),
+ height: 300,
+ layout: {
+ type: 'fit',
+ },
+ modal: true,
+ items: [
+ {
+ xtype: 'grid',
+ store: {},
+ 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) {
+ let view = this.up('pveAgentIPView');
+
+ var win = Ext.create('PVE.window.IPInfo');
+ win.down('grid').getStore().setData(view.nics);
+ win.show();
+ },
+ text: gettext('More'),
+ },
+ ],
+ },
+ ],
+
+ getDefaultIps: function(nics) {
+ var me = this;
+ var ips = [];
+ nics.forEach(function(nic) {
+ if (nic['hardware-address'] &&
+ nic['hardware-address'] !== '00:00:00:00:00:00' &&
+ nic['hardware-address'] !== '0:0:0:0:0:0') {
+ 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;
+ let agentRec = store.getById('agent');
+ let state = store.getById('status');
+
+ me.agent = agentRec && agentRec.data.value === 1;
+ me.running = state && state.data.value === 'running';
+
+ 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) {
+ 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, me.ipStore);
+
+ // if we already have info about the vm, use it immediately
+ if (me.rstore.getCount()) {
+ me.startIPStore(me.rstore, me.rstore.getData(), false);
+ }
+
+ // check if the guest agent is there on every statusstore load
+ me.mon(me.rstore, 'load', me.startIPStore, me);
+ },
+});
+Ext.define('PVE.qemu.AudioInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveAudioInputPanel',
+
+ // FIXME: enable once we bumped doc-gen so this ref is included
+ //onlineHelp: 'qm_audio_device',
+
+ onGetValues: function(values) {
+ var ret = PVE.Parser.printPropertyString(values);
+ if (ret === '') {
+ return {
+ 'delete': 'audio0',
+ };
+ }
+ return {
+ audio0: ret,
+ };
+ },
+
+ items: [{
+ name: 'device',
+ xtype: 'proxmoxKVComboBox',
+ value: 'ich9-intel-hda',
+ fieldLabel: gettext('Audio Device'),
+ comboItems: [
+ ['ich9-intel-hda', 'ich9-intel-hda'],
+ ['intel-hda', 'intel-hda'],
+ ['AC97', 'AC97'],
+ ],
+ }, {
+ name: 'driver',
+ xtype: 'proxmoxKVComboBox',
+ value: 'spice',
+ fieldLabel: gettext('Backend Driver'),
+ comboItems: [
+ ['spice', 'SPICE'],
+ ['none', `${Proxmox.Utils.NoneText} (${gettext('Dummy Device')})`],
+ ],
+ }],
+});
+
+Ext.define('PVE.qemu.AudioEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ vmconfig: undefined,
+
+ subject: gettext('Audio Device'),
+
+ items: [{
+ xtype: 'pveAudioInputPanel',
+ }],
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ me.load({
+ success: function(response) {
+ me.vmconfig = response.result.data;
+
+ var audio0 = me.vmconfig.audio0;
+ if (audio0) {
+ me.setValues(PVE.Parser.parsePropertyString(audio0));
+ }
+ },
+ });
+ },
+});
+Ext.define('pve-boot-order-entry', {
+ extend: 'Ext.data.Model',
+ fields: [
+ { name: 'name', type: 'string' },
+ { name: 'enabled', type: 'bool' },
+ { name: 'desc', type: 'string' },
+ ],
+});
+
+Ext.define('PVE.qemu.BootOrderPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuBootOrderPanel',
+
+ onlineHelp: 'qm_bootorder',
+
+ vmconfig: {}, // store loaded vm config
+ store: undefined,
+
+ inUpdate: false,
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ let me = this;
+
+ let grid = me.lookup('grid');
+ let marker = me.lookup('marker');
+ let emptyWarning = me.lookup('emptyWarning');
+
+ marker.originalValue = undefined;
+
+ view.store = Ext.create('Ext.data.Store', {
+ model: 'pve-boot-order-entry',
+ listeners: {
+ update: function() {
+ this.commitChanges();
+ let val = view.calculateValue();
+ if (marker.originalValue === undefined) {
+ marker.originalValue = val;
+ }
+ view.inUpdate = true;
+ marker.setValue(val);
+ view.inUpdate = false;
+ marker.checkDirty();
+ emptyWarning.setHidden(val !== '');
+ grid.getView().refresh();
+ },
+ },
+ });
+ grid.setStore(view.store);
+ },
+ },
+
+ isCloudinit: (v) => v.match(/media=cdrom/) && v.match(/[:/]vm-\d+-cloudinit/),
+
+ isDisk: function(value) {
+ return PVE.Utils.bus_match.test(value);
+ },
+
+ isBootdev: function(dev, value) {
+ return (this.isDisk(dev) && !this.isCloudinit(value)) ||
+ (/^net\d+/).test(dev) ||
+ (/^hostpci\d+/).test(dev) ||
+ ((/^usb\d+/).test(dev) && !(/spice/).test(value));
+ },
+
+ setVMConfig: function(vmconfig) {
+ let me = this;
+ me.vmconfig = vmconfig;
+
+ me.store.removeAll();
+
+ let boot = PVE.Parser.parsePropertyString(me.vmconfig.boot, "legacy");
+
+ let bootorder = [];
+ if (boot.order) {
+ bootorder = boot.order.split(';').map(dev => ({ name: dev, enabled: true }));
+ } else if (!(/^\s*$/).test(me.vmconfig.boot)) {
+ // legacy style, transform to new bootorder
+ let order = boot.legacy || 'cdn';
+ let bootdisk = me.vmconfig.bootdisk || undefined;
+
+ // get the first 4 characters (acdn)
+ // ignore the rest (there should never be more than 4)
+ let orderList = order.split('').slice(0, 4);
+
+ // build bootdev list
+ for (let i = 0; i < orderList.length; i++) {
+ let list = [];
+ if (orderList[i] === 'c') {
+ if (bootdisk !== undefined && me.vmconfig[bootdisk]) {
+ list.push(bootdisk);
+ }
+ } else if (orderList[i] === 'd') {
+ Ext.Object.each(me.vmconfig, function(key, value) {
+ if (me.isDisk(key) && value.match(/media=cdrom/) && !me.isCloudinit(value)) {
+ list.push(key);
+ }
+ });
+ } else if (orderList[i] === 'n') {
+ Ext.Object.each(me.vmconfig, function(key, value) {
+ if ((/^net\d+/).test(key)) {
+ list.push(key);
+ }
+ });
+ }
+
+ // Object.each iterates in random order, sort alphabetically
+ list.sort();
+ list.forEach(dev => bootorder.push({ name: dev, enabled: true }));
+ }
+ }
+
+ // add disabled devices as well
+ let disabled = [];
+ Ext.Object.each(me.vmconfig, function(key, value) {
+ if (me.isBootdev(key, value) &&
+ !Ext.Array.some(bootorder, x => x.name === key)) {
+ disabled.push(key);
+ }
+ });
+ disabled.sort();
+ disabled.forEach(dev => bootorder.push({ name: dev, enabled: false }));
+
+ // add descriptions
+ bootorder.forEach(entry => {
+ entry.desc = me.vmconfig[entry.name];
+ });
+
+ me.store.insert(0, bootorder);
+ me.store.fireEvent("update");
+ },
+
+ calculateValue: function() {
+ let me = this;
+ return me.store.getData().items
+ .filter(x => x.data.enabled)
+ .map(x => x.data.name)
+ .join(';');
+ },
+
+ onGetValues: function() {
+ let me = this;
+ // Note: we allow an empty value, so no 'delete' option
+ let val = { order: me.calculateValue() };
+ let res = { boot: PVE.Parser.printPropertyString(val) };
+ return res;
+ },
+
+ items: [
+ {
+ xtype: 'grid',
+ reference: 'grid',
+ margin: '0 0 5 0',
+ minHeight: 150,
+ defaults: {
+ sortable: false,
+ hideable: false,
+ draggable: false,
+ },
+ columns: [
+ {
+ header: '#',
+ flex: 4,
+ renderer: (value, metaData, record, rowIndex) => {
+ let dragHandle = "";
+ let idx = (rowIndex + 1).toString();
+ if (record.get('enabled')) {
+ return dragHandle + idx;
+ } else {
+ return dragHandle + "" + idx + "";
+ }
+ },
+ },
+ {
+ xtype: 'checkcolumn',
+ header: gettext('Enabled'),
+ dataIndex: 'enabled',
+ flex: 4,
+ },
+ {
+ header: gettext('Device'),
+ dataIndex: 'name',
+ flex: 6,
+ renderer: (value, metaData, record, rowIndex) => {
+ let desc = record.get('desc');
+
+ let icon = '', iconCls;
+ if (value.match(/^net\d+$/)) {
+ iconCls = 'exchange';
+ } else if (desc.match(/media=cdrom/)) {
+ metaData.tdCls = 'pve-itype-icon-cdrom';
+ } else {
+ iconCls = 'hdd-o';
+ }
+ if (iconCls !== undefined) {
+ metaData.tdCls += 'pve-itype-fa';
+ icon = ``;
+ }
+
+ return icon + value;
+ },
+ },
+ {
+ header: gettext('Description'),
+ dataIndex: 'desc',
+ flex: 20,
+ },
+ ],
+ viewConfig: {
+ plugins: {
+ ptype: 'gridviewdragdrop',
+ dragText: gettext('Drag and drop to reorder'),
+ },
+ },
+ listeners: {
+ drop: function() {
+ // doesn't fire automatically on reorder
+ this.getStore().fireEvent("update");
+ },
+ },
+ },
+ {
+ xtype: 'component',
+ html: gettext('Drag and drop to reorder'),
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'emptyWarning',
+ userCls: 'pmx-hint',
+ value: gettext('Warning: No devices selected, the VM will probably not boot!'),
+ },
+ {
+ // for dirty marking and 'reset' function
+ xtype: 'field',
+ reference: 'marker',
+ hidden: true,
+ setValue: function(val) {
+ let me = this;
+ let panel = me.up('pveQemuBootOrderPanel');
+
+ // on form reset, go back to original state
+ if (!panel.inUpdate) {
+ panel.setVMConfig(panel.vmconfig);
+ }
+
+ // not a subclass, so no callParent; just do it manually
+ me.setRawValue(me.valueToRaw(val));
+ return me.mixins.field.setValue.call(me, val);
+ },
+ },
+ ],
+});
+
+Ext.define('PVE.qemu.BootOrderEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ items: [{
+ xtype: 'pveQemuBootOrderPanel',
+ itemId: 'inputpanel',
+ }],
+
+ subject: gettext('Boot Order'),
+ width: 640,
+
+ initComponent: function() {
+ let me = this;
+ me.callParent();
+ me.load({
+ success: ({ result }) => me.down('#inputpanel').setVMConfig(result.data),
+ });
+ },
+});
+Ext.define('PVE.qemu.CDInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuCDInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var confid = me.confid || values.controller + values.deviceid;
+
+ me.drive.media = 'cdrom';
+ if (values.mediaType === 'iso') {
+ me.drive.file = values.cdimage;
+ } else if (values.mediaType === 'cdrom') {
+ me.drive.file = 'cdrom';
+ } else {
+ me.drive.file = 'none';
+ }
+
+ var params = {};
+
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+
+ return params;
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+
+ if (me.bussel) {
+ me.bussel.setVMConfig(vmconfig, 'cdrom');
+ }
+ },
+
+ setDrive: function(drive) {
+ var me = this;
+
+ var values = {};
+ if (drive.file === 'cdrom') {
+ values.mediaType = 'cdrom';
+ } else if (drive.file === 'none') {
+ values.mediaType = 'none';
+ } else {
+ values.mediaType = 'iso';
+ 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', {
+ withVirtIO: false,
+ });
+ items.push(me.bussel);
+ }
+
+ items.push({
+ xtype: 'radiofield',
+ name: 'mediaType',
+ inputValue: 'iso',
+ boxLabel: gettext('Use CD/DVD disc image file (iso)'),
+ checked: true,
+ listeners: {
+ change: function(f, value) {
+ if (!me.rendered) {
+ return;
+ }
+ me.down('field[name=cdstorage]').setDisabled(!value);
+ var cdImageField = me.down('field[name=cdimage]');
+ cdImageField.setDisabled(!value);
+ if (value) {
+ cdImageField.validate();
+ } else {
+ cdImageField.reset();
+ }
+ },
+ },
+ });
+
+ 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;
+
+ 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);
+ }
+ },
+ });
+ },
+});
+Ext.define('PVE.qemu.CIDriveInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveCIDriveInputPanel',
+
+ insideWizard: false,
+
+ vmconfig: {}, // used to select usused disks
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var drive = {};
+ var params = {};
+ drive.file = values.hdstorage + ":cloudinit";
+ drive.format = values.diskformat;
+ params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive);
+ return params;
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ setVMConfig: function(config) {
+ var me = this;
+ me.down('#drive').setVMConfig(config, 'cdrom');
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.drive = {};
+
+ me.items = [
+ {
+ xtype: 'pveControllerSelector',
+ withVirtIO: false,
+ itemId: 'drive',
+ fieldLabel: gettext('CloudInit Drive'),
+ name: 'drive',
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ itemId: 'storselector',
+ storageContent: 'images',
+ nodename: me.nodename,
+ hideSize: true,
+ },
+ ];
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.qemu.CIDriveEdit', {
+ extend: 'Proxmox.window.Edit',
+ xtype: 'pveCIDriveEdit',
+
+ isCreate: true,
+ subject: gettext('CloudInit Drive'),
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [{
+ xtype: 'pveCIDriveInputPanel',
+ itemId: 'cipanel',
+ nodename: nodename,
+ }];
+
+ me.callParent();
+
+ me.load({
+ success: function(response, opts) {
+ me.down('#cipanel').setVMConfig(response.result.data);
+ },
+ });
+ },
+});
+Ext.define('PVE.qemu.CloudInit', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ xtype: 'pveCiPanel',
+
+ onlineHelp: 'qm_cloud_init',
+
+ tbar: [
+ {
+ xtype: 'proxmoxButton',
+ disabled: true,
+ dangerous: true,
+ confirmMsg: function(rec) {
+ let view = this.up('grid');
+ var warn = gettext('Are you sure you want to remove entry {0}');
+
+ var entry = rec.data.key;
+ var msg = Ext.String.format(warn, "'"
+ + view.renderKey(entry, {}, rec) + "'");
+
+ return msg;
+ },
+ enableFn: function(record) {
+ let view = this.up('grid');
+ var caps = Ext.state.Manager.get('GuiCap');
+ if (view.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() {
+ let view = this.up('grid');
+ let records = view.getSelection();
+ if (!records || !records.length) {
+ return;
+ }
+
+ var id = records[0].data.key;
+ var match = id.match(/^net(\d+)$/);
+ if (match) {
+ id = 'ipconfig' + match[1];
+ }
+
+ var params = {};
+ params.delete = id;
+ Proxmox.Utils.API2Request({
+ url: view.baseurl + '/config',
+ waitMsgTarget: view,
+ method: 'PUT',
+ params: params,
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ callback: function() {
+ view.reload();
+ },
+ });
+ },
+ text: gettext('Remove'),
+ },
+ {
+ xtype: 'proxmoxButton',
+ disabled: true,
+ enableFn: function(rec) {
+ let view = this.up('pveCiPanel');
+ return !!view.rows[rec.data.key].editor;
+ },
+ handler: function() {
+ let view = this.up('grid');
+ view.run_editor();
+ },
+ text: gettext('Edit'),
+ },
+ '-',
+ {
+ xtype: 'button',
+ itemId: 'savebtn',
+ text: gettext('Regenerate Image'),
+ handler: function() {
+ let view = this.up('grid');
+ var eject_params = {};
+ var insert_params = {};
+ let disk = PVE.Parser.parseQemuDrive(view.ciDriveId, view.ciDrive);
+ var storage = '';
+ var stormatch = disk.file.match(/^([^:]+):/);
+ if (stormatch) {
+ storage = stormatch[1];
+ }
+ eject_params[view.ciDriveId] = 'none,media=cdrom';
+ insert_params[view.ciDriveId] = storage + ':cloudinit';
+
+ var failure = function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ };
+
+ Proxmox.Utils.API2Request({
+ url: view.baseurl + '/config',
+ waitMsgTarget: view,
+ method: 'PUT',
+ params: eject_params,
+ failure: failure,
+ callback: function() {
+ Proxmox.Utils.API2Request({
+ url: view.baseurl + '/config',
+ waitMsgTarget: view,
+ method: 'PUT',
+ params: insert_params,
+ failure: failure,
+ callback: function() {
+ view.reload();
+ },
+ });
+ },
+ });
+ },
+ },
+ ],
+
+ border: false,
+
+ set_button_status: function(rstore, records, success) {
+ if (!success || records.length < 1) {
+ return;
+ }
+ var me = this;
+ var found;
+ records.forEach(function(record) {
+ if (found) {
+ return;
+ }
+ var id = record.data.key;
+ var value = record.data.value;
+ var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit");
+ if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) {
+ found = id;
+ me.ciDriveId = found;
+ me.ciDrive = value;
+ }
+ });
+
+ 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;
+
+ let caps_ci = caps.vms['VM.Config.Cloudinit'] || caps.vms['VM.Config.Network'];
+ /* editor is string and object */
+ me.rows = {
+ ciuser: {
+ header: gettext('User'),
+ iconCls: 'fa fa-user',
+ never_delete: true,
+ defaultValue: '',
+ editor: caps_ci ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('User'),
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.defaultText,
+ fieldLabel: gettext('User'),
+ name: 'ciuser',
+ },
+ ],
+ } : undefined,
+ renderer: function(value) {
+ return value || Proxmox.Utils.defaultText;
+ },
+ },
+ cipassword: {
+ header: gettext('Password'),
+ iconCls: 'fa fa-unlock',
+ defaultValue: '',
+ editor: caps_ci ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Password'),
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ inputType: 'password',
+ deleteEmpty: true,
+ emptyText: Proxmox.Utils.noneText,
+ fieldLabel: gettext('Password'),
+ name: 'cipassword',
+ },
+ ],
+ } : undefined,
+ renderer: function(value) {
+ return 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_ci ? 'PVE.qemu.SSHKeyEdit' : undefined,
+ never_delete: true,
+ renderer: function(value) {
+ value = decodeURIComponent(value);
+ var keys = value.split('\n');
+ var text = [];
+ keys.forEach(function(key) {
+ if (key.length) {
+ let res = PVE.Parser.parseSSHKey(key);
+ if (res) {
+ key = Ext.String.htmlEncode(res.comment);
+ if (res.options) {
+ key += ' (' + gettext('with options') + ')';
+ }
+ text.push(key);
+ return;
+ }
+ // Most likely invalid at this point, so just stick to
+ // the old value.
+ text.push(Ext.String.htmlEncode(key));
+ }
+ });
+ if (text.length) {
+ return text.join('
');
+ } else {
+ return Proxmox.Utils.noneText;
+ }
+ },
+ defaultValue: '',
+ },
+ };
+ 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,
+ };
+ }
+
+ PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) {
+ me.rows[type+id] = {
+ visible: false,
+ };
+ });
+ me.callParent();
+ me.mon(me.rstore, 'load', me.set_button_status, me);
+ },
+});
+Ext.define('PVE.qemu.CmdMenu', {
+ extend: 'Ext.menu.Menu',
+
+ showSeparator: false,
+ initComponent: function() {
+ let me = this;
+
+ let info = me.pveSelNode.data;
+ if (!info.node) {
+ throw "no node name specified";
+ }
+ if (!info.vmid) {
+ throw "no VM ID specified";
+ }
+
+ let vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: `/nodes/${info.node}/${info.type}/${info.vmid}/status/${cmd}`,
+ method: 'POST',
+ failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ });
+ };
+ let confirmedVMCommand = (cmd, params, confirmTask) => {
+ let task = confirmTask || `qm${cmd}`;
+ let msg = Proxmox.Utils.format_task_description(task, info.vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, btn => {
+ if (btn === 'yes') {
+ vm_command(cmd, params);
+ }
+ });
+ };
+
+ let caps = Ext.state.Manager.get('GuiCap');
+ let standalone = PVE.data.ResourceStore.getNodes().length < 2;
+
+ let running = false, stopped = true, suspended = false;
+ switch (info.status) {
+ case 'running':
+ running = true;
+ stopped = false;
+ break;
+ case 'suspended':
+ stopped = false;
+ suspended = true;
+ break;
+ case 'paused':
+ stopped = false;
+ suspended = true;
+ break;
+ default: break;
+ }
+
+ me.title = "VM " + info.vmid;
+
+ me.items = [
+ {
+ text: gettext('Start'),
+ iconCls: 'fa fa-fw fa-play',
+ hidden: running || suspended,
+ disabled: running || suspended,
+ handler: () => vm_command('start'),
+ },
+ {
+ text: gettext('Pause'),
+ iconCls: 'fa fa-fw fa-pause',
+ hidden: stopped || suspended,
+ disabled: stopped || suspended,
+ handler: () => confirmedVMCommand('suspend', undefined, 'qmpause'),
+ },
+ {
+ text: gettext('Hibernate'),
+ iconCls: 'fa fa-fw fa-download',
+ hidden: stopped || suspended,
+ disabled: stopped || suspended,
+ tooltip: gettext('Suspend to disk'),
+ handler: () => confirmedVMCommand('suspend', { todisk: 1 }),
+ },
+ {
+ text: gettext('Resume'),
+ iconCls: 'fa fa-fw fa-play',
+ hidden: !suspended,
+ handler: () => vm_command('resume'),
+ },
+ {
+ text: gettext('Shutdown'),
+ iconCls: 'fa fa-fw fa-power-off',
+ disabled: stopped || suspended,
+ handler: () => confirmedVMCommand('shutdown'),
+ },
+ {
+ text: gettext('Stop'),
+ iconCls: 'fa fa-fw fa-stop',
+ disabled: stopped,
+ tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
+ handler: () => confirmedVMCommand('stop'),
+ },
+ {
+ text: gettext('Reboot'),
+ iconCls: 'fa fa-fw fa-refresh',
+ disabled: stopped,
+ tooltip: Ext.String.format(gettext('Reboot {0}'), 'VM'),
+ handler: () => confirmedVMCommand('reboot'),
+ },
+ {
+ xtype: 'menuseparator',
+ hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone'],
+ },
+ {
+ text: gettext('Migrate'),
+ iconCls: 'fa fa-fw fa-send-o',
+ hidden: standalone || !caps.vms['VM.Migrate'],
+ handler: function() {
+ Ext.create('PVE.window.Migrate', {
+ vmtype: 'qemu',
+ nodename: info.node,
+ vmid: info.vmid,
+ autoShow: true,
+ });
+ },
+ },
+ {
+ text: gettext('Clone'),
+ iconCls: 'fa fa-fw fa-clone',
+ hidden: !caps.vms['VM.Clone'],
+ handler: () => PVE.window.Clone.wrap(info.node, info.vmid, me.isTemplate, 'qemu'),
+ },
+ {
+ text: gettext('Convert to template'),
+ iconCls: 'fa fa-fw fa-file-o',
+ hidden: !caps.vms['VM.Allocate'],
+ handler: function() {
+ let msg = Proxmox.Utils.format_task_description('qmtemplate', info.vmid);
+ Ext.Msg.confirm(gettext('Confirm'), msg, btn => {
+ if (btn === 'yes') {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${info.node}/qemu/${info.vmid}/template`,
+ method: 'POST',
+ failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus),
+ });
+ }
+ });
+ },
+ },
+ { xtype: 'menuseparator' },
+ {
+ text: gettext('Console'),
+ iconCls: 'fa fa-fw fa-terminal',
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: `/nodes/${info.node}/qemu/${info.vmid}/status/current`,
+ failure: (response, opts) => Ext.Msg.alert('Error', response.htmlStatus),
+ success: function({ result: { data } }, opts) {
+ PVE.Utils.openDefaultConsoleWindow(
+ {
+ spice: data.spice,
+ xtermjs: data.serial,
+ },
+ 'kvm',
+ info.vmid,
+ info.node,
+ info.name,
+ );
+ },
+ });
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.qemu.Config', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.qemu.Config',
+
+ onlineHelp: 'chapter_virtual_machines',
+ userCls: 'proxmox-tags-full',
+
+ initComponent: function() {
+ var me = this;
+ var vm = me.pveSelNode.data;
+
+ var nodename = vm.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = vm.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var template = !!vm.template;
+
+ var running = !!vm.uptime;
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var base_url = '/nodes/' + nodename + "/qemu/" + vmid;
+
+ me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
+ url: '/api2/json' + base_url + '/status/current',
+ interval: 1000,
+ });
+
+ var vm_command = function(cmd, params) {
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: base_url + '/status/' + cmd,
+ waitMsgTarget: me,
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ });
+ };
+
+ var resumeBtn = Ext.create('Ext.Button', {
+ text: gettext('Resume'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ hidden: true,
+ handler: function() {
+ vm_command('resume');
+ },
+ iconCls: 'fa fa-play',
+ });
+
+ var startBtn = Ext.create('Ext.Button', {
+ text: gettext('Start'),
+ disabled: !caps.vms['VM.PowerMgmt'] || running,
+ hidden: template,
+ handler: function() {
+ vm_command('start');
+ },
+ iconCls: 'fa fa-play',
+ });
+
+ var migrateBtn = Ext.create('Ext.Button', {
+ text: gettext('Migrate'),
+ disabled: !caps.vms['VM.Migrate'],
+ hidden: PVE.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'],
+ 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'],
+ 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 = vm.hastate;
+ Ext.create('PVE.ha.VMResourceEdit', {
+ vmid: vmid,
+ isCreate: !ha || ha === 'unmanaged',
+ }).show();
+ },
+ },
+ {
+ text: gettext('Remove'),
+ itemId: 'removeBtn',
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ Ext.create('PVE.window.SafeDestroyGuest', {
+ url: base_url,
+ item: { type: 'VM', id: vmid },
+ taskName: 'qmdestroy',
+ }).show();
+ },
+ iconCls: 'fa fa-trash-o',
+ },
+ ],
+},
+ });
+
+ var shutdownBtn = Ext.create('PVE.button.Split', {
+ text: gettext('Shutdown'),
+ disabled: !caps.vms['VM.PowerMgmt'] || !running,
+ hidden: template,
+ confirmMsg: Proxmox.Utils.format_task_description('qmshutdown', vmid),
+ handler: function() {
+ vm_command('shutdown');
+ },
+ menu: {
+ items: [{
+ text: gettext('Reboot'),
+ disabled: !caps.vms['VM.PowerMgmt'],
+ tooltip: Ext.String.format(gettext('Shutdown, apply pending changes and reboot {0}'), 'VM'),
+ confirmMsg: Proxmox.Utils.format_task_description('qmreboot', vmid),
+ handler: function() {
+ vm_command("reboot");
+ },
+ iconCls: 'fa fa-refresh',
+ }, {
+ 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'],
+ tooltip: Ext.String.format(gettext('Reset {0} immediately'), 'VM'),
+ confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid),
+ handler: function() {
+ vm_command("reset");
+ },
+ iconCls: 'fa fa-bolt',
+ }],
+ },
+ iconCls: 'fa fa-power-off',
+ });
+
+ var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
+ disabled: !caps.vms['VM.Console'],
+ hidden: template,
+ consoleType: 'kvm',
+ // disable spice/xterm for default action until status api call succeeded
+ enableSpice: false,
+ enableXtermjs: false,
+ consoleName: vm.name,
+ nodename: nodename,
+ vmid: vmid,
+ });
+
+ var statusTxt = Ext.create('Ext.toolbar.TextItem', {
+ data: {
+ lock: undefined,
+ },
+ tpl: [
+ '',
+ ' ({lock})',
+ '',
+ ],
+ });
+
+ let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
+ tags: vm.tags,
+ canEdit: !!caps.vms['VM.Config.Options'],
+ listeners: {
+ change: function(tags) {
+ Proxmox.Utils.API2Request({
+ url: base_url + '/config',
+ method: 'PUT',
+ params: {
+ tags,
+ },
+ success: function() {
+ me.statusStore.load();
+ },
+ failure: function(response) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ me.statusStore.load();
+ },
+ });
+ },
+ },
+ });
+
+ let vm_text = `${vm.vmid} (${vm.name})`;
+
+ Ext.apply(me, {
+ title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm_text, nodename),
+ hstateid: 'kvmtab',
+ tbarSpacing: false,
+ tbar: [statusTxt, tagsContainer, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
+ defaults: { statusStore: me.statusStore },
+ items: [
+ {
+ title: gettext('Summary'),
+ xtype: 'pveGuestSummary',
+ iconCls: 'fa fa-book',
+ itemId: 'summary',
+ },
+ ],
+ });
+
+ if (caps.vms['VM.Console'] && !template) {
+ me.items.push({
+ title: gettext('Console'),
+ itemId: 'console',
+ iconCls: 'fa fa-terminal',
+ xtype: 'pveNoVncConsole',
+ vmid: vmid,
+ consoleType: 'kvm',
+ nodename: nodename,
+ });
+ }
+
+ me.items.push(
+ {
+ title: gettext('Hardware'),
+ itemId: 'hardware',
+ iconCls: 'fa fa-desktop',
+ xtype: 'PVE.qemu.HardwareView',
+ },
+ {
+ title: 'Cloud-Init',
+ itemId: 'cloudinit',
+ iconCls: 'fa fa-cloud',
+ xtype: 'pveCiPanel',
+ },
+ {
+ title: gettext('Options'),
+ iconCls: 'fa fa-gear',
+ itemId: 'options',
+ xtype: 'PVE.qemu.Options',
+ },
+ {
+ title: gettext('Task History'),
+ itemId: 'tasks',
+ xtype: 'proxmoxNodeTasks',
+ iconCls: 'fa fa-list-alt',
+ nodename: nodename,
+ preFilter: {
+ vmid,
+ },
+ },
+ );
+
+ if (caps.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'] ||
+ caps.vms['VM.Audit']) && !template) {
+ me.items.push({
+ title: gettext('Snapshots'),
+ iconCls: 'fa fa-history',
+ type: 'qemu',
+ xtype: 'pveGuestSnapshotTree',
+ itemId: 'snapshot',
+ });
+ }
+
+ if (caps.vms['VM.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();
+
+ var prevQMPStatus = 'unknown';
+ me.mon(me.statusStore, 'load', function(s, records, success) {
+ var status;
+ var qmpstatus;
+ var spice = false;
+ var xtermjs = false;
+ var lock;
+ var rec;
+
+ if (!success) {
+ status = qmpstatus = 'unknown';
+ } else {
+ rec = s.data.get('status');
+ status = rec ? rec.data.value : 'unknown';
+ rec = s.data.get('qmpstatus');
+ qmpstatus = rec ? rec.data.value : 'unknown';
+ rec = s.data.get('template');
+ template = rec ? rec.data.value : false;
+ rec = s.data.get('lock');
+ lock = rec ? rec.data.value : undefined;
+
+ spice = !!s.data.get('spice');
+ xtermjs = !!s.data.get('serial');
+ }
+
+ rec = s.data.get('tags');
+ tagsContainer.loadTags(rec?.data?.value);
+
+ if (template) {
+ return;
+ }
+
+ var resume = ['prelaunch', 'paused', 'suspended'].indexOf(qmpstatus) !== -1;
+
+ if (resume || lock === 'suspended') {
+ startBtn.setVisible(false);
+ resumeBtn.setVisible(true);
+ } else {
+ startBtn.setVisible(true);
+ resumeBtn.setVisible(false);
+ }
+
+ consoleBtn.setEnableSpice(spice);
+ consoleBtn.setEnableXtermJS(xtermjs);
+
+ statusTxt.update({ lock: lock });
+
+ let guest_running = status === 'running' &&
+ !(qmpstatus === "shutdown" || qmpstatus === "prelaunch");
+ startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || template || guest_running);
+
+ shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
+ me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
+ consoleBtn.setDisabled(template);
+
+ let wasStopped = ['prelaunch', 'stopped', 'suspended'].indexOf(prevQMPStatus) !== -1;
+ if (wasStopped && qmpstatus === 'running') {
+ let con = me.down('#console');
+ if (con) {
+ con.reload();
+ }
+ }
+
+ prevQMPStatus = qmpstatus;
+ });
+
+ me.on('afterrender', function() {
+ me.statusStore.startUpdate();
+ });
+
+ me.on('destroy', function() {
+ me.statusStore.stopUpdate();
+ });
+ },
+});
+Ext.define('PVE.qemu.CreateWizard', {
+ extend: 'PVE.window.Wizard',
+ alias: 'widget.pveQemuCreateWizard',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ viewModel: {
+ data: {
+ nodename: '',
+ current: {
+ scsihw: '',
+ },
+ },
+ formulas: {
+ cgroupMode: function(get) {
+ const nodeInfo = PVE.data.ResourceStore.getNodes().find(
+ node => node.node === get('nodename'),
+ );
+ return nodeInfo ? nodeInfo['cgroup-mode'] : 2;
+ },
+ },
+ },
+
+ cbindData: {
+ nodename: undefined,
+ },
+
+ subject: gettext('Virtual Machine'),
+
+ 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: 'pveMultiHDPanel',
+ bind: {
+ nodename: '{nodename}',
+ },
+ title: gettext('Disks'),
+ },
+ {
+ xtype: 'pveQemuProcessorPanel',
+ insideWizard: true,
+ title: gettext('CPU'),
+ },
+ {
+ xtype: 'pveQemuMemoryPanel',
+ insideWizard: true,
+ title: gettext('Memory'),
+ },
+ {
+ xtype: 'pveQemuNetworkInputPanel',
+ bind: {
+ nodename: '{nodename}',
+ },
+ title: gettext('Network'),
+ insideWizard: true,
+ },
+ {
+ title: gettext('Confirm'),
+ layout: 'fit',
+ items: [
+ {
+ xtype: 'grid',
+ store: {
+ model: 'KeyValue',
+ sorters: [{
+ property: 'key',
+ direction: 'ASC',
+ }],
+ },
+ columns: [
+ { header: 'Key', width: 150, dataIndex: 'key' },
+ { header: 'Value', flex: 1, dataIndex: 'value' },
+ ],
+ },
+ ],
+ 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.DisplayInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveDisplayInputPanel',
+ onlineHelp: 'qm_display',
+
+ onGetValues: function(values) {
+ let ret = PVE.Parser.printPropertyString(values, 'type');
+ if (ret === '') {
+ return { 'delete': 'vga' };
+ }
+ return { vga: ret };
+ },
+
+ items: [{
+ name: 'type',
+ xtype: 'proxmoxKVComboBox',
+ value: '__default__',
+ deleteEmpty: false,
+ fieldLabel: gettext('Graphic card'),
+ comboItems: Object.entries(PVE.Utils.kvm_vga_drivers),
+ validator: function(v) {
+ let cfg = this.up('proxmoxWindowEdit').vmconfig || {};
+
+ if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) {
+ let fmt = gettext("Serial interface '{0}' is not correctly configured.");
+ return Ext.String.format(fmt, v);
+ }
+ return true;
+ },
+ listeners: {
+ change: function(cb, val) {
+ if (!val) {
+ return;
+ }
+ let memoryfield = this.up('panel').down('field[name=memory]');
+ let disableMemoryField = false;
+
+ if (val === "cirrus") {
+ memoryfield.setEmptyText("4");
+ } else if (val === "std" || val.match(/^qxl\d?$/) || val === "vmware") {
+ memoryfield.setEmptyText("16");
+ } else if (val.match(/^virtio/)) {
+ memoryfield.setEmptyText("256");
+ } else if (val.match(/^(serial\d|none)$/)) {
+ memoryfield.setEmptyText("N/A");
+ disableMemoryField = true;
+ } else {
+ console.debug("unexpected display type", val);
+ memoryfield.setEmptyText(Proxmox.Utils.defaultText);
+ }
+ memoryfield.setDisabled(disableMemoryField);
+ },
+ },
+ },
+ {
+ 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() {
+ let me = this;
+
+ me.callParent();
+
+ me.load({
+ success: function(response) {
+ me.vmconfig = response.result.data;
+ let vga = me.vmconfig.vga || '__default__';
+ me.setValues(PVE.Parser.parsePropertyString(vga, 'type'));
+ },
+ });
+ },
+});
+/* 'change' property is assigned a string and then a function */
+Ext.define('PVE.qemu.HDInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuHDInputPanel',
+ onlineHelp: 'qm_hard_disk',
+
+ insideWizard: false,
+
+ unused: false, // ADD usused disk imaged
+
+ vmconfig: {}, // used to select usused disks
+
+ viewModel: {
+ data: {
+ isSCSI: false,
+ isVirtIO: false,
+ isSCSISingle: false,
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ onControllerChange: function(field) {
+ let me = this;
+ let vm = this.getViewModel();
+
+ let value = field.getValue();
+ vm.set('isSCSI', value.match(/^scsi/));
+ vm.set('isVirtIO', value.match(/^virtio/));
+
+ me.fireIdChange();
+ },
+
+ fireIdChange: function() {
+ let view = this.getView();
+ view.fireEvent('diskidchange', view, view.bussel.getConfId());
+ },
+
+ control: {
+ 'field[name=controller]': {
+ change: 'onControllerChange',
+ afterrender: 'onControllerChange',
+ },
+ 'field[name=deviceid]': {
+ change: 'fireIdChange',
+ },
+ 'field[name=scsiController]': {
+ change: function(f, value) {
+ let vm = this.getViewModel();
+ vm.set('isSCSISingle', value === 'virtio-scsi-single');
+ },
+ },
+ },
+
+ init: function(view) {
+ var vm = this.getViewModel();
+ if (view.isCreate) {
+ vm.set('isIncludedInBackup', true);
+ }
+ if (view.confid) {
+ vm.set('isSCSI', view.confid.match(/^scsi/));
+ vm.set('isVirtIO', view.confid.match(/^virtio/));
+ }
+ },
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var params = {};
+ var confid = me.confid || values.controller + values.deviceid;
+
+ if (me.unused) {
+ me.drive.file = me.vmconfig[values.unusedId];
+ confid = values.controller + values.deviceid;
+ } else if (me.isCreate) {
+ if (values.hdimage) {
+ me.drive.file = values.hdimage;
+ } else {
+ me.drive.file = values.hdstorage + ":" + values.disksize;
+ }
+ me.drive.format = values.diskformat;
+ }
+
+ PVE.Utils.propertyStringSet(me.drive, !values.backup, 'backup', '0');
+ PVE.Utils.propertyStringSet(me.drive, values.noreplicate, 'replicate', 'no');
+ PVE.Utils.propertyStringSet(me.drive, values.discard, 'discard', 'on');
+ PVE.Utils.propertyStringSet(me.drive, values.ssd, 'ssd', 'on');
+ PVE.Utils.propertyStringSet(me.drive, values.iothread, 'iothread', 'on');
+ PVE.Utils.propertyStringSet(me.drive, values.readOnly, 'ro', 'on');
+ PVE.Utils.propertyStringSet(me.drive, values.cache, 'cache');
+ PVE.Utils.propertyStringSet(me.drive, values.aio, 'aio');
+
+ ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'].forEach(name => {
+ let burst_name = `${name}_max`;
+ PVE.Utils.propertyStringSet(me.drive, values[name], name);
+ PVE.Utils.propertyStringSet(me.drive, values[burst_name], burst_name);
+ });
+
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+
+ return params;
+ },
+
+ updateVMConfig: function(vmconfig) {
+ var me = this;
+ me.vmconfig = vmconfig;
+ me.bussel?.updateVMConfig(vmconfig);
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+
+ me.vmconfig = vmconfig;
+
+ if (me.bussel) {
+ me.bussel.setVMConfig(vmconfig);
+ me.scsiController.setValue(vmconfig.scsihw);
+ }
+ if (me.unusedDisks) {
+ 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.backup = PVE.Parser.parseBoolean(drive.backup, 1);
+ values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1);
+ values.diskformat = drive.format || 'raw';
+ values.cache = drive.cache || '__default__';
+ values.discard = drive.discard === 'on';
+ values.ssd = PVE.Parser.parseBoolean(drive.ssd);
+ values.iothread = PVE.Parser.parseBoolean(drive.iothread);
+ values.readOnly = PVE.Parser.parseBoolean(drive.ro);
+ values.aio = drive.aio || '__default__';
+
+ values.mbps_rd = drive.mbps_rd;
+ values.mbps_wr = drive.mbps_wr;
+ values.iops_rd = drive.iops_rd;
+ values.iops_wr = drive.iops_wr;
+ values.mbps_rd_max = drive.mbps_rd_max;
+ values.mbps_wr_max = drive.mbps_wr_max;
+ values.iops_rd_max = drive.iops_rd_max;
+ values.iops_wr_max = drive.iops_wr_max;
+
+ me.setValues(values);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ hasAdvanced: true,
+
+ initComponent: function() {
+ var me = this;
+
+ me.drive = {};
+
+ let column1 = [];
+ let column2 = [];
+
+ let advancedColumn1 = [];
+ let advancedColumn2 = [];
+
+ if (!me.confid || me.unused) {
+ me.bussel = Ext.create('PVE.form.ControllerSelector', {
+ vmconfig: me.vmconfig,
+ selectFree: true,
+ });
+ column1.push(me.bussel);
+
+ me.scsiController = Ext.create('Ext.form.field.Display', {
+ fieldLabel: gettext('SCSI Controller'),
+ reference: 'scsiController',
+ name: 'scsiController',
+ bind: me.insideWizard ? {
+ value: '{current.scsihw}',
+ visible: '{isSCSI}',
+ } : {
+ visible: '{isSCSI}',
+ },
+ renderer: PVE.Utils.render_scsihw,
+ submitValue: false,
+ hidden: true,
+ });
+ column1.push(me.scsiController);
+ }
+
+ if (me.unused) {
+ me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', {
+ name: 'unusedId',
+ fieldLabel: gettext('Disk image'),
+ matchFieldWidth: false,
+ listConfig: {
+ width: 350,
+ },
+ data: [],
+ allowBlank: false,
+ });
+ column1.push(me.unusedDisks);
+ } else if (me.isCreate) {
+ column1.push({
+ xtype: 'pveDiskStorageSelector',
+ storageContent: 'images',
+ name: 'disk',
+ nodename: me.nodename,
+ autoSelect: me.insideWizard,
+ });
+ } else {
+ column1.push({
+ xtype: 'textfield',
+ disabled: true,
+ submitValue: false,
+ fieldLabel: gettext('Disk image'),
+ name: 'hdimage',
+ });
+ }
+
+ column2.push(
+ {
+ xtype: 'CacheTypeSelector',
+ name: 'cache',
+ value: '__default__',
+ fieldLabel: gettext('Cache'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Discard'),
+ reference: 'discard',
+ name: 'discard',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'iothread',
+ fieldLabel: 'IO thread',
+ clearOnDisable: true,
+ bind: me.insideWizard || me.isCreate ? {
+ disabled: '{!isVirtIO && !isSCSI}',
+ // Checkbox.setValue handles Arrays in a different way, therefore cast to bool
+ value: '{!!isVirtIO || (isSCSI && isSCSISingle)}',
+ } : {
+ disabled: '{!isVirtIO && !isSCSI}',
+ },
+ },
+ );
+
+ advancedColumn1.push(
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('SSD emulation'),
+ name: 'ssd',
+ clearOnDisable: true,
+ bind: {
+ disabled: '{isVirtIO}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'readOnly', // `ro` in the config, we map in get/set values
+ defaultValue: 0,
+ fieldLabel: gettext('Read-only'),
+ clearOnDisable: true,
+ bind: {
+ disabled: '{!isVirtIO && !isSCSI}',
+ },
+ },
+ );
+
+ advancedColumn2.push(
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Backup'),
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Include volume in backup job'),
+ },
+ name: 'backup',
+ bind: {
+ value: '{isIncludedInBackup}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Skip replication'),
+ name: 'noreplicate',
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'aio',
+ fieldLabel: gettext('Async IO'),
+ allowBlank: false,
+ value: '__default__',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + ' (io_uring)'],
+ ['io_uring', 'io_uring'],
+ ['native', 'native'],
+ ['threads', 'threads'],
+ ],
+ },
+ );
+
+ let labelWidth = 140;
+
+ let bwColumn1 = [
+ {
+ xtype: 'numberfield',
+ name: 'mbps_rd',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Read limit') + ' (MB/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited'),
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_wr',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Write limit') + ' (MB/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited'),
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_rd',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Read limit') + ' (ops/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited'),
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_wr',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Write limit') + ' (ops/s)',
+ labelWidth: labelWidth,
+ emptyText: gettext('unlimited'),
+ },
+ ];
+
+ let bwColumn2 = [
+ {
+ xtype: 'numberfield',
+ name: 'mbps_rd_max',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Read max burst') + ' (MB)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default'),
+ },
+ {
+ xtype: 'numberfield',
+ name: 'mbps_wr_max',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Write max burst') + ' (MB)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default'),
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_rd_max',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Read max burst') + ' (ops)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default'),
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'iops_wr_max',
+ minValue: 10,
+ step: 10,
+ fieldLabel: gettext('Write max burst') + ' (ops)',
+ labelWidth: labelWidth,
+ emptyText: gettext('default'),
+ },
+ ];
+
+ me.items = [
+ {
+ xtype: 'tabpanel',
+ plain: true,
+ bodyPadding: 10,
+ border: 0,
+ items: [
+ {
+ title: gettext('Disk'),
+ xtype: 'inputpanel',
+ reference: 'diskpanel',
+ column1,
+ column2,
+ advancedColumn1,
+ advancedColumn2,
+ showAdvanced: me.showAdvanced,
+ getValues: () => ({}),
+ },
+ {
+ title: gettext('Bandwidth'),
+ xtype: 'inputpanel',
+ reference: 'bwpanel',
+ column1: bwColumn1,
+ column2: bwColumn2,
+ showAdvanced: me.showAdvanced,
+ getValues: () => ({}),
+ },
+ ],
+ },
+ ];
+
+ me.callParent();
+ },
+
+ setAdvancedVisible: function(visible) {
+ this.lookup('diskpanel').setAdvancedVisible(visible);
+ this.lookup('bwpanel').setAdvancedVisible(visible);
+ },
+});
+
+Ext.define('PVE.qemu.HDEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ backgroundDelay: 5,
+
+ width: 600,
+ bodyPadding: 0,
+
+ 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,
+ });
+
+ if (unused) {
+ me.subject = gettext('Unused Disk');
+ } else if (me.isCreate) {
+ me.subject = gettext('Hard Disk');
+ } else {
+ me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
+ }
+
+ me.items = [ipanel];
+
+ me.callParent();
+ /* 'data' is assigned an empty array in same file, and here we
+ * use it like an object
+ */
+ me.load({
+ success: function(response, options) {
+ ipanel.setVMConfig(response.result.data);
+ if (me.confid) {
+ 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
+ }
+ },
+ });
+ },
+});
+Ext.define('PVE.qemu.EFIDiskInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveEFIDiskInputPanel',
+
+ insideWizard: false,
+
+ unused: false, // ADD usused disk imaged
+
+ vmconfig: {}, // used to select usused disks
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.disabled) {
+ return {};
+ }
+
+ var confid = 'efidisk0';
+
+ if (values.hdimage) {
+ me.drive.file = values.hdimage;
+ } else {
+ // we use 1 here, because for efi the size gets overridden from the backend
+ me.drive.file = values.hdstorage + ":1";
+ }
+
+ // always default to newer 4m type with secure boot support, if we're
+ // adding a new EFI disk there can't be any old state anyway
+ me.drive.efitype = '4m';
+ me.drive['pre-enrolled-keys'] = values.preEnrolledKeys;
+ delete values.preEnrolledKeys;
+
+ me.drive.format = values.diskformat;
+ let params = {};
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+ return params;
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ setDisabled: function(disabled) {
+ let me = this;
+ me.down('pveDiskStorageSelector').setDisabled(disabled);
+ me.down('proxmoxcheckbox[name=preEnrolledKeys]').setDisabled(disabled);
+ me.callParent(arguments);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.drive = {};
+
+ me.items = [
+ {
+ xtype: 'pveDiskStorageSelector',
+ name: 'efidisk0',
+ storageLabel: gettext('EFI Storage'),
+ storageContent: 'images',
+ nodename: me.nodename,
+ disabled: me.disabled,
+ hideSize: true,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'preEnrolledKeys',
+ checked: true,
+ fieldLabel: gettext("Pre-Enroll keys"),
+ disabled: me.disabled,
+ //boxLabel: '(e.g., Microsoft secure-boot keys')',
+ autoEl: {
+ tag: 'div',
+ 'data-qtip': gettext('Use EFIvars image with standard distribution and Microsoft secure boot keys enrolled.'),
+ },
+ },
+ {
+ xtype: 'label',
+ text: gettext("Warning: The VM currently does not uses 'OVMF (UEFI)' as BIOS."),
+ userCls: 'pmx-hint',
+ hidden: me.usesEFI,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.qemu.EFIDiskEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+ subject: gettext('EFI Disk'),
+
+ width: 450,
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [{
+ xtype: 'pveEFIDiskInputPanel',
+ onlineHelp: 'qm_bios_and_uefi',
+ confid: me.confid,
+ nodename: nodename,
+ usesEFI: me.usesEFI,
+ isCreate: true,
+ }];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.qemu.TPMDiskInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveTPMDiskInputPanel',
+
+ unused: false,
+ vmconfig: {},
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.disabled) {
+ return {};
+ }
+
+ var confid = 'tpmstate0';
+
+ if (values.hdimage) {
+ me.drive.file = values.hdimage;
+ } else {
+ // size is constant, so just use 1
+ me.drive.file = values.hdstorage + ":1";
+ }
+
+ me.drive.version = values.version;
+ var params = {};
+ params[confid] = PVE.Parser.printQemuDrive(me.drive);
+ return params;
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+ me.down('#hdstorage').setNodename(nodename);
+ me.down('#hdimage').setStorage(undefined, nodename);
+ },
+
+ setDisabled: function(disabled) {
+ let me = this;
+ me.down('pveDiskStorageSelector').setDisabled(disabled);
+ me.down('proxmoxKVComboBox[name=version]').setDisabled(disabled);
+ me.callParent(arguments);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.drive = {};
+
+ me.items = [
+ {
+ xtype: 'pveDiskStorageSelector',
+ name: me.disktype + '0',
+ storageLabel: gettext('TPM Storage'),
+ storageContent: 'images',
+ nodename: me.nodename,
+ disabled: me.disabled,
+ hideSize: true,
+ hideFormat: true,
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'version',
+ value: 'v2.0',
+ fieldLabel: gettext('Version'),
+ deleteEmpty: false,
+ disabled: me.disabled,
+ comboItems: [
+ ['v1.2', 'v1.2'],
+ ['v2.0', 'v2.0'],
+ ],
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.qemu.TPMDiskEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+ subject: gettext('TPM State'),
+
+ width: 450,
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.items = [{
+ xtype: 'pveTPMDiskInputPanel',
+ //onlineHelp: 'qm_tpm', FIXME: add once available
+ confid: me.confid,
+ nodename: nodename,
+ isCreate: true,
+ }];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.window.HDMove', {
+ extend: 'Proxmox.window.Edit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ resizable: false,
+ modal: true,
+ width: 350,
+ border: false,
+ layout: 'fit',
+ showReset: false,
+ showTaskViewer: true,
+ method: 'POST',
+
+ cbindData: function() {
+ let me = this;
+ return {
+ disk: me.disk,
+ isQemu: me.type === 'qemu',
+ nodename: me.nodename,
+ url: () => {
+ let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume';
+ return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`;
+ },
+ };
+ },
+
+ cbind: {
+ title: get => get('isQemu') ? gettext("Move disk") : gettext('Move Volume'),
+ submitText: get => get('title'),
+ qemu: '{isQemu}',
+ url: '{url}',
+ },
+
+ getValues: function() {
+ let me = this;
+ let values = me.formPanel.getForm().getValues();
+
+ let params = {
+ storage: values.hdstorage,
+ };
+ params[me.qemu ? 'disk' : 'volume'] = me.disk;
+
+ if (values.diskformat && me.qemu) {
+ params.format = values.diskformat;
+ }
+
+ if (values.deleteDisk) {
+ params.delete = 1;
+ }
+ return params;
+ },
+
+ items: [
+ {
+ xtype: 'form',
+ reference: 'moveFormPanel',
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%',
+ },
+ items: [
+ {
+ xtype: 'displayfield',
+ cbind: {
+ name: get => get('isQemu') ? 'disk' : 'volume',
+ fieldLabel: get => get('isQemu') ? gettext('Disk') : gettext('Mount Point'),
+ value: '{disk}',
+ },
+ allowBlank: false,
+ },
+ {
+ xtype: 'pveDiskStorageSelector',
+ storageLabel: gettext('Target Storage'),
+ cbind: {
+ nodename: '{nodename}',
+ storageContent: get => get('isQemu') ? 'images' : 'rootdir',
+ hideFormat: get => get('disk') === 'tpmstate0',
+ },
+ hideSize: true,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Delete source'),
+ name: 'deleteDisk',
+ uncheckedValue: 0,
+ checked: false,
+ },
+ ],
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.type) {
+ throw "no type specified";
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.window.HDResize', {
+ extend: 'Ext.window.Window',
+
+ resizable: false,
+
+ resize_disk: function(disk, size) {
+ var me = this;
+ var params = { disk: disk, size: '+' + size + 'G' };
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize',
+ waitMsgTarget: me,
+ method: 'PUT',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ 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();
+ },
+});
+Ext.define('PVE.qemu.HardwareView', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.PVE.qemu.HardwareView'],
+
+ onlineHelp: 'qm_virtual_machines_settings',
+
+ renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
+ var me = this;
+ var rows = me.rows;
+ var rowdef = rows[key] || {};
+ var iconCls = rowdef.iconCls;
+ var icon = '';
+ var txt = rowdef.header || key;
+
+ metaData.tdAttr = "valign=middle";
+
+ if (rowdef.isOnStorageBus) {
+ var value = me.getObjectValue(key, '', false);
+ if (value === '') {
+ value = me.getObjectValue(key, '', true);
+ }
+ if (value.match(/vm-.*-cloudinit/)) {
+ iconCls = 'cloud';
+ txt = rowdef.cloudheader;
+ } else if (value.match(/media=cdrom/)) {
+ metaData.tdCls = 'pve-itype-icon-cdrom';
+ return rowdef.cdheader;
+ }
+ }
+
+ if (rowdef.tdCls) {
+ metaData.tdCls = rowdef.tdCls;
+ } else if (iconCls) {
+ icon = "";
+ metaData.tdCls += " pve-itype-fa";
+ }
+
+ // only return icons in grid but not remove dialog
+ if (rowIndex !== undefined) {
+ return icon + txt;
+ } else {
+ return txt;
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ const { node: nodename, vmid } = me.pveSelNode.data;
+ if (!nodename) {
+ throw "no node name specified";
+ } else if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ const caps = Ext.state.Manager.get('GuiCap');
+ const diskCap = caps.vms['VM.Config.Disk'];
+ const cdromCap = caps.vms['VM.Config.CDROM'];
+
+ let isCloudInitKey = v => v && v.toString().match(/vm-.*-cloudinit/);
+
+ const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename);
+ let processorEditor = {
+ xtype: 'pveQemuProcessorEdit',
+ cgroupMode: nodeInfo['cgroup-mode'],
+ };
+
+ let rows = {
+ memory: {
+ header: gettext('Memory'),
+ editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
+ never_delete: true,
+ defaultValue: '512',
+ tdCls: 'pve-itype-icon-memory',
+ group: 2,
+ multiKey: ['memory', 'balloon', 'shares'],
+ 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']
+ ? processorEditor : undefined,
+ tdCls: 'pve-itype-icon-cpu',
+ 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);
+
+ let res = Ext.String.format(
+ '{0} ({1} sockets, {2} cores)', sockets * cores, sockets, cores);
+
+ if (model) {
+ res += ' [' + model + ']';
+ }
+ if (numa) {
+ res += ' [numa=' + numa +']';
+ }
+ if (vcpus) {
+ res += ' [vcpus=' + vcpus +']';
+ }
+ if (cpulimit) {
+ res += ' [cpulimit=' + cpulimit +']';
+ }
+ if (cpuunits) {
+ res += ' [cpuunits=' + cpuunits +']';
+ }
+
+ return res;
+ },
+ },
+ bios: {
+ header: 'BIOS',
+ group: 4,
+ never_delete: true,
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
+ defaultValue: '',
+ iconCls: 'microchip',
+ renderer: PVE.Utils.render_qemu_bios,
+ },
+ vga: {
+ header: gettext('Display'),
+ editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
+ never_delete: true,
+ iconCls: 'desktop',
+ group: 5,
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_vga_driver,
+ },
+ machine: {
+ header: gettext('Machine'),
+ editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined,
+ iconCls: 'cogs',
+ never_delete: true,
+ group: 6,
+ defaultValue: '',
+ renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
+ let ostype = me.getObjectValue('ostype', undefined, pending);
+ if (PVE.Utils.is_windows(ostype) &&
+ (!value || value === 'pc' || value === 'q35')) {
+ return value === 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1';
+ }
+ return PVE.Utils.render_qemu_machine(value);
+ },
+ },
+ scsihw: {
+ header: gettext('SCSI Controller'),
+ iconCls: 'database',
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
+ renderer: PVE.Utils.render_scsihw,
+ group: 7,
+ never_delete: true,
+ defaultValue: '',
+ },
+ vmstate: {
+ header: gettext('Hibernation VM State'),
+ iconCls: 'download',
+ del_extra_msg: gettext('The saved VM state will be permanently lost.'),
+ group: 100,
+ },
+ cores: {
+ visible: false,
+ },
+ cpu: {
+ visible: false,
+ },
+ numa: {
+ visible: false,
+ },
+ balloon: {
+ visible: false,
+ },
+ hotplug: {
+ visible: false,
+ },
+ vcpus: {
+ visible: false,
+ },
+ cpuunits: {
+ visible: false,
+ },
+ cpulimit: {
+ visible: false,
+ },
+ shares: {
+ visible: false,
+ },
+ ostype: {
+ visible: false,
+ },
+ };
+
+ PVE.Utils.forEachBus(undefined, function(type, id) {
+ let confid = type + id;
+ rows[confid] = {
+ group: 10,
+ iconCls: 'hdd-o',
+ editor: 'PVE.qemu.HDEdit',
+ isOnStorageBus: true,
+ header: gettext('Hard Disk') + ' (' + confid +')',
+ cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
+ cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')',
+ };
+ });
+ for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) {
+ let confid = "net" + i.toString();
+ rows[confid] = {
+ group: 15,
+ order: i,
+ iconCls: 'exchange',
+ editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
+ never_delete: !caps.vms['VM.Config.Network'],
+ header: gettext('Network Device') + ' (' + confid +')',
+ };
+ }
+ rows.efidisk0 = {
+ group: 20,
+ iconCls: 'hdd-o',
+ editor: null,
+ never_delete: !caps.vms['VM.Config.Disk'],
+ header: gettext('EFI Disk'),
+ };
+ rows.tpmstate0 = {
+ group: 22,
+ iconCls: 'hdd-o',
+ editor: null,
+ never_delete: !caps.vms['VM.Config.Disk'],
+ header: gettext('TPM State'),
+ };
+ for (let i = 0; i < PVE.Utils.hardware_counts.usb; i++) {
+ let confid = "usb" + i.toString();
+ rows[confid] = {
+ group: 25,
+ order: i,
+ iconCls: 'usb',
+ editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
+ never_delete: !caps.nodes['Sys.Console'],
+ header: gettext('USB Device') + ' (' + confid + ')',
+ };
+ }
+ for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
+ let confid = "hostpci" + i.toString();
+ rows[confid] = {
+ group: 30,
+ order: i,
+ tdCls: 'pve-itype-icon-pci',
+ never_delete: !caps.nodes['Sys.Console'],
+ editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
+ header: gettext('PCI Device') + ' (' + confid + ')',
+ };
+ }
+ for (let i = 0; i < PVE.Utils.hardware_counts.serial; i++) {
+ let confid = "serial" + i.toString();
+ rows[confid] = {
+ group: 35,
+ order: i,
+ tdCls: 'pve-itype-icon-serial',
+ never_delete: !caps.nodes['Sys.Console'],
+ header: gettext('Serial Port') + ' (' + confid + ')',
+ };
+ }
+ rows.audio0 = {
+ group: 40,
+ iconCls: 'volume-up',
+ editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
+ never_delete: !caps.vms['VM.Config.HWType'],
+ header: gettext('Audio Device'),
+ };
+ for (let i = 0; i < 256; i++) {
+ rows["unused" + i.toString()] = {
+ group: 99,
+ order: i,
+ iconCls: 'hdd-o',
+ del_extra_msg: gettext('This will permanently erase all data.'),
+ editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
+ header: gettext('Unused Disk') + ' ' + i.toString(),
+ };
+ }
+ rows.rng0 = {
+ group: 45,
+ tdCls: 'pve-itype-icon-die',
+ editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.RNGEdit' : undefined,
+ never_delete: !caps.nodes['Sys.Console'],
+ header: gettext("VirtIO RNG"),
+ };
+
+ var sorterFn = function(rec1, rec2) {
+ var v1 = rec1.data.key;
+ var v2 = rec2.data.key;
+ var g1 = rows[v1].group || 0;
+ var g2 = rows[v2].group || 0;
+ var order1 = rows[v1].order || 0;
+ var order2 = rows[v2].order || 0;
+
+ if (g1 - g2 !== 0) {
+ return g1 - g2;
+ }
+
+ if (order1 - order2 !== 0) {
+ return order1 - order2;
+ }
+
+ if (v1 > v2) {
+ return 1;
+ } else if (v1 < v2) {
+ return -1;
+ } else {
+ return 0;
+ }
+ };
+
+ let baseurl = `nodes/${nodename}/qemu/${vmid}/config`;
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec || !rows[rec.data.key]?.editor) {
+ return;
+ }
+ let rowdef = rows[rec.data.key];
+ let editor = rowdef.editor;
+
+ if (rowdef.isOnStorageBus) {
+ let value = me.getObjectValue(rec.data.key, '', true);
+ if (isCloudInitKey(value)) {
+ return;
+ } else if (value.match(/media=cdrom/)) {
+ editor = 'PVE.qemu.CDEdit';
+ } else if (!diskCap) {
+ return;
+ }
+ }
+
+ let commonOpts = {
+ autoShow: true,
+ pveSelNode: me.pveSelNode,
+ confid: rec.data.key,
+ url: `/api2/extjs/${baseurl}`,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ };
+
+ if (Ext.isString(editor)) {
+ Ext.create(editor, commonOpts);
+ } else {
+ let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor));
+ win.load();
+ }
+ };
+
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ selModel: sm,
+ disabled: true,
+ handler: run_editor,
+ });
+
+ let move_menuitem = new Ext.menu.Item({
+ text: gettext('Move Storage'),
+ tooltip: gettext('Move disk to another storage'),
+ iconCls: 'fa fa-database',
+ selModel: sm,
+ handler: () => {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ Ext.create('PVE.window.HDMove', {
+ autoShow: true,
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid,
+ type: 'qemu',
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+ });
+
+ let reassign_menuitem = new Ext.menu.Item({
+ text: gettext('Reassign Owner'),
+ tooltip: gettext('Reassign disk to another VM'),
+ iconCls: 'fa fa-desktop',
+ selModel: sm,
+ handler: () => {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ Ext.create('PVE.window.GuestDiskReassign', {
+ autoShow: true,
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid,
+ type: 'qemu',
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+ });
+
+ let resize_menuitem = new Ext.menu.Item({
+ text: gettext('Resize'),
+ iconCls: 'fa fa-plus',
+ selModel: sm,
+ handler: () => {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ Ext.create('PVE.window.HDResize', {
+ autoShow: true,
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+ });
+
+ let diskaction_btn = new Proxmox.button.Button({
+ text: gettext('Disk Action'),
+ disabled: true,
+ menu: {
+ items: [
+ move_menuitem,
+ reassign_menuitem,
+ resize_menuitem,
+ ],
+ },
+ });
+
+
+ let remove_btn = new Proxmox.button.Button({
+ text: gettext('Remove'),
+ defaultText: gettext('Remove'),
+ altText: gettext('Detach'),
+ selModel: sm,
+ disabled: true,
+ dangerous: true,
+ RESTMethod: 'PUT',
+ confirmMsg: function(rec) {
+ let warn = gettext('Are you sure you want to remove entry {0}');
+ if (this.text === this.altText) {
+ warn = gettext('Are you sure you want to detach entry {0}');
+ }
+ let rendered = me.renderKey(rec.data.key, {}, rec);
+ let msg = Ext.String.format(warn, `'${rendered}'`);
+
+ if (rows[rec.data.key].del_extra_msg) {
+ msg += '
' + rows[rec.data.key].del_extra_msg;
+ }
+ return msg;
+ },
+ handler: function(btn, e, rec) {
+ Proxmox.Utils.API2Request({
+ url: '/api2/extjs/' + baseurl,
+ waitMsgTarget: me,
+ method: btn.RESTMethod,
+ params: {
+ 'delete': rec.data.key,
+ },
+ callback: () => me.reload(),
+ failure: response => Ext.Msg.alert('Error', response.htmlStatus),
+ success: function(response, options) {
+ if (btn.RESTMethod === 'POST') {
+ Ext.create('Proxmox.window.TaskProgress', {
+ autoShow: true,
+ upid: response.result.data,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ }
+ },
+ });
+ },
+ listeners: {
+ render: function(btn) {
+ // hack: calculate the max button width on first display to prevent the whole
+ // toolbar to move when we switch between the "Remove" and "Detach" labels
+ var def = btn.getSize().width;
+
+ btn.setText(btn.altText);
+ var alt = btn.getSize().width;
+
+ btn.setText(btn.defaultText);
+
+ var optimal = alt > def ? alt : def;
+ btn.setSize({ width: optimal });
+ },
+ },
+ });
+
+ let revert_btn = new PVE.button.PendingRevert({
+ apiurl: '/api2/extjs/' + baseurl,
+ });
+
+ let efidisk_menuitem = Ext.create('Ext.menu.Item', {
+ text: gettext('EFI Disk'),
+ iconCls: 'fa fa-fw fa-hdd-o black',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: function() {
+ let { data: bios } = me.rstore.getData().map.bios || {};
+
+ Ext.create('PVE.qemu.EFIDiskEdit', {
+ autoShow: true,
+ url: '/api2/extjs/' + baseurl,
+ pveSelNode: me.pveSelNode,
+ usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf',
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+ });
+
+ let counts = {};
+ let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type];
+ let isAtUsbLimit = () => {
+ let ostype = me.getObjectValue('ostype');
+ let machine = me.getObjectValue('machine');
+ return counts.usb >= PVE.Utils.get_max_usb_count(ostype, machine);
+ };
+
+ let set_button_status = function() {
+ let selection_model = me.getSelectionModel();
+ let rec = selection_model.getSelection()[0];
+
+ counts = {}; // en/disable hardwarebuttons
+ let hasCloudInit = false;
+ me.rstore.getData().items.forEach(function({ id, data }) {
+ if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) {
+ hasCloudInit = true;
+ return;
+ }
+
+ let match = id.match(/^([^\d]+)\d+$/);
+ if (match && PVE.Utils.hardware_counts[match[1]] !== undefined) {
+ let type = match[1];
+ counts[type] = (counts[type] || 0) + 1;
+ }
+ });
+
+ // heuristic only for disabling some stuff, the backend has the final word.
+ const noSysConsolePerm = !caps.nodes['Sys.Console'];
+ const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
+ const noVMConfigNetPerm = !caps.vms['VM.Config.Network'];
+ const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk'];
+ const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM'];
+ const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit'];
+
+ me.down('#addUsb').setDisabled(noSysConsolePerm || isAtUsbLimit());
+ me.down('#addPci').setDisabled(noSysConsolePerm || isAtLimit('hostpci'));
+ me.down('#addAudio').setDisabled(noVMConfigHWTypePerm || isAtLimit('audio'));
+ me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial'));
+ me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net'));
+ me.down('#addRng').setDisabled(noSysConsolePerm || isAtLimit('rng'));
+ efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk'));
+ me.down('#addTpmState').setDisabled(noSysConsolePerm || isAtLimit('tpmstate'));
+ me.down('#addCloudinitDrive').setDisabled(noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit);
+
+ if (!rec) {
+ remove_btn.disable();
+ edit_btn.disable();
+ diskaction_btn.disable();
+ revert_btn.disable();
+ return;
+ }
+ const { key, value } = rec.data;
+ const row = rows[key];
+
+ const deleted = !!rec.data.delete;
+ const pending = deleted || me.hasPendingChanges(key);
+
+ const isCloudInit = isCloudInitKey(value);
+ const isCDRom = value && !!value.toString().match(/media=cdrom/);
+
+ const isUnusedDisk = key.match(/^unused\d+/);
+ const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
+ const isDisk = isUnusedDisk || isUsedDisk;
+ const isEfi = key === 'efidisk0';
+ const tpmMoveable = key === 'tpmstate0' && !me.pveSelNode.data.running;
+
+ let cannotDelete = deleted || row.never_delete;
+ cannotDelete ||= isCDRom && !cdromCap;
+ cannotDelete ||= isDisk && !diskCap;
+ cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm;
+ remove_btn.setDisabled(cannotDelete);
+
+ remove_btn.setText(isUsedDisk && !isCloudInit ? remove_btn.altText : remove_btn.defaultText);
+ remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
+
+ edit_btn.setDisabled(
+ deleted || !row.editor || isCloudInit || (isCDRom && !cdromCap) || (isDisk && !diskCap));
+
+ diskaction_btn.setDisabled(
+ pending ||
+ !diskCap ||
+ isCloudInit ||
+ !(isDisk || isEfi || tpmMoveable),
+ );
+ move_menuitem.setDisabled(isUnusedDisk);
+ reassign_menuitem.setDisabled(pending || (isEfi || tpmMoveable));
+ resize_menuitem.setDisabled(pending || !isUsedDisk);
+
+ revert_btn.setDisabled(!pending);
+ };
+
+ let editorFactory = (classPath, extraOptions) => {
+ extraOptions = extraOptions || {};
+ return () => Ext.create(`PVE.qemu.${classPath}`, {
+ autoShow: true,
+ url: `/api2/extjs/${baseurl}`,
+ pveSelNode: me.pveSelNode,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ isAdd: true,
+ isCreate: true,
+ ...extraOptions,
+ });
+ };
+
+ Ext.apply(me, {
+ url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
+ interval: 5000,
+ selModel: sm,
+ run_editor: run_editor,
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ cls: 'pve-add-hw-menu',
+ items: [
+ {
+ text: gettext('Hard Disk'),
+ iconCls: 'fa fa-fw fa-hdd-o black',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: editorFactory('HDEdit'),
+ },
+ {
+ text: gettext('CD/DVD Drive'),
+ iconCls: 'pve-itype-icon-cdrom',
+ disabled: !caps.vms['VM.Config.CDROM'],
+ handler: editorFactory('CDEdit'),
+ },
+ {
+ text: gettext('Network Device'),
+ itemId: 'addNet',
+ iconCls: 'fa fa-fw fa-exchange black',
+ disabled: !caps.vms['VM.Config.Network'],
+ handler: editorFactory('NetworkEdit'),
+ },
+ efidisk_menuitem,
+ {
+ text: gettext('TPM State'),
+ itemId: 'addTpmState',
+ iconCls: 'fa fa-fw fa-hdd-o black',
+ disabled: !caps.vms['VM.Config.Disk'],
+ handler: editorFactory('TPMDiskEdit'),
+ },
+ {
+ text: gettext('USB Device'),
+ itemId: 'addUsb',
+ iconCls: 'fa fa-fw fa-usb black',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: editorFactory('USBEdit'),
+ },
+ {
+ text: gettext('PCI Device'),
+ itemId: 'addPci',
+ iconCls: 'pve-itype-icon-pci',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: editorFactory('PCIEdit'),
+ },
+ {
+ text: gettext('Serial Port'),
+ itemId: 'addSerial',
+ iconCls: 'pve-itype-icon-serial',
+ disabled: !caps.vms['VM.Config.Options'],
+ handler: editorFactory('SerialEdit'),
+ },
+ {
+ text: gettext('CloudInit Drive'),
+ itemId: 'addCloudinitDrive',
+ iconCls: 'fa fa-fw fa-cloud black',
+ disabled: !caps.vms['VM.Config.CDROM'] || !caps.vms['VM.Config.Cloudinit'],
+ handler: editorFactory('CIDriveEdit'),
+ },
+ {
+ text: gettext('Audio Device'),
+ itemId: 'addAudio',
+ iconCls: 'fa fa-fw fa-volume-up black',
+ disabled: !caps.vms['VM.Config.HWType'],
+ handler: editorFactory('AudioEdit'),
+ },
+ {
+ text: gettext("VirtIO RNG"),
+ itemId: 'addRng',
+ iconCls: 'pve-itype-icon-die',
+ disabled: !caps.nodes['Sys.Console'],
+ handler: editorFactory('RNGEdit'),
+ },
+ ],
+ }),
+ },
+ remove_btn,
+ edit_btn,
+ diskaction_btn,
+ revert_btn,
+ ],
+ rows: rows,
+ sorterFn: sorterFn,
+ listeners: {
+ itemdblclick: run_editor,
+ selectionchange: set_button_status,
+ },
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate, me.rstore);
+ me.on('destroy', me.rstore.stopUpdate, me.rstore);
+
+ me.mon(me.getStore(), 'datachanged', set_button_status, me);
+ },
+});
+Ext.define('PVE.qemu.IPConfigPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveIPConfigPanel',
+
+ insideWizard: false,
+
+ vmconfig: {},
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (values.ipv4mode !== 'static') {
+ values.ip = values.ipv4mode;
+ }
+
+ if (values.ipv6mode !== 'static') {
+ values.ip6 = values.ipv6mode;
+ }
+
+ var params = {};
+
+ var cfg = PVE.Parser.printIPConfig(values);
+ if (cfg === '') {
+ params.delete = [me.confid];
+ } else {
+ params[me.confid] = cfg;
+ }
+ return params;
+ },
+
+ setVMConfig: function(config) {
+ var me = this;
+ me.vmconfig = config;
+ },
+
+ setIPConfig: function(confid, data) {
+ var me = this;
+
+ me.confid = confid;
+
+ if (data.ip === 'dhcp') {
+ data.ipv4mode = data.ip;
+ data.ip = '';
+ } else {
+ data.ipv4mode = 'static';
+ }
+ if (data.ip6 === 'dhcp' || data.ip6 === 'auto') {
+ data.ipv6mode = data.ip6;
+ data.ip6 = '';
+ } else {
+ data.ipv6mode = 'static';
+ }
+
+ me.ipconfig = data;
+ me.setValues(me.ipconfig);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.ipconfig = {};
+
+ me.column1 = [
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Network Device'),
+ value: me.netid,
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle',
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: gettext('IPv4') + ':',
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv4mode',
+ inputValue: 'static',
+ checked: false,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip]').setDisabled(!value);
+ me.down('field[name=gw]').setDisabled(!value);
+ },
+ },
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('DHCP'),
+ name: 'ipv4mode',
+ inputValue: 'dhcp',
+ checked: false,
+ margin: '0 0 0 10',
+ },
+ ],
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip',
+ vtype: 'IPCIDRAddress',
+ value: '',
+ disabled: true,
+ fieldLabel: gettext('IPv4/CIDR'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw',
+ value: '',
+ vtype: 'IPAddress',
+ disabled: true,
+ fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')',
+ },
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'displayfield',
+ },
+ {
+ layout: {
+ type: 'hbox',
+ align: 'middle',
+ },
+ border: false,
+ margin: '0 0 5 0',
+ items: [
+ {
+ xtype: 'label',
+ text: gettext('IPv6') + ':',
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('Static'),
+ name: 'ipv6mode',
+ inputValue: 'static',
+ checked: false,
+ margin: '0 0 0 10',
+ listeners: {
+ change: function(cb, value) {
+ me.down('field[name=ip6]').setDisabled(!value);
+ me.down('field[name=gw6]').setDisabled(!value);
+ },
+ },
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('DHCP'),
+ name: 'ipv6mode',
+ inputValue: 'dhcp',
+ checked: false,
+ margin: '0 0 0 10',
+ },
+ {
+ xtype: 'radiofield',
+ boxLabel: gettext('SLAAC'),
+ name: 'ipv6mode',
+ inputValue: 'auto',
+ checked: false,
+ margin: '0 0 0 10',
+ },
+ ],
+ },
+ {
+ xtype: 'textfield',
+ name: 'ip6',
+ value: '',
+ vtype: 'IP6CIDRAddress',
+ disabled: true,
+ fieldLabel: gettext('IPv6/CIDR'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'gw6',
+ vtype: 'IP6Address',
+ value: '',
+ disabled: true,
+ fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')',
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.qemu.IPConfigEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent: function() {
+ var me = this;
+
+ // convert confid from netX to ipconfigX
+ var match = me.confid.match(/^net(\d+)$/);
+ if (match) {
+ me.netid = me.confid;
+ me.confid = 'ipconfig' + match[1];
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = !me.confid;
+
+ var ipanel = Ext.create('PVE.qemu.IPConfigPanel', {
+ confid: me.confid,
+ netid: me.netid,
+ nodename: nodename,
+ });
+
+ Ext.applyIf(me, {
+ subject: gettext('Network Config'),
+ items: ipanel,
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ me.vmconfig = response.result.data;
+ var ipconfig = {};
+ var value = me.vmconfig[me.confid];
+ if (value) {
+ ipconfig = PVE.Parser.parseIPConfig(me.confid, value);
+ if (!ipconfig) {
+ Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration'));
+ me.close();
+ return;
+ }
+ }
+ ipanel.setIPConfig(me.confid, ipconfig);
+ ipanel.setVMConfig(me.vmconfig);
+ },
+ });
+ },
+});
+Ext.define('PVE.qemu.KeyboardEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ Ext.applyIf(me, {
+ subject: gettext('Keyboard Layout'),
+ items: {
+ xtype: 'VNCKeyboardSelector',
+ name: 'keyboard',
+ value: '__default__',
+ fieldLabel: gettext('Keyboard Layout'),
+ },
+ });
+
+ me.callParent();
+
+ me.load();
+ },
+});
+Ext.define('PVE.qemu.MachineInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveMachineInputPanel',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'combobox[name=machine]': {
+ change: 'onMachineChange',
+ },
+ },
+ onMachineChange: function(field, value) {
+ let me = this;
+ let version = me.lookup('version');
+ let store = version.getStore();
+ let oldRec = store.findRecord('id', version.getValue(), 0, false, false, true);
+ let type = value === 'q35' ? 'q35' : 'i440fx';
+ store.clearFilter();
+ store.addFilter(val => val.data.id === 'latest' || val.data.type === type);
+ if (!me.getView().isWindows) {
+ version.setValue('latest');
+ } else {
+ store.isWindows = true;
+ if (!oldRec) {
+ return;
+ }
+ let oldVers = oldRec.data.version;
+ // we already filtered by correct type, so just check version property
+ let rec = store.findRecord('version', oldVers, 0, false, false, true);
+ if (rec) {
+ version.select(rec);
+ }
+ }
+ },
+ },
+
+ onGetValues: function(values) {
+ if (values.version && values.version !== 'latest') {
+ values.machine = values.version;
+ delete values.delete;
+ }
+ delete values.version;
+ return values;
+ },
+
+ setValues: function(values) {
+ let me = this;
+
+ me.isWindows = values.isWindows;
+ if (values.machine === 'pc') {
+ values.machine = '__default__';
+ }
+
+ if (me.isWindows) {
+ if (values.machine === '__default__') {
+ values.version = 'pc-i440fx-5.1';
+ } else if (values.machine === 'q35') {
+ values.version = 'pc-q35-5.1';
+ }
+ }
+ if (values.machine !== '__default__' && values.machine !== 'q35') {
+ values.version = values.machine;
+ values.machine = values.version.match(/q35/) ? 'q35' : '__default__';
+
+ // avoid hiding a pinned version
+ me.setAdvancedVisible(true);
+ }
+
+ this.callParent(arguments);
+ },
+
+ items: {
+ xtype: 'proxmoxKVComboBox',
+ name: 'machine',
+ reference: 'machine',
+ fieldLabel: gettext('Machine'),
+ comboItems: [
+ ['__default__', PVE.Utils.render_qemu_machine('')],
+ ['q35', 'q35'],
+ ],
+ },
+
+ advancedItems: [
+ {
+ xtype: 'combobox',
+ name: 'version',
+ reference: 'version',
+ fieldLabel: gettext('Version'),
+ emptyText: gettext('Latest'),
+ value: 'latest',
+ editable: false,
+ valueField: 'id',
+ displayField: 'version',
+ queryParam: false,
+ store: {
+ autoLoad: true,
+ fields: ['id', 'type', 'version'],
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/nodes/localhost/capabilities/qemu/machines",
+ },
+ listeners: {
+ load: function(records) {
+ if (!this.isWindows) {
+ this.insert(0, { id: 'latest', type: 'any', version: gettext('Latest') });
+ }
+ },
+ },
+ },
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Note'),
+ value: gettext('Machine version change may affect hardware layout and settings in the guest OS.'),
+ },
+ ],
+});
+
+Ext.define('PVE.qemu.MachineEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ subject: gettext('Machine'),
+
+ items: {
+ xtype: 'pveMachineInputPanel',
+ },
+
+ width: 400,
+
+ initComponent: function() {
+ let me = this;
+
+ me.callParent();
+
+ me.load({
+ success: function(response) {
+ let conf = response.result.data;
+ let values = {
+ machine: conf.machine || '__default__',
+ };
+ values.isWindows = PVE.Utils.is_windows(conf.ostype);
+ me.setValues(values);
+ },
+ });
+ },
+});
+Ext.define('PVE.qemu.MemoryInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuMemoryPanel',
+ onlineHelp: 'qm_memory',
+
+ insideWizard: false,
+
+ viewModel: {}, // inherit data from createWizard if insideWizard
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ control: {
+ '#': {
+ afterrender: 'setMemory',
+ },
+ },
+
+ setMemory: function() {
+ let me = this;
+ let view = me.getView(), viewModel = me.getViewModel();
+ if (view.insideWizard) {
+ let memory = view.down('pveMemoryField[name=memory]');
+ // NOTE: we only set memory but that then sets balloon in its change handler
+ if (viewModel.get('current.ostype') === 'win11') {
+ memory.setValue('4096');
+ } else {
+ memory.setValue('2048');
+ }
+ }
+ },
+ },
+
+ onGetValues: function(values) {
+ 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',
+ value: '512', // better defaults get set via the view controllers afterrender
+ minValue: 1,
+ step: 32,
+ hotplug: me.hotplug,
+ listeners: {
+ change: function(f, value, old) {
+ var bf = me.down('field[name=balloon]');
+ var balloon = bf.getValue();
+ bf.setMaxValue(value);
+ if (balloon === old) {
+ bf.setValue(value);
+ }
+ bf.validate();
+ },
+ },
+ },
+ ];
+
+ me.advancedItems= [
+ {
+ xtype: 'pveMemoryField',
+ name: 'balloon',
+ minValue: 1,
+ maxValue: me.insideWizard ? 2048 : 512,
+ value: '512', // better defaults get set (indirectly) via the view controllers afterrender
+ step: 32,
+ fieldLabel: gettext('Minimum memory') + ' (MiB)',
+ hotplug: me.hotplug,
+ labelWidth: labelWidth,
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ var memory = me.down('field[name=memory]').getValue();
+ var shares = me.down('field[name=shares]');
+ shares.setDisabled(value === memory);
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'shares',
+ disabled: true,
+ minValue: 0,
+ maxValue: 50000,
+ value: '',
+ step: 10,
+ fieldLabel: gettext('Shares'),
+ labelWidth: labelWidth,
+ allowBlank: true,
+ emptyText: Proxmox.Utils.defaultText + ' (1000)',
+ submitEmptyText: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ labelWidth: labelWidth,
+ value: '1',
+ name: 'ballooning',
+ fieldLabel: gettext('Ballooning Device'),
+ listeners: {
+ change: function(f, value) {
+ var bf = me.down('field[name=balloon]');
+ var shares = me.down('field[name=shares]');
+ var memory = me.down('field[name=memory]');
+ bf.setDisabled(!value);
+ shares.setDisabled(!value || bf.getValue() === memory.getValue());
+ },
+ },
+ },
+ ];
+
+ 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.Monitor', {
+ extend: 'Ext.panel.Panel',
+
+ alias: 'widget.pveQemuMonitor',
+
+ // start to trim saved command output once there are *both*, more than `commandLimit` commands
+ // executed and the total of saved in+output is over `lineLimit` lines; repeat by dropping one
+ // full command output until either condition is false again
+ commandLimit: 10,
+ lineLimit: 5000,
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var history = [];
+ var histNum = -1;
+ let commands = [];
+
+ var textbox = Ext.createWidget('panel', {
+ region: 'center',
+ xtype: 'panel',
+ autoScroll: true,
+ border: true,
+ margins: '5 5 5 5',
+ bodyStyle: 'font-family: monospace;',
+ });
+
+ var scrollToEnd = function() {
+ var el = textbox.getTargetEl();
+ var dom = Ext.getDom(el);
+
+ var clientHeight = dom.clientHeight;
+ // BrowserBug: clientHeight reports 0 in IE9 StrictMode
+ // Instead we are using offsetHeight and hardcoding borders
+ if (Ext.isIE9 && Ext.isStrict) {
+ clientHeight = dom.offsetHeight + 2;
+ }
+ dom.scrollTop = dom.scrollHeight - clientHeight;
+ };
+
+ var refresh = function() {
+ textbox.update(`${commands.flat(2).join('\n')}`);
+ scrollToEnd();
+ };
+
+ let recordInput = line => {
+ commands.push([line]);
+
+ // drop oldest commands and their output until we're not over both limits anymore
+ while (commands.length > me.commandLimit && commands.flat(2).length > me.lineLimit) {
+ commands.shift();
+ }
+ };
+
+ let addResponse = lines => commands[commands.length - 1].push(lines);
+
+ var executeCmd = function(cmd) {
+ recordInput("# " + Ext.htmlEncode(cmd), true);
+ if (cmd) {
+ history.unshift(cmd);
+ if (history.length > 20) {
+ history.splice(20);
+ }
+ }
+ histNum = -1;
+
+ refresh();
+ Proxmox.Utils.API2Request({
+ params: { command: cmd },
+ url: '/nodes/' + nodename + '/qemu/' + vmid + "/monitor",
+ method: 'POST',
+ waitMsgTarget: me,
+ success: function(response, opts) {
+ var res = response.result.data;
+ addResponse(res.split('\n').map(line => Ext.htmlEncode(line)));
+ refresh();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ },
+ });
+ };
+
+ Ext.apply(me, {
+ layout: { type: 'border' },
+ border: false,
+ items: [
+ textbox,
+ {
+ region: 'south',
+ margins: '0 5 5 5',
+ border: false,
+ xtype: 'textfield',
+ name: 'cmd',
+ value: '',
+ fieldStyle: 'font-family: monospace;',
+ allowBlank: true,
+ listeners: {
+ afterrender: function(f) {
+ f.focus(false);
+ recordInput("Type 'help' for help.");
+ refresh();
+ },
+ specialkey: function(f, e) {
+ var key = e.getKey();
+ switch (key) {
+ case e.ENTER:
+ 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.MultiHDPanel', {
+ extend: 'PVE.panel.MultiDiskPanel',
+ alias: 'widget.pveMultiHDPanel',
+
+ onlineHelp: 'qm_hard_disk',
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ // maxCount is the sum of all controller ids - 1 (ide2 is fixed in the wizard)
+ maxCount: Object.values(PVE.Utils.diskControllerMaxIDs)
+ .reduce((previous, current) => previous+current, 0) - 1,
+
+ getNextFreeDisk: function(vmconfig) {
+ let clist = PVE.Utils.sortByPreviousUsage(vmconfig);
+ return PVE.Utils.nextFreeDisk(clist, vmconfig);
+ },
+
+ addPanel: function(itemId, vmconfig, nextFreeDisk) {
+ let me = this;
+ return me.getView().add({
+ vmconfig,
+ border: false,
+ showAdvanced: Ext.state.Manager.getProvider().get('proxmox-advanced-cb'),
+ xtype: 'pveQemuHDInputPanel',
+ bind: {
+ nodename: '{nodename}',
+ },
+ padding: '0 0 0 5',
+ itemId,
+ isCreate: true,
+ insideWizard: true,
+ });
+ },
+
+ getBaseVMConfig: function() {
+ let me = this;
+ let vm = me.getViewModel();
+
+ return {
+ ide2: 'media=cdrom',
+ scsihw: vm.get('current.scsihw'),
+ ostype: vm.get('current.ostype'),
+ };
+ },
+
+ diskSorter: {
+ sorterFn: function(rec1, rec2) {
+ let [, name1, id1] = PVE.Utils.bus_match.exec(rec1.data.name);
+ let [, name2, id2] = PVE.Utils.bus_match.exec(rec2.data.name);
+
+ if (name1 === name2) {
+ return parseInt(id1, 10) - parseInt(id2, 10);
+ }
+
+ return name1 < name2 ? -1 : 1;
+ },
+ },
+
+ deleteDisabled: () => false,
+ },
+});
+Ext.define('PVE.qemu.NetworkInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuNetworkInputPanel',
+ onlineHelp: 'qm_network_device',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+
+ me.network.model = values.model;
+ if (values.nonetwork) {
+ return {};
+ } else {
+ me.network.bridge = values.bridge;
+ me.network.tag = values.tag;
+ me.network.firewall = values.firewall;
+ }
+ me.network.macaddr = values.macaddr;
+ me.network.disconnect = values.disconnect;
+ me.network.queues = values.queues;
+ me.network.mtu = values.mtu;
+
+ if (values.rate) {
+ me.network.rate = values.rate;
+ } else {
+ delete me.network.rate;
+ }
+
+ var params = {};
+
+ params[me.confid] = PVE.Parser.printQemuNetwork(me.network);
+
+ return params;
+ },
+
+ viewModel: {
+ data: {
+ networkModel: undefined,
+ mtu: '',
+ },
+ formulas: {
+ isVirtio: get => get('networkModel') === 'virtio',
+ showMtuHint: get => get('mtu') === 1,
+ },
+ },
+
+ setNetwork: function(confid, data) {
+ var me = this;
+
+ me.confid = confid;
+
+ if (data) {
+ data.networkmode = data.bridge ? 'bridge' : 'nat';
+ } else {
+ data = {};
+ data.networkmode = 'bridge';
+ }
+ me.network = data;
+
+ me.setValues(me.network);
+ },
+
+ setNodename: function(nodename) {
+ var me = this;
+
+ me.bridgesel.setNodename(nodename);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.network = {};
+ me.confid = 'net0';
+
+ me.column1 = [];
+ me.column2 = [];
+
+ me.bridgesel = Ext.create('PVE.form.BridgeSelector', {
+ name: 'bridge',
+ fieldLabel: gettext('Bridge'),
+ nodename: me.nodename,
+ autoSelect: true,
+ allowBlank: false,
+ });
+
+ me.column1 = [
+ me.bridgesel,
+ {
+ xtype: 'pveVlanField',
+ name: 'tag',
+ value: '',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Firewall'),
+ name: 'firewall',
+ checked: me.insideWizard || me.isCreate,
+ },
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Disconnect'),
+ name: 'disconnect',
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mtu',
+ fieldLabel: 'MTU',
+ bind: {
+ disabled: '{!isVirtio}',
+ value: '{mtu}',
+ },
+ emptyText: '1500 (1 = bridge MTU)',
+ minValue: 1,
+ maxValue: 65520,
+ allowBlank: true,
+ validator: val => val === '' || val >= 576 || val === '1'
+ ? true
+ : gettext('MTU needs to be >= 576 or 1 to inherit the MTU from the underlying bridge.'),
+ },
+ ];
+
+ if (me.insideWizard) {
+ me.column1.unshift({
+ xtype: 'checkbox',
+ name: 'nonetwork',
+ inputValue: 'none',
+ boxLabel: gettext('No network device'),
+ listeners: {
+ change: function(cb, value) {
+ var fields = [
+ 'disconnect',
+ 'bridge',
+ 'tag',
+ 'firewall',
+ 'model',
+ 'macaddr',
+ 'rate',
+ 'queues',
+ 'mtu',
+ ];
+ fields.forEach(function(fieldname) {
+ me.down('field[name='+fieldname+']').setDisabled(value);
+ });
+ me.down('field[name=bridge]').validate();
+ },
+ },
+ });
+ me.column2.unshift({
+ xtype: 'displayfield',
+ });
+ }
+
+ me.column2.push(
+ {
+ xtype: 'pveNetworkCardSelector',
+ name: 'model',
+ fieldLabel: gettext('Model'),
+ bind: '{networkModel}',
+ value: PVE.qemu.OSDefaults.generic.networkCard,
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'macaddr',
+ fieldLabel: gettext('MAC address'),
+ vtype: 'MacAddress',
+ allowBlank: true,
+ emptyText: 'auto',
+ });
+ me.advancedColumn2 = [
+ {
+ xtype: 'numberfield',
+ name: 'rate',
+ fieldLabel: gettext('Rate limit') + ' (MB/s)',
+ minValue: 0,
+ maxValue: 10*1024,
+ value: '',
+ emptyText: 'unlimited',
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'queues',
+ fieldLabel: 'Multiqueue',
+ minValue: 1,
+ maxValue: 64,
+ value: '',
+ allowBlank: true,
+ },
+ ];
+ me.advancedColumnB = [
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext("Use the special value '1' to inherit the MTU value from the underlying bridge"),
+ bind: {
+ hidden: '{!showMtuHint}',
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.qemu.NetworkEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ isAdd: true,
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ me.isCreate = !me.confid;
+
+ var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', {
+ confid: me.confid,
+ nodename: nodename,
+ isCreate: me.isCreate,
+ });
+
+ Ext.applyIf(me, {
+ subject: gettext('Network Device'),
+ items: ipanel,
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var i, confid;
+ me.vmconfig = response.result.data;
+ if (!me.isCreate) {
+ 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;
+ }
+ }
+
+ let ostype = me.vmconfig.ostype;
+ let defaults = PVE.qemu.OSDefaults.getDefaults(ostype);
+ let data = {
+ model: defaults.networkCard,
+ };
+
+ ipanel.setNetwork(me.confid, data);
+ }
+ },
+ });
+ },
+});
+/*
+ * This class holds performance *recommended* settings for the PVE Qemu wizards
+ * the *mandatory* settings are set in the PVE::QemuServer
+ * config_to_command sub
+ * We store this here until we get the data from the API server
+*/
+
+// this is how you would add an hypothetic FreeBSD > 10 entry
+//
+//virtio-blk is stable but virtIO net still
+// problematic as of 10.3
+// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059
+// addOS({
+// parent: 'generic', // inherits defaults
+// pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js
+// busType: 'virtio' // must match a pveBusController value
+// // networkCard muss match a pveNetworkCardSelector
+
+
+Ext.define('PVE.qemu.OSDefaults', {
+ singleton: true, // will also force creation when loaded
+
+ constructor: function() {
+ let me = this;
+
+ let addOS = function(settings) {
+ if (Object.prototype.hasOwnProperty.call(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-single',
+ };
+
+ // 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.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);
+ this.getViewModel().set('current.ostype', ostype);
+ },
+ setWidget: function(widget, newValue) {
+ // changing a widget is safe only if ComponentQuery.query returns us
+ // a single value array
+ var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget);
+ if (widgets.length === 1) {
+ widgets[0].setValue(newValue);
+ } else {
+ // ignore multiple disks, we only want to set the type if there is a single disk
+ }
+ },
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: 'displayfield',
+ value: gettext('Guest OS') + ':',
+ hidden: !me.insideWizard,
+ },
+ {
+ xtype: 'combobox',
+ submitValue: false,
+ name: 'osbase',
+ fieldLabel: gettext('Type'),
+ editable: false,
+ queryMode: 'local',
+ value: 'Linux',
+ store: Object.keys(PVE.Utils.kvm_ostypes),
+ },
+ {
+ xtype: 'combobox',
+ name: 'ostype',
+ reference: 'ostype',
+ fieldLabel: gettext('Version'),
+ value: 'l26',
+ allowBlank: false,
+ editable: false,
+ queryMode: 'local',
+ valueField: 'val',
+ displayField: 'desc',
+ store: {
+ fields: ['desc', 'val'],
+ data: PVE.Utils.kvm_ostypes.Linux,
+ listeners: {
+ datachanged: function(store) {
+ var ostype = me.lookup('ostype');
+ var old_val = ostype.getValue();
+ if (!me.insideWizard && old_val && store.find('val', old_val) !== -1) {
+ ostype.setValue(old_val);
+ } else {
+ ostype.setValue(store.getAt(0));
+ }
+ },
+ },
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.qemu.OSTypeEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ subject: 'OS Type',
+
+ items: [{ xtype: 'pveQemuOSTypePanel' }],
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var value = response.result.data.ostype || 'other';
+ var osinfo = PVE.Utils.get_kvm_osinfo(value);
+ me.setValues({ ostype: value, osbase: osinfo.base });
+ },
+ });
+ },
+});
+Ext.define('PVE.qemu.Options', {
+ extend: 'Proxmox.grid.PendingObjectGrid',
+ alias: ['widget.PVE.qemu.Options'],
+
+ onlineHelp: 'qm_options',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var vmid = me.pveSelNode.data.vmid;
+ if (!vmid) {
+ throw "no VM ID specified";
+ }
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ var rows = {
+ name: {
+ required: true,
+ defaultValue: me.pveSelNode.data.name,
+ header: gettext('Name'),
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Name'),
+ items: {
+ xtype: 'inputpanel',
+ items: {
+ xtype: 'textfield',
+ name: 'name',
+ vtype: 'DnsName',
+ value: '',
+ fieldLabel: gettext('Name'),
+ allowBlank: true,
+ },
+ onGetValues: function(values) {
+ var params = values;
+ if (values.name === undefined ||
+ values.name === null ||
+ values.name === '') {
+ params = { 'delete': 'name' };
+ }
+ return params;
+ },
+ },
+ } : undefined,
+ },
+ onboot: {
+ header: gettext('Start at boot'),
+ defaultValue: '',
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Start at boot'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'onboot',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Start at boot'),
+ },
+ } : undefined,
+ },
+ startup: {
+ header: gettext('Start/Shutdown order'),
+ defaultValue: '',
+ renderer: PVE.Utils.render_kvm_startup,
+ editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify']
+ ? {
+ xtype: 'pveWindowStartupEdit',
+ onlineHelp: 'qm_startup_and_shutdown',
+ } : undefined,
+ },
+ ostype: {
+ header: gettext('OS Type'),
+ editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined,
+ renderer: PVE.Utils.render_kvm_ostype,
+ defaultValue: 'other',
+ },
+ bootdisk: {
+ visible: false,
+ },
+ boot: {
+ header: gettext('Boot Order'),
+ defaultValue: 'cdn',
+ editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined,
+ multiKey: ['boot', 'bootdisk'],
+ renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) {
+ if (/^\s*$/.test(order)) {
+ return gettext('(No boot device selected)');
+ }
+ let boot = PVE.Parser.parsePropertyString(order, "legacy");
+ if (boot.order) {
+ let list = boot.order.split(';');
+ let ret = '';
+ list.forEach(dev => {
+ if (ret) {
+ ret += ', ';
+ }
+ ret += dev;
+ });
+ return ret;
+ }
+
+ // legacy style and fallback
+ let i;
+ var text = '';
+ var bootdisk = me.getObjectValue('bootdisk', undefined, pending);
+ order = boot.legacy || 'cdn';
+ for (i = 0; i < order.length; i++) {
+ if (text) {
+ text += ', ';
+ }
+ var sel = order.substring(i, i + 1);
+ if (sel === 'c') {
+ if (bootdisk) {
+ text += bootdisk;
+ } else {
+ text += gettext('first disk');
+ }
+ } else if (sel === 'n') {
+ text += gettext('any net');
+ } else if (sel === 'a') {
+ text += gettext('Floppy');
+ } else if (sel === 'd') {
+ text += gettext('any CD-ROM');
+ } else {
+ text += sel;
+ }
+ }
+ return text;
+ },
+ },
+ tablet: {
+ header: gettext('Use tablet for pointer'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Use tablet for pointer'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'tablet',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled'),
+ },
+ } : undefined,
+ },
+ hotplug: {
+ header: gettext('Hotplug'),
+ defaultValue: 'disk,network,usb',
+ renderer: PVE.Utils.render_hotplug_features,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Hotplug'),
+ items: {
+ xtype: 'pveHotplugFeatureSelector',
+ name: 'hotplug',
+ value: '',
+ multiSelect: true,
+ fieldLabel: gettext('Hotplug'),
+ allowBlank: true,
+ },
+ } : undefined,
+ },
+ acpi: {
+ header: gettext('ACPI support'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('ACPI support'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'acpi',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled'),
+ },
+ } : undefined,
+ },
+ kvm: {
+ header: gettext('KVM hardware virtualization'),
+ defaultValue: true,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.HWType'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('KVM hardware virtualization'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'kvm',
+ checked: true,
+ uncheckedValue: 0,
+ defaultValue: 1,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled'),
+ },
+ } : undefined,
+ },
+ freeze: {
+ header: gettext('Freeze CPU at startup'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.PowerMgmt'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Freeze CPU at startup'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'freeze',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ labelWidth: 140,
+ fieldLabel: gettext('Freeze CPU at startup'),
+ },
+ } : undefined,
+ },
+ localtime: {
+ header: gettext('Use local time for RTC'),
+ defaultValue: '__default__',
+ renderer: PVE.Utils.render_localtime,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Use local time for RTC'),
+ width: 400,
+ items: {
+ xtype: 'proxmoxKVComboBox',
+ name: 'localtime',
+ value: '__default__',
+ comboItems: [
+ ['__default__', PVE.Utils.render_localtime('__default__')],
+ [1, PVE.Utils.render_localtime(1)],
+ [0, PVE.Utils.render_localtime(0)],
+ ],
+ labelWidth: 140,
+ fieldLabel: gettext('Use local time for RTC'),
+ },
+ } : undefined,
+ },
+ startdate: {
+ header: gettext('RTC start date'),
+ defaultValue: 'now',
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('RTC start date'),
+ items: {
+ xtype: 'proxmoxtextfield',
+ name: 'startdate',
+ deleteEmpty: true,
+ value: 'now',
+ fieldLabel: gettext('RTC start date'),
+ vtype: 'QemuStartDate',
+ allowBlank: true,
+ },
+ } : undefined,
+ },
+ smbios1: {
+ header: gettext('SMBIOS settings (type1)'),
+ defaultValue: '',
+ renderer: Ext.String.htmlEncode,
+ editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined,
+ },
+ agent: {
+ header: 'QEMU Guest Agent',
+ defaultValue: false,
+ renderer: PVE.Utils.render_qga_features,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Qemu Agent'),
+ width: 350,
+ onlineHelp: 'qm_qemu_agent',
+ items: {
+ xtype: 'pveAgentFeatureSelector',
+ name: 'agent',
+ },
+ } : undefined,
+ },
+ protection: {
+ header: gettext('Protection'),
+ defaultValue: false,
+ renderer: Proxmox.Utils.format_boolean,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Protection'),
+ items: {
+ xtype: 'proxmoxcheckbox',
+ name: 'protection',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Enabled'),
+ },
+ } : undefined,
+ },
+ spice_enhancements: {
+ header: gettext('Spice Enhancements'),
+ defaultValue: false,
+ renderer: PVE.Utils.render_spice_enhancements,
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('Spice Enhancements'),
+ onlineHelp: 'qm_spice_enhancements',
+ items: {
+ xtype: 'pveSpiceEnhancementSelector',
+ name: 'spice_enhancements',
+ },
+ } : undefined,
+ },
+ vmstatestorage: {
+ header: gettext('VM State storage'),
+ defaultValue: '',
+ renderer: val => val || gettext('Automatic'),
+ editor: caps.vms['VM.Config.Options'] ? {
+ xtype: 'proxmoxWindowEdit',
+ subject: gettext('VM State storage'),
+ onlineHelp: 'chapter_virtual_machines', // FIXME: use 'qm_vmstatestorage' once available
+ width: 350,
+ items: {
+ xtype: 'pveStorageSelector',
+ storageContent: 'images',
+ allowBlank: true,
+ emptyText: gettext("Automatic (Storage used by the VM, or 'local')"),
+ autoSelect: false,
+ deleteEmpty: true,
+ skipEmptyText: true,
+ nodename: nodename,
+ name: 'vmstatestorage',
+ },
+ } : undefined,
+ },
+ hookscript: {
+ header: gettext('Hookscript'),
+ },
+ };
+
+ var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
+
+ var edit_btn = new Ext.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ handler: function() { me.run_editor(); },
+ });
+
+ var revert_btn = new PVE.button.PendingRevert();
+
+ var set_button_status = function() {
+ var sm = me.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ if (!rec) {
+ edit_btn.disable();
+ return;
+ }
+
+ var key = rec.data.key;
+ var pending = rec.data.delete || me.hasPendingChanges(key);
+ var rowdef = rows[key];
+
+ edit_btn.setDisabled(!rowdef.editor);
+ revert_btn.setDisabled(!pending);
+ };
+
+ Ext.apply(me, {
+ url: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/pending",
+ interval: 5000,
+ cwidth1: 250,
+ tbar: [edit_btn, revert_btn],
+ rows: rows,
+ editorConfig: {
+ url: "/api2/extjs/" + baseurl,
+ },
+ listeners: {
+ itemdblclick: me.run_editor,
+ selectionchange: set_button_status,
+ },
+ });
+
+ me.callParent();
+
+ me.on('activate', () => me.rstore.startUpdate());
+ me.on('destroy', () => me.rstore.stopUpdate());
+ me.on('deactivate', () => me.rstore.stopUpdate());
+
+ me.mon(me.getStore(), 'datachanged', function() {
+ set_button_status();
+ });
+ },
+});
+
+Ext.define('PVE.qemu.PCIInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+
+ onlineHelp: 'qm_pci_passthrough_vm_config',
+
+ 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) {
+ if (!values.host.match(/^[0-9a-f]{4}:/i)) { // add optional domain
+ values.host = "0000:" + values.host;
+ }
+ if (values.host.length < 11) { // 0000:00:00 format not 0000:00:00.0
+ values.host += ".0";
+ values.multifunction = true;
+ }
+ }
+
+ values['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) {
+ let me = this;
+ if (!me.confid) {
+ for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
+ if (!me.vmconfig['hostpci' + i.toString()]) {
+ me.confid = 'hostpci' + i.toString();
+ break;
+ }
+ }
+ // FIXME: what if no confid was found??
+ }
+ values.host.replace(/^0000:/, ''); // remove optional '0000' domain
+
+ if (values.multifunction) {
+ values.host = values.host.substring(0, values.host.indexOf('.')); // skip the '.X'
+ delete values.multifunction;
+ }
+
+ if (values.rombar) {
+ delete values.rombar;
+ } else {
+ values.rombar = 0;
+ }
+
+ if (!values.romfile) {
+ delete values.romfile;
+ }
+
+ let ret = {};
+ ret[me.confid] = PVE.Parser.printPropertyString(values, 'host');
+ return ret;
+ },
+
+ initComponent: function() {
+ let 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;
+ }
+ if (records.every((val) => val.data.iommugroup === -1)) { // no IOMMU groups
+ let 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: 'pmx-hint',
+ });
+ me.items.insert(0, warning);
+ me.updateLayout(); // insert does not trigger that
+ }
+ },
+ listeners: {
+ change: function(pcisel, value) {
+ if (!value) {
+ return;
+ }
+ let pciDev = pcisel.getStore().getById(value);
+ let mdevfield = me.down('field[name=mdev]');
+ mdevfield.setDisabled(!pciDev || !pciDev.data.mdev);
+ if (!pciDev) {
+ return;
+ }
+ if (pciDev.data.mdev) {
+ mdevfield.setPciID(value);
+ }
+ let iommu = pciDev.data.iommugroup;
+ if (iommu === -1) {
+ return;
+ }
+ // try to find out if there are more devices in that iommu group
+ let id = pciDev.data.id.substring(0, 5); // 00:00
+ let count = 0;
+ pcisel.getStore().each(({ data }) => {
+ if (data.iommugroup === iommu && data.id.substring(0, 5) !== id) {
+ count++;
+ return false;
+ }
+ return true;
+ });
+ let 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: 'pmx-hint',
+ });
+ me.items.insert(0, warning);
+ me.updateLayout(); // insert does not trigger that
+ } else if (!count && warning) {
+ me.remove(warning);
+ }
+ },
+ },
+ },
+ {
+ 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) {
+ let multiFunction = me.down('field[name=multifunction]');
+ if (value) {
+ multiFunction.setValue(false);
+ }
+ multiFunction.setDisabled(!!value);
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Primary GPU'),
+ name: 'x-vga',
+ },
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: 'ROM-Bar',
+ name: 'rombar',
+ },
+ {
+ xtype: 'displayfield',
+ submitValue: true,
+ hidden: true,
+ fieldLabel: 'ROM-File',
+ name: 'romfile',
+ },
+ {
+ xtype: 'textfield',
+ name: 'vendor-id',
+ fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Vendor')),
+ emptyText: gettext('From Device'),
+ vtype: 'PciId',
+ allowBlank: true,
+ submitEmpty: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'device-id',
+ fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Device')),
+ emptyText: gettext('From Device'),
+ vtype: 'PciId',
+ allowBlank: true,
+ submitEmpty: false,
+ },
+ ];
+
+ me.advancedColumn2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: 'PCI-Express',
+ name: 'pcie',
+ },
+ {
+ xtype: 'textfield',
+ name: 'sub-vendor-id',
+ fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Sub-Vendor')),
+ emptyText: gettext('From Device'),
+ vtype: 'PciId',
+ allowBlank: true,
+ submitEmpty: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'sub-device-id',
+ fieldLabel: Ext.String.format(gettext('{0} ID'), gettext('Sub-Device')),
+ emptyText: gettext('From Device'),
+ vtype: 'PciId',
+ allowBlank: true,
+ submitEmpty: false,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.qemu.PCIEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ subject: gettext('PCI Device'),
+
+ vmconfig: undefined,
+ isAdd: true,
+
+ initComponent: function() {
+ let me = this;
+
+ me.isCreate = !me.confid;
+
+ let ipanel = Ext.create('PVE.qemu.PCIInputPanel', {
+ confid: me.confid,
+ pveSelNode: me.pveSelNode,
+ });
+
+ Ext.apply(me, {
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ me.load({
+ success: ({ result }) => ipanel.setVMConfig(result.data),
+ });
+ },
+});
+// The view model of the parent shoul contain a 'cgroupMode' variable (or params for v2 are used).
+Ext.define('PVE.qemu.ProcessorInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.pveQemuProcessorPanel',
+ onlineHelp: 'qm_cpu',
+
+ insideWizard: false,
+
+ viewModel: {
+ data: {
+ socketCount: 1,
+ coreCount: 1,
+ showCustomModelPermWarning: false,
+ userIsRoot: false,
+ },
+ formulas: {
+ totalCoreCount: get => get('socketCount') * get('coreCount'),
+ cpuunitsDefault: (get) => get('cgroupMode') === 1 ? 1024 : 100,
+ cpuunitsMin: (get) => get('cgroupMode') === 1 ? 2 : 1,
+ cpuunitsMax: (get) => get('cgroupMode') === 1 ? 262144 : 10000,
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ init: function() {
+ let me = this;
+ let viewModel = me.getViewModel();
+
+ viewModel.set('userIsRoot', Proxmox.UserName === 'root@pam');
+ },
+ },
+
+ onGetValues: function(values) {
+ let me = this;
+ let cpuunitsDefault = me.getViewModel().get('cpuunitsDefault');
+
+ if (Array.isArray(values.delete)) {
+ values.delete = values.delete.join(',');
+ }
+
+ PVE.Utils.delete_if_default(values, 'cpulimit', '0', me.insideWizard);
+ PVE.Utils.delete_if_default(values, 'cpuunits', `${cpuunitsDefault}`, me.insideWizard);
+
+ // build the cpu options:
+ me.cpu.cputype = values.cputype;
+
+ if (values.flags) {
+ me.cpu.flags = values.flags;
+ } else {
+ delete me.cpu.flags;
+ }
+
+ delete values.cputype;
+ delete values.flags;
+ var cpustring = PVE.Parser.printQemuCpu(me.cpu);
+
+ // remove cputype delete request:
+ var del = values.delete;
+ delete values.delete;
+ if (del) {
+ del = del.split(',');
+ Ext.Array.remove(del, 'cputype');
+ } else {
+ del = [];
+ }
+
+ if (cpustring) {
+ values.cpu = cpustring;
+ } else {
+ del.push('cpu');
+ }
+
+ var delarr = del.join(',');
+ if (delarr) {
+ values.delete = delarr;
+ }
+
+ return values;
+ },
+
+ setValues: function(values) {
+ let me = this;
+
+ let type = values.cputype;
+ let typeSelector = me.lookupReference('cputype');
+ let typeStore = typeSelector.getStore();
+ typeStore.on('load', (store, records, success) => {
+ if (!success || !type || records.some(x => x.data.name === type)) {
+ return;
+ }
+
+ // if we get here, a custom CPU model is selected for the VM but we
+ // don't have permission to configure it - it will not be in the
+ // list retrieved from the API, so add it manually to allow changing
+ // other processor options
+ typeStore.add({
+ name: type,
+ displayname: type.replace(/^custom-/, ''),
+ custom: 1,
+ vendor: gettext("Unknown"),
+ });
+ typeSelector.select(type);
+ });
+
+ me.callParent([values]);
+ },
+
+ cpu: {},
+
+ column1: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'sockets',
+ minValue: 1,
+ maxValue: 4,
+ value: '1',
+ fieldLabel: gettext('Sockets'),
+ allowBlank: false,
+ bind: {
+ value: '{socketCount}',
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'cores',
+ minValue: 1,
+ maxValue: 128,
+ value: '1',
+ fieldLabel: gettext('Cores'),
+ allowBlank: false,
+ bind: {
+ value: '{coreCount}',
+ },
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'CPUModelSelector',
+ name: 'cputype',
+ reference: 'cputype',
+ fieldLabel: gettext('Type'),
+ },
+ {
+ xtype: 'displayfield',
+ fieldLabel: gettext('Total cores'),
+ name: 'totalcores',
+ isFormField: false,
+ bind: {
+ value: '{totalCoreCount}',
+ },
+ },
+ ],
+
+ columnB: [
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext('WARNING: You do not have permission to configure custom CPU types, if you change the type you will not be able to go back!'),
+ hidden: true,
+ bind: {
+ hidden: '{!showCustomModelPermWarning}',
+ },
+ },
+ ],
+
+ advancedColumn1: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'vcpus',
+ minValue: 1,
+ maxValue: 1,
+ value: '',
+ fieldLabel: gettext('VCPUs'),
+ deleteEmpty: true,
+ allowBlank: true,
+ emptyText: '1',
+ bind: {
+ emptyText: '{totalCoreCount}',
+ maxValue: '{totalCoreCount}',
+ },
+ },
+ {
+ xtype: 'numberfield',
+ name: 'cpulimit',
+ minValue: 0,
+ maxValue: 128, // api maximum
+ value: '',
+ step: 1,
+ fieldLabel: gettext('CPU limit'),
+ allowBlank: true,
+ emptyText: gettext('unlimited'),
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'affinity',
+ vtype: 'CpuSet',
+ value: '',
+ fieldLabel: gettext('CPU Affinity'),
+ allowBlank: true,
+ emptyText: gettext("All Cores"),
+ deleteEmpty: true,
+ bind: {
+ disabled: '{!userIsRoot}',
+ },
+ },
+ ],
+
+ advancedColumn2: [
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'cpuunits',
+ fieldLabel: gettext('CPU units'),
+ minValue: '1',
+ maxValue: '10000',
+ value: '',
+ emptyText: '100',
+ bind: {
+ minValue: '{cpuunitsMin}',
+ maxValue: '{cpuunitsMax}',
+ emptyText: '{cpuunitsDefault}',
+ },
+ deleteEmpty: true,
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Enable NUMA'),
+ name: 'numa',
+ uncheckedValue: 0,
+ },
+ ],
+ advancedColumnB: [
+ {
+ xtype: 'label',
+ text: 'Extra CPU Flags:',
+ },
+ {
+ xtype: 'vmcpuflagselector',
+ name: 'flags',
+ },
+ ],
+});
+
+Ext.define('PVE.qemu.ProcessorEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveQemuProcessorEdit',
+
+ width: 700,
+
+ viewModel: {
+ data: {
+ cgroupMode: 2,
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+ me.getViewModel().set('cgroupMode', me.cgroupMode);
+
+ var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('Processors'),
+ items: ipanel,
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+ var value = data.cpu;
+ if (value) {
+ var cpu = PVE.Parser.parseQemuCpu(value);
+ ipanel.cpu = cpu;
+ data.cputype = cpu.cputype;
+ if (cpu.flags) {
+ data.flags = cpu.flags;
+ }
+
+ let caps = Ext.state.Manager.get('GuiCap');
+ if (data.cputype.indexOf('custom-') === 0 &&
+ !caps.nodes['Sys.Audit']) {
+ let vm = ipanel.getViewModel();
+ vm.set("showCustomModelPermWarning", true);
+ }
+ }
+ me.setValues(data);
+ },
+ });
+ },
+});
+Ext.define('PVE.qemu.BiosEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: 'widget.pveQemuBiosEdit',
+
+ onlineHelp: 'qm_bios_and_uefi',
+ subject: 'BIOS',
+ autoLoad: true,
+
+ viewModel: {
+ data: {
+ bios: '__default__',
+ efidisk0: false,
+ },
+ formulas: {
+ showEFIDiskHint: (get) => get('bios') === 'ovmf' && !get('efidisk0'),
+ },
+ },
+
+ items: [
+ {
+ xtype: 'pveQemuBiosSelector',
+ onlineHelp: 'qm_bios_and_uefi',
+ name: 'bios',
+ value: '__default__',
+ bind: '{bios}',
+ fieldLabel: 'BIOS',
+ },
+ {
+ xtype: 'displayfield',
+ name: 'efidisk0',
+ bind: '{efidisk0}',
+ hidden: true,
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext('You need to add an EFI disk for storing the EFI settings. See the online help for details.'),
+ bind: {
+ hidden: '{!showEFIDiskHint}',
+ },
+ },
+ ],
+});
+Ext.define('PVE.qemu.RNGInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveRNGInputPanel',
+
+ onlineHelp: 'qm_virtio_rng',
+
+ onGetValues: function(values) {
+ if (values.max_bytes === "") {
+ values.max_bytes = "0";
+ } else if (values.max_bytes === "1024" && values.period === "") {
+ delete values.max_bytes;
+ }
+
+ var ret = PVE.Parser.printPropertyString(values);
+
+ return {
+ rng0: ret,
+ };
+ },
+
+ setValues: function(values) {
+ if (values.max_bytes === 0) {
+ values.max_bytes = null;
+ }
+
+ this.callParent(arguments);
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ '#max_bytes': {
+ change: function(el, newVal) {
+ let limitWarning = this.lookupReference('limitWarning');
+ limitWarning.setHidden(!!newVal);
+ },
+ },
+ '#source': {
+ change: function(el, newVal) {
+ let limitWarning = this.lookupReference('sourceWarning');
+ limitWarning.setHidden(newVal !== '/dev/random');
+ },
+ },
+ },
+ },
+
+ items: [{
+ itemId: 'source',
+ name: 'source',
+ xtype: 'proxmoxKVComboBox',
+ value: '/dev/urandom',
+ fieldLabel: gettext('Entropy source'),
+ labelWidth: 130,
+ comboItems: [
+ ['/dev/urandom', '/dev/urandom'],
+ ['/dev/random', '/dev/random'],
+ ['/dev/hwrng', '/dev/hwrng'],
+ ],
+ },
+ {
+ xtype: 'numberfield',
+ itemId: 'max_bytes',
+ name: 'max_bytes',
+ minValue: 0,
+ step: 1,
+ value: 1024,
+ fieldLabel: gettext('Limit (Bytes/Period)'),
+ labelWidth: 130,
+ emptyText: gettext('unlimited'),
+ },
+ {
+ xtype: 'numberfield',
+ name: 'period',
+ minValue: 1,
+ step: 1,
+ fieldLabel: gettext('Period') + ' (ms)',
+ labelWidth: 130,
+ emptyText: '1000',
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'sourceWarning',
+ value: gettext('Using /dev/random as entropy source is discouraged, as it can lead to host entropy starvation. /dev/urandom is preferred, and does not lead to a decrease in security in practice.'),
+ userCls: 'pmx-hint',
+ hidden: true,
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'limitWarning',
+ value: gettext('Disabling the limiter can potentially allow a guest to overload the host. Proceed with caution.'),
+ userCls: 'pmx-hint',
+ hidden: true,
+ }],
+});
+
+Ext.define('PVE.qemu.RNGEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ subject: gettext('VirtIO RNG'),
+
+ items: [{
+ xtype: 'pveRNGInputPanel',
+ }],
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response) {
+ me.vmconfig = response.result.data;
+
+ var rng0 = me.vmconfig.rng0;
+ if (rng0) {
+ me.setValues(PVE.Parser.parsePropertyString(rng0));
+ }
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.qemu.SSHKeyInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveQemuSSHKeyInputPanel',
+
+ insideWizard: false,
+
+ onGetValues: function(values) {
+ var me = this;
+ if (values.sshkeys) {
+ values.sshkeys.trim();
+ }
+ if (!values.sshkeys.length) {
+ values = {};
+ values.delete = 'sshkeys';
+ return values;
+ } else {
+ values.sshkeys = encodeURIComponent(values.sshkeys);
+ }
+ return values;
+ },
+
+ items: [
+ {
+ xtype: 'textarea',
+ itemId: 'sshkeys',
+ name: 'sshkeys',
+ height: 250,
+ },
+ {
+ xtype: 'filebutton',
+ itemId: 'filebutton',
+ name: 'file',
+ text: gettext('Load SSH Key File'),
+ fieldLabel: 'test',
+ listeners: {
+ change: function(btn, e, value) {
+ let view = this.up('inputpanel');
+ e = e.event;
+ Ext.Array.each(e.target.files, function(file) {
+ PVE.Utils.loadSSHKeyFromFile(file, function(res) {
+ let keysField = view.down('#sshkeys');
+ var old = keysField.getValue();
+ keysField.setValue(old + res);
+ });
+ });
+ btn.reset();
+ },
+ },
+ },
+ ],
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+ if (!window.FileReader) {
+ me.down('#filebutton').setVisible(false);
+ }
+ },
+});
+
+Ext.define('PVE.qemu.SSHKeyEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 800,
+
+ initComponent: function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel');
+
+ Ext.apply(me, {
+ subject: gettext('SSH Keys'),
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ if (!me.create) {
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+ if (data.sshkeys) {
+ data.sshkeys = decodeURIComponent(data.sshkeys);
+ ipanel.setValues(data);
+ }
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.qemu.ScsiHwEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ Ext.applyIf(me, {
+ subject: gettext('SCSI Controller Type'),
+ items: {
+ xtype: 'pveScsiHwSelector',
+ name: 'scsihw',
+ value: '__default__',
+ fieldLabel: gettext('Type'),
+ },
+ });
+
+ me.callParent();
+
+ me.load();
+ },
+});
+Ext.define('PVE.qemu.SerialnputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+
+ autoComplete: false,
+
+ setVMConfig: function(vmconfig) {
+ var me = this, i;
+ me.vmconfig = vmconfig;
+
+ for (i = 0; i < 4; i++) {
+ 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;
+ }
+ let view = this.up('panel');
+ if (view.vmconfig !== undefined && Ext.isDefined(view.vmconfig['serial' + id])) {
+ return "This device is already in use.";
+ }
+ return true;
+ },
+ },
+ ],
+});
+
+Ext.define('PVE.qemu.SerialEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ vmconfig: undefined,
+
+ isAdd: true,
+
+ subject: gettext('Serial Port'),
+
+ initComponent: function() {
+ var me = this;
+
+ // for now create of (socket) serial port only
+ me.isCreate = true;
+
+ var ipanel = Ext.create('PVE.qemu.SerialnputPanel', {});
+
+ Ext.apply(me, {
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ ipanel.setVMConfig(response.result.data);
+ },
+ });
+ },
+});
+Ext.define('PVE.qemu.Smbios1InputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ alias: 'widget.PVE.qemu.Smbios1InputPanel',
+
+ insideWizard: false,
+
+ smbios1: {},
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var params = {
+ smbios1: PVE.Parser.printQemuSmbios1(values),
+ };
+
+ return params;
+ },
+
+ setSmbios1: function(data) {
+ var me = this;
+
+ me.smbios1 = data;
+
+ me.setValues(me.smbios1);
+ },
+
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: 'UUID',
+ regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/,
+ name: 'uuid',
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Manufacturer'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em',
+ },
+ name: 'manufacturer',
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Product'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em',
+ },
+ name: 'product',
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Version'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em',
+ },
+ name: 'version',
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Serial'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em',
+ },
+ name: 'serial',
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: 'SKU',
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em',
+ },
+ name: 'sku',
+ },
+ {
+ xtype: 'textareafield',
+ fieldLabel: gettext('Family'),
+ fieldStyle: {
+ height: '2em',
+ minHeight: '2em',
+ },
+ name: 'family',
+ },
+ ],
+});
+
+Ext.define('PVE.qemu.Smbios1Edit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {});
+
+ Ext.applyIf(me, {
+ subject: gettext('SMBIOS settings (type1)'),
+ width: 450,
+ items: ipanel,
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ me.vmconfig = response.result.data;
+ var value = me.vmconfig.smbios1;
+ if (value) {
+ 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.SystemInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pveQemuSystemPanel',
+
+ onlineHelp: 'qm_system_settings',
+
+ viewModel: {
+ data: {
+ efi: false,
+ addefi: true,
+ },
+
+ formulas: {
+ efidisk: function(get) {
+ return get('efi') && get('addefi');
+ },
+ },
+ },
+
+ onGetValues: function(values) {
+ if (values.vga && values.vga.substr(0, 6) === 'serial') {
+ values['serial' + values.vga.substr(6, 1)] = 'socket';
+ }
+
+ delete values.hdimage;
+ delete values.hdstorage;
+ delete values.diskformat;
+
+ delete values.preEnrolledKeys; // efidisk
+ delete values.version; // tpmstate
+
+ return values;
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ scsihwChange: function(field, value) {
+ var me = this;
+ if (me.getView().insideWizard) {
+ me.getViewModel().set('current.scsihw', value);
+ }
+ },
+
+ biosChange: function(field, value) {
+ var me = this;
+ if (me.getView().insideWizard) {
+ me.getViewModel().set('efi', value === 'ovmf');
+ }
+ },
+
+ control: {
+ 'pveScsiHwSelector': {
+ change: 'scsihwChange',
+ },
+ 'pveQemuBiosSelector': {
+ change: 'biosChange',
+ },
+ '#': {
+ afterrender: 'setMachine',
+ },
+ },
+
+ setMachine: function() {
+ let me = this;
+ let vm = this.getViewModel();
+ let ostype = vm.get('current.ostype');
+ if (ostype === 'win11') {
+ me.lookup('machine').setValue('q35');
+ me.lookup('bios').setValue('ovmf');
+ me.lookup('addtpmbox').setValue(true);
+ }
+ },
+ },
+
+ column1: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ value: '__default__',
+ deleteEmpty: false,
+ fieldLabel: gettext('Graphic card'),
+ name: 'vga',
+ comboItems: Object.entries(PVE.Utils.kvm_vga_drivers),
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'machine',
+ reference: 'machine',
+ value: '__default__',
+ fieldLabel: gettext('Machine'),
+ comboItems: [
+ ['__default__', PVE.Utils.render_qemu_machine('')],
+ ['q35', 'q35'],
+ ],
+ },
+ {
+ xtype: 'displayfield',
+ value: gettext('Firmware'),
+ },
+ {
+ xtype: 'pveQemuBiosSelector',
+ name: 'bios',
+ reference: 'bios',
+ value: '__default__',
+ fieldLabel: 'BIOS',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ bind: {
+ value: '{addefi}',
+ hidden: '{!efi}',
+ disabled: '{!efi}',
+ },
+ hidden: true,
+ submitValue: false,
+ disabled: true,
+ fieldLabel: gettext('Add EFI Disk'),
+ },
+ {
+ xtype: 'pveEFIDiskInputPanel',
+ name: 'efidisk0',
+ storageContent: 'images',
+ bind: {
+ nodename: '{nodename}',
+ hidden: '{!efi}',
+ disabled: '{!efidisk}',
+ },
+ autoSelect: false,
+ disabled: true,
+ hidden: true,
+ hideSize: true,
+ usesEFI: true,
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'pveScsiHwSelector',
+ name: 'scsihw',
+ value: '__default__',
+ bind: {
+ value: '{current.scsihw}',
+ },
+ fieldLabel: gettext('SCSI Controller'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'agent',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ deleteDefaultValue: true,
+ fieldLabel: gettext('Qemu Agent'),
+ },
+ {
+ // fake for spacing
+ xtype: 'displayfield',
+ value: ' ',
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ reference: 'addtpmbox',
+ bind: {
+ value: '{addtpm}',
+ },
+ submitValue: false,
+ fieldLabel: gettext('Add TPM'),
+ },
+ {
+ xtype: 'pveTPMDiskInputPanel',
+ name: 'tpmstate0',
+ storageContent: 'images',
+ bind: {
+ nodename: '{nodename}',
+ hidden: '{!addtpm}',
+ disabled: '{!addtpm}',
+ },
+ disabled: true,
+ hidden: true,
+ },
+ ],
+
+});
+Ext.define('PVE.qemu.USBInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ autoComplete: false,
+ onlineHelp: 'qm_usb_passthrough',
+
+ viewModel: {
+ data: {},
+ },
+
+ setVMConfig: function(vmconfig) {
+ var me = this;
+ me.vmconfig = vmconfig;
+ let max_usb = PVE.Utils.get_max_usb_count(me.vmconfig.ostype, me.vmconfig.machine);
+ if (max_usb > PVE.Utils.hardware_counts.usb_old) {
+ me.down('field[name=usb3]').setDisabled(true);
+ }
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+ if (!me.confid) {
+ let max_usb = PVE.Utils.get_max_usb_count(me.vmconfig.ostype, me.vmconfig.machine);
+ for (let i = 0; i < max_usb; i++) {
+ let id = 'usb' + i.toString();
+ if (!me.vmconfig[id]) {
+ me.confid = id;
+ break;
+ }
+ }
+ }
+ var val = "";
+ var type = me.down('radiofield').getGroupValue();
+ switch (type) {
+ case 'spice':
+ val = 'spice';
+ break;
+ case 'hostdevice':
+ case 'port':
+ val = 'host=' + values[type];
+ delete values[type];
+ break;
+ default:
+ throw "invalid type selected";
+ }
+
+ if (values.usb3) {
+ delete values.usb3;
+ val += ',usb3=1';
+ }
+ values[me.confid] = val;
+ return values;
+ },
+
+ items: [
+ {
+ xtype: 'fieldcontainer',
+ defaultType: 'radiofield',
+ layout: 'fit',
+ items: [
+ {
+ name: 'usb',
+ inputValue: 'spice',
+ boxLabel: gettext('Spice Port'),
+ submitValue: false,
+ checked: true,
+ },
+ {
+ name: 'usb',
+ inputValue: 'hostdevice',
+ boxLabel: gettext('Use USB Vendor/Device ID'),
+ reference: 'hostdevice',
+ submitValue: false,
+ },
+ {
+ xtype: 'pveUSBSelector',
+ disabled: true,
+ type: 'device',
+ name: 'hostdevice',
+ cbind: { pveSelNode: '{pveSelNode}' },
+ bind: { disabled: '{!hostdevice.checked}' },
+ editable: true,
+ allowBlank: false,
+ fieldLabel: gettext('Choose Device'),
+ labelAlign: 'right',
+ },
+ {
+ name: 'usb',
+ inputValue: 'port',
+ boxLabel: gettext('Use USB Port'),
+ reference: 'port',
+ submitValue: false,
+ },
+ {
+ xtype: 'pveUSBSelector',
+ disabled: true,
+ name: 'port',
+ cbind: { pveSelNode: '{pveSelNode}' },
+ bind: { disabled: '{!port.checked}' },
+ editable: true,
+ type: 'port',
+ allowBlank: false,
+ fieldLabel: gettext('Choose Port'),
+ labelAlign: 'right',
+ },
+ {
+ xtype: 'checkbox',
+ name: 'usb3',
+ inputValue: true,
+ checked: true,
+ reference: 'usb3',
+ fieldLabel: gettext('Use USB3'),
+ },
+ ],
+ },
+ ],
+});
+
+Ext.define('PVE.qemu.USBEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ vmconfig: undefined,
+
+ isAdd: true,
+ width: 400,
+ subject: gettext('USB Device'),
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = !me.confid;
+
+ var ipanel = Ext.create('PVE.qemu.USBInputPanel', {
+ confid: me.confid,
+ pveSelNode: me.pveSelNode,
+ });
+
+ Ext.apply(me, {
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ me.load({
+ success: function(response, options) {
+ ipanel.setVMConfig(response.result.data);
+ if (me.isCreate) {
+ return;
+ }
+
+ var data = response.result.data[me.confid].split(',');
+ var port, hostdevice, usb3 = false;
+ var type = 'spice';
+
+ for (let 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.sdn.Browser', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.sdn.Browser',
+
+ onlineHelp: 'chapter_pvesdn',
+
+ initComponent: function() {
+ let me = this;
+
+ let nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+ let sdnId = me.pveSelNode.data.sdn;
+ if (!sdnId) {
+ throw "no sdn ID specified";
+ }
+
+ me.items = [];
+
+ Ext.apply(me, {
+ title: Ext.String.format(gettext("Zone {0} on node {1}"), `'${sdnId}'`, `'${nodename}'`),
+ hstateid: 'sdntab',
+ });
+
+ const caps = Ext.state.Manager.get('GuiCap');
+
+ if (caps.sdn['SDN.Audit']) {
+ me.items.push({
+ xtype: 'pveSDNZoneContentView',
+ title: gettext('Content'),
+ iconCls: 'fa fa-th',
+ itemId: 'content',
+ });
+ }
+ if (caps.sdn['Permissions.Modify']) {
+ me.items.push({
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ path: `/sdn/zones/${sdnId}`,
+ });
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.ControllerView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveSDNControllerView'],
+
+ onlineHelp: 'pvesdn_config_controllers',
+
+ stateful: true,
+ stateId: 'grid-sdn-controller',
+
+ createSDNControllerEditWindow: function(type, sid) {
+ var schema = PVE.Utils.sdncontrollerSchema[type];
+ if (!schema || !schema.ipanel) {
+ throw "no editor registered for controller type: " + type;
+ }
+
+ Ext.create('PVE.sdn.controllers.BaseEdit', {
+ paneltype: 'PVE.sdn.controllers.' + schema.ipanel,
+ type: type,
+ controllerid: sid,
+ autoShow: true,
+ listeners: {
+ destroy: this.reloadStore,
+ },
+ });
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ var store = new Ext.data.Store({
+ model: 'pve-sdn-controller',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/controllers?pending=1",
+ },
+ sorters: {
+ property: 'controller',
+ direction: 'ASC',
+ },
+ });
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ let type = rec.data.type, controller = rec.data.controller;
+ me.createSDNControllerEditWindow(type, controller);
+ };
+
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/cluster/sdn/controllers/',
+ callback: () => store.load(),
+ });
+
+ // else we cannot dynamically generate the add menu handlers
+ let addHandleGenerator = function(type) {
+ return function() { me.createSDNControllerEditWindow(type); };
+ };
+ let addMenuItems = [];
+ for (const [type, controller] of Object.entries(PVE.Utils.sdncontrollerSchema)) {
+ if (controller.hideAdd) {
+ continue;
+ }
+ addMenuItems.push({
+ text: PVE.Utils.format_sdncontroller_type(type),
+ iconCls: 'fa fa-fw fa-' + controller.faIcon,
+ handler: addHandleGenerator(type),
+ });
+ }
+
+ Ext.apply(me, {
+ store: store,
+ reloadStore: () => store.load(),
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: addMenuItems,
+ }),
+ },
+ remove_btn,
+ edit_btn,
+ ],
+ columns: [
+ {
+ header: 'ID',
+ flex: 2,
+ sortable: true,
+ dataIndex: 'controller',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'controller', 1);
+ },
+ },
+ {
+ header: gettext('Type'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'type',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'type', 1);
+ },
+ },
+ {
+ header: gettext('Node'),
+ flex: 1,
+ sortable: true,
+ dataIndex: 'node',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'node', 1);
+ },
+ },
+ {
+ header: gettext('State'),
+ width: 100,
+ dataIndex: 'state',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending_state(rec, value);
+ },
+ },
+ ],
+ listeners: {
+ activate: () => store.load(),
+ itemdblclick: run_editor,
+ },
+ });
+ store.load();
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.Status', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSDNStatus',
+
+ onlineHelp: 'chapter_pvesdn',
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.rstore = Ext.create('Proxmox.data.ObjectStore', {
+ interval: me.interval,
+ model: 'pve-sdn-status',
+ storeid: 'pve-store-' + ++Ext.idSeed,
+ groupField: 'type',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/cluster/resources',
+ },
+ });
+
+ me.items = [{
+ xtype: 'pveSDNStatusView',
+ title: gettext('Status'),
+ rstore: me.rstore,
+ border: 0,
+ collapsible: true,
+ padding: '0 0 20 0',
+ }];
+
+ me.callParent();
+ me.on('activate', me.rstore.startUpdate);
+ },
+});
+Ext.define('PVE.sdn.StatusView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNStatusView',
+
+ sortPriority: {
+ sdn: 1,
+ node: 2,
+ status: 3,
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.rstore) {
+ throw "no rstore given";
+ }
+
+ Proxmox.Utils.monStoreErrors(me, me.rstore);
+
+ var store = Ext.create('Proxmox.data.DiffStore', {
+ rstore: me.rstore,
+ sortAfterUpdate: true,
+ sorters: [{
+ sorterFn: function(rec1, rec2) {
+ var p1 = me.sortPriority[rec1.data.type];
+ var p2 = me.sortPriority[rec2.data.type];
+ return p1 !== p2 ? p1 > p2 ? 1 : -1 : 0;
+ },
+ }],
+ filters: {
+ property: 'type',
+ value: 'sdn',
+ operator: '==',
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ stateful: false,
+ tbar: [
+ {
+ text: gettext('Apply'),
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: '/cluster/sdn/',
+ method: 'PUT',
+ waitMsgTarget: me,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ });
+ },
+ },
+ ],
+ viewConfig: {
+ trackOver: false,
+ },
+ columns: [
+ {
+ header: 'SDN',
+ width: 80,
+ dataIndex: 'sdn',
+ },
+ {
+ header: gettext('Node'),
+ width: 80,
+ dataIndex: 'node',
+ },
+ {
+ header: gettext('Status'),
+ width: 80,
+ flex: 1,
+ dataIndex: 'status',
+ },
+ ],
+ });
+
+ me.callParent();
+
+ me.on('activate', me.rstore.startUpdate);
+ me.on('destroy', me.rstore.stopUpdate);
+ },
+}, function() {
+ Ext.define('pve-sdn-status', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id', 'type', 'node', 'status', 'sdn',
+ ],
+ idProperty: 'id',
+ });
+});
+Ext.define('PVE.sdn.VnetInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onGetValues: function(values) {
+ let me = this;
+
+ if (me.isCreate) {
+ values.type = 'vnet';
+ }
+
+ if (!values.vlanaware) {
+ delete values.vlanaware;
+ }
+
+ return values;
+ },
+
+ items: [
+ {
+ xtype: 'pmxDisplayEditField',
+ name: 'vnet',
+ cbind: {
+ editable: '{isCreate}',
+ },
+ maxLength: 8,
+ flex: 1,
+ allowBlank: false,
+ fieldLabel: gettext('Name'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'alias',
+ fieldLabel: gettext('Alias'),
+ allowBlank: true,
+ },
+ {
+ xtype: 'pveSDNZoneSelector',
+ fieldLabel: gettext('Zone'),
+ name: 'zone',
+ value: '',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'tag',
+ minValue: 1,
+ maxValue: 16777216,
+ fieldLabel: gettext('Tag'),
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'vlanaware',
+ uncheckedValue: 0,
+ checked: false,
+ fieldLabel: gettext('VLAN Aware'),
+ },
+ ],
+});
+
+Ext.define('PVE.sdn.VnetEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ subject: gettext('VNet'),
+
+ vnet: undefined,
+
+ width: 350,
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = me.vnet === undefined;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/cluster/sdn/vnets';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/cluster/sdn/vnets/' + me.vnet;
+ me.method = 'PUT';
+ }
+
+ let ipanel = Ext.create('PVE.sdn.VnetInputPanel', {
+ isCreate: me.isCreate,
+ });
+
+ Ext.apply(me, {
+ items: [
+ ipanel,
+ ],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ let values = response.result.data;
+ ipanel.setValues(values);
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.sdn.VnetView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNVnetView',
+
+ onlineHelp: 'pvesdn_config_vnet',
+
+ stateful: true,
+ stateId: 'grid-sdn-vnet',
+
+ subnetview_panel: undefined,
+
+ initComponent: function() {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'pve-sdn-vnet',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/vnets?pending=1",
+ },
+ sorters: {
+ property: 'vnet',
+ direction: 'ASC',
+ },
+ });
+
+ let reload = () => store.load();
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+
+ let win = Ext.create('PVE.sdn.VnetEdit', {
+ autoShow: true,
+ onlineHelp: 'pvesdn_config_vnet',
+ vnet: rec.data.vnet,
+ });
+ win.on('destroy', reload);
+ };
+
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/cluster/sdn/vnets/',
+ callback: reload,
+ });
+
+ let set_button_status = function() {
+ var rec = me.selModel.getSelection()[0];
+
+ if (!rec || rec.data.state === 'deleted') {
+ edit_btn.disable();
+ remove_btn.disable();
+ }
+ };
+
+ Ext.apply(me, {
+ store: store,
+ reloadStore: reload,
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ tbar: [
+ {
+ text: gettext('Create'),
+ handler: function() {
+ let win = Ext.create('PVE.sdn.VnetEdit', {
+ autoShow: true,
+ onlineHelp: 'pvesdn_config_vnet',
+ type: 'vnet',
+ });
+ win.on('destroy', reload);
+ },
+ },
+ remove_btn,
+ edit_btn,
+ ],
+ columns: [
+ {
+ header: 'ID',
+ flex: 2,
+ dataIndex: 'vnet',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'vnet', 1);
+ },
+ },
+ {
+ header: gettext('Alias'),
+ flex: 1,
+ dataIndex: 'alias',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'alias');
+ },
+ },
+ {
+ header: gettext('Zone'),
+ flex: 1,
+ dataIndex: 'zone',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'zone');
+ },
+ },
+ {
+ header: gettext('Tag'),
+ flex: 1,
+ dataIndex: 'tag',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'tag');
+ },
+ },
+ {
+ header: gettext('VLAN Aware'),
+ flex: 1,
+ dataIndex: 'vlanaware',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'vlanaware');
+ },
+ },
+ {
+ header: gettext('State'),
+ width: 100,
+ dataIndex: 'state',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending_state(rec, value);
+ },
+ },
+ ],
+ listeners: {
+ activate: reload,
+ itemdblclick: run_editor,
+ selectionchange: set_button_status,
+ show: reload,
+ select: function(_sm, rec) {
+ let url = `/cluster/sdn/vnets/${rec.data.vnet}/subnets`;
+ me.subnetview_panel.setBaseUrl(url);
+ },
+ deselect: function() {
+ me.subnetview_panel.setBaseUrl(undefined);
+ },
+ },
+ });
+ store.load();
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.Vnet', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSDNVnet',
+
+ title: 'Vnet',
+
+ onlineHelp: 'pvesdn_config_vnet',
+
+ initComponent: function() {
+ var me = this;
+
+ var subnetview_panel = Ext.createWidget('pveSDNSubnetView', {
+ title: gettext('Subnets'),
+ region: 'center',
+ border: false,
+ });
+
+ var vnetview_panel = Ext.createWidget('pveSDNVnetView', {
+ title: 'Vnets',
+ region: 'west',
+ subnetview_panel: subnetview_panel,
+ width: '50%',
+ border: false,
+ split: true,
+ });
+
+ Ext.apply(me, {
+ layout: 'border',
+ items: [vnetview_panel, subnetview_panel],
+ listeners: {
+ show: function() {
+ subnetview_panel.fireEvent('show', subnetview_panel);
+ },
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.SubnetInputPanel', {
+ extend: 'Proxmox.panel.InputPanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onGetValues: function(values) {
+ let me = this;
+
+ if (me.isCreate) {
+ values.type = 'subnet';
+ values.subnet = values.cidr;
+ delete values.cidr;
+ }
+
+ if (!values.gateway) {
+ delete values.gateway;
+ }
+ if (!values.snat) {
+ delete values.snat;
+ }
+
+ return values;
+ },
+
+ items: [
+ {
+ xtype: 'pmxDisplayEditField',
+ name: 'cidr',
+ cbind: {
+ editable: '{isCreate}',
+ },
+ flex: 1,
+ allowBlank: false,
+ fieldLabel: gettext('Subnet'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'gateway',
+ vtype: 'IP64Address',
+ fieldLabel: gettext('Gateway'),
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'snat',
+ uncheckedValue: 0,
+ checked: false,
+ fieldLabel: 'SNAT',
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'dnszoneprefix',
+ skipEmptyText: true,
+ fieldLabel: gettext('DNS zone prefix'),
+ allowBlank: true,
+ },
+ ],
+});
+
+Ext.define('PVE.sdn.SubnetEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ subject: gettext('Subnet'),
+
+ subnet: undefined,
+
+ width: 350,
+
+ base_url: undefined,
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = me.subnet === undefined;
+
+ if (me.isCreate) {
+ me.url = me.base_url;
+ me.method = 'POST';
+ } else {
+ me.url = me.base_url + '/' + me.subnet;
+ me.method = 'PUT';
+ }
+
+ let ipanel = Ext.create('PVE.sdn.SubnetInputPanel', {
+ isCreate: me.isCreate,
+ });
+
+ Ext.apply(me, {
+ items: [
+ ipanel,
+ ],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ let values = response.result.data;
+ ipanel.setValues(values);
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.sdn.SubnetView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNSubnetView',
+
+ stateful: true,
+ stateId: 'grid-sdn-subnet',
+
+ base_url: undefined,
+
+ remove_btn: undefined,
+
+ setBaseUrl: function(url) {
+ let me = this;
+
+ me.base_url = url;
+
+ if (url === undefined) {
+ me.store.removeAll();
+ me.create_btn.disable();
+ } else {
+ me.remove_btn.baseurl = url + '/';
+ me.store.setProxy({
+ type: 'proxmox',
+ url: '/api2/json/' + url + '?pending=1',
+ });
+ me.create_btn.enable();
+ me.store.load();
+ }
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'pve-sdn-subnet',
+ });
+
+ let reload = function() {
+ store.load();
+ };
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+
+ let win = Ext.create('PVE.sdn.SubnetEdit', {
+ autoShow: true,
+ subnet: rec.data.subnet,
+ base_url: me.base_url,
+ });
+ win.on('destroy', reload);
+ };
+
+ me.create_btn = new Proxmox.button.Button({
+ text: gettext('Create'),
+ disabled: true,
+ handler: function() {
+ let win = Ext.create('PVE.sdn.SubnetEdit', {
+ autoShow: true,
+ base_url: me.base_url,
+ type: 'subnet',
+ });
+ win.on('destroy', reload);
+ },
+ });
+
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ me.remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: me.base_url + '/',
+ callback: () => store.load(),
+ });
+
+ let set_button_status = function() {
+ var rec = me.selModel.getSelection()[0];
+
+ if (!rec || rec.data.state === 'deleted') {
+ edit_btn.disable();
+ me.remove_btn.disable();
+ }
+ };
+
+ Ext.apply(me, {
+ store: store,
+ reloadStore: reload,
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ tbar: [
+ me.create_btn,
+ me.remove_btn,
+ edit_btn,
+ ],
+ columns: [
+ {
+ header: 'ID',
+ flex: 2,
+ dataIndex: 'cidr',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'cidr', 1);
+ },
+ },
+ {
+ header: gettext('Gateway'),
+ flex: 1,
+ dataIndex: 'gateway',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'gateway');
+ },
+ },
+ {
+ header: 'SNAT',
+ flex: 1,
+ dataIndex: 'snat',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'snat');
+ },
+ },
+ {
+ header: gettext('Dns prefix'),
+ flex: 1,
+ dataIndex: 'dnszoneprefix',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'dnszoneprefix');
+ },
+ },
+ {
+ header: gettext('State'),
+ width: 100,
+ dataIndex: 'state',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending_state(rec, value);
+ },
+ },
+
+ ],
+ listeners: {
+ activate: reload,
+ itemdblclick: run_editor,
+ selectionchange: set_button_status,
+ },
+ });
+
+ me.callParent();
+
+ if (me.base_url) {
+ me.setBaseUrl(me.base_url); // load
+ }
+ },
+}, function() {
+ Ext.define('pve-sdn-subnet', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'cidr',
+ 'gateway',
+ 'snat',
+ ],
+ idProperty: 'subnet',
+ });
+});
+Ext.define('PVE.sdn.ZoneContentView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: 'widget.pveSDNZoneContentView',
+
+ stateful: true,
+ stateId: 'grid-sdnzone-content',
+ viewConfig: {
+ trackOver: false,
+ loadMask: false,
+ },
+ features: [
+ {
+ ftype: 'grouping',
+ groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
+ },
+ ],
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var zone = me.pveSelNode.data.sdn;
+ if (!zone) {
+ throw "no zone ID specified";
+ }
+
+ var baseurl = "/nodes/" + nodename + "/sdn/zones/" + zone + "/content";
+ var store = Ext.create('Ext.data.Store', {
+ model: 'pve-sdnzone-content',
+ groupField: 'content',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json' + baseurl,
+ },
+ sorters: {
+ property: 'vnet',
+ direction: 'ASC',
+ },
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var reload = function() {
+ store.load();
+ };
+
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ ],
+ columns: [
+ {
+ header: 'VNet',
+ width: 100,
+ sortable: true,
+ dataIndex: 'vnet',
+ },
+ {
+ header: 'Alias',
+ width: 300,
+ sortable: true,
+ dataIndex: 'alias',
+ },
+ {
+ header: gettext('Status'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'status',
+ },
+ {
+ header: gettext('Details'),
+ flex: 1,
+ dataIndex: 'statusmsg',
+ },
+ ],
+ listeners: {
+ activate: reload,
+ },
+ });
+
+ me.callParent();
+ },
+}, function() {
+ Ext.define('pve-sdnzone-content', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'vnet', 'status', 'statusmsg',
+ {
+ name: 'text',
+ convert: function(value, record) {
+ // check for volid, because if you click on a grouping header,
+ // it calls convert (but with an empty volid)
+ if (value || record.data.vnet === null) {
+ return value;
+ }
+ return PVE.Utils.format_sdnvnet_type(value, {}, record);
+ },
+ },
+ ],
+ idProperty: 'vnet',
+ });
+});
+Ext.define('PVE.sdn.ZoneView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveSDNZoneView'],
+
+ onlineHelp: 'pvesdn_config_zone',
+
+ stateful: true,
+ stateId: 'grid-sdn-zone',
+
+ createSDNEditWindow: function(type, sid) {
+ let schema = PVE.Utils.sdnzoneSchema[type];
+ if (!schema || !schema.ipanel) {
+ throw "no editor registered for zone type: " + type;
+ }
+
+ Ext.create('PVE.sdn.zones.BaseEdit', {
+ paneltype: 'PVE.sdn.zones.' + schema.ipanel,
+ type: type,
+ zone: sid,
+ autoShow: true,
+ listeners: {
+ destroy: this.reloadStore,
+ },
+ });
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'pve-sdn-zone',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/zones?pending=1",
+ },
+ sorters: {
+ property: 'zone',
+ direction: 'ASC',
+ },
+ });
+
+ let reload = function() {
+ store.load();
+ };
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ let type = rec.data.type,
+ zone = rec.data.zone;
+
+ me.createSDNEditWindow(type, zone);
+ };
+
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/cluster/sdn/zones/',
+ callback: reload,
+ });
+
+ let set_button_status = function() {
+ var rec = me.selModel.getSelection()[0];
+
+ if (!rec || rec.data.state === 'deleted') {
+ edit_btn.disable();
+ remove_btn.disable();
+ }
+ };
+
+ // else we cannot dynamically generate the add menu handlers
+ let addHandleGenerator = function(type) {
+ return function() { me.createSDNEditWindow(type); };
+ };
+ let addMenuItems = [];
+ for (const [type, zone] of Object.entries(PVE.Utils.sdnzoneSchema)) {
+ if (zone.hideAdd) {
+ continue;
+ }
+ addMenuItems.push({
+ text: PVE.Utils.format_sdnzone_type(type),
+ iconCls: 'fa fa-fw fa-' + zone.faIcon,
+ handler: addHandleGenerator(type),
+ });
+ }
+
+ Ext.apply(me, {
+ store: store,
+ reloadStore: reload,
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: addMenuItems,
+ }),
+ },
+ remove_btn,
+ edit_btn,
+ ],
+ columns: [
+ {
+ header: 'ID',
+ width: 100,
+ dataIndex: 'zone',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'zone', 1);
+ },
+ },
+ {
+ header: gettext('Type'),
+ width: 100,
+ dataIndex: 'type',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'type', 1);
+ },
+ },
+ {
+ header: 'MTU',
+ width: 50,
+ dataIndex: 'mtu',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'mtu');
+ },
+ },
+ {
+ header: 'Ipam',
+ flex: 3,
+ dataIndex: 'ipam',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'ipam');
+ },
+ },
+ {
+ header: gettext('Domain'),
+ flex: 3,
+ dataIndex: 'dnszone',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'dnszone');
+ },
+ },
+ {
+ header: gettext('Dns'),
+ flex: 3,
+ dataIndex: 'dns',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'dns');
+ },
+ },
+ {
+ header: gettext('Reverse dns'),
+ flex: 3,
+ dataIndex: 'reversedns',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'reversedns');
+ },
+ },
+ {
+ header: gettext('Nodes'),
+ flex: 3,
+ dataIndex: 'nodes',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending(rec, value, 'nodes');
+ },
+ },
+ {
+ header: gettext('State'),
+ width: 100,
+ dataIndex: 'state',
+ renderer: function(value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending_state(rec, value);
+ },
+ },
+ ],
+ listeners: {
+ activate: reload,
+ itemdblclick: run_editor,
+ selectionchange: set_button_status,
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.Options', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveSDNOptions',
+
+ title: 'Options',
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+
+ onlineHelp: 'pvesdn_config_controllers',
+
+ items: [
+ {
+ xtype: 'pveSDNControllerView',
+ title: gettext('Controllers'),
+ flex: 1,
+ padding: '0 0 20 0',
+ border: 0,
+ },
+ {
+ xtype: 'pveSDNIpamView',
+ title: 'IPAMs',
+ flex: 1,
+ padding: '0 0 20 0',
+ border: 0,
+ }, {
+ xtype: 'pveSDNDnsView',
+ title: 'DNS',
+ flex: 1,
+ border: 0,
+ },
+ ],
+});
+Ext.define('PVE.panel.SDNControllerBase', {
+ extend: 'Proxmox.panel.InputPanel',
+
+ type: '',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.controller;
+ }
+
+ return values;
+ },
+});
+
+Ext.define('PVE.sdn.controllers.BaseEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = !me.controllerid;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/cluster/sdn/controllers';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/cluster/sdn/controllers/' + me.controllerid;
+ me.method = 'PUT';
+ }
+
+ var ipanel = Ext.create(me.paneltype, {
+ type: me.type,
+ isCreate: me.isCreate,
+ controllerid: me.controllerid,
+ });
+
+ Ext.apply(me, {
+ subject: PVE.Utils.format_sdncontroller_type(me.type),
+ isAdd: true,
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+ var ctypes = values.content || '';
+
+ values.content = ctypes.split(',');
+
+ if (values.nodes) {
+ values.nodes = values.nodes.split(',');
+ }
+ values.enable = values.disable ? 0 : 1;
+
+ ipanel.setValues(values);
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.sdn.controllers.EvpnInputPanel', {
+ extend: 'PVE.panel.SDNControllerBase',
+
+ onlineHelp: 'pvesdn_controller_plugin_evpn',
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'controller',
+ maxLength: 8,
+ value: me.controllerid || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'asn',
+ minValue: 1,
+ maxValue: 4294967295,
+ value: 65000,
+ fieldLabel: 'ASN #',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'peers',
+ fieldLabel: gettext('Peers'),
+ allowBlank: false,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.controllers.BgpInputPanel', {
+ extend: 'PVE.panel.SDNControllerBase',
+
+ onlineHelp: 'pvesdn_controller_plugin_evpn',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ values.controller = 'bgp' + values.node;
+ } else {
+ delete values.controller;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: 'pveNodeSelector',
+ name: 'node',
+ fieldLabel: gettext('Node'),
+ multiSelect: false,
+ autoSelect: false,
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'asn',
+ minValue: 1,
+ maxValue: 4294967295,
+ value: 65000,
+ fieldLabel: 'ASN #',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'peers',
+ fieldLabel: gettext('Peers'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'ebgp',
+ uncheckedValue: 0,
+ checked: false,
+ fieldLabel: 'EBGP',
+ },
+
+ ];
+
+ me.advancedItems = [
+
+ {
+ xtype: 'textfield',
+ name: 'loopback',
+ fieldLabel: gettext('Loopback Interface'),
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'ebgp-multihop',
+ minValue: 1,
+ maxValue: 100,
+ fieldLabel: 'ebgp-multihop',
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'bgp-multipath-as-path-relax',
+ uncheckedValue: 0,
+ checked: false,
+ fieldLabel: 'bgp-multipath-as-path-relax',
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.IpamView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveSDNIpamView'],
+
+ stateful: true,
+ stateId: 'grid-sdn-ipam',
+
+ createSDNEditWindow: function(type, sid) {
+ let schema = PVE.Utils.sdnipamSchema[type];
+ if (!schema || !schema.ipanel) {
+ throw "no editor registered for ipam type: " + type;
+ }
+
+ Ext.create('PVE.sdn.ipams.BaseEdit', {
+ paneltype: 'PVE.sdn.ipams.' + schema.ipanel,
+ type: type,
+ ipam: sid,
+ autoShow: true,
+ listeners: {
+ destroy: this.reloadStore,
+ },
+ });
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'pve-sdn-ipam',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/ipams",
+ },
+ sorters: {
+ property: 'ipam',
+ direction: 'ASC',
+ },
+ });
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ let type = rec.data.type, ipam = rec.data.ipam;
+ me.createSDNEditWindow(type, ipam);
+ };
+
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/cluster/sdn/ipams/',
+ callback: () => store.load(),
+ });
+
+ // else we cannot dynamically generate the add menu handlers
+ let addHandleGenerator = function(type) {
+ return function() { me.createSDNEditWindow(type); };
+ };
+ let addMenuItems = [];
+ for (const [type, ipam] of Object.entries(PVE.Utils.sdnipamSchema)) {
+ if (ipam.hideAdd) {
+ continue;
+ }
+ addMenuItems.push({
+ text: PVE.Utils.format_sdnipam_type(type),
+ iconCls: 'fa fa-fw fa-' + ipam.faIcon,
+ handler: addHandleGenerator(type),
+ });
+ }
+
+ Ext.apply(me, {
+ store: store,
+ reloadStore: () => store.load(),
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: addMenuItems,
+ }),
+ },
+ remove_btn,
+ edit_btn,
+ ],
+ columns: [
+ {
+ header: 'ID',
+ flex: 2,
+ dataIndex: 'ipam',
+ },
+ {
+ header: gettext('Type'),
+ flex: 1,
+ dataIndex: 'type',
+ renderer: PVE.Utils.format_sdnipam_type,
+ },
+ {
+ header: 'url',
+ flex: 1,
+ dataIndex: 'url',
+ },
+ ],
+ listeners: {
+ activate: () => store.load(),
+ itemdblclick: run_editor,
+ },
+ });
+
+ store.load();
+ me.callParent();
+ },
+});
+Ext.define('PVE.panel.SDNIpamBase', {
+ extend: 'Proxmox.panel.InputPanel',
+
+ type: '',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.ipam;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.sdn.ipams.BaseEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = !me.ipam;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/cluster/sdn/ipams';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/cluster/sdn/ipams/' + me.ipam;
+ me.method = 'PUT';
+ }
+
+ var ipanel = Ext.create(me.paneltype, {
+ type: me.type,
+ isCreate: me.isCreate,
+ ipam: me.ipam,
+ });
+
+ Ext.apply(me, {
+ subject: PVE.Utils.format_sdnipam_type(me.type),
+ isAdd: true,
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+ var ctypes = values.content || '';
+
+ values.content = ctypes.split(',');
+
+ if (values.nodes) {
+ values.nodes = values.nodes.split(',');
+ }
+ values.enable = values.disable ? 0 : 1;
+
+ ipanel.setValues(values);
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.sdn.ipams.NetboxInputPanel', {
+ extend: 'PVE.panel.SDNIpamBase',
+
+ onlineHelp: 'pvesdn_ipam_plugin_netbox',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.ipam;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'ipam',
+ maxLength: 10,
+ value: me.zone || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'url',
+ fieldLabel: gettext('Url'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'token',
+ fieldLabel: gettext('Token'),
+ allowBlank: false,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.ipams.PVEIpamInputPanel', {
+ extend: 'PVE.panel.SDNIpamBase',
+
+ onlineHelp: 'pvesdn_ipam_plugin_pveipam',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.ipam;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'ipam',
+ maxLength: 10,
+ value: me.zone || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.ipams.PhpIpamInputPanel', {
+ extend: 'PVE.panel.SDNIpamBase',
+
+ onlineHelp: 'pvesdn_ipam_plugin_phpipam',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.ipam;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'ipam',
+ maxLength: 10,
+ value: me.zone || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'url',
+ fieldLabel: gettext('Url'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'token',
+ fieldLabel: gettext('Token'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'section',
+ fieldLabel: gettext('Section'),
+ allowBlank: false,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.DnsView', {
+ extend: 'Ext.grid.GridPanel',
+ alias: ['widget.pveSDNDnsView'],
+
+ stateful: true,
+ stateId: 'grid-sdn-dns',
+
+ createSDNEditWindow: function(type, sid) {
+ let schema = PVE.Utils.sdndnsSchema[type];
+ if (!schema || !schema.ipanel) {
+ throw "no editor registered for dns type: " + type;
+ }
+
+ Ext.create('PVE.sdn.dns.BaseEdit', {
+ paneltype: 'PVE.sdn.dns.' + schema.ipanel,
+ type: type,
+ dns: sid,
+ autoShow: true,
+ listeners: {
+ destroy: this.reloadStore,
+ },
+ });
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let store = new Ext.data.Store({
+ model: 'pve-sdn-dns',
+ proxy: {
+ type: 'proxmox',
+ url: "/api2/json/cluster/sdn/dns",
+ },
+ sorters: {
+ property: 'dns',
+ direction: 'ASC',
+ },
+ });
+
+ let sm = Ext.create('Ext.selection.RowModel', {});
+
+ let run_editor = function() {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ let type = rec.data.type,
+ dns = rec.data.dns;
+
+ me.createSDNEditWindow(type, dns);
+ };
+
+ let edit_btn = new Proxmox.button.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ selModel: sm,
+ handler: run_editor,
+ });
+
+ let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
+ selModel: sm,
+ baseurl: '/cluster/sdn/dns/',
+ callback: () => store.load(),
+ });
+
+ // else we cannot dynamically generate the add menu handlers
+ let addHandleGenerator = function(type) {
+ return function() { me.createSDNEditWindow(type); };
+ };
+ let addMenuItems = [];
+ for (const [type, dns] of Object.entries(PVE.Utils.sdndnsSchema)) {
+ if (dns.hideAdd) {
+ continue;
+ }
+ addMenuItems.push({
+ text: PVE.Utils.format_sdndns_type(type),
+ iconCls: 'fa fa-fw fa-' + dns.faIcon,
+ handler: addHandleGenerator(type),
+ });
+ }
+
+ Ext.apply(me, {
+ store: store,
+ reloadStore: () => store.load(),
+ selModel: sm,
+ viewConfig: {
+ trackOver: false,
+ },
+ tbar: [
+ {
+ text: gettext('Add'),
+ menu: new Ext.menu.Menu({
+ items: addMenuItems,
+ }),
+ },
+ remove_btn,
+ edit_btn,
+ ],
+ columns: [
+ {
+ header: 'ID',
+ flex: 2,
+ dataIndex: 'dns',
+ },
+ {
+ header: gettext('Type'),
+ flex: 1,
+ dataIndex: 'type',
+ renderer: PVE.Utils.format_sdndns_type,
+ },
+ {
+ header: 'url',
+ flex: 1,
+ dataIndex: 'url',
+ },
+ ],
+ listeners: {
+ activate: () => store.load(),
+ itemdblclick: run_editor,
+ },
+ });
+
+ store.load();
+ me.callParent();
+ },
+});
+Ext.define('PVE.panel.SDNDnsBase', {
+ extend: 'Proxmox.panel.InputPanel',
+
+ type: '',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.dns;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.sdn.dns.BaseEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = !me.dns;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/cluster/sdn/dns';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/cluster/sdn/dns/' + me.dns;
+ me.method = 'PUT';
+ }
+
+ var ipanel = Ext.create(me.paneltype, {
+ type: me.type,
+ isCreate: me.isCreate,
+ dns: me.dns,
+ });
+
+ Ext.apply(me, {
+ subject: PVE.Utils.format_sdndns_type(me.type),
+ isAdd: true,
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+ var ctypes = values.content || '';
+
+ values.content = ctypes.split(',');
+
+ if (values.nodes) {
+ values.nodes = values.nodes.split(',');
+ }
+ values.enable = values.disable ? 0 : 1;
+
+ ipanel.setValues(values);
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.sdn.dns.PowerdnsInputPanel', {
+ extend: 'PVE.panel.SDNDnsBase',
+
+ onlineHelp: 'pvesdn_dns_plugin_powerdns',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.dns;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'dns',
+ maxLength: 10,
+ value: me.dns || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'url',
+ fieldLabel: 'url',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'key',
+ fieldLabel: gettext('api key'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'ttl',
+ fieldLabel: 'ttl',
+ allowBlank: true,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.panel.SDNZoneBase', {
+ extend: 'Proxmox.panel.InputPanel',
+
+ type: '',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.zone;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.advancedItems = [
+ {
+ xtype: 'pveSDNIpamSelector',
+ fieldLabel: gettext('Ipam'),
+ name: 'ipam',
+ value: 'pve',
+ allowBlank: false,
+ },
+ {
+ xtype: 'pveSDNDnsSelector',
+ fieldLabel: gettext('Dns server'),
+ name: 'dns',
+ value: '',
+ allowBlank: true,
+ },
+ {
+ xtype: 'pveSDNDnsSelector',
+ fieldLabel: gettext('Reverse Dns server'),
+ name: 'reversedns',
+ value: '',
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'dnszone',
+ skipEmptyText: true,
+ fieldLabel: gettext('DNS zone'),
+ allowBlank: true,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.sdn.zones.BaseEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ width: 400,
+
+ initComponent: function() {
+ var me = this;
+
+ me.isCreate = !me.zone;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/cluster/sdn/zones';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/cluster/sdn/zones/' + me.zone;
+ me.method = 'PUT';
+ }
+
+ var ipanel = Ext.create(me.paneltype, {
+ type: me.type,
+ isCreate: me.isCreate,
+ zone: me.zone,
+ });
+
+ Ext.apply(me, {
+ subject: PVE.Utils.format_sdnzone_type(me.type),
+ isAdd: true,
+ items: [ipanel],
+ });
+
+ me.callParent();
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ var values = response.result.data;
+ var ctypes = values.content || '';
+
+ values.content = ctypes.split(',');
+
+ if (values.nodes) {
+ values.nodes = values.nodes.split(',');
+ }
+
+ if (values.exitnodes) {
+ values.exitnodes = values.exitnodes.split(',');
+ }
+
+ values.enable = values.disable ? 0 : 1;
+
+ ipanel.setValues(values);
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.sdn.zones.EvpnInputPanel', {
+ extend: 'PVE.panel.SDNZoneBase',
+
+ onlineHelp: 'pvesdn_zone_plugin_evpn',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.zone;
+ }
+
+ if (!values.mac) {
+ delete values.mac;
+ }
+
+ if (values['advertise-subnets'] === 0) {
+ delete values['advertise-subnets'];
+ }
+
+ if (values['exitnodes-local-routing'] === 0) {
+ delete values['exitnodes-local-routing'];
+ }
+
+ if (values['disable-arp-nd-suppression'] === 0) {
+ delete values['disable-arp-nd-suppression'];
+ }
+
+ if (values['exitnodes-primary'] === '') {
+ delete values['exitnodes-primary'];
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'zone',
+ maxLength: 8,
+ value: me.zone || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'pveSDNControllerSelector',
+ fieldLabel: gettext('Controller'),
+ name: 'controller',
+ value: '',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'vrf-vxlan',
+ minValue: 1,
+ maxValue: 16000000,
+ fieldLabel: 'VRF-VXLAN Tag',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'mac',
+ fieldLabel: gettext('Vnet MAC address'),
+ vtype: 'MacAddress',
+ allowBlank: true,
+ emptyText: 'auto',
+ },
+ {
+ xtype: 'pveNodeSelector',
+ name: 'exitnodes',
+ fieldLabel: gettext('Exit Nodes'),
+ multiSelect: true,
+ autoSelect: false,
+ },
+ {
+ xtype: 'pveNodeSelector',
+ name: 'exitnodes-primary',
+ fieldLabel: gettext('Primary Exit Node'),
+ multiSelect: false,
+ autoSelect: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'exitnodes-local-routing',
+ uncheckedValue: 0,
+ checked: false,
+ fieldLabel: gettext('Exit Nodes local routing'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'advertise-subnets',
+ uncheckedValue: 0,
+ checked: false,
+ fieldLabel: gettext('Advertise subnets'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'disable-arp-nd-suppression',
+ uncheckedValue: 0,
+ checked: false,
+ fieldLabel: gettext('Disable arp-nd suppression'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'rt-import',
+ fieldLabel: gettext('Route-target import'),
+ allowBlank: true,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mtu',
+ minValue: 100,
+ maxValue: 65000,
+ fieldLabel: 'MTU',
+ skipEmptyText: true,
+ allowBlank: true,
+ emptyText: 'auto',
+ },
+ {
+ xtype: 'pveNodeSelector',
+ name: 'nodes',
+ fieldLabel: gettext('Nodes'),
+ emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
+ multiSelect: true,
+ autoSelect: false,
+ },
+
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.zones.QinQInputPanel', {
+ extend: 'PVE.panel.SDNZoneBase',
+
+ onlineHelp: 'pvesdn_zone_plugin_qinq',
+
+ onGetValues: function(values) {
+ let me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.sdn;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'zone',
+ maxLength: 8,
+ value: me.zone || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'bridge',
+ fieldLabel: 'Bridge',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'tag',
+ minValue: 0,
+ maxValue: 4096,
+ fieldLabel: gettext('Service VLAN'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'vlan-protocol',
+ fieldLabel: gettext('Service-VLAN Protocol'),
+ allowBlank: true,
+ value: '802.1q',
+ comboItems: [
+ ['802.1q', '802.1q'],
+ ['802.1ad', '802.1ad'],
+ ],
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mtu',
+ minValue: 100,
+ maxValue: 65000,
+ fieldLabel: 'MTU',
+ skipEmptyText: true,
+ allowBlank: true,
+ emptyText: 'auto',
+ },
+ {
+ xtype: 'pveNodeSelector',
+ name: 'nodes',
+ fieldLabel: gettext('Nodes'),
+ emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
+ multiSelect: true,
+ autoSelect: false,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.zones.SimpleInputPanel', {
+ extend: 'PVE.panel.SDNZoneBase',
+
+ onlineHelp: 'pvesdn_zone_plugin_simple',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.zone;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'zone',
+ maxLength: 10,
+ value: me.zone || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mtu',
+ minValue: 100,
+ maxValue: 65000,
+ fieldLabel: 'MTU',
+ skipEmptyText: true,
+ allowBlank: true,
+ emptyText: 'auto',
+ },
+ {
+ xtype: 'pveNodeSelector',
+ name: 'nodes',
+ fieldLabel: gettext('Nodes'),
+ emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
+ multiSelect: true,
+ autoSelect: false,
+ },
+
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.zones.VlanInputPanel', {
+ extend: 'PVE.panel.SDNZoneBase',
+
+ onlineHelp: 'pvesdn_zone_plugin_vlan',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.zone;
+ }
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'zone',
+ maxLength: 10,
+ value: me.zone || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'bridge',
+ fieldLabel: 'Bridge',
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mtu',
+ minValue: 100,
+ maxValue: 65000,
+ fieldLabel: 'MTU',
+ skipEmptyText: true,
+ allowBlank: true,
+ emptyText: 'auto',
+ },
+ {
+ xtype: 'pveNodeSelector',
+ name: 'nodes',
+ fieldLabel: gettext('Nodes'),
+ emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
+ multiSelect: true,
+ autoSelect: false,
+ },
+
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.sdn.zones.VxlanInputPanel', {
+ extend: 'PVE.panel.SDNZoneBase',
+
+ onlineHelp: 'pvesdn_zone_plugin_vxlan',
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.zone;
+ }
+
+ delete values.mode;
+
+ return values;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.items = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ maxLength: 8,
+ name: 'zone',
+ value: me.zone || '',
+ fieldLabel: 'ID',
+ allowBlank: false,
+ },
+ {
+ xtype: 'textfield',
+ name: 'peers',
+ fieldLabel: gettext('Peer Address List'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mtu',
+ minValue: 100,
+ maxValue: 65000,
+ fieldLabel: 'MTU',
+ skipEmptyText: true,
+ allowBlank: true,
+ emptyText: 'auto',
+ },
+ {
+ xtype: 'pveNodeSelector',
+ name: 'nodes',
+ fieldLabel: gettext('Nodes'),
+ emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
+ multiSelect: true,
+ autoSelect: false,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.ContentView', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: 'widget.pveStorageContentView',
+
+ viewConfig: {
+ trackOver: false,
+ loadMask: false,
+ },
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = me.pveSelNode.data.node;
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+ }
+ const nodename = me.nodename;
+
+ if (!me.storage) {
+ me.storage = me.pveSelNode.data.storage;
+ if (!me.storage) {
+ throw "no storage ID specified";
+ }
+ }
+ const storage = me.storage;
+
+ var content = me.content;
+ if (!content) {
+ throw "no content type specified";
+ }
+
+ const baseurl = `/nodes/${nodename}/storage/${storage}/content`;
+ let store = me.store = Ext.create('Ext.data.Store', {
+ model: 'pve-storage-content',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json' + baseurl,
+ extraParams: {
+ content: content,
+ },
+ },
+ sorters: {
+ property: 'volid',
+ direction: 'ASC',
+ },
+ });
+
+ if (!me.sm) {
+ me.sm = Ext.create('Ext.selection.RowModel', {});
+ }
+ let sm = me.sm;
+
+ let reload = () => store.load();
+
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ if (!me.tbar) {
+ me.tbar = [];
+ }
+ if (me.useUploadButton) {
+ me.tbar.unshift(
+ {
+ xtype: 'button',
+ text: gettext('Upload'),
+ disabled: !me.enableUploadButton,
+ handler: function() {
+ Ext.create('PVE.window.UploadToStorage', {
+ nodename: nodename,
+ storage: storage,
+ content: content,
+ autoShow: true,
+ taskDone: () => reload(),
+ });
+ },
+ },
+ {
+ xtype: 'button',
+ text: gettext('Download from URL'),
+ disabled: !me.enableDownloadUrlButton,
+ handler: function() {
+ Ext.create('PVE.window.DownloadUrlToStorage', {
+ nodename: nodename,
+ storage: storage,
+ content: content,
+ autoShow: true,
+ taskDone: () => reload(),
+ });
+ },
+ },
+ '-',
+ );
+ }
+ if (!me.useCustomRemoveButton) {
+ me.tbar.push({
+ xtype: 'proxmoxStdRemoveButton',
+ selModel: sm,
+ enableFn: rec => !rec?.data?.protected,
+ delay: 5,
+ callback: () => reload(),
+ baseurl: baseurl + '/',
+ });
+ }
+ me.tbar.push(
+ '->',
+ gettext('Search') + ':',
+ ' ',
+ {
+ xtype: 'textfield',
+ width: 200,
+ enableKeyEvents: true,
+ emptyText: content === 'backup' ? gettext('Name, Format, Notes') : gettext('Name, Format'),
+ listeners: {
+ keyup: {
+ buffer: 500,
+ fn: function(field) {
+ let needle = field.getValue().toLocaleLowerCase();
+ store.clearFilter(true);
+ store.filter([
+ {
+ filterFn: ({ data }) =>
+ data.text?.toLocaleLowerCase().includes(needle) ||
+ data.notes?.toLocaleLowerCase().includes(needle),
+ },
+ ]);
+ },
+ },
+ change: function(field, newValue, oldValue) {
+ if (newValue !== this.originalValue) {
+ this.triggers.clear.setVisible(true);
+ }
+ },
+ },
+ triggers: {
+ clear: {
+ cls: 'pmx-clear-trigger',
+ weight: -1,
+ hidden: true,
+ handler: function() {
+ this.triggers.clear.setVisible(false);
+ this.setValue(this.originalValue);
+ store.clearFilter();
+ },
+ },
+ },
+ },
+ );
+
+ let availableColumns = {
+ 'name': {
+ header: gettext('Name'),
+ flex: 2,
+ sortable: true,
+ renderer: PVE.Utils.render_storage_content,
+ dataIndex: 'text',
+ },
+ 'notes': {
+ header: gettext('Notes'),
+ flex: 1,
+ renderer: Ext.htmlEncode,
+ dataIndex: 'notes',
+ },
+ 'protected': {
+ header: ``,
+ tooltip: gettext('Protected'),
+ width: 30,
+ renderer: v => v ? `` : '',
+ sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0),
+ dataIndex: 'protected',
+ },
+ 'date': {
+ header: gettext('Date'),
+ width: 150,
+ dataIndex: 'vdate',
+ },
+ 'format': {
+ header: gettext('Format'),
+ width: 100,
+ dataIndex: 'format',
+ },
+ 'size': {
+ header: gettext('Size'),
+ width: 100,
+ renderer: Proxmox.Utils.format_size,
+ dataIndex: 'size',
+ },
+ };
+
+ let showColumns = me.showColumns || ['name', 'date', 'format', 'size'];
+
+ Object.keys(availableColumns).forEach(function(key) {
+ if (!showColumns.includes(key)) {
+ delete availableColumns[key];
+ }
+ });
+
+ if (me.extraColumns && typeof me.extraColumns === 'object') {
+ Object.assign(availableColumns, me.extraColumns);
+ }
+ const columns = Object.values(availableColumns);
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: me.tbar,
+ columns: columns,
+ listeners: {
+ activate: reload,
+ },
+ });
+
+ me.callParent();
+ },
+}, function() {
+ Ext.define('pve-storage-content', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'volid', 'content', 'format', 'size', 'used', 'vmid',
+ 'channel', 'id', 'lun', 'notes', 'verification',
+ {
+ name: 'text',
+ convert: function(value, record) {
+ // check for volid, because if you click on a grouping header,
+ // it calls convert (but with an empty volid)
+ if (value || record.data.volid === null) {
+ return value;
+ }
+ return PVE.Utils.render_storage_content(value, {}, record);
+ },
+ },
+ {
+ name: 'vdate',
+ convert: function(value, record) {
+ // check for volid, because if you click on a grouping header,
+ // it calls convert (but with an empty volid)
+ if (value || record.data.volid === null) {
+ return value;
+ }
+ let t = record.data.content;
+ if (t === "backup") {
+ let v = record.data.volid;
+ let match = v.match(/(\d{4}_\d{2}_\d{2})-(\d{2}_\d{2}_\d{2})/);
+ if (match) {
+ let date = match[1].replace(/_/g, '-');
+ let time = match[2].replace(/_/g, ':');
+ return date + " " + time;
+ }
+ }
+ if (record.data.ctime) {
+ let ctime = new Date(record.data.ctime * 1000);
+ return Ext.Date.format(ctime, 'Y-m-d H:i:s');
+ }
+ return '';
+ },
+ },
+ ],
+ idProperty: 'volid',
+ });
+});
+Ext.define('PVE.storage.BackupView', {
+ extend: 'PVE.storage.ContentView',
+
+ alias: 'widget.pveStorageBackupView',
+
+ showColumns: ['name', 'notes', 'protected', 'date', 'format', 'size'],
+
+ initComponent: function() {
+ let me = this;
+
+ let nodename = me.nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ let storage = me.storage = me.pveSelNode.data.storage;
+ if (!storage) {
+ throw "no storage ID specified";
+ }
+
+ me.content = 'backup';
+
+ let sm = me.sm = Ext.create('Ext.selection.RowModel', {});
+
+ let pruneButton = Ext.create('Proxmox.button.Button', {
+ text: gettext('Prune group'),
+ disabled: true,
+ selModel: sm,
+ setBackupGroup: function(backup) {
+ if (backup) {
+ let name = backup.text;
+ let vmid = backup.vmid;
+ let format = backup.format;
+
+ let vmtype;
+ if (name.startsWith('vzdump-lxc-') || format === "pbs-ct") {
+ vmtype = 'lxc';
+ } else if (name.startsWith('vzdump-qemu-') || format === "pbs-vm") {
+ vmtype = 'qemu';
+ }
+
+ if (vmid && vmtype) {
+ this.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`);
+ this.vmid = vmid;
+ this.vmtype = vmtype;
+ this.setDisabled(false);
+ return;
+ }
+ }
+ this.setText(gettext('Prune group'));
+ this.vmid = null;
+ this.vmtype = null;
+ this.setDisabled(true);
+ },
+ handler: function(b, e, rec) {
+ Ext.create('PVE.window.Prune', {
+ autoShow: true,
+ nodename,
+ storage,
+ backup_id: this.vmid,
+ backup_type: this.vmtype,
+ listeners: {
+ destroy: () => me.store.load(),
+ },
+ });
+ },
+ });
+
+ me.on('selectionchange', function(model, srecords, eOpts) {
+ if (srecords.length === 1) {
+ pruneButton.setBackupGroup(srecords[0].data);
+ } else {
+ pruneButton.setBackupGroup(null);
+ }
+ });
+
+ let isPBS = me.pluginType === 'pbs';
+
+ me.tbar = [
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Restore'),
+ selModel: sm,
+ disabled: true,
+ handler: function(b, e, rec) {
+ let vmtype;
+ if (PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format)) {
+ vmtype = 'qemu';
+ } else if (PVE.Utils.volume_is_lxc_backup(rec.data.volid, rec.data.format)) {
+ vmtype = 'lxc';
+ } else {
+ return;
+ }
+
+ Ext.create('PVE.window.Restore', {
+ autoShow: true,
+ nodename,
+ volid: rec.data.volid,
+ volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
+ vmtype,
+ isPBS,
+ listeners: {
+ destroy: () => me.store.load(),
+ },
+ });
+ },
+ },
+ ];
+ if (isPBS) {
+ me.tbar.push({
+ xtype: 'proxmoxButton',
+ text: gettext('File Restore'),
+ disabled: true,
+ selModel: sm,
+ handler: function(b, e, rec) {
+ let isVMArchive = PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format);
+ Ext.create('Proxmox.window.FileBrowser', {
+ title: gettext('File Restore') + " - " + rec.data.text,
+ listURL: `/api2/json/nodes/localhost/storage/${me.storage}/file-restore/list`,
+ downloadURL: `/api2/json/nodes/localhost/storage/${me.storage}/file-restore/download`,
+ extraParams: {
+ volume: rec.data.volid,
+ },
+ archive: isVMArchive ? 'all' : undefined,
+ autoShow: true,
+ });
+ },
+ });
+ }
+ me.tbar.push(
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Show Configuration'),
+ disabled: true,
+ selModel: sm,
+ handler: function(b, e, rec) {
+ Ext.create('PVE.window.BackupConfig', {
+ autoShow: true,
+ volume: rec.data.volid,
+ pveSelNode: me.pveSelNode,
+ });
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit Notes'),
+ disabled: true,
+ selModel: sm,
+ handler: function(b, e, rec) {
+ let volid = rec.data.volid;
+ Ext.create('Proxmox.window.Edit', {
+ autoShow: true,
+ autoLoad: true,
+ width: 600,
+ height: 400,
+ resizable: true,
+ title: gettext('Notes'),
+ url: `/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`,
+ layout: 'fit',
+ items: [
+ {
+ xtype: 'textarea',
+ layout: 'fit',
+ name: 'notes',
+ height: '100%',
+ },
+ ],
+ listeners: {
+ destroy: () => me.store.load(),
+ },
+ });
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Change Protection'),
+ disabled: true,
+ handler: function(button, event, record) {
+ const volid = record.data.volid;
+ Proxmox.Utils.API2Request({
+ url: `/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`,
+ method: 'PUT',
+ waitMsgTarget: me,
+ params: { 'protected': record.data.protected ? 0 : 1 },
+ failure: response => Ext.Msg.alert('Error', response.htmlStatus),
+ success: () => {
+ me.store.load({
+ callback: () => sm.fireEvent('selectionchange', sm, [record]),
+ });
+ },
+ });
+ },
+ },
+ '-',
+ pruneButton,
+ );
+
+ if (isPBS) {
+ me.extraColumns = {
+ encrypted: {
+ header: gettext('Encrypted'),
+ dataIndex: 'encrypted',
+ renderer: PVE.Utils.render_backup_encryption,
+ sorter: {
+ property: 'encrypted',
+ transform: encrypted => encrypted ? 1 : 0,
+ },
+ },
+ verification: {
+ header: gettext('Verify State'),
+ dataIndex: 'verification',
+ renderer: PVE.Utils.render_backup_verification,
+ sorter: {
+ property: 'verification',
+ transform: value => {
+ let state = value?.state ?? 'none';
+ let order = PVE.Utils.verificationStateOrder;
+ return order[state] ?? order.__default__;
+ },
+ },
+ },
+ };
+ }
+
+ me.callParent();
+
+ me.store.getSorters().clear();
+ me.store.setSorters([
+ {
+ property: 'vmid',
+ direction: 'ASC',
+ },
+ {
+ property: 'vdate',
+ direction: 'DESC',
+ },
+ ]);
+ },
+});
+Ext.define('PVE.panel.StorageBase', {
+ extend: 'Proxmox.panel.InputPanel',
+ controller: 'storageEdit',
+
+ type: '',
+
+ onGetValues: function(values) {
+ let me = this;
+
+ if (me.isCreate) {
+ values.type = me.type;
+ } else {
+ delete values.storage;
+ }
+
+ values.disable = values.enable ? 0 : 1;
+ delete values.enable;
+
+ return values;
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.column1.unshift({
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'storage',
+ value: me.storageId || '',
+ fieldLabel: 'ID',
+ vtype: 'StorageId',
+ allowBlank: false,
+ });
+
+ me.column2 = me.column2 || [];
+ me.column2.unshift(
+ {
+ xtype: 'pveNodeSelector',
+ name: 'nodes',
+ reference: 'storageNodeRestriction',
+ disabled: me.storageId === 'local',
+ fieldLabel: gettext('Nodes'),
+ emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
+ multiSelect: true,
+ autoSelect: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'enable',
+ checked: true,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Enable'),
+ },
+ );
+
+ const qemuImgStorageTypes = ['dir', 'btrfs', 'nfs', 'cifs', 'glusterfs'];
+
+ if (qemuImgStorageTypes.includes(me.type)) {
+ const preallocSelector = {
+ xtype: 'pvePreallocationSelector',
+ name: 'preallocation',
+ fieldLabel: gettext('Preallocation'),
+ allowBlank: false,
+ deleteEmpty: !me.isCreate,
+ value: '__default__',
+ };
+
+ me.advancedColumn1 = me.advancedColumn1 || [];
+ me.advancedColumn2 = me.advancedColumn2 || [];
+ if (me.advancedColumn2.length < me.advancedColumn1.length) {
+ me.advancedColumn2.unshift(preallocSelector);
+ } else {
+ me.advancedColumn1.unshift(preallocSelector);
+ }
+ }
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.BaseEdit', {
+ extend: 'Proxmox.window.Edit',
+
+ apiCallDone: function(success, response, options) {
+ let me = this;
+ if (typeof me.ipanel.apiCallDone === "function") {
+ me.ipanel.apiCallDone(success, response, options);
+ }
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ me.isCreate = !me.storageId;
+
+ if (me.isCreate) {
+ me.url = '/api2/extjs/storage';
+ me.method = 'POST';
+ } else {
+ me.url = '/api2/extjs/storage/' + me.storageId;
+ me.method = 'PUT';
+ }
+
+ me.ipanel = Ext.create(me.paneltype, {
+ title: gettext('General'),
+ type: me.type,
+ isCreate: me.isCreate,
+ storageId: me.storageId,
+ });
+
+ Ext.apply(me, {
+ subject: PVE.Utils.format_storage_type(me.type),
+ isAdd: true,
+ bodyPadding: 0,
+ items: {
+ xtype: 'tabpanel',
+ region: 'center',
+ layout: 'fit',
+ bodyPadding: 10,
+ items: [
+ me.ipanel,
+ {
+ xtype: 'pveBackupJobPrunePanel',
+ title: gettext('Backup Retention'),
+ hasMaxProtected: true,
+ isCreate: me.isCreate,
+ keepAllDefaultForCreate: true,
+ showPBSHint: me.ipanel.isPBS,
+ fallbackHintHtml: gettext('Without any keep option, the node\'s vzdump.conf or `keep-all` is used as fallback for backup jobs'),
+ },
+ ],
+ },
+ });
+
+ if (me.ipanel.extraTabs) {
+ me.ipanel.extraTabs.forEach(panel => {
+ panel.isCreate = me.isCreate;
+ me.items.items.push(panel);
+ });
+ }
+
+ me.callParent();
+
+ if (!me.canDoBackups) {
+ // cannot mask now, not fully rendered until activated
+ me.down('pmxPruneInputPanel').needMask = true;
+ }
+
+ if (!me.isCreate) {
+ me.load({
+ success: function(response, options) {
+ let values = response.result.data;
+ let ctypes = values.content || '';
+
+ values.content = ctypes.split(',');
+
+ if (values.nodes) {
+ values.nodes = values.nodes.split(',');
+ }
+ values.enable = values.disable ? 0 : 1;
+ if (values['prune-backups']) {
+ let retention = PVE.Parser.parsePropertyString(values['prune-backups']);
+ delete values['prune-backups'];
+ Object.assign(values, retention);
+ } else if (values.maxfiles !== undefined) {
+ if (values.maxfiles > 0) {
+ values['keep-last'] = values.maxfiles;
+ }
+ delete values.maxfiles;
+ }
+
+ me.query('inputpanel').forEach(panel => {
+ panel.setValues(values);
+ });
+ },
+ });
+ }
+ },
+});
+Ext.define('PVE.storage.Browser', {
+ extend: 'PVE.panel.Config',
+ alias: 'widget.PVE.storage.Browser',
+
+ onlineHelp: 'chapter_storage',
+
+ initComponent: function() {
+ let me = this;
+
+ let nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ let storeid = me.pveSelNode.data.storage;
+ if (!storeid) {
+ throw "no storage ID specified";
+ }
+
+ me.items = [
+ {
+ title: gettext('Summary'),
+ xtype: 'pveStorageSummary',
+ iconCls: 'fa fa-book',
+ itemId: 'summary',
+ },
+ ];
+
+ let caps = Ext.state.Manager.get('GuiCap');
+
+ Ext.apply(me, {
+ title: Ext.String.format(gettext("Storage {0} on node {1}"), `'${storeid}'`, `'${nodename}'`),
+ hstateid: 'storagetab',
+ });
+
+ if (
+ caps.storage['Datastore.Allocate'] ||
+ caps.storage['Datastore.AllocateSpace'] ||
+ caps.storage['Datastore.Audit']
+ ) {
+ let storageInfo = PVE.data.ResourceStore.findRecord(
+ 'id',
+ `storage/${nodename}/${storeid}`,
+ 0, // startIndex
+ false, // anyMatch
+ true, // caseSensitive
+ true, // exactMatch
+ );
+ let res = storageInfo.data;
+ let plugin = res.plugintype;
+ let contents = res.content.split(',');
+
+ let enableUpload = !!caps.storage['Datastore.AllocateTemplate'];
+ let enableDownloadUrl = enableUpload && !!(caps.nodes['Sys.Audit'] && caps.nodes['Sys.Modify']);
+
+ if (contents.includes('backup')) {
+ me.items.push({
+ xtype: 'pveStorageBackupView',
+ title: gettext('Backups'),
+ iconCls: 'fa fa-floppy-o',
+ itemId: 'contentBackup',
+ pluginType: plugin,
+ });
+ }
+ if (contents.includes('images')) {
+ me.items.push({
+ xtype: 'pveStorageImageView',
+ title: gettext('VM Disks'),
+ iconCls: 'fa fa-hdd-o',
+ itemId: 'contentImages',
+ content: 'images',
+ pluginType: plugin,
+ });
+ }
+ if (contents.includes('rootdir')) {
+ me.items.push({
+ xtype: 'pveStorageImageView',
+ title: gettext('CT Volumes'),
+ iconCls: 'fa fa-hdd-o lxc',
+ itemId: 'contentRootdir',
+ content: 'rootdir',
+ pluginType: plugin,
+ });
+ }
+ if (contents.includes('iso')) {
+ me.items.push({
+ xtype: 'pveStorageContentView',
+ title: gettext('ISO Images'),
+ iconCls: 'pve-itype-treelist-item-icon-cdrom',
+ itemId: 'contentIso',
+ content: 'iso',
+ pluginType: plugin,
+ enableUploadButton: enableUpload,
+ enableDownloadUrlButton: enableDownloadUrl,
+ useUploadButton: true,
+ });
+ }
+ if (contents.includes('vztmpl')) {
+ me.items.push({
+ xtype: 'pveStorageTemplateView',
+ title: gettext('CT Templates'),
+ iconCls: 'fa fa-file-o lxc',
+ itemId: 'contentVztmpl',
+ pluginType: plugin,
+ enableUploadButton: enableUpload,
+ enableDownloadUrlButton: enableDownloadUrl,
+ useUploadButton: true,
+ });
+ }
+ if (contents.includes('snippets')) {
+ me.items.push({
+ xtype: 'pveStorageContentView',
+ title: gettext('Snippets'),
+ iconCls: 'fa fa-file-code-o',
+ itemId: 'contentSnippets',
+ content: 'snippets',
+ pluginType: plugin,
+ });
+ }
+ }
+
+ if (caps.storage['Permissions.Modify']) {
+ me.items.push({
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ path: `/storage/${storeid}`,
+ });
+ }
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.CIFSScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveCIFSScan',
+
+ queryParam: 'server',
+
+ valueField: 'share',
+ displayField: 'share',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: gettext('Scanning...'),
+ width: 350,
+ },
+ doRawQuery: Ext.emptyFn,
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.cifsServer) {
+ me.store.removeAll();
+ }
+
+ var params = {};
+ if (me.cifsUsername) {
+ params.username = me.cifsUsername;
+ }
+ if (me.cifsPassword) {
+ params.password = me.cifsPassword;
+ }
+ if (me.cifsDomain) {
+ params.domain = me.cifsDomain;
+ }
+
+ me.store.getProxy().setExtraParams(params);
+ me.allQuery = me.cifsServer;
+
+ me.callParent();
+ },
+
+ resetProxy: function() {
+ let me = this;
+ me.lastQuery = null;
+ if (!me.readOnly && !me.disabled) {
+ if (me.isExpanded) {
+ me.collapse();
+ }
+ }
+ },
+
+ setServer: function(server) {
+ if (this.cifsServer !== server) {
+ this.cifsServer = server;
+ this.resetProxy();
+ }
+ },
+ setUsername: function(username) {
+ if (this.cifsUsername !== username) {
+ this.cifsUsername = username;
+ this.resetProxy();
+ }
+ },
+ setPassword: function(password) {
+ if (this.cifsPassword !== password) {
+ this.cifsPassword = password;
+ this.resetProxy();
+ }
+ },
+ setDomain: function(domain) {
+ if (this.cifsDomain !== domain) {
+ this.cifsDomain = domain;
+ this.resetProxy();
+ }
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ let store = Ext.create('Ext.data.Store', {
+ fields: ['description', 'share'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/cifs',
+ },
+ });
+ store.sort('share', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+
+ let picker = me.getPicker();
+ // don't use monStoreErrors directly, it doesn't copes well with comboboxes
+ picker.mon(store, 'beforeload', function(s, operation, eOpts) {
+ picker.unmask();
+ delete picker.minHeight;
+ });
+ picker.mon(store.proxy, 'afterload', function(proxy, request, success) {
+ if (success) {
+ Proxmox.Utils.setErrorMask(picker, false);
+ return;
+ }
+ let error = request._operation.getError();
+ let msg = Proxmox.Utils.getResponseErrorMessage(error);
+ if (msg) {
+ picker.minHeight = 100;
+ }
+ Proxmox.Utils.setErrorMask(picker, msg);
+ });
+ },
+});
+
+Ext.define('PVE.storage.CIFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_cifs',
+
+ onGetValues: function(values) {
+ let me = this;
+
+ if (values.password?.length === 0) {
+ delete values.password;
+ }
+ if (values.username?.length === 0) {
+ delete values.username;
+ }
+
+ 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=share]');
+ exportField.setServer(value);
+ }
+ },
+ },
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ value: '',
+ fieldLabel: gettext('Username'),
+ emptyText: gettext('Guest user'),
+ listeners: {
+ change: function(f, value) {
+ if (!me.isCreate) {
+ return;
+ }
+ var exportField = me.down('field[name=share]');
+ exportField.setUsername(value);
+ },
+ },
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ inputType: 'password',
+ name: 'password',
+ value: me.isCreate ? '' : '********',
+ emptyText: me.isCreate ? gettext('None') : '',
+ fieldLabel: gettext('Password'),
+ minLength: 1,
+ listeners: {
+ change: function(f, value) {
+ let exportField = me.down('field[name=share]');
+ exportField.setPassword(value);
+ },
+ },
+ },
+ {
+ xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield',
+ name: 'share',
+ value: '',
+ fieldLabel: 'Share',
+ allowBlank: false,
+ },
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'domain',
+ value: me.isCreate ? '' : undefined,
+ fieldLabel: gettext('Domain'),
+ allowBlank: true,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ let exportField = me.down('field[name=share]');
+ exportField.setDomain(value);
+ }
+ },
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.CephFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ controller: 'cephstorage',
+
+ onlineHelp: 'storage_cephfs',
+
+ viewModel: {
+ type: 'cephstorage',
+ },
+
+ setValues: function(values) {
+ if (values.monhost) {
+ this.viewModel.set('pveceph', false);
+ this.lookupReference('pvecephRef').setValue(false);
+ this.lookupReference('pvecephRef').resetOriginalValue();
+ }
+ this.callParent([values]);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+ me.type = 'cephfs';
+
+ me.column1 = [];
+
+ me.column1.push(
+ {
+ xtype: 'textfield',
+ name: 'monhost',
+ vtype: 'HostList',
+ value: '',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}',
+ },
+ fieldLabel: 'Monitor(s)',
+ allowBlank: false,
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'monhost',
+ bind: {
+ disabled: '{!pveceph}',
+ hidden: '{!pveceph}',
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)',
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ value: 'admin',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ },
+ fieldLabel: gettext('User name'),
+ allowBlank: true,
+ },
+ );
+
+ if (me.isCreate) {
+ me.column1.push({
+ xtype: 'pveCephFSSelector',
+ nodename: me.nodename,
+ name: 'fs-name',
+ bind: {
+ disabled: '{!pveceph}',
+ submitValue: '{pveceph}',
+ hidden: '{!pveceph}',
+ },
+ fieldLabel: gettext('FS Name'),
+ allowBlank: false,
+ }, {
+ xtype: 'textfield',
+ nodename: me.nodename,
+ name: 'fs-name',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}',
+ },
+ fieldLabel: gettext('FS Name'),
+ });
+ }
+
+ me.column2 = [
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['backup', 'iso', 'vztmpl', 'snippets'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: 'backup',
+ multiSelect: true,
+ allowBlank: false,
+ },
+ ];
+
+ me.columnB = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'keyring',
+ fieldLabel: gettext('Secret Key'),
+ value: me.isCreate ? '' : '***********',
+ allowBlank: false,
+ bind: {
+ hidden: '{pveceph}',
+ disabled: '{pveceph}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'pveceph',
+ reference: 'pvecephRef',
+ bind: {
+ disabled: '{!pvecephPossible}',
+ value: '{pveceph}',
+ },
+ checked: true,
+ uncheckedValue: 0,
+ submitValue: false,
+ hidden: !me.isCreate,
+ boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS'),
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.DirInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_directory',
+
+ initComponent: function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'path',
+ value: '',
+ fieldLabel: gettext('Directory'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: 'images',
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false,
+ },
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'shared',
+ uncheckedValue: 0,
+ fieldLabel: gettext('Shared'),
+ },
+ ];
+
+ 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() {
+ // nothing
+ },
+
+ 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.callParent();
+ },
+});
+Ext.define('PVE.storage.ImageView', {
+ extend: 'PVE.storage.ContentView',
+
+ alias: 'widget.pveStorageImageView',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename = me.pveSelNode.data.node;
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var storage = me.storage = me.pveSelNode.data.storage;
+ if (!me.storage) {
+ throw "no storage ID specified";
+ }
+
+ if (!me.content || (me.content !== 'images' && me.content !== 'rootdir')) {
+ throw "content needs to be either 'images' or 'rootdir'";
+ }
+
+ var sm = me.sm = Ext.create('Ext.selection.RowModel', {});
+
+ var reload = function() {
+ me.store.load();
+ };
+
+ me.tbar = [
+ {
+ xtype: 'proxmoxButton',
+ selModel: sm,
+ text: gettext('Remove'),
+ disabled: true,
+ handler: function(btn, event, rec) {
+ let url = `/nodes/${nodename}/storage/${storage}/content/${rec.data.volid}`;
+ var vmid = rec.data.vmid;
+
+ var store = PVE.data.ResourceStore;
+
+ if (vmid && store.findVMID(vmid)) {
+ 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('Proxmox.window.SafeDestroy', {
+ title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid),
+ showProgress: true,
+ url: url,
+ item: { type: 'Image', id: vmid },
+ taskName: 'unknownimgdel',
+ }).show();
+ win.on('destroy', reload);
+ },
+ },
+ ];
+ me.useCustomRemoveButton = true;
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.IScsiScan', {
+ extend: 'PVE.form.ComboBoxSetStoreNode',
+ alias: 'widget.pveIScsiScan',
+
+ queryParam: 'portal',
+ valueField: 'target',
+ displayField: 'target',
+ matchFieldWidth: false,
+ allowBlank: false,
+
+ listConfig: {
+ width: 350,
+ columns: [
+ {
+ dataIndex: 'target',
+ flex: 1,
+ },
+ ],
+ emptyText: PVE.Utils.renderNotFound(gettext('iSCSI Target')),
+ },
+
+ config: {
+ apiSuffix: '/scan/iscsi',
+ },
+
+ showNodeSelector: true,
+
+ reload: function() {
+ let me = this;
+ if (!me.isDisabled()) {
+ me.getStore().load();
+ }
+ },
+
+ setPortal: function(portal) {
+ let me = this;
+ me.portal = portal;
+ me.getStore().getProxy().setExtraParams({ portal });
+ me.reload();
+ },
+
+ setNodeName: function(value) {
+ let me = this;
+ me.callParent([value]);
+ me.reload();
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ let store = Ext.create('Ext.data.Store', {
+ fields: ['target', 'portal'],
+ proxy: {
+ type: 'proxmox',
+ url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
+ },
+ });
+ store.sort('target', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.IScsiInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'storage_open_iscsi',
+
+ onGetValues: function(values) {
+ let me = this;
+
+ values.content = values.luns ? 'images' : 'none';
+ delete values.luns;
+
+ return me.callParent([values]);
+ },
+
+ setValues: function(values) {
+ values.luns = values.content.indexOf('images') !== -1;
+ this.callParent([values]);
+ },
+
+ column1: [
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: '{isCreate}',
+ },
+
+ name: 'portal',
+ value: '',
+ fieldLabel: 'Portal',
+ allowBlank: false,
+
+ editConfig: {
+ listeners: {
+ change: {
+ fn: function(f, value) {
+ let panel = this.up('inputpanel');
+ let exportField = panel.lookup('iScsiTargetScan');
+ if (exportField) {
+ exportField.setDisabled(!value);
+ exportField.setPortal(value);
+ exportField.setValue('');
+ }
+ },
+ buffer: 500,
+ },
+ },
+ },
+ },
+ {
+ cbind: {
+ xtype: (get) => get('isCreate') ? 'pveIScsiScan' : 'displayfield',
+ readOnly: '{!isCreate}',
+ disabled: '{isCreate}',
+ },
+
+ name: 'target',
+ value: '',
+ fieldLabel: gettext('Target'),
+ allowBlank: false,
+ reference: 'iScsiTargetScan',
+ listeners: {
+ nodechanged: function(value) {
+ this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
+ },
+ },
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'checkbox',
+ name: 'luns',
+ checked: true,
+ fieldLabel: gettext('Use LUNs directly'),
+ },
+ ],
+});
+Ext.define('PVE.storage.VgSelector', {
+ extend: 'PVE.form.ComboBoxSetStoreNode',
+ alias: 'widget.pveVgSelector',
+ valueField: 'vg',
+ displayField: 'vg',
+ queryMode: 'local',
+ editable: false,
+
+ listConfig: {
+ columns: [
+ {
+ dataIndex: 'vg',
+ flex: 1,
+ },
+ ],
+ emptyText: PVE.Utils.renderNotFound('VGs'),
+ },
+
+ config: {
+ apiSuffix: '/scan/lvm',
+ },
+
+ showNodeSelector: true,
+
+ setNodeName: function(value) {
+ let me = this;
+ me.callParent([value]);
+ me.getStore().load();
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ let store = Ext.create('Ext.data.Store', {
+ autoLoad: {}, // true,
+ fields: ['vg', 'size', 'free'],
+ proxy: {
+ type: 'proxmox',
+ url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
+ },
+ });
+
+ store.sort('vg', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.BaseStorageSelector', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveBaseStorageSelector',
+
+ existingGroupsText: gettext("Existing volume groups"),
+ queryMode: 'local',
+ editable: false,
+ value: '',
+ valueField: 'storage',
+ displayField: 'text',
+ initComponent: function() {
+ let me = this;
+
+ let store = Ext.create('Ext.data.Store', {
+ autoLoad: {
+ addRecords: true,
+ params: {
+ type: 'iscsi',
+ },
+ },
+ fields: ['storage', 'type', 'content',
+ {
+ name: 'text',
+ convert: function(value, record) {
+ if (record.data.storage) {
+ return record.data.storage + " (iSCSI)";
+ } else {
+ return me.existingGroupsText;
+ }
+ },
+ }],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/storage/',
+ },
+ });
+
+ store.loadData([{ storage: '' }], true);
+
+ store.sort('storage', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.LunSelector', {
+ extend: 'PVE.form.FileSelector',
+ alias: 'widget.pveStorageLunSelector',
+
+ nodename: 'localhost',
+ storageContent: 'images',
+ allowBlank: false,
+
+ initComponent: function() {
+ let me = this;
+
+ if (PVE.data.ResourceStore.getNodes().length > 1) {
+ me.errorHeight = 140;
+ Ext.apply(me.listConfig ?? {}, {
+ tbar: {
+ xtype: 'toolbar',
+ items: [
+ {
+ xtype: "pveStorageScanNodeSelector",
+ autoSelect: false,
+ fieldLabel: gettext('Node to scan'),
+ listeners: {
+ change: (_field, value) => me.setNodename(value),
+ },
+ },
+ ],
+ },
+ emptyText: me.listConfig?.emptyText ?? PVE.Utils.renderNotFound(gettext('Volume')),
+ });
+ }
+
+ me.callParent();
+ },
+
+});
+
+Ext.define('PVE.storage.LVMInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'storage_lvm',
+
+ column1: [
+ {
+ xtype: 'pveBaseStorageSelector',
+ name: 'basesel',
+ fieldLabel: gettext('Base storage'),
+ cbind: {
+ disabled: '{!isCreate}',
+ hidden: '{!isCreate}',
+ },
+ submitValue: false,
+ listeners: {
+ change: function(f, value) {
+ let me = this;
+ let vgField = me.up('inputpanel').lookup('volumeGroupSelector');
+ let vgNameField = me.up('inputpanel').lookup('vgName');
+ let baseField = me.up('inputpanel').lookup('lunSelector');
+
+ vgField.setVisible(!value);
+ vgField.setDisabled(!!value);
+
+ baseField.setVisible(!!value);
+ baseField.setDisabled(!value);
+ baseField.setStorage(value);
+
+ vgNameField.setVisible(!!value);
+ vgNameField.setDisabled(!value);
+ },
+ },
+ },
+ {
+ xtype: 'pveStorageLunSelector',
+ name: 'base',
+ fieldLabel: gettext('Base volume'),
+ reference: 'lunSelector',
+ hidden: true,
+ disabled: true,
+ },
+ {
+ xtype: 'pveVgSelector',
+ name: 'vgname',
+ fieldLabel: gettext('Volume group'),
+ reference: 'volumeGroupSelector',
+ cbind: {
+ disabled: '{!isCreate}',
+ hidden: '{!isCreate}',
+ },
+ allowBlank: false,
+ listeners: {
+ nodechanged: function(value) {
+ this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
+ },
+ },
+ },
+ {
+ name: 'vgname',
+ fieldLabel: gettext('Volume group'),
+ reference: 'vgName',
+ cbind: {
+ xtype: (get) => get('isCreate') ? 'textfield' : 'displayfield',
+ hidden: '{isCreate}',
+ disabled: '{isCreate}',
+ },
+ value: '',
+ allowBlank: false,
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false,
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'shared',
+ uncheckedValue: 0,
+ fieldLabel: gettext('Shared'),
+ },
+ ],
+});
+Ext.define('PVE.storage.TPoolSelector', {
+ extend: 'PVE.form.ComboBoxSetStoreNode',
+ alias: 'widget.pveTPSelector',
+
+ queryParam: 'vg',
+ valueField: 'lv',
+ displayField: 'lv',
+ editable: false,
+ allowBlank: false,
+
+ listConfig: {
+ emptyText: PVE.Utils.renderNotFound('Thin-Pool'),
+ columns: [
+ {
+ dataIndex: 'lv',
+ flex: 1,
+ },
+ ],
+ },
+
+ config: {
+ apiSuffix: '/scan/lvmthin',
+ },
+
+ reload: function() {
+ let me = this;
+ if (!me.isDisabled()) {
+ me.getStore().load();
+ }
+ },
+
+ setVG: function(myvg) {
+ let me = this;
+ me.vg = myvg;
+ me.getStore().getProxy().setExtraParams({ vg: myvg });
+ me.reload();
+ },
+
+ setNodeName: function(value) {
+ let me = this;
+ me.callParent([value]);
+ me.reload();
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ let store = Ext.create('Ext.data.Store', {
+ fields: ['lv'],
+ proxy: {
+ type: 'proxmox',
+ url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
+ },
+ });
+
+ store.sort('lv', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.BaseVGSelector', {
+ extend: 'PVE.form.ComboBoxSetStoreNode',
+ alias: 'widget.pveBaseVGSelector',
+
+ valueField: 'vg',
+ displayField: 'vg',
+ queryMode: 'local',
+ editable: false,
+ allowBlank: false,
+
+ listConfig: {
+ columns: [
+ {
+ dataIndex: 'vg',
+ flex: 1,
+ },
+ ],
+ },
+
+ showNodeSelector: true,
+
+ config: {
+ apiSuffix: '/scan/lvm',
+ },
+
+ setNodeName: function(value) {
+ let me = this;
+ me.callParent([value]);
+ me.getStore().load();
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ let store = Ext.create('Ext.data.Store', {
+ autoLoad: {},
+ fields: ['vg', 'size', 'free'],
+ proxy: {
+ type: 'proxmox',
+ url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
+ },
+ });
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.LvmThinInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'storage_lvmthin',
+
+ column1: [
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: '{isCreate}',
+ },
+
+ name: 'vgname',
+ fieldLabel: gettext('Volume group'),
+
+ editConfig: {
+ xtype: 'pveBaseVGSelector',
+ listeners: {
+ nodechanged: function(value) {
+ let panel = this.up('inputpanel');
+ panel.lookup('thinPoolSelector').setNodeName(value);
+ panel.lookup('storageNodeRestriction').setValue(value);
+ },
+ change: function(f, value) {
+ let vgField = this.up('inputpanel').lookup('thinPoolSelector');
+ if (vgField && !f.isDisabled()) {
+ vgField.setDisabled(!value);
+ vgField.setVG(value);
+ vgField.setValue('');
+ }
+ },
+ },
+ },
+ },
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: '{isCreate}',
+ },
+
+ name: 'thinpool',
+ fieldLabel: gettext('Thin Pool'),
+ allowBlank: false,
+
+ editConfig: {
+ xtype: 'pveTPSelector',
+ reference: 'thinPoolSelector',
+ disabled: true,
+ },
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false,
+ },
+ ],
+});
+Ext.define('PVE.storage.BTRFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_btrfs',
+
+ initComponent: function() {
+ let me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'path',
+ value: '',
+ fieldLabel: gettext('Path'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ fieldLabel: gettext('Content'),
+ allowBlank: false,
+ },
+ ];
+
+ me.columnB = [
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: `BTRFS integration is currently a technology preview.`,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.NFSScan', {
+ extend: 'Ext.form.field.ComboBox',
+ alias: 'widget.pveNFSScan',
+
+ queryParam: 'server',
+
+ valueField: 'path',
+ displayField: 'path',
+ matchFieldWidth: false,
+ listConfig: {
+ loadingText: gettext('Scanning...'),
+ width: 350,
+ },
+ doRawQuery: function() {
+ // do nothing
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+
+ if (!me.queryCaching || me.lastQuery !== me.nfsServer) {
+ me.store.removeAll();
+ }
+
+ me.allQuery = me.nfsServer;
+
+ me.callParent();
+ },
+
+ setServer: function(server) {
+ var me = this;
+
+ me.nfsServer = server;
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ var store = Ext.create('Ext.data.Store', {
+ fields: ['path', 'options'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + me.nodename + '/scan/nfs',
+ },
+ });
+
+ store.sort('path', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.NFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_nfs',
+
+ options: [],
+
+ onGetValues: function(values) {
+ var me = this;
+
+ var i;
+ var res = [];
+ for (i = 0; i < me.options.length; i++) {
+ var item = me.options[i];
+ if (!item.match(/^vers=(.*)$/)) {
+ res.push(item);
+ }
+ }
+ if (values.nfsversion && values.nfsversion !== '__default__') {
+ res.push('vers=' + values.nfsversion);
+ }
+ delete values.nfsversion;
+ values.options = res.join(',');
+ if (values.options === '') {
+ delete values.options;
+ if (!me.isCreate) {
+ values.delete = "options";
+ }
+ }
+
+ return me.callParent([values]);
+ },
+
+ setValues: function(values) {
+ var me = this;
+ if (values.options) {
+ me.options = values.options.split(',');
+ me.options.forEach(function(item) {
+ var match = item.match(/^vers=(.*)$/);
+ if (match) {
+ values.nfsversion = match[1];
+ }
+ });
+ }
+ return me.callParent([values]);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'server',
+ value: '',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ listeners: {
+ change: function(f, value) {
+ if (me.isCreate) {
+ 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.advancedColumn2 = [
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('NFS Version'),
+ name: 'nfsversion',
+ value: '__default__',
+ deleteEmpty: false,
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText],
+ ['3', '3'],
+ ['4', '4'],
+ ['4.1', '4.1'],
+ ['4.2', '4.2'],
+ ],
+ },
+ ];
+
+ me.callParent();
+ },
+});
+/*global QRCode*/
+Ext.define('PVE.Storage.PBSKeyShow', {
+ extend: 'Ext.window.Window',
+ xtype: 'pvePBSKeyShow',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ width: 600,
+ modal: true,
+ resizable: false,
+ title: gettext('Important: Save your Encryption Key'),
+
+ // avoid that esc closes this by mistake, force user to more manual action
+ onEsc: Ext.emptyFn,
+ closable: false,
+
+ items: [
+ {
+ xtype: 'form',
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+ bodyPadding: 10,
+ border: false,
+ defaults: {
+ anchor: '100%',
+ border: false,
+ padding: '10 0 0 0',
+ },
+ items: [
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Key'),
+ labelWidth: 80,
+ inputId: 'encryption-key-value',
+ cbind: {
+ value: '{key}',
+ },
+ editable: false,
+ },
+ {
+ xtype: 'component',
+ html: gettext('Keep your encryption key safe, but easily accessible for disaster recovery.')
+ + '
' + gettext('We recommend the following safe-keeping strategy:'),
+ },
+ {
+ xtyp: 'container',
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'component',
+ html: '1. ' + gettext('Save the key in your password manager.'),
+ flex: 1,
+ },
+ {
+ xtype: 'button',
+ text: gettext('Copy Key'),
+ iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ width: 110,
+ handler: function(b) {
+ document.getElementById('encryption-key-value').select();
+ document.execCommand("copy");
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'component',
+ html: '2. ' + gettext('Download the key to a USB (pen) drive, placed in secure vault.'),
+ flex: 1,
+ },
+ {
+ xtype: 'button',
+ text: gettext('Download'),
+ iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ width: 110,
+ handler: function(b) {
+ let win = this.up('window');
+
+ let pveID = PVE.ClusterName || window.location.hostname;
+ let name = `pve-${pveID}-storage-${win.sid}.enc`;
+
+ let hiddenElement = document.createElement('a');
+ hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key);
+ hiddenElement.target = '_blank';
+ hiddenElement.download = name;
+ hiddenElement.click();
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'component',
+ html: '3. ' + gettext('Print as paperkey, laminated and placed in secure vault.'),
+ flex: 1,
+ },
+ {
+ xtype: 'button',
+ text: gettext('Print Key'),
+ iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ width: 110,
+ handler: function(b) {
+ let win = this.up('window');
+ win.paperkey(win.key);
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ xtype: 'component',
+ border: false,
+ padding: '10 10 10 10',
+ userCls: 'pmx-hint',
+ html: gettext('Please save the encryption key - losing it will render any backup created with it unusable'),
+ },
+ ],
+ buttons: [
+ {
+ text: gettext('Close'),
+ handler: function(b) {
+ let win = this.up('window');
+ win.close();
+ },
+ },
+ ],
+ paperkey: function(keyString) {
+ let me = this;
+
+ const key = JSON.parse(keyString);
+
+ const qrwidth = 500;
+ let qrdiv = document.createElement('div');
+ let qrcode = new QRCode(qrdiv, {
+ width: qrwidth,
+ height: qrwidth,
+ correctLevel: QRCode.CorrectLevel.H,
+ });
+ qrcode.makeCode(keyString);
+
+ let shortKeyFP = '';
+ if (key.fingerprint) {
+ shortKeyFP = PVE.Utils.render_pbs_fingerprint(key.fingerprint);
+ }
+
+ let printFrame = document.createElement("iframe");
+ Object.assign(printFrame.style, {
+ position: "fixed",
+ right: "0",
+ bottom: "0",
+ width: "0",
+ height: "0",
+ border: "0",
+ });
+ const prettifiedKey = JSON.stringify(key, null, 2);
+ const keyQrBase64 = qrdiv.children[0].toDataURL("image/png");
+ const html = `
+ Encryption Key - Storage '${me.sid}' (${shortKeyFP})
+
+-----BEGIN PROXMOX BACKUP KEY-----
+${prettifiedKey}
+-----END PROXMOX BACKUP KEY-----
+
+ `;
+
+ printFrame.src = "data:text/html;base64," + btoa(html);
+ document.body.appendChild(printFrame);
+ me.on('destroy', () => document.body.removeChild(printFrame));
+ },
+});
+
+Ext.define('PVE.panel.PBSEncryptionKeyTab', {
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pvePBSEncryptionKeyTab',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'storage_pbs_encryption',
+
+ onGetValues: function(form) {
+ let values = {};
+ if (form.cryptMode === 'upload') {
+ values['encryption-key'] = form['crypt-key-upload'];
+ } else if (form.cryptMode === 'autogenerate') {
+ values['encryption-key'] = 'autogen';
+ } else if (form.cryptMode === 'none') {
+ if (!this.isCreate) {
+ values.delete = ['encryption-key'];
+ }
+ }
+ return values;
+ },
+
+ setValues: function(values) {
+ let me = this;
+ let vm = me.getViewModel();
+
+ let cryptKeyInfo = values['encryption-key'];
+ if (cryptKeyInfo) {
+ let icon = ' ';
+ if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) { // new style fingerprint
+ let shortKeyFP = PVE.Utils.render_pbs_fingerprint(cryptKeyInfo);
+ values['crypt-key-fp'] = icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
+ } else {
+ // old key without FP
+ values['crypt-key-fp'] = icon + gettext('Active');
+ }
+ } else {
+ values['crypt-key-fp'] = gettext('None');
+ let cryptModeNone = me.down('radiofield[inputValue=none]');
+ cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
+ cryptModeNone.setValue(true);
+ }
+ vm.set('keepCryptVisible', !!cryptKeyInfo);
+ vm.set('allowEdit', !cryptKeyInfo);
+
+ me.callParent([values]);
+ },
+
+ viewModel: {
+ data: {
+ allowEdit: true,
+ keepCryptVisible: false,
+ },
+ formulas: {
+ showDangerousHint: get => {
+ let allowEdit = get('allowEdit');
+ return get('keepCryptVisible') && allowEdit;
+ },
+ },
+ },
+
+ items: [
+ {
+ xtype: 'displayfield',
+ name: 'crypt-key-fp',
+ fieldLabel: gettext('Encryption Key'),
+ padding: '2 0',
+ },
+ {
+ xtype: 'checkbox',
+ name: 'crypt-allow-edit',
+ boxLabel: gettext('Edit existing encryption key (dangerous!)'),
+ hidden: true,
+ submitValue: false,
+ isDirty: () => false,
+ bind: {
+ hidden: '{!keepCryptVisible}',
+ value: '{allowEdit}',
+ },
+ },
+ {
+ xtype: 'radiofield',
+ name: 'cryptMode',
+ inputValue: 'keep',
+ boxLabel: gettext('Keep encryption key'),
+ padding: '0 0 0 25',
+ cbind: {
+ hidden: '{isCreate}',
+ checked: '{!isCreate}',
+ },
+ bind: {
+ hidden: '{!keepCryptVisible}',
+ disabled: '{!allowEdit}',
+ },
+ },
+ {
+ xtype: 'radiofield',
+ name: 'cryptMode',
+ inputValue: 'none',
+ checked: true,
+ padding: '0 0 0 25',
+ cbind: {
+ disabled: '{!isCreate}',
+ checked: '{isCreate}',
+ boxLabel: get => get('isCreate')
+ ? gettext('Do not encrypt backups')
+ : gettext('Delete existing encryption key'),
+ },
+ bind: {
+ disabled: '{!allowEdit}',
+ },
+ },
+ {
+ xtype: 'radiofield',
+ name: 'cryptMode',
+ inputValue: 'autogenerate',
+ boxLabel: gettext('Auto-generate a client encryption key'),
+ padding: '0 0 0 25',
+ cbind: {
+ disabled: '{!isCreate}',
+ },
+ bind: {
+ disabled: '{!allowEdit}',
+ },
+ },
+ {
+ xtype: 'radiofield',
+ name: 'cryptMode',
+ inputValue: 'upload',
+ boxLabel: gettext('Upload an existing client encryption key'),
+ padding: '0 0 0 25',
+ cbind: {
+ disabled: '{!isCreate}',
+ },
+ bind: {
+ disabled: '{!allowEdit}',
+ },
+ listeners: {
+ change: function(f, value) {
+ let panel = this.up('inputpanel');
+ if (!panel.rendered) {
+ return;
+ }
+ let uploadKeyField = panel.down('field[name=crypt-key-upload]');
+ uploadKeyField.setDisabled(!value);
+ uploadKeyField.setHidden(!value);
+
+ let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
+ uploadKeyButton.setDisabled(!value);
+ uploadKeyButton.setHidden(!value);
+
+ if (value) {
+ uploadKeyField.validate();
+ } else {
+ uploadKeyField.reset();
+ }
+ },
+ },
+ },
+ {
+ xtype: 'fieldcontainer',
+ layout: 'hbox',
+ items: [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'crypt-key-upload',
+ fieldLabel: gettext('Key'),
+ value: '',
+ disabled: true,
+ hidden: true,
+ allowBlank: false,
+ labelAlign: 'right',
+ flex: 1,
+ emptyText: gettext('You can drag-and-drop a key file here.'),
+ validator: function(value) {
+ if (value.length) {
+ let key;
+ try {
+ key = JSON.parse(value);
+ } catch (e) {
+ return "Failed to parse key - " + e;
+ }
+ if (key.data === undefined) {
+ return "Does not seems like a valid Proxmox Backup key!";
+ }
+ }
+ return true;
+ },
+ afterRender: function() {
+ if (!window.FileReader) {
+ // No FileReader support in this browser
+ return;
+ }
+ let cancel = function(ev) {
+ ev = ev.event;
+ if (ev.preventDefault) {
+ ev.preventDefault();
+ }
+ };
+ this.inputEl.on('dragover', cancel);
+ this.inputEl.on('dragenter', cancel);
+ this.inputEl.on('drop', ev => {
+ cancel(ev);
+ let files = ev.event.dataTransfer.files;
+ PVE.Utils.loadTextFromFile(files[0], v => this.setValue(v));
+ });
+ },
+ },
+ {
+ xtype: 'filebutton',
+ name: 'crypt-upload-button',
+ iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
+ cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+ margin: '0 0 0 4',
+ disabled: true,
+ hidden: true,
+ listeners: {
+ change: function(btn, e, value) {
+ let ev = e.event;
+ let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
+ PVE.Utils.loadTextFromFile(ev.target.files[0], v => field.setValue(v));
+ btn.reset();
+ },
+ },
+ },
+ ],
+ },
+ {
+ xtype: 'component',
+ border: false,
+ padding: '5 2',
+ userCls: 'pmx-hint',
+ html: // `${gettext('Warning')}: ` +
+ ` ` +
+ gettext('Deleting or replacing the encryption key will break restoring backups created with it!'),
+ hidden: true,
+ bind: {
+ hidden: '{!showDangerousHint}',
+ },
+ },
+ ],
+});
+
+Ext.define('PVE.storage.PBSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ onlineHelp: 'storage_pbs',
+
+ apiCallDone: function(success, response, options) {
+ let res = response.result.data;
+ if (!(res && res.config && res.config['encryption-key'])) {
+ return;
+ }
+ let key = res.config['encryption-key'];
+ Ext.create('PVE.Storage.PBSKeyShow', {
+ autoShow: true,
+ sid: res.storage,
+ key: key,
+ });
+ },
+
+ isPBS: true, // HACK
+
+ extraTabs: [
+ {
+ xtype: 'pvePBSEncryptionKeyTab',
+ title: gettext('Encryption'),
+ },
+ ],
+
+ setValues: function(values) {
+ let me = this;
+
+ let server = values.server;
+ if (values.port !== undefined) {
+ if (Proxmox.Utils.IP6_match.test(server)) {
+ server = `[${server}]`;
+ }
+ server += `:${values.port}`;
+ }
+ values.hostport = server;
+
+ return me.callParent([values]);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
+ fieldLabel: gettext('Server'),
+ allowBlank: false,
+ name: 'hostport',
+ submitValue: false,
+ vtype: 'HostPort',
+ listeners: {
+ change: function(field, newvalue) {
+ let server = newvalue;
+ let port;
+
+ let match = Proxmox.Utils.HostPort_match.exec(newvalue);
+ if (match === null) {
+ match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue);
+ if (match === null) {
+ match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue);
+ }
+ }
+
+ if (match !== null) {
+ server = match[1];
+ if (match[2] !== undefined) {
+ port = match[2];
+ }
+ }
+
+ field.up('inputpanel').down('field[name=server]').setValue(server);
+ field.up('inputpanel').down('field[name=port]').setValue(port);
+ },
+ },
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ hidden: true,
+ name: 'server',
+ submitValue: me.isCreate, // it is fixed
+ },
+ {
+ xtype: 'proxmoxtextfield',
+ hidden: true,
+ deleteEmpty: !me.isCreate,
+ name: 'port',
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ value: '',
+ emptyText: gettext('Example') + ': admin@pbs',
+ fieldLabel: gettext('Username'),
+ regex: /\S+@\w+/,
+ regexText: gettext('Example') + ': admin@pbs',
+ allowBlank: false,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ inputType: 'password',
+ name: 'password',
+ value: me.isCreate ? '' : '********',
+ emptyText: me.isCreate ? gettext('None') : '',
+ fieldLabel: gettext('Password'),
+ allowBlank: false,
+ },
+ ];
+
+ me.column2 = [
+ {
+ xtype: 'displayfield',
+ name: 'content',
+ value: 'backup',
+ submitValue: true,
+ fieldLabel: gettext('Content'),
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'datastore',
+ value: '',
+ fieldLabel: 'Datastore',
+ allowBlank: false,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'namespace',
+ value: '',
+ emptyText: gettext('Root'),
+ fieldLabel: gettext('Namespace'),
+ allowBlank: true,
+ },
+ ];
+
+ me.columnB = [
+ {
+ xtype: 'proxmoxtextfield',
+ name: 'fingerprint',
+ value: me.isCreate ? null : undefined,
+ fieldLabel: gettext('Fingerprint'),
+ emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
+ regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
+ regexText: gettext('Example') + ': AB:CD:EF:...',
+ deleteEmpty: !me.isCreate,
+ allowBlank: true,
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.Ceph.Model', {
+ extend: 'Ext.app.ViewModel',
+ alias: 'viewmodel.cephstorage',
+
+ data: {
+ pveceph: true,
+ pvecephPossible: true,
+ namespacePresent: false,
+ },
+});
+
+Ext.define('PVE.storage.Ceph.Controller', {
+ extend: 'PVE.controller.StorageEdit',
+ alias: 'controller.cephstorage',
+
+ control: {
+ '#': {
+ afterrender: 'queryMonitors',
+ },
+ 'textfield[name=username]': {
+ disable: 'resetField',
+ },
+ 'displayfield[name=monhost]': {
+ enable: 'queryMonitors',
+ },
+ 'textfield[name=monhost]': {
+ disable: 'resetField',
+ enable: 'resetField',
+ },
+ 'textfield[name=namespace]': {
+ change: 'updateNamespaceHint',
+ },
+ },
+ resetField: function(field) {
+ field.reset();
+ },
+ updateNamespaceHint: function(field, newVal, oldVal) {
+ this.getViewModel().set('namespacePresent', newVal);
+ },
+ queryMonitors: function(field, newVal, oldVal) {
+ // we get called with two signatures, the above one for a field
+ // change event and the afterrender from the view, this check only
+ // can be true for the field change one and omit the API request if
+ // pveceph got unchecked - as it's not needed there.
+ if (field && !newVal && oldVal) {
+ return;
+ }
+ var view = this.getView();
+ var vm = this.getViewModel();
+ if (!(view.isCreate || vm.get('pveceph'))) {
+ return; // only query on create or if editing a pveceph store
+ }
+
+ var monhostField = this.lookupReference('monhost');
+
+ Proxmox.Utils.API2Request({
+ url: '/api2/json/nodes/localhost/ceph/mon',
+ method: 'GET',
+ scope: this,
+ callback: function(options, success, response) {
+ var data = response.result.data;
+ if (response.status === 200) {
+ if (data.length > 0) {
+ 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();
+ }
+ if (values.namespace) {
+ this.getViewModel().set('namespacePresent', true);
+ }
+ this.callParent([values]);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+ me.type = 'rbd';
+
+ me.column1 = [];
+
+ if (me.isCreate) {
+ me.column1.push({
+ xtype: 'pveCephPoolSelector',
+ nodename: me.nodename,
+ name: 'pool',
+ bind: {
+ disabled: '{!pveceph}',
+ submitValue: '{pveceph}',
+ hidden: '{!pveceph}',
+ },
+ fieldLabel: gettext('Pool'),
+ allowBlank: false,
+ }, {
+ xtype: 'textfield',
+ name: 'pool',
+ value: 'rbd',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}',
+ },
+ fieldLabel: gettext('Pool'),
+ allowBlank: false,
+ });
+ } else {
+ me.column1.push({
+ xtype: 'displayfield',
+ nodename: me.nodename,
+ name: 'pool',
+ fieldLabel: gettext('Pool'),
+ allowBlank: false,
+ });
+ }
+
+ me.column1.push(
+ {
+ xtype: 'textfield',
+ name: 'monhost',
+ vtype: 'HostList',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ hidden: '{pveceph}',
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)',
+ allowBlank: false,
+ },
+ {
+ xtype: 'displayfield',
+ reference: 'monhost',
+ bind: {
+ disabled: '{!pveceph}',
+ hidden: '{!pveceph}',
+ },
+ value: '',
+ fieldLabel: 'Monitor(s)',
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'username',
+ bind: {
+ disabled: '{pveceph}',
+ submitValue: '{!pveceph}',
+ },
+ value: 'admin',
+ fieldLabel: gettext('User name'),
+ allowBlank: true,
+ },
+ );
+
+ me.column2 = [
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images'],
+ multiSelect: true,
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'krbd',
+ uncheckedValue: 0,
+ fieldLabel: 'KRBD',
+ },
+ ];
+
+ me.columnB = [
+ {
+ xtype: me.isCreate ? 'textarea' : 'displayfield',
+ name: 'keyring',
+ fieldLabel: 'Keyring',
+ value: me.isCreate ? '' : '***********',
+ allowBlank: false,
+ bind: {
+ hidden: '{pveceph}',
+ disabled: '{pveceph}',
+ },
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'pveceph',
+ reference: 'pvecephRef',
+ bind: {
+ disabled: '{!pvecephPossible}',
+ value: '{pveceph}',
+ },
+ checked: true,
+ uncheckedValue: 0,
+ submitValue: false,
+ hidden: !me.isCreate,
+ boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool'),
+ },
+ ];
+
+ me.advancedColumn1 = [
+ {
+ xtype: 'pmxDisplayEditField',
+ editable: me.isCreate,
+ name: 'namespace',
+ value: '',
+ fieldLabel: gettext('Namespace'),
+ allowBlank: true,
+ },
+ ];
+ me.advancedColumn2 = [
+ {
+ xtype: 'displayfield',
+ name: 'namespace-hint',
+ userCls: 'pmx-hint',
+ value: gettext('RBD namespaces must be created manually!'),
+ bind: {
+ hidden: '{!namespacePresent}',
+ },
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.StatusView', {
+ extend: 'Proxmox.panel.StatusView',
+ alias: 'widget.pveStorageStatusView',
+
+ height: 230,
+ title: gettext('Status'),
+
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
+
+ defaults: {
+ xtype: 'pmxInfoWidget',
+ padding: '0 30 5 30',
+ },
+ items: [
+ {
+ xtype: 'box',
+ height: 30,
+ },
+ {
+ itemId: 'enabled',
+ title: gettext('Enabled'),
+ printBar: false,
+ textField: 'disabled',
+ renderer: Proxmox.Utils.format_neg_boolean,
+ },
+ {
+ itemId: 'active',
+ title: gettext('Active'),
+ printBar: false,
+ textField: 'active',
+ renderer: Proxmox.Utils.format_boolean,
+ },
+ {
+ itemId: 'content',
+ title: gettext('Content'),
+ printBar: false,
+ textField: 'content',
+ renderer: PVE.Utils.format_content_types,
+ },
+ {
+ itemId: 'type',
+ title: gettext('Type'),
+ printBar: false,
+ textField: 'type',
+ renderer: PVE.Utils.format_storage_type,
+ },
+ {
+ xtype: 'box',
+ height: 10,
+ },
+ {
+ itemId: 'usage',
+ title: gettext('Usage'),
+ valueField: 'used',
+ maxField: 'total',
+ renderer: (val, max) => {
+ if (max === undefined) {
+ return val;
+ }
+ return Proxmox.Utils.render_size_usage(val, max, true);
+ },
+ },
+ ],
+
+ updateTitle: function() {
+ // nothing
+ },
+});
+Ext.define('PVE.storage.Summary', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.pveStorageSummary',
+ scrollable: true,
+ bodyPadding: 5,
+ tbar: [
+ '->',
+ {
+ xtype: 'proxmoxRRDTypeSelector',
+ },
+ ],
+ layout: {
+ type: 'column',
+ },
+ defaults: {
+ padding: 5,
+ columnWidth: 1,
+ },
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var storage = me.pveSelNode.data.storage;
+ if (!storage) {
+ throw "no storage ID specified";
+ }
+
+ var rstore = Ext.create('Proxmox.data.ObjectStore', {
+ url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status",
+ interval: 1000,
+ });
+
+ var rrdstore = Ext.create('Proxmox.data.RRDStore', {
+ rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata",
+ model: 'pve-rrd-storage',
+ });
+
+ Ext.apply(me, {
+ items: [
+ {
+ xtype: 'pveStorageStatusView',
+ pveSelNode: me.pveSelNode,
+ rstore: rstore,
+ },
+ {
+ xtype: 'proxmoxRRDChart',
+ title: gettext('Usage'),
+ fields: ['total', 'used'],
+ fieldTitles: ['Total Size', 'Used Size'],
+ store: rrdstore,
+ },
+ ],
+ listeners: {
+ activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); },
+ destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); },
+ },
+ });
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.grid.TemplateSelector', {
+ extend: 'Ext.grid.GridPanel',
+
+ alias: 'widget.pveTemplateSelector',
+
+ stateful: true,
+ stateId: 'grid-template-selector',
+ viewConfig: {
+ trackOver: false,
+ },
+ initComponent: function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var baseurl = "/nodes/" + me.nodename + "/aplinfo";
+ var store = new Ext.data.Store({
+ model: 'pve-aplinfo',
+ groupField: 'section',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json' + baseurl,
+ },
+ });
+
+ var sm = Ext.create('Ext.selection.RowModel', {});
+
+ var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
+ groupHeaderTpl: '{[ "Section: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
+ });
+
+ var reload = function() {
+ store.load();
+ };
+
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ Ext.apply(me, {
+ store: store,
+ selModel: sm,
+ tbar: [
+ '->',
+ gettext('Search'),
+ {
+ xtype: 'textfield',
+ width: 200,
+ enableKeyEvents: true,
+ listeners: {
+ buffer: 500,
+ keyup: function(field) {
+ var value = field.getValue().toLowerCase();
+ store.clearFilter(true);
+ store.filterBy(function(rec) {
+ return rec.data.package.toLowerCase().indexOf(value) !== -1 ||
+ rec.data.headline.toLowerCase().indexOf(value) !== -1;
+ });
+ },
+ },
+ },
+ ],
+ features: [groupingFeature],
+ columns: [
+ {
+ header: gettext('Type'),
+ width: 80,
+ dataIndex: 'type',
+ },
+ {
+ header: gettext('Package'),
+ flex: 1,
+ dataIndex: 'package',
+ },
+ {
+ header: gettext('Version'),
+ width: 80,
+ dataIndex: 'version',
+ },
+ {
+ header: gettext('Description'),
+ flex: 1.5,
+ renderer: Ext.String.htmlEncode,
+ dataIndex: 'headline',
+ },
+ ],
+ listeners: {
+ afterRender: reload,
+ },
+ });
+
+ me.callParent();
+ },
+
+}, function() {
+ Ext.define('pve-aplinfo', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'template', 'type', 'package', 'version', 'headline', 'infopage',
+ 'description', 'os', 'section',
+ ],
+ idProperty: 'template',
+ });
+});
+
+Ext.define('PVE.storage.TemplateDownload', {
+ extend: 'Ext.window.Window',
+ alias: 'widget.pveTemplateDownload',
+
+ modal: true,
+ title: gettext('Templates'),
+ layout: 'fit',
+ width: 900,
+ height: 600,
+ initComponent: function() {
+ var me = this;
+
+ var grid = Ext.create('PVE.grid.TemplateSelector', {
+ border: false,
+ scrollable: true,
+ nodename: me.nodename,
+ });
+
+ var sm = grid.getSelectionModel();
+
+ var submitBtn = Ext.create('Proxmox.button.Button', {
+ text: gettext('Download'),
+ disabled: true,
+ selModel: sm,
+ handler: function(button, event, rec) {
+ Proxmox.Utils.API2Request({
+ url: '/nodes/' + me.nodename + '/aplinfo',
+ params: {
+ storage: me.storage,
+ template: rec.data.template,
+ },
+ method: 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ var upid = response.result.data;
+
+ Ext.create('Proxmox.window.TaskViewer', {
+ upid: upid,
+ listeners: {
+ destroy: me.reloadGrid,
+ },
+ }).show();
+
+ me.close();
+ },
+ });
+ },
+ });
+
+ Ext.apply(me, {
+ items: grid,
+ buttons: [submitBtn],
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.TemplateView', {
+ extend: 'PVE.storage.ContentView',
+
+ alias: 'widget.pveStorageTemplateView',
+
+ initComponent: function() {
+ var me = this;
+
+ var nodename = me.nodename = me.pveSelNode.data.node;
+ if (!nodename) {
+ throw "no node name specified";
+ }
+
+ var storage = me.storage = me.pveSelNode.data.storage;
+ if (!storage) {
+ throw "no storage ID specified";
+ }
+
+ me.content = 'vztmpl';
+
+ var reload = function() {
+ me.store.load();
+ };
+
+ var templateButton = Ext.create('Proxmox.button.Button', {
+ itemId: 'tmpl-btn',
+ text: gettext('Templates'),
+ handler: function() {
+ var win = Ext.create('PVE.storage.TemplateDownload', {
+ nodename: nodename,
+ storage: storage,
+ reloadGrid: reload,
+ });
+ win.show();
+ },
+ });
+
+ me.tbar = [templateButton];
+ me.useUploadButton = true;
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.ZFSInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+
+ viewModel: {
+ parent: null,
+ data: {
+ isLIO: false,
+ isComstar: true,
+ hasWriteCacheOption: true,
+ },
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+ control: {
+ 'field[name=iscsiprovider]': {
+ change: 'changeISCSIProvider',
+ },
+ },
+ changeISCSIProvider: function(f, newVal, oldVal) {
+ var vm = this.getViewModel();
+ vm.set('isLIO', newVal === 'LIO');
+ vm.set('isComstar', newVal === 'comstar');
+ vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
+ },
+ },
+
+ onGetValues: function(values) {
+ var me = this;
+
+ if (me.isCreate) {
+ values.content = 'images';
+ }
+
+ values.nowritecache = values.writecache ? 0 : 1;
+ delete values.writecache;
+
+ return me.callParent([values]);
+ },
+
+ setValues: function(values) {
+ values.writecache = values.nowritecache ? 0 : 1;
+ this.callParent([values]);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ me.column1 = [
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'portal',
+ value: '',
+ fieldLabel: gettext('Portal'),
+ allowBlank: false,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'pool',
+ value: '',
+ fieldLabel: gettext('Pool'),
+ allowBlank: false,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'blocksize',
+ value: '4k',
+ fieldLabel: gettext('Block Size'),
+ allowBlank: false,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'target',
+ value: '',
+ fieldLabel: gettext('Target'),
+ allowBlank: false,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'comstar_tg',
+ value: '',
+ fieldLabel: gettext('Target group'),
+ bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ allowBlank: true,
+ },
+ ];
+
+ me.column2 = [
+ {
+ xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield',
+ name: 'iscsiprovider',
+ value: 'comstar',
+ fieldLabel: gettext('iSCSI Provider'),
+ allowBlank: false,
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'sparse',
+ checked: false,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Thin provision'),
+ },
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'writecache',
+ checked: true,
+ bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' },
+ uncheckedValue: 0,
+ fieldLabel: gettext('Write cache'),
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'comstar_hg',
+ value: '',
+ bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
+ fieldLabel: gettext('Host group'),
+ allowBlank: true,
+ },
+ {
+ xtype: me.isCreate ? 'textfield' : 'displayfield',
+ name: 'lio_tpg',
+ value: '',
+ bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
+ allowBlank: false,
+ fieldLabel: gettext('Target portal group'),
+ },
+ ];
+
+ me.callParent();
+ },
+});
+Ext.define('PVE.storage.ZFSPoolSelector', {
+ extend: 'PVE.form.ComboBoxSetStoreNode',
+ alias: 'widget.pveZFSPoolSelector',
+ valueField: 'pool',
+ displayField: 'pool',
+ queryMode: 'local',
+ editable: false,
+ allowBlank: false,
+
+ listConfig: {
+ columns: [
+ {
+ dataIndex: 'pool',
+ flex: 1,
+ },
+ ],
+ emptyText: PVE.Utils.renderNotFound(gettext('ZFS Pool')),
+ },
+
+ config: {
+ apiSuffix: '/scan/zfs',
+ },
+
+ showNodeSelector: true,
+
+ setNodeName: function(value) {
+ let me = this;
+ me.callParent([value]);
+ me.getStore().load();
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ me.nodename = 'localhost';
+ }
+
+ let store = Ext.create('Ext.data.Store', {
+ autoLoad: {}, // true,
+ fields: ['pool', 'size', 'free'],
+ proxy: {
+ type: 'proxmox',
+ url: `${me.apiBaseUrl}${me.nodename}${me.apiSuffix}`,
+ },
+ });
+ store.sort('pool', 'ASC');
+
+ Ext.apply(me, {
+ store: store,
+ });
+
+ me.callParent();
+ },
+});
+
+Ext.define('PVE.storage.ZFSPoolInputPanel', {
+ extend: 'PVE.panel.StorageBase',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onlineHelp: 'storage_zfspool',
+
+ column1: [
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: '{isCreate}',
+ },
+
+ name: 'pool',
+ fieldLabel: gettext('ZFS Pool'),
+ allowBlank: false,
+
+ editConfig: {
+ xtype: 'pveZFSPoolSelector',
+ reference: 'zfsPoolSelector',
+ listeners: {
+ nodechanged: function(value) {
+ this.up('inputpanel').lookup('storageNodeRestriction').setValue(value);
+ },
+ },
+ },
+ },
+ {
+ xtype: 'pveContentTypeSelector',
+ cts: ['images', 'rootdir'],
+ fieldLabel: gettext('Content'),
+ name: 'content',
+ value: ['images', 'rootdir'],
+ multiSelect: true,
+ allowBlank: false,
+ },
+ ],
+
+ column2: [
+ {
+ xtype: 'proxmoxcheckbox',
+ name: 'sparse',
+ checked: false,
+ uncheckedValue: 0,
+ fieldLabel: gettext('Thin provision'),
+ },
+ {
+ xtype: 'textfield',
+ name: 'blocksize',
+ emptyText: '8k',
+ fieldLabel: gettext('Block Size'),
+ allowBlank: true,
+ },
+ ],
+});
+/*
+ * 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) {
+ // override me
+ },
+
+ // private
+ updateLoginData: function(loginData) {
+ let me = this;
+ me.loginData = loginData;
+ Proxmox.Utils.setAuthData(loginData);
+
+ let rt = me.down('pveResourceTree');
+ rt.setDatacenterText(loginData.clustername);
+ PVE.ClusterName = loginData.clustername;
+
+ if (loginData.cap) {
+ Ext.state.Manager.set('GuiCap', loginData.cap);
+ }
+ me.response401count = 0;
+
+ me.onLogin(loginData);
+ },
+
+ // private
+ showLogin: function() {
+ let me = this;
+
+ Proxmox.Utils.authClear();
+ Ext.state.Manager.clear('GuiCap');
+ Proxmox.UserName = null;
+ me.loginData = null;
+
+ if (!me.login) {
+ me.login = Ext.create('PVE.window.LoginWindow', {
+ handler: function(data) {
+ me.login = null;
+ me.updateLoginData(data);
+ Proxmox.Utils.checked_command(Ext.emptyFn); // display subscription status
+ },
+ });
+ }
+ me.onLogin(null);
+ me.login.show();
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ Ext.tip.QuickTipManager.init();
+
+ // fixme: what about other errors
+ Ext.Ajax.on('requestexception', function(conn, response, options) {
+ if ((response.status === 401 || response.status === '401') && !PVE.Utils.silenceAuthFailures) { // auth failure
+ // don't immediately show as logged out to cope better with some big
+ // upgrades, which may temporarily produce a false positive 401 err
+ me.response401count++;
+ if (me.response401count > 5) {
+ me.showLogin();
+ }
+ }
+ });
+
+ me.callParent();
+
+ if (!Proxmox.Utils.authOK()) {
+ me.showLogin();
+ } else if (me.loginData) {
+ me.onLogin(me.loginData);
+ }
+
+ Ext.TaskManager.start({
+ run: function() {
+ let ticket = Proxmox.Utils.authOK();
+ if (!ticket || !Proxmox.UserName) {
+ return;
+ }
+
+ Ext.Ajax.request({
+ params: {
+ username: Proxmox.UserName,
+ password: ticket,
+ },
+ url: '/api2/json/access/ticket',
+ method: 'POST',
+ success: function(response, opts) {
+ let obj = Ext.decode(response.responseText);
+ me.updateLoginData(obj.data);
+ },
+ });
+ },
+ interval: 15 * 60 * 1000,
+ });
+ },
+});
+
+Ext.define('PVE.StdWorkspace', {
+ extend: 'PVE.Workspace',
+
+ alias: ['widget.pveStdWorkspace'],
+
+ // private
+ setContent: function(comp) {
+ let me = this;
+
+ let view = me.child('#content');
+ let layout = view.getLayout();
+ let current = layout.getActiveItem();
+
+ if (comp) {
+ Proxmox.Utils.setErrorMask(view, false);
+ comp.border = false;
+ view.add(comp);
+ if (current !== null && layout.getNext()) {
+ layout.next();
+ let task = Ext.create('Ext.util.DelayedTask', function() {
+ view.remove(current);
+ });
+ task.delay(10);
+ }
+ } else {
+ view.removeAll(); // helper for cleaning the content when logging out
+ }
+ },
+
+ selectById: function(nodeid) {
+ let me = this;
+ me.down('pveResourceTree').selectById(nodeid);
+ },
+
+ onLogin: function(loginData) {
+ let me = this;
+
+ me.updateUserInfo();
+
+ if (loginData) {
+ PVE.data.ResourceStore.startUpdate();
+
+ Proxmox.Utils.API2Request({
+ url: '/version',
+ method: 'GET',
+ success: function(response) {
+ PVE.VersionInfo = response.result.data;
+ me.updateVersionInfo();
+ },
+ });
+
+ PVE.UIOptions.update();
+
+ Proxmox.Utils.API2Request({
+ url: '/cluster/sdn',
+ method: 'GET',
+ success: function(response) {
+ PVE.SDNInfo = response.result.data;
+ },
+ failure: function(response) {
+ PVE.SDNInfo = null;
+ let ui = Ext.ComponentQuery.query('treelistitem[text="SDN"]')[0];
+ if (ui) {
+ ui.addCls('x-hidden-display');
+ }
+ },
+ });
+
+ Proxmox.Utils.API2Request({
+ url: '/access/domains',
+ method: 'GET',
+ success: function(response) {
+ let [_username, realm] = Proxmox.Utils.parse_userid(Proxmox.UserName);
+ response.result.data.forEach((domain) => {
+ if (domain.realm === realm) {
+ let schema = PVE.Utils.authSchema[domain.type];
+ if (schema) {
+ me.query('#tfaitem')[0].setHidden(!schema.tfa);
+ me.query('#passworditem')[0].setHidden(!schema.pwchange);
+ }
+ }
+ });
+ },
+ });
+ }
+ },
+
+ updateUserInfo: function() {
+ let me = this;
+ let ui = me.query('#userinfo')[0];
+ ui.setText(Ext.String.htmlEncode(Proxmox.UserName || ''));
+ ui.updateLayout();
+ },
+
+ updateVersionInfo: function() {
+ let me = this;
+
+ let ui = me.query('#versioninfo')[0];
+
+ if (PVE.VersionInfo) {
+ let version = PVE.VersionInfo.version;
+ ui.update('Virtual Environment ' + version);
+ } else {
+ ui.update('Virtual Environment');
+ }
+ ui.updateLayout();
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ Ext.History.init();
+
+ let appState = Ext.create('PVE.StateProvider');
+ Ext.state.Manager.setProvider(appState);
+
+ let selview = Ext.create('PVE.form.ViewSelector', {
+ flex: 1,
+ padding: '0 5 0 0',
+ });
+
+ let rtree = Ext.createWidget('pveResourceTree', {
+ viewFilter: selview.getViewFilter(),
+ flex: 1,
+ selModel: {
+ selType: 'treemodel',
+ listeners: {
+ selectionchange: function(sm, selected) {
+ if (selected.length <= 0) {
+ return;
+ }
+ let treeNode = selected[0];
+ let treeTypeToClass = {
+ root: 'PVE.dc.Config',
+ node: 'PVE.node.Config',
+ qemu: 'PVE.qemu.Config',
+ lxc: 'pveLXCConfig',
+ storage: 'PVE.storage.Browser',
+ sdn: 'PVE.sdn.Browser',
+ pool: 'pvePoolConfig',
+ };
+ PVE.curSelectedNode = treeNode;
+ me.setContent({
+ xtype: treeTypeToClass[treeNode.data.type || 'root'] || 'pvePanelConfig',
+ showSearch: treeNode.data.id === 'root' || Ext.isDefined(treeNode.data.groupbyid),
+ pveSelNode: treeNode,
+ workspace: me,
+ viewFilter: selview.getViewFilter(),
+ });
+ },
+ },
+ },
+ });
+
+ selview.on('select', function(combo, records) {
+ if (records) {
+ let view = combo.getViewFilter();
+ rtree.setViewFilter(view);
+ }
+ });
+
+ let caps = appState.get('GuiCap');
+
+ let createVM = Ext.createWidget('button', {
+ pack: 'end',
+ margin: '3 5 0 0',
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-desktop',
+ text: gettext("Create VM"),
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ let wiz = Ext.create('PVE.qemu.CreateWizard', {});
+ wiz.show();
+ },
+ });
+
+ let createCT = Ext.createWidget('button', {
+ pack: 'end',
+ margin: '3 5 0 0',
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-cube',
+ text: gettext("Create CT"),
+ disabled: !caps.vms['VM.Allocate'],
+ handler: function() {
+ let wiz = Ext.create('PVE.lxc.CreateWizard', {});
+ wiz.show();
+ },
+ });
+
+ appState.on('statechange', function(sp, key, value) {
+ if (key === 'GuiCap' && value) {
+ caps = value;
+ createVM.setDisabled(!caps.vms['VM.Allocate']);
+ createCT.setDisabled(!caps.vms['VM.Allocate']);
+ }
+ });
+
+ Ext.apply(me, {
+ layout: { type: 'border' },
+ border: false,
+ items: [
+ {
+ region: 'north',
+ title: gettext('Header'), // for ARIA
+ header: false, // avoid rendering the title
+ layout: {
+ type: 'hbox',
+ align: 'middle',
+ },
+ baseCls: 'x-plain',
+ defaults: {
+ baseCls: 'x-plain',
+ },
+ border: false,
+ margin: '2 0 2 5',
+ items: [
+ {
+ xtype: 'proxmoxlogo',
+ },
+ {
+ minWidth: 150,
+ id: 'versioninfo',
+ html: 'Virtual Environment',
+ style: {
+ 'font-size': '14px',
+ 'line-height': '18px',
+ },
+ },
+ {
+ xtype: 'pveGlobalSearchField',
+ tree: rtree,
+ },
+ {
+ flex: 1,
+ },
+ {
+ xtype: 'proxmoxHelpButton',
+ hidden: false,
+ baseCls: 'x-btn',
+ iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ',
+ listenToGlobalEvent: false,
+ onlineHelp: 'pve_documentation_index',
+ text: gettext('Documentation'),
+ margin: '0 5 0 0',
+ },
+ createVM,
+ createCT,
+ {
+ pack: 'end',
+ margin: '0 5 0 0',
+ id: 'userinfo',
+ xtype: 'button',
+ baseCls: 'x-btn',
+ style: {
+ // proxmox dark grey p light grey as border
+ backgroundColor: '#464d4d',
+ borderColor: '#ABBABA',
+ },
+ iconCls: 'fa fa-user',
+ menu: [
+ {
+ iconCls: 'fa fa-gear',
+ text: gettext('My Settings'),
+ handler: function() {
+ var win = Ext.create('PVE.window.Settings');
+ win.show();
+ },
+ },
+ {
+ text: gettext('Password'),
+ itemId: 'passworditem',
+ iconCls: 'fa fa-fw fa-key',
+ handler: function() {
+ var win = Ext.create('Proxmox.window.PasswordEdit', {
+ userid: Proxmox.UserName,
+ });
+ win.show();
+ },
+ },
+ {
+ text: 'TFA',
+ itemId: 'tfaitem',
+ iconCls: 'fa fa-fw fa-lock',
+ handler: function(btn, event, rec) {
+ Ext.state.Manager.getProvider().set('dctab', { value: 'tfa' }, true);
+ me.selectById('root');
+ },
+ },
+ {
+ iconCls: 'fa fa-paint-brush',
+ text: gettext('Color Theme'),
+ handler: function() {
+ Ext.create('Proxmox.window.ThemeEditWindow')
+ .show();
+ },
+ },
+ {
+ iconCls: 'fa fa-language',
+ text: gettext('Language'),
+ handler: function() {
+ Ext.create('Proxmox.window.LanguageEditWindow')
+ .show();
+ },
+ },
+ '-',
+ {
+ iconCls: 'fa fa-fw fa-sign-out',
+ text: gettext("Logout"),
+ handler: function() {
+ PVE.data.ResourceStore.loadData([], false);
+ me.showLogin();
+ me.setContent(null);
+ var rt = me.down('pveResourceTree');
+ 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: 300,
+ items: [
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ padding: '0 0 5 0',
+ items: [
+ selview,
+ {
+ xtype: 'button',
+ cls: 'x-btn-default-toolbar-small',
+ iconCls: 'fa fa-fw fa-gear x-btn-icon-el-default-toolbar-small',
+ handler: () => {
+ Ext.create('PVE.window.TreeSettingsEdit', {
+ autoShow: true,
+ apiCallDone: () => PVE.UIOptions.fireUIConfigChanged(),
+ });
+ },
+ },
+ ],
+ },
+ rtree,
+ ],
+ listeners: {
+ resize: function(panel, width, height) {
+ var viewWidth = me.getSize().width;
+ if (width > viewWidth - 100) {
+ 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() {
+ let modalWindows = Ext.ComponentQuery.query('window[modal]');
+ if (modalWindows.length > 0) {
+ modalWindows.forEach(win => win.alignTo(me, 'c-c'));
+ }
+ });
+ },
+});
+