60995 lines
1.3 MiB
60995 lines
1.3 MiB
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_notifications" : {
|
|
"link" : "/pve-docs/chapter-notifications.html#chapter_notifications",
|
|
"title" : "Notifications"
|
|
},
|
|
"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"
|
|
},
|
|
"notification_matchers" : {
|
|
"link" : "/pve-docs/chapter-notifications.html#notification_matchers",
|
|
"subtitle" : "Notification Matchers",
|
|
"title" : "Notifications"
|
|
},
|
|
"notification_targets_gotify" : {
|
|
"link" : "/pve-docs/chapter-notifications.html#notification_targets_gotify",
|
|
"subtitle" : "Gotify",
|
|
"title" : "Notifications"
|
|
},
|
|
"notification_targets_sendmail" : {
|
|
"link" : "/pve-docs/chapter-notifications.html#notification_targets_sendmail",
|
|
"subtitle" : "Sendmail",
|
|
"title" : "Notifications"
|
|
},
|
|
"notification_targets_smtp" : {
|
|
"link" : "/pve-docs/chapter-notifications.html#notification_targets_smtp",
|
|
"subtitle" : "SMTP",
|
|
"title" : "Notifications"
|
|
},
|
|
"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" : "PVE 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_machine_type" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_machine_type",
|
|
"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"
|
|
},
|
|
"resource_mapping" : {
|
|
"link" : "/pve-docs/chapter-qm.html#resource_mapping",
|
|
"subtitle" : "Resource Mapping",
|
|
"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_account" : {
|
|
"link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certs_acme_account",
|
|
"subtitle" : "ACME Account",
|
|
"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-ad" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#user-realms-ad",
|
|
"subtitle" : "Microsoft Active Directory (AD)",
|
|
"title" : "User Management"
|
|
},
|
|
"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|e1000e?|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+)?|\.\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];
|
|
},
|
|
|
|
filterPropertyStringList: function(list, filterFn, defaultKey) {
|
|
return list.filter((entry) => filterFn(PVE.Parser.parsePropertyString(entry, defaultKey)));
|
|
},
|
|
},
|
|
});
|
|
/* 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 '
|
|
+'<a target="_blank" href="https://www.proxmox.com/en/proxmox-virtual-environment/pricing">'
|
|
+'www.proxmox.com</a> to get a list of available options.',
|
|
|
|
getClusterSubscriptionLevel: async function() {
|
|
let { result } = await Proxmox.Async.api2({ url: '/cluster/status' });
|
|
let levelMap = Object.fromEntries(
|
|
result.data.filter(v => v.type === 'node').map(v => [v.name, v.level]),
|
|
);
|
|
return levelMap;
|
|
},
|
|
|
|
kvm_ostypes: {
|
|
'Linux': [
|
|
{ desc: '6.x - 2.6 Kernel', val: 'l26' },
|
|
{ desc: '2.4 Kernel', val: 'l24' },
|
|
],
|
|
'Microsoft Windows': [
|
|
{ desc: '11/2022/2025', 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 "<i class='fa " + cls + "'></i> ";
|
|
},
|
|
|
|
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 '<div style="text-decoration: line-through;">'+ value +'</div>';
|
|
}
|
|
} 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 = `<i class="fa fa-fw fa-refresh warning"></i>`;
|
|
|
|
if (value === 'deleted') {
|
|
return '<span>' + icon + value + '</span>';
|
|
}
|
|
|
|
let tip = gettext('Pending Changes') + ': <br>';
|
|
|
|
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} <br>`;
|
|
}
|
|
}
|
|
return '<span data-qtip="' + tip + '">'+ icon + value + '</span>';
|
|
},
|
|
|
|
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 '<i class="fa fa-' + iconCls + '"></i> ' + 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 = `<i class="fa fa-fw fa-lock good"></i>`;
|
|
return `<span data-qtip="${tip}">${icon} ${gettext('Encrypted')}</span>`;
|
|
},
|
|
|
|
render_backup_verification: function(v, meta, record) {
|
|
let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${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 `<span data-qtip="${tip}"> ${i(iconCls, txt)} </span>`;
|
|
},
|
|
|
|
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 `<i class="fa fa-${iconCls}"></i> ${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 (key === 'freeze-fs-on-backup' && PVE.Parser.parseBoolean(value)) {
|
|
continue;
|
|
} 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,
|
|
},
|
|
esxi: {
|
|
name: 'ESXi',
|
|
ipanel: 'ESXIInputPanel',
|
|
faIcon: 'cloud-download',
|
|
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',
|
|
},
|
|
isis: {
|
|
name: 'isis',
|
|
ipanel: 'IsisInputPanel',
|
|
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) {
|
|
let data = record.data;
|
|
let result;
|
|
if (Ext.isNumber(data.channel) &&
|
|
Ext.isNumber(data.id) &&
|
|
Ext.isNumber(data.lun)) {
|
|
result = "CH " +
|
|
Ext.String.leftPad(data.channel, 2, '0') +
|
|
" ID " + data.id + " LUN " + data.lun;
|
|
} else if (data.content === 'import') {
|
|
result = data.volid.replace(/^.*?:/, '');
|
|
} else {
|
|
result = data.volid.replace(/^.*?:(.*?\/)?/, '');
|
|
}
|
|
return Ext.String.htmlEncode(result);
|
|
},
|
|
|
|
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 if (type === 'storage' && record.content.indexOf('import') !== -1) {
|
|
return 'fa fa-cloud-download';
|
|
} 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 = '<i class="fa-fw x-grid-icon-custom ' + cls + '"></i> ';
|
|
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 '<a target="_blank" href="' + value + '">' + value + '</a>';
|
|
}
|
|
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('<br>');
|
|
}
|
|
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]}<br>`;
|
|
}
|
|
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, AppleWebKit 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|AppleWebKit|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;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
lxc_mp_counts: {
|
|
mp: 256,
|
|
unused: 256,
|
|
},
|
|
|
|
forEachLxcMP: function(func, includeUnused) {
|
|
for (let i = 0; i < PVE.Utils.lxc_mp_counts.mp; i++) {
|
|
let cont = func('mp', i, `mp${i}`);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!includeUnused) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < PVE.Utils.lxc_mp_counts.unused; i++) {
|
|
let cont = func('unused', i, `unused${i}`);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
lxc_dev_count: 256,
|
|
|
|
forEachLxcDev: function(func) {
|
|
for (let i = 0; i < PVE.Utils.lxc_dev_count; i++) {
|
|
let cont = func(i, `dev${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 || Proxmox.NodeName;
|
|
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;
|
|
},
|
|
|
|
nextFreeLxcMP: function(type, config) {
|
|
for (let i = 0; i < PVE.Utils.lxc_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,
|
|
},
|
|
|
|
isStandaloneNode: function() {
|
|
return PVE.data.ResourceStore.getNodes().length < 2;
|
|
},
|
|
|
|
// main use case of this helper is the login window
|
|
getUiLanguage: function() {
|
|
let languageCookie = Ext.util.Cookies.get('PVELangCookie');
|
|
if (languageCookie === 'kr') {
|
|
// fix-up 'kr' being used for Korean by mistake FIXME: remove with PVE 9
|
|
let dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
|
|
languageCookie = 'ko';
|
|
Ext.util.Cookies.set('PVELangCookie', languageCookie, dt);
|
|
}
|
|
return languageCookie || Proxmox.defaultLang || 'en';
|
|
},
|
|
},
|
|
|
|
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('Bulk migrate 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')],
|
|
resize: ['VM/CT', gettext('Resize')],
|
|
spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'],
|
|
spiceshell: ['', gettext('Shell') + ' (Spice)'],
|
|
startall: ['', gettext('Bulk start VMs and Containers')],
|
|
stopall: ['', gettext('Bulk shutdown VMs and Containers')],
|
|
suspendall: ['', gettext('Suspend all VMs')],
|
|
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': '/mapping' },
|
|
{ 'value': '/mapping/notifications' },
|
|
{ 'value': '/mapping/pci' },
|
|
{ 'value': '/mapping/usb' },
|
|
{ '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' },
|
|
],
|
|
});
|
|
// This is a container intended to show a field on the first column and one on the second column.
|
|
// One can set a ratio for the field sizes.
|
|
//
|
|
// Works around a limitation of our input panel column1/2 handling that entries are not vertically
|
|
// aligned when one of them has wrapping text (like it happens sometimes with such longer
|
|
// descriptions)
|
|
Ext.define('PVE.container.TwoColumnContainer', {
|
|
extend: 'Ext.container.Container',
|
|
alias: 'widget.pveTwoColumnContainer',
|
|
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'stretch',
|
|
},
|
|
|
|
// The default ratio of the start widget. It an be an integer or a floating point number
|
|
startFlex: 1,
|
|
|
|
// The default ratio of the end widget. It an be an integer or a floating point number
|
|
endFlex: 1,
|
|
|
|
// the padding between the two columns
|
|
columnPadding: 20,
|
|
|
|
// the config of the first widget
|
|
startColumn: undefined,
|
|
|
|
// the config of the second widget
|
|
endColumn: undefined,
|
|
|
|
// same as fields in a panel
|
|
padding: '0 0 5 0',
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
if (!me.startColumn) {
|
|
throw "no start widget configured";
|
|
}
|
|
if (!me.endColumn) {
|
|
throw "no end widget configured";
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
items: [
|
|
Ext.applyIf({ flex: me.startFlex }, me.startColumn),
|
|
{
|
|
xtype: 'box',
|
|
width: me.columnPadding,
|
|
},
|
|
Ext.applyIf({ flex: me.endFlex }, me.endColumn),
|
|
],
|
|
});
|
|
|
|
me.callParent();
|
|
},
|
|
});
|
|
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: 'proxmoxcheckbox',
|
|
boxLabel: gettext('Freeze/thaw guest filesystems on backup for consistency'),
|
|
name: 'freeze-fs-on-backup',
|
|
reference: 'freeze_fs_on_backup',
|
|
bind: {
|
|
disabled: '{!enabled.checked}',
|
|
},
|
|
disabled: true,
|
|
uncheckedValue: '0',
|
|
defaultValue: '1',
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
userCls: 'pmx-hint',
|
|
value: gettext('Freeze/thaw for guest filesystems disabled. This can lead to inconsistent disk backups.'),
|
|
bind: {
|
|
hidden: '{freeze_fs_on_backup.checked}',
|
|
},
|
|
},
|
|
{
|
|
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) {
|
|
if (PVE.Parser.parseBoolean(values['freeze-fs-on-backup'])) {
|
|
delete values['freeze-fs-on-backup'];
|
|
}
|
|
|
|
const agentstr = PVE.Parser.printPropertyString(values, 'enabled');
|
|
return { agent: agentstr };
|
|
},
|
|
|
|
setValues: function(values) {
|
|
let res = PVE.Parser.parsePropertyString(values.agent, 'enabled');
|
|
if (!Ext.isDefined(res['freeze-fs-on-backup'])) {
|
|
res['freeze-fs-on-backup'] = 1;
|
|
}
|
|
|
|
this.callParent([res]);
|
|
},
|
|
});
|
|
Ext.define('PVE.form.BackupCompressionSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveBackupCompressionSelector'],
|
|
comboItems: [
|
|
['0', Proxmox.Utils.noneText],
|
|
['lzo', 'LZO (' + gettext('fast') + ')'],
|
|
['gzip', 'GZIP (' + gettext('good') + ')'],
|
|
['zstd', 'ZSTD (' + gettext('fast and good') + ')'],
|
|
],
|
|
});
|
|
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 && v !== null) {
|
|
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: [
|
|
'<ul class="x-list-plain"><tpl for=".">',
|
|
'<li role="option" class="x-boundlist-item">{text}</li>',
|
|
'</tpl></ul>',
|
|
],
|
|
|
|
displayTpl: [
|
|
'<tpl for=".">',
|
|
'{value}',
|
|
'</tpl>',
|
|
],
|
|
|
|
});
|
|
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.Utils.isStandaloneNode()) {
|
|
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.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',
|
|
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',
|
|
itemId: 'hdimage',
|
|
fieldLabel: gettext('Disk image'),
|
|
nodename: me.nodename,
|
|
disabled: true,
|
|
hidden: true,
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
itemId: 'disksize',
|
|
name: 'disksize',
|
|
fieldLabel: `${gettext('Disk size')} (${gettext('GiB')})`,
|
|
hidden: me.hideSize,
|
|
disabled: me.hideSize,
|
|
minValue: 0.001,
|
|
maxValue: 128*1024,
|
|
decimalPrecision: 3,
|
|
value: me.defaultSize,
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'pveDiskFormatSelector',
|
|
itemId: '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.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',
|
|
|
|
editable: true,
|
|
anyMatch: true,
|
|
forceSelection: true,
|
|
|
|
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: 'scopedref',
|
|
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',
|
|
'scope',
|
|
{
|
|
name: 'scopedref',
|
|
calculate: function(v) {
|
|
if (v.type === 'alias') {
|
|
return `${v.scope}/${v.name}`;
|
|
} else if (v.type === 'ipset') {
|
|
return `+${v.scope}/${v.name}`;
|
|
} else {
|
|
return v.ref;
|
|
}
|
|
},
|
|
},
|
|
],
|
|
idProperty: 'ref',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: url,
|
|
},
|
|
sorters: {
|
|
property: 'ref',
|
|
direction: 'ASC',
|
|
},
|
|
});
|
|
|
|
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('Scope'),
|
|
dataIndex: 'scope',
|
|
hideable: false,
|
|
width: 140,
|
|
renderer: function(value) {
|
|
return value === 'dc' ? gettext("Datacenter") : gettext("Guest");
|
|
},
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode,
|
|
minWidth: 60,
|
|
flex: 1,
|
|
},
|
|
);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
columns: columns,
|
|
width: 500,
|
|
},
|
|
});
|
|
|
|
me.on('beforequery', function(queryPlan) {
|
|
return !(queryPlan.query === null || queryPlan.query.match(/^\d/));
|
|
});
|
|
|
|
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('<br>');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
|
|
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.MultiPCISelector', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pveMultiPCISelector',
|
|
|
|
emptyText: gettext('No Devices found'),
|
|
|
|
mixins: {
|
|
field: 'Ext.form.field.Field',
|
|
},
|
|
|
|
// will be called after loading finished
|
|
onLoadCallBack: Ext.emptyFn,
|
|
|
|
getValue: function() {
|
|
let me = this;
|
|
return me.value ?? [];
|
|
},
|
|
|
|
getSubmitData: function() {
|
|
let me = this;
|
|
let res = {};
|
|
res[me.name] = me.getValue();
|
|
return res;
|
|
},
|
|
|
|
setValue: function(value) {
|
|
let me = this;
|
|
|
|
value ??= [];
|
|
|
|
me.updateSelectedDevices(value);
|
|
|
|
return me.mixins.field.setValue.call(me, value);
|
|
},
|
|
|
|
getErrors: function() {
|
|
let me = this;
|
|
|
|
let errorCls = ['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid'];
|
|
|
|
if (me.getValue().length < 1) {
|
|
let error = gettext("Must choose at least one device");
|
|
me.addCls(errorCls);
|
|
me.getActionEl()?.dom.setAttribute('data-errorqtip', error);
|
|
|
|
return [error];
|
|
}
|
|
|
|
me.removeCls(errorCls);
|
|
me.getActionEl()?.dom.setAttribute('data-errorqtip', "");
|
|
|
|
return [];
|
|
},
|
|
|
|
viewConfig: {
|
|
getRowClass: function(record) {
|
|
if (record.data.disabled === true) {
|
|
return 'x-item-disabled';
|
|
}
|
|
return '';
|
|
},
|
|
},
|
|
|
|
updateSelectedDevices: function(value = []) {
|
|
let me = this;
|
|
|
|
let recs = [];
|
|
let store = me.getStore();
|
|
|
|
for (const map of value) {
|
|
let parsed = PVE.Parser.parsePropertyString(map);
|
|
if (parsed.node !== me.nodename) {
|
|
continue;
|
|
}
|
|
|
|
let rec = store.getById(parsed.path);
|
|
if (rec) {
|
|
recs.push(rec);
|
|
}
|
|
}
|
|
|
|
me.suspendEvent('change');
|
|
me.setSelection();
|
|
me.setSelection(recs);
|
|
me.resumeEvent('change');
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
let me = this;
|
|
|
|
if (!nodename || me.nodename === nodename) {
|
|
return;
|
|
}
|
|
|
|
me.nodename = nodename;
|
|
|
|
me.getStore().setProxy({
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/hardware/pci?pci-class-blacklist=',
|
|
});
|
|
|
|
me.setSelection();
|
|
|
|
me.getStore().load({
|
|
callback: (recs, op, success) => me.addSlotRecords(recs, op, success),
|
|
});
|
|
},
|
|
|
|
setMdev: function(mdev) {
|
|
let me = this;
|
|
if (mdev) {
|
|
me.getStore().addFilter({
|
|
id: 'mdev-filter',
|
|
property: 'mdev',
|
|
value: '1',
|
|
operator: '=',
|
|
});
|
|
} else {
|
|
me.getStore().removeFilter('mdev-filter');
|
|
}
|
|
me.setSelection();
|
|
},
|
|
|
|
// adds the virtual 'slot' records (e.g. '0000:01:00') to the store
|
|
addSlotRecords: function(records, _op, success) {
|
|
let me = this;
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
let slots = {};
|
|
records.forEach((rec) => {
|
|
let slotname = rec.data.id.slice(0, -2); // remove function
|
|
if (slots[slotname] !== undefined) {
|
|
slots[slotname].count++;
|
|
rec.set('slot', slots[slotname]);
|
|
return;
|
|
}
|
|
slots[slotname] = {
|
|
count: 1,
|
|
};
|
|
|
|
rec.set('slot', slots[slotname]);
|
|
|
|
if (rec.data.id.endsWith('.0')) {
|
|
slots[slotname].device = rec.data;
|
|
}
|
|
});
|
|
|
|
let store = me.getStore();
|
|
|
|
for (const [slot, { count, device }] of Object.entries(slots)) {
|
|
if (count === 1) {
|
|
continue;
|
|
}
|
|
store.add(Ext.apply({}, {
|
|
id: slot,
|
|
mdev: undefined,
|
|
device_name: gettext('Pass through all functions as one device'),
|
|
}, device));
|
|
}
|
|
|
|
me.updateSelectedDevices(me.value);
|
|
},
|
|
|
|
selectionChange: function(_grid, selection) {
|
|
let me = this;
|
|
|
|
let ids = {};
|
|
selection
|
|
.filter(rec => rec.data.id.indexOf('.') === -1)
|
|
.forEach((rec) => { ids[rec.data.id] = true; });
|
|
|
|
let to_disable = [];
|
|
|
|
me.getStore().each(rec => {
|
|
let id = rec.data.id;
|
|
rec.set('disabled', false);
|
|
if (id.indexOf('.') === -1) {
|
|
return;
|
|
}
|
|
let slot = id.slice(0, -2); // remove function
|
|
|
|
if (ids[slot]) {
|
|
to_disable.push(rec);
|
|
rec.set('disabled', true);
|
|
}
|
|
});
|
|
|
|
me.suspendEvent('selectionchange');
|
|
me.getSelectionModel().deselect(to_disable);
|
|
me.resumeEvent('selectionchange');
|
|
|
|
me.value = me.getSelection().map((rec) => {
|
|
let res = {
|
|
path: rec.data.id,
|
|
node: me.nodename,
|
|
id: `${rec.data.vendor}:${rec.data.device}`.replace(/0x/g, ''),
|
|
'subsystem-id': `${rec.data.subsystem_vendor}:${rec.data.subsystem_device}`.replace(/0x/g, ''),
|
|
};
|
|
|
|
if (rec.data.iommugroup !== -1) {
|
|
res.iommugroup = rec.data.iommugroup;
|
|
}
|
|
|
|
return PVE.Parser.printPropertyString(res);
|
|
});
|
|
me.checkChange();
|
|
},
|
|
|
|
selModel: {
|
|
type: 'checkboxmodel',
|
|
mode: 'SIMPLE',
|
|
},
|
|
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
dataIndex: 'id',
|
|
renderer: function(value, _md, rec) {
|
|
if (value.match(/\.[0-9a-f]/i) && rec.data.slot?.count > 1) {
|
|
return ` ${value}`;
|
|
}
|
|
return value;
|
|
},
|
|
width: 150,
|
|
},
|
|
{
|
|
header: gettext('IOMMU Group'),
|
|
dataIndex: 'iommugroup',
|
|
renderer: (v, _md, rec) => rec.data.slot === rec.data.id ? '' : v === -1 ? '-' : v,
|
|
width: 50,
|
|
},
|
|
{
|
|
header: gettext('Vendor'),
|
|
dataIndex: 'vendor_name',
|
|
flex: 3,
|
|
},
|
|
{
|
|
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);
|
|
},
|
|
},
|
|
],
|
|
|
|
listeners: {
|
|
selectionchange: function() {
|
|
this.selectionChange(...arguments);
|
|
},
|
|
},
|
|
|
|
store: {
|
|
fields: [
|
|
'id', 'vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev',
|
|
'subsystem_vendor', 'subsystem_device', 'disabled',
|
|
{
|
|
name: 'subsystem-vendor',
|
|
calculate: function(data) {
|
|
return data.subsystem_vendor;
|
|
},
|
|
},
|
|
{
|
|
name: 'subsystem-device',
|
|
calculate: function(data) {
|
|
return data.subsystem_device;
|
|
},
|
|
},
|
|
],
|
|
sorters: [
|
|
{
|
|
property: 'id',
|
|
direction: 'ASC',
|
|
},
|
|
],
|
|
},
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
let nodename = me.nodename;
|
|
me.nodename = undefined;
|
|
|
|
me.callParent();
|
|
|
|
me.mon(me.getStore(), 'load', me.onLoadCallBack);
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
|
|
|
|
me.setNodename(nodename);
|
|
|
|
me.initField();
|
|
},
|
|
});
|
|
Ext.define('PVE.form.NetworkCardSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: 'widget.pveNetworkCardSelector',
|
|
comboItems: [
|
|
['e1000', 'Intel E1000'],
|
|
['e1000e', 'Intel E1000E'],
|
|
['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,
|
|
|
|
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.NotificationModeSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveNotificationModeSelector'],
|
|
comboItems: [
|
|
['notification-target', gettext('Target')],
|
|
['mailto', gettext('E-Mail')],
|
|
],
|
|
});
|
|
Ext.define('PVE.form.NotificationTargetSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveNotificationTargetSelector'],
|
|
|
|
// 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: 'name',
|
|
displayField: 'name',
|
|
deleteEmpty: true,
|
|
skipEmptyText: true,
|
|
|
|
store: {
|
|
fields: ['name', 'type', 'comment'],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/cluster/notifications/targets',
|
|
},
|
|
sorters: [
|
|
{
|
|
property: 'name',
|
|
direction: 'ASC',
|
|
},
|
|
],
|
|
autoLoad: true,
|
|
},
|
|
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Target'),
|
|
dataIndex: 'name',
|
|
sortable: true,
|
|
hideable: false,
|
|
flex: 1,
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
dataIndex: 'type',
|
|
sortable: true,
|
|
hideable: false,
|
|
flex: 1,
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
sortable: true,
|
|
hideable: false,
|
|
flex: 2,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
Ext.define('PVE.form.EmailNotificationSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveEmailNotificationSelector'],
|
|
comboItems: [
|
|
['always', gettext('Always')],
|
|
['failure', gettext('On failure only')],
|
|
],
|
|
});
|
|
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: {
|
|
minHeight: 80,
|
|
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-mapped-pci-model', {
|
|
extend: 'Ext.data.Model',
|
|
|
|
fields: ['id', 'path', 'vendor', 'device', 'iommugroup', 'mdev', 'description', 'map'],
|
|
idProperty: 'id',
|
|
});
|
|
|
|
Ext.define('PVE.form.PCIMapSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
xtype: 'pvePCIMapSelector',
|
|
|
|
store: {
|
|
model: 'pve-mapped-pci-model',
|
|
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: gettext('ID'),
|
|
dataIndex: 'id',
|
|
flex: 1,
|
|
},
|
|
{
|
|
header: gettext('Description'),
|
|
dataIndex: 'description',
|
|
flex: 1,
|
|
renderer: Ext.String.htmlEncode,
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'checks',
|
|
renderer: function(value) {
|
|
let me = this;
|
|
|
|
if (!Ext.isArray(value) || !value?.length) {
|
|
return `<i class="fa fa-check-circle good"></i> ${gettext('Mapping matches host data')}`;
|
|
}
|
|
|
|
let checks = [];
|
|
|
|
value.forEach((check) => {
|
|
let iconCls;
|
|
switch (check?.severity) {
|
|
case 'warning':
|
|
iconCls = 'fa-exclamation-circle warning';
|
|
break;
|
|
case 'error':
|
|
iconCls = 'fa-times-circle critical';
|
|
break;
|
|
}
|
|
|
|
let message = check?.message;
|
|
let icon = `<i class="fa ${iconCls}"></i>`;
|
|
if (iconCls !== undefined) {
|
|
checks.push(`${icon} ${message}`);
|
|
}
|
|
});
|
|
|
|
return checks.join('<br>');
|
|
},
|
|
flex: 3,
|
|
},
|
|
],
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
if (!nodename || me.nodename === nodename) {
|
|
return;
|
|
}
|
|
|
|
me.nodename = nodename;
|
|
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: `/api2/json/cluster/mapping/pci?check-node=${nodename}`,
|
|
});
|
|
|
|
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',
|
|
width: 380,
|
|
|
|
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");
|
|
},
|
|
|
|
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/usb`,
|
|
});
|
|
|
|
me.store.load();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (me.pveSelNode) {
|
|
me.nodename = me.pveSelNode.data.node;
|
|
}
|
|
|
|
var nodename = me.nodename;
|
|
me.nodename = undefined;
|
|
|
|
if (me.type !== 'device' && me.type !== 'port') {
|
|
throw "no valid type specified";
|
|
}
|
|
|
|
let store = new Ext.data.Store({
|
|
model: `pve-usb-${me.type}`,
|
|
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: {
|
|
minHeight: 80,
|
|
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();
|
|
|
|
me.setNodename(nodename);
|
|
},
|
|
|
|
}, 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('Unknown');
|
|
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('PVE.form.USBMapSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: 'widget.pveUSBMapSelector',
|
|
|
|
store: {
|
|
fields: ['name', 'vendor', 'device', 'path'],
|
|
filterOnLoad: true,
|
|
sorters: [
|
|
{
|
|
property: 'name',
|
|
direction: 'ASC',
|
|
},
|
|
],
|
|
},
|
|
|
|
allowBlank: false,
|
|
autoSelect: false,
|
|
displayField: 'id',
|
|
valueField: 'id',
|
|
|
|
listConfig: {
|
|
width: 800,
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'id',
|
|
flex: 1,
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'errors',
|
|
flex: 2,
|
|
renderer: function(value) {
|
|
let me = this;
|
|
|
|
if (!Ext.isArray(value) || !value?.length) {
|
|
return `<i class="fa fa-check-circle good"></i> ${gettext('Mapping matches host data')}`;
|
|
}
|
|
|
|
let errors = [];
|
|
|
|
value.forEach((error) => {
|
|
let iconCls;
|
|
switch (error?.severity) {
|
|
case 'warning':
|
|
iconCls = 'fa-exclamation-circle warning';
|
|
break;
|
|
case 'error':
|
|
iconCls = 'fa-times-circle critical';
|
|
break;
|
|
}
|
|
|
|
let message = error?.message;
|
|
let icon = `<i class="fa ${iconCls}"></i>`;
|
|
if (iconCls !== undefined) {
|
|
errors.push(`${icon} ${message}`);
|
|
}
|
|
});
|
|
|
|
return errors.join('<br>');
|
|
},
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'description',
|
|
flex: 1,
|
|
renderer: Ext.String.htmlEncode,
|
|
},
|
|
],
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
if (!nodename || me.nodename === nodename) {
|
|
return;
|
|
}
|
|
|
|
me.nodename = nodename;
|
|
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: `/api2/json/cluster/mapping/usb?check-node=${nodename}`,
|
|
});
|
|
|
|
me.store.load();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.nodename;
|
|
me.nodename = undefined;
|
|
|
|
me.callParent();
|
|
|
|
me.setNodename(nodename);
|
|
},
|
|
});
|
|
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?full=1",
|
|
},
|
|
idProperty: 'userid',
|
|
});
|
|
Ext.define('PVE.form.VlanField', {
|
|
extend: 'Ext.form.field.Number',
|
|
alias: ['widget.pveVlanField'],
|
|
|
|
deleteEmpty: false,
|
|
|
|
emptyText: gettext('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',
|
|
},
|
|
|
|
userCls: 'proxmox-tags-circle',
|
|
|
|
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: gettext('Tags'),
|
|
dataIndex: 'tags',
|
|
renderer: tags => PVE.Utils.renderTags(tags, PVE.UIOptions.tagOverrides),
|
|
flex: 1,
|
|
},
|
|
{
|
|
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) {
|
|
if (Ext.isNumeric(item)) {
|
|
notFound.push(item);
|
|
} else {
|
|
console.warn(`invalid item in vm selection: ${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;
|
|
value ??= [];
|
|
if (!Ext.isArray(value)) {
|
|
value = value.split(',').filter(v => v !== '');
|
|
}
|
|
|
|
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.getValue().length === 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',
|
|
margin: '5 0 0 0',
|
|
},
|
|
],
|
|
|
|
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: [
|
|
'<i class="handle fa fa-bars"></i>',
|
|
'<span>{tag}</span>',
|
|
'<i class="action fa fa-minus-square"></i>',
|
|
],
|
|
|
|
focusable: true,
|
|
getFocusEl: function() {
|
|
return Ext.get(this.tagEl());
|
|
},
|
|
|
|
onFocus: function() {
|
|
this.selectText();
|
|
},
|
|
|
|
// 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,
|
|
editOnly: false,
|
|
|
|
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: '<i class="fa fa-long-arrow-up"></i>',
|
|
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 (vm.get('hideFinishButtons')) {
|
|
return;
|
|
}
|
|
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.getViewModel().set('editOnly', view.editOnly);
|
|
|
|
me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
|
|
let vm = me.getViewModel();
|
|
view.toggleCls('hide-handles', PVE.UIOptions.shouldSortTags());
|
|
me.loadTags(me.oldTags, !vm.get('editMode')); // refresh tag colors and order
|
|
});
|
|
|
|
if (view.editOnly) {
|
|
me.toggleEdit();
|
|
}
|
|
},
|
|
},
|
|
|
|
getTags: function() {
|
|
let me =this;
|
|
let controller = me.getController();
|
|
let tags = [];
|
|
controller.forEachTag((cmp) => {
|
|
if (cmp.tag.length) {
|
|
tags.push(cmp.tag);
|
|
}
|
|
});
|
|
|
|
return tags;
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
tagCount: 0,
|
|
editMode: false,
|
|
canEdit: true,
|
|
isDirty: false,
|
|
editOnly: true,
|
|
},
|
|
|
|
formulas: {
|
|
hideNoTags: function(get) {
|
|
return get('tagCount') !== 0 || !get('canEdit');
|
|
},
|
|
hideEditBtn: function(get) {
|
|
return get('editMode') || !get('canEdit');
|
|
},
|
|
hideFinishButtons: function(get) {
|
|
return !get('editMode') || get('editOnly');
|
|
},
|
|
},
|
|
},
|
|
|
|
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: '{hideFinishButtons}',
|
|
},
|
|
hidden: true,
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
iconCls: 'fa fa-times',
|
|
tooltip: gettext('Cancel Edit'),
|
|
bind: {
|
|
hidden: '{hideFinishButtons}',
|
|
},
|
|
hidden: true,
|
|
margin: '0 5 0 0',
|
|
ui: 'default-toolbar',
|
|
handler: 'cancelClick',
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
iconCls: 'fa fa-check',
|
|
tooltip: gettext('Finish Edit'),
|
|
bind: {
|
|
hidden: '{hideFinishButtons}',
|
|
disabled: '{!isDirty}',
|
|
},
|
|
hidden: true,
|
|
handler: 'editClick',
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
cls: 'pve-tag-inline-button',
|
|
html: `<i data-qtip="${gettext('Edit Tags')}" class="fa fa-pencil"></i>`,
|
|
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();
|
|
},
|
|
});
|
|
// mostly copied from ExtJS FileButton, but added 'multiple' at the relevant
|
|
// places so we have a file picker where one can select multiple files
|
|
// changes are marked with an 'pmx:' comment
|
|
Ext.define('PVE.form.MultiFileButton', {
|
|
extend: 'Ext.form.field.FileButton',
|
|
alias: 'widget.pveMultiFileButton',
|
|
|
|
afterTpl: [
|
|
'<input id="{id}-fileInputEl" data-ref="fileInputEl" class="{childElCls} {inputCls}" ',
|
|
'type="file" size="1" name="{inputName}" unselectable="on" multiple ', // pmx: added multiple
|
|
'<tpl if="accept != null">accept="{accept}"</tpl>',
|
|
'<tpl if="tabIndex != null">tabindex="{tabIndex}"</tpl>',
|
|
'>',
|
|
],
|
|
|
|
createFileInput: function(isTemporary) {
|
|
var me = this,
|
|
fileInputEl, listeners;
|
|
|
|
fileInputEl = me.fileInputEl = me.el.createChild({
|
|
name: me.inputName || me.id,
|
|
multiple: true, // pmx: added multiple option
|
|
id: !isTemporary ? me.id + '-fileInputEl' : undefined,
|
|
cls: me.inputCls + (me.getInherited().rtl ? ' ' + Ext.baseCSSPrefix + 'rtl' : ''),
|
|
tag: 'input',
|
|
type: 'file',
|
|
size: 1,
|
|
unselectable: 'on',
|
|
}, me.afterInputGuard); // Nothing special happens outside of IE/Edge
|
|
|
|
// This is our focusEl
|
|
fileInputEl.dom.setAttribute('data-componentid', me.id);
|
|
|
|
if (me.tabIndex !== null) {
|
|
me.setTabIndex(me.tabIndex);
|
|
}
|
|
|
|
if (me.accept) {
|
|
fileInputEl.dom.setAttribute('accept', me.accept);
|
|
}
|
|
|
|
// We place focus and blur listeners on fileInputEl to activate Button's
|
|
// focus and blur style treatment
|
|
listeners = {
|
|
scope: me,
|
|
change: me.fireChange,
|
|
mousedown: me.handlePrompt,
|
|
keydown: me.handlePrompt,
|
|
focus: me.onFileFocus,
|
|
blur: me.onFileBlur,
|
|
};
|
|
|
|
if (me.useTabGuards) {
|
|
listeners.keydown = me.onFileInputKeydown;
|
|
}
|
|
|
|
fileInputEl.on(listeners);
|
|
},
|
|
});
|
|
Ext.define('PVE.form.TagFieldSet', {
|
|
extend: 'Ext.form.FieldSet',
|
|
alias: 'widget.pveTagFieldSet',
|
|
mixins: ['Ext.form.field.Field'],
|
|
|
|
title: gettext('Tags'),
|
|
padding: '0 5 5 5',
|
|
|
|
getValue: function() {
|
|
let me = this;
|
|
let tags = me.down('pveTagEditContainer').getTags().filter(t => t !== '');
|
|
return tags.join(';');
|
|
},
|
|
|
|
setValue: function(value) {
|
|
let me = this;
|
|
value ??= [];
|
|
if (!Ext.isArray(value)) {
|
|
value = value.split(/[;, ]/).filter(t => t !== '');
|
|
}
|
|
me.down('pveTagEditContainer').loadTags(value.join(';'));
|
|
},
|
|
|
|
getErrors: function(value) {
|
|
value ??= [];
|
|
if (!Ext.isArray(value)) {
|
|
value = value.split(/[;, ]/).filter(t => t !== '');
|
|
}
|
|
if (value.some(t => !t.match(PVE.Utils.tagCharRegex))) {
|
|
return [gettext("Tags contain invalid characters.")];
|
|
}
|
|
return [];
|
|
},
|
|
|
|
getSubmitData: function() {
|
|
let me = this;
|
|
let value = me.getValue();
|
|
if (me.disabled || !me.submitValue || value === '') {
|
|
return null;
|
|
}
|
|
let data = {};
|
|
data[me.getName()] = value;
|
|
return data;
|
|
},
|
|
|
|
layout: 'fit',
|
|
|
|
items: [
|
|
{
|
|
xtype: 'pveTagEditContainer',
|
|
userCls: 'proxmox-tags-full proxmox-tag-fieldset',
|
|
editOnly: true,
|
|
allowBlank: true,
|
|
layout: 'column',
|
|
scrollable: true,
|
|
},
|
|
],
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
me.callParent();
|
|
me.initField();
|
|
},
|
|
});
|
|
Ext.define('PVE.form.IsoSelector', {
|
|
extend: 'Ext.container.Container',
|
|
alias: 'widget.pveIsoSelector',
|
|
mixins: [
|
|
'Ext.form.field.Field',
|
|
'Proxmox.Mixin.CBind',
|
|
],
|
|
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch',
|
|
},
|
|
|
|
nodename: undefined,
|
|
insideWizard: false,
|
|
labelWidth: undefined,
|
|
labelAlign: 'right',
|
|
|
|
cbindData: function() {
|
|
let me = this;
|
|
return {
|
|
nodename: me.nodename,
|
|
insideWizard: me.insideWizard,
|
|
};
|
|
},
|
|
|
|
getValue: function() {
|
|
return this.lookup('file').getValue();
|
|
},
|
|
|
|
setValue: function(value) {
|
|
let me = this;
|
|
if (!value) {
|
|
me.lookup('file').reset();
|
|
return;
|
|
}
|
|
var match = value.match(/^([^:]+):/);
|
|
if (match) {
|
|
me.lookup('storage').setValue(match[1]);
|
|
me.lookup('file').setValue(value);
|
|
}
|
|
},
|
|
|
|
getErrors: function() {
|
|
let me = this;
|
|
me.lookup('storage').validate();
|
|
let file = me.lookup('file');
|
|
file.validate();
|
|
let value = file.getValue();
|
|
if (!value || !value.length) {
|
|
return [""]; // for validation
|
|
}
|
|
return [];
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
let me = this;
|
|
me.lookup('storage').setNodename(nodename);
|
|
me.lookup('file').setStorage(undefined, nodename);
|
|
},
|
|
|
|
setDisabled: function(disabled) {
|
|
let me = this;
|
|
me.lookup('storage').setDisabled(disabled);
|
|
me.lookup('file').setDisabled(disabled);
|
|
return me.callParent([disabled]);
|
|
},
|
|
|
|
referenceHolder: true,
|
|
|
|
items: [
|
|
{
|
|
xtype: 'pveStorageSelector',
|
|
reference: 'storage',
|
|
isFormField: false,
|
|
fieldLabel: gettext('Storage'),
|
|
storageContent: 'iso',
|
|
allowBlank: false,
|
|
cbind: {
|
|
nodename: '{nodename}',
|
|
autoSelect: '{insideWizard}',
|
|
insideWizard: '{insideWizard}',
|
|
disabled: '{disabled}',
|
|
labelWidth: '{labelWidth}',
|
|
labelAlign: '{labelAlign}',
|
|
},
|
|
listeners: {
|
|
change: function(f, value) {
|
|
let me = this;
|
|
let selector = me.up('pveIsoSelector');
|
|
selector.lookup('file').setStorage(value);
|
|
selector.checkChange();
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveFileSelector',
|
|
reference: 'file',
|
|
isFormField: false,
|
|
storageContent: 'iso',
|
|
fieldLabel: gettext('ISO image'),
|
|
labelAlign: 'right',
|
|
cbind: {
|
|
nodename: '{nodename}',
|
|
disabled: '{disabled}',
|
|
labelWidth: '{labelWidth}',
|
|
labelAlign: '{labelAlign}',
|
|
},
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function() {
|
|
this.up('pveIsoSelector').checkChange();
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
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: `<i class="fa fa-shield"></i>`,
|
|
tooltip: gettext('Protected'),
|
|
width: 30,
|
|
renderer: v => v ? `<i data-qtip="${gettext('Protected')}" class="fa fa-shield"></i>` : '',
|
|
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: '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 caps = Ext.state.Manager.get('GuiCap');
|
|
let canEdit = !!caps.vms['VM.Config.Network'] || !!caps.dc['Sys.Modify'] || !!caps.nodes['Sys.Modify'];
|
|
|
|
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 || !canEdit) {
|
|
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,
|
|
enableFn: rec => canEdit,
|
|
handler: run_editor,
|
|
});
|
|
|
|
me.addBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Add'),
|
|
disabled: !caps.vms['VM.Config.Network'] && !caps.dc['Sys.Modify'] && !caps.nodes['Sys.Modify'],
|
|
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', {
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: rec => !!caps.vms['VM.Config.Network'] || !!caps.dc['Sys.Modify'] || !!caps.nodes['Sys.Modify'],
|
|
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";
|
|
}
|
|
|
|
let caps = Ext.state.Manager.get('GuiCap');
|
|
let canEdit = caps.vms['VM.Config.Network'] || caps.dc['Sys.Modify'] || caps.nodes['Sys.Modify'];
|
|
|
|
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');
|
|
add_boolean_row('nftables', gettext('nftables (tech preview)'), 0);
|
|
} 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];
|
|
if (canEdit) {
|
|
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: () => { if (canEdit) { 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,
|
|
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,
|
|
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 {
|
|
if (me.canEdit) {
|
|
me.addBtn.setDisabled(false);
|
|
if (me.groupBtn) {
|
|
me.groupBtn.setDisabled(false);
|
|
}
|
|
}
|
|
me.removeBtn.baseurl = url + '/';
|
|
|
|
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', {});
|
|
|
|
me.caps = Ext.state.Manager.get('GuiCap');
|
|
me.canEdit = !!me.caps.vms['VM.Config.Network'] || !!me.caps.dc['Sys.Modify'] || !!me.caps.nodes['Sys.Modify'];
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec || !me.canEdit) {
|
|
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,
|
|
enableFn: rec => me.canEdit,
|
|
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') && me.canEdit,
|
|
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', {
|
|
enableFn: rec => me.canEdit,
|
|
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 = '<p>' + Ext.htmlEncode(errors[name]) + '</p>';
|
|
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 = "<i class='pve-grid-fa fa fa-fw fa-reorder cursor-move'></i>";
|
|
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 = `<span data-qtip="${comment}">${comment}</span>`;
|
|
}
|
|
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: 640,
|
|
height: 480,
|
|
isAdd: true,
|
|
isCreate: true,
|
|
|
|
extraRequestParams: {
|
|
'allow-move': 1,
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.pool) {
|
|
throw "no pool specified";
|
|
}
|
|
|
|
me.url = '/pools/';
|
|
me.method = 'PUT';
|
|
me.extraRequestParams.poolid = me.pool;
|
|
|
|
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 !== me.pool;
|
|
},
|
|
],
|
|
});
|
|
|
|
var vmGrid = Ext.create('widget.grid', {
|
|
store: vmStore,
|
|
border: true,
|
|
height: 360,
|
|
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('Current Pool'),
|
|
dataIndex: 'pool',
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'uptime',
|
|
renderer: v => v ? Proxmox.Utils.runningText : 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,
|
|
{
|
|
xtype: 'displayfield',
|
|
userCls: 'pmx-hint',
|
|
value: gettext('Selected guests who are already part of a pool will be removed from it first.'),
|
|
},
|
|
],
|
|
});
|
|
|
|
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.method = 'PUT';
|
|
me.extraRequestParams.poolid = me.pool;
|
|
|
|
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[0].members',
|
|
url: "/api2/json/pools/?poolid=" + 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, poolid: me.pool };
|
|
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/',
|
|
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.Utils.isStandaloneNode()) {
|
|
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',
|
|
renderer: Proxmox.Utils.renderEnabledIcon,
|
|
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('<i class="fa fa-ban warning" title="'
|
|
+ gettext("Removal Scheduled") + '"></i>');
|
|
states.push(gettext("Removal Scheduled"));
|
|
}
|
|
if (record.data.error) {
|
|
icons.push('<i class="fa fa-times critical" title="'
|
|
+ gettext("Error") + '"></i>');
|
|
states.push(record.data.error);
|
|
}
|
|
if (icons.length === 0) {
|
|
icons.push('<i class="fa fa-check good"></i>');
|
|
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.Utils.isStandaloneNode()) {
|
|
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 advanced backup options intended to be used as part of an edit/create window.
|
|
*/
|
|
Ext.define('PVE.panel.BackupAdvancedOptions', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveBackupAdvancedOptionsPanel',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
cbindData: function() {
|
|
let me = this;
|
|
me.isCreate = !!me.isCreate;
|
|
return {};
|
|
},
|
|
|
|
viewModel: {
|
|
data: {},
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
toggleFleecing: function(cb, value) {
|
|
let me = this;
|
|
me.lookup('fleecingStorage').setDisabled(!value);
|
|
},
|
|
|
|
control: {
|
|
'proxmoxcheckbox[reference=fleecingEnabled]': {
|
|
change: 'toggleFleecing',
|
|
},
|
|
},
|
|
},
|
|
|
|
onGetValues: function(formValues) {
|
|
let me = this;
|
|
if (me.needMask) { // isMasked() may not yet be true if not rendered once
|
|
return {};
|
|
}
|
|
|
|
let options = {};
|
|
|
|
if (!me.isCreate) {
|
|
options.delete = []; // to avoid having to check this all the time
|
|
}
|
|
const deletePropertyOnEdit = me.isCreate
|
|
? () => { /* no-op on create */ }
|
|
: key => options.delete.push(key);
|
|
|
|
let fleecing = {}, fleecingOptions = ['fleecing-enabled', 'fleecing-storage'];
|
|
let performance = {}, performanceOptions = ['max-workers', 'pbs-entries-max'];
|
|
|
|
for (const [key, value] of Object.entries(formValues)) {
|
|
if (performanceOptions.includes(key)) {
|
|
performance[key] = value;
|
|
// deleteEmpty is not currently implemented for pveBandwidthField
|
|
} else if (key === 'bwlimit' && value === '') {
|
|
deletePropertyOnEdit('bwlimit');
|
|
} else if (key === 'delete') {
|
|
if (Array.isArray(value)) {
|
|
value.filter(opt => !performanceOptions.includes(opt)).forEach(
|
|
opt => deletePropertyOnEdit(opt),
|
|
);
|
|
} else if (!performanceOptions.includes(formValues.delete)) {
|
|
deletePropertyOnEdit(value);
|
|
}
|
|
} else if (fleecingOptions.includes(key)) {
|
|
let fleecingKey = key.slice('fleecing-'.length);
|
|
fleecing[fleecingKey] = value;
|
|
} else {
|
|
options[key] = value;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(performance).length > 0) {
|
|
options.performance = PVE.Parser.printPropertyString(performance);
|
|
} else {
|
|
deletePropertyOnEdit('performance');
|
|
}
|
|
|
|
if (Object.keys(fleecing).length > 0) {
|
|
options.fleecing = PVE.Parser.printPropertyString(fleecing);
|
|
} else {
|
|
deletePropertyOnEdit('fleecing');
|
|
}
|
|
|
|
if (me.isCreate) {
|
|
delete options.delete;
|
|
}
|
|
|
|
return options;
|
|
},
|
|
|
|
onSetValues: function(values) {
|
|
if (values.fleecing) {
|
|
for (const [key, value] of Object.entries(values.fleecing)) {
|
|
values[`fleecing-${key}`] = value;
|
|
}
|
|
delete values.fleecing;
|
|
}
|
|
if (values["pbs-change-detection-mode"] === '__default__') {
|
|
delete values["pbs-change-detection-mode"];
|
|
}
|
|
return values;
|
|
},
|
|
|
|
updateCompression: function(value, disabled) {
|
|
this.lookup('zstdThreadCount').setDisabled(disabled || value !== 'zstd');
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'pveTwoColumnContainer',
|
|
startColumn: {
|
|
xtype: 'pveBandwidthField',
|
|
name: 'bwlimit',
|
|
fieldLabel: gettext('Bandwidth Limit'),
|
|
emptyText: gettext('Fallback'),
|
|
backendUnit: 'KiB',
|
|
allowZero: true,
|
|
emptyValue: '',
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': Ext.String.format(gettext('Use {0} for unlimited'), 0),
|
|
},
|
|
},
|
|
endFlex: 2,
|
|
endColumn: {
|
|
xtype: 'displayfield',
|
|
value: `${gettext('Limit I/O bandwidth.')} ${Ext.String.format(gettext("Schema default: {0}"), 0)}`,
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveTwoColumnContainer',
|
|
startColumn: {
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'zstd',
|
|
reference: 'zstdThreadCount',
|
|
fieldLabel: Ext.String.format(gettext('{0} Threads'), 'Zstd'),
|
|
fieldStyle: 'text-align: right',
|
|
emptyText: gettext('Fallback'),
|
|
minValue: 0,
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('With 0, half of the available cores are used'),
|
|
},
|
|
},
|
|
endFlex: 2,
|
|
endColumn: {
|
|
xtype: 'displayfield',
|
|
value: `${gettext('Threads used for zstd compression (non-PBS).')} ${Ext.String.format(gettext("Schema default: {0}"), 1)}`,
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveTwoColumnContainer',
|
|
startColumn: {
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'max-workers',
|
|
minValue: 1,
|
|
maxValue: 256,
|
|
fieldLabel: gettext('IO-Workers'),
|
|
fieldStyle: 'text-align: right',
|
|
emptyText: gettext('Fallback'),
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
},
|
|
endFlex: 2,
|
|
endColumn: {
|
|
xtype: 'displayfield',
|
|
value: `${gettext('I/O workers in the QEMU process (VMs only).')} ${Ext.String.format(gettext("Schema default: {0}"), 16)}`,
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveTwoColumnContainer',
|
|
startColumn: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'fleecing-enabled',
|
|
reference: 'fleecingEnabled',
|
|
fieldLabel: gettext('Fleecing'),
|
|
uncheckedValue: 0,
|
|
value: 0,
|
|
},
|
|
endFlex: 2,
|
|
endColumn: {
|
|
xtype: 'displayfield',
|
|
value: gettext('Backup write cache that can reduce IO pressure inside guests (VMs only).'),
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveTwoColumnContainer',
|
|
startColumn: {
|
|
xtype: 'pveStorageSelector',
|
|
name: 'fleecing-storage',
|
|
fieldLabel: gettext('Fleecing Storage'),
|
|
reference: 'fleecingStorage',
|
|
clusterView: true,
|
|
storageContent: 'images',
|
|
allowBlank: false,
|
|
disabled: true,
|
|
},
|
|
endFlex: 2,
|
|
endColumn: {
|
|
xtype: 'displayfield',
|
|
value: gettext('Prefer a fast and local storage, ideally with support for discard and thin-provisioning or sparse files.'),
|
|
},
|
|
},
|
|
{
|
|
// It's part of the 'performance' property string, so have a field to preserve the
|
|
// value, but don't expose it. It's a rather niche setting and difficult to
|
|
// convey/understand what it does.
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'pbs-entries-max',
|
|
hidden: true,
|
|
fieldLabel: 'TODO',
|
|
fieldStyle: 'text-align: right',
|
|
emptyText: 'TODO',
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveTwoColumnContainer',
|
|
startColumn: {
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Repeat missed'),
|
|
name: 'repeat-missed',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
cbind: {
|
|
deleteDefaultValue: '{!isCreate}',
|
|
},
|
|
},
|
|
endFlex: 2,
|
|
endColumn: {
|
|
xtype: 'displayfield',
|
|
value: gettext("Run jobs as soon as possible if they couldn't start on schedule, for example, due to the node being offline."),
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveTwoColumnContainer',
|
|
startColumn: {
|
|
xtype: 'proxmoxKVComboBox',
|
|
fieldLabel: gettext('PBS change detection mode'),
|
|
name: 'pbs-change-detection-mode',
|
|
deleteEmpty: true,
|
|
value: '__default__',
|
|
comboItems: [
|
|
['__default__', "Default"],
|
|
['data', "Data"],
|
|
['metadata', "Metadata"],
|
|
],
|
|
},
|
|
endFlex: 2,
|
|
endColumn: {
|
|
xtype: 'displayfield',
|
|
value: gettext("EXPERIMENTAL: Mode to detect file changes and archive encoding format for container backups."),
|
|
},
|
|
},
|
|
{
|
|
xtype: 'component',
|
|
padding: '5 1',
|
|
html: `<span class="pmx-hint">${gettext('Note')}</span>: ${
|
|
gettext("The node-specific 'vzdump.conf' or, if this is not set, the default from the config schema is used to determine fallback values.")}`,
|
|
},
|
|
],
|
|
});
|
|
/*
|
|
* 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: [
|
|
'<h3>{title}</h3>',
|
|
'<i class="fa fa-5x {iconCls}"></i>',
|
|
'<br /><br/>',
|
|
'{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 caps = Ext.state.Manager.get('GuiCap');
|
|
let canEdit = !!caps.vms['VM.Config.Network'] || !!caps.dc['Sys.Modify'] || !!caps.nodes['Sys.Modify'];
|
|
|
|
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 || !canEdit) {
|
|
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,
|
|
enableFn: rec => canEdit,
|
|
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', {
|
|
enableFn: rec => canEdit,
|
|
selModel: sm,
|
|
baseurl: me.base_url + '/',
|
|
callback: reload,
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
tbar: ['<b>IPSet:</b>', me.addBtn, me.removeBtn, me.editBtn],
|
|
selModel: sm,
|
|
columns: [
|
|
{
|
|
header: 'IPSet',
|
|
dataIndex: 'name',
|
|
minWidth: 150,
|
|
flex: 1,
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 4,
|
|
},
|
|
],
|
|
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,
|
|
},
|
|
});
|
|
|
|
if (!canEdit) {
|
|
me.addBtn.setDisabled(true);
|
|
}
|
|
|
|
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,
|
|
allowBlank: false,
|
|
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 {
|
|
if (me.canEdit) {
|
|
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', {});
|
|
|
|
me.caps = Ext.state.Manager.get('GuiCap');
|
|
me.canEdit = !!me.caps.vms['VM.Config.Network'] || !!me.caps.dc['Sys.Modify'] || !!me.caps.nodes['Sys.Modify'];
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec || !me.canEdit) {
|
|
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,
|
|
enableFn: rec => me.canEdit,
|
|
selModel: sm,
|
|
handler: run_editor,
|
|
});
|
|
|
|
me.addBtn = new Proxmox.button.Button({
|
|
text: gettext('Add'),
|
|
disabled: true,
|
|
enableFn: rec => me.canEdit,
|
|
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', {
|
|
disabled: true,
|
|
enableFn: rec => me.canEdit,
|
|
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 = '<p>' + Ext.htmlEncode(msg) + '</p>';
|
|
metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' +
|
|
html.replace(/"/g, '"') + '"';
|
|
}
|
|
}
|
|
return value;
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
tbar: ['<b>IP/CIDR:</b>', me.addBtn, me.removeBtn, me.editBtn],
|
|
store: store,
|
|
selModel: sm,
|
|
listeners: {
|
|
itemdblclick: run_editor,
|
|
},
|
|
columns: [
|
|
{
|
|
xtype: 'rownumberer',
|
|
// cannot use width on instantiation as rownumberer hard-wires that in the
|
|
// constructor to avoid being overridden by applyDefaults
|
|
minWidth: 40,
|
|
},
|
|
{
|
|
header: gettext('IP/CIDR'),
|
|
dataIndex: 'cidr',
|
|
minWidth: 150,
|
|
flex: 1,
|
|
renderer: function(value, metaData, record) {
|
|
value = render_errors(value, metaData, record);
|
|
if (record.data.nomatch) {
|
|
return '<b>! </b>' + value;
|
|
}
|
|
return value;
|
|
},
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
flex: 3,
|
|
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: '<h3>{title}:</h3>',
|
|
},
|
|
{
|
|
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}<br />${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',
|
|
};
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
init: function(view) {
|
|
if (view.pveSelNode.data.type !== 'lxc') {
|
|
return;
|
|
}
|
|
|
|
const nodename = view.pveSelNode.data.node;
|
|
const vmid = view.pveSelNode.data.vmid;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: `/api2/extjs/nodes/${nodename}/lxc/${vmid}/config`,
|
|
waitMsgTargetView: view,
|
|
method: 'GET',
|
|
success: ({ result }) => {
|
|
view.down('#unprivileged').updateValue(
|
|
Proxmox.Utils.format_boolean(result.data.unprivileged));
|
|
view.ostype = Ext.htmlEncode(result.data.ostype);
|
|
},
|
|
});
|
|
},
|
|
},
|
|
|
|
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,
|
|
},
|
|
{
|
|
itemId: 'unprivileged',
|
|
iconCls: 'fa fa-lock fa-fw',
|
|
title: gettext('Unprivileged'),
|
|
printBar: false,
|
|
cbind: {
|
|
hidden: '{isQemu}',
|
|
},
|
|
},
|
|
{
|
|
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)
|
|
+ ')';
|
|
}
|
|
|
|
let title = `<div class="left-aligned">${me.getRecordValue('name') + text}</div>`;
|
|
|
|
if (me.pveSelNode.data.type === 'lxc' && me.ostype && me.ostype !== 'unmanaged') {
|
|
// Manual mappings for distros with special casing
|
|
const namemap = {
|
|
'archlinux': 'Arch Linux',
|
|
'nixos': 'NixOS',
|
|
'opensuse': 'openSUSE',
|
|
'centos': 'CentOS',
|
|
};
|
|
|
|
const distro = namemap[me.ostype] ?? Ext.String.capitalize(me.ostype);
|
|
title += `<div class="right-aligned">
|
|
<i class="fl-${me.ostype} fl-fw"></i> ${distro}</div>`;
|
|
}
|
|
|
|
me.setTitle(title);
|
|
},
|
|
});
|
|
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 = ' <i class="fa warning fa-warning"></i>';
|
|
}
|
|
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;
|
|
}
|
|
},
|
|
|
|
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 = '<div class="usage-wrapper">';
|
|
status += `<div class="usage-negative" style="height: ${remainingHeight}%"></div>`;
|
|
status += `<div class="usage" style="height: ${barHeight}%"></div>`;
|
|
status += '</div> ';
|
|
}
|
|
}
|
|
if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
|
|
if (PVE.UIOptions.getTreeSortingValue('sort-field') !== 'vmid') {
|
|
info.text = `${info.name} (${String(info.vmid)})`;
|
|
}
|
|
}
|
|
info.text = `<span>${status}${info.text}</span>`;
|
|
info.text += PVE.Utils.renderTags(info.tags, PVE.UIOptions.tagOverrides);
|
|
},
|
|
|
|
getToolTip: function(info) {
|
|
if (info.type === 'pool' || info.groupbyid !== undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
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);
|
|
}
|
|
if (info.type === 'storage') {
|
|
let usage = info.disk / info.maxdisk;
|
|
if (usage >= 0.0 && usage <= 1.0) {
|
|
qtips.push(Ext.String.format(gettext("Usage: {0}%"), (usage*100).toFixed(2)));
|
|
}
|
|
}
|
|
|
|
let tip = qtips.join(', ');
|
|
info.tip = tip;
|
|
return tip;
|
|
},
|
|
|
|
// private
|
|
addChildSorted: function(node, info) {
|
|
let me = this;
|
|
|
|
me.setIconCls(info);
|
|
me.setText(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);
|
|
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,
|
|
afterrender: function() {
|
|
if (me.tip) {
|
|
return;
|
|
}
|
|
let selectors = [
|
|
'.x-tree-node-text > span:not(.proxmox-tag-dark):not(.proxmox-tag-light)',
|
|
'.x-tree-icon',
|
|
];
|
|
me.tip = Ext.create('Ext.tip.ToolTip', {
|
|
target: me.el,
|
|
delegate: selectors.join(', '),
|
|
trackMouse: true,
|
|
renderTo: Ext.getBody(),
|
|
listeners: {
|
|
beforeshow: function(tip) {
|
|
let rec = me.getView().getRecord(tip.triggerElement);
|
|
let tipText = me.getToolTip(rec.data);
|
|
if (tipText) {
|
|
tip.update(tipText);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
},
|
|
});
|
|
},
|
|
},
|
|
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.tree.ResourceMapTree', {
|
|
extend: 'Ext.tree.Panel',
|
|
alias: 'widget.pveResourceMapTree',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
rootVisible: false,
|
|
|
|
emptyText: gettext('No Mapping found'),
|
|
|
|
// will be opened on edit
|
|
editWindowClass: undefined,
|
|
|
|
// The base url of the resource
|
|
baseUrl: undefined,
|
|
|
|
// icon class to show on the entries
|
|
mapIconCls: undefined,
|
|
|
|
// if given, should be a function that takes a nodename and returns
|
|
// the url for getting the data to check the status
|
|
getStatusCheckUrl: undefined,
|
|
|
|
// the result of above api call and the nodename is passed and can set the status
|
|
checkValidity: undefined,
|
|
|
|
// the property that denotes a single map entry for a node
|
|
entryIdProperty: undefined,
|
|
|
|
cbindData: function(initialConfig) {
|
|
let me = this;
|
|
const caps = Ext.state.Manager.get('GuiCap');
|
|
me.canConfigure = !!caps.mapping['Mapping.Modify'];
|
|
|
|
return {};
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
addMapping: function() {
|
|
let me = this;
|
|
let view = me.getView();
|
|
Ext.create(view.editWindowClass, {
|
|
url: view.baseUrl,
|
|
autoShow: true,
|
|
listeners: {
|
|
destroy: () => me.load(),
|
|
},
|
|
});
|
|
},
|
|
|
|
add: function(_grid, _rI, _cI, _item, _e, rec) {
|
|
let me = this;
|
|
if (rec.data.type !== 'entry') {
|
|
return;
|
|
}
|
|
|
|
me.openMapEditWindow(rec.data.name);
|
|
},
|
|
|
|
editDblClick: function() {
|
|
let me = this;
|
|
let view = me.getView();
|
|
let selection = view.getSelection();
|
|
if (!selection || selection.length < 1) {
|
|
return;
|
|
}
|
|
|
|
me.edit(selection[0]);
|
|
},
|
|
|
|
editAction: function(_grid, _rI, _cI, _item, _e, rec) {
|
|
this.edit(rec);
|
|
},
|
|
|
|
edit: function(rec) {
|
|
let me = this;
|
|
if (rec.data.type === 'map') {
|
|
return;
|
|
}
|
|
|
|
me.openMapEditWindow(rec.data.name, rec.data.node, rec.data.type === 'entry');
|
|
},
|
|
|
|
openMapEditWindow: function(name, nodename, entryOnly) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
|
|
Ext.create(view.editWindowClass, {
|
|
url: `${view.baseUrl}/${name}`,
|
|
autoShow: true,
|
|
autoLoad: true,
|
|
entryOnly,
|
|
nodename,
|
|
name,
|
|
listeners: {
|
|
destroy: () => me.load(),
|
|
},
|
|
});
|
|
},
|
|
|
|
remove: function(_grid, _rI, _cI, _item, _e, rec) {
|
|
let me = this;
|
|
let msg, id;
|
|
let view = me.getView();
|
|
let confirmMsg;
|
|
switch (rec.data.type) {
|
|
case 'entry':
|
|
msg = gettext("Are you sure you want to remove '{0}'");
|
|
confirmMsg = Ext.String.format(msg, rec.data.name);
|
|
break;
|
|
case 'node':
|
|
msg = gettext("Are you sure you want to remove '{0}' entries for '{1}'");
|
|
confirmMsg = Ext.String.format(msg, rec.data.node, rec.data.name);
|
|
break;
|
|
case 'map':
|
|
msg = gettext("Are you sure you want to remove '{0}' on '{1}' for '{2}'");
|
|
id = rec.data[view.entryIdProperty];
|
|
confirmMsg = Ext.String.format(msg, id, rec.data.node, rec.data.name);
|
|
break;
|
|
default:
|
|
throw "invalid type";
|
|
}
|
|
Ext.Msg.confirm(gettext('Confirm'), confirmMsg, function(btn) {
|
|
if (btn === 'yes') {
|
|
me.executeRemove(rec.data);
|
|
}
|
|
});
|
|
},
|
|
|
|
executeRemove: function(data) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
|
|
let url = `${view.baseUrl}/${data.name}`;
|
|
let method = 'PUT';
|
|
let params = {
|
|
digest: me.lookup[data.name].digest,
|
|
};
|
|
let map = me.lookup[data.name].map;
|
|
switch (data.type) {
|
|
case 'entry':
|
|
method = 'DELETE';
|
|
params = undefined;
|
|
break;
|
|
case 'node':
|
|
params.map = PVE.Parser.filterPropertyStringList(map, (e) => e.node !== data.node);
|
|
break;
|
|
case 'map':
|
|
params.map = PVE.Parser.filterPropertyStringList(map, (e) =>
|
|
Object.entries(e).some(([key, value]) => data[key] !== value));
|
|
break;
|
|
default:
|
|
throw "invalid type";
|
|
}
|
|
if (!params?.map.length) {
|
|
method = 'DELETE';
|
|
params = undefined;
|
|
}
|
|
Proxmox.Utils.API2Request({
|
|
url,
|
|
method,
|
|
params,
|
|
success: function() {
|
|
me.load();
|
|
},
|
|
});
|
|
},
|
|
|
|
load: function() {
|
|
let me = this;
|
|
let view = me.getView();
|
|
Proxmox.Utils.API2Request({
|
|
url: view.baseUrl,
|
|
method: 'GET',
|
|
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
|
success: function({ result: { data } }) {
|
|
let lookup = {};
|
|
data.forEach((entry) => {
|
|
lookup[entry.id] = Ext.apply({}, entry);
|
|
entry.iconCls = 'fa fa-fw fa-folder-o';
|
|
entry.name = entry.id;
|
|
entry.text = entry.id;
|
|
entry.type = 'entry';
|
|
|
|
let nodes = {};
|
|
for (const map of entry.map) {
|
|
let parsed = PVE.Parser.parsePropertyString(map);
|
|
parsed.iconCls = view.mapIconCls;
|
|
parsed.leaf = true;
|
|
parsed.name = entry.id;
|
|
parsed.text = parsed[view.entryIdProperty];
|
|
parsed.type = 'map';
|
|
|
|
if (nodes[parsed.node] === undefined) {
|
|
nodes[parsed.node] = {
|
|
children: [],
|
|
expanded: true,
|
|
iconCls: 'fa fa-fw fa-building-o',
|
|
leaf: false,
|
|
name: entry.id,
|
|
node: parsed.node,
|
|
text: parsed.node,
|
|
type: 'node',
|
|
};
|
|
}
|
|
nodes[parsed.node].children.push(parsed);
|
|
}
|
|
delete entry.id;
|
|
entry.children = Object.values(nodes);
|
|
entry.leaf = entry.children.length === 0;
|
|
});
|
|
me.lookup = lookup;
|
|
if (view.getStatusCheckUrl !== undefined && view.checkValidity !== undefined) {
|
|
me.loadStatusData();
|
|
}
|
|
view.setRootNode({
|
|
children: data,
|
|
});
|
|
let root = view.getRootNode();
|
|
root.expand();
|
|
root.childNodes.forEach(node => node.expand());
|
|
},
|
|
});
|
|
},
|
|
|
|
nodeLoadingState: {},
|
|
|
|
loadStatusData: function() {
|
|
let me = this;
|
|
let view = me.getView();
|
|
PVE.data.ResourceStore.getNodes().forEach(({ node }) => {
|
|
me.nodeLoadingState[node] = true;
|
|
let url = view.getStatusCheckUrl(node);
|
|
Proxmox.Utils.API2Request({
|
|
url,
|
|
method: 'GET',
|
|
failure: function(response) {
|
|
me.nodeLoadingState[node] = false;
|
|
view.getRootNode()?.cascade(function(rec) {
|
|
if (rec.data.node !== node) {
|
|
return;
|
|
}
|
|
|
|
rec.set('valid', 0);
|
|
rec.set('errmsg', response.htmlStatus);
|
|
rec.commit();
|
|
});
|
|
},
|
|
success: function({ result: { data } }) {
|
|
me.nodeLoadingState[node] = false;
|
|
view.checkValidity(data, node);
|
|
},
|
|
});
|
|
});
|
|
},
|
|
|
|
renderStatus: function(value, _metadata, record) {
|
|
let me = this;
|
|
if (record.data.type !== 'map') {
|
|
return '';
|
|
}
|
|
let iconCls;
|
|
let status;
|
|
if (value === undefined) {
|
|
if (me.nodeLoadingState[record.data.node]) {
|
|
iconCls = 'fa-spinner fa-spin';
|
|
status = gettext('Loading...');
|
|
} else {
|
|
iconCls = 'fa-question-circle';
|
|
status = gettext('Unknown Node');
|
|
}
|
|
} else {
|
|
let state = value ? 'good' : 'critical';
|
|
iconCls = PVE.Utils.get_health_icon(state, true);
|
|
status = value ? gettext("Mapping matches host data") : record.data.errmsg || Proxmox.Utils.unknownText;
|
|
}
|
|
return `<i class="fa ${iconCls}"></i> ${status}`;
|
|
},
|
|
|
|
getAddClass: function(v, mD, rec) {
|
|
let cls = 'fa fa-plus-circle';
|
|
if (rec.data.type !== 'entry' || rec.data.children?.length >= PVE.data.ResourceStore.getNodes().length) {
|
|
cls += ' pmx-action-hidden';
|
|
}
|
|
return cls;
|
|
},
|
|
|
|
isAddDisabled: function(v, r, c, i, rec) {
|
|
return rec.data.type !== 'entry' || rec.data.children?.length >= PVE.data.ResourceStore.getNodes().length;
|
|
},
|
|
|
|
init: function(view) {
|
|
let me = this;
|
|
|
|
['editWindowClass', 'baseUrl', 'mapIconCls', 'entryIdProperty'].forEach((property) => {
|
|
if (view[property] === undefined) {
|
|
throw `No ${property} defined`;
|
|
}
|
|
});
|
|
|
|
me.load();
|
|
},
|
|
},
|
|
|
|
store: {
|
|
sorters: 'text',
|
|
data: {},
|
|
},
|
|
|
|
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
handler: 'addMapping',
|
|
cbind: {
|
|
disabled: '{!canConfigure}',
|
|
},
|
|
},
|
|
],
|
|
|
|
listeners: {
|
|
itemdblclick: 'editDblClick',
|
|
},
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
let columns = [...me.columns];
|
|
columns.splice(1, 0, {
|
|
xtype: 'actioncolumn',
|
|
text: gettext('Actions'),
|
|
width: 80,
|
|
items: [
|
|
{
|
|
getTip: (v, m, { data }) =>
|
|
Ext.String.format(gettext("Add new host mapping for '{0}'"), data.name),
|
|
getClass: 'getAddClass',
|
|
isActionDisabled: 'isAddDisabled',
|
|
handler: 'add',
|
|
},
|
|
{
|
|
iconCls: 'fa fa-pencil',
|
|
getTip: (v, m, { data }) => data.type === 'entry'
|
|
? Ext.String.format(gettext("Edit Mapping '{0}'"), data.name)
|
|
: Ext.String.format(gettext("Edit Mapping '{0}' for '{1}'"), data.name, data.node),
|
|
getClass: (v, m, { data }) => data.type !== 'map' ? 'fa fa-pencil' : 'pmx-hidden',
|
|
isActionDisabled: (v, r, c, i, rec) => rec.data.type === 'map',
|
|
handler: 'editAction',
|
|
},
|
|
{
|
|
iconCls: 'fa fa-trash-o',
|
|
getTip: (v, m, { data }) => data.type === 'entry'
|
|
? Ext.String.format(gettext("Remove '{0}'"), data.name)
|
|
: data.type === 'node'
|
|
? Ext.String.format(gettext("Remove mapping for '{0}'"), data.node)
|
|
: Ext.String.format(gettext("Remove mapping '{0}'"), data.path),
|
|
handler: 'remove',
|
|
},
|
|
],
|
|
});
|
|
me.columns = columns;
|
|
|
|
me.callParent();
|
|
},
|
|
});
|
|
Ext.define('PVE.sdn.DhcpTree', {
|
|
extend: 'Ext.tree.Panel',
|
|
xtype: 'pveDhcpTree',
|
|
|
|
layout: 'fit',
|
|
rootVisible: false,
|
|
animate: false,
|
|
|
|
store: {
|
|
sorters: ['ip', 'name'],
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
reload: function() {
|
|
let me = this;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: `/cluster/sdn/ipams/pve/status`,
|
|
method: 'GET',
|
|
success: function(response, opts) {
|
|
let root = {
|
|
name: '__root',
|
|
expanded: true,
|
|
children: [],
|
|
};
|
|
|
|
let zones = {};
|
|
let vnets = {};
|
|
let subnets = {};
|
|
|
|
response.result.data.forEach((element) => {
|
|
element.leaf = true;
|
|
|
|
if (!(element.zone in zones)) {
|
|
let zone = {
|
|
name: element.zone,
|
|
type: 'zone',
|
|
iconCls: 'fa fa-th',
|
|
expanded: true,
|
|
children: [],
|
|
};
|
|
|
|
zones[element.zone] = zone;
|
|
root.children.push(zone);
|
|
}
|
|
|
|
if (!(element.vnet in vnets)) {
|
|
let vnet = {
|
|
name: element.vnet,
|
|
zone: element.zone,
|
|
type: 'vnet',
|
|
iconCls: 'fa fa-network-wired x-fa-treepanel',
|
|
expanded: true,
|
|
children: [],
|
|
};
|
|
|
|
vnets[element.vnet] = vnet;
|
|
zones[element.zone].children.push(vnet);
|
|
}
|
|
|
|
if (!(element.subnet in subnets)) {
|
|
let subnet = {
|
|
name: element.subnet,
|
|
zone: element.zone,
|
|
vnet: element.vnet,
|
|
type: 'subnet',
|
|
iconCls: 'x-tree-icon-none',
|
|
expanded: true,
|
|
children: [],
|
|
};
|
|
|
|
subnets[element.subnet] = subnet;
|
|
vnets[element.vnet].children.push(subnet);
|
|
}
|
|
|
|
element.type = 'mapping';
|
|
element.iconCls = 'x-tree-icon-none';
|
|
subnets[element.subnet].children.push(element);
|
|
});
|
|
|
|
me.getView().setRootNode(root);
|
|
},
|
|
});
|
|
},
|
|
|
|
init: function(view) {
|
|
let me = this;
|
|
me.reload();
|
|
},
|
|
|
|
onDelete: function(table, rI, cI, item, e, { data }) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
|
|
Ext.Msg.show({
|
|
title: gettext('Confirm'),
|
|
icon: Ext.Msg.WARNING,
|
|
message: Ext.String.format(gettext('Are you sure you want to remove DHCP mapping {0}'), `${data.mac} / ${data.ip}`),
|
|
buttons: Ext.Msg.YESNO,
|
|
defaultFocus: 'no',
|
|
callback: function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
|
|
let params = {
|
|
zone: data.zone,
|
|
mac: data.mac,
|
|
ip: data.ip,
|
|
};
|
|
|
|
let encodedParams = Ext.Object.toQueryString(params);
|
|
|
|
let url = `/cluster/sdn/vnets/${data.vnet}/ips?${encodedParams}`;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url,
|
|
method: 'DELETE',
|
|
waitMsgTarget: view,
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
callback: me.reload.bind(me),
|
|
});
|
|
},
|
|
});
|
|
},
|
|
|
|
editAction: function(_grid, _rI, _cI, _item, _e, rec) {
|
|
this.edit(rec);
|
|
},
|
|
|
|
editDblClick: function() {
|
|
let me = this;
|
|
|
|
let view = me.getView();
|
|
let selection = view.getSelection();
|
|
|
|
if (!selection || selection.length < 1) {
|
|
return;
|
|
}
|
|
|
|
me.edit(selection[0]);
|
|
},
|
|
|
|
edit: function(rec) {
|
|
let me = this;
|
|
|
|
if (rec.data.type === 'mapping' && !rec.data.gateway) {
|
|
me.openEditWindow(rec.data);
|
|
}
|
|
},
|
|
|
|
openEditWindow: function(data) {
|
|
let me = this;
|
|
|
|
Ext.create('PVE.sdn.IpamEdit', {
|
|
autoShow: true,
|
|
mapping: data,
|
|
extraRequestParams: {
|
|
vmid: data.vmid,
|
|
mac: data.mac,
|
|
zone: data.zone,
|
|
vnet: data.vnet,
|
|
},
|
|
listeners: {
|
|
destroy: () => me.reload(),
|
|
},
|
|
});
|
|
},
|
|
},
|
|
|
|
listeners: {
|
|
itemdblclick: 'editDblClick',
|
|
},
|
|
|
|
tbar: [
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Reload'),
|
|
handler: 'reload',
|
|
},
|
|
],
|
|
|
|
columns: [
|
|
{
|
|
xtype: 'treecolumn',
|
|
text: gettext('Name / VMID'),
|
|
dataIndex: 'name',
|
|
width: 200,
|
|
renderer: function(value, meta, record) {
|
|
if (record.get('gateway')) {
|
|
return gettext('Gateway');
|
|
}
|
|
|
|
return record.get('name') ?? record.get('vmid') ?? ' ';
|
|
},
|
|
},
|
|
{
|
|
text: gettext('IP Address'),
|
|
dataIndex: 'ip',
|
|
width: 200,
|
|
},
|
|
{
|
|
text: 'MAC',
|
|
dataIndex: 'mac',
|
|
width: 200,
|
|
},
|
|
{
|
|
text: gettext('Gateway'),
|
|
dataIndex: 'gateway',
|
|
width: 200,
|
|
},
|
|
{
|
|
header: gettext('Actions'),
|
|
xtype: 'actioncolumn',
|
|
dataIndex: 'text',
|
|
width: 150,
|
|
items: [
|
|
{
|
|
handler: function(table, rI, cI, item, e, { data }) {
|
|
let me = this;
|
|
|
|
Ext.create('PVE.sdn.IpamEdit', {
|
|
autoShow: true,
|
|
mapping: {},
|
|
isCreate: true,
|
|
extraRequestParams: {
|
|
vnet: data.name,
|
|
zone: data.zone,
|
|
},
|
|
listeners: {
|
|
destroy: () => {
|
|
me.up('pveDhcpTree').controller.reload();
|
|
},
|
|
},
|
|
});
|
|
},
|
|
getTip: (v, m, rec) => gettext('Add'),
|
|
getClass: (v, m, { data }) => {
|
|
if (data.type === 'vnet') {
|
|
return 'fa fa-plus-square';
|
|
}
|
|
|
|
return 'pmx-hidden';
|
|
},
|
|
},
|
|
{
|
|
handler: 'editAction',
|
|
getTip: (v, m, rec) => gettext('Edit'),
|
|
getClass: (v, m, { data }) => {
|
|
if (data.type === 'mapping' && !data.gateway) {
|
|
return 'fa fa-pencil fa-fw';
|
|
}
|
|
|
|
return 'pmx-hidden';
|
|
},
|
|
},
|
|
{
|
|
handler: 'onDelete',
|
|
getTip: (v, m, rec) => gettext('Delete'),
|
|
getClass: (v, m, { data }) => {
|
|
if (data.type === 'mapping' && !data.gateway) {
|
|
return 'fa critical fa-trash-o';
|
|
}
|
|
|
|
return 'pmx-hidden';
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
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.BackupCompressionSelector', {
|
|
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,
|
|
});
|
|
|
|
let notificationModeSelector = Ext.create({
|
|
xtype: 'proxmoxKVComboBox',
|
|
comboItems: [
|
|
['auto', gettext('Auto')],
|
|
['legacy-sendmail', gettext('Email (legacy)')],
|
|
['notification-system', gettext('Notification system')],
|
|
],
|
|
fieldLabel: gettext('Notification mode'),
|
|
name: 'notification-mode',
|
|
value: 'auto',
|
|
listeners: {
|
|
change: function(field, value) {
|
|
mailtoField.setDisabled(value === 'notification-system');
|
|
},
|
|
},
|
|
});
|
|
|
|
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['notification-mode'] !== undefined) {
|
|
notificationModeSelector.setValue(data['notification-mode']);
|
|
}
|
|
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,
|
|
notificationModeSelector,
|
|
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 => `<code>{{${v}}}</code>`).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['notification-mode']) {
|
|
params['notification-mode'] = values['notification-mode'];
|
|
}
|
|
|
|
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`, `suspendall`
|
|
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: 'fieldcontainer',
|
|
layout: 'hbox',
|
|
items: [{
|
|
flex: 1,
|
|
xtype: 'pveNodeSelector',
|
|
name: 'target',
|
|
disallowedNodes: [me.nodename],
|
|
fieldLabel: gettext('Target node'),
|
|
labelWidth: 200,
|
|
allowBlank: false,
|
|
onlineValidator: true,
|
|
padding: '0 10 0 0',
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'maxworkers',
|
|
minValue: 1,
|
|
maxValue: 100,
|
|
value: 1,
|
|
fieldLabel: gettext('Parallel jobs'),
|
|
allowBlank: false,
|
|
flex: 1,
|
|
}],
|
|
},
|
|
{
|
|
xtype: 'fieldcontainer',
|
|
layout: 'hbox',
|
|
items: [{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Allow local disk migration'),
|
|
name: 'with-local-disks',
|
|
labelWidth: 200,
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
flex: 1,
|
|
padding: '0 10 0 0',
|
|
},
|
|
{
|
|
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
|
|
flex: 1,
|
|
}],
|
|
},
|
|
);
|
|
} else if (me.action === 'startall') {
|
|
items.push({
|
|
xtype: 'hiddenfield',
|
|
name: 'force',
|
|
value: 1,
|
|
});
|
|
} else if (me.action === 'stopall') {
|
|
items.push({
|
|
xtype: 'fieldcontainer',
|
|
layout: 'hbox',
|
|
items: [{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'force-stop',
|
|
labelWidth: 120,
|
|
fieldLabel: gettext('Force Stop'),
|
|
boxLabel: gettext('Force stop guest if shutdown times out.'),
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
flex: 1,
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'timeout',
|
|
fieldLabel: gettext('Timeout (s)'),
|
|
labelWidth: 120,
|
|
emptyText: '180',
|
|
minValue: 0,
|
|
maxValue: 7200,
|
|
allowBlank: true,
|
|
flex: 1,
|
|
}],
|
|
});
|
|
}
|
|
|
|
let refreshLxcWarning = function(vmids, records) {
|
|
let showWarning = records.some(
|
|
item => vmids.includes(item.data.vmid) && item.data.type === 'lxc' && item.data.status === 'running',
|
|
);
|
|
me.down('#lxcwarning').setVisible(showWarning);
|
|
};
|
|
|
|
let defaultStatus = me.action === 'migrateall' ? '' : me.action === 'startall' ? 'stopped' : 'running';
|
|
let defaultType = me.action === 'suspendall' ? 'qemu' : '';
|
|
|
|
let statusMap = [];
|
|
let poolMap = [];
|
|
let haMap = [];
|
|
let tagMap = [];
|
|
PVE.data.ResourceStore.each((rec) => {
|
|
if (['qemu', 'lxc'].indexOf(rec.data.type) !== -1) {
|
|
statusMap[rec.data.status] = true;
|
|
}
|
|
if (rec.data.type === 'pool') {
|
|
poolMap[rec.data.pool] = true;
|
|
}
|
|
if (rec.data.hastate !== "") {
|
|
haMap[rec.data.hastate] = true;
|
|
}
|
|
if (rec.data.tags !== "") {
|
|
rec.data.tags.split(/[,; ]/).forEach((tag) => {
|
|
if (tag !== '') {
|
|
tagMap[tag] = true;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
let statusList = Object.keys(statusMap).map(key => [key, key]);
|
|
statusList.unshift(['', gettext('All')]);
|
|
let poolList = Object.keys(poolMap).map(key => [key, key]);
|
|
let tagList = Object.keys(tagMap).map(key => ({ value: key }));
|
|
let haList = Object.keys(haMap).map(key => [key, key]);
|
|
|
|
let clearFilters = function() {
|
|
me.down('#namefilter').setValue('');
|
|
['name', 'status', 'pool', 'type', 'hastate', 'includetag', 'excludetag', 'vmid'].forEach((filter) => {
|
|
me.down(`#${filter}filter`).setValue('');
|
|
});
|
|
};
|
|
|
|
let filterChange = function() {
|
|
let nameValue = me.down('#namefilter').getValue();
|
|
let filterCount = 0;
|
|
|
|
if (nameValue !== '') {
|
|
filterCount++;
|
|
}
|
|
|
|
let arrayFiltersData = [];
|
|
['pool', 'hastate'].forEach((filter) => {
|
|
let selected = me.down(`#${filter}filter`).getValue() ?? [];
|
|
if (selected.length) {
|
|
filterCount++;
|
|
arrayFiltersData.push([filter, [...selected]]);
|
|
}
|
|
});
|
|
|
|
let singleFiltersData = [];
|
|
['status', 'type'].forEach((filter) => {
|
|
let selected = me.down(`#${filter}filter`).getValue() ?? '';
|
|
if (selected.length) {
|
|
filterCount++;
|
|
singleFiltersData.push([filter, selected]);
|
|
}
|
|
});
|
|
|
|
let includeTags = me.down('#includetagfilter').getValue() ?? [];
|
|
if (includeTags.length) {
|
|
filterCount++;
|
|
}
|
|
let excludeTags = me.down('#excludetagfilter').getValue() ?? [];
|
|
if (excludeTags.length) {
|
|
filterCount++;
|
|
}
|
|
|
|
let fieldSet = me.down('#filters');
|
|
let clearBtn = me.down('#clearBtn');
|
|
if (filterCount) {
|
|
fieldSet.setTitle(Ext.String.format(gettext('Filters ({0})'), filterCount));
|
|
clearBtn.setDisabled(false);
|
|
} else {
|
|
fieldSet.setTitle(gettext('Filters'));
|
|
clearBtn.setDisabled(true);
|
|
}
|
|
|
|
let filterFn = function(value) {
|
|
let name = value.data.name.toLowerCase().indexOf(nameValue.toLowerCase()) !== -1;
|
|
let arrayFilters = arrayFiltersData.every(([filter, selected]) =>
|
|
!selected.length || selected.indexOf(value.data[filter]) !== -1);
|
|
let singleFilters = singleFiltersData.every(([filter, selected]) =>
|
|
!selected.length || value.data[filter].indexOf(selected) !== -1);
|
|
let tags = value.data.tags.split(/[;, ]/).filter(t => !!t);
|
|
let includeFilter = !includeTags.length || tags.some(tag => includeTags.indexOf(tag) !== -1);
|
|
let excludeFilter = !excludeTags.length || tags.every(tag => excludeTags.indexOf(tag) === -1);
|
|
|
|
return name && arrayFilters && singleFilters && includeFilter && excludeFilter;
|
|
};
|
|
let vmselector = me.down('#vms');
|
|
vmselector.getStore().setFilters({
|
|
id: 'customFilter',
|
|
filterFn,
|
|
});
|
|
vmselector.checkChange();
|
|
if (me.action === 'migrateall') {
|
|
let records = vmselector.getSelection();
|
|
refreshLxcWarning(vmselector.getValue(), records);
|
|
}
|
|
};
|
|
|
|
items.push({
|
|
xtype: 'fieldset',
|
|
itemId: 'filters',
|
|
collapsible: true,
|
|
title: gettext('Filters'),
|
|
layout: 'hbox',
|
|
items: [
|
|
{
|
|
xtype: 'container',
|
|
flex: 1,
|
|
padding: 5,
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch',
|
|
},
|
|
defaults: {
|
|
listeners: {
|
|
change: filterChange,
|
|
},
|
|
isFormField: false,
|
|
},
|
|
items: [
|
|
{
|
|
fieldLabel: gettext("Name"),
|
|
itemId: 'namefilter',
|
|
xtype: 'textfield',
|
|
},
|
|
{
|
|
xtype: 'combobox',
|
|
itemId: 'statusfilter',
|
|
fieldLabel: gettext("Status"),
|
|
emptyText: gettext('All'),
|
|
editable: false,
|
|
value: defaultStatus,
|
|
store: statusList,
|
|
},
|
|
{
|
|
xtype: 'combobox',
|
|
itemId: 'poolfilter',
|
|
fieldLabel: gettext("Pool"),
|
|
emptyText: gettext('All'),
|
|
editable: false,
|
|
multiSelect: true,
|
|
store: poolList,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch',
|
|
},
|
|
flex: 1,
|
|
padding: 5,
|
|
defaults: {
|
|
listeners: {
|
|
change: filterChange,
|
|
},
|
|
isFormField: false,
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'combobox',
|
|
itemId: 'typefilter',
|
|
fieldLabel: gettext("Type"),
|
|
emptyText: gettext('All'),
|
|
editable: false,
|
|
value: defaultType,
|
|
store: [
|
|
['', gettext('All')],
|
|
['lxc', gettext('CT')],
|
|
['qemu', gettext('VM')],
|
|
],
|
|
},
|
|
{
|
|
xtype: 'proxmoxComboGrid',
|
|
itemId: 'includetagfilter',
|
|
fieldLabel: gettext("Include Tags"),
|
|
emptyText: gettext('All'),
|
|
editable: false,
|
|
multiSelect: true,
|
|
valueField: 'value',
|
|
displayField: 'value',
|
|
listConfig: {
|
|
userCls: 'proxmox-tags-full',
|
|
columns: [
|
|
{
|
|
dataIndex: 'value',
|
|
flex: 1,
|
|
renderer: value =>
|
|
PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
|
|
},
|
|
],
|
|
},
|
|
store: {
|
|
data: tagList,
|
|
},
|
|
listeners: {
|
|
change: filterChange,
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxComboGrid',
|
|
itemId: 'excludetagfilter',
|
|
fieldLabel: gettext("Exclude Tags"),
|
|
emptyText: gettext('None'),
|
|
multiSelect: true,
|
|
editable: false,
|
|
valueField: 'value',
|
|
displayField: 'value',
|
|
listConfig: {
|
|
userCls: 'proxmox-tags-full',
|
|
columns: [
|
|
{
|
|
dataIndex: 'value',
|
|
flex: 1,
|
|
renderer: value =>
|
|
PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
|
|
},
|
|
],
|
|
},
|
|
store: {
|
|
data: tagList,
|
|
},
|
|
listeners: {
|
|
change: filterChange,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch',
|
|
},
|
|
flex: 1,
|
|
padding: 5,
|
|
defaults: {
|
|
listeners: {
|
|
change: filterChange,
|
|
},
|
|
isFormField: false,
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'combobox',
|
|
itemId: 'hastatefilter',
|
|
fieldLabel: gettext("HA status"),
|
|
emptyText: gettext('All'),
|
|
multiSelect: true,
|
|
editable: false,
|
|
store: haList,
|
|
listeners: {
|
|
change: filterChange,
|
|
},
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'end',
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'button',
|
|
itemId: 'clearBtn',
|
|
text: gettext('Clear Filters'),
|
|
disabled: true,
|
|
handler: clearFilters,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
items.push({
|
|
xtype: 'vmselector',
|
|
itemId: 'vms',
|
|
name: 'vms',
|
|
flex: 1,
|
|
height: 300,
|
|
selectAll: true,
|
|
allowBlank: false,
|
|
plugins: '',
|
|
nodename: me.nodename,
|
|
listeners: {
|
|
selectionchange: function(vmselector, records) {
|
|
if (me.action === 'migrateall') {
|
|
let vmids = me.down('#vms').getValue();
|
|
refreshLxcWarning(vmids, records);
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch',
|
|
},
|
|
fieldDefaults: {
|
|
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();
|
|
|
|
filterChange();
|
|
},
|
|
});
|
|
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 `<p class="install-mask">
|
|
${Ext.String.format(gettext('{0} is not initialized.'), 'Ceph')}
|
|
${gettext('You need to create an initial config once.')}</p>`;
|
|
} else {
|
|
return '<p class="install-mask">' +
|
|
Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + '<br>' +
|
|
gettext('Would you like to install it now?') + '</p>';
|
|
}
|
|
},
|
|
},
|
|
},
|
|
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.lookup('diskselector').getComponent('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',
|
|
vtype: 'DnsName',
|
|
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: '<div style="margin: auto; padding: 2.5px;"><b>/</b></div>',
|
|
},
|
|
{
|
|
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.') + `<br>${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') + `<br>${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',
|
|
inputAttrTpl: 'autocomplete=username',
|
|
bind: {
|
|
visible: "{!openid}",
|
|
disabled: "{openid}",
|
|
},
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
inputType: 'password',
|
|
fieldLabel: gettext('Password'),
|
|
name: 'password',
|
|
reference: 'passwordField',
|
|
inputAttrTpl: 'autocomplete=current-password',
|
|
bind: {
|
|
visible: "{!openid}",
|
|
disabled: "{openid}",
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pmxRealmComboBox',
|
|
name: 'realm',
|
|
},
|
|
{
|
|
xtype: 'proxmoxLanguageSelector',
|
|
fieldLabel: gettext('Language'),
|
|
value: PVE.Utils.getUiLanguage(),
|
|
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: async 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);
|
|
}
|
|
|
|
me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
|
|
|
|
if (vm.get('vmtype') === 'qemu') {
|
|
await me.checkQemuPreconditions(resetMigrationPossible);
|
|
} else {
|
|
me.checkLxcPreconditions(resetMigrationPossible);
|
|
}
|
|
|
|
// 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] ?? {};
|
|
if (disallowed.unavailable_storages !== undefined) {
|
|
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 (disallowed['unavailable-resources'] !== undefined) {
|
|
let unavailableResources = disallowed['unavailable-resources'].join(', ');
|
|
|
|
migration.possible = false;
|
|
migration.preconditions.push({
|
|
text: 'Mapped Resources (' + unavailableResources + ') not available on selected target. ',
|
|
severity: 'error',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let blockingResources = [];
|
|
let mappedResources = migrateStats['mapped-resources'] ?? [];
|
|
|
|
for (const res of migrateStats.local_resources) {
|
|
if (mappedResources.indexOf(res) === -1) {
|
|
blockingResources.push(res);
|
|
}
|
|
}
|
|
|
|
if (blockingResources.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}',
|
|
blockingResources.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.',
|
|
blockingResources.join(', ')),
|
|
severity: 'warning',
|
|
});
|
|
}
|
|
}
|
|
|
|
if (mappedResources && mappedResources.length) {
|
|
if (vm.get('running')) {
|
|
migration.possible = false;
|
|
migration.preconditions.push({
|
|
text: Ext.String.format('Can\'t migrate running VM with mapped resources: {0}',
|
|
mappedResources.join(', ')),
|
|
severity: 'error',
|
|
});
|
|
}
|
|
}
|
|
|
|
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 '<i class="fa fa-exclamation-triangle warning"></i> ';
|
|
case 'error':
|
|
return '<i class="fa fa-times critical"></i>';
|
|
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 '<div style="text-decoration: line-through;">'+ text +'</div>';
|
|
} 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 += `<br>${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',
|
|
vtype: 'DnsName',
|
|
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'));
|
|
me.lookup('editNotesOnDoubleClick').setValue(sp.get('edit-notes-on-double-click', false));
|
|
|
|
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),
|
|
},
|
|
'field[reference=editNotesOnDoubleClick]': {
|
|
change: (e, v) => Ext.state.Manager.getProvider().set('edit-notes-on-double-click', 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: 125,
|
|
stateId: 'summarycolumns',
|
|
reference: 'summarycolumns',
|
|
comboItems: [
|
|
['auto', 'auto'],
|
|
['1', '1'],
|
|
['2', '2'],
|
|
['3', '3'],
|
|
],
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
fieldLabel: gettext('Guest Notes') + ':',
|
|
labelWidth: 125,
|
|
stateId: 'guest-notes-collapse',
|
|
reference: 'guestNotesCollapse',
|
|
comboItems: [
|
|
['never', 'Show by default'],
|
|
['always', 'Collapse by default'],
|
|
['auto', 'auto (Collapse if empty)'],
|
|
],
|
|
},
|
|
{
|
|
xtype: 'checkbox',
|
|
fieldLabel: gettext('Notes'),
|
|
labelWidth: 125,
|
|
boxLabel: gettext('Open editor on double-click'),
|
|
reference: 'editNotesOnDoubleClick',
|
|
inputValue: true,
|
|
uncheckedValue: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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;
|
|
|
|
let filename = data.filename || "";
|
|
let compression = '__default__';
|
|
if (view.content === 'iso') {
|
|
const matches = filename.match(/^(.+)\.(gz|lzo|zst)$/i);
|
|
if (matches) {
|
|
filename = matches[1];
|
|
compression = matches[2].toLowerCase();
|
|
}
|
|
}
|
|
|
|
view.setValues({
|
|
filename,
|
|
compression,
|
|
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: 'proxmoxKVComboBox',
|
|
name: 'compression',
|
|
fieldLabel: gettext('Decompression algorithm'),
|
|
allowBlank: true,
|
|
hasNoneOption: true,
|
|
deleteEmpty: false,
|
|
value: '__default__',
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.NoneText],
|
|
['lzo', 'LZO'],
|
|
['gz', 'GZIP'],
|
|
['zst', 'ZSTD'],
|
|
],
|
|
cbind: {
|
|
hidden: get => get('content') !== 'iso',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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: 540,
|
|
|
|
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.lxc_mp_counts.mps - 1
|
|
: PVE.Utils.lxc_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.nextFreeLxcMP(
|
|
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.GuestStop', {
|
|
extend: 'Ext.window.MessageBox',
|
|
|
|
closeAction: 'destroy',
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
if (!me.vm) {
|
|
throw "no vm specified";
|
|
}
|
|
|
|
let isQemuVM = me.vm.type === 'qemu';
|
|
let overruleTaskType = isQemuVM ? 'qmshutdown' : 'vzshutdown';
|
|
|
|
me.taskType = isQemuVM ? 'qmstop' : 'vzstop';
|
|
me.url = `/nodes/${me.nodename}/${me.vm.type}/${me.vm.vmid}/status/stop`;
|
|
|
|
let caps = Ext.state.Manager.get('GuiCap');
|
|
let hasSysModify = !!caps.nodes['Sys.Modify'];
|
|
|
|
// offer to overrule if there is at least one matching shutdown task and the guest is not
|
|
// HA-enabled. Also allow users to abort tasks started by one of their API tokens.
|
|
let activeShutdownTask = Ext.getStore('pve-cluster-tasks')?.findBy(task =>
|
|
(hasSysModify || task.data.user === Proxmox.UserName) &&
|
|
task.data.id === me.vm.vmid.toString() &&
|
|
task.data.status === undefined &&
|
|
task.data.type === overruleTaskType,
|
|
) !== -1;
|
|
let haEnabled = me.vm.hastate && me.vm.hastate !== 'unmanaged';
|
|
|
|
me.callParent();
|
|
|
|
// message box has its actual content in a sub-container, the top one is just for layouting
|
|
me.promptContainer.add({
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'overrule-shutdown',
|
|
checked: !haEnabled && activeShutdownTask,
|
|
boxLabel: gettext('Overrule active shutdown tasks'),
|
|
hidden: !(hasSysModify || activeShutdownTask),
|
|
disabled: !(hasSysModify || activeShutdownTask) || haEnabled,
|
|
padding: '3 0 0 0',
|
|
});
|
|
},
|
|
|
|
handler: function(btn) {
|
|
let me = this;
|
|
if (btn === 'yes') {
|
|
let overruleField = me.promptContainer.down('proxmoxcheckbox[name=overrule-shutdown]');
|
|
let params = !overruleField.isDisabled() && overruleField.getSubmitValue()
|
|
? { 'overrule-shutdown': 1 }
|
|
: undefined;
|
|
Proxmox.Utils.API2Request({
|
|
url: me.url,
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
params: params,
|
|
failure: response => Ext.Msg.alert('Error', response.htmlStatus),
|
|
});
|
|
}
|
|
},
|
|
|
|
show: function() {
|
|
let me = this;
|
|
let cfg = {
|
|
title: gettext('Confirm'),
|
|
icon: Ext.Msg.WARNING,
|
|
msg: Proxmox.Utils.format_task_description(me.taskType, me.vm.vmid),
|
|
buttons: Ext.Msg.YESNO,
|
|
callback: btn => me.handler(btn),
|
|
};
|
|
me.callParent([cfg]);
|
|
},
|
|
});
|
|
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.window.PCIMapEditWindow', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
width: 800,
|
|
|
|
subject: gettext('PCI mapping'),
|
|
|
|
onlineHelp: 'resource_mapping',
|
|
|
|
method: 'POST',
|
|
|
|
cbindData: function(initialConfig) {
|
|
let me = this;
|
|
me.isCreate = (!me.name || !me.nodename) && !me.entryOnly;
|
|
me.method = me.name ? 'PUT' : 'POST';
|
|
me.hideMapping = !!me.entryOnly;
|
|
me.hideComment = me.name && !me.entryOnly;
|
|
me.hideNodeSelector = me.nodename || me.entryOnly;
|
|
me.hideNode = !me.nodename || !me.hideNodeSelector;
|
|
return {
|
|
name: me.name,
|
|
nodename: me.nodename,
|
|
};
|
|
},
|
|
|
|
submitUrl: function(_url, data) {
|
|
let me = this;
|
|
let name = me.method === 'PUT' ? me.name : '';
|
|
return `/cluster/mapping/pci/${name}`;
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
if (view.method === "POST") {
|
|
delete me.digest;
|
|
}
|
|
|
|
if (values.iommugroup === -1) {
|
|
delete values.iommugroup;
|
|
}
|
|
|
|
let nodename = values.node ?? view.nodename;
|
|
delete values.node;
|
|
if (me.originalMap) {
|
|
let otherMaps = PVE.Parser
|
|
.filterPropertyStringList(me.originalMap, (e) => e.node !== nodename);
|
|
if (otherMaps.length) {
|
|
values.map = values.map.concat(otherMaps);
|
|
}
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
onSetValues: function(values) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
me.originalMap = [...values.map];
|
|
let configuredNodes = [];
|
|
values.map = PVE.Parser.filterPropertyStringList(values.map, (e) => {
|
|
configuredNodes.push(e.node);
|
|
return e.node === view.nodename;
|
|
});
|
|
|
|
me.lookup('nodeselector').disallowedNodes = configuredNodes;
|
|
return values;
|
|
},
|
|
|
|
checkIommu: function(store, records, success) {
|
|
let me = this;
|
|
if (!success || !records.length) {
|
|
return;
|
|
}
|
|
me.lookup('iommu_warning').setVisible(
|
|
records.every((val) => val.data.iommugroup === -1),
|
|
);
|
|
|
|
let value = me.lookup('pciselector').getValue();
|
|
me.checkIsolated(value);
|
|
},
|
|
|
|
checkIsolated: function(value) {
|
|
let me = this;
|
|
|
|
let store = me.lookup('pciselector').getStore();
|
|
|
|
let isIsolated = function(entry) {
|
|
let isolated = true;
|
|
let parsed = PVE.Parser.parsePropertyString(entry);
|
|
parsed.iommugroup = parseInt(parsed.iommugroup, 10);
|
|
if (!parsed.iommugroup) {
|
|
return isolated;
|
|
}
|
|
store.each(({ data }) => {
|
|
let isSubDevice = data.id.startsWith(parsed.path);
|
|
if (data.iommugroup === parsed.iommugroup && data.id !== parsed.path && !isSubDevice) {
|
|
isolated = false;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return isolated;
|
|
};
|
|
|
|
let showWarning = false;
|
|
if (Ext.isArray(value)) {
|
|
for (const entry of value) {
|
|
if (!isIsolated(entry)) {
|
|
showWarning = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
showWarning = isIsolated(value);
|
|
}
|
|
me.lookup('group_warning').setVisible(showWarning);
|
|
},
|
|
|
|
mdevChange: function(mdevField, value) {
|
|
this.lookup('pciselector').setMdev(value);
|
|
},
|
|
|
|
nodeChange: function(_field, value) {
|
|
this.lookup('pciselector').setNodename(value);
|
|
},
|
|
|
|
pciChange: function(_field, value) {
|
|
let me = this;
|
|
me.lookup('multiple_warning').setVisible(Ext.isArray(value) && value.length > 1);
|
|
me.checkIsolated(value);
|
|
},
|
|
|
|
control: {
|
|
'field[name=mdev]': {
|
|
change: 'mdevChange',
|
|
},
|
|
'pveNodeSelector': {
|
|
change: 'nodeChange',
|
|
},
|
|
'pveMultiPCISelector': {
|
|
change: 'pciChange',
|
|
},
|
|
},
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'inputpanel',
|
|
onGetValues: function(values) {
|
|
return this.up('window').getController().onGetValues(values);
|
|
},
|
|
|
|
onSetValues: function(values) {
|
|
return this.up('window').getController().onSetValues(values);
|
|
},
|
|
|
|
columnT: [
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'iommu_warning',
|
|
hidden: true,
|
|
columnWidth: 1,
|
|
padding: '0 0 10 0',
|
|
value: gettext('No IOMMU detected, please activate it. See Documentation for further information.'),
|
|
userCls: 'pmx-hint',
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'multiple_warning',
|
|
hidden: true,
|
|
columnWidth: 1,
|
|
padding: '0 0 10 0',
|
|
value: gettext('When multiple devices are selected, the first free one will be chosen on guest start.'),
|
|
userCls: 'pmx-hint',
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'group_warning',
|
|
hidden: true,
|
|
columnWidth: 1,
|
|
padding: '0 0 10 0',
|
|
itemId: 'iommuwarning',
|
|
value: gettext('A selected device is not in a separate IOMMU group, make sure this is intended.'),
|
|
userCls: 'pmx-hint',
|
|
},
|
|
],
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
fieldLabel: gettext('Name'),
|
|
labelWidth: 120,
|
|
cbind: {
|
|
editable: '{!name}',
|
|
value: '{name}',
|
|
submitValue: '{isCreate}',
|
|
},
|
|
name: 'id',
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Mapping on Node'),
|
|
labelWidth: 120,
|
|
name: 'node',
|
|
cbind: {
|
|
value: '{nodename}',
|
|
disabled: '{hideNode}',
|
|
hidden: '{hideNode}',
|
|
},
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
reference: 'nodeselector',
|
|
fieldLabel: gettext('Mapping on Node'),
|
|
labelWidth: 120,
|
|
name: 'node',
|
|
cbind: {
|
|
disabled: '{hideNodeSelector}',
|
|
hidden: '{hideNodeSelector}',
|
|
},
|
|
allowBlank: false,
|
|
},
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Use with Mediated Devices'),
|
|
labelWidth: 200,
|
|
reference: 'mdev',
|
|
name: 'mdev',
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
disabled: '{hideComment}',
|
|
},
|
|
},
|
|
],
|
|
|
|
columnB: [
|
|
{
|
|
xtype: 'pveMultiPCISelector',
|
|
fieldLabel: gettext('Device'),
|
|
labelWidth: 120,
|
|
height: 300,
|
|
reference: 'pciselector',
|
|
name: 'map',
|
|
cbind: {
|
|
nodename: '{nodename}',
|
|
disabled: '{hideMapping}',
|
|
hidden: '{hideMapping}',
|
|
},
|
|
allowBlank: false,
|
|
onLoadCallBack: 'checkIommu',
|
|
margin: '0 0 10 0',
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
fieldLabel: gettext('Comment'),
|
|
labelWidth: 120,
|
|
submitValue: true,
|
|
name: 'description',
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
disabled: '{hideComment}',
|
|
hidden: '{hideComment}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
Ext.define('PVE.window.USBMapEditWindow', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
cbindData: function(initialConfig) {
|
|
let me = this;
|
|
me.isCreate = !me.name;
|
|
me.method = me.isCreate ? 'POST' : 'PUT';
|
|
me.hideMapping = !!me.entryOnly;
|
|
me.hideComment = me.name && !me.entryOnly;
|
|
me.hideNodeSelector = me.nodename || me.entryOnly;
|
|
me.hideNode = !me.nodename || !me.hideNodeSelector;
|
|
return {
|
|
name: me.name,
|
|
nodename: me.nodename,
|
|
};
|
|
},
|
|
|
|
submitUrl: function(_url, data) {
|
|
let me = this;
|
|
let name = me.isCreate ? '' : me.name;
|
|
return `/cluster/mapping/usb/${name}`;
|
|
},
|
|
|
|
title: gettext('Add USB mapping'),
|
|
|
|
onlineHelp: 'resource_mapping',
|
|
|
|
method: 'POST',
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
values.node ??= view.nodename;
|
|
|
|
let type = me.getView().down('radiofield').getGroupValue();
|
|
let name = values.name;
|
|
let description = values.description;
|
|
delete values.description;
|
|
delete values.name;
|
|
|
|
if (type === 'path') {
|
|
let usbsel = me.lookup(type);
|
|
let usbDev = usbsel.getStore().findRecord('usbid', values[type], 0, false, true, true);
|
|
|
|
if (!usbDev) {
|
|
return {};
|
|
}
|
|
values.id = `${usbDev.data.vendid}:${usbDev.data.prodid}`;
|
|
}
|
|
|
|
let map = [];
|
|
if (me.originalMap) {
|
|
map = PVE.Parser.filterPropertyStringList(me.originalMap, (e) => e.node !== values.node);
|
|
}
|
|
if (values.id) {
|
|
map.push(PVE.Parser.printPropertyString(values));
|
|
}
|
|
|
|
values = { map };
|
|
if (description) {
|
|
values.description = description;
|
|
}
|
|
|
|
if (view.isCreate) {
|
|
values.id = name;
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
onSetValues: function(values) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
me.originalMap = [...values.map];
|
|
let configuredNodes = [];
|
|
PVE.Parser.filterPropertyStringList(values.map, (e) => {
|
|
configuredNodes.push(e.node);
|
|
if (e.node === view.nodename) {
|
|
values = e;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
me.lookup('nodeselector').disallowedNodes = configuredNodes;
|
|
if (values.path) {
|
|
values.usb = 'path';
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
modeChange: function(field, value) {
|
|
let me = this;
|
|
let type = field.inputValue;
|
|
let usbsel = me.lookup(type);
|
|
usbsel.setDisabled(!value);
|
|
},
|
|
|
|
nodeChange: function(_field, value) {
|
|
this.lookup('id').setNodename(value);
|
|
this.lookup('path').setNodename(value);
|
|
},
|
|
|
|
|
|
init: function(view) {
|
|
let me = this;
|
|
|
|
if (!view.nodename) {
|
|
//throw "no nodename given";
|
|
}
|
|
},
|
|
|
|
control: {
|
|
'radiofield': {
|
|
change: 'modeChange',
|
|
},
|
|
'pveNodeSelector': {
|
|
change: 'nodeChange',
|
|
},
|
|
},
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'inputpanel',
|
|
onGetValues: function(values) {
|
|
return this.up('window').getController().onGetValues(values);
|
|
},
|
|
|
|
onSetValues: function(values) {
|
|
return this.up('window').getController().onSetValues(values);
|
|
},
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
fieldLabel: gettext('Name'),
|
|
cbind: {
|
|
editable: '{!name}',
|
|
value: '{name}',
|
|
submitValue: '{isCreate}',
|
|
},
|
|
name: 'name',
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Mapping on Node'),
|
|
labelWidth: 120,
|
|
name: 'node',
|
|
cbind: {
|
|
value: '{nodename}',
|
|
disabled: '{hideNode}',
|
|
hidden: '{hideNode}',
|
|
},
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
reference: 'nodeselector',
|
|
fieldLabel: gettext('Mapping on Node'),
|
|
labelWidth: 120,
|
|
name: 'node',
|
|
cbind: {
|
|
disabled: '{hideNodeSelector}',
|
|
hidden: '{hideNodeSelector}',
|
|
},
|
|
allowBlank: false,
|
|
},
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'fieldcontainer',
|
|
defaultType: 'radiofield',
|
|
layout: 'fit',
|
|
cbind: {
|
|
disabled: '{hideMapping}',
|
|
hidden: '{hideMapping}',
|
|
},
|
|
items: [
|
|
{
|
|
name: 'usb',
|
|
inputValue: 'id',
|
|
checked: true,
|
|
boxLabel: gettext('Use USB Vendor/Device ID'),
|
|
submitValue: false,
|
|
},
|
|
{
|
|
xtype: 'pveUSBSelector',
|
|
type: 'device',
|
|
reference: 'id',
|
|
name: 'id',
|
|
cbind: {
|
|
nodename: '{nodename}',
|
|
disabled: '{hideMapping}',
|
|
},
|
|
editable: true,
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Choose Device'),
|
|
labelAlign: 'right',
|
|
},
|
|
{
|
|
name: 'usb',
|
|
inputValue: 'path',
|
|
boxLabel: gettext('Use USB Port'),
|
|
submitValue: false,
|
|
},
|
|
{
|
|
xtype: 'pveUSBSelector',
|
|
disabled: true,
|
|
name: 'path',
|
|
reference: 'path',
|
|
cbind: {
|
|
nodename: '{nodename}',
|
|
},
|
|
editable: true,
|
|
type: 'port',
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Choose Port'),
|
|
labelAlign: 'right',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
|
|
columnB: [
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
fieldLabel: gettext('Comment'),
|
|
submitValue: true,
|
|
name: 'description',
|
|
cbind: {
|
|
disabled: '{hideComment}',
|
|
hidden: '{hideComment}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
Ext.define('PVE.window.GuestImport', {
|
|
extend: 'Proxmox.window.Edit', // fixme: Proxmox.window.Edit?
|
|
alias: 'widget.pveGuestImportWindow',
|
|
|
|
title: gettext('Import Guest'),
|
|
|
|
width: 720,
|
|
bodyPadding: 0,
|
|
|
|
submitUrl: function() {
|
|
let me = this;
|
|
return `/nodes/${me.nodename}/qemu`;
|
|
},
|
|
|
|
isAdd: true,
|
|
isCreate: true,
|
|
submitText: gettext('Import'),
|
|
showTaskViewer: true,
|
|
method: 'POST',
|
|
|
|
loadUrl: function(_url, { storage, nodename, volumeName }) {
|
|
let args = Ext.Object.toQueryString({ volume: volumeName });
|
|
return `/nodes/${nodename}/storage/${storage}/import-metadata?${args}`;
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
setNodename: function(_column, widget) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
widget.setNodename(view.nodename);
|
|
},
|
|
|
|
diskStorageChange: function(storageSelector, value) {
|
|
let me = this;
|
|
|
|
let grid = me.lookup('diskGrid');
|
|
let rec = storageSelector.getWidgetRecord();
|
|
let validFormats = storageSelector.store.getById(value)?.data.format;
|
|
grid.query('pveDiskFormatSelector').some((selector) => {
|
|
if (selector.getWidgetRecord().data.id !== rec.data.id) {
|
|
return false;
|
|
}
|
|
|
|
if (validFormats?.[0]?.qcow2) {
|
|
selector.setDisabled(false);
|
|
selector.setValue('qcow2');
|
|
} else {
|
|
selector.setValue('raw');
|
|
selector.setDisabled(true);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
},
|
|
|
|
isoStorageChange: function(storageSelector, value) {
|
|
let me = this;
|
|
|
|
let grid = me.lookup('cdGrid');
|
|
let rec = storageSelector.getWidgetRecord();
|
|
grid.query('pveFileSelector').some((selector) => {
|
|
if (selector.getWidgetRecord().data.id !== rec.data.id) {
|
|
return false;
|
|
}
|
|
|
|
selector.setStorage(value);
|
|
if (!value) {
|
|
selector.setValue('');
|
|
}
|
|
|
|
return true;
|
|
});
|
|
},
|
|
|
|
onOSBaseChange: function(_field, value) {
|
|
let me = this;
|
|
let ostype = me.lookup('ostype');
|
|
let store = ostype.getStore();
|
|
store.setData(PVE.Utils.kvm_ostypes[value]);
|
|
let old_val = ostype.getValue();
|
|
if (old_val && store.find('val', old_val) !== -1) {
|
|
ostype.setValue(old_val);
|
|
} else {
|
|
ostype.setValue(store.getAt(0));
|
|
}
|
|
},
|
|
|
|
calculateConfig: function() {
|
|
let me = this;
|
|
let inputPanel = me.lookup('mainInputPanel');
|
|
let summaryGrid = me.lookup('summaryGrid');
|
|
let values = inputPanel.getValues();
|
|
summaryGrid.getStore().setData(Object.entries(values).map(([key, value]) => ({ key, value })));
|
|
},
|
|
|
|
calculateAdditionalCDIdx: function() {
|
|
let me = this;
|
|
|
|
let maxIde = me.getMaxControllerId('ide');
|
|
let maxSata = me.getMaxControllerId('sata');
|
|
// only ide0 and ide2 can be used reliably for isos (e.g. for q35)
|
|
if (maxIde < 0) {
|
|
return 'ide0';
|
|
}
|
|
if (maxIde < 2) {
|
|
return 'ide2';
|
|
}
|
|
if (maxSata < PVE.Utils.diskControllerMaxIDs.sata - 1) {
|
|
return `sata${maxSata+1}`;
|
|
}
|
|
|
|
return '';
|
|
},
|
|
|
|
// assume assigned sata disks indices are continuous, so without holes
|
|
getMaxControllerId: function(controller) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
if (!controller) {
|
|
return -1;
|
|
}
|
|
|
|
let max = view[`max${controller}`];
|
|
if (max !== undefined) {
|
|
return max;
|
|
}
|
|
|
|
max = -1;
|
|
for (const key of Object.keys(me.getView().vmConfig)) {
|
|
if (!key.toLowerCase().startsWith(controller)) {
|
|
continue;
|
|
}
|
|
let idx = parseInt(key.slice(controller.length), 10);
|
|
if (idx > max) {
|
|
max = idx;
|
|
}
|
|
}
|
|
me.lookup('diskGrid').getStore().each(rec => {
|
|
if (!rec.data.id.toLowerCase().startsWith(controller)) {
|
|
return;
|
|
}
|
|
let idx = parseInt(rec.data.id.slice(controller.length), 10);
|
|
if (idx > max) {
|
|
max = idx;
|
|
}
|
|
});
|
|
me.lookup('cdGrid').getStore().each(rec => {
|
|
if (!rec.data.id.toLowerCase().startsWith(controller) || rec.data.hidden) {
|
|
return;
|
|
}
|
|
let idx = parseInt(rec.data.id.slice(controller.length), 10);
|
|
if (idx > max) {
|
|
max = idx;
|
|
}
|
|
});
|
|
|
|
view[`max${controller}`] = max;
|
|
return max;
|
|
},
|
|
|
|
renderDisk: function(value, metaData, record, rowIndex, colIndex, store, tableView) {
|
|
let diskGrid = tableView.grid ?? this.lookup('diskGrid');
|
|
if (diskGrid.diskMap) {
|
|
let mappedID = diskGrid.diskMap[value];
|
|
if (mappedID) {
|
|
let prefix = '';
|
|
if (mappedID === value) { // mapped to the same value means we ran out of IDs
|
|
let warning = gettext('Too many disks, could not map to SATA.');
|
|
prefix = `<i data-qtip="${warning}" class="fa fa-exclamation-triangle warning"></i> `;
|
|
}
|
|
return `${prefix}${mappedID}`;
|
|
}
|
|
}
|
|
return value;
|
|
},
|
|
|
|
refreshGrids: function() {
|
|
this.lookup('diskGrid').reconfigure();
|
|
this.lookup('cdGrid').reconfigure();
|
|
this.lookup('netGrid').reconfigure();
|
|
},
|
|
|
|
onOSTypeChange: function(_cb, value) {
|
|
let me = this;
|
|
if (!value) {
|
|
return;
|
|
}
|
|
let store = me.lookup('cdGrid').getStore();
|
|
let collection = store.getData().getSource() ?? store.getData();
|
|
let rec = collection.find('autogenerated', true);
|
|
|
|
let isWindows = (value ?? '').startsWith('w');
|
|
if (rec) {
|
|
rec.set('hidden', !isWindows);
|
|
rec.commit();
|
|
}
|
|
let prepareVirtio = me.lookup('prepareForVirtIO').getValue();
|
|
let defaultScsiHw = me.getView().vmConfig.scsihw ?? '__default__';
|
|
me.lookup('scsihw').setValue(prepareVirtio && isWindows ? 'virtio-scsi-single' : defaultScsiHw);
|
|
|
|
me.refreshGrids();
|
|
},
|
|
|
|
onPrepareVirtioChange: function(_cb, value) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
let diskGrid = me.lookup('diskGrid');
|
|
|
|
diskGrid.diskMap = {};
|
|
if (value) {
|
|
const hasAdditionalSataCDROM =
|
|
me.getViewModel().get('isWindows') && view.additionalCdIdx?.startsWith('sata');
|
|
|
|
diskGrid.getStore().each(rec => {
|
|
let diskID = rec.data.id;
|
|
if (!diskID.toLowerCase().startsWith('scsi')) {
|
|
return; // continue
|
|
}
|
|
let offset = parseInt(diskID.slice(4), 10);
|
|
let newIdx = offset + me.getMaxControllerId('sata') + 1;
|
|
if (hasAdditionalSataCDROM) {
|
|
newIdx++;
|
|
}
|
|
let mappedID = `sata${newIdx}`;
|
|
if (newIdx >= PVE.Utils.diskControllerMaxIDs.sata) {
|
|
mappedID = diskID; // map to self so that the renderer can detect that we're out of IDs
|
|
}
|
|
diskGrid.diskMap[diskID] = mappedID;
|
|
});
|
|
}
|
|
|
|
let scsihw = me.lookup('scsihw');
|
|
scsihw.suspendEvents();
|
|
scsihw.setValue(value ? 'virtio-scsi-single' : me.getView().vmConfig.scsihw);
|
|
scsihw.resumeEvents();
|
|
|
|
me.refreshGrids();
|
|
},
|
|
|
|
onScsiHwChange: function(_field, value) {
|
|
let me = this;
|
|
me.getView().vmConfig.scsihw = value;
|
|
},
|
|
|
|
onUniqueMACChange: function(_cb, value) {
|
|
let me = this;
|
|
|
|
me.getViewModel().set('uniqueMACAdresses', value);
|
|
|
|
me.lookup('netGrid').reconfigure();
|
|
},
|
|
|
|
renderMacAddress: function(value, metaData, record, rowIndex, colIndex, store, view) {
|
|
let me = this;
|
|
let vm = me.getViewModel();
|
|
|
|
return !vm.get('uniqueMACAdresses') && value ? value : 'auto';
|
|
},
|
|
|
|
control: {
|
|
'grid field': {
|
|
// update records from widgetcolumns
|
|
change: function(widget, value) {
|
|
let rec = widget.getWidgetRecord();
|
|
rec.set(widget.name, value);
|
|
rec.commit();
|
|
},
|
|
},
|
|
'grid[reference=diskGrid] pveStorageSelector': {
|
|
change: 'diskStorageChange',
|
|
},
|
|
'grid[reference=cdGrid] pveStorageSelector': {
|
|
change: 'isoStorageChange',
|
|
},
|
|
'field[name=osbase]': {
|
|
change: 'onOSBaseChange',
|
|
},
|
|
'panel[reference=summaryTab]': {
|
|
activate: 'calculateConfig',
|
|
},
|
|
'proxmoxcheckbox[reference=prepareForVirtIO]': {
|
|
change: 'onPrepareVirtioChange',
|
|
},
|
|
'combobox[name=ostype]': {
|
|
change: 'onOSTypeChange',
|
|
},
|
|
'pveScsiHwSelector': {
|
|
change: 'onScsiHwChange',
|
|
},
|
|
'proxmoxcheckbox[name=uniqueMACs]': {
|
|
change: 'onUniqueMACChange',
|
|
},
|
|
},
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
coreCount: 1,
|
|
socketCount: 1,
|
|
liveImport: false,
|
|
os: 'l26',
|
|
maxCdDrives: false,
|
|
uniqueMACAdresses: false,
|
|
warnings: [],
|
|
},
|
|
|
|
formulas: {
|
|
totalCoreCount: get => get('socketCount') * get('coreCount'),
|
|
hideWarnings: get => get('warnings').length === 0,
|
|
warningsText: get => '<ul style="margin: 0; padding-left: 20px;">'
|
|
+ get('warnings').map(w => `<li>${w}</li>`).join('') + '</ul>',
|
|
liveImportNote: get => !get('liveImport') ? ''
|
|
: gettext('Note: If anything goes wrong during the live-import, new data written by the VM may be lost.'),
|
|
isWindows: get => (get('os') ?? '').startsWith('w'),
|
|
},
|
|
},
|
|
|
|
items: [{
|
|
xtype: 'tabpanel',
|
|
defaults: {
|
|
bodyPadding: 10,
|
|
},
|
|
items: [
|
|
{
|
|
title: gettext('General'),
|
|
xtype: 'inputpanel',
|
|
reference: 'mainInputPanel',
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
let view = me.up('pveGuestImportWindow');
|
|
let vm = view.getViewModel();
|
|
let diskGrid = view.lookup('diskGrid');
|
|
|
|
// from pveDiskStorageSelector
|
|
let defaultStorage = values.hdstorage;
|
|
let defaultFormat = values.diskformat;
|
|
delete values.hdstorage;
|
|
delete values.diskformat;
|
|
|
|
let defaultBridge = values.defaultBridge;
|
|
delete values.defaultBridge;
|
|
|
|
let config = { ...view.vmConfig };
|
|
Ext.apply(config, values);
|
|
|
|
if (config.scsi0) {
|
|
config.scsi0 = config.scsi0.replace('local:0,', 'local:0,format=qcow2,');
|
|
}
|
|
|
|
let parsedBoot = PVE.Parser.parsePropertyString(config.boot ?? '');
|
|
if (parsedBoot.order) {
|
|
parsedBoot.order = parsedBoot.order.split(';');
|
|
}
|
|
|
|
let diskMap = diskGrid.diskMap ?? {};
|
|
diskGrid.getStore().each(rec => {
|
|
if (!rec.data.enable) {
|
|
return;
|
|
}
|
|
let id = diskMap[rec.data.id] ?? rec.data.id;
|
|
if (id !== rec.data.id && parsedBoot?.order) {
|
|
let idx = parsedBoot.order.indexOf(rec.data.id);
|
|
if (idx !== -1) {
|
|
parsedBoot.order[idx] = id;
|
|
}
|
|
}
|
|
let data = {
|
|
...rec.data,
|
|
};
|
|
delete data.enable;
|
|
delete data.id;
|
|
delete data.size;
|
|
if (!data.file) {
|
|
data.file = defaultStorage;
|
|
data.format = defaultFormat;
|
|
}
|
|
data.file += ':0'; // for our special api format
|
|
if (id === 'efidisk0') {
|
|
delete data['import-from'];
|
|
}
|
|
config[id] = PVE.Parser.printQemuDrive(data);
|
|
});
|
|
|
|
if (parsedBoot.order) {
|
|
parsedBoot.order = parsedBoot.order.join(';');
|
|
}
|
|
config.boot = PVE.Parser.printPropertyString(parsedBoot);
|
|
|
|
view.lookup('netGrid').getStore().each((rec) => {
|
|
if (!rec.data.enable) {
|
|
return;
|
|
}
|
|
let id = rec.data.id;
|
|
let data = {
|
|
...rec.data,
|
|
};
|
|
delete data.enable;
|
|
delete data.id;
|
|
if (!data.bridge) {
|
|
data.bridge = defaultBridge;
|
|
}
|
|
if (vm.get('uniqueMACAdresses')) {
|
|
data.macaddr = undefined;
|
|
}
|
|
config[id] = PVE.Parser.printQemuNetwork(data);
|
|
});
|
|
|
|
view.lookup('cdGrid').getStore().each((rec) => {
|
|
if (!rec.data.enable) {
|
|
return;
|
|
}
|
|
let id = rec.data.id;
|
|
let cd = {
|
|
media: 'cdrom',
|
|
file: rec.data.file ? rec.data.file : 'none',
|
|
};
|
|
config[id] = PVE.Parser.printPropertyString(cd);
|
|
});
|
|
|
|
config.scsihw = view.lookup('scsihw').getValue();
|
|
|
|
if (view.lookup('liveimport').getValue()) {
|
|
config['live-restore'] = 1;
|
|
}
|
|
|
|
// remove __default__ values
|
|
for (const [key, value] of Object.entries(config)) {
|
|
if (value === '__default__') {
|
|
delete config[key];
|
|
}
|
|
}
|
|
|
|
return config;
|
|
},
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'pveGuestIDSelector',
|
|
name: 'vmid',
|
|
fieldLabel: 'VM',
|
|
guestType: 'qemu',
|
|
loadNextFreeID: true,
|
|
validateExists: false,
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Sockets'),
|
|
name: 'sockets',
|
|
reference: 'socketsField',
|
|
value: 1,
|
|
minValue: 1,
|
|
maxValue: 128,
|
|
allowBlank: true,
|
|
bind: {
|
|
value: '{socketCount}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Cores'),
|
|
name: 'cores',
|
|
reference: 'coresField',
|
|
value: 1,
|
|
minValue: 1,
|
|
maxValue: 1024,
|
|
allowBlank: true,
|
|
bind: {
|
|
value: '{coreCount}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveMemoryField',
|
|
fieldLabel: gettext('Memory') + ' (MiB)',
|
|
name: 'memory',
|
|
reference: 'memoryField',
|
|
value: 512,
|
|
allowBlank: true,
|
|
},
|
|
{ xtype: 'displayfield' }, // spacer
|
|
{ xtype: 'displayfield' }, // spacer
|
|
{
|
|
xtype: 'pveDiskStorageSelector',
|
|
reference: 'defaultStorage',
|
|
storageLabel: gettext('Default Storage'),
|
|
storageContent: 'images',
|
|
autoSelect: true,
|
|
hideSize: true,
|
|
name: 'defaultStorage',
|
|
},
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Name'),
|
|
name: 'name',
|
|
vtype: 'DnsName',
|
|
reference: 'nameField',
|
|
allowBlank: true,
|
|
},
|
|
{
|
|
xtype: 'CPUModelSelector',
|
|
name: 'cpu',
|
|
reference: 'cputype',
|
|
value: 'x86-64-v2-AES',
|
|
fieldLabel: gettext('CPU Type'),
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Total cores'),
|
|
name: 'totalcores',
|
|
isFormField: false,
|
|
bind: {
|
|
value: '{totalCoreCount}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'combobox',
|
|
submitValue: false,
|
|
name: 'osbase',
|
|
fieldLabel: gettext('OS 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',
|
|
bind: {
|
|
value: '{os}',
|
|
},
|
|
store: {
|
|
fields: ['desc', 'val'],
|
|
data: PVE.Utils.kvm_ostypes.Linux,
|
|
},
|
|
},
|
|
{ xtype: 'displayfield' }, // spacer
|
|
{
|
|
xtype: 'PVE.form.BridgeSelector',
|
|
reference: 'defaultBridge',
|
|
name: 'defaultBridge',
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Default Bridge'),
|
|
},
|
|
],
|
|
|
|
columnB: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Live Import'),
|
|
reference: 'liveimport',
|
|
isFormField: false,
|
|
boxLabel: gettext('Starts a previously stopped VM on Proxmox VE and imports the disks in the background.'),
|
|
bind: {
|
|
value: '{liveImport}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
userCls: 'pmx-hint black',
|
|
value: gettext('Note: If anything goes wrong during the live-import, new data written by the VM may be lost.'),
|
|
bind: {
|
|
hidden: '{!liveImport}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Warnings'),
|
|
labelWidth: 200,
|
|
hidden: true,
|
|
bind: {
|
|
hidden: '{hideWarnings}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'warningText',
|
|
userCls: 'pmx-hint',
|
|
hidden: true,
|
|
bind: {
|
|
hidden: '{hideWarnings}',
|
|
value: '{warningsText}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: gettext('Advanced'),
|
|
xtype: 'inputpanel',
|
|
|
|
// the first inputpanel handles all values, so prevent value leakage here
|
|
onGetValues: () => ({}),
|
|
|
|
columnT: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Disks'),
|
|
labelWidth: 200,
|
|
},
|
|
{
|
|
xtype: 'grid',
|
|
reference: 'diskGrid',
|
|
minHeight: 60,
|
|
maxHeight: 150,
|
|
store: {
|
|
data: [],
|
|
sorters: [
|
|
'id',
|
|
],
|
|
},
|
|
columns: [
|
|
{
|
|
xtype: 'checkcolumn',
|
|
header: gettext('Use'),
|
|
width: 50,
|
|
dataIndex: 'enable',
|
|
listeners: {
|
|
checkchange: function(_column, _rowIndex, _checked, record) {
|
|
record.commit();
|
|
},
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Disk'),
|
|
dataIndex: 'id',
|
|
renderer: 'renderDisk',
|
|
},
|
|
{
|
|
text: gettext('Source'),
|
|
dataIndex: 'import-from',
|
|
flex: 1,
|
|
renderer: function(value) {
|
|
return value.replace(/^.*\//, '');
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Size'),
|
|
dataIndex: 'size',
|
|
renderer: (value) => {
|
|
if (Ext.isNumeric(value)) {
|
|
return Proxmox.Utils.render_size(value);
|
|
}
|
|
return value ?? Proxmox.Utils.unknownText;
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Storage'),
|
|
dataIndex: 'file',
|
|
xtype: 'widgetcolumn',
|
|
width: 150,
|
|
widget: {
|
|
xtype: 'pveStorageSelector',
|
|
isFormField: false,
|
|
autoSelect: false,
|
|
allowBlank: true,
|
|
emptyText: gettext('From Default'),
|
|
name: 'file',
|
|
storageContent: 'images',
|
|
},
|
|
onWidgetAttach: 'setNodename',
|
|
},
|
|
{
|
|
text: gettext('Format'),
|
|
dataIndex: 'format',
|
|
xtype: 'widgetcolumn',
|
|
width: 150,
|
|
widget: {
|
|
xtype: 'pveDiskFormatSelector',
|
|
name: 'format',
|
|
disabled: true,
|
|
isFormField: false,
|
|
matchFieldWidth: false,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
boxLabel: gettext('Prepare for VirtIO-SCSI'),
|
|
reference: 'prepareForVirtIO',
|
|
name: 'prepareForVirtIO',
|
|
submitValue: false,
|
|
disabled: true,
|
|
bind: {
|
|
disabled: '{!isWindows}',
|
|
},
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Maps SCSI disks to SATA and changes the SCSI Controller. Useful for a quicker switch to VirtIO-SCSI attached disks'),
|
|
},
|
|
},
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'pveScsiHwSelector',
|
|
reference: 'scsihw',
|
|
name: 'scsihw',
|
|
value: '__default__',
|
|
submitValue: false,
|
|
fieldLabel: gettext('SCSI Controller'),
|
|
},
|
|
],
|
|
|
|
columnB: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('CD/DVD Drives'),
|
|
labelWidth: 200,
|
|
},
|
|
{
|
|
xtype: 'grid',
|
|
reference: 'cdGrid',
|
|
minHeight: 60,
|
|
maxHeight: 150,
|
|
store: {
|
|
data: [],
|
|
sorters: [
|
|
'id',
|
|
],
|
|
filters: [
|
|
function(rec) {
|
|
return !rec.data.hidden;
|
|
},
|
|
],
|
|
},
|
|
columns: [
|
|
{
|
|
xtype: 'checkcolumn',
|
|
header: gettext('Use'),
|
|
width: 50,
|
|
dataIndex: 'enable',
|
|
listeners: {
|
|
checkchange: function(_column, _rowIndex, _checked, record) {
|
|
record.commit();
|
|
},
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Slot'),
|
|
dataIndex: 'id',
|
|
sorted: true,
|
|
},
|
|
{
|
|
text: gettext('Storage'),
|
|
xtype: 'widgetcolumn',
|
|
width: 150,
|
|
widget: {
|
|
xtype: 'pveStorageSelector',
|
|
isFormField: false,
|
|
autoSelect: false,
|
|
allowBlank: true,
|
|
emptyText: Proxmox.Utils.noneText,
|
|
storageContent: 'iso',
|
|
},
|
|
onWidgetAttach: 'setNodename',
|
|
},
|
|
{
|
|
text: gettext('ISO'),
|
|
dataIndex: 'file',
|
|
xtype: 'widgetcolumn',
|
|
flex: 1,
|
|
widget: {
|
|
xtype: 'pveFileSelector',
|
|
name: 'file',
|
|
isFormField: false,
|
|
allowBlank: true,
|
|
emptyText: Proxmox.Utils.noneText,
|
|
storageContent: 'iso',
|
|
},
|
|
onWidgetAttach: 'setNodename',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Network Interfaces'),
|
|
labelWidth: 200,
|
|
style: {
|
|
paddingTop: '10px',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'grid',
|
|
minHeight: 58,
|
|
maxHeight: 150,
|
|
reference: 'netGrid',
|
|
store: {
|
|
data: [],
|
|
sorters: [
|
|
'id',
|
|
],
|
|
},
|
|
columns: [
|
|
{
|
|
xtype: 'checkcolumn',
|
|
header: gettext('Use'),
|
|
width: 50,
|
|
dataIndex: 'enable',
|
|
listeners: {
|
|
checkchange: function(_column, _rowIndex, _checked, record) {
|
|
record.commit();
|
|
},
|
|
},
|
|
},
|
|
{
|
|
text: gettext('ID'),
|
|
dataIndex: 'id',
|
|
},
|
|
{
|
|
text: gettext('MAC address'),
|
|
flex: 7,
|
|
dataIndex: 'macaddr',
|
|
renderer: 'renderMacAddress',
|
|
},
|
|
{
|
|
text: gettext('Model'),
|
|
flex: 7,
|
|
dataIndex: 'model',
|
|
xtype: 'widgetcolumn',
|
|
widget: {
|
|
xtype: 'pveNetworkCardSelector',
|
|
name: 'model',
|
|
isFormField: false,
|
|
allowBlank: false,
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Bridge'),
|
|
dataIndex: 'bridge',
|
|
xtype: 'widgetcolumn',
|
|
flex: 6,
|
|
widget: {
|
|
xtype: 'PVE.form.BridgeSelector',
|
|
name: 'bridge',
|
|
isFormField: false,
|
|
autoSelect: false,
|
|
allowBlank: true,
|
|
emptyText: gettext('From Default'),
|
|
},
|
|
onWidgetAttach: 'setNodename',
|
|
},
|
|
{
|
|
text: gettext('VLAN Tag'),
|
|
dataIndex: 'tag',
|
|
xtype: 'widgetcolumn',
|
|
flex: 5,
|
|
widget: {
|
|
xtype: 'pveVlanField',
|
|
fieldLabel: undefined,
|
|
name: 'tag',
|
|
isFormField: false,
|
|
allowBlank: true,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'uniqueMACs',
|
|
boxLabel: gettext('Unique MAC addresses'),
|
|
uncheckedValue: false,
|
|
value: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: gettext('Resulting Config'),
|
|
reference: 'summaryTab',
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
reference: 'summaryGrid',
|
|
maxHeight: 400,
|
|
scrollable: true,
|
|
store: {
|
|
model: 'KeyValue',
|
|
sorters: [{
|
|
property: 'key',
|
|
direction: 'ASC',
|
|
}],
|
|
},
|
|
columns: [
|
|
{ header: 'Key', width: 150, dataIndex: 'key' },
|
|
{ header: 'Value', flex: 1, dataIndex: 'value' },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}],
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
if (!me.volumeName) {
|
|
throw "no volumeName given";
|
|
}
|
|
|
|
if (!me.storage) {
|
|
throw "no storage given";
|
|
}
|
|
|
|
if (!me.nodename) {
|
|
throw "no nodename given";
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
me.setTitle(Ext.String.format(gettext('Import Guest - {0}'), `${me.storage}:${me.volumeName}`));
|
|
|
|
me.lookup('defaultStorage').setNodename(me.nodename);
|
|
me.lookup('defaultBridge').setNodename(me.nodename);
|
|
|
|
let renderWarning = w => {
|
|
const warningsCatalogue = {
|
|
'cdrom-image-ignored': gettext("CD-ROM images cannot get imported, if required you can reconfigure the '{0}' drive in the 'Advanced' tab."),
|
|
'nvme-unsupported': gettext("NVMe disks are currently not supported, '{0}' will get attaced as SCSI"),
|
|
'ovmf-with-lsi-unsupported': gettext("OVMF is built without LSI drivers, scsi hardware was set to '{1}'"),
|
|
'serial-port-socket-only': gettext("Serial socket '{0}' will be mapped to a socket"),
|
|
'guest-is-running': gettext('Virtual guest seems to be running on source host. Import might fail or have inconsistent state!'),
|
|
'efi-state-lost': Ext.String.format(
|
|
gettext('EFI state cannot be imported, you may need to reconfigure the boot order (see {0})'),
|
|
'<a href="https://pve.proxmox.com/wiki/OVMF/UEFI_Boot_Entries">OVMF/UEFI Boot Entries</a>',
|
|
),
|
|
};
|
|
let message = warningsCatalogue[w.type];
|
|
if (!w.type || !message) {
|
|
return w.message ?? w.type ?? gettext('Unknown warning');
|
|
}
|
|
return Ext.String.format(message, w.key ?? 'unknown', w.value ?? 'unknown');
|
|
};
|
|
|
|
me.load({
|
|
success: function(response) {
|
|
let data = response.result.data;
|
|
me.vmConfig = data['create-args'];
|
|
|
|
let disks = [];
|
|
for (const [id, value] of Object.entries(data.disks ?? {})) {
|
|
let volid = Ext.htmlEncode('<none>');
|
|
let size = 'auto';
|
|
if (Ext.isObject(value)) {
|
|
volid = value.volid;
|
|
size = value.size;
|
|
}
|
|
disks.push({
|
|
id,
|
|
enable: true,
|
|
size,
|
|
'import-from': volid,
|
|
format: 'raw',
|
|
});
|
|
}
|
|
|
|
let nets = [];
|
|
for (const [id, parsed] of Object.entries(data.net ?? {})) {
|
|
parsed.id = id;
|
|
parsed.enable = true;
|
|
nets.push(parsed);
|
|
}
|
|
|
|
let cdroms = [];
|
|
for (const [id, value] of Object.entries(me.vmConfig)) {
|
|
if (!Ext.isString(value) || !value.match(/media=cdrom/)) {
|
|
continue;
|
|
}
|
|
cdroms.push({
|
|
enable: true,
|
|
hidden: false,
|
|
id,
|
|
});
|
|
delete me.vmConfig[id];
|
|
}
|
|
|
|
me.lookup('diskGrid').getStore().setData(disks);
|
|
me.lookup('netGrid').getStore().setData(nets);
|
|
me.lookup('cdGrid').getStore().setData(cdroms);
|
|
|
|
let additionalCdIdx = me.getController().calculateAdditionalCDIdx();
|
|
if (additionalCdIdx === '') {
|
|
me.getViewModel().set('maxCdDrives', true);
|
|
} else if (cdroms.length === 0) {
|
|
me.additionalCdIdx = additionalCdIdx;
|
|
me.lookup('cdGrid').getStore().add({
|
|
enable: true,
|
|
hidden: !(me.vmConfig.ostype ?? '').startsWith('w'),
|
|
id: additionalCdIdx,
|
|
autogenerated: true,
|
|
});
|
|
}
|
|
|
|
me.getViewModel().set('warnings', data.warnings.map(w => renderWarning(w)));
|
|
|
|
let osinfo = PVE.Utils.get_kvm_osinfo(me.vmConfig.ostype ?? '');
|
|
let prepareForVirtIO = (me.vmConfig.ostype ?? '').startsWith('w') && (me.vmConfig.bios ?? '').indexOf('ovmf') !== -1;
|
|
|
|
me.setValues({
|
|
osbase: osinfo.base,
|
|
...me.vmConfig,
|
|
});
|
|
|
|
|
|
me.lookup('prepareForVirtIO').setValue(prepareForVirtIO);
|
|
},
|
|
});
|
|
},
|
|
});
|
|
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: gettext('Use watchdog based fencing.'),
|
|
},
|
|
columns: [
|
|
{
|
|
header: gettext('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: gettext('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'],
|
|
|
|
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 = `<p>${Ext.htmlEncode(data.errors.group)}</p>`;
|
|
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,
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Case-Sensitive'),
|
|
name: 'case-sensitive',
|
|
uncheckedValue: 0,
|
|
checked: true,
|
|
},
|
|
];
|
|
|
|
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: 'proxmoxKVComboBox',
|
|
name: 'mode',
|
|
fieldLabel: gettext('Mode'),
|
|
editable: false,
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText + ' (LDAP)'],
|
|
['ldap', 'LDAP'],
|
|
['ldap+starttls', 'STARTTLS'],
|
|
['ldaps', 'LDAPS'],
|
|
],
|
|
value: '__default__',
|
|
deleteEmpty: !me.isCreate,
|
|
listeners: {
|
|
change: function(field, newValue) {
|
|
let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]');
|
|
if (newValue === 'ldap' || newValue === '__default__') {
|
|
verifyCheckbox.disable();
|
|
verifyCheckbox.setValue(0);
|
|
} else {
|
|
verifyCheckbox.enable();
|
|
}
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Verify Certificate'),
|
|
name: 'verify',
|
|
uncheckedValue: 0,
|
|
disabled: true,
|
|
checked: false,
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Verify TLS certificate of the server'),
|
|
},
|
|
},
|
|
];
|
|
|
|
me.advancedItems = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Check connection'),
|
|
name: 'check-connection',
|
|
uncheckedValue: 0,
|
|
checked: true,
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip':
|
|
gettext('Verify connection parameters and bind credentials on save'),
|
|
},
|
|
},
|
|
];
|
|
|
|
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;
|
|
}
|
|
|
|
if (!me.isCreate) {
|
|
// Delete old `secure` parameter. It has been deprecated in favor to the
|
|
// `mode` parameter. Migration happens automatically in `onSetValues`.
|
|
Proxmox.Utils.assemble_field_data(values, { 'delete': 'secure' });
|
|
}
|
|
|
|
|
|
return me.callParent([values]);
|
|
},
|
|
|
|
onSetValues(values) {
|
|
let me = this;
|
|
|
|
if (values.secure !== undefined && !values.mode) {
|
|
// If `secure` is set, use it to determine the correct setting for `mode`
|
|
// `secure` is later deleted by `onSetValues` .
|
|
// In case *both* are set, we simply ignore `secure` and use
|
|
// whatever `mode` is set to.
|
|
values.mode = values.secure ? 'ldaps' : 'ldap';
|
|
}
|
|
|
|
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: 'proxmoxKVComboBox',
|
|
name: 'mode',
|
|
fieldLabel: gettext('Mode'),
|
|
editable: false,
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText + ' (LDAP)'],
|
|
['ldap', 'LDAP'],
|
|
['ldap+starttls', 'STARTTLS'],
|
|
['ldaps', 'LDAPS'],
|
|
],
|
|
value: '__default__',
|
|
deleteEmpty: !me.isCreate,
|
|
listeners: {
|
|
change: function(field, newValue) {
|
|
let verifyCheckbox = field.nextSibling('proxmoxcheckbox[name=verify]');
|
|
if (newValue === 'ldap' || newValue === '__default__') {
|
|
verifyCheckbox.disable();
|
|
verifyCheckbox.setValue(0);
|
|
} else {
|
|
verifyCheckbox.enable();
|
|
}
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Verify Certificate'),
|
|
name: 'verify',
|
|
uncheckedValue: 0,
|
|
disabled: true,
|
|
checked: false,
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Verify TLS certificate of the server'),
|
|
},
|
|
},
|
|
];
|
|
|
|
me.advancedItems = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Check connection'),
|
|
name: 'check-connection',
|
|
uncheckedValue: 0,
|
|
checked: true,
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip':
|
|
gettext('Verify connection parameters and bind credentials on save'),
|
|
},
|
|
},
|
|
];
|
|
|
|
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;
|
|
}
|
|
|
|
if (!me.isCreate) {
|
|
// Delete old `secure` parameter. It has been deprecated in favor to the
|
|
// `mode` parameter. Migration happens automatically in `onSetValues`.
|
|
Proxmox.Utils.assemble_field_data(values, { 'delete': 'secure' });
|
|
}
|
|
|
|
return me.callParent([values]);
|
|
},
|
|
|
|
onSetValues(values) {
|
|
let me = this;
|
|
|
|
if (values.secure !== undefined && !values.mode) {
|
|
// If `secure` is set, use it to determine the correct setting for `mode`
|
|
// `secure` is later deleted by `onSetValues` .
|
|
// In case *both* are set, we simply ignore `secure` and use
|
|
// whatever `mode` is set to.
|
|
values.mode = values.secure ? 'ldaps' : 'ldap';
|
|
}
|
|
|
|
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: {
|
|
itemdblclick: () => me.run_editor(),
|
|
},
|
|
});
|
|
|
|
me.callParent();
|
|
me.reload();
|
|
},
|
|
});
|
|
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: 'notification-policy',
|
|
fieldLabel: gettext('Notification'),
|
|
renderer: function(value) {
|
|
let record = this.up('pveBackupInfo')?.record;
|
|
|
|
// Fall back to old value, in case this option is not migrated yet.
|
|
let policy = value || record?.mailnotification || 'always';
|
|
|
|
let when = gettext('Always');
|
|
if (policy === 'failure') {
|
|
when = gettext('On failure only');
|
|
} else if (policy === 'never') {
|
|
when = gettext('Never');
|
|
}
|
|
|
|
// Notification-target takes precedence
|
|
let target = record?.['notification-target'] ||
|
|
record?.mailto ||
|
|
gettext('No target configured');
|
|
|
|
return `${when} (${target})`;
|
|
},
|
|
},
|
|
{
|
|
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'),
|
|
renderer: Ext.String.htmlEncode,
|
|
},
|
|
{
|
|
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: '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"),
|
|
width: 720,
|
|
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;
|
|
}
|
|
|
|
// Get rid of new-old parameters for notification settings.
|
|
// These should only be set for those selected few who ran
|
|
// pve-manager from pvetest.
|
|
if (!isCreate) {
|
|
Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-policy' });
|
|
Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-target' });
|
|
}
|
|
|
|
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 mode = me.lookup('modeSelector').getValue();
|
|
|
|
if (mode !== 'pool') {
|
|
return;
|
|
}
|
|
|
|
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();
|
|
}
|
|
},
|
|
|
|
compressionChange: function(f, value, oldValue) {
|
|
this.getView().lookup('backupAdvanced').updateCompression(value, f.isDisabled());
|
|
},
|
|
|
|
compressionDisable: function(f) {
|
|
this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), true);
|
|
},
|
|
|
|
compressionEnable: function(f) {
|
|
this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), false);
|
|
},
|
|
|
|
prepareValues: function(data) {
|
|
let me = this;
|
|
let viewModel = me.getViewModel();
|
|
|
|
// Migrate 'new'-old notification-policy back to old-old mailnotification.
|
|
// Only should affect users who used pve-manager from pvetest. This was a remnant of
|
|
// notifications before the overhaul.
|
|
let policy = data['notification-policy'];
|
|
if (policy === 'always' || policy === 'failure') {
|
|
data.mailnotification = policy;
|
|
}
|
|
|
|
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';
|
|
}
|
|
viewModel.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']);
|
|
}
|
|
|
|
if (data.performance) {
|
|
Object.assign(data, data.performance);
|
|
delete data.performance;
|
|
}
|
|
|
|
return data;
|
|
},
|
|
|
|
init: function(view) {
|
|
let me = this;
|
|
|
|
if (view.isCreate) {
|
|
me.lookup('modeSelector').setValue('include');
|
|
} else {
|
|
view.load({
|
|
success: function(response, _options) {
|
|
let values = me.prepareValues(response.result.data);
|
|
view.setValues(values);
|
|
},
|
|
});
|
|
}
|
|
},
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
selMode: 'include',
|
|
notificationMode: '__default__',
|
|
mailto: '',
|
|
mailNotification: 'always',
|
|
},
|
|
|
|
formulas: {
|
|
poolMode: (get) => get('selMode') === 'pool',
|
|
disableVMSelection: (get) => get('selMode') !== 'include' &&
|
|
get('selMode') !== 'exclude',
|
|
showMailtoFields: (get) =>
|
|
['auto', 'legacy-sendmail', '__default__'].includes(get('notificationMode')),
|
|
enableMailnotificationField: (get) => {
|
|
let mode = get('notificationMode');
|
|
let mailto = get('mailto');
|
|
|
|
return (['auto', '__default__'].includes(mode) && mailto) ||
|
|
mode === 'legacy-sendmail';
|
|
},
|
|
},
|
|
},
|
|
|
|
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: 'proxmoxKVComboBox',
|
|
comboItems: [
|
|
[
|
|
'__default__',
|
|
Ext.String.format(
|
|
gettext('{0} (Auto)'), Proxmox.Utils.defaultText,
|
|
),
|
|
],
|
|
['auto', gettext('Auto')],
|
|
['legacy-sendmail', gettext('Email (legacy)')],
|
|
['notification-system', gettext('Notification system')],
|
|
],
|
|
fieldLabel: gettext('Notification mode'),
|
|
name: 'notification-mode',
|
|
value: '__default__',
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
bind: {
|
|
value: '{notificationMode}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Send email to'),
|
|
name: 'mailto',
|
|
bind: {
|
|
hidden: '{!showMailtoFields}',
|
|
value: '{mailto}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveEmailNotificationSelector',
|
|
fieldLabel: gettext('Send email'),
|
|
name: 'mailnotification',
|
|
cbind: {
|
|
value: (get) => get('isCreate') ? 'always' : '',
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
bind: {
|
|
hidden: '{!showMailtoFields}',
|
|
disabled: '{!enableMailnotificationField}',
|
|
value: '{mailNotification}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveBackupCompressionSelector',
|
|
reference: 'compressionSelector',
|
|
fieldLabel: gettext('Compression'),
|
|
name: 'compress',
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
value: 'zstd',
|
|
listeners: {
|
|
change: 'compressionChange',
|
|
disable: 'compressionDisable',
|
|
enable: 'compressionEnable',
|
|
},
|
|
},
|
|
{
|
|
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}',
|
|
},
|
|
},
|
|
],
|
|
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.')
|
|
+ '<br>'
|
|
+ Ext.String.format(
|
|
gettext('Possible template variables are: {0}'),
|
|
PVE.Utils.notesTemplateVars.map(v => `<code>{{${v}}}</code>`).join(', '),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
xtype: 'pveBackupAdvancedOptionsPanel',
|
|
reference: 'backupAdvanced',
|
|
title: gettext('Advanced'),
|
|
cbind: {
|
|
isCreate: '{isCreate}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
Ext.create('PVE.dc.BackupEdit', {
|
|
autoShow: true,
|
|
jobid: rec.data.id,
|
|
listeners: {
|
|
destroy: () => reload(),
|
|
},
|
|
});
|
|
};
|
|
|
|
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', 'fleecing'].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:<br />' + errors.join('<br />'));
|
|
}
|
|
}
|
|
};
|
|
|
|
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',
|
|
renderer: Proxmox.Utils.renderEnabledIcon,
|
|
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',
|
|
},
|
|
{
|
|
title: gettext('Realms'),
|
|
xtype: 'panel',
|
|
layout: {
|
|
type: 'border',
|
|
},
|
|
groups: ['permissions'],
|
|
iconCls: 'fa fa-address-book-o',
|
|
itemId: 'domains',
|
|
items: [
|
|
{
|
|
xtype: 'pveAuthView',
|
|
region: 'center',
|
|
border: false,
|
|
},
|
|
{
|
|
xtype: 'pveRealmSyncJobView',
|
|
title: gettext('Realm Sync Jobs'),
|
|
region: 'south',
|
|
collapsible: true,
|
|
animCollapse: false,
|
|
border: false,
|
|
height: '50%',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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 x-fa-sdn-treelist',
|
|
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: 'VNets',
|
|
hidden: true,
|
|
iconCls: 'fa fa-network-wired x-fa-sdn-treelist',
|
|
itemId: 'sdnvnet',
|
|
},
|
|
{
|
|
xtype: 'pveSDNOptions',
|
|
groups: ['sdn'],
|
|
title: gettext('Options'),
|
|
hidden: true,
|
|
iconCls: 'fa fa-gear',
|
|
itemId: 'sdnoptions',
|
|
},
|
|
{
|
|
xtype: 'pveDhcpTree',
|
|
groups: ['sdn'],
|
|
title: gettext('IPAM'),
|
|
hidden: true,
|
|
iconCls: 'fa fa-map-signs',
|
|
itemId: 'sdnmappings',
|
|
});
|
|
}
|
|
|
|
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',
|
|
});
|
|
}
|
|
|
|
if (caps.mapping['Mapping.Audit'] ||
|
|
caps.mapping['Mapping.Use'] ||
|
|
caps.mapping['Mapping.Modify']) {
|
|
me.items.push(
|
|
{
|
|
xtype: 'container',
|
|
onlineHelp: 'resource_mapping',
|
|
title: gettext('Resource Mappings'),
|
|
itemId: 'resources',
|
|
iconCls: 'fa fa-folder-o',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch',
|
|
multi: true,
|
|
},
|
|
scrollable: true,
|
|
defaults: {
|
|
border: false,
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'pveDcPCIMapView',
|
|
title: gettext('PCI Devices'),
|
|
flex: 1,
|
|
},
|
|
{
|
|
xtype: 'splitter',
|
|
collapsible: false,
|
|
performCollapse: false,
|
|
},
|
|
{
|
|
xtype: 'pveDcUSBMapView',
|
|
title: gettext('USB Devices'),
|
|
flex: 1,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
}
|
|
|
|
if (caps.mapping['Mapping.Audit'] ||
|
|
caps.mapping['Mapping.Use'] ||
|
|
caps.mapping['Mapping.Modify']) {
|
|
me.items.push(
|
|
{
|
|
xtype: 'pmxNotificationConfigView',
|
|
title: gettext('Notifications'),
|
|
itemId: 'notification-targets',
|
|
iconCls: 'fa fa-bell-o',
|
|
baseUrl: '/cluster/notifications',
|
|
},
|
|
);
|
|
}
|
|
|
|
if (caps.dc['Sys.Audit']) {
|
|
me.items.push({
|
|
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: [
|
|
'<h3>' + gettext("Virtual Machines") + '</h3>',
|
|
'<div>',
|
|
'<div class="left-aligned">',
|
|
'<i class="good fa fa-fw fa-play-circle"> </i>',
|
|
gettext('Running'),
|
|
'</div>',
|
|
'<div class="right-aligned">{running}</div>',
|
|
'</div>',
|
|
'<tpl if="paused > 0">',
|
|
'<div>',
|
|
'<div class="left-aligned">',
|
|
'<i class="warning fa fa-fw fa-pause-circle"> </i>',
|
|
gettext('Paused'),
|
|
'</div>',
|
|
'<div class="right-aligned">{paused}</div>',
|
|
'</div>',
|
|
'</tpl>',
|
|
'<div>',
|
|
'<div class="left-aligned">',
|
|
'<i class="faded fa fa-fw fa-stop-circle"> </i>',
|
|
gettext('Stopped'),
|
|
'</div>',
|
|
'<div class="right-aligned">{stopped}</div>',
|
|
'</div>',
|
|
'<tpl if="template > 0">',
|
|
'<div>',
|
|
'<div class="left-aligned">',
|
|
'<i class="fa fa-fw fa-circle-o"> </i>',
|
|
gettext('Templates'),
|
|
'</div>',
|
|
'<div class="right-aligned">{template}</div>',
|
|
'</div>',
|
|
'</tpl>',
|
|
],
|
|
},
|
|
{
|
|
itemId: 'lxc',
|
|
data: {
|
|
running: 0,
|
|
paused: 0,
|
|
stopped: 0,
|
|
template: 0,
|
|
},
|
|
cls: 'centered-flex-column',
|
|
tpl: [
|
|
'<h3>' + gettext("LXC Container") + '</h3>',
|
|
'<div>',
|
|
'<div class="left-aligned">',
|
|
'<i class="good fa fa-fw fa-play-circle"> </i>',
|
|
gettext('Running'),
|
|
'</div>',
|
|
'<div class="right-aligned">{running}</div>',
|
|
'</div>',
|
|
'<tpl if="paused > 0">',
|
|
'<div>',
|
|
'<div class="left-aligned">',
|
|
'<i class="warning fa fa-fw fa-pause-circle"> </i>',
|
|
gettext('Paused'),
|
|
'</div>',
|
|
'<div class="right-aligned">{paused}</div>',
|
|
'</div>',
|
|
'</tpl>',
|
|
'<div>',
|
|
'<div class="left-aligned">',
|
|
'<i class="faded fa fa-fw fa-stop-circle"> </i>',
|
|
gettext('Stopped'),
|
|
'</div>',
|
|
'<div class="right-aligned">{stopped}</div>',
|
|
'</div>',
|
|
'<tpl if="template > 0">',
|
|
'<div>',
|
|
'<div class="left-aligned">',
|
|
'<i class="fa fa-fw fa-circle-o"> </i>',
|
|
gettext('Templates'),
|
|
'</div>',
|
|
'<div class="right-aligned">{template}</div>',
|
|
'</div>',
|
|
'</tpl>',
|
|
],
|
|
},
|
|
{
|
|
itemId: 'error',
|
|
colspan: 2,
|
|
data: {
|
|
num: 0,
|
|
},
|
|
columnWidth: 1,
|
|
padding: '10 250 0 250',
|
|
tpl: [
|
|
'<tpl if="num > 0">',
|
|
'<div class="left-aligned">',
|
|
'<i class="critical fa fa-fw fa-times-circle"> </i>',
|
|
gettext('Error'),
|
|
'</div>',
|
|
'<div class="right-aligned">{num}</div>',
|
|
'</tpl>',
|
|
],
|
|
},
|
|
],
|
|
|
|
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: [
|
|
'<h3>' + gettext('Nodes') + '</h3><br />',
|
|
'<div style="width: 150px;margin: auto;font-size: 12pt">',
|
|
'<div class="left-aligned">',
|
|
'<i class="good fa fa-fw fa-check"> </i>',
|
|
gettext('Online'),
|
|
'</div>',
|
|
'<div class="right-aligned">{online}</div>',
|
|
'<br /><br />',
|
|
'<div class="left-aligned">',
|
|
'<i class="critical fa fa-fw fa-times"> </i>',
|
|
gettext('Offline'),
|
|
'</div>',
|
|
'<div class="right-aligned">{offline}</div>',
|
|
'</div>',
|
|
],
|
|
},
|
|
{
|
|
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 '<i class="fa ' + PVE.Utils.get_health_icon(cls) + '"><i/>';
|
|
},
|
|
},
|
|
{
|
|
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_text_row('mac_prefix', gettext('MAC address prefix'), {
|
|
deleteEmpty: true,
|
|
vtype: 'MacPrefix',
|
|
defaultValue: 'BC:24:11',
|
|
});
|
|
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: `<span class='pmx-hint'>${gettext('Note:')}</span> `
|
|
+ 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: `<span class='pmx-hint'>${gettext('Note:')}</span> `
|
|
+ gettext('WebAuthn requires using a trusted certificate.'),
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
id: 'idChangeWarning',
|
|
hidden: true,
|
|
padding: '5 0 0 0',
|
|
html: '<i class="fa fa-exclamation-triangle warning"></i> '
|
|
+ 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 `<div class="proxmox-tags-${cls}">${tags}</div>`;
|
|
},
|
|
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: {
|
|
url: get => `/api2/extjs/pools/${!get('isCreate') ? '?poolid=' + 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,
|
|
},
|
|
],
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
me.callParent();
|
|
if (me.poolid) {
|
|
me.load({
|
|
success: function(response) {
|
|
let data = response.result.data;
|
|
if (Ext.isArray(data)) {
|
|
me.setValues(data[0]);
|
|
} else {
|
|
me.setValues(data);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
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();
|
|
},
|
|
getUrl: function(rec) {
|
|
return '/pools/?poolid=' + rec.getId();
|
|
},
|
|
});
|
|
|
|
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 caps = Ext.state.Manager.get('GuiCap');
|
|
let canEdit = !!caps.dc['Sys.Modify'];
|
|
|
|
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 || !canEdit) {
|
|
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'),
|
|
enableFn: rec => canEdit,
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor,
|
|
});
|
|
me.addBtn = new Proxmox.button.Button({
|
|
text: gettext('Create'),
|
|
disabled: !canEdit,
|
|
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: (rec) => canEdit && rec && me.base_url,
|
|
callback: () => reload(),
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
tbar: ['<b>' + gettext('Group') + ':</b>', 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: '<b>' + gettext('Rules') + ':</b>',
|
|
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: [
|
|
{
|
|
xtype: 'pveHealthWidget',
|
|
itemId: 'subscriptions',
|
|
userCls: 'pointer',
|
|
listeners: {
|
|
element: 'el',
|
|
click: function() {
|
|
if (this.component.userCls === 'pointer') {
|
|
window.open('https://www.proxmox.com/en/proxmox-virtual-environment/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: '<h1>No valid subscription</h1>' + PVE.Utils.noSubKeyHtml,
|
|
|
|
communityHtml: 'Please use the public community <a target="_blank" href="https://forum.proxmox.com">forum</a> for any questions.',
|
|
|
|
activeHtml: 'Please use our <a target="_blank" href="https://my.proxmox.com">support portal</a> for any questions. You can also use the public community <a target="_blank" href="https://forum.proxmox.com">forum</a> to get additional information.',
|
|
|
|
bugzillaHtml: '<h1>Bug Tracking</h1>Our bug tracking system is available <a target="_blank" href="https://bugzilla.proxmox.com">here</a>.',
|
|
|
|
docuHtml: function() {
|
|
var me = this;
|
|
var guideUrl = window.location.origin + me.pveGuidePath;
|
|
var text = Ext.String.format('<h1>Documentation</h1>'
|
|
+ 'The official Proxmox VE Administration Guide'
|
|
+ ' is included with this installation and can be browsed at '
|
|
+ '<a target="_blank" href="{0}">{0}</a>', guideUrl);
|
|
return text;
|
|
},
|
|
|
|
updateActive: function(data) {
|
|
var me = this;
|
|
|
|
var html = '<h1>' + data.productname + '</h1>' + me.activeHtml;
|
|
html += '<br><br>' + me.docuHtml();
|
|
html += '<br><br>' + me.bugzillaHtml;
|
|
|
|
me.update(html);
|
|
},
|
|
|
|
updateCommunity: function(data) {
|
|
var me = this;
|
|
|
|
var html = '<h1>' + data.productname + '</h1>' + me.communityHtml;
|
|
html += '<br><br>' + me.docuHtml();
|
|
html += '<br><br>' + 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} <span style='float:right;'>${realm}</span>`;
|
|
},
|
|
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' ||
|
|
data.keys === 'x!oath' ||
|
|
data.keys === 'x!u2f' ||
|
|
data.keys === 'x!yubico') {
|
|
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,
|
|
confirmCurrentPassword: Proxmox.UserName !== 'root@pam',
|
|
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(),
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
let unlock_btn = new Proxmox.button.Button({
|
|
text: gettext('Unlock TFA'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: rec => !!(caps.access['User.Modify'] &&
|
|
(rec.data['totp-locked'] || rec.data['tfa-locked-until'])),
|
|
handler: function(btn, event, rec) {
|
|
Ext.Msg.confirm(
|
|
Ext.String.format(gettext('Unlock TFA authentication for {0}'), rec.data.userid),
|
|
gettext("Locked 2nd factors can happen if the user's password was leaked. Are you sure you want to unlock the user?"),
|
|
function(btn_response) {
|
|
if (btn_response === 'yes') {
|
|
Proxmox.Utils.API2Request({
|
|
url: `/access/users/${rec.data.userid}/unlock-tfa`,
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
failure: function(response, options) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
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,
|
|
'-',
|
|
unlock_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: 120,
|
|
sortable: true,
|
|
renderer: function(v, metaData, record) {
|
|
let tfa_type = PVE.Parser.parseTfaType(v);
|
|
if (tfa_type === undefined) {
|
|
return Proxmox.Utils.noText;
|
|
}
|
|
|
|
if (tfa_type !== 1) {
|
|
return tfa_type;
|
|
}
|
|
|
|
let locked_until = record.data['tfa-locked-until'];
|
|
if (locked_until !== undefined) {
|
|
let now = new Date().getTime() / 1000;
|
|
if (locked_until > now) {
|
|
return gettext('Locked');
|
|
}
|
|
}
|
|
|
|
if (record.data['totp-locked']) {
|
|
return gettext('TOTP Locked');
|
|
}
|
|
|
|
return Proxmox.Utils.yesText;
|
|
},
|
|
dataIndex: 'keys',
|
|
},
|
|
{
|
|
header: gettext('Groups'),
|
|
dataIndex: 'groups',
|
|
renderer: Ext.htmlEncode,
|
|
flex: 2,
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
sortable: false,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'comment',
|
|
flex: 3,
|
|
},
|
|
],
|
|
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.dc.RealmSyncJobView', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pveRealmSyncJobView',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-realmsyncjobs',
|
|
|
|
emptyText: Ext.String.format(gettext('No {0} configured'), gettext('Realm Sync Job')),
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
addRealmSyncJob: function(button) {
|
|
let me = this;
|
|
Ext.create(`PVE.dc.RealmSyncJobEdit`, {
|
|
autoShow: true,
|
|
listeners: {
|
|
destroy: () => me.reload(),
|
|
},
|
|
});
|
|
},
|
|
|
|
editRealmSyncJob: function() {
|
|
let me = this;
|
|
let view = me.getView();
|
|
let selection = view.getSelection();
|
|
if (!selection || selection.length < 1) {
|
|
return;
|
|
}
|
|
|
|
Ext.create(`PVE.dc.RealmSyncJobEdit`, {
|
|
jobid: selection[0].data.id,
|
|
autoShow: true,
|
|
listeners: {
|
|
destroy: () => me.reload(),
|
|
},
|
|
});
|
|
},
|
|
|
|
runNow: function() {
|
|
let me = this;
|
|
let view = me.getView();
|
|
let selection = view.getSelection();
|
|
if (!selection || selection.length < 1) {
|
|
return;
|
|
}
|
|
|
|
let params = selection[0].data;
|
|
let realm = params.realm;
|
|
|
|
let propertiesToDelete = ['comment', 'realm', 'id', 'type', 'schedule', 'last-run', 'next-run', 'enabled'];
|
|
for (const prop of propertiesToDelete) {
|
|
delete params[prop];
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: `/access/domains/${realm}/sync`,
|
|
params,
|
|
waitMsgTarget: view,
|
|
method: 'POST',
|
|
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
|
success: function(response, options) {
|
|
Ext.create('Proxmox.window.TaskProgress', {
|
|
autoShow: true,
|
|
upid: response.result.data,
|
|
taskDone: () => { me.reload(); },
|
|
});
|
|
},
|
|
});
|
|
},
|
|
|
|
reload: function() {
|
|
this.getView().getStore().load();
|
|
},
|
|
},
|
|
|
|
store: {
|
|
autoLoad: true,
|
|
id: 'realm-syncs',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/cluster/jobs/realm-sync',
|
|
},
|
|
},
|
|
|
|
viewConfig: {
|
|
getRowClass: (record, _index) => record.get('enabled') ? '' : 'proxmox-disabled-row',
|
|
},
|
|
|
|
columns: [
|
|
{
|
|
header: gettext('Enabled'),
|
|
width: 80,
|
|
dataIndex: 'enabled',
|
|
sortable: true,
|
|
align: 'center',
|
|
stopSelection: false,
|
|
renderer: Proxmox.Utils.renderEnabledIcon,
|
|
},
|
|
{
|
|
text: gettext('Name'),
|
|
flex: 1,
|
|
dataIndex: 'id',
|
|
hidden: true,
|
|
},
|
|
{
|
|
text: gettext('Realm'),
|
|
width: 200,
|
|
dataIndex: 'realm',
|
|
},
|
|
{
|
|
header: gettext('Schedule'),
|
|
width: 150,
|
|
dataIndex: 'schedule',
|
|
},
|
|
{
|
|
text: gettext('Next Run'),
|
|
dataIndex: 'next-run',
|
|
width: 150,
|
|
renderer: PVE.Utils.render_next_event,
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
renderer: Ext.htmlEncode,
|
|
sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''),
|
|
flex: 1,
|
|
},
|
|
],
|
|
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
handler: 'addRealmSyncJob',
|
|
},
|
|
{
|
|
text: gettext('Edit'),
|
|
xtype: 'proxmoxButton',
|
|
handler: 'editRealmSyncJob',
|
|
disabled: true,
|
|
},
|
|
{
|
|
xtype: 'proxmoxStdRemoveButton',
|
|
baseurl: `/api2/extjs/cluster/jobs/realm-sync`,
|
|
callback: 'reload',
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
handler: 'runNow',
|
|
disabled: true,
|
|
text: gettext('Run Now'),
|
|
},
|
|
],
|
|
|
|
listeners: {
|
|
itemdblclick: 'editRealmSyncJob',
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.callParent();
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.getStore());
|
|
},
|
|
});
|
|
|
|
Ext.define('PVE.dc.RealmSyncJobEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
subject: gettext('Realm Sync Job'),
|
|
onlineHelp: 'pveum_ldap_sync',
|
|
|
|
// don't focus the schedule field on edit
|
|
defaultFocus: 'field[name=id]',
|
|
|
|
cbindData: function() {
|
|
let me = this;
|
|
me.isCreate = !me.jobid;
|
|
me.jobid = me.jobid || "";
|
|
let url = '/api2/extjs/cluster/jobs/realm-sync';
|
|
me.url = me.jobid ? `${url}/${me.jobid}` : url;
|
|
me.method = me.isCreate ? 'POST' : 'PUT';
|
|
if (!me.isCreate) {
|
|
me.subject = `${me.subject}: ${me.jobid}`;
|
|
}
|
|
return {};
|
|
},
|
|
|
|
submitUrl: function(url, values) {
|
|
return this.isCreate ? `${url}/${values.id}` : url;
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
updateDefaults: function(_field, newValue) {
|
|
let me = this;
|
|
|
|
['scope', 'enable-new', 'schedule'].forEach((reference) => {
|
|
me.lookup(reference)?.setDisabled(false);
|
|
});
|
|
|
|
// only update on create
|
|
if (!me.getView().isCreate) {
|
|
return;
|
|
}
|
|
Proxmox.Utils.API2Request({
|
|
url: `/access/domains/${newValue}`,
|
|
success: function(response) {
|
|
// first reset the fields to their default
|
|
['acl', 'entry', 'properties'].forEach(opt => {
|
|
me.lookup(`remove-vanished-${opt}`)?.setValue(false);
|
|
});
|
|
me.lookup('enable-new')?.setValue('1');
|
|
me.lookup('scope')?.setValue(undefined);
|
|
|
|
let options = response?.result?.data?.['sync-defaults-options'];
|
|
if (options) {
|
|
let parsed = PVE.Parser.parsePropertyString(options);
|
|
if (parsed['remove-vanished']) {
|
|
let opts = parsed['remove-vanished'].split(';');
|
|
for (const opt of opts) {
|
|
me.lookup(`remove-vanished-${opt}`)?.setValue(true);
|
|
}
|
|
delete parsed['remove-vanished'];
|
|
}
|
|
for (const [name, value] of Object.entries(parsed)) {
|
|
me.lookup(name)?.setValue(value);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
},
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'inputpanel',
|
|
|
|
cbind: {
|
|
isCreate: '{isCreate}',
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
|
|
let vanished_opts = [];
|
|
['acl', 'entry', 'properties'].forEach((prop) => {
|
|
if (values[`remove-vanished-${prop}`]) {
|
|
vanished_opts.push(prop);
|
|
}
|
|
delete values[`remove-vanished-${prop}`];
|
|
});
|
|
|
|
if (!values.id && me.isCreate) {
|
|
values.id = 'realmsync-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
|
|
}
|
|
|
|
if (vanished_opts.length > 0) {
|
|
values['remove-vanished'] = vanished_opts.join(';');
|
|
} else {
|
|
values['remove-vanished'] = 'none';
|
|
}
|
|
|
|
PVE.Utils.delete_if_default(values, 'node', '');
|
|
|
|
if (me.isCreate) {
|
|
delete values.delete; // on create we cannot delete values
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
editConfig: {
|
|
xtype: 'pmxRealmComboBox',
|
|
storeFilter: rec => rec.data.type === 'ldap' || rec.data.type === 'ad',
|
|
},
|
|
listConfig: {
|
|
emptyText: `<div class="x-grid-empty">${gettext('No LDAP/AD Realm found')}</div>`,
|
|
},
|
|
cbind: {
|
|
editable: '{isCreate}',
|
|
},
|
|
listeners: {
|
|
change: 'updateDefaults',
|
|
},
|
|
fieldLabel: gettext('Realm'),
|
|
name: 'realm',
|
|
reference: 'realm',
|
|
},
|
|
{
|
|
xtype: 'pveCalendarEvent',
|
|
fieldLabel: gettext('Schedule'),
|
|
disabled: true,
|
|
allowBlank: false,
|
|
name: 'schedule',
|
|
reference: 'schedule',
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Enable Job'),
|
|
name: 'enabled',
|
|
reference: 'enabled',
|
|
uncheckedValue: 0,
|
|
defaultValue: 1,
|
|
checked: true,
|
|
},
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'scope',
|
|
reference: 'scope',
|
|
disabled: true,
|
|
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')],
|
|
],
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
value: '1',
|
|
deleteEmpty: false,
|
|
disabled: true,
|
|
allowBlank: false,
|
|
comboItems: [
|
|
['1', Proxmox.Utils.yesText],
|
|
['0', Proxmox.Utils.noText],
|
|
],
|
|
name: 'enable-new',
|
|
reference: 'enable-new',
|
|
fieldLabel: gettext('Enable New'),
|
|
},
|
|
],
|
|
|
|
columnB: [
|
|
{
|
|
xtype: 'fieldset',
|
|
title: gettext('Remove Vanished Options'),
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('ACL'),
|
|
name: 'remove-vanished-acl',
|
|
reference: 'remove-vanished-acl',
|
|
boxLabel: gettext('Remove ACLs of vanished users and groups.'),
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Entry'),
|
|
name: 'remove-vanished-entry',
|
|
reference: 'remove-vanished-entry',
|
|
boxLabel: gettext('Remove vanished user and group entries.'),
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Properties'),
|
|
name: 'remove-vanished-properties',
|
|
reference: 'remove-vanished-properties',
|
|
boxLabel: gettext('Remove vanished properties from synced users.'),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'comment',
|
|
fieldLabel: gettext('Job Comment'),
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Description of the job'),
|
|
},
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'defaulthint',
|
|
value: gettext('Default sync options can be set by editing the realm.'),
|
|
userCls: 'pmx-hint',
|
|
hidden: true,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
me.callParent();
|
|
if (me.jobid) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
let values = response.result.data;
|
|
|
|
if (values['remove-vanished']) {
|
|
let opts = values['remove-vanished'].split(';');
|
|
for (const opt of opts) {
|
|
values[`remove-vanished-${opt}`] = 1;
|
|
}
|
|
}
|
|
me.down('inputpanel').setValues(values);
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
Ext.define('pve-resource-pci-tree', {
|
|
extend: 'Ext.data.Model',
|
|
idProperty: 'internalId',
|
|
fields: ['type', 'text', 'path', 'id', 'subsystem-id', 'iommugroup', 'description', 'digest'],
|
|
});
|
|
|
|
Ext.define('PVE.dc.PCIMapView', {
|
|
extend: 'PVE.tree.ResourceMapTree',
|
|
alias: 'widget.pveDcPCIMapView',
|
|
|
|
editWindowClass: 'PVE.window.PCIMapEditWindow',
|
|
baseUrl: '/cluster/mapping/pci',
|
|
mapIconCls: 'pve-itype-icon-pci',
|
|
getStatusCheckUrl: (node) => `/nodes/${node}/hardware/pci?pci-class-blacklist=`,
|
|
entryIdProperty: 'path',
|
|
|
|
checkValidity: function(data, node) {
|
|
let me = this;
|
|
let ids = {};
|
|
data.forEach((entry) => {
|
|
ids[entry.id] = entry;
|
|
});
|
|
me.getRootNode()?.cascade(function(rec) {
|
|
if (rec.data.node !== node || rec.data.type !== 'map') {
|
|
return;
|
|
}
|
|
|
|
let id = rec.data.path;
|
|
if (!id.match(/\.\d$/)) {
|
|
id += '.0';
|
|
}
|
|
let device = ids[id];
|
|
if (!device) {
|
|
rec.set('valid', 0);
|
|
rec.set('errmsg', Ext.String.format(gettext("Cannot find PCI id {0}"), id));
|
|
rec.commit();
|
|
return;
|
|
}
|
|
|
|
|
|
let deviceId = `${device.vendor}:${device.device}`.replace(/0x/g, '');
|
|
let subId = `${device.subsystem_vendor}:${device.subsystem_device}`.replace(/0x/g, '');
|
|
|
|
let toCheck = {
|
|
id: deviceId,
|
|
'subsystem-id': subId,
|
|
iommugroup: device.iommugroup !== -1 ? device.iommugroup : undefined,
|
|
};
|
|
|
|
let valid = 1;
|
|
let errors = [];
|
|
let errText = gettext("Configuration for {0} not correct ('{1}' != '{2}')");
|
|
for (const [key, validValue] of Object.entries(toCheck)) {
|
|
if (`${rec.data[key]}` !== `${validValue}`) {
|
|
errors.push(Ext.String.format(errText, key, rec.data[key] ?? '', validValue));
|
|
valid = 0;
|
|
}
|
|
}
|
|
|
|
rec.set('valid', valid);
|
|
rec.set('errmsg', errors.join('<br>'));
|
|
rec.commit();
|
|
});
|
|
},
|
|
|
|
store: {
|
|
sorters: 'text',
|
|
model: 'pve-resource-pci-tree',
|
|
data: {},
|
|
},
|
|
|
|
columns: [
|
|
{
|
|
xtype: 'treecolumn',
|
|
text: gettext('ID/Node/Path'),
|
|
dataIndex: 'text',
|
|
width: 200,
|
|
},
|
|
{
|
|
text: gettext('Vendor/Device'),
|
|
dataIndex: 'id',
|
|
},
|
|
{
|
|
text: gettext('Subsystem Vendor/Device'),
|
|
dataIndex: 'subsystem-id',
|
|
},
|
|
{
|
|
text: gettext('IOMMU-Group'),
|
|
dataIndex: 'iommugroup',
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'valid',
|
|
flex: 1,
|
|
renderer: 'renderStatus',
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'description',
|
|
renderer: function(value, _meta, record) {
|
|
return Ext.String.htmlEncode(value ?? record.data.comment);
|
|
},
|
|
flex: 1,
|
|
},
|
|
],
|
|
});
|
|
Ext.define('pve-resource-usb-tree', {
|
|
extend: 'Ext.data.Model',
|
|
idProperty: 'internalId',
|
|
fields: ['type', 'text', 'path', 'id', 'description', 'digest'],
|
|
});
|
|
|
|
Ext.define('PVE.dc.USBMapView', {
|
|
extend: 'PVE.tree.ResourceMapTree',
|
|
alias: 'widget.pveDcUSBMapView',
|
|
|
|
editWindowClass: 'PVE.window.USBMapEditWindow',
|
|
baseUrl: '/cluster/mapping/usb',
|
|
mapIconCls: 'fa fa-usb',
|
|
getStatusCheckUrl: (node) => `/nodes/${node}/hardware/usb`,
|
|
entryIdProperty: 'id',
|
|
|
|
checkValidity: function(data, node) {
|
|
let me = this;
|
|
let ids = {};
|
|
let paths = {};
|
|
data.forEach((entry) => {
|
|
ids[`${entry.vendid}:${entry.prodid}`] = entry;
|
|
paths[`${entry.busnum}-${entry.usbpath}`] = entry;
|
|
});
|
|
me.getRootNode()?.cascade(function(rec) {
|
|
if (rec.data.node !== node || rec.data.type !== 'map') {
|
|
return;
|
|
}
|
|
|
|
let device;
|
|
if (rec.data.path) {
|
|
device = paths[rec.data.path];
|
|
}
|
|
device ??= ids[rec.data.id];
|
|
|
|
if (!device) {
|
|
rec.set('valid', 0);
|
|
rec.set('errmsg', Ext.String.format(gettext("Cannot find USB device {0}"), rec.data.id));
|
|
rec.commit();
|
|
return;
|
|
}
|
|
|
|
|
|
let deviceId = `${device.vendid}:${device.prodid}`.replace(/0x/g, '');
|
|
|
|
let toCheck = {
|
|
id: deviceId,
|
|
};
|
|
|
|
let valid = 1;
|
|
let errors = [];
|
|
let errText = gettext("Configuration for {0} not correct ('{1}' != '{2}')");
|
|
for (const [key, validValue] of Object.entries(toCheck)) {
|
|
if (rec.data[key] !== validValue) {
|
|
errors.push(Ext.String.format(errText, key, rec.data[key] ?? '', validValue));
|
|
valid = 0;
|
|
}
|
|
}
|
|
|
|
rec.set('valid', valid);
|
|
rec.set('errmsg', errors.join('<br>'));
|
|
rec.commit();
|
|
});
|
|
},
|
|
|
|
store: {
|
|
sorters: 'text',
|
|
model: 'pve-resource-usb-tree',
|
|
data: {},
|
|
},
|
|
|
|
columns: [
|
|
{
|
|
xtype: 'treecolumn',
|
|
text: gettext('ID/Node/Vendor&Device'),
|
|
dataIndex: 'text',
|
|
width: 200,
|
|
},
|
|
{
|
|
text: gettext('Path'),
|
|
dataIndex: 'path',
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'valid',
|
|
flex: 1,
|
|
renderer: 'renderStatus',
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'description',
|
|
renderer: function(value, _meta, record) {
|
|
return Ext.String.htmlEncode(value ?? record.data.comment);
|
|
},
|
|
flex: 1,
|
|
},
|
|
],
|
|
});
|
|
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.Utils.isStandaloneNode();
|
|
|
|
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: () => {
|
|
Ext.create('PVE.GuestStop', {
|
|
nodename: info.node,
|
|
vm: info,
|
|
autoShow: true,
|
|
});
|
|
},
|
|
},
|
|
{
|
|
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'],
|
|
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
|
|
handler: function() {
|
|
Ext.create('PVE.GuestStop', {
|
|
nodename: nodename,
|
|
vm: vm,
|
|
autoShow: true,
|
|
});
|
|
},
|
|
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.Utils.isStandaloneNode(),
|
|
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: [
|
|
'<tpl if="lock">',
|
|
'<i class="fa fa-lg fa-lock"></i> ({lock})',
|
|
'</tpl>',
|
|
],
|
|
});
|
|
|
|
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.Audit']) {
|
|
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',
|
|
},
|
|
);
|
|
}
|
|
|
|
if (caps.vms['VM.Console']) {
|
|
me.items.push(
|
|
{
|
|
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',
|
|
log_select_timespan: true,
|
|
submitFormat: 'U',
|
|
},
|
|
);
|
|
}
|
|
|
|
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: 'textarea',
|
|
name: 'ssh-public-keys',
|
|
value: '',
|
|
fieldLabel: gettext('SSH public key(s)'),
|
|
allowBlank: true,
|
|
validator: function(value) {
|
|
let pwfield = this.up().down('field[name=password]');
|
|
if (value.length) {
|
|
let keys = value.indexOf('\n') !== -1 ? value.split('\n') : [value];
|
|
if (keys.some(key => key !== '' && !PVE.Parser.parseSSHKey(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: 'pveMultiFileButton',
|
|
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('textarea[name=ssh-public-keys]');
|
|
for (const file of e?.target?.files ?? []) {
|
|
PVE.Utils.loadSSHKeyFromFile(file, v => {
|
|
let oldValue = field.getValue();
|
|
field.setValue(oldValue ? `${oldValue}\n${v.trim()}` : v.trim());
|
|
});
|
|
}
|
|
btn.reset();
|
|
},
|
|
},
|
|
},
|
|
],
|
|
advancedColumnB: [
|
|
{
|
|
xtype: 'pveTagFieldSet',
|
|
name: 'tags',
|
|
maxHeight: 150,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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.DeviceInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
autoComplete: false,
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
},
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
let me = this;
|
|
me.vmconfig = vmconfig;
|
|
|
|
if (me.isCreate) {
|
|
PVE.Utils.forEachLxcDev((i, name) => {
|
|
if (!Ext.isDefined(vmconfig[name])) {
|
|
me.confid = name;
|
|
me.down('field[name=devid]').setValue(i);
|
|
return false;
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
let confid = me.isCreate ? "dev" + values.devid : me.confid;
|
|
delete values.devid;
|
|
let val = PVE.Parser.printPropertyString(values, 'path');
|
|
let ret = {};
|
|
ret[confid] = val;
|
|
return ret;
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'devid',
|
|
minValue: 0,
|
|
maxValue: PVE.Utils.lxc_dev_count - 1,
|
|
hidden: true,
|
|
allowBlank: false,
|
|
disabled: true,
|
|
cbind: {
|
|
disabled: '{!isCreate}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'path',
|
|
fieldLabel: gettext('Device Path'),
|
|
labelWidth: 120,
|
|
editable: true,
|
|
allowBlank: false,
|
|
emptyText: '/dev/xyz',
|
|
validator: v => v.startsWith('/dev/') ? true : gettext("Path has to start with /dev/"),
|
|
},
|
|
],
|
|
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'uid',
|
|
editable: true,
|
|
fieldLabel: Ext.String.format(gettext('{0} in CT'), 'UID'),
|
|
labelWidth: 120,
|
|
emptyText: '0',
|
|
minValue: 0,
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'gid',
|
|
editable: true,
|
|
fieldLabel: Ext.String.format(gettext('{0} in CT'), 'GID'),
|
|
labelWidth: 120,
|
|
emptyText: '0',
|
|
minValue: 0,
|
|
},
|
|
],
|
|
|
|
advancedColumn2: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'mode',
|
|
editable: true,
|
|
fieldLabel: Ext.String.format(gettext('Access Mode in CT')),
|
|
labelWidth: 120,
|
|
emptyText: '0660',
|
|
validator: function(value) {
|
|
if (/^0[0-7]{3}$|^$/i.test(value)) {
|
|
return true;
|
|
}
|
|
return gettext("Access mode has to be an octal number");
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
Ext.define('PVE.lxc.DeviceEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
vmconfig: undefined,
|
|
|
|
isAdd: true,
|
|
width: 450,
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
me.isCreate = !me.confid;
|
|
|
|
let ipanel = Ext.create('PVE.lxc.DeviceInputPanel', {
|
|
confid: me.confid,
|
|
isCreate: me.isCreate,
|
|
pveSelNode: me.pveSelNode,
|
|
});
|
|
|
|
let subject;
|
|
if (me.isCreate) {
|
|
subject = gettext('Device');
|
|
} else {
|
|
subject = gettext('Device') + ' (' + me.confid + ')';
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
subject: subject,
|
|
items: [ipanel],
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
ipanel.setVMConfig(response.result.data);
|
|
if (me.isCreate) {
|
|
return;
|
|
}
|
|
|
|
let data = PVE.Parser.parsePropertyString(response.result.data[me.confid], 'path');
|
|
|
|
let values = {
|
|
path: data.path,
|
|
mode: data.mode,
|
|
uid: data.uid,
|
|
gid: data.gid,
|
|
};
|
|
|
|
ipanel.setValues(values);
|
|
},
|
|
});
|
|
},
|
|
});
|
|
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.forEachLxcMP((bus, i, name) => {
|
|
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.lxc_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')} (${gettext('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 + "<br>" + 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 + "<br>" + 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 = `<i class='pve-grid-fa fa fa-fw fa-${rowdef.iconCls}'></i>`;
|
|
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;
|
|
|
|
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.forEachLxcMP(function(bus, i, confid) {
|
|
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);
|
|
|
|
let deveditor = Proxmox.UserName === 'root@pam' ? 'PVE.lxc.DeviceEdit' : undefined;
|
|
|
|
PVE.Utils.forEachLxcDev(function(i, confid) {
|
|
rows[confid] = {
|
|
group: 7,
|
|
order: i,
|
|
tdCls: 'pve-itype-icon-pci',
|
|
editor: deveditor,
|
|
header: gettext('Device') + ' (' + confid + ')',
|
|
};
|
|
});
|
|
|
|
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 isDevice = key.match(/^dev\d+/);
|
|
|
|
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 || isDevice) || 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(),
|
|
},
|
|
});
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Device Passthrough'),
|
|
iconCls: 'pve-itype-icon-pci',
|
|
disabled: Proxmox.UserName !== 'root@pam',
|
|
handler: function() {
|
|
Ext.create('PVE.lxc.DeviceEdit', {
|
|
autoShow: true,
|
|
url: `/api2/extjs/${baseurl}`,
|
|
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.lxc_mp_counts.mp + 1,
|
|
|
|
getNextFreeDisk: function(vmconfig) {
|
|
let nextFreeDisk;
|
|
if (!vmconfig.rootfs) {
|
|
return {
|
|
confid: 'rootfs',
|
|
};
|
|
} else {
|
|
for (let i = 0; i < PVE.Utils.lxc_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.Utils.isStandaloneNode();
|
|
|
|
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: `<h3>Ceph?</h3>
|
|
<blockquote cite="https://ceph.com/"><p>"<b>Ceph</b> is a unified,
|
|
distributed storage system, designed for excellent performance, reliability,
|
|
and scalability."</p></blockquote>
|
|
<p>
|
|
<b>Ceph</b> is currently <b>not installed</b> 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.
|
|
</p>
|
|
<p>
|
|
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 <a target="_blank" href="https://docs.ceph.com/en/latest/">ceph.com</a>.
|
|
</p>`,
|
|
});
|
|
|
|
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: "quincy", version: "17.2" },
|
|
{ release: "reef", version: "18.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',
|
|
18: 'reef',
|
|
19: 'squid',
|
|
};
|
|
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,
|
|
|
|
width: 760, // 4:3
|
|
height: 570,
|
|
|
|
viewModel: {
|
|
data: {
|
|
nodename: '',
|
|
cephRelease: 'reef',
|
|
cephRepo: 'enterprise',
|
|
configuration: true,
|
|
isInstalled: false,
|
|
nodeHasSubscription: true, // avoid warning hint until fully loaded
|
|
allHaveSubscription: true, // avoid warning hint until fully loaded
|
|
},
|
|
formulas: {
|
|
repoHintHidden: get => get('allHaveSubscription') && get('cephRepo') === 'enterprise',
|
|
repoHint: function(get) {
|
|
let repo = get('cephRepo');
|
|
let nodeSub = get('nodeHasSubscription'), allSub = get('allHaveSubscription');
|
|
|
|
if (repo === 'enterprise') {
|
|
if (!nodeSub) {
|
|
return gettext('The enterprise repository is enabled, but there is no active subscription!');
|
|
} else if (!allSub) {
|
|
return gettext('Not all nodes have an active subscription, which is required for cluster-wide enterprise repo access');
|
|
}
|
|
return ''; // should be hidden
|
|
} else if (repo === 'no-subscription') {
|
|
return allSub
|
|
? gettext("Cluster has active subscriptions and would be elligible for using the enterprise repository.")
|
|
: gettext("The no-subscription repository is not the best choice for production setups.");
|
|
} else {
|
|
return gettext('The test repository should only be used for test setups or after consulting the official Proxmox support!');
|
|
}
|
|
},
|
|
},
|
|
},
|
|
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);
|
|
let viewModel = this.getViewModel();
|
|
var isInstalled = this.getViewModel().get('isInstalled');
|
|
if (isInstalled) {
|
|
viewModel.set('configuration', false);
|
|
this.setInitialTab(2);
|
|
}
|
|
|
|
PVE.Utils.getClusterSubscriptionLevel().then(subcriptionMap => {
|
|
viewModel.set('nodeHasSubscription', !!subcriptionMap[this.nodename]);
|
|
|
|
let allHaveSubscription = Object.values(subcriptionMap).every(level => !!level);
|
|
viewModel.set('allHaveSubscription', allHaveSubscription);
|
|
});
|
|
},
|
|
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: 'displayfield',
|
|
fieldLabel: gettext('Hint'),
|
|
labelClsExtra: 'pmx-hint',
|
|
submitValue: false,
|
|
labelWidth: 50,
|
|
bind: {
|
|
value: '{repoHint}',
|
|
hidden: '{repoHintHidden}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveCephHighestVersionDisplay',
|
|
labelWidth: 150,
|
|
cbind: {
|
|
nodename: '{nodename}',
|
|
},
|
|
gotNewestVersion: function(release, maxversiontext, maxversion) {
|
|
if (release === 'unknown') {
|
|
return;
|
|
}
|
|
let wizard = this.up('pveCephInstallWizard');
|
|
wizard.getViewModel().set('cephRelease', release);
|
|
},
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
layout: 'hbox',
|
|
defaults: {
|
|
border: false,
|
|
layout: 'anchor',
|
|
flex: 1,
|
|
},
|
|
items: [{
|
|
xtype: 'pveCephVersionSelector',
|
|
labelWidth: 150,
|
|
padding: '0 10 0 0',
|
|
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),
|
|
);
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
fieldLabel: gettext('Repository'),
|
|
padding: '0 0 0 10',
|
|
comboItems: [
|
|
['enterprise', gettext('Enterprise (recommended)')],
|
|
['no-subscription', gettext('No-Subscription')],
|
|
['test', gettext('Test')],
|
|
],
|
|
labelWidth: 150,
|
|
submitValue: false,
|
|
value: 'enterprise',
|
|
bind: {
|
|
value: '{cephRepo}',
|
|
},
|
|
}],
|
|
},
|
|
],
|
|
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');
|
|
let repo = wizard.getViewModel().get('cephRepo');
|
|
me.cmdOpts = `--version\0${release}\0--repository\0${repo}`;
|
|
},
|
|
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: '<h3>Installation successful!</h3>'+
|
|
'<p>The basic installation and configuration is complete. Depending on your setup, some of the following steps are required to start using Ceph:</p>'+
|
|
'<ol><li>Install Ceph on other nodes</li>'+
|
|
'<li>Create additional Ceph Monitors</li>'+
|
|
'<li>Create Ceph OSDs</li>'+
|
|
'<li>Create Ceph Pools</li></ol>'+
|
|
'<p>To learn more, click on the help button below.</p>',
|
|
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: gettext('Data Pool'),
|
|
flex: 1,
|
|
dataIndex: 'data_pool',
|
|
},
|
|
{
|
|
header: gettext('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: gettext('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')} (${gettext('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: gettext('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: gettext('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')} (${gettext('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 ' +
|
|
'<a target="_blank" href="' + Proxmox.Utils.get_help_link('chapter_pveceph') + '">the reference documentation</a>.',
|
|
},
|
|
],
|
|
});
|
|
|
|
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) +
|
|
"<br>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 + ' <i class="fa ' + updownicon + '"></i> / ' +
|
|
inout + ' <i class="fa ' + inouticon + '"></i>';
|
|
|
|
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<br>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: [
|
|
'<tpl if="osd">',
|
|
'osd.{osd}:',
|
|
'<tpl else>',
|
|
gettext('No OSD selected'),
|
|
'</tpl>',
|
|
],
|
|
},
|
|
{
|
|
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 (PSS)'),
|
|
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')}<br>(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')}<br>(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: gettext('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',
|
|
|
|
defaultSize: undefined,
|
|
defaultMinSize: undefined,
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
init: function(view) {
|
|
let vm = this.getViewModel();
|
|
vm.set('size', Number(view.defaultSize));
|
|
vm.set('minSize', Number(view.defaultMinSize));
|
|
},
|
|
sizeChange: function(field, val) {
|
|
let vm = this.getViewModel();
|
|
let minSize = Math.round(val / 2);
|
|
if (minSize > 1) {
|
|
vm.set('minSize', minSize);
|
|
}
|
|
vm.set('size', val); // bind does not work in a pmxDisplayEditField, update manually
|
|
},
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
minSize: null,
|
|
size: null,
|
|
},
|
|
formulas: {
|
|
minSizeLabel: (get) => {
|
|
if (get('showMinSizeOneWarning') || get('showMinSizeHalfWarning')) {
|
|
return `${gettext('Min. Size')} <i class="fa fa-exclamation-triangle warning"></i>`;
|
|
}
|
|
return gettext('Min. Size');
|
|
},
|
|
showMinSizeOneWarning: (get) => get('minSize') === 1,
|
|
showMinSizeHalfWarning: (get) => {
|
|
let minSize = get('minSize');
|
|
let size = get('size');
|
|
if (minSize === 1) {
|
|
return false;
|
|
}
|
|
return minSize < (size / 2) && minSize !== size;
|
|
},
|
|
},
|
|
},
|
|
|
|
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',
|
|
cbind: {
|
|
value: (get) => get('defaultSize'),
|
|
},
|
|
minValue: 2,
|
|
maxValue: 7,
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: 'sizeChange',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
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',
|
|
bind: {
|
|
fieldLabel: '{minSizeLabel}',
|
|
value: '{minSize}',
|
|
},
|
|
name: 'min_size',
|
|
cbind: {
|
|
value: (get) => get('defaultMinSize'),
|
|
minValue: (get) => {
|
|
if (Number(get('defaultMinSize')) === 1) {
|
|
return 1;
|
|
} else {
|
|
return get('isCreate') ? 2 : 1;
|
|
}
|
|
},
|
|
},
|
|
maxValue: 7,
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
bind: {
|
|
hidden: '{!showMinSizeHalfWarning}',
|
|
},
|
|
hidden: true,
|
|
userCls: 'pmx-hint',
|
|
value: gettext('min_size < size/2 can lead to data loss, incomplete PGs or unfound objects.'),
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
bind: {
|
|
hidden: '{!showMinSizeOneWarning}',
|
|
},
|
|
hidden: true,
|
|
userCls: 'pmx-hint',
|
|
value: gettext('a min_size of 1 is not recommended and can lead to data loss'),
|
|
},
|
|
{
|
|
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,
|
|
defaultSize: undefined,
|
|
defaultMinSize: undefined,
|
|
},
|
|
|
|
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}',
|
|
defaultSize: '{defaultSize}',
|
|
defaultMinSize: '{defaultMinSize}',
|
|
},
|
|
}],
|
|
});
|
|
|
|
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('Pool #'),
|
|
minWidth: 70,
|
|
flex: 1,
|
|
align: 'right',
|
|
sortable: true,
|
|
dataIndex: 'pool',
|
|
},
|
|
{
|
|
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 = '<i class="fa fa-info-circle faded"></i> 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 = '<i class="fa fa-info-circle faded"></i> ' + 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() {
|
|
let keys = [
|
|
'global:osd-pool-default-min-size',
|
|
'global:osd-pool-default-size',
|
|
];
|
|
let params = {
|
|
'config-keys': keys.join(';'),
|
|
};
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/localhost/ceph/cfg/value',
|
|
method: 'GET',
|
|
params,
|
|
waitMsgTarget: me.getView(),
|
|
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
|
success: function({ result: { data } }) {
|
|
let global = data.global;
|
|
let defaultSize = global?.['osd-pool-default-size'] ?? 3;
|
|
let defaultMinSize = global?.['osd-pool-default-min-size'] ?? 2;
|
|
|
|
Ext.create('PVE.Ceph.PoolEdit', {
|
|
title: gettext('Create') + ': Ceph Pool',
|
|
isCreate: true,
|
|
isErasure: false,
|
|
defaultSize,
|
|
defaultMinSize,
|
|
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 = '<b>' + mgr.title + '</b>';
|
|
mgr.statuses.push(gettext('Status') + ': <b>active</b>');
|
|
} 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 = '<b>' + mds.title + '</b>';
|
|
mds.statuses.push(gettext('Status') + ': <b>' + mdsmessages[mds.name]+"</b>");
|
|
} 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('<br>');
|
|
|
|
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: '<h3>{title}</h3>',
|
|
},
|
|
],
|
|
|
|
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}: ',
|
|
'<i class="fa fa-fw {iconCls}"></i>',
|
|
],
|
|
|
|
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-ceph-warnings', {
|
|
extend: 'Ext.data.Model',
|
|
fields: ['id', 'summary', 'detail', 'severity'],
|
|
idProperty: 'id',
|
|
});
|
|
|
|
|
|
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,
|
|
maxHeight: 430,
|
|
stateful: true,
|
|
stateId: 'ceph-status-warnings',
|
|
viewConfig: {
|
|
enableTextSelection: true,
|
|
listeners: {
|
|
collapsebody: function(rowNode, record) {
|
|
record.set('expanded', false);
|
|
record.commit();
|
|
},
|
|
expandbody: function(rowNode, record) {
|
|
record.set('expanded', true);
|
|
record.commit();
|
|
},
|
|
},
|
|
},
|
|
// we load the store manually, to show an emptyText specify an empty intermediate store
|
|
store: {
|
|
type: 'diff',
|
|
trackRemoved: false,
|
|
data: [],
|
|
rstore: {
|
|
storeid: 'pve-ceph-warnings',
|
|
type: 'update',
|
|
model: 'pve-ceph-warnings',
|
|
},
|
|
},
|
|
updateHealth: function(health) {
|
|
let checks = health.checks || {};
|
|
|
|
let checkRecords = Object.keys(checks).sort().map(key => {
|
|
let check = checks[key];
|
|
let data = {
|
|
id: key,
|
|
summary: check.summary.message,
|
|
detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, '').trimStart(),
|
|
severity: check.severity,
|
|
};
|
|
data.noDetails = data.detail.length === 0;
|
|
data.detailsCls = data.detail.length === 0 ? 'pmx-opacity-75' : '';
|
|
if (data.detail.length === 0) {
|
|
data.detail = "no additional data";
|
|
}
|
|
return data;
|
|
});
|
|
|
|
let rstore = this.getStore().rstore;
|
|
rstore.loadData(checkRecords, false);
|
|
rstore.fireEvent('load', rstore, checkRecords, true);
|
|
},
|
|
emptyText: gettext('No Warnings/Errors'),
|
|
columns: [
|
|
{
|
|
dataIndex: 'severity',
|
|
tooltip: gettext('Severity'),
|
|
align: 'center',
|
|
width: 38,
|
|
renderer: function(value) {
|
|
let health = PVE.Utils.map_ceph_health[value];
|
|
let icon = PVE.Utils.get_health_icon(health);
|
|
return `<i class="fa fa-fw ${icon}"></i>`;
|
|
},
|
|
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'),
|
|
renderer: function(value, metaData, record, rI, cI, store, view) {
|
|
if (record.get('expanded')) {
|
|
metaData.tdCls = 'pmx-column-wrapped';
|
|
}
|
|
return value;
|
|
},
|
|
flex: 1,
|
|
},
|
|
{
|
|
xtype: 'actioncolumn',
|
|
width: 50,
|
|
align: 'center',
|
|
tooltip: gettext('Actions'),
|
|
items: [
|
|
{
|
|
iconCls: 'x-fa fa-clipboard',
|
|
tooltip: gettext('Copy to Clipboard'),
|
|
handler: function(grid, rowindex, colindex, item, e, { data }) {
|
|
let detail = data.noDetails ? '': `\n${data.detail}`;
|
|
navigator.clipboard
|
|
.writeText(`${data.severity}: ${data.summary}${detail}`)
|
|
.catch(err => Ext.Msg.alert(gettext('Error'), err));
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
listeners: {
|
|
itemdblclick: function(view, record, row, rowIdx, e) {
|
|
// inspired by Ext.grid.plugin.RowExpander, but for double click
|
|
let rowNode = view.getNode(rowIdx);
|
|
let normalRow = Ext.fly(rowNode);
|
|
|
|
let collapsedCls = view.rowBodyFeature.rowCollapsedCls;
|
|
|
|
if (normalRow.hasCls(collapsedCls)) {
|
|
view.rowBodyFeature.rowExpander.toggleRow(rowIdx, record);
|
|
}
|
|
},
|
|
},
|
|
plugins: [
|
|
{
|
|
ptype: 'rowexpander',
|
|
expandOnDblClick: false,
|
|
scrollIntoViewOnExpand: false,
|
|
rowBodyTpl: '<pre class="pve-ceph-warning-detail {detailsCls}">{detail}</pre>',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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
|
|
const usage = total > 0 ? used / total : 0;
|
|
me.down('#space').updateValue(usage, 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: [
|
|
'<h3>OSDs</h3>',
|
|
'<table class="osds">',
|
|
'<tr><td></td>',
|
|
'<td><i class="fa fa-fw good fa-circle"></i>',
|
|
gettext('In'),
|
|
'</td>',
|
|
'<td><i class="fa fa-fw warning fa-circle-o"></i>',
|
|
gettext('Out'),
|
|
'</td>',
|
|
'</tr>',
|
|
'<tr>',
|
|
'<td><i class="fa fa-fw good fa-arrow-circle-up"></i>',
|
|
gettext('Up'),
|
|
'</td>',
|
|
'<td>{upin}</td>',
|
|
'<td>{upout}</td>',
|
|
'</tr>',
|
|
'<tr>',
|
|
'<td><i class="fa fa-fw critical fa-arrow-circle-down"></i>',
|
|
gettext('Down'),
|
|
'</td>',
|
|
'<td>{downin}</td>',
|
|
'<td>{downout}</td>',
|
|
'</tr>',
|
|
'</table>',
|
|
'<br /><div>',
|
|
gettext('Total'),
|
|
': {total}',
|
|
'</div><br />',
|
|
'<tpl if="oldOSD.length > 0">',
|
|
'<i class="fa fa-refresh warning"></i> ' + gettext('Outdated OSDs') + "<br>",
|
|
'<div class="osds">',
|
|
'<tpl for="oldOSD">',
|
|
'<div class="left-aligned">osd.{id}:</div>',
|
|
'<div class="right-aligned">{version}</div><br />',
|
|
'<div style="clear:both"></div>',
|
|
'</tpl>',
|
|
'</div>',
|
|
'</tpl>',
|
|
'</div>',
|
|
'<tpl if="ghostOSD.length > 0">',
|
|
'<br />',
|
|
`<i class="fa fa-question-circle warning"></i> ${gettext('Ghost OSDs')}<br>`,
|
|
`<div data-qtip="${gettext('OSDs with no metadata, possibly left over from removal')}" class="osds">`,
|
|
'<tpl for="ghostOSD">',
|
|
'<div class="left-aligned">osd.{id}</div>',
|
|
'<div style="clear:both"></div>',
|
|
'</tpl>',
|
|
'</div>',
|
|
'</tpl>',
|
|
],
|
|
},
|
|
{
|
|
flex: 1,
|
|
border: false,
|
|
itemId: 'pgchart',
|
|
xtype: 'polar',
|
|
height: 184,
|
|
innerPadding: 5,
|
|
insetPadding: 5,
|
|
colors: [
|
|
'#CFCFCF',
|
|
'#21BF4B',
|
|
'#3892d4',
|
|
'#FFCC00',
|
|
'#FF6C59',
|
|
],
|
|
store: { },
|
|
series: [
|
|
{
|
|
type: 'pie',
|
|
donut: 60,
|
|
angleField: 'count',
|
|
tooltip: {
|
|
trackMouse: true,
|
|
renderer: function(tooltip, record, ctx) {
|
|
var html = record.get('text');
|
|
html += '<br>';
|
|
record.get('states').forEach(function(state) {
|
|
html += '<br>' +
|
|
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: [
|
|
'<h3>PGs</h3>',
|
|
'<tpl for="states">',
|
|
'<div class="left-aligned"><i class ="fa fa-circle {cls}"></i> {state_name}:</div>',
|
|
'<div class="right-aligned">{count}</div><br />',
|
|
'<div style="clear:both"></div>',
|
|
'</tpl>',
|
|
],
|
|
}],
|
|
|
|
// similar to mgr dashboard
|
|
pgstates: {
|
|
// clean
|
|
clean: 1,
|
|
active: 1,
|
|
|
|
// busy
|
|
activating: 2,
|
|
backfill_wait: 2,
|
|
backfilling: 2,
|
|
creating: 2,
|
|
deep: 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,
|
|
|
|
// warning
|
|
degraded: 3,
|
|
undersized: 3,
|
|
|
|
// critical
|
|
backfill_toofull: 4,
|
|
backfill_unfound: 4,
|
|
down: 4,
|
|
incomplete: 4,
|
|
inconsistent: 4,
|
|
recovery_toofull: 4,
|
|
recovery_unfound: 4,
|
|
snaptrim_error: 4,
|
|
stale: 4,
|
|
},
|
|
|
|
statecategories: [
|
|
{
|
|
text: gettext('Unknown'),
|
|
count: 0,
|
|
states: [],
|
|
cls: 'faded',
|
|
},
|
|
{
|
|
text: gettext('Clean'),
|
|
cls: 'good',
|
|
},
|
|
{
|
|
text: gettext('Busy'),
|
|
cls: 'pve-ceph-status-busy',
|
|
},
|
|
{
|
|
text: gettext('Warning'),
|
|
cls: 'warning',
|
|
},
|
|
{
|
|
text: gettext('Critical'),
|
|
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,
|
|
referenceHolder: true,
|
|
onlineHelp: "sysadmin_certs_acme_account",
|
|
|
|
viewModel: {
|
|
data: {
|
|
customDirectory: false,
|
|
eabRequired: false,
|
|
},
|
|
formulas: {
|
|
eabEmptyText: function(get) {
|
|
return get('eabRequired') ? gettext("required") : gettext("optional");
|
|
},
|
|
},
|
|
},
|
|
|
|
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',
|
|
notFoundIsValid: true,
|
|
isFormField: false,
|
|
allowBlank: false,
|
|
valueField: 'url',
|
|
displayField: 'name',
|
|
fieldLabel: gettext('ACME Directory'),
|
|
store: {
|
|
listeners: {
|
|
'load': function() {
|
|
this.add({ name: gettext("Custom"), url: '' });
|
|
},
|
|
},
|
|
autoLoad: true,
|
|
fields: ['name', 'url'],
|
|
idProperty: ['name'],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/cluster/acme/directories',
|
|
},
|
|
},
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1,
|
|
},
|
|
{
|
|
header: gettext('URL'),
|
|
dataIndex: 'url',
|
|
flex: 1,
|
|
},
|
|
],
|
|
},
|
|
listeners: {
|
|
change: function(combogrid, value) {
|
|
let me = this;
|
|
|
|
let vm = me.up('window').getViewModel();
|
|
let dirField = me.up('window').lookupReference('directoryInput');
|
|
let tosButton = me.up('window').lookupReference('queryTos');
|
|
|
|
let isCustom = combogrid.getSelection().get('name') === gettext("Custom");
|
|
vm.set('customDirectory', isCustom);
|
|
|
|
dirField.setValue(value);
|
|
|
|
if (!isCustom) {
|
|
tosButton.click();
|
|
} else {
|
|
me.up('window').clearToSFields();
|
|
}
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'fieldcontainer',
|
|
layout: 'hbox',
|
|
fieldLabel: gettext('URL'),
|
|
bind: {
|
|
hidden: '{!customDirectory}',
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'directory',
|
|
reference: 'directoryInput',
|
|
flex: 1,
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function(textbox, value) {
|
|
let me = this;
|
|
me.up('window').clearToSFields();
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
margin: '0 0 0 5',
|
|
reference: 'queryTos',
|
|
text: gettext('Query URL'),
|
|
listeners: {
|
|
click: function(button) {
|
|
let me = this;
|
|
|
|
let w = me.up('window');
|
|
let vm = w.getViewModel();
|
|
let disp = w.down('#tos_url_display');
|
|
let field = w.down('#tos_url');
|
|
let checkbox = w.down('#tos_checkbox');
|
|
let value = w.lookupReference('directoryInput').getValue();
|
|
w.clearToSFields();
|
|
|
|
if (!value) {
|
|
return;
|
|
} else {
|
|
disp.setValue(gettext("Loading"));
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/cluster/acme/meta',
|
|
method: 'GET',
|
|
params: {
|
|
directory: value,
|
|
},
|
|
success: function(response, opt) {
|
|
if (response.result.data && response.result.data.termsOfService) {
|
|
field.setValue(response.result.data.termsOfService);
|
|
disp.setValue(response.result.data.termsOfService);
|
|
checkbox.setHidden(false);
|
|
} else {
|
|
// Needed to pass input verification and enable register button
|
|
// has no influence on the submitted form
|
|
checkbox.setValue(true);
|
|
disp.setValue("No terms of service agreement required");
|
|
}
|
|
vm.set('eabRequired', !!(response.result.data &&
|
|
response.result.data.externalAccountRequired));
|
|
},
|
|
failure: function(response, opt) {
|
|
disp.setValue(undefined);
|
|
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;
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'eab-kid',
|
|
fieldLabel: gettext('EAB Key ID'),
|
|
bind: {
|
|
hidden: '{!customDirectory}',
|
|
allowBlank: '{!eabRequired}',
|
|
emptyText: '{eabEmptyText}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'eab-hmac-key',
|
|
fieldLabel: gettext('EAB Key'),
|
|
bind: {
|
|
hidden: '{!customDirectory}',
|
|
allowBlank: '{!eabRequired}',
|
|
emptyText: '{eabEmptyText}',
|
|
},
|
|
},
|
|
],
|
|
|
|
clearToSFields: function() {
|
|
let me = this;
|
|
|
|
let disp = me.down('#tos_url_display');
|
|
let field = me.down('#tos_url');
|
|
let checkbox = me.down('#tos_checkbox');
|
|
|
|
disp.setValue("Terms of service not fetched yet");
|
|
field.setValue(undefined);
|
|
checkbox.setValue(undefined);
|
|
checkbox.setHidden(true);
|
|
},
|
|
|
|
});
|
|
|
|
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;
|
|
// reload only if the Web UI is open on the same node that the cert was ordered for
|
|
if (this.getView().nodename !== Proxmox.NodeName) {
|
|
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 Suspend'),
|
|
itemId: 'bulksuspend',
|
|
iconCls: 'fa fa-fw fa-download',
|
|
handler: function() {
|
|
Ext.create('PVE.window.BulkAction', {
|
|
nodename: this.up('menu').nodename,
|
|
title: gettext('Bulk Suspend'),
|
|
btnText: gettext('Suspend'),
|
|
action: 'suspendall',
|
|
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);
|
|
me.getComponent('bulksuspend').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);
|
|
}
|
|
|
|
if (PVE.Utils.isStandaloneNode()) {
|
|
me.getComponent('bulkmigrate').setVisible(false);
|
|
}
|
|
},
|
|
});
|
|
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 Suspend'),
|
|
iconCls: 'fa fa-fw fa-download',
|
|
disabled: !caps.vms['VM.PowerMgmt'],
|
|
handler: function() {
|
|
Ext.create('PVE.window.BulkAction', {
|
|
autoShow: true,
|
|
nodename: nodename,
|
|
title: gettext('Bulk Suspend'),
|
|
btnText: gettext('Suspend'),
|
|
action: 'suspendall',
|
|
});
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Bulk Migrate'),
|
|
iconCls: 'fa fa-fw fa-send-o',
|
|
disabled: !caps.vms['VM.Migrate'],
|
|
hidden: PVE.Utils.isStandaloneNode(),
|
|
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: gettext('System Log'),
|
|
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: gettext('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',
|
|
log_select_timespan: true,
|
|
submitFormat: 'U',
|
|
},
|
|
{
|
|
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: '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: [
|
|
'<tpl if="dirName">',
|
|
gettext('Directory') + ' {dirName}:',
|
|
'<tpl else>',
|
|
Ext.String.format(gettext('No {0} selected'), gettext('directory')),
|
|
'</tpl>',
|
|
],
|
|
},
|
|
{
|
|
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: [
|
|
'<tpl if="volumeGroup">',
|
|
'Volume group {volumeGroup}:',
|
|
'<tpl else>',
|
|
Ext.String.format(gettext('No {0} selected'), 'volume group'),
|
|
'</tpl>',
|
|
],
|
|
},
|
|
{
|
|
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: [
|
|
'<tpl if="thinPool">',
|
|
'<tpl if="volumeGroup">',
|
|
'Thinpool {volumeGroup}/{thinPool}:',
|
|
'<tpl else>', // volumeGroup
|
|
'Missing volume group (node running old version?)',
|
|
'</tpl>',
|
|
'<tpl else>', // thinPool
|
|
Ext.String.format(gettext('No {0} selected'), 'thinpool'),
|
|
'</tpl>',
|
|
],
|
|
},
|
|
{
|
|
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: 390,
|
|
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: '',
|
|
},
|
|
{
|
|
colspan: 2,
|
|
title: gettext('Kernel Version'),
|
|
printBar: false,
|
|
// TODO: remove with next major and only use newish current-kernel textfield
|
|
multiField: true,
|
|
//textField: 'current-kernel',
|
|
renderer: ({ data }) => {
|
|
if (!data['current-kernel']) {
|
|
return data.kversion;
|
|
}
|
|
let kernel = data['current-kernel'];
|
|
let buildDate = kernel.version.match(/\((.+)\)\s*$/)?.[1] ?? 'unknown';
|
|
return `${kernel.sysname} ${kernel.release} (${buildDate})`;
|
|
},
|
|
value: '',
|
|
},
|
|
{
|
|
colspan: 2,
|
|
title: gettext('Boot Mode'),
|
|
printBar: false,
|
|
textField: 'boot-info',
|
|
renderer: boot => {
|
|
if (boot.mode === 'legacy-bios') {
|
|
return 'Legacy BIOS';
|
|
} else if (boot.mode === 'efi') {
|
|
return `EFI${boot.secureboot ? ' (Secure Boot)' : ''}`;
|
|
}
|
|
return Proxmox.Utils.unknownText;
|
|
},
|
|
value: '',
|
|
},
|
|
{
|
|
itemId: 'version',
|
|
colspan: 2,
|
|
printBar: false,
|
|
title: gettext('Manager Version'),
|
|
textField: 'pveversion',
|
|
value: '',
|
|
},
|
|
{
|
|
itemId: 'thermal',
|
|
colspan: 2,
|
|
printBar: false,
|
|
title: gettext('Thermal'),
|
|
textField: 'thermal',
|
|
renderer: function (value) {
|
|
value = JSON.parse(value);
|
|
const cpu0 = value['coretemp-isa-0000']['Package id 0']['temp1_input'].toFixed(1);
|
|
const board = value['acpitz-acpi-0']['temp1']['temp1_input'].toFixed(1);
|
|
const nvme = value['nvme-pci-0c00']['Composite']['temp1_input'].toFixed(1);
|
|
return `CPU: ${cpu0}\xb0C | Board: ${board}\xb0C | NVME: ${nvme}\xb0C`;
|
|
},
|
|
},
|
|
{
|
|
itemId: 'networksp',
|
|
colspan: 2,
|
|
printBar: false,
|
|
title: gettext('Network Speed'),
|
|
textField: 'networksp',
|
|
renderer: function (sp) {
|
|
if (!Array.isArray(sp)) {
|
|
return '';
|
|
}
|
|
const sps = sp.map(function (s) { return String(s).match(/(?<=:\s+)(.+)/g)?.[0] });
|
|
return sps.join(' | ');
|
|
},
|
|
},
|
|
],
|
|
|
|
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: 350,
|
|
|
|
items: {
|
|
xtype: 'textfield',
|
|
name: 'key',
|
|
value: '',
|
|
fieldLabel: gettext('Subscription Key'),
|
|
labelWidth: 120,
|
|
getSubmitValue: function() {
|
|
return this.processRawValue(this.getRawValue())?.trim();
|
|
},
|
|
},
|
|
|
|
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";
|
|
}
|
|
|
|
let rows = {
|
|
productname: {
|
|
header: gettext('Type'),
|
|
},
|
|
key: {
|
|
header: gettext('Subscription Key'),
|
|
},
|
|
status: {
|
|
header: gettext('Status'),
|
|
renderer: v => {
|
|
let message = me.getObjectValue('message');
|
|
return message ? `${v}: ${message}` : v;
|
|
},
|
|
},
|
|
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: v => v ? gettext('Yes') : gettext('No'),
|
|
},
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: `/api2/json/nodes/${me.nodename}/subscription`,
|
|
cwidth1: 170,
|
|
tbar: [
|
|
{
|
|
text: gettext('Upload Subscription Key'),
|
|
handler: () => Ext.create('PVE.node.SubscriptionKeyEdit', {
|
|
autoShow: true,
|
|
url: `/api2/extjs/nodes/${me.nodename}/subscription`,
|
|
listeners: {
|
|
destroy: () => me.rstore.load(),
|
|
},
|
|
}),
|
|
},
|
|
{
|
|
text: gettext('Check'),
|
|
handler: () => Proxmox.Utils.API2Request({
|
|
params: { force: 1 },
|
|
url: `/nodes/${me.nodename}/subscription`,
|
|
method: 'POST',
|
|
waitMsgTarget: me,
|
|
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
|
callback: () => me.rstore.load(),
|
|
}),
|
|
},
|
|
{
|
|
text: gettext('Remove Subscription'),
|
|
xtype: 'proxmoxStdRemoveButton',
|
|
confirmMsg: gettext('Are you sure you want to remove the subscription key?'),
|
|
baseurl: `/nodes/${me.nodename}/subscription`,
|
|
dangerous: true,
|
|
selModel: false,
|
|
callback: () => me.rstore.load(),
|
|
},
|
|
'-',
|
|
{
|
|
text: gettext('System Report'),
|
|
handler: function() {
|
|
Proxmox.Utils.checked_command(function() { me.showReport(); });
|
|
},
|
|
},
|
|
],
|
|
rows: rows,
|
|
listeners: {
|
|
activate: () => me.rstore.load(),
|
|
},
|
|
});
|
|
|
|
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: 390,
|
|
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 <a target="_blank" href="' +
|
|
Proxmox.Utils.get_help_link('chapter_zfs') + '">the reference documentation</a>.',
|
|
},
|
|
],
|
|
});
|
|
|
|
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: [
|
|
'<tpl if="pool">',
|
|
'Pool {pool}:',
|
|
'<tpl else>',
|
|
Ext.String.format(gettext('No {0} selected'), 'pool'),
|
|
'</tpl>',
|
|
],
|
|
},
|
|
{
|
|
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/?poolid=" + 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'),
|
|
viewConfig: {
|
|
enableTextSelection: true,
|
|
},
|
|
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('<br>');
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
Ext.define('PVE.qemu.AgentIPView', {
|
|
extend: 'Ext.container.Container',
|
|
xtype: 'pveAgentIPView',
|
|
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'top',
|
|
},
|
|
|
|
nics: [],
|
|
|
|
items: [
|
|
{
|
|
xtype: 'box',
|
|
html: '<i class="fa fa-exchange"></i> 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('<br>');
|
|
}
|
|
} 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 = "<i class='pve-grid-fa fa fa-fw fa-reorder cursor-move'></i>";
|
|
let idx = (rowIndex + 1).toString();
|
|
if (record.get('enabled')) {
|
|
return dragHandle + idx;
|
|
} else {
|
|
return dragHandle + "<span class='faded'>" + idx + "</span>";
|
|
}
|
|
},
|
|
},
|
|
{
|
|
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 = `<i class="pve-grid-fa fa fa-fw fa-${iconCls}"></i>`;
|
|
}
|
|
|
|
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';
|
|
values.cdimage = drive.file;
|
|
}
|
|
|
|
me.drive = drive;
|
|
|
|
me.setValues(values);
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
me.isosel.setNodename(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;
|
|
}
|
|
var cdImageField = me.down('pveIsoSelector');
|
|
cdImageField.setDisabled(!value);
|
|
if (value) {
|
|
cdImageField.validate();
|
|
} else {
|
|
cdImageField.reset();
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
|
|
me.isosel = Ext.create('PVE.form.IsoSelector', {
|
|
nodename: me.nodename,
|
|
insideWizard: me.insideWizard,
|
|
name: 'cdimage',
|
|
});
|
|
|
|
items.push(me.isosel);
|
|
|
|
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');
|
|
let caps_ci = caps.vms['VM.Config.Network'] || caps.vms['VM.Config.Cloudinit'];
|
|
if (view.rows[record.data.key].never_delete || !caps_ci) {
|
|
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 = '<i class="' + rowdef.iconCls + '"></i> ';
|
|
}
|
|
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 Ext.String.htmlEncode(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 Ext.String.htmlEncode(value || Proxmox.Utils.noneText);
|
|
},
|
|
},
|
|
searchdomain: {
|
|
header: gettext('DNS domain'),
|
|
iconCls: 'fa fa-globe',
|
|
editor: caps_ci ? 'PVE.lxc.DNSEdit' : undefined,
|
|
never_delete: true,
|
|
defaultValue: gettext('use host settings'),
|
|
},
|
|
nameserver: {
|
|
header: gettext('DNS servers'),
|
|
iconCls: 'fa fa-globe',
|
|
editor: caps_ci ? '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 += ' <span style="color:gray">(' + gettext('with options') + ')</span>';
|
|
}
|
|
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('<br>');
|
|
} else {
|
|
return Proxmox.Utils.noneText;
|
|
}
|
|
},
|
|
defaultValue: '',
|
|
},
|
|
ciupgrade: {
|
|
header: gettext('Upgrade packages'),
|
|
iconCls: 'fa fa-archive',
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
defaultValue: 1,
|
|
editor: {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Upgrade packages on boot'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'ciupgrade',
|
|
uncheckedValue: 0,
|
|
value: 1, // serves as default value, using defaultValue is not enough
|
|
fieldLabel: gettext('Upgrade packages'),
|
|
labelWidth: 140,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
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_ci ? '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.Utils.isStandaloneNode();
|
|
|
|
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: () => {
|
|
Ext.create('PVE.GuestStop', {
|
|
nodename: info.node,
|
|
vm: info,
|
|
autoShow: true,
|
|
});
|
|
},
|
|
},
|
|
{
|
|
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.Utils.isStandaloneNode(),
|
|
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'],
|
|
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
|
|
handler: function() {
|
|
Ext.create('PVE.GuestStop', {
|
|
nodename: nodename,
|
|
vm: vm,
|
|
autoShow: true,
|
|
});
|
|
},
|
|
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: [
|
|
'<tpl if="lock">',
|
|
'<i class="fa fa-lg fa-lock"></i> ({lock})',
|
|
'</tpl>',
|
|
],
|
|
});
|
|
|
|
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.Audit']) {
|
|
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',
|
|
},
|
|
);
|
|
}
|
|
|
|
if (caps.vms['VM.Console']) {
|
|
me.items.push(
|
|
{
|
|
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',
|
|
log_select_timespan: true,
|
|
submitFormat: 'U',
|
|
},
|
|
);
|
|
}
|
|
|
|
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'),
|
|
|
|
// fot the special case that we have 2 cdrom drives
|
|
//
|
|
// emulates part of the backend bootorder logic, but includes all
|
|
// cdrom drives since we don't know which one the user put in a bootable iso
|
|
// and hardcodes the known values (ide0/2, net0)
|
|
calculateBootOrder: function(values) {
|
|
// user selected windows + second cdrom
|
|
if (values.ide0 && values.ide0.match(/media=cdrom/)) {
|
|
let disk;
|
|
PVE.Utils.forEachBus(['ide', 'scsi', 'virtio', 'sata'], (type, id) => {
|
|
let confId = type + id;
|
|
if (!values[confId]) {
|
|
return undefined;
|
|
}
|
|
if (values[confId].match(/media=cdrom/)) {
|
|
return undefined;
|
|
}
|
|
disk = confId;
|
|
return false; // abort loop
|
|
});
|
|
|
|
let order = [];
|
|
if (disk) {
|
|
order.push(disk);
|
|
}
|
|
order.push('ide0', 'ide2');
|
|
if (values.net0) {
|
|
order.push('net0');
|
|
}
|
|
|
|
return `order=${order.join(';')}`;
|
|
}
|
|
return undefined;
|
|
},
|
|
|
|
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'),
|
|
},
|
|
],
|
|
|
|
advancedColumnB: [
|
|
{
|
|
xtype: 'pveTagFieldSet',
|
|
name: 'tags',
|
|
maxHeight: 150,
|
|
},
|
|
],
|
|
|
|
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,
|
|
bind: {
|
|
nodename: '{nodename}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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) {
|
|
let wizard = this.up('window');
|
|
var kv = wizard.getValues();
|
|
var data = [];
|
|
|
|
let boot = wizard.calculateBootOrder(kv);
|
|
if (boot) {
|
|
kv.boot = boot;
|
|
}
|
|
|
|
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;
|
|
|
|
let boot = wizard.calculateBootOrder(kv);
|
|
if (boot) {
|
|
kv.boot = boot;
|
|
}
|
|
|
|
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 };
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
type: '__default__',
|
|
clipboard: '__default__',
|
|
},
|
|
formulas: {
|
|
matchNonGUIOption: function(get) {
|
|
return get('type').match(/^(serial\d|none)$/);
|
|
},
|
|
memoryEmptyText: function(get) {
|
|
let val = get('type');
|
|
if (val === "cirrus") {
|
|
return "4";
|
|
} else if (val === "std" || val.match(/^qxl\d?$/) || val === "vmware") {
|
|
return "16";
|
|
} else if (val.match(/^virtio/)) {
|
|
return "256";
|
|
} else if (get('matchNonGUIOption')) {
|
|
return "N/A";
|
|
} else {
|
|
console.debug("unexpected display type", val);
|
|
return Proxmox.Utils.defaultText;
|
|
}
|
|
},
|
|
isVNC: get => get('clipboard') === 'vnc',
|
|
hideDefaultHint: get => get('isVNC') || get('matchNonGUIOption'),
|
|
hideVNCHint: get => !get('isVNC') || get('matchNonGUIOption'),
|
|
},
|
|
},
|
|
|
|
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;
|
|
},
|
|
bind: {
|
|
value: '{type}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
emptyText: Proxmox.Utils.defaultText,
|
|
fieldLabel: gettext('Memory') + ' (MiB)',
|
|
minValue: 4,
|
|
maxValue: 512,
|
|
step: 4,
|
|
name: 'memory',
|
|
bind: {
|
|
emptyText: '{memoryEmptyText}',
|
|
disabled: '{matchNonGUIOption}',
|
|
},
|
|
}],
|
|
|
|
advancedItems: [
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'clipboard',
|
|
deleteEmpty: false,
|
|
value: '__default__',
|
|
fieldLabel: gettext('Clipboard'),
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText],
|
|
['vnc', 'VNC'],
|
|
],
|
|
bind: {
|
|
value: '{clipboard}',
|
|
disabled: '{matchNonGUIOption}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
name: 'vncHint',
|
|
userCls: 'pmx-hint',
|
|
value: gettext('You cannot use the default SPICE clipboard if the VNC Clipboard is selected.') + ' ' +
|
|
gettext('VNC Clipboard requires spice-tools installed in the Guest-VM.'),
|
|
bind: {
|
|
hidden: '{hideVNCHint}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
name: 'defaultHint',
|
|
userCls: 'pmx-hint',
|
|
value: gettext('This option depends on your display type.') + ' ' +
|
|
gettext('If the display type uses SPICE you are able to use the default SPICE Clipboard.'),
|
|
bind: {
|
|
hidden: '{hideDefaultHint}',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
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) {
|
|
Ext.create('Proxmox.window.TaskProgress', {
|
|
autoShow: true,
|
|
upid: response.result.data,
|
|
});
|
|
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')} (${gettext('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 = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
|
|
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'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.USBEdit' : undefined,
|
|
never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
|
|
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'] && !caps.mapping['Mapping.Use'],
|
|
editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? '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 += '<br>' + 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 noHWPerm = !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'];
|
|
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(noHWPerm || isAtUsbLimit());
|
|
me.down('#addPci').setDisabled(noHWPerm || 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),
|
|
);
|
|
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'] && !caps.mapping['Mapping.Use'],
|
|
handler: editorFactory('USBEdit'),
|
|
},
|
|
{
|
|
text: gettext('PCI Device'),
|
|
itemId: 'addPci',
|
|
iconCls: 'pve-itype-icon-pci',
|
|
disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
|
|
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',
|
|
onlineHelp: 'qm_machine_type',
|
|
|
|
viewModel: {
|
|
data: {
|
|
type: '__default__',
|
|
},
|
|
formulas: {
|
|
q35: get => get('type') === 'q35',
|
|
},
|
|
},
|
|
|
|
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.delete === 'machine' && values.viommu) {
|
|
delete values.delete;
|
|
values.machine = 'pc';
|
|
}
|
|
if (values.version && values.version !== 'latest') {
|
|
values.machine = values.version;
|
|
delete values.delete;
|
|
}
|
|
delete values.version;
|
|
if (values.delete === 'machine' && !values.viommu) {
|
|
return values;
|
|
}
|
|
let ret = {};
|
|
ret.machine = PVE.Parser.printPropertyString(values, 'machine');
|
|
return ret;
|
|
},
|
|
|
|
setValues: function(values) {
|
|
let me = this;
|
|
|
|
let machineConf = PVE.Parser.parsePropertyString(values.machine, 'type');
|
|
values.machine = machineConf.type;
|
|
|
|
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';
|
|
}
|
|
}
|
|
|
|
values.viommu = machineConf.viommu || '__default__';
|
|
|
|
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'],
|
|
],
|
|
bind: {
|
|
value: '{type}',
|
|
},
|
|
},
|
|
|
|
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.'),
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'viommu',
|
|
fieldLabel: gettext('vIOMMU'),
|
|
reference: 'viommu-q35',
|
|
deleteEmpty: false,
|
|
value: '__default__',
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText + ' (None)'],
|
|
['intel', gettext('Intel (AMD Compatible)')],
|
|
['virtio', 'VirtIO'],
|
|
],
|
|
bind: {
|
|
hidden: '{!q35}',
|
|
disabled: '{!q35}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'viommu',
|
|
fieldLabel: gettext('vIOMMU'),
|
|
reference: 'viommu-i440fx',
|
|
deleteEmpty: false,
|
|
value: '__default__',
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText + ' (None)'],
|
|
['virtio', 'VirtIO'],
|
|
],
|
|
bind: {
|
|
hidden: '{q35}',
|
|
disabled: '{q35}',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
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(`<pre>${commands.flat(2).join('\n')}</pre>`);
|
|
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();
|
|
|
|
let res = {
|
|
ide2: 'media=cdrom',
|
|
scsihw: vm.get('current.scsihw'),
|
|
ostype: vm.get('current.ostype'),
|
|
};
|
|
|
|
if (vm.get('current.ide0') === "some") {
|
|
res.ide0 = "media=cdrom";
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
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',
|
|
cputype: 'x86-64-v2-AES',
|
|
};
|
|
|
|
// 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',
|
|
},
|
|
'checkbox[reference=enableSecondCD]': {
|
|
change: 'onSecondCDChange',
|
|
},
|
|
},
|
|
onOSBaseChange: function(field, value) {
|
|
let me = this;
|
|
me.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]);
|
|
if (me.getView().insideWizard) {
|
|
let isWindows = value === 'Microsoft Windows';
|
|
let enableSecondCD = me.lookup('enableSecondCD');
|
|
enableSecondCD.setVisible(isWindows);
|
|
if (!isWindows) {
|
|
enableSecondCD.setValue(false);
|
|
}
|
|
}
|
|
},
|
|
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);
|
|
me.setWidget('CPUModelSelector', targetValues.cputype);
|
|
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
|
|
}
|
|
},
|
|
onSecondCDChange: function(widget, value, lastValue) {
|
|
let me = this;
|
|
let vm = me.getViewModel();
|
|
let updateVMConfig = function() {
|
|
let widgets = Ext.ComponentQuery.query('pveMultiHDPanel');
|
|
if (widgets.length === 1) {
|
|
widgets[0].getController().updateVMConfig();
|
|
}
|
|
};
|
|
if (value) {
|
|
// only for windows
|
|
vm.set('current.ide0', "some");
|
|
vm.notify();
|
|
updateVMConfig();
|
|
me.setWidget('pveBusSelector', 'scsi');
|
|
me.setWidget('pveNetworkCardSelector', 'virtio');
|
|
} else {
|
|
vm.set('current.ide0', "");
|
|
vm.notify();
|
|
updateVMConfig();
|
|
me.setWidget('pveBusSelector', 'scsi');
|
|
let ostype = me.lookup('ostype').getValue();
|
|
var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype);
|
|
me.setWidget('pveBusSelector', targetValues.busType);
|
|
}
|
|
},
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
me.lookup('isoSelector').setNodename(nodename);
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
if (values.ide0) {
|
|
let drive = {
|
|
media: 'cdrom',
|
|
file: values.ide0,
|
|
};
|
|
values.ide0 = PVE.Parser.printQemuDrive(drive);
|
|
}
|
|
return values;
|
|
},
|
|
|
|
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));
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
if (me.insideWizard) {
|
|
me.items.push(
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
reference: 'enableSecondCD',
|
|
isFormField: false,
|
|
hidden: true,
|
|
checked: false,
|
|
boxLabel: gettext('Add additional drive for VirtIO drivers'),
|
|
listeners: {
|
|
change: function(cb, value) {
|
|
me.lookup('isoSelector').setDisabled(!value);
|
|
me.lookup('isoSelector').setHidden(!value);
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveIsoSelector',
|
|
reference: 'isoSelector',
|
|
name: 'ide0',
|
|
nodename: me.nodename,
|
|
insideWizard: true,
|
|
hidden: true,
|
|
disabled: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
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',
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
me.vmconfig = vmconfig;
|
|
|
|
let hostpci = me.vmconfig[view.confid] || '';
|
|
|
|
let 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.type = 'raw';
|
|
} else if (values.mapping) {
|
|
values.type = 'mapped';
|
|
}
|
|
|
|
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);
|
|
|
|
view.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
|
|
let pcie = me.lookup('pcie');
|
|
pcie.setDisabled(true);
|
|
pcie.setBoxLabel(gettext('Q35 only'));
|
|
}
|
|
|
|
if (values.romfile) {
|
|
me.lookup('romfile').setVisible(true);
|
|
}
|
|
},
|
|
|
|
selectorEnable: function(selector) {
|
|
let me = this;
|
|
me.pciDevChange(selector, selector.getValue());
|
|
},
|
|
|
|
pciDevChange: function(pcisel, value) {
|
|
let me = this;
|
|
let mdevfield = me.lookup('mdev');
|
|
if (!value) {
|
|
if (!pcisel.isDisabled()) {
|
|
mdevfield.setDisabled(true);
|
|
}
|
|
return;
|
|
}
|
|
let pciDev = pcisel.getStore().getById(value);
|
|
|
|
mdevfield.setDisabled(!pciDev || !pciDev.data.mdev);
|
|
if (!pciDev) {
|
|
return;
|
|
}
|
|
|
|
let path = value;
|
|
if (pciDev.data.map) {
|
|
// find local mapping
|
|
for (const entry of pciDev.data.map) {
|
|
let mapping = PVE.Parser.parsePropertyString(entry);
|
|
if (mapping.node === pcisel.up('inputpanel').nodename) {
|
|
path = mapping.path.split(';')[0];
|
|
break;
|
|
}
|
|
}
|
|
if (path.indexOf('.') === -1) {
|
|
path += '.0';
|
|
}
|
|
}
|
|
|
|
if (pciDev.data.mdev) {
|
|
mdevfield.setPciID(path);
|
|
}
|
|
if (pcisel.reference === 'selector') {
|
|
let iommu = pciDev.data.iommugroup;
|
|
if (iommu === -1) {
|
|
return;
|
|
}
|
|
// try to find out if there are more devices in that iommu group
|
|
let id = path.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;
|
|
});
|
|
me.lookup('group_warning').setVisible(count > 0);
|
|
}
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
let view = me.getView();
|
|
if (!view.confid) {
|
|
for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
|
|
if (!me.vmconfig['hostpci' + i.toString()]) {
|
|
view.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 = 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;
|
|
}
|
|
|
|
delete values.type;
|
|
|
|
let ret = {};
|
|
ret[view.confid] = PVE.Parser.printPropertyString(values, 'host');
|
|
return ret;
|
|
},
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
isMapped: true,
|
|
},
|
|
},
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
return this.getController().setVMConfig(vmconfig);
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
return this.getController().onGetValues(values);
|
|
},
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.columnT = [
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'iommu_warning',
|
|
hidden: true,
|
|
columnWidth: 1,
|
|
padding: '0 0 10 0',
|
|
value: 'No IOMMU detected, please activate it.' +
|
|
'See Documentation for further information.',
|
|
userCls: 'pmx-hint',
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'group_warning',
|
|
hidden: true,
|
|
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.column1 = [
|
|
{
|
|
xtype: 'radiofield',
|
|
name: 'type',
|
|
inputValue: 'mapped',
|
|
boxLabel: gettext('Mapped Device'),
|
|
bind: {
|
|
value: '{isMapped}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pvePCIMapSelector',
|
|
fieldLabel: gettext('Device'),
|
|
reference: 'mapped_selector',
|
|
name: 'mapping',
|
|
labelAlign: 'right',
|
|
nodename: me.nodename,
|
|
allowBlank: false,
|
|
bind: {
|
|
disabled: '{!isMapped}',
|
|
},
|
|
listeners: {
|
|
change: 'pciDevChange',
|
|
enable: 'selectorEnable',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
name: 'type',
|
|
inputValue: 'raw',
|
|
checked: true,
|
|
boxLabel: gettext('Raw Device'),
|
|
},
|
|
{
|
|
xtype: 'pvePCISelector',
|
|
fieldLabel: gettext('Device'),
|
|
name: 'host',
|
|
reference: 'selector',
|
|
nodename: me.nodename,
|
|
labelAlign: 'right',
|
|
allowBlank: false,
|
|
disabled: true,
|
|
bind: {
|
|
disabled: '{isMapped}',
|
|
},
|
|
onLoadCallBack: function(store, records, success) {
|
|
if (!success || !records.length) {
|
|
return;
|
|
}
|
|
me.lookup('iommu_warning').setVisible(
|
|
records.every((val) => val.data.iommugroup === -1),
|
|
);
|
|
},
|
|
listeners: {
|
|
change: 'pciDevChange',
|
|
enable: 'selectorEnable',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('All Functions'),
|
|
reference: 'all_functions',
|
|
disabled: true,
|
|
labelAlign: 'right',
|
|
name: 'multifunction',
|
|
bind: {
|
|
disabled: '{isMapped}',
|
|
},
|
|
},
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'pveMDevSelector',
|
|
name: 'mdev',
|
|
reference: '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',
|
|
reference: 'romfile',
|
|
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',
|
|
reference: 'pcie',
|
|
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: 256,
|
|
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',
|
|
|
|
cbindData: function(initialConfig) {
|
|
let me = this;
|
|
if (!me.pveSelNode) {
|
|
throw "no pveSelNode given";
|
|
}
|
|
|
|
return { nodename: me.pveSelNode.data.node };
|
|
},
|
|
|
|
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 'mapped':
|
|
val = `mapping=${values[type]}`;
|
|
delete values.mapped;
|
|
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: 'mapped',
|
|
boxLabel: gettext('Use mapped Device'),
|
|
reference: 'mapped',
|
|
submitValue: false,
|
|
},
|
|
{
|
|
xtype: 'pveUSBMapSelector',
|
|
disabled: true,
|
|
name: 'mapped',
|
|
cbind: { nodename: '{nodename}' },
|
|
bind: { disabled: '{!mapped.checked}' },
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Choose Device'),
|
|
labelAlign: 'right',
|
|
},
|
|
{
|
|
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;
|
|
}
|
|
|
|
let data = PVE.Parser.parsePropertyString(response.result.data[me.confid], 'host');
|
|
let port, hostdevice, mapped, usb3 = false;
|
|
let usb;
|
|
|
|
if (data.host) {
|
|
if (/^(0x)?[a-zA-Z0-9]{4}:(0x)?[a-zA-Z0-9]{4}$/.test(data.host)) {
|
|
hostdevice = data.host.replace('0x', '');
|
|
usb = 'hostdevice';
|
|
} else if (/^(\d+)-(\d+(\.\d+)*)$/.test(data.host)) {
|
|
port = data.host;
|
|
usb = 'port';
|
|
} else if (/^spice$/i.test(data.host)) {
|
|
usb = 'spice';
|
|
}
|
|
} else if (data.mapping) {
|
|
mapped = data.mapping;
|
|
usb = 'mapped';
|
|
}
|
|
|
|
usb3 = data.usb3 ?? false;
|
|
|
|
var values = {
|
|
usb,
|
|
hostdevice,
|
|
port,
|
|
usb3,
|
|
mapped,
|
|
};
|
|
|
|
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');
|
|
|
|
me.items.push({
|
|
nodename: nodename,
|
|
zone: sdnId,
|
|
xtype: 'pveSDNZoneContentPanel',
|
|
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';
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
name: 'vnet',
|
|
cbind: {
|
|
editable: '{isCreate}',
|
|
},
|
|
maxLength: 8,
|
|
flex: 1,
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Name'),
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'alias',
|
|
fieldLabel: gettext('Alias'),
|
|
allowBlank: true,
|
|
skipEmptyText: true,
|
|
cbind: {
|
|
deleteEmpty: "{!isCreate}",
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveSDNZoneSelector',
|
|
fieldLabel: gettext('Zone'),
|
|
name: 'zone',
|
|
value: '',
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'tag',
|
|
minValue: 1,
|
|
maxValue: 16777216,
|
|
fieldLabel: gettext('Tag'),
|
|
allowBlank: true,
|
|
cbind: {
|
|
deleteEmpty: "{!isCreate}",
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'vlanaware',
|
|
uncheckedValue: null,
|
|
checked: false,
|
|
fieldLabel: gettext('VLAN Aware'),
|
|
cbind: {
|
|
deleteEmpty: "{!isCreate}",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
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.VnetACLAdd', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveSDNVnetACLAdd'],
|
|
|
|
url: '/access/acl',
|
|
method: 'PUT',
|
|
isAdd: true,
|
|
isCreate: true,
|
|
|
|
width: 400,
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
let items = [
|
|
{
|
|
xtype: 'hiddenfield',
|
|
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'),
|
|
});
|
|
|
|
items.push({
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'vlan',
|
|
minValue: 1,
|
|
maxValue: 4096,
|
|
allowBlank: true,
|
|
fieldLabel: 'VLAN',
|
|
emptyText: gettext('All'),
|
|
});
|
|
|
|
let ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
items: items,
|
|
onlineHelp: 'pveum_permission_management',
|
|
onGetValues: function(values) {
|
|
if (values.vlan) {
|
|
values.path = values.path + "/" + values.vlan;
|
|
delete values.vlan;
|
|
}
|
|
return values;
|
|
},
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: [ipanel],
|
|
});
|
|
|
|
me.callParent();
|
|
},
|
|
});
|
|
|
|
Ext.define('PVE.sdn.VnetACLView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveSDNVnetACLView'],
|
|
|
|
onlineHelp: 'chapter_user_management',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-acls',
|
|
|
|
// use fixed path
|
|
path: undefined,
|
|
|
|
setPath: function(path) {
|
|
let me = this;
|
|
|
|
me.path = path;
|
|
|
|
if (path === undefined) {
|
|
me.down('#groupmenu').setDisabled(true);
|
|
me.down('#usermenu').setDisabled(true);
|
|
me.down('#tokenmenu').setDisabled(true);
|
|
} else {
|
|
me.down('#groupmenu').setDisabled(false);
|
|
me.down('#usermenu').setDisabled(false);
|
|
me.down('#tokenmenu').setDisabled(false);
|
|
me.store.load();
|
|
}
|
|
},
|
|
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',
|
|
},
|
|
});
|
|
|
|
store.addFilter(Ext.create('Ext.util.Filter', {
|
|
filterFn: item => item.data.path.replace(/(\/sdn\/zones\/(.*)\/(.*))\/[0-9]*$/, '$1') === me.path,
|
|
}));
|
|
|
|
let render_ugid = function(ugid, metaData, record) {
|
|
if (record.data.type === 'group') {
|
|
return '@' + ugid;
|
|
}
|
|
|
|
return Ext.String.htmlEncode(ugid);
|
|
};
|
|
|
|
let render_vlan = function(path, metaData, record) {
|
|
let vlan = 'any';
|
|
const match = path.match(/(\/sdn\/zones\/)(.*)\/(.*)\/([0-9]*)$/);
|
|
if (match) {
|
|
vlan = match[4];
|
|
}
|
|
|
|
return Ext.String.htmlEncode(vlan);
|
|
};
|
|
|
|
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',
|
|
},
|
|
{
|
|
header: gettext('VLAN'),
|
|
flex: 1,
|
|
sortable: true,
|
|
renderer: render_vlan,
|
|
dataIndex: 'path',
|
|
},
|
|
];
|
|
|
|
|
|
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'),
|
|
disabled: !me.path,
|
|
itemId: 'groupmenu',
|
|
iconCls: 'fa fa-fw fa-group',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.sdn.VnetACLAdd', {
|
|
aclType: 'group',
|
|
path: me.path,
|
|
});
|
|
win.on('destroy', () => store.load());
|
|
win.show();
|
|
},
|
|
},
|
|
{
|
|
text: gettext('User Permission'),
|
|
disabled: !me.path,
|
|
itemId: 'usermenu',
|
|
iconCls: 'fa fa-fw fa-user',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.sdn.VnetACLAdd', {
|
|
aclType: 'user',
|
|
path: me.path,
|
|
});
|
|
win.on('destroy', () => store.load());
|
|
win.show();
|
|
},
|
|
},
|
|
{
|
|
text: gettext('API Token Permission'),
|
|
disabled: !me.path,
|
|
itemId: 'tokenmenu',
|
|
iconCls: 'fa fa-fw fa-user-o',
|
|
handler: function() {
|
|
let win = Ext.create('PVE.sdn.VnetACLAdd', {
|
|
aclType: 'token',
|
|
path: me.path,
|
|
});
|
|
win.on('destroy', () => store.load());
|
|
win.show();
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
remove_btn,
|
|
],
|
|
viewConfig: {
|
|
trackOver: false,
|
|
},
|
|
columns: columns,
|
|
listeners: {
|
|
},
|
|
});
|
|
|
|
me.callParent();
|
|
},
|
|
}, function() {
|
|
Ext.define('pve-acl-vnet', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'path', 'type', 'ugid', 'roleid',
|
|
{
|
|
name: 'propagate',
|
|
type: 'boolean',
|
|
},
|
|
],
|
|
});
|
|
});
|
|
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;
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
name: 'cidr',
|
|
cbind: {
|
|
editable: '{isCreate}',
|
|
},
|
|
flex: 1,
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Subnet'),
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'gateway',
|
|
vtype: 'IP64Address',
|
|
fieldLabel: gettext('Gateway'),
|
|
allowBlank: true,
|
|
skipEmptyText: true,
|
|
cbind: {
|
|
deleteEmpty: "{!isCreate}",
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'snat',
|
|
uncheckedValue: null,
|
|
checked: false,
|
|
fieldLabel: 'SNAT',
|
|
cbind: {
|
|
deleteEmpty: "{!isCreate}",
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'dnszoneprefix',
|
|
skipEmptyText: true,
|
|
fieldLabel: gettext('DNS Zone Prefix'),
|
|
allowBlank: true,
|
|
cbind: {
|
|
deleteEmpty: "{!isCreate}",
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
Ext.define('PVE.sdn.SubnetDhcpRangePanel', {
|
|
extend: 'Ext.form.FieldContainer',
|
|
mixins: ['Ext.form.field.Field'],
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
me.callParent();
|
|
me.initField();
|
|
},
|
|
|
|
// since value is an array of objects we need to override isEquals here
|
|
isEqual: function(value1, value2) {
|
|
return JSON.stringify(value1) === JSON.stringify(value2);
|
|
},
|
|
|
|
getValue: function() {
|
|
let me = this;
|
|
let store = me.lookup('grid').getStore();
|
|
|
|
let value = [];
|
|
|
|
store.getData()
|
|
.each((item) => {
|
|
// needs a deep copy otherwise we run in to ExtJS reference
|
|
// shenaningans
|
|
value.push({
|
|
'start-address': item.data['start-address'],
|
|
'end-address': item.data['end-address'],
|
|
});
|
|
});
|
|
|
|
return value;
|
|
},
|
|
|
|
getSubmitData: function() {
|
|
let me = this;
|
|
|
|
let data = {};
|
|
|
|
let value = me.getValue()
|
|
.map((item) => `start-address=${item['start-address']},end-address=${item['end-address']}`);
|
|
|
|
if (value.length) {
|
|
data[me.getName()] = value;
|
|
} else if (!me.isCreate) {
|
|
data.delete = me.getName();
|
|
}
|
|
|
|
return data;
|
|
},
|
|
|
|
setValue: function(dhcpRanges) {
|
|
let me = this;
|
|
let store = me.lookup('grid').getStore();
|
|
|
|
let data = [];
|
|
|
|
dhcpRanges.forEach((item) => {
|
|
// needs a deep copy otherwise we run in to ExtJS reference
|
|
// shenaningans
|
|
data.push({
|
|
'start-address': item['start-address'],
|
|
'end-address': item['end-address'],
|
|
});
|
|
});
|
|
|
|
store.setData(data);
|
|
},
|
|
|
|
getErrors: function() {
|
|
let me = this;
|
|
let errors = [];
|
|
|
|
return errors;
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
addRange: function() {
|
|
let me = this;
|
|
me.lookup('grid').getStore().add({});
|
|
|
|
me.getView().checkChange();
|
|
},
|
|
|
|
removeRange: function(field) {
|
|
let me = this;
|
|
let record = field.getWidgetRecord();
|
|
|
|
me.lookup('grid').getStore().remove(record);
|
|
|
|
me.getView().checkChange();
|
|
},
|
|
|
|
onValueChange: function(field, value) {
|
|
let me = this;
|
|
let record = field.getWidgetRecord();
|
|
let column = field.getWidgetColumn();
|
|
|
|
record.set(column.dataIndex, value);
|
|
record.commit();
|
|
|
|
me.getView().checkChange();
|
|
},
|
|
|
|
control: {
|
|
'grid button': {
|
|
click: 'removeRange',
|
|
},
|
|
'field': {
|
|
change: 'onValueChange',
|
|
},
|
|
},
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
reference: 'grid',
|
|
scrollable: true,
|
|
store: {
|
|
fields: ['start-address', 'end-address'],
|
|
},
|
|
columns: [
|
|
{
|
|
text: gettext('Start Address'),
|
|
xtype: 'widgetcolumn',
|
|
dataIndex: 'start-address',
|
|
flex: 1,
|
|
widget: {
|
|
xtype: 'textfield',
|
|
vtype: 'IP64Address',
|
|
},
|
|
},
|
|
{
|
|
text: gettext('End Address'),
|
|
xtype: 'widgetcolumn',
|
|
dataIndex: 'end-address',
|
|
flex: 1,
|
|
widget: {
|
|
xtype: 'textfield',
|
|
vtype: 'IP64Address',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'widgetcolumn',
|
|
width: 40,
|
|
widget: {
|
|
xtype: 'button',
|
|
iconCls: 'fa fa-trash-o',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
layout: {
|
|
type: 'hbox',
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'button',
|
|
text: gettext('Add'),
|
|
iconCls: 'fa fa-plus-circle',
|
|
handler: 'addRange',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
Ext.define('PVE.sdn.SubnetEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
subject: gettext('Subnet'),
|
|
|
|
subnet: undefined,
|
|
|
|
width: 350,
|
|
|
|
base_url: undefined,
|
|
|
|
bodyPadding: 0,
|
|
|
|
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,
|
|
title: gettext('General'),
|
|
});
|
|
|
|
let dhcpPanel = Ext.create('PVE.sdn.SubnetDhcpRangePanel', {
|
|
isCreate: me.isCreate,
|
|
title: gettext('DHCP Ranges'),
|
|
name: 'dhcp-range',
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: [
|
|
{
|
|
xtype: 'tabpanel',
|
|
bodyPadding: 10,
|
|
items: [ipanel, dhcpPanel],
|
|
},
|
|
],
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
me.setValues(response.result.data);
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
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: gettext('Subnet'),
|
|
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;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.zone) {
|
|
throw "no zone ID specified";
|
|
}
|
|
|
|
var baseurl = "/nodes/" + me.nodename + "/sdn/zones/" + me.zone + "/content";
|
|
if (me.zone === 'localnetwork') {
|
|
baseurl = "/nodes/" + me.nodename + "/network?type=any_local_bridge";
|
|
}
|
|
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,
|
|
show: reload,
|
|
select: function(_sm, rec) {
|
|
let path = `/sdn/zones/${me.zone}/${rec.data.vnet}`;
|
|
me.permissions_panel.setPath(path);
|
|
},
|
|
deselect: function() {
|
|
me.permissions_panel.setPath(undefined);
|
|
},
|
|
},
|
|
});
|
|
store.load();
|
|
me.callParent();
|
|
},
|
|
}, function() {
|
|
Ext.define('pve-sdnzone-content', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
{
|
|
name: 'iface',
|
|
convert: function(value, record) {
|
|
//map local vmbr to vnet
|
|
if (record.data.iface) {
|
|
record.data.vnet = record.data.iface;
|
|
}
|
|
return value;
|
|
},
|
|
},
|
|
{
|
|
name: 'comments',
|
|
convert: function(value, record) {
|
|
//map local vmbr comments to vnet alias
|
|
if (record.data.comments) {
|
|
record.data.alias = record.data.comments;
|
|
}
|
|
return value;
|
|
},
|
|
},
|
|
'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.ZoneContentPanel', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveSDNZoneContentPanel',
|
|
|
|
title: 'VNet',
|
|
|
|
onlineHelp: 'pvesdn_config_vnet',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var permissions_panel = Ext.createWidget('pveSDNVnetACLView', {
|
|
title: gettext('VNet Permissions'),
|
|
region: 'center',
|
|
border: false,
|
|
});
|
|
|
|
var vnetview_panel = Ext.createWidget('pveSDNZoneContentView', {
|
|
title: 'VNets',
|
|
region: 'west',
|
|
permissions_panel: permissions_panel,
|
|
nodename: me.nodename,
|
|
zone: me.zone,
|
|
width: '50%',
|
|
border: false,
|
|
split: true,
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
layout: 'border',
|
|
items: [vnetview_panel, permissions_panel],
|
|
listeners: {
|
|
show: function() {
|
|
permissions_panel.fireEvent('show', permissions_panel);
|
|
},
|
|
},
|
|
});
|
|
|
|
me.callParent();
|
|
},
|
|
});
|
|
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.IpamEditInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
isCreate: false,
|
|
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
|
|
if (!values.vmid) {
|
|
delete values.vmid;
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
name: 'vmid',
|
|
fieldLabel: 'VMID',
|
|
allowBlank: false,
|
|
editable: false,
|
|
cbind: {
|
|
hidden: '{isCreate}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
name: 'mac',
|
|
fieldLabel: 'MAC',
|
|
allowBlank: false,
|
|
cbind: {
|
|
editable: '{isCreate}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'ip',
|
|
fieldLabel: gettext('IP Address'),
|
|
allowBlank: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
Ext.define('PVE.sdn.IpamEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
subject: gettext('DHCP Mapping'),
|
|
width: 350,
|
|
|
|
isCreate: false,
|
|
mapping: {},
|
|
|
|
url: '/cluster/sdn/vnets',
|
|
|
|
submitUrl: function(url, values) {
|
|
return `${url}/${values.vnet}/ips`;
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.method = me.isCreate ? 'POST' : 'PUT';
|
|
|
|
let ipanel = Ext.create('PVE.sdn.IpamEditInputPanel', {
|
|
isCreate: me.isCreate,
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: [
|
|
ipanel,
|
|
],
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
ipanel.setValues(me.mapping);
|
|
},
|
|
});
|
|
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: 'IPAM',
|
|
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.controllers.IsisInputPanel', {
|
|
extend: 'PVE.panel.SDNControllerBase',
|
|
|
|
onlineHelp: 'pvesdn_controller_plugin_evpn',
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
if (me.isCreate) {
|
|
values.type = me.type;
|
|
values.controller = 'isis' + 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: 'textfield',
|
|
name: 'isis-domain',
|
|
fieldLabel: 'Domain',
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'isis-net',
|
|
fieldLabel: 'Network entity title',
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'isis-ifaces',
|
|
fieldLabel: gettext('Interfaces'),
|
|
allowBlank: false,
|
|
},
|
|
];
|
|
|
|
me.advancedItems = [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'loopback',
|
|
fieldLabel: gettext('Loopback Interface'),
|
|
},
|
|
];
|
|
|
|
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.items.unshift({
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'zone',
|
|
maxLength: 8,
|
|
value: me.zone || '',
|
|
fieldLabel: 'ID',
|
|
allowBlank: false,
|
|
});
|
|
|
|
me.items.push(
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'mtu',
|
|
minValue: 100,
|
|
maxValue: 65000,
|
|
fieldLabel: 'MTU',
|
|
allowBlank: true,
|
|
emptyText: 'auto',
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
name: 'nodes',
|
|
fieldLabel: gettext('Nodes'),
|
|
emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
|
|
multiSelect: true,
|
|
autoSelect: false,
|
|
},
|
|
{
|
|
xtype: 'pveSDNIpamSelector',
|
|
fieldLabel: gettext('IPAM'),
|
|
name: 'ipam',
|
|
value: me.ipam || 'pve',
|
|
allowBlank: false,
|
|
},
|
|
);
|
|
|
|
me.advancedItems = me.advancedItems ?? [];
|
|
|
|
me.advancedItems.unshift(
|
|
{
|
|
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,
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.items = [
|
|
{
|
|
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: 'proxmoxtextfield',
|
|
name: 'mac',
|
|
fieldLabel: gettext('VNet MAC Address'),
|
|
vtype: 'MacAddress',
|
|
allowBlank: true,
|
|
emptyText: 'auto',
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
{
|
|
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,
|
|
skipEmptyText: true,
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'exitnodes-local-routing',
|
|
uncheckedValue: null,
|
|
checked: false,
|
|
fieldLabel: gettext('Exit Nodes Local Routing'),
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'advertise-subnets',
|
|
uncheckedValue: null,
|
|
checked: false,
|
|
fieldLabel: gettext('Advertise Subnets'),
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'disable-arp-nd-suppression',
|
|
uncheckedValue: null,
|
|
checked: false,
|
|
fieldLabel: gettext('Disable ARP-nd Suppression'),
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'rt-import',
|
|
fieldLabel: gettext('Route Target Import'),
|
|
allowBlank: true,
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
];
|
|
|
|
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: 'textfield',
|
|
name: 'bridge',
|
|
fieldLabel: 'Bridge',
|
|
allowBlank: false,
|
|
vtype: 'BridgeName',
|
|
minLength: 1,
|
|
maxLength: 10,
|
|
},
|
|
{
|
|
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'],
|
|
],
|
|
},
|
|
];
|
|
|
|
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 = [];
|
|
me.advancedItems = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'dhcp',
|
|
inputValue: 'dnsmasq',
|
|
uncheckedValue: null,
|
|
checked: false,
|
|
fieldLabel: gettext('automatic DHCP'),
|
|
deleteEmpty: !me.isCreate,
|
|
},
|
|
];
|
|
|
|
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: 'textfield',
|
|
name: 'bridge',
|
|
fieldLabel: 'Bridge',
|
|
allowBlank: false,
|
|
vtype: 'BridgeName',
|
|
minLength: 1,
|
|
maxLength: 10,
|
|
},
|
|
];
|
|
|
|
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: 'textfield',
|
|
name: 'peers',
|
|
fieldLabel: gettext('Peer Address List'),
|
|
allowBlank: false,
|
|
},
|
|
];
|
|
|
|
me.callParent();
|
|
},
|
|
});
|
|
Ext.define('PVE.storage.ContentView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: 'widget.pveStorageContentView',
|
|
|
|
itemdblclick: Ext.emptyFn,
|
|
|
|
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);
|
|
|
|
let tbar = me.tbar ? [...me.tbar] : [];
|
|
if (me.useUploadButton) {
|
|
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) {
|
|
tbar.push({
|
|
xtype: 'proxmoxStdRemoveButton',
|
|
selModel: sm,
|
|
enableFn: rec => !rec?.data?.protected,
|
|
delay: 5,
|
|
callback: () => reload(),
|
|
baseurl: baseurl + '/',
|
|
});
|
|
}
|
|
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: `<i class="fa fa-shield"></i>`,
|
|
tooltip: gettext('Protected'),
|
|
width: 30,
|
|
renderer: v => v ? `<i data-qtip="${gettext('Protected')}" class="fa fa-shield"></i>` : '',
|
|
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,
|
|
selModel: sm,
|
|
tbar,
|
|
columns,
|
|
listeners: {
|
|
activate: reload,
|
|
itemdblclick: (view, record) => me.itemdblclick(view, record),
|
|
},
|
|
});
|
|
|
|
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,
|
|
);
|
|
|
|
me.extraColumns = {};
|
|
|
|
if (isPBS) {
|
|
me.extraColumns.encrypted = {
|
|
header: gettext('Encrypted'),
|
|
dataIndex: 'encrypted',
|
|
renderer: PVE.Utils.render_backup_encryption,
|
|
sorter: {
|
|
property: 'encrypted',
|
|
transform: encrypted => encrypted ? 1 : 0,
|
|
},
|
|
};
|
|
me.extraColumns.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.extraColumns.vmid = {
|
|
header: 'VMID',
|
|
dataIndex: 'vmid',
|
|
hidden: true,
|
|
sorter: (a, b) => (a.data.vmid ?? 0) - (b.data.vmid ?? 0),
|
|
};
|
|
|
|
me.callParent();
|
|
|
|
me.store.getSorters().clear();
|
|
me.store.setSorters([
|
|
{
|
|
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";
|
|
}
|
|
|
|
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;
|
|
|
|
me.items = plugin !== 'esxi' ? [
|
|
{
|
|
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 contents = res.content.split(',');
|
|
|
|
let enableUpload = !!caps.storage['Datastore.AllocateTemplate'];
|
|
let enableDownloadUrl = enableUpload && (
|
|
!!(caps.nodes['Sys.Audit'] && caps.nodes['Sys.Modify']) || // for backward compat
|
|
!!caps.nodes['Sys.AccessNetwork'] // new explicit priv for querying (local) networks
|
|
);
|
|
|
|
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 (contents.includes('import')) {
|
|
let createGuestImportWindow = (selection) => {
|
|
if (!selection) {
|
|
return;
|
|
}
|
|
|
|
let volumeName = selection.data.volid.replace(/^.*?:/, '');
|
|
|
|
Ext.create('PVE.window.GuestImport', {
|
|
storage: storeid,
|
|
volumeName,
|
|
nodename,
|
|
autoShow: true,
|
|
});
|
|
};
|
|
me.items.push({
|
|
xtype: 'pveStorageContentView',
|
|
title: gettext('Virtual Guests'),
|
|
iconCls: 'fa fa-desktop',
|
|
itemId: 'contentImport',
|
|
content: 'import',
|
|
useCustomRemoveButton: true, // hide default remove button
|
|
showColumns: ['name', 'format'],
|
|
itemdblclick: (view, record) => createGuestImportWindow(record),
|
|
tbar: [
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
disabled: true,
|
|
text: gettext('Import'),
|
|
iconCls: 'fa fa-cloud-download',
|
|
handler: function() {
|
|
let grid = this.up('pveStorageContentView');
|
|
let selection = grid.getSelection()?.[0];
|
|
|
|
createGuestImportWindow(selection);
|
|
},
|
|
},
|
|
],
|
|
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;
|
|
}
|
|
if (values.subdir?.length === 0) {
|
|
delete values.subdir;
|
|
}
|
|
|
|
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);
|
|
}
|
|
},
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
editable: me.isCreate,
|
|
name: 'subdir',
|
|
fieldLabel: gettext('Subdirectory'),
|
|
allowBlank: true,
|
|
emptyText: gettext('/some/path'),
|
|
},
|
|
];
|
|
|
|
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'),
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Enable if the underlying file system is already shared between nodes.'),
|
|
},
|
|
},
|
|
];
|
|
|
|
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 += '<br />' + 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.Utils.isStandaloneNode()) {
|
|
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'),
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Enable if the LVM is located on a shared LUN.'),
|
|
},
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'saferemove',
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Wipe Removed Volumes'),
|
|
},
|
|
],
|
|
});
|
|
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.')
|
|
+ '<br>' + 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 = `<html><head><script>
|
|
window.addEventListener('DOMContentLoaded', (ev) => window.print());
|
|
</script><style>@media print and (max-height: 150mm) {
|
|
h4, p { margin: 0; font-size: 1em; }
|
|
}</style></head><body style="padding: 5px;">
|
|
<h4>Encryption Key - Storage '${me.sid}' (${shortKeyFP})</h4>
|
|
<p style="font-size:1.2em;font-family:monospace;white-space:pre-wrap;overflow-wrap:break-word;">
|
|
-----BEGIN PROXMOX BACKUP KEY-----
|
|
${prettifiedKey}
|
|
-----END PROXMOX BACKUP KEY-----</p>
|
|
<center><img style="width: 100%; max-width: ${qrwidth}px;" src="${keyQrBase64}"></center>
|
|
</body></html>`;
|
|
|
|
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 = '<span class="fa fa-lock good"></span> ';
|
|
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: // `<b style="color:red;font-weight:600;">${gettext('Warning')}</b>: ` +
|
|
`<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` +
|
|
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: '16k',
|
|
fieldLabel: gettext('Block Size'),
|
|
allowBlank: true,
|
|
},
|
|
],
|
|
});
|
|
Ext.define('PVE.storage.ESXIInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
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.server = server;
|
|
|
|
return me.callParent([values]);
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
|
|
if (values.password?.length === 0) {
|
|
delete values.password;
|
|
}
|
|
if (values.username?.length === 0) {
|
|
delete values.username;
|
|
}
|
|
|
|
if (me.isCreate) {
|
|
let serverPortMatch = Proxmox.Utils.HostPort_match.exec(values.server);
|
|
if (serverPortMatch === null) {
|
|
serverPortMatch = Proxmox.Utils.HostPortBrackets_match.exec(values.server);
|
|
if (serverPortMatch === null) {
|
|
serverPortMatch = Proxmox.Utils.IP6_dotnotation_match.exec(values.server);
|
|
}
|
|
}
|
|
|
|
if (serverPortMatch !== null) {
|
|
values.server = serverPortMatch[1];
|
|
if (serverPortMatch[2] !== undefined) {
|
|
values.port = serverPortMatch[2];
|
|
}
|
|
}
|
|
}
|
|
|
|
return me.callParent([values]);
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: 'pmxDisplayEditField',
|
|
name: 'server',
|
|
fieldLabel: gettext('Server'),
|
|
editable: me.isCreate,
|
|
emptyText: gettext('IP address or hostname'),
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'username',
|
|
fieldLabel: gettext('Username'),
|
|
allowBlank: false,
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'password',
|
|
fieldLabel: gettext('Password'),
|
|
inputType: 'password',
|
|
emptyText: gettext('Unchanged'),
|
|
minLength: 1,
|
|
allowBlank: !me.isCreate,
|
|
},
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'skip-cert-verification',
|
|
fieldLabel: gettext('Skip Certificate Verification'),
|
|
value: false,
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
deleteDefaultValue: !me.isCreate,
|
|
},
|
|
];
|
|
|
|
me.callParent();
|
|
},
|
|
});
|
|
/*
|
|
* 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,
|
|
confirmCurrentPassword: Proxmox.UserName !== 'root@pam',
|
|
});
|
|
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 && viewWidth > 150) {
|
|
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 && viewHeight > 200) {
|
|
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'));
|
|
}
|
|
});
|
|
|
|
let tagSelectors = [];
|
|
['circle', 'dense'].forEach((style) => {
|
|
['dark', 'light'].forEach((variant) => {
|
|
tagSelectors.push(`.proxmox-tags-${style} .proxmox-tag-${variant}`);
|
|
});
|
|
});
|
|
|
|
Ext.create('Ext.tip.ToolTip', {
|
|
target: me.el,
|
|
delegate: tagSelectors.join(', '),
|
|
trackMouse: true,
|
|
renderTo: Ext.getBody(),
|
|
border: 0,
|
|
minWidth: 0,
|
|
padding: 0,
|
|
bodyBorder: 0,
|
|
bodyPadding: 0,
|
|
dismissDelay: 0,
|
|
userCls: 'pmx-tag-tooltip',
|
|
shadow: false,
|
|
listeners: {
|
|
beforeshow: function(tip) {
|
|
let tag = Ext.htmlEncode(tip.triggerElement.innerHTML);
|
|
let tagEl = Proxmox.Utils.getTagElement(tag, PVE.UIOptions.tagOverrides);
|
|
tip.update(`<span class="proxmox-tags-full">${tagEl}</span>`);
|
|
},
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|