You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
39779 lines
852 KiB
39779 lines
852 KiB
var pveOnlineHelpInfo = {
|
|
"ceph_rados_block_devices" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices",
|
|
"title" : "Ceph RADOS Block Devices (RBD)"
|
|
},
|
|
"chapter_ha_manager" : {
|
|
"link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager",
|
|
"title" : "High Availability"
|
|
},
|
|
"chapter_lvm" : {
|
|
"link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm",
|
|
"title" : "Logical Volume Manager (LVM)"
|
|
},
|
|
"chapter_pct" : {
|
|
"link" : "/pve-docs/chapter-pct.html#chapter_pct",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"chapter_pve_firewall" : {
|
|
"link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall",
|
|
"title" : "Proxmox VE Firewall"
|
|
},
|
|
"chapter_pveceph" : {
|
|
"link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph",
|
|
"title" : "Manage Ceph Services on Proxmox VE Nodes"
|
|
},
|
|
"chapter_pvecm" : {
|
|
"link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm",
|
|
"title" : "Cluster Manager"
|
|
},
|
|
"chapter_pvesr" : {
|
|
"link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr",
|
|
"title" : "Storage Replication"
|
|
},
|
|
"chapter_storage" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#chapter_storage",
|
|
"title" : "Proxmox VE Storage"
|
|
},
|
|
"chapter_system_administration" : {
|
|
"link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration",
|
|
"title" : "Host System Administration"
|
|
},
|
|
"chapter_user_management" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#chapter_user_management",
|
|
"title" : "User Management"
|
|
},
|
|
"chapter_virtual_machines" : {
|
|
"link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"chapter_vzdump" : {
|
|
"link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump",
|
|
"title" : "Backup and Restore"
|
|
},
|
|
"chapter_zfs" : {
|
|
"link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs",
|
|
"title" : "ZFS on Linux"
|
|
},
|
|
"datacenter_configuration_file" : {
|
|
"link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file",
|
|
"title" : "Datacenter Configuration"
|
|
},
|
|
"getting_help" : {
|
|
"link" : "/pve-docs/pve-admin-guide.html#getting_help",
|
|
"title" : "Getting Help"
|
|
},
|
|
"gui_my_settings" : {
|
|
"link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings",
|
|
"subtitle" : "My Settings",
|
|
"title" : "Graphical User Interface"
|
|
},
|
|
"ha_manager_fencing" : {
|
|
"link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing",
|
|
"subtitle" : "Fencing",
|
|
"title" : "High Availability"
|
|
},
|
|
"ha_manager_groups" : {
|
|
"link" : "/pve-docs/chapter-ha-manager.html#ha_manager_groups",
|
|
"subtitle" : "Groups",
|
|
"title" : "High Availability"
|
|
},
|
|
"ha_manager_resource_config" : {
|
|
"link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resource_config",
|
|
"subtitle" : "Resources",
|
|
"title" : "High Availability"
|
|
},
|
|
"ha_manager_resources" : {
|
|
"link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resources",
|
|
"subtitle" : "Resources",
|
|
"title" : "High Availability"
|
|
},
|
|
"pct_configuration" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_configuration",
|
|
"subtitle" : "Configuration",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_container_images" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_container_images",
|
|
"subtitle" : "Container Images",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_container_network" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_container_network",
|
|
"subtitle" : "Network",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_container_storage" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_container_storage",
|
|
"subtitle" : "Container Storage",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_cpu" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_cpu",
|
|
"subtitle" : "CPU",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_general" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_general",
|
|
"subtitle" : "General Settings",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_memory" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_memory",
|
|
"subtitle" : "Memory",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_migration" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_migration",
|
|
"subtitle" : "Migration",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_options" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_options",
|
|
"subtitle" : "Options",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_snapshots" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_snapshots",
|
|
"subtitle" : "Snapshots",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pct_startup_and_shutdown" : {
|
|
"link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown",
|
|
"subtitle" : "Automatic Start and Shutdown of Containers",
|
|
"title" : "Proxmox Container Toolkit"
|
|
},
|
|
"pve_admin_guide" : {
|
|
"link" : "/pve-docs/pve-admin-guide.html",
|
|
"title" : "Proxmox VE Administration Guide"
|
|
},
|
|
"pve_ceph_install" : {
|
|
"link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install",
|
|
"subtitle" : "Installation of Ceph Packages",
|
|
"title" : "Manage Ceph Services on Proxmox VE Nodes"
|
|
},
|
|
"pve_ceph_osds" : {
|
|
"link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds",
|
|
"subtitle" : "Creating Ceph OSDs",
|
|
"title" : "Manage Ceph Services on Proxmox VE Nodes"
|
|
},
|
|
"pve_ceph_pools" : {
|
|
"link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools",
|
|
"subtitle" : "Creating Ceph Pools",
|
|
"title" : "Manage Ceph Services on Proxmox VE Nodes"
|
|
},
|
|
"pve_documentation_index" : {
|
|
"link" : "/pve-docs/index.html",
|
|
"title" : "Proxmox VE Documentation Index"
|
|
},
|
|
"pve_firewall_cluster_wide_setup" : {
|
|
"link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup",
|
|
"subtitle" : "Cluster Wide Setup",
|
|
"title" : "Proxmox VE Firewall"
|
|
},
|
|
"pve_firewall_host_specific_configuration" : {
|
|
"link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration",
|
|
"subtitle" : "Host Specific Configuration",
|
|
"title" : "Proxmox VE Firewall"
|
|
},
|
|
"pve_firewall_ip_aliases" : {
|
|
"link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases",
|
|
"subtitle" : "IP Aliases",
|
|
"title" : "Proxmox VE Firewall"
|
|
},
|
|
"pve_firewall_ip_sets" : {
|
|
"link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets",
|
|
"subtitle" : "IP Sets",
|
|
"title" : "Proxmox VE Firewall"
|
|
},
|
|
"pve_firewall_vm_container_configuration" : {
|
|
"link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration",
|
|
"subtitle" : "VM/Container Configuration",
|
|
"title" : "Proxmox VE Firewall"
|
|
},
|
|
"pve_service_daemons" : {
|
|
"link" : "/pve-docs/index.html#_service_daemons",
|
|
"title" : "Service Daemons"
|
|
},
|
|
"pveceph_fs" : {
|
|
"link" : "/pve-docs/chapter-pveceph.html#pveceph_fs",
|
|
"subtitle" : "CephFS",
|
|
"title" : "Manage Ceph Services on Proxmox VE Nodes"
|
|
},
|
|
"pveceph_fs_create" : {
|
|
"link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create",
|
|
"subtitle" : "Create a CephFS",
|
|
"title" : "Manage Ceph Services on Proxmox VE Nodes"
|
|
},
|
|
"pvecm_create_cluster" : {
|
|
"link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster",
|
|
"subtitle" : "Create the Cluster",
|
|
"title" : "Cluster Manager"
|
|
},
|
|
"pvesr_schedule_time_format" : {
|
|
"link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format",
|
|
"subtitle" : "Schedule Format",
|
|
"title" : "Storage Replication"
|
|
},
|
|
"pveum_authentication_realms" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms",
|
|
"subtitle" : "Authentication Realms",
|
|
"title" : "User Management"
|
|
},
|
|
"pveum_groups" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#pveum_groups",
|
|
"subtitle" : "Groups",
|
|
"title" : "User Management"
|
|
},
|
|
"pveum_permission_management" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#pveum_permission_management",
|
|
"subtitle" : "Permission Management",
|
|
"title" : "User Management"
|
|
},
|
|
"pveum_pools" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#pveum_pools",
|
|
"subtitle" : "Pools",
|
|
"title" : "User Management"
|
|
},
|
|
"pveum_roles" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#pveum_roles",
|
|
"subtitle" : "Roles",
|
|
"title" : "User Management"
|
|
},
|
|
"pveum_tfa_auth" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#pveum_tfa_auth",
|
|
"subtitle" : "Two factor authentication",
|
|
"title" : "User Management"
|
|
},
|
|
"pveum_users" : {
|
|
"link" : "/pve-docs/chapter-pveum.html#pveum_users",
|
|
"subtitle" : "Users",
|
|
"title" : "User Management"
|
|
},
|
|
"qm_bios_and_uefi" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi",
|
|
"subtitle" : "BIOS and UEFI",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_cloud_init" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_cloud_init",
|
|
"title" : "Cloud-Init Support"
|
|
},
|
|
"qm_copy_and_clone" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone",
|
|
"subtitle" : "Copies and Clones",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_cpu" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_cpu",
|
|
"subtitle" : "CPU",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_general_settings" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_general_settings",
|
|
"subtitle" : "General Settings",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_hard_disk" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_hard_disk",
|
|
"subtitle" : "Hard Disk",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_memory" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_memory",
|
|
"subtitle" : "Memory",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_migration" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_migration",
|
|
"subtitle" : "Migration",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_network_device" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_network_device",
|
|
"subtitle" : "Network Device",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_options" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_options",
|
|
"subtitle" : "Options",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_os_settings" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_os_settings",
|
|
"subtitle" : "OS Settings",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_pci_passthrough" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough",
|
|
"title" : "PCI(e) Passthrough"
|
|
},
|
|
"qm_startup_and_shutdown" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown",
|
|
"subtitle" : "Automatic Start and Shutdown of Virtual Machines",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_system_settings" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_system_settings",
|
|
"subtitle" : "System Settings",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_usb_passthrough" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough",
|
|
"subtitle" : "USB Passthrough",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"qm_virtual_machines_settings" : {
|
|
"link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings",
|
|
"subtitle" : "Virtual Machines Settings",
|
|
"title" : "Qemu/KVM Virtual Machines"
|
|
},
|
|
"storage_cephfs" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_cephfs",
|
|
"title" : "Ceph Filesystem (CephFS)"
|
|
},
|
|
"storage_cifs" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_cifs",
|
|
"title" : "CIFS Backend"
|
|
},
|
|
"storage_directory" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_directory",
|
|
"title" : "Directory Backend"
|
|
},
|
|
"storage_glusterfs" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_glusterfs",
|
|
"title" : "GlusterFS Backend"
|
|
},
|
|
"storage_lvm" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_lvm",
|
|
"title" : "LVM Backend"
|
|
},
|
|
"storage_lvmthin" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_lvmthin",
|
|
"title" : "LVM thin Backend"
|
|
},
|
|
"storage_nfs" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_nfs",
|
|
"title" : "NFS Backend"
|
|
},
|
|
"storage_open_iscsi" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_open_iscsi",
|
|
"title" : "Open-iSCSI initiator"
|
|
},
|
|
"storage_zfspool" : {
|
|
"link" : "/pve-docs/chapter-pvesm.html#storage_zfspool",
|
|
"title" : "Local ZFS Pool Backend"
|
|
},
|
|
"sysadmin_certificate_management" : {
|
|
"link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management",
|
|
"title" : "Certificate Management"
|
|
},
|
|
"sysadmin_network_configuration" : {
|
|
"link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration",
|
|
"title" : "Network Configuration"
|
|
}
|
|
};
|
|
Ext.ns('PVE');
|
|
|
|
// avoid errors related to Accessible Rich Internet Applications
|
|
// (access for people with disabilities)
|
|
// TODO reenable after all components are upgraded
|
|
Ext.enableAria = false;
|
|
Ext.enableAriaButtons = false;
|
|
Ext.enableAriaPanels = false;
|
|
|
|
// avoid errors when running without development tools
|
|
if (!Ext.isDefined(Ext.global.console)) {
|
|
var console = {
|
|
log: function() {}
|
|
};
|
|
}
|
|
console.log("Starting PVE Manager");
|
|
|
|
Ext.Ajax.defaultHeaders = {
|
|
'Accept': 'application/json'
|
|
};
|
|
|
|
/*jslint confusion: true */
|
|
Ext.define('PVE.Utils', { utilities: {
|
|
|
|
// this singleton contains miscellaneous utilities
|
|
|
|
toolkit: undefined, // (extjs|touch), set inside Toolkit.js
|
|
|
|
bus_match: /^(ide|sata|virtio|scsi)\d+$/,
|
|
|
|
log_severity_hash: {
|
|
0: "panic",
|
|
1: "alert",
|
|
2: "critical",
|
|
3: "error",
|
|
4: "warning",
|
|
5: "notice",
|
|
6: "info",
|
|
7: "debug"
|
|
},
|
|
|
|
support_level_hash: {
|
|
'c': gettext('Community'),
|
|
'b': gettext('Basic'),
|
|
's': gettext('Standard'),
|
|
'p': gettext('Premium')
|
|
},
|
|
|
|
noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit <a target="_blank" href="https://www.proxmox.com/products/proxmox-ve/subscription-service-plans">www.proxmox.com</a> to get a list of available options.',
|
|
|
|
kvm_ostypes: {
|
|
'Linux': [
|
|
{ desc: '5.x - 2.6 Kernel', val: 'l26' },
|
|
{ desc: '2.4 Kernel', val: 'l24' }
|
|
],
|
|
'Microsoft Windows': [
|
|
{ desc: '10/2016', val: 'win10' },
|
|
{ desc: '8.x/2012/2012r2', val: 'win8' },
|
|
{ desc: '7/2008r2', val: 'win7' },
|
|
{ desc: 'Vista/2008', val: 'w2k8' },
|
|
{ desc: 'XP/2003', val: 'wxp' },
|
|
{ desc: '2000', val: 'w2k' }
|
|
],
|
|
'Solaris Kernel': [
|
|
{ desc: '-', val: 'solaris'}
|
|
],
|
|
'Other': [
|
|
{ desc: '-', val: 'other'}
|
|
]
|
|
},
|
|
|
|
get_health_icon: function(state, circle) {
|
|
if (circle === undefined) {
|
|
circle = false;
|
|
}
|
|
|
|
if (state === undefined) {
|
|
state = 'uknown';
|
|
}
|
|
|
|
var icon = 'faded fa-question';
|
|
switch(state) {
|
|
case 'good':
|
|
icon = 'good fa-check';
|
|
break;
|
|
case '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) {
|
|
if (a === b) {
|
|
return 0;
|
|
}
|
|
let avers = a.toString().split('.');
|
|
let bvers = b.toString().split('.');
|
|
|
|
while (true) {
|
|
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_OLD':'old',
|
|
'HEALTH_WARN':'warning',
|
|
'HEALTH_ERR':'critical'
|
|
},
|
|
|
|
render_ceph_health: function(healthObj) {
|
|
var state = {
|
|
iconCls: PVE.Utils.get_health_icon(),
|
|
text: ''
|
|
};
|
|
|
|
if (!healthObj || !healthObj.status) {
|
|
return state;
|
|
}
|
|
|
|
var health = PVE.Utils.map_ceph_health[healthObj.status];
|
|
|
|
state.iconCls = PVE.Utils.get_health_icon(health, true);
|
|
state.text = healthObj.status;
|
|
|
|
return state;
|
|
},
|
|
|
|
render_zfs_health: function(value) {
|
|
if (typeof value == 'undefined'){
|
|
return "";
|
|
}
|
|
var iconCls = 'question-circle';
|
|
switch (value) {
|
|
case 'AVAIL':
|
|
case 'ONLINE':
|
|
iconCls = 'check-circle good';
|
|
break;
|
|
case 'REMOVED':
|
|
case 'DEGRADED':
|
|
iconCls = 'exclamation-circle warning';
|
|
break;
|
|
case 'UNAVAIL':
|
|
case 'FAULTED':
|
|
case 'OFFLINE':
|
|
iconCls = 'times-circle critical';
|
|
break;
|
|
default: //unknown
|
|
}
|
|
|
|
return '<i class="fa fa-' + iconCls + '"></i> ' + value;
|
|
|
|
},
|
|
|
|
get_kvm_osinfo: function(value) {
|
|
var info = { base: 'Other' }; // default
|
|
if (value) {
|
|
Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) {
|
|
Ext.each(PVE.Utils.kvm_ostypes[k], function(e) {
|
|
if (e.val === value) {
|
|
info = { desc: e.desc, base: k };
|
|
}
|
|
});
|
|
});
|
|
}
|
|
return info;
|
|
},
|
|
|
|
render_kvm_ostype: function (value) {
|
|
var osinfo = PVE.Utils.get_kvm_osinfo(value);
|
|
if (osinfo.desc && osinfo.desc !== '-') {
|
|
return osinfo.base + ' ' + osinfo.desc;
|
|
} else {
|
|
return osinfo.base;
|
|
}
|
|
},
|
|
|
|
render_hotplug_features: function (value) {
|
|
var fa = [];
|
|
|
|
if (!value || (value === '0')) {
|
|
return gettext('Disabled');
|
|
}
|
|
|
|
if (value === '1') {
|
|
value = 'disk,network,usb';
|
|
}
|
|
|
|
Ext.each(value.split(','), function(el) {
|
|
if (el === 'disk') {
|
|
fa.push(gettext('Disk'));
|
|
} else if (el === 'network') {
|
|
fa.push(gettext('Network'));
|
|
} else if (el === 'usb') {
|
|
fa.push('USB');
|
|
} else if (el === 'memory') {
|
|
fa.push(gettext('Memory'));
|
|
} else if (el === 'cpu') {
|
|
fa.push(gettext('CPU'));
|
|
} else {
|
|
fa.push(el);
|
|
}
|
|
});
|
|
|
|
return fa.join(', ');
|
|
},
|
|
|
|
render_qga_features: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')';
|
|
}
|
|
var props = PVE.Parser.parsePropertyString(value, 'enabled');
|
|
if (!PVE.Parser.parseBoolean(props.enabled)) {
|
|
return Proxmox.Utils.disabledText;
|
|
}
|
|
|
|
delete props.enabled;
|
|
var agentstring = Proxmox.Utils.enabledText;
|
|
|
|
Ext.Object.each(props, function(key, value) {
|
|
var keystring = '' ;
|
|
agentstring += ', ' + key + ': ';
|
|
|
|
if (PVE.Parser.parseBoolean(value)) {
|
|
agentstring += Proxmox.Utils.enabledText;
|
|
} else {
|
|
agentstring += Proxmox.Utils.disabledText;
|
|
}
|
|
});
|
|
|
|
return agentstring;
|
|
},
|
|
|
|
render_qemu_machine: function(value) {
|
|
return value || (Proxmox.Utils.defaultText + ' (i440fx)');
|
|
},
|
|
|
|
render_qemu_bios: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText + ' (SeaBIOS)';
|
|
} else if (value === 'seabios') {
|
|
return "SeaBIOS";
|
|
} else if (value === 'ovmf') {
|
|
return "OVMF (UEFI)";
|
|
} else {
|
|
return value;
|
|
}
|
|
},
|
|
|
|
render_dc_ha_opts: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText;
|
|
} else {
|
|
return PVE.Parser.printPropertyString(value);
|
|
}
|
|
},
|
|
render_as_property_string: function(value) {
|
|
return (!value) ? Proxmox.Utils.defaultText
|
|
: PVE.Parser.printPropertyString(value);
|
|
},
|
|
|
|
render_scsihw: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText + ' (LSI 53C895A)';
|
|
} else if (value === 'lsi') {
|
|
return 'LSI 53C895A';
|
|
} else if (value === 'lsi53c810') {
|
|
return 'LSI 53C810';
|
|
} else if (value === 'megasas') {
|
|
return 'MegaRAID SAS 8708EM2';
|
|
} else if (value === 'virtio-scsi-pci') {
|
|
return 'VirtIO SCSI';
|
|
} else if (value === 'virtio-scsi-single') {
|
|
return 'VirtIO SCSI single';
|
|
} else if (value === 'pvscsi') {
|
|
return 'VMware PVSCSI';
|
|
} else {
|
|
return value;
|
|
}
|
|
},
|
|
|
|
// fixme: auto-generate this
|
|
// for now, please keep in sync with PVE::Tools::kvmkeymaps
|
|
kvm_keymaps: {
|
|
//ar: 'Arabic',
|
|
da: 'Danish',
|
|
de: 'German',
|
|
'de-ch': 'German (Swiss)',
|
|
'en-gb': 'English (UK)',
|
|
'en-us': 'English (USA)',
|
|
es: 'Spanish',
|
|
//et: 'Estonia',
|
|
fi: 'Finnish',
|
|
//fo: 'Faroe Islands',
|
|
fr: 'French',
|
|
'fr-be': 'French (Belgium)',
|
|
'fr-ca': 'French (Canada)',
|
|
'fr-ch': 'French (Swiss)',
|
|
//hr: 'Croatia',
|
|
hu: 'Hungarian',
|
|
is: 'Icelandic',
|
|
it: 'Italian',
|
|
ja: 'Japanese',
|
|
lt: 'Lithuanian',
|
|
//lv: 'Latvian',
|
|
mk: 'Macedonian',
|
|
nl: 'Dutch',
|
|
//'nl-be': 'Dutch (Belgium)',
|
|
no: 'Norwegian',
|
|
pl: 'Polish',
|
|
pt: 'Portuguese',
|
|
'pt-br': 'Portuguese (Brazil)',
|
|
//ru: 'Russian',
|
|
sl: 'Slovenian',
|
|
sv: 'Swedish',
|
|
//th: 'Thai',
|
|
tr: 'Turkish'
|
|
},
|
|
|
|
kvm_vga_drivers: {
|
|
std: gettext('Standard VGA'),
|
|
vmware: gettext('VMware compatible'),
|
|
qxl: 'SPICE',
|
|
qxl2: 'SPICE dual monitor',
|
|
qxl3: 'SPICE three monitors',
|
|
qxl4: 'SPICE four monitors',
|
|
serial0: gettext('Serial terminal') + ' 0',
|
|
serial1: gettext('Serial terminal') + ' 1',
|
|
serial2: gettext('Serial terminal') + ' 2',
|
|
serial3: gettext('Serial terminal') + ' 3',
|
|
virtio: 'VirtIO-GPU',
|
|
none: Proxmox.Utils.noneText
|
|
},
|
|
|
|
render_kvm_language: function (value) {
|
|
if (!value || value === '__default__') {
|
|
return Proxmox.Utils.defaultText;
|
|
}
|
|
var text = PVE.Utils.kvm_keymaps[value];
|
|
if (text) {
|
|
return text + ' (' + value + ')';
|
|
}
|
|
return value;
|
|
},
|
|
|
|
kvm_keymap_array: function() {
|
|
var data = [['__default__', PVE.Utils.render_kvm_language('')]];
|
|
Ext.Object.each(PVE.Utils.kvm_keymaps, function(key, value) {
|
|
data.push([key, PVE.Utils.render_kvm_language(value)]);
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
console_map: {
|
|
'__default__': Proxmox.Utils.defaultText + ' (xterm.js)',
|
|
'vv': 'SPICE (remote-viewer)',
|
|
'html5': 'HTML5 (noVNC)',
|
|
'xtermjs': 'xterm.js'
|
|
},
|
|
|
|
render_console_viewer: function(value) {
|
|
value = value || '__default__';
|
|
if (PVE.Utils.console_map[value]) {
|
|
return PVE.Utils.console_map[value];
|
|
}
|
|
return value;
|
|
},
|
|
|
|
console_viewer_array: function() {
|
|
return Ext.Array.map(Object.keys(PVE.Utils.console_map), function(v) {
|
|
return [v, PVE.Utils.render_console_viewer(v)];
|
|
});
|
|
},
|
|
|
|
render_kvm_vga_driver: function (value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.defaultText;
|
|
}
|
|
var vga = PVE.Parser.parsePropertyString(value, 'type');
|
|
var text = PVE.Utils.kvm_vga_drivers[vga.type];
|
|
if (!vga.type) {
|
|
text = Proxmox.Utils.defaultText;
|
|
}
|
|
if (text) {
|
|
return text + ' (' + value + ')';
|
|
}
|
|
return value;
|
|
},
|
|
|
|
kvm_vga_driver_array: function() {
|
|
var data = [['__default__', PVE.Utils.render_kvm_vga_driver('')]];
|
|
Ext.Object.each(PVE.Utils.kvm_vga_drivers, function(key, value) {
|
|
data.push([key, PVE.Utils.render_kvm_vga_driver(value)]);
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
render_kvm_startup: function(value) {
|
|
var startup = PVE.Parser.parseStartup(value);
|
|
|
|
var res = 'order=';
|
|
if (startup.order === undefined) {
|
|
res += 'any';
|
|
} else {
|
|
res += startup.order;
|
|
}
|
|
if (startup.up !== undefined) {
|
|
res += ',up=' + startup.up;
|
|
}
|
|
if (startup.down !== undefined) {
|
|
res += ',down=' + startup.down;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
extractFormActionError: function(action) {
|
|
var msg;
|
|
switch (action.failureType) {
|
|
case Ext.form.action.Action.CLIENT_INVALID:
|
|
msg = gettext('Form fields may not be submitted with invalid values');
|
|
break;
|
|
case Ext.form.action.Action.CONNECT_FAILURE:
|
|
msg = gettext('Connection error');
|
|
var resp = action.response;
|
|
if (resp.status && resp.statusText) {
|
|
msg += " " + resp.status + ": " + resp.statusText;
|
|
}
|
|
break;
|
|
case Ext.form.action.Action.LOAD_FAILURE:
|
|
case Ext.form.action.Action.SERVER_INVALID:
|
|
msg = Proxmox.Utils.extractRequestError(action.result, true);
|
|
break;
|
|
}
|
|
return msg;
|
|
},
|
|
|
|
format_duration_short: function(ut) {
|
|
|
|
if (ut < 60) {
|
|
return ut.toFixed(1) + 's';
|
|
}
|
|
|
|
if (ut < 3600) {
|
|
var mins = ut / 60;
|
|
return mins.toFixed(1) + 'm';
|
|
}
|
|
|
|
if (ut < 86400) {
|
|
var hours = ut / 3600;
|
|
return hours.toFixed(1) + 'h';
|
|
}
|
|
|
|
var days = ut / 86400;
|
|
return days.toFixed(1) + 'd';
|
|
},
|
|
|
|
contentTypes: {
|
|
'images': gettext('Disk image'),
|
|
'backup': gettext('VZDump backup file'),
|
|
'vztmpl': gettext('Container template'),
|
|
'iso': gettext('ISO image'),
|
|
'rootdir': gettext('Container'),
|
|
'snippets': gettext('Snippets')
|
|
},
|
|
|
|
storageSchema: {
|
|
dir: {
|
|
name: Proxmox.Utils.directoryText,
|
|
ipanel: 'DirInputPanel',
|
|
faIcon: 'folder'
|
|
},
|
|
lvm: {
|
|
name: 'LVM',
|
|
ipanel: 'LVMInputPanel',
|
|
faIcon: 'folder'
|
|
},
|
|
lvmthin: {
|
|
name: 'LVM-Thin',
|
|
ipanel: 'LvmThinInputPanel',
|
|
faIcon: 'folder'
|
|
},
|
|
nfs: {
|
|
name: 'NFS',
|
|
ipanel: 'NFSInputPanel',
|
|
faIcon: 'building'
|
|
},
|
|
cifs: {
|
|
name: 'CIFS',
|
|
ipanel: 'CIFSInputPanel',
|
|
faIcon: 'building'
|
|
},
|
|
glusterfs: {
|
|
name: 'GlusterFS',
|
|
ipanel: 'GlusterFsInputPanel',
|
|
faIcon: 'building'
|
|
},
|
|
iscsi: {
|
|
name: 'iSCSI',
|
|
ipanel: 'IScsiInputPanel',
|
|
faIcon: 'building'
|
|
},
|
|
cephfs: {
|
|
name: 'CephFS',
|
|
ipanel: 'CephFSInputPanel',
|
|
faIcon: 'building'
|
|
},
|
|
pvecephfs: {
|
|
name: 'CephFS (PVE)',
|
|
ipanel: 'CephFSInputPanel',
|
|
hideAdd: true,
|
|
faIcon: 'building'
|
|
},
|
|
rbd: {
|
|
name: 'RBD',
|
|
ipanel: 'RBDInputPanel',
|
|
faIcon: 'building'
|
|
},
|
|
pveceph: {
|
|
name: 'RBD (PVE)',
|
|
ipanel: 'RBDInputPanel',
|
|
hideAdd: true,
|
|
faIcon: 'building'
|
|
},
|
|
zfs: {
|
|
name: 'ZFS over iSCSI',
|
|
ipanel: 'ZFSInputPanel',
|
|
faIcon: 'building'
|
|
},
|
|
zfspool: {
|
|
name: 'ZFS',
|
|
ipanel: 'ZFSPoolInputPanel',
|
|
faIcon: 'folder'
|
|
},
|
|
drbd: {
|
|
name: 'DRBD',
|
|
hideAdd: true
|
|
}
|
|
},
|
|
|
|
format_storage_type: function(value, md, record) {
|
|
if (value === 'rbd') {
|
|
value = (!record || record.get('monhost') ? 'rbd' : 'pveceph');
|
|
} else if (value === 'cephfs') {
|
|
value = (!record || record.get('monhost') ? 'cephfs' : 'pvecephfs');
|
|
}
|
|
|
|
var schema = PVE.Utils.storageSchema[value];
|
|
if (schema) {
|
|
return schema.name;
|
|
}
|
|
return Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
format_ha: function(value) {
|
|
var text = Proxmox.Utils.noneText;
|
|
|
|
if (value.managed) {
|
|
text = value.state || Proxmox.Utils.noneText;
|
|
|
|
text += ', ' + Proxmox.Utils.groupText + ': ';
|
|
text += value.group || Proxmox.Utils.noneText;
|
|
}
|
|
|
|
return text;
|
|
},
|
|
|
|
format_content_types: function(value) {
|
|
return value.split(',').sort().map(function(ct) {
|
|
return PVE.Utils.contentTypes[ct] || ct;
|
|
}).join(', ');
|
|
},
|
|
|
|
render_storage_content: function(value, metaData, record) {
|
|
var data = record.data;
|
|
if (Ext.isNumber(data.channel) &&
|
|
Ext.isNumber(data.id) &&
|
|
Ext.isNumber(data.lun)) {
|
|
return "CH " +
|
|
Ext.String.leftPad(data.channel,2, '0') +
|
|
" ID " + data.id + " LUN " + data.lun;
|
|
}
|
|
return data.volid.replace(/^.*:(.*\/)?/,'');
|
|
},
|
|
|
|
render_serverity: function (value) {
|
|
return PVE.Utils.log_severity_hash[value] || value;
|
|
},
|
|
|
|
render_cpu: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
|
|
if (!(record.data.uptime && Ext.isNumeric(value))) {
|
|
return '';
|
|
}
|
|
|
|
var maxcpu = record.data.maxcpu || 1;
|
|
|
|
if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) {
|
|
return '';
|
|
}
|
|
|
|
var per = value * 100;
|
|
|
|
return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU');
|
|
},
|
|
|
|
render_size: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
/*jslint confusion: true */
|
|
|
|
if (!Ext.isNumeric(value)) {
|
|
return '';
|
|
}
|
|
|
|
return Proxmox.Utils.format_size(value);
|
|
},
|
|
|
|
render_bandwidth: function(value) {
|
|
if (!Ext.isNumeric(value)) {
|
|
return '';
|
|
}
|
|
|
|
return Proxmox.Utils.format_size(value) + '/s';
|
|
},
|
|
|
|
render_timestamp_human_readable: function(value) {
|
|
return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s');
|
|
},
|
|
|
|
render_duration: function(value) {
|
|
if (value === undefined) {
|
|
return '-';
|
|
}
|
|
return PVE.Utils.format_duration_short(value);
|
|
},
|
|
|
|
calculate_mem_usage: function(data) {
|
|
if (!Ext.isNumeric(data.mem) ||
|
|
data.maxmem === 0 ||
|
|
data.uptime < 1) {
|
|
return -1;
|
|
}
|
|
|
|
return (data.mem / data.maxmem);
|
|
},
|
|
|
|
render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
if (!Ext.isNumeric(value) || value === -1) {
|
|
return '';
|
|
}
|
|
if (value > 1 ) {
|
|
// we got no percentage but bytes
|
|
var mem = value;
|
|
var maxmem = record.data.maxmem;
|
|
if (!record.data.uptime ||
|
|
maxmem === 0 ||
|
|
!Ext.isNumeric(mem)) {
|
|
return '';
|
|
}
|
|
|
|
return ((mem*100)/maxmem).toFixed(1) + " %";
|
|
}
|
|
return (value*100).toFixed(1) + " %";
|
|
},
|
|
|
|
render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
|
|
var mem = value;
|
|
var maxmem = record.data.maxmem;
|
|
|
|
if (!record.data.uptime) {
|
|
return '';
|
|
}
|
|
|
|
if (!(Ext.isNumeric(mem) && maxmem)) {
|
|
return '';
|
|
}
|
|
|
|
return PVE.Utils.render_size(value);
|
|
},
|
|
|
|
calculate_disk_usage: function(data) {
|
|
|
|
if (!Ext.isNumeric(data.disk) ||
|
|
data.type === 'qemu' ||
|
|
(data.type === 'lxc' && data.uptime === 0) ||
|
|
data.maxdisk === 0) {
|
|
return -1;
|
|
}
|
|
|
|
return (data.disk / data.maxdisk);
|
|
},
|
|
|
|
render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
if (!Ext.isNumeric(value) || value === -1) {
|
|
return '';
|
|
}
|
|
|
|
return (value * 100).toFixed(1) + " %";
|
|
},
|
|
|
|
render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) {
|
|
|
|
var disk = value;
|
|
var maxdisk = record.data.maxdisk;
|
|
var type = record.data.type;
|
|
|
|
if (!Ext.isNumeric(disk) ||
|
|
type === 'qemu' ||
|
|
maxdisk === 0 ||
|
|
(type === 'lxc' && record.data.uptime === 0)) {
|
|
return '';
|
|
}
|
|
|
|
return PVE.Utils.render_size(value);
|
|
},
|
|
|
|
get_object_icon_class: function(type, record) {
|
|
var status = '';
|
|
var objType = type;
|
|
|
|
if (type === 'type') {
|
|
// for folder view
|
|
objType = record.groupbyid;
|
|
} else if (record.template) {
|
|
// templates
|
|
objType = 'template';
|
|
status = type;
|
|
} else {
|
|
// everything else
|
|
status = record.status + ' ha-' + record.hastate;
|
|
}
|
|
|
|
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 functions for new status panel */
|
|
|
|
render_usage: function(val) {
|
|
return (val*100).toFixed(2) + '%';
|
|
},
|
|
|
|
render_cpu_usage: function(val, max) {
|
|
return Ext.String.format(gettext('{0}% of {1}') +
|
|
' ' + gettext('CPU(s)'), (val*100).toFixed(2), max);
|
|
},
|
|
|
|
render_size_usage: function(val, max) {
|
|
if (max === 0) {
|
|
return gettext('N/A');
|
|
}
|
|
return (val*100/max).toFixed(2) + '% '+ '(' +
|
|
Ext.String.format(gettext('{0} of {1}'),
|
|
PVE.Utils.render_size(val), PVE.Utils.render_size(max)) + ')';
|
|
},
|
|
|
|
/* this is different for nodes */
|
|
render_node_cpu_usage: function(value, record) {
|
|
return PVE.Utils.render_cpu_usage(value, record.cpus);
|
|
},
|
|
|
|
/* this is different for nodes */
|
|
render_node_size_usage: function(record) {
|
|
return PVE.Utils.render_size_usage(record.used, record.total);
|
|
},
|
|
|
|
render_optional_url: function(value) {
|
|
var match;
|
|
if (value && (match = value.match(/^https?:\/\//)) !== null) {
|
|
return '<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);
|
|
},
|
|
|
|
render_u2f_error: function(error) {
|
|
var ErrorNames = {
|
|
'1': gettext('Other Error'),
|
|
'2': gettext('Bad Request'),
|
|
'3': gettext('Configuration Unsupported'),
|
|
'4': gettext('Device Ineligible'),
|
|
'5': gettext('Timeout')
|
|
};
|
|
return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
|
|
},
|
|
|
|
windowHostname: function() {
|
|
return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match,
|
|
function(m, addr, offset, original) { return addr; });
|
|
},
|
|
|
|
openDefaultConsoleWindow: function(consoles, vmtype, vmid, nodename, vmname, cmd) {
|
|
var dv = PVE.Utils.defaultViewer(consoles);
|
|
PVE.Utils.openConsoleWindow(dv, vmtype, vmid, nodename, vmname, cmd);
|
|
},
|
|
|
|
openConsoleWindow: function(viewer, vmtype, vmid, nodename, vmname, cmd) {
|
|
// kvm, lxc, shell, upgrade
|
|
|
|
if (vmid == undefined && (vmtype === 'kvm' || vmtype === 'lxc')) {
|
|
throw "missing vmid";
|
|
}
|
|
|
|
if (!nodename) {
|
|
throw "no nodename specified";
|
|
}
|
|
|
|
if (viewer === 'html5') {
|
|
PVE.Utils.openVNCViewer(vmtype, vmid, nodename, vmname, cmd);
|
|
} else if (viewer === 'xtermjs') {
|
|
Proxmox.Utils.openXtermJsViewer(vmtype, vmid, nodename, vmname, cmd);
|
|
} else if (viewer === 'vv') {
|
|
var url;
|
|
var params = { proxy: PVE.Utils.windowHostname() };
|
|
if (vmtype === 'kvm') {
|
|
url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy';
|
|
PVE.Utils.openSpiceViewer(url, params);
|
|
} else if (vmtype === 'lxc') {
|
|
url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy';
|
|
PVE.Utils.openSpiceViewer(url, params);
|
|
} else if (vmtype === 'shell') {
|
|
url = '/nodes/' + nodename + '/spiceshell';
|
|
PVE.Utils.openSpiceViewer(url, params);
|
|
} else if (vmtype === 'upgrade') {
|
|
url = '/nodes/' + nodename + '/spiceshell';
|
|
params.upgrade = 1;
|
|
PVE.Utils.openSpiceViewer(url, params);
|
|
} else if (vmtype === 'cmd') {
|
|
url = '/nodes/' + nodename + '/spiceshell';
|
|
params.cmd = cmd;
|
|
PVE.Utils.openSpiceViewer(url, params);
|
|
}
|
|
} else {
|
|
throw "unknown viewer type";
|
|
}
|
|
},
|
|
|
|
defaultViewer: function(consoles) {
|
|
|
|
var allowSpice, allowXtermjs;
|
|
|
|
if (consoles === true) {
|
|
allowSpice = true;
|
|
allowXtermjs = true;
|
|
} else if (typeof consoles === 'object') {
|
|
allowSpice = consoles.spice;
|
|
allowXtermjs = !!consoles.xtermjs;
|
|
}
|
|
var dv = PVE.VersionInfo.console || '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) {
|
|
var url = Ext.Object.toQueryString({
|
|
console: vmtype, // kvm, lxc, upgrade or shell
|
|
novnc: 1,
|
|
vmid: vmid,
|
|
vmname: vmname,
|
|
node: nodename,
|
|
resize: 'off',
|
|
cmd: cmd
|
|
});
|
|
var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427");
|
|
if (nw) {
|
|
nw.focus();
|
|
}
|
|
},
|
|
|
|
openSpiceViewer: function(url, params){
|
|
|
|
var downloadWithName = function(uri, name) {
|
|
var link = Ext.DomHelper.append(document.body, {
|
|
tag: 'a',
|
|
href: uri,
|
|
css : 'display:none;visibility:hidden;height:0px;'
|
|
});
|
|
|
|
// Note: we need to tell android the correct file name extension
|
|
// but we do not set 'download' tag for other environments, because
|
|
// It can have strange side effects (additional user prompt on firefox)
|
|
var andriod = navigator.userAgent.match(/Android/i) ? true : false;
|
|
if (andriod) {
|
|
link.download = name;
|
|
}
|
|
|
|
if (link.fireEvent) {
|
|
link.fireEvent('onclick');
|
|
} else {
|
|
var evt = document.createEvent("MouseEvents");
|
|
evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
|
|
link.dispatchEvent(evt);
|
|
}
|
|
};
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: url,
|
|
params: params,
|
|
method: 'POST',
|
|
failure: function(response, opts){
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, opts){
|
|
var raw = "[virt-viewer]\n";
|
|
Ext.Object.each(response.result.data, function(k, v) {
|
|
raw += k + "=" + v + "\n";
|
|
});
|
|
var url = 'data:application/x-virt-viewer;charset=UTF-8,' +
|
|
encodeURIComponent(raw);
|
|
|
|
downloadWithName(url, "pve-spice.vv");
|
|
}
|
|
});
|
|
},
|
|
|
|
openTreeConsole: function(tree, record, item, index, e) {
|
|
e.stopEvent();
|
|
var nodename = record.data.node;
|
|
var vmid = record.data.vmid;
|
|
var vmname = record.data.name;
|
|
if (record.data.type === 'qemu' && !record.data.template) {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
let conf = response.result.data;
|
|
var 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) {
|
|
|
|
var list = menu.query('menuitem');
|
|
|
|
Ext.Array.each(list, function(item) {
|
|
if (item.text === text) {
|
|
if (item.handler) {
|
|
item.handler();
|
|
return 1;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
createCmdMenu: function(v, record, item, index, event) {
|
|
event.stopEvent();
|
|
if (!(v instanceof Ext.tree.View)) {
|
|
v.select(record);
|
|
}
|
|
var menu;
|
|
var template = !!record.data.template;
|
|
var type = record.data.type;
|
|
|
|
if (template) {
|
|
if (type === 'qemu' || type == 'lxc') {
|
|
menu = Ext.create('PVE.menu.TemplateMenu', {
|
|
pveSelNode: record
|
|
});
|
|
}
|
|
} else if (type === 'qemu' ||
|
|
type === 'lxc' ||
|
|
type === 'node') {
|
|
menu = Ext.create('PVE.' + type + '.CmdMenu', {
|
|
pveSelNode: record,
|
|
nodename: record.data.node
|
|
});
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
menu.showAt(event.getXY());
|
|
return menu;
|
|
},
|
|
|
|
// helper for deleting field which are set to there default values
|
|
delete_if_default: function(values, fieldname, default_val, create) {
|
|
if (values[fieldname] === '' || values[fieldname] === default_val) {
|
|
if (!create) {
|
|
if (values['delete']) {
|
|
values['delete'] += ',' + fieldname;
|
|
} else {
|
|
values['delete'] = fieldname;
|
|
}
|
|
}
|
|
|
|
delete values[fieldname];
|
|
}
|
|
},
|
|
|
|
loadSSHKeyFromFile: function(file, callback) {
|
|
// ssh-keygen produces 740 bytes for an average 4096 bit rsa key, with
|
|
// a user@host comment, 1420 for 8192 bits; current max is 16kbit
|
|
// assume: 740*8 for max. 32kbit (5920 byte file)
|
|
// round upwards to nearest nice number => 8192 bytes, leaves lots of comment space
|
|
if (file.size > 8192) {
|
|
Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size);
|
|
return;
|
|
}
|
|
/*global
|
|
FileReader
|
|
*/
|
|
var reader = new FileReader();
|
|
reader.onload = function(evt) {
|
|
callback(evt.target.result);
|
|
};
|
|
reader.readAsText(file);
|
|
},
|
|
|
|
bus_counts: { ide: 4, sata: 6, scsi: 16, virtio: 16 },
|
|
|
|
// types is either undefined (all busses), an array of busses, or a single bus
|
|
forEachBus: function(types, func) {
|
|
var busses = Object.keys(PVE.Utils.bus_counts);
|
|
var i, j, count, cont;
|
|
|
|
if (Ext.isArray(types)) {
|
|
busses = types;
|
|
} else if (Ext.isDefined(types)) {
|
|
busses = [ types ];
|
|
}
|
|
|
|
// check if we only have valid busses
|
|
for (i = 0; i < busses.length; i++) {
|
|
if (!PVE.Utils.bus_counts[busses[i]]) {
|
|
throw "invalid bus: '" + busses[i] + "'";
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < busses.length; i++) {
|
|
count = PVE.Utils.bus_counts[busses[i]];
|
|
for (j = 0; j < count; j++) {
|
|
cont = func(busses[i], j);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
mp_counts: { mps: 256, unused: 256 },
|
|
|
|
forEachMP: function(func, includeUnused) {
|
|
var i, cont;
|
|
for (i = 0; i < PVE.Utils.mp_counts.mps; i++) {
|
|
cont = func('mp', i);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!includeUnused) {
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < PVE.Utils.mp_counts.unused; i++) {
|
|
cont = func('unused', i);
|
|
if (!cont && cont !== undefined) {
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
cleanEmptyObjectKeys: function (obj) {
|
|
var propName;
|
|
for (propName in obj) {
|
|
if (obj.hasOwnProperty(propName)) {
|
|
if (obj[propName] === null || obj[propName] === undefined) {
|
|
delete obj[propName];
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
handleStoreErrorOrMask: function(me, store, regex, callback) {
|
|
|
|
me.mon(store, 'load', function (proxy, response, success, operation) {
|
|
|
|
if (success) {
|
|
Proxmox.Utils.setErrorMask(me, false);
|
|
return;
|
|
}
|
|
var msg;
|
|
|
|
if (operation.error.statusText) {
|
|
if (operation.error.statusText.match(regex)) {
|
|
callback(me, operation.error);
|
|
return;
|
|
} else {
|
|
msg = operation.error.statusText + ' (' + operation.error.status + ')';
|
|
}
|
|
} else {
|
|
msg = gettext('Connection error');
|
|
}
|
|
Proxmox.Utils.setErrorMask(me, msg);
|
|
});
|
|
},
|
|
|
|
showCephInstallOrMask: function(container, msg, nodename, callback){
|
|
var regex = new RegExp("not (installed|initialized)", "i");
|
|
if (msg.match(regex)) {
|
|
if (Proxmox.UserName === 'root@pam') {
|
|
container.el.mask();
|
|
if (!container.down('pveCephInstallWindow')){
|
|
var isInstalled = msg.match(/not initialized/i) ? true : false;
|
|
var win = Ext.create('PVE.ceph.Install', {
|
|
nodename: nodename
|
|
});
|
|
win.getViewModel().set('isInstalled', isInstalled);
|
|
container.add(win);
|
|
win.show();
|
|
callback(win);
|
|
}
|
|
} else {
|
|
container.mask(Ext.String.format(gettext('{0} not installed.') +
|
|
' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']);
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
},
|
|
|
|
singleton: true,
|
|
constructor: function() {
|
|
var me = this;
|
|
Ext.apply(me, me.utilities);
|
|
}
|
|
|
|
});
|
|
|
|
// ExtJS related things
|
|
|
|
Proxmox.Utils.toolkit = 'extjs';
|
|
|
|
// custom PVE specific VTypes
|
|
Ext.apply(Ext.form.field.VTypes, {
|
|
|
|
QemuStartDate: function(v) {
|
|
return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v);
|
|
},
|
|
QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"',
|
|
IP64AddressList: function(v) {
|
|
var list = v.split(/[\ \,\;]+/);
|
|
var i;
|
|
for (i = 0; i < list.length; i++) {
|
|
if (list[i] == '') {
|
|
continue;
|
|
}
|
|
|
|
if (!Proxmox.Utils.IP64_match.test(list[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2',
|
|
IP64AddressListMask: /[A-Fa-f0-9\,\:\.\;\ ]/
|
|
});
|
|
|
|
Ext.define('PVE.form.field.Display', {
|
|
override: 'Ext.form.field.Display',
|
|
|
|
setSubmitValue: function(value) {
|
|
// do nothing, this is only to allow generalized bindings for the:
|
|
// `me.isCreate ? 'textfield' : 'displayfield'` cases we have.
|
|
}
|
|
});
|
|
// Some configuration values are complex strings -
|
|
// so we need parsers/generators for them.
|
|
|
|
Ext.define('PVE.Parser', { statics: {
|
|
|
|
// this class only contains static functions
|
|
|
|
parseACME: function(value) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
var res = {};
|
|
var errors = false;
|
|
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; //continue
|
|
}
|
|
|
|
var match_res;
|
|
if ((match_res = p.match(/^(?:domains=)?((?:[a-zA-Z0-9\-\.]+[;, ]?)+)$/)) !== null) {
|
|
res.domains = match_res[1].split(/[;, ]/);
|
|
} else {
|
|
errors = true;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (errors || !res) {
|
|
return;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
parseBoolean: function(value, default_value) {
|
|
if (!Ext.isDefined(value)) {
|
|
return default_value;
|
|
}
|
|
value = value.toLowerCase();
|
|
return value === '1' ||
|
|
value === 'on' ||
|
|
value === 'yes' ||
|
|
value === 'true';
|
|
},
|
|
|
|
parsePropertyString: function(value, defaultKey) {
|
|
var res = {},
|
|
error;
|
|
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
var kv = p.split('=', 2);
|
|
if (Ext.isDefined(kv[1])) {
|
|
res[kv[0]] = kv[1];
|
|
} else if (Ext.isDefined(defaultKey)) {
|
|
if (Ext.isDefined(res[defaultKey])) {
|
|
error = 'defaultKey may be only defined once in propertyString';
|
|
return false; // break
|
|
}
|
|
res[defaultKey] = kv[0];
|
|
} else {
|
|
error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
|
|
return false; // break
|
|
}
|
|
});
|
|
|
|
if (error !== undefined) {
|
|
console.error(error);
|
|
return;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printPropertyString: function(data, defaultKey) {
|
|
var stringparts = [],
|
|
gotDefaultKeyVal = false,
|
|
defaultKeyVal;
|
|
|
|
Ext.Object.each(data, function(key, value) {
|
|
if (defaultKey !== undefined && key === defaultKey) {
|
|
gotDefaultKeyVal = true;
|
|
defaultKeyVal = value;
|
|
} else {
|
|
stringparts.push(key + '=' + value);
|
|
}
|
|
});
|
|
|
|
stringparts = stringparts.sort();
|
|
if (gotDefaultKeyVal) {
|
|
stringparts.unshift(defaultKeyVal);
|
|
}
|
|
|
|
return stringparts.join(',');
|
|
},
|
|
|
|
parseQemuNetwork: function(key, value) {
|
|
if (!(key && value)) {
|
|
return;
|
|
}
|
|
|
|
var res = {};
|
|
|
|
var errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
|
|
var match_res;
|
|
|
|
if ((match_res = p.match(/^(ne2k_pci|e1000|e1000-82540em|e1000-82544gc|e1000-82545em|vmxnet3|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i)) !== null) {
|
|
res.model = match_res[1].toLowerCase();
|
|
if (match_res[3]) {
|
|
res.macaddr = match_res[3];
|
|
}
|
|
} else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) {
|
|
res.bridge = match_res[1];
|
|
} else if ((match_res = p.match(/^rate=(\d+(\.\d+)?)$/)) !== null) {
|
|
res.rate = match_res[1];
|
|
} else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) {
|
|
res.tag = match_res[1];
|
|
} else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
|
|
res.firewall = match_res[1];
|
|
} else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
|
|
res.disconnect = match_res[1];
|
|
} else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) {
|
|
res.queues = match_res[1];
|
|
} else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) {
|
|
res.trunks = match_res[1];
|
|
} else {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
});
|
|
|
|
if (errors || !res.model) {
|
|
return;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printQemuNetwork: function(net) {
|
|
|
|
var netstr = net.model;
|
|
if (net.macaddr) {
|
|
netstr += "=" + net.macaddr;
|
|
}
|
|
if (net.bridge) {
|
|
netstr += ",bridge=" + net.bridge;
|
|
if (net.tag) {
|
|
netstr += ",tag=" + net.tag;
|
|
}
|
|
if (net.firewall) {
|
|
netstr += ",firewall=" + net.firewall;
|
|
}
|
|
}
|
|
if (net.rate) {
|
|
netstr += ",rate=" + net.rate;
|
|
}
|
|
if (net.queues) {
|
|
netstr += ",queues=" + net.queues;
|
|
}
|
|
if (net.disconnect) {
|
|
netstr += ",link_down=" + net.disconnect;
|
|
}
|
|
if (net.trunks) {
|
|
netstr += ",trunks=" + net.trunks;
|
|
}
|
|
return netstr;
|
|
},
|
|
|
|
parseQemuDrive: function(key, value) {
|
|
if (!(key && value)) {
|
|
return;
|
|
}
|
|
|
|
var res = {};
|
|
|
|
var match_res = key.match(/^([a-z]+)(\d+)$/);
|
|
if (!match_res) {
|
|
return;
|
|
}
|
|
res['interface'] = match_res[1];
|
|
res.index = match_res[2];
|
|
|
|
var errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
var match_res = p.match(/^([a-z_]+)=(\S+)$/);
|
|
if (!match_res) {
|
|
if (!p.match(/\=/)) {
|
|
res.file = p;
|
|
return; // continue
|
|
}
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
var k = match_res[1];
|
|
if (k === 'volume') {
|
|
k = 'file';
|
|
}
|
|
|
|
if (Ext.isDefined(res[k])) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
|
|
var v = match_res[2];
|
|
|
|
if (k === 'cache' && v === 'off') {
|
|
v = 'none';
|
|
}
|
|
|
|
res[k] = v;
|
|
});
|
|
|
|
if (errors || !res.file) {
|
|
return;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printQemuDrive: function(drive) {
|
|
|
|
var drivestr = drive.file;
|
|
|
|
Ext.Object.each(drive, function(key, value) {
|
|
if (!Ext.isDefined(value) || key === 'file' ||
|
|
key === 'index' || key === 'interface') {
|
|
return; // continue
|
|
}
|
|
drivestr += ',' + key + '=' + value;
|
|
});
|
|
|
|
return drivestr;
|
|
},
|
|
|
|
parseIPConfig: function(key, value) {
|
|
if (!(key && value)) {
|
|
return;
|
|
}
|
|
|
|
var res = {};
|
|
|
|
var errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
|
|
var match_res;
|
|
if ((match_res = p.match(/^ip=(\S+)$/)) !== null) {
|
|
res.ip = match_res[1];
|
|
} else if ((match_res = p.match(/^gw=(\S+)$/)) !== null) {
|
|
res.gw = match_res[1];
|
|
} else if ((match_res = p.match(/^ip6=(\S+)$/)) !== null) {
|
|
res.ip6 = match_res[1];
|
|
} else if ((match_res = p.match(/^gw6=(\S+)$/)) !== null) {
|
|
res.gw6 = match_res[1];
|
|
} else {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
});
|
|
|
|
if (errors) {
|
|
return;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printIPConfig: function(cfg) {
|
|
var c = "";
|
|
var str = "";
|
|
if (cfg.ip) {
|
|
str += "ip=" + cfg.ip;
|
|
c = ",";
|
|
}
|
|
if (cfg.gw) {
|
|
str += c + "gw=" + cfg.gw;
|
|
c = ",";
|
|
}
|
|
if (cfg.ip6) {
|
|
str += c + "ip6=" + cfg.ip6;
|
|
c = ",";
|
|
}
|
|
if (cfg.gw6) {
|
|
str += c + "gw6=" + cfg.gw6;
|
|
c = ",";
|
|
}
|
|
return str;
|
|
},
|
|
|
|
parseOpenVZNetIf: function(value) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
var res = {};
|
|
|
|
var errors = false;
|
|
Ext.Array.each(value.split(';'), function(item) {
|
|
if (!item || item.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
|
|
var data = {};
|
|
Ext.Array.each(item.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
var match_res = p.match(/^(ifname|mac|bridge|host_ifname|host_mac|mac_filter)=(\S+)$/);
|
|
if (!match_res) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
if (match_res[1] === 'bridge'){
|
|
var bridgevlanf = match_res[2];
|
|
var bridge_res = bridgevlanf.match(/^(vmbr(\d+))(v(\d+))?(f)?$/);
|
|
if (!bridge_res) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
data.bridge = bridge_res[1];
|
|
data.tag = bridge_res[4];
|
|
/*jslint confusion: true*/
|
|
data.firewall = bridge_res[5] ? 1 : 0;
|
|
/*jslint confusion: false*/
|
|
} else {
|
|
data[match_res[1]] = match_res[2];
|
|
}
|
|
});
|
|
|
|
if (errors || !data.ifname) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
|
|
data.raw = item;
|
|
|
|
res[data.ifname] = data;
|
|
});
|
|
|
|
return errors ? undefined: res;
|
|
},
|
|
|
|
printOpenVZNetIf: function(netif) {
|
|
var netarray = [];
|
|
|
|
Ext.Object.each(netif, function(iface, data) {
|
|
var tmparray = [];
|
|
Ext.Array.each(['ifname', 'mac', 'bridge', 'host_ifname' , 'host_mac', 'mac_filter', 'tag', 'firewall'], function(key) {
|
|
var value = data[key];
|
|
if (key === 'bridge'){
|
|
if(data.tag){
|
|
value = value + 'v' + data.tag;
|
|
}
|
|
if (data.firewall){
|
|
value = value + 'f';
|
|
}
|
|
}
|
|
if (value) {
|
|
tmparray.push(key + '=' + value);
|
|
}
|
|
|
|
});
|
|
netarray.push(tmparray.join(','));
|
|
});
|
|
|
|
return netarray.join(';');
|
|
},
|
|
|
|
parseLxcNetwork: function(value) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
var data = {};
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
var match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/);
|
|
if (match_res) {
|
|
data[match_res[1]] = match_res[2];
|
|
} else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
|
|
data.firewall = PVE.Parser.parseBoolean(match_res[1]);
|
|
} else {
|
|
// todo: simply ignore errors ?
|
|
return; // continue
|
|
}
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
printLxcNetwork: function(data) {
|
|
var tmparray = [];
|
|
Ext.Array.each(['bridge', 'hwaddr', 'mtu', 'name', 'ip',
|
|
'gw', 'ip6', 'gw6', 'firewall', 'tag'], function(key) {
|
|
var value = data[key];
|
|
if (value) {
|
|
tmparray.push(key + '=' + value);
|
|
}
|
|
});
|
|
|
|
/*jslint confusion: true*/
|
|
if (data.rate > 0) {
|
|
tmparray.push('rate=' + data.rate);
|
|
}
|
|
/*jslint confusion: false*/
|
|
return tmparray.join(',');
|
|
},
|
|
|
|
parseLxcMountPoint: function(value) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
var res = {};
|
|
|
|
var errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
var match_res = p.match(/^([a-z_]+)=(.+)$/);
|
|
if (!match_res) {
|
|
if (!p.match(/\=/)) {
|
|
res.file = p;
|
|
return; // continue
|
|
}
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
var k = match_res[1];
|
|
if (k === 'volume') {
|
|
k = 'file';
|
|
}
|
|
|
|
if (Ext.isDefined(res[k])) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
|
|
var v = match_res[2];
|
|
|
|
res[k] = v;
|
|
});
|
|
|
|
if (errors || !res.file) {
|
|
return;
|
|
}
|
|
|
|
var m = res.file.match(/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):/i);
|
|
if (m) {
|
|
res.storage = m[1];
|
|
res.type = 'volume';
|
|
} else if (res.file.match(/^\/dev\//)) {
|
|
res.type = 'device';
|
|
} else {
|
|
res.type = 'bind';
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printLxcMountPoint: function(mp) {
|
|
var drivestr = mp.file;
|
|
|
|
Ext.Object.each(mp, function(key, value) {
|
|
if (!Ext.isDefined(value) || key === 'file' ||
|
|
key === 'type' || key === 'storage') {
|
|
return; // continue
|
|
}
|
|
drivestr += ',' + key + '=' + value;
|
|
});
|
|
|
|
return drivestr;
|
|
},
|
|
|
|
parseStartup: function(value) {
|
|
if (value === undefined) {
|
|
return;
|
|
}
|
|
|
|
var res = {};
|
|
|
|
var errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
|
|
var match_res;
|
|
|
|
if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) {
|
|
res.order = match_res[2];
|
|
} else if ((match_res = p.match(/^up=(\d+)$/)) !== null) {
|
|
res.up = match_res[1];
|
|
} else if ((match_res = p.match(/^down=(\d+)$/)) !== null) {
|
|
res.down = match_res[1];
|
|
} else {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
});
|
|
|
|
if (errors) {
|
|
return;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printStartup: function(startup) {
|
|
var arr = [];
|
|
if (startup.order !== undefined && startup.order !== '') {
|
|
arr.push('order=' + startup.order);
|
|
}
|
|
if (startup.up !== undefined && startup.up !== '') {
|
|
arr.push('up=' + startup.up);
|
|
}
|
|
if (startup.down !== undefined && startup.down !== '') {
|
|
arr.push('down=' + startup.down);
|
|
}
|
|
|
|
return arr.join(',');
|
|
},
|
|
|
|
parseQemuSmbios1: function(value) {
|
|
var res = value.split(',').reduce(function (accumulator, currentValue) {
|
|
var splitted = currentValue.split(new RegExp("=(.+)"));
|
|
accumulator[splitted[0]] = splitted[1];
|
|
return accumulator;
|
|
}, {});
|
|
|
|
if (PVE.Parser.parseBoolean(res.base64, false)) {
|
|
Ext.Object.each(res, function(key, value) {
|
|
if (key === 'uuid') { return; }
|
|
res[key] = Ext.util.Base64.decode(value);
|
|
});
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printQemuSmbios1: function(data) {
|
|
|
|
var datastr = '';
|
|
var base64 = false;
|
|
Ext.Object.each(data, function(key, value) {
|
|
if (value === '') { return; }
|
|
if (key === 'uuid') {
|
|
datastr += (datastr !== '' ? ',' : '') + key + '=' + value;
|
|
} else {
|
|
// values should be base64 encoded from now on, mark config strings correspondingly
|
|
if (!base64) {
|
|
base64 = true;
|
|
datastr += (datastr !== '' ? ',' : '') + 'base64=1';
|
|
}
|
|
datastr += (datastr !== '' ? ',' : '') + key + '=' + Ext.util.Base64.encode(value);
|
|
}
|
|
});
|
|
|
|
return datastr;
|
|
},
|
|
|
|
parseTfaConfig: function(value) {
|
|
var res = {};
|
|
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
var kva = p.split('=', 2);
|
|
res[kva[0]] = kva[1];
|
|
});
|
|
|
|
return res;
|
|
},
|
|
|
|
parseTfaType: function(value) {
|
|
/*jslint confusion: true*/
|
|
var match;
|
|
if (!value || !value.length) {
|
|
return undefined;
|
|
} else if (value === 'x!oath') {
|
|
return 'totp';
|
|
} else if (!!(match = value.match(/^x!(.+)$/))) {
|
|
return match[1];
|
|
} else {
|
|
return 1;
|
|
}
|
|
},
|
|
|
|
parseQemuCpu: function(value) {
|
|
if (!value) {
|
|
return {};
|
|
}
|
|
|
|
var res = {};
|
|
|
|
var errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
|
|
if (!p.match(/\=/)) {
|
|
if (Ext.isDefined(res.cpu)) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
res.cputype = p;
|
|
return; // continue
|
|
}
|
|
|
|
var match_res = p.match(/^([a-z_]+)=(\S+)$/);
|
|
if (!match_res) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
|
|
var k = match_res[1];
|
|
if (Ext.isDefined(res[k])) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
|
|
res[k] = match_res[2];
|
|
});
|
|
|
|
if (errors || !res.cputype) {
|
|
return;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printQemuCpu: function(cpu) {
|
|
var cpustr = cpu.cputype;
|
|
var optstr = '';
|
|
|
|
Ext.Object.each(cpu, function(key, value) {
|
|
if (!Ext.isDefined(value) || key === 'cputype') {
|
|
return; // continue
|
|
}
|
|
optstr += ',' + key + '=' + value;
|
|
});
|
|
|
|
if (!cpustr) {
|
|
if (optstr) {
|
|
return 'kvm64' + optstr;
|
|
}
|
|
return;
|
|
}
|
|
|
|
return cpustr + optstr;
|
|
},
|
|
|
|
parseSSHKey: function(key) {
|
|
// |--- options can have quotes--| type key comment
|
|
var keyre = /^(?:((?:[^\s"]|\"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
|
|
var typere = /^(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)$/;
|
|
|
|
var m = key.match(keyre);
|
|
if (!m) {
|
|
return null;
|
|
}
|
|
if (m.length < 3 || !m[2]) { // [2] is always either type or key
|
|
return null;
|
|
}
|
|
if (m[1] && m[1].match(typere)) {
|
|
return {
|
|
type: m[1],
|
|
key: m[2],
|
|
comment: m[3]
|
|
};
|
|
}
|
|
if (m[2].match(typere)) {
|
|
return {
|
|
options: m[1],
|
|
type: m[2],
|
|
key: m[3],
|
|
comment: m[4]
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}});
|
|
/* This state provider keeps part of the state inside
|
|
* the browser history.
|
|
*
|
|
* We compress (shorten) url using dictionary based compression
|
|
* i.e. use column separated list instead of url encoded hash:
|
|
* #v\d* version/format
|
|
* := indicates string values
|
|
* :\d+ lookup value in dictionary hash
|
|
* #v1:=value1:5:=value2:=value3:...
|
|
*/
|
|
|
|
Ext.define('PVE.StateProvider', {
|
|
extend: 'Ext.state.LocalStorageProvider',
|
|
|
|
// private
|
|
setHV: function(name, newvalue, fireEvents) {
|
|
var me = this;
|
|
|
|
var changes = false;
|
|
var oldtext = Ext.encode(me.UIState[name]);
|
|
var newtext = Ext.encode(newvalue);
|
|
if (newtext != oldtext) {
|
|
changes = true;
|
|
me.UIState[name] = newvalue;
|
|
//console.log("changed old " + name + " " + oldtext);
|
|
//console.log("changed new " + name + " " + newtext);
|
|
if (fireEvents) {
|
|
me.fireEvent("statechange", me, name, { value: newvalue });
|
|
}
|
|
}
|
|
return changes;
|
|
},
|
|
|
|
// private
|
|
hslist: [
|
|
// order is important for notifications
|
|
// [ name, default ]
|
|
['view', 'server'],
|
|
['rid', 'root'],
|
|
['ltab', 'tasks'],
|
|
['nodetab', ''],
|
|
['storagetab', ''],
|
|
['pooltab', ''],
|
|
['kvmtab', ''],
|
|
['lxctab', ''],
|
|
['dctab', '']
|
|
],
|
|
|
|
hprefix: 'v1',
|
|
|
|
compDict: {
|
|
cloudinit: 52,
|
|
replication: 51,
|
|
system: 50,
|
|
monitor: 49,
|
|
'ha-fencing': 48,
|
|
'ha-groups': 47,
|
|
'ha-resources': 46,
|
|
'ceph-log': 45,
|
|
'ceph-crushmap':44,
|
|
'ceph-pools': 43,
|
|
'ceph-osdtree': 42,
|
|
'ceph-disklist': 41,
|
|
'ceph-monlist': 40,
|
|
'ceph-config': 39,
|
|
ceph: 38,
|
|
'firewall-fwlog': 37,
|
|
'firewall-options': 36,
|
|
'firewall-ipset': 35,
|
|
'firewall-aliases': 34,
|
|
'firewall-sg': 33,
|
|
firewall: 32,
|
|
apt: 31,
|
|
members: 30,
|
|
snapshot: 29,
|
|
ha: 28,
|
|
support: 27,
|
|
pools: 26,
|
|
syslog: 25,
|
|
ubc: 24,
|
|
initlog: 23,
|
|
openvz: 22,
|
|
backup: 21,
|
|
resources: 20,
|
|
content: 19,
|
|
root: 18,
|
|
domains: 17,
|
|
roles: 16,
|
|
groups: 15,
|
|
users: 14,
|
|
time: 13,
|
|
dns: 12,
|
|
network: 11,
|
|
services: 10,
|
|
options: 9,
|
|
console: 8,
|
|
hardware: 7,
|
|
permissions: 6,
|
|
summary: 5,
|
|
tasks: 4,
|
|
clog: 3,
|
|
storage: 2,
|
|
folder: 1,
|
|
server: 0
|
|
},
|
|
|
|
decodeHToken: function(token) {
|
|
var me = this;
|
|
|
|
var state = {};
|
|
if (!token) {
|
|
Ext.Array.each(me.hslist, function(rec) {
|
|
state[rec[0]] = rec[1];
|
|
});
|
|
return state;
|
|
}
|
|
|
|
// return Ext.urlDecode(token);
|
|
|
|
var items = token.split(':');
|
|
var prefix = items.shift();
|
|
|
|
if (prefix != me.hprefix) {
|
|
return me.decodeHToken();
|
|
}
|
|
|
|
Ext.Array.each(me.hslist, function(rec) {
|
|
var value = items.shift();
|
|
if (value) {
|
|
if (value[0] === '=') {
|
|
value = decodeURIComponent(value.slice(1));
|
|
} else {
|
|
Ext.Object.each(me.compDict, function(key, cv) {
|
|
if (value == cv) {
|
|
value = key;
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
state[rec[0]] = value;
|
|
});
|
|
|
|
return state;
|
|
},
|
|
|
|
encodeHToken: function(state) {
|
|
var me = this;
|
|
|
|
// return Ext.urlEncode(state);
|
|
|
|
var ctoken = me.hprefix;
|
|
Ext.Array.each(me.hslist, function(rec) {
|
|
var value = state[rec[0]];
|
|
if (!Ext.isDefined(value)) {
|
|
value = rec[1];
|
|
}
|
|
value = encodeURIComponent(value);
|
|
if (!value) {
|
|
ctoken += ':';
|
|
} else {
|
|
var comp = me.compDict[value];
|
|
if (Ext.isDefined(comp)) {
|
|
ctoken += ":" + comp;
|
|
} else {
|
|
ctoken += ":=" + value;
|
|
}
|
|
}
|
|
});
|
|
|
|
return ctoken;
|
|
},
|
|
|
|
constructor: function(config){
|
|
var me = this;
|
|
|
|
me.callParent([config]);
|
|
|
|
me.UIState = me.decodeHToken(); // set default
|
|
|
|
var history_change_cb = function(token) {
|
|
//console.log("HC " + token);
|
|
if (!token) {
|
|
var res = window.confirm(gettext('Are you sure you want to navigate away from this page?'));
|
|
if (res){
|
|
// process text value and close...
|
|
Ext.History.back();
|
|
} else {
|
|
Ext.History.forward();
|
|
}
|
|
return;
|
|
}
|
|
|
|
var newstate = me.decodeHToken(token);
|
|
Ext.Array.each(me.hslist, function(rec) {
|
|
if (typeof newstate[rec[0]] == "undefined") {
|
|
return;
|
|
}
|
|
me.setHV(rec[0], newstate[rec[0]], true);
|
|
});
|
|
};
|
|
|
|
var start_token = Ext.History.getToken();
|
|
if (start_token) {
|
|
history_change_cb(start_token);
|
|
} else {
|
|
var htext = me.encodeHToken(me.UIState);
|
|
Ext.History.add(htext);
|
|
}
|
|
|
|
Ext.History.on('change', history_change_cb);
|
|
},
|
|
|
|
get: function(name, defaultValue){
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
var data;
|
|
|
|
if (typeof me.UIState[name] != "undefined") {
|
|
data = { value: me.UIState[name] };
|
|
} else {
|
|
data = me.callParent(arguments);
|
|
if (!data && name === 'GuiCap') {
|
|
data = { vms: {}, storage: {}, access: {}, nodes: {}, dc: {} };
|
|
}
|
|
}
|
|
|
|
//console.log("GET " + name + " " + Ext.encode(data));
|
|
return data;
|
|
},
|
|
|
|
clear: function(name){
|
|
var me = this;
|
|
|
|
if (typeof me.UIState[name] != "undefined") {
|
|
me.UIState[name] = null;
|
|
}
|
|
|
|
me.callParent(arguments);
|
|
},
|
|
|
|
set: function(name, value, fireevent){
|
|
var me = this;
|
|
|
|
//console.log("SET " + name + " " + Ext.encode(value));
|
|
if (typeof me.UIState[name] != "undefined") {
|
|
var newvalue = value ? value.value : null;
|
|
if (me.setHV(name, newvalue, fireevent)) {
|
|
var htext = me.encodeHToken(me.UIState);
|
|
Ext.History.add(htext);
|
|
}
|
|
} else {
|
|
me.callParent(arguments);
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.menu.Item', {
|
|
extend: 'Ext.menu.Item',
|
|
alias: 'widget.pveMenuItem',
|
|
|
|
// set to wrap the handler callback in a confirm dialog showing this text
|
|
confirmMsg: false,
|
|
|
|
// set to focus 'No' instead of 'Yes' button and show a warning symbol
|
|
dangerous: false,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (me.handler) {
|
|
me.setHandler(me.handler, me.scope);
|
|
}
|
|
|
|
me.callParent();
|
|
},
|
|
|
|
setHandler: function(fn, scope) {
|
|
var me = this;
|
|
me.scope = scope;
|
|
me.handler = function(button, e) {
|
|
var rec, msg;
|
|
if (me.confirmMsg) {
|
|
msg = me.confirmMsg;
|
|
Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
|
|
Ext.Msg.show({
|
|
title: gettext('Confirm'),
|
|
icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
|
|
msg: msg,
|
|
buttons: Ext.Msg.YESNO,
|
|
defaultFocus: me.dangerous ? 'no' : 'yes',
|
|
callback: function(btn) {
|
|
if (btn === 'yes') {
|
|
Ext.callback(fn, me.scope, [me, e], 0, me);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
Ext.callback(fn, me.scope, [me, e], 0, me);
|
|
}
|
|
};
|
|
}
|
|
});
|
|
Ext.define('PVE.menu.TemplateMenu', {
|
|
extend: 'Ext.menu.Menu',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var guestType = me.pveSelNode.data.type;
|
|
if (guestType !== 'qemu' && guestType != 'lxc') {
|
|
throw "invalid guest type";
|
|
}
|
|
|
|
var vmname = me.pveSelNode.data.name;
|
|
|
|
var template = me.pveSelNode.data.template;
|
|
|
|
var vm_command = function(cmd, params) {
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + nodename + '/' + guestType + '/' + vmid + "/status/" + cmd,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + vmid;
|
|
|
|
me.items = [
|
|
{
|
|
text: gettext('Migrate'),
|
|
iconCls: 'fa fa-fw fa-send-o',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.Migrate', {
|
|
vmtype: guestType,
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Clone'),
|
|
iconCls: 'fa fa-fw fa-clone',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.Clone', {
|
|
nodename: nodename,
|
|
guestType: guestType,
|
|
vmid: vmid,
|
|
isTemplate: template
|
|
});
|
|
win.show();
|
|
}
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.button.ConsoleButton', {
|
|
extend: 'Ext.button.Split',
|
|
alias: 'widget.pveConsoleButton',
|
|
|
|
consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd'
|
|
|
|
cmd: undefined,
|
|
|
|
consoleName: undefined,
|
|
|
|
iconCls: 'fa fa-terminal',
|
|
|
|
enableSpice: true,
|
|
enableXtermjs: true,
|
|
|
|
nodename: undefined,
|
|
|
|
vmid: 0,
|
|
|
|
text: gettext('Console'),
|
|
|
|
setEnableSpice: function(enable){
|
|
var me = this;
|
|
|
|
me.enableSpice = enable;
|
|
me.down('#spicemenu').setDisabled(!enable);
|
|
},
|
|
|
|
setEnableXtermJS: function(enable){
|
|
var me = this;
|
|
|
|
me.enableXtermjs = enable;
|
|
me.down('#xtermjs').setDisabled(!enable);
|
|
},
|
|
|
|
handler: function() {
|
|
var me = this;
|
|
var consoles = {
|
|
spice: me.enableSpice,
|
|
xtermjs: me.enableXtermjs
|
|
};
|
|
PVE.Utils.openDefaultConsoleWindow(consoles, me.consoleType, me.vmid,
|
|
me.nodename, me.consoleName, me.cmd);
|
|
},
|
|
|
|
menu: [
|
|
{
|
|
xtype:'menuitem',
|
|
text: 'noVNC',
|
|
iconCls: 'pve-itype-icon-novnc',
|
|
type: 'html5',
|
|
handler: function(button) {
|
|
var me = this.up('button');
|
|
PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
|
|
}
|
|
},
|
|
{
|
|
xterm: 'menuitem',
|
|
itemId: 'spicemenu',
|
|
text: 'SPICE',
|
|
type: 'vv',
|
|
iconCls: 'pve-itype-icon-virt-viewer',
|
|
handler: function(button) {
|
|
var me = this.up('button');
|
|
PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
|
|
}
|
|
},
|
|
{
|
|
text: 'xterm.js',
|
|
itemId: 'xtermjs',
|
|
iconCls: 'pve-itype-icon-xtermjs',
|
|
type: 'xtermjs',
|
|
handler: function(button) {
|
|
var me = this.up('button');
|
|
PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd);
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/* Button features:
|
|
* - observe selection changes to enable/disable the button using enableFn()
|
|
* - pop up confirmation dialog using confirmMsg()
|
|
*
|
|
* does this for the button and every menu item
|
|
*/
|
|
Ext.define('PVE.button.Split', {
|
|
extend: 'Ext.button.Split',
|
|
alias: 'widget.pveSplitButton',
|
|
|
|
// the selection model to observe
|
|
selModel: undefined,
|
|
|
|
// if 'false' handler will not be called (button disabled)
|
|
enableFn: function(record) { },
|
|
|
|
// function(record) or text
|
|
confirmMsg: false,
|
|
|
|
// take special care in confirm box (select no as default).
|
|
dangerous: false,
|
|
|
|
handlerWrapper: function(button, event) {
|
|
var me = this;
|
|
var rec, msg;
|
|
if (me.selModel) {
|
|
rec = me.selModel.getSelection()[0];
|
|
if (!rec || (me.enableFn(rec) === false)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (me.confirmMsg) {
|
|
msg = me.confirmMsg;
|
|
// confirMsg can be boolean or function
|
|
/*jslint confusion: true*/
|
|
if (Ext.isFunction(me.confirmMsg)) {
|
|
msg = me.confirmMsg(rec);
|
|
}
|
|
/*jslint confusion: false*/
|
|
Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
|
|
Ext.Msg.show({
|
|
title: gettext('Confirm'),
|
|
icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
|
|
msg: msg,
|
|
buttons: Ext.Msg.YESNO,
|
|
callback: function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
me.realHandler(button, event, rec);
|
|
}
|
|
});
|
|
} else {
|
|
me.realHandler(button, event, rec);
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
|
|
var me = this;
|
|
|
|
if (me.handler) {
|
|
me.realHandler = me.handler;
|
|
me.handler = me.handlerWrapper;
|
|
}
|
|
|
|
if (me.menu && me.menu.items) {
|
|
me.menu.items.forEach(function(item) {
|
|
if (item.handler) {
|
|
item.realHandler = item.handler;
|
|
item.handler = me.handlerWrapper;
|
|
}
|
|
|
|
if (item.selModel) {
|
|
me.mon(item.selModel, "selectionchange", function() {
|
|
var rec = item.selModel.getSelection()[0];
|
|
if (!rec || (item.enableFn(rec) === false )) {
|
|
item.setDisabled(true);
|
|
} else {
|
|
item.setDisabled(false);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
if (me.selModel) {
|
|
|
|
me.mon(me.selModel, "selectionchange", function() {
|
|
var rec = me.selModel.getSelection()[0];
|
|
if (!rec || (me.enableFn(rec) === false)) {
|
|
me.setDisabled(true);
|
|
} else {
|
|
me.setDisabled(false);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.controller.StorageEdit', {
|
|
extend: 'Ext.app.ViewController',
|
|
alias: 'controller.storageEdit',
|
|
control: {
|
|
'field[name=content]': {
|
|
change: function(field, value) {
|
|
var hasBackups = Ext.Array.contains(value, 'backup');
|
|
var maxfiles = this.lookupReference('maxfiles');
|
|
if (!maxfiles) {
|
|
return;
|
|
}
|
|
|
|
if (!hasBackups) {
|
|
// clear values which will never be submitted
|
|
maxfiles.reset();
|
|
}
|
|
maxfiles.setDisabled(!hasBackups);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.CmdMenu', {
|
|
extend: 'Ext.menu.Menu',
|
|
|
|
showSeparator: false,
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var vmname = me.pveSelNode.data.name;
|
|
|
|
var vm_command = function(cmd, params) {
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + nodename + '/qemu/' + vmid + "/status/" + cmd,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var running = false;
|
|
var stopped = true;
|
|
var suspended = false;
|
|
var standalone = PVE.data.ResourceStore.getNodes().length < 2;
|
|
|
|
switch (me.pveSelNode.data.status) {
|
|
case 'running':
|
|
running = true;
|
|
stopped = false;
|
|
break;
|
|
case 'suspended':
|
|
stopped = false;
|
|
suspended = true;
|
|
break;
|
|
case 'paused':
|
|
stopped = false;
|
|
suspended = true;
|
|
break;
|
|
default: break;
|
|
}
|
|
|
|
me.title = "VM " + vmid;
|
|
|
|
me.items = [
|
|
{
|
|
text: gettext('Start'),
|
|
iconCls: 'fa fa-fw fa-play',
|
|
hidden: running || suspended,
|
|
disabled: running || suspended,
|
|
handler: function() {
|
|
vm_command('start');
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Pause'),
|
|
iconCls: 'fa fa-fw fa-pause',
|
|
hidden: stopped || suspended,
|
|
disabled: stopped || suspended,
|
|
handler: function() {
|
|
var msg = Proxmox.Utils.format_task_description('qmpause', vmid);
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
vm_command('suspend');
|
|
});
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Hibernate'),
|
|
iconCls: 'fa fa-fw fa-download',
|
|
hidden: stopped || suspended,
|
|
disabled: stopped || suspended,
|
|
tooltip: gettext('Suspend to disk'),
|
|
handler: function() {
|
|
var msg = Proxmox.Utils.format_task_description('qmsuspend', vmid);
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
vm_command('suspend', { todisk: 1 });
|
|
});
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Resume'),
|
|
iconCls: 'fa fa-fw fa-play',
|
|
hidden: !suspended,
|
|
handler: function() {
|
|
vm_command('resume');
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Shutdown'),
|
|
iconCls: 'fa fa-fw fa-power-off',
|
|
disabled: stopped || suspended,
|
|
handler: function() {
|
|
var msg = Proxmox.Utils.format_task_description('qmshutdown', vmid);
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
|
|
vm_command('shutdown');
|
|
});
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Stop'),
|
|
iconCls: 'fa fa-fw fa-stop',
|
|
disabled: stopped,
|
|
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
|
|
handler: function() {
|
|
var msg = Proxmox.Utils.format_task_description('qmstop', vmid);
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
|
|
vm_command("stop");
|
|
});
|
|
}
|
|
},
|
|
{
|
|
xtype: 'menuseparator',
|
|
hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone']
|
|
},
|
|
{
|
|
text: gettext('Migrate'),
|
|
iconCls: 'fa fa-fw fa-send-o',
|
|
hidden: standalone || !caps.vms['VM.Migrate'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.Migrate', {
|
|
vmtype: 'qemu',
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Clone'),
|
|
iconCls: 'fa fa-fw fa-clone',
|
|
hidden: !caps.vms['VM.Clone'],
|
|
handler: function() {
|
|
PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'qemu');
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Convert to template'),
|
|
iconCls: 'fa fa-fw fa-file-o',
|
|
hidden: !caps.vms['VM.Allocate'],
|
|
handler: function() {
|
|
var msg = Proxmox.Utils.format_task_description('qmtemplate', vmid);
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + nodename + '/qemu/' + vmid + '/template',
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
},
|
|
{ xtype: 'menuseparator' },
|
|
{
|
|
text: gettext('Console'),
|
|
iconCls: 'fa fa-fw fa-terminal',
|
|
handler: function() {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
var allowSpice = response.result.data.spice;
|
|
var allowXtermjs = response.result.data.serial;
|
|
var consoles = {
|
|
spice: allowSpice,
|
|
xtermjs: allowXtermjs
|
|
};
|
|
PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.lxc.CmdMenu', {
|
|
extend: 'Ext.menu.Menu',
|
|
|
|
showSeparator: false,
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no CT ID specified";
|
|
}
|
|
var vmname = me.pveSelNode.data.name;
|
|
|
|
var vm_command = function(cmd, params) {
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + nodename + '/lxc/' + vmid + "/status/" + cmd,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var running = false;
|
|
var stopped = true;
|
|
var suspended = false;
|
|
var standalone = PVE.data.ResourceStore.getNodes().length < 2;
|
|
|
|
switch (me.pveSelNode.data.status) {
|
|
case 'running':
|
|
running = true;
|
|
stopped = false;
|
|
break;
|
|
case 'paused':
|
|
stopped = false;
|
|
suspended = true;
|
|
break;
|
|
default: break;
|
|
}
|
|
|
|
me.title = 'CT ' + vmid;
|
|
|
|
me.items = [
|
|
{
|
|
text: gettext('Start'),
|
|
iconCls: 'fa fa-fw fa-play',
|
|
disabled: running,
|
|
handler: function() {
|
|
vm_command('start');
|
|
}
|
|
},
|
|
// {
|
|
// text: gettext('Suspend'),
|
|
// iconCls: 'fa fa-fw fa-pause',
|
|
// hidde: suspended,
|
|
// disabled: stopped || suspended,
|
|
// handler: function() {
|
|
// var msg = Proxmox.Utils.format_task_description('vzsuspend', vmid);
|
|
// Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
// if (btn !== 'yes') {
|
|
// return;
|
|
// }
|
|
//
|
|
// vm_command('suspend');
|
|
// });
|
|
// }
|
|
// },
|
|
// {
|
|
// text: gettext('Resume'),
|
|
// iconCls: 'fa fa-fw fa-play',
|
|
// hidden: !suspended,
|
|
// handler: function() {
|
|
// vm_command('resume');
|
|
// }
|
|
// },
|
|
{
|
|
text: gettext('Shutdown'),
|
|
iconCls: 'fa fa-fw fa-power-off',
|
|
disabled: stopped || suspended,
|
|
handler: function() {
|
|
var msg = Proxmox.Utils.format_task_description('vzshutdown', vmid);
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
|
|
vm_command('shutdown');
|
|
});
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Stop'),
|
|
iconCls: 'fa fa-fw fa-stop',
|
|
disabled: stopped,
|
|
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
|
|
handler: function() {
|
|
var msg = Proxmox.Utils.format_task_description('vzstop', vmid);
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
|
|
vm_command("stop");
|
|
});
|
|
}
|
|
},
|
|
{
|
|
xtype: 'menuseparator',
|
|
hidden: standalone || !caps.vms['VM.Migrate']
|
|
},
|
|
{
|
|
text: gettext('Clone'),
|
|
iconCls: 'fa fa-fw fa-clone',
|
|
hidden: !caps.vms['VM.Clone'],
|
|
handler: function() {
|
|
PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'lxc');
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Migrate'),
|
|
iconCls: 'fa fa-fw fa-send-o',
|
|
hidden: standalone || !caps.vms['VM.Migrate'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.Migrate', {
|
|
vmtype: 'lxc',
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Convert to template'),
|
|
iconCls: 'fa fa-fw fa-file-o',
|
|
handler: function() {
|
|
var msg = Proxmox.Utils.format_task_description('vztemplate', vmid);
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + nodename + '/lxc/' + vmid + '/template',
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
},
|
|
{ xtype: 'menuseparator' },
|
|
{
|
|
text: gettext('Console'),
|
|
iconCls: 'fa fa-fw fa-terminal',
|
|
handler: function() {
|
|
PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname);
|
|
}
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.node.CmdMenu', {
|
|
extend: 'Ext.menu.Menu',
|
|
xtype: 'nodeCmdMenu',
|
|
|
|
showSeparator: false,
|
|
|
|
items: [
|
|
{
|
|
text: gettext('Create VM'),
|
|
itemId: 'createvm',
|
|
iconCls: 'fa fa-desktop',
|
|
handler: function() {
|
|
var me = this.up('menu');
|
|
var wiz = Ext.create('PVE.qemu.CreateWizard', {
|
|
nodename: me.nodename
|
|
});
|
|
wiz.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Create CT'),
|
|
itemId: 'createct',
|
|
iconCls: 'fa fa-cube',
|
|
handler: function() {
|
|
var me = this.up('menu');
|
|
var wiz = Ext.create('PVE.lxc.CreateWizard', {
|
|
nodename: me.nodename
|
|
});
|
|
wiz.show();
|
|
}
|
|
},
|
|
{ xtype: 'menuseparator' },
|
|
{
|
|
text: gettext('Bulk Start'),
|
|
itemId: 'bulkstart',
|
|
iconCls: 'fa fa-fw fa-play',
|
|
handler: function() {
|
|
var me = this.up('menu');
|
|
var win = Ext.create('PVE.window.BulkAction', {
|
|
nodename: me.nodename,
|
|
title: gettext('Bulk Start'),
|
|
btnText: gettext('Start'),
|
|
action: 'startall'
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Bulk Stop'),
|
|
itemId: 'bulkstop',
|
|
iconCls: 'fa fa-fw fa-stop',
|
|
handler: function() {
|
|
var me = this.up('menu');
|
|
var win = Ext.create('PVE.window.BulkAction', {
|
|
nodename: me.nodename,
|
|
title: gettext('Bulk Stop'),
|
|
btnText: gettext('Stop'),
|
|
action: 'stopall'
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Bulk Migrate'),
|
|
itemId: 'bulkmigrate',
|
|
iconCls: 'fa fa-fw fa-send-o',
|
|
handler: function() {
|
|
var me = this.up('menu');
|
|
var win = Ext.create('PVE.window.BulkAction', {
|
|
nodename: me.nodename,
|
|
title: gettext('Bulk Migrate'),
|
|
btnText: gettext('Migrate'),
|
|
action: 'migrateall'
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{ xtype: 'menuseparator' },
|
|
{
|
|
text: gettext('Shell'),
|
|
itemId: 'shell',
|
|
iconCls: 'fa fa-fw fa-terminal',
|
|
handler: function() {
|
|
var me = this.up('menu');
|
|
PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, me.nodename, undefined);
|
|
}
|
|
},
|
|
{ xtype: 'menuseparator' },
|
|
{
|
|
text: gettext('Wake-on-LAN'),
|
|
itemId: 'wakeonlan',
|
|
iconCls: 'fa fa-fw fa-power-off',
|
|
handler: function() {
|
|
var me = this.up('menu');
|
|
Proxmox.Utils.API2Request({
|
|
param: {},
|
|
url: '/nodes/' + me.nodename + '/wakeonlan',
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
Ext.Msg.show({
|
|
title: 'Success',
|
|
icon: Ext.Msg.INFO,
|
|
msg: Ext.String.format(gettext("Wake on LAN packet send for '{0}': '{1}'"), me.nodename, response.result.data)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw 'no nodename specified';
|
|
}
|
|
|
|
me.title = gettext('Node') + " '" + me.nodename + "'";
|
|
me.callParent();
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
// disable not allowed options
|
|
if (!caps.vms['VM.Allocate']) {
|
|
me.getComponent('createct').setDisabled(true);
|
|
me.getComponent('createvm').setDisabled(true);
|
|
}
|
|
|
|
if (!caps.nodes['Sys.PowerMgmt']) {
|
|
me.getComponent('bulkstart').setDisabled(true);
|
|
me.getComponent('bulkstop').setDisabled(true);
|
|
me.getComponent('bulkmigrate').setDisabled(true);
|
|
me.getComponent('wakeonlan').setDisabled(true);
|
|
}
|
|
|
|
if (!caps.nodes['Sys.Console']) {
|
|
me.getComponent('shell').setDisabled(true);
|
|
}
|
|
|
|
if (me.pveSelNode.data.running) {
|
|
me.getComponent('wakeonlan').setDisabled(true);
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.noVncConsole', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveNoVncConsole',
|
|
|
|
nodename: undefined,
|
|
|
|
vmid: undefined,
|
|
|
|
cmd: undefined,
|
|
|
|
consoleType: undefined, // lxc, kvm, shell, cmd
|
|
|
|
layout: 'fit',
|
|
|
|
xtermjs: false,
|
|
|
|
border: false,
|
|
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.consoleType) {
|
|
throw "no console type specified";
|
|
}
|
|
|
|
if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
// always use same iframe, to avoid running several noVnc clients
|
|
// at same time (to avoid performance problems)
|
|
var box = Ext.create('Ext.ux.IFrame', { itemid : "vncconsole" });
|
|
|
|
var type = me.xtermjs ? 'xtermjs' : 'novnc';
|
|
Ext.apply(me, {
|
|
items: box,
|
|
listeners: {
|
|
activate: function() {
|
|
var queryDict = {
|
|
console: me.consoleType, // kvm, lxc, upgrade or shell
|
|
vmid: me.vmid,
|
|
node: me.nodename,
|
|
cmd: me.cmd,
|
|
resize: 'scale'
|
|
};
|
|
queryDict[type] = 1;
|
|
PVE.Utils.cleanEmptyObjectKeys(queryDict);
|
|
var url = '/?' + Ext.Object.toQueryString(queryDict);
|
|
box.load(url);
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.on('afterrender', function() {
|
|
me.focus();
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.data.PermPathStore', {
|
|
extend: 'Ext.data.Store',
|
|
alias: 'store.pvePermPath',
|
|
fields: [ 'value' ],
|
|
autoLoad: false,
|
|
data: [
|
|
{'value': '/'},
|
|
{'value': '/access'},
|
|
{'value': '/nodes'},
|
|
{'value': '/pool'},
|
|
{'value': '/storage'},
|
|
{'value': '/vms'}
|
|
],
|
|
|
|
constructor: function(config) {
|
|
var me = this;
|
|
|
|
config = config || {};
|
|
|
|
me.callParent([config]);
|
|
|
|
me.suspendEvents();
|
|
PVE.data.ResourceStore.each(function(record) {
|
|
switch (record.get('type')) {
|
|
case 'node':
|
|
me.add({value: '/nodes/' + record.get('text')});
|
|
break;
|
|
|
|
case 'qemu':
|
|
me.add({value: '/vms/' + record.get('vmid')});
|
|
break;
|
|
|
|
case 'lxc':
|
|
me.add({value: '/vms/' + record.get('vmid')});
|
|
break;
|
|
|
|
case 'storage':
|
|
me.add({value: '/storage/' + record.get('storage')});
|
|
break;
|
|
case 'pool':
|
|
me.add({value: '/pool/' + record.get('pool')});
|
|
break;
|
|
}
|
|
});
|
|
me.resumeEvents();
|
|
|
|
me.fireEvent('refresh', me);
|
|
me.fireEvent('datachanged', me);
|
|
|
|
me.sort({
|
|
property: 'value',
|
|
direction: 'ASC'
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.data.ResourceStore', {
|
|
extend: 'Proxmox.data.UpdateStore',
|
|
singleton: true,
|
|
|
|
findVMID: function(vmid) {
|
|
var me = this, i;
|
|
|
|
return (me.findExact('vmid', parseInt(vmid, 10)) >= 0);
|
|
},
|
|
|
|
// returns the cached data from all nodes
|
|
getNodes: function() {
|
|
var me = this;
|
|
|
|
var nodes = [];
|
|
me.each(function(record) {
|
|
if (record.get('type') == "node") {
|
|
nodes.push( record.getData() );
|
|
}
|
|
});
|
|
|
|
return nodes;
|
|
},
|
|
|
|
storageIsShared: function(storage_path) {
|
|
var me = this;
|
|
|
|
var index = me.findExact('id', storage_path);
|
|
|
|
return me.getAt(index).data.shared;
|
|
},
|
|
|
|
guestNode: function(vmid) {
|
|
var me = this;
|
|
|
|
var index = me.findExact('vmid', parseInt(vmid, 10));
|
|
|
|
return me.getAt(index).data.node;
|
|
},
|
|
|
|
constructor: function(config) {
|
|
// fixme: how to avoid those warnings
|
|
/*jslint confusion: true */
|
|
|
|
var me = this;
|
|
|
|
config = config || {};
|
|
|
|
var field_defaults = {
|
|
type: {
|
|
header: gettext('Type'),
|
|
type: 'string',
|
|
renderer: PVE.Utils.render_resource_type,
|
|
sortable: true,
|
|
hideable: false,
|
|
width: 100
|
|
},
|
|
id: {
|
|
header: 'ID',
|
|
type: 'string',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 80
|
|
},
|
|
running: {
|
|
header: gettext('Online'),
|
|
type: 'boolean',
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
hidden: true,
|
|
convert: function(value, record) {
|
|
var info = record.data;
|
|
return (Ext.isNumeric(info.uptime) && (info.uptime > 0));
|
|
}
|
|
},
|
|
text: {
|
|
header: gettext('Description'),
|
|
type: 'string',
|
|
sortable: true,
|
|
width: 200,
|
|
convert: function(value, record) {
|
|
var info = record.data;
|
|
var text;
|
|
|
|
if (value) {
|
|
return value;
|
|
}
|
|
|
|
if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
|
|
text = String(info.vmid);
|
|
if (info.name) {
|
|
text += " (" + info.name + ')';
|
|
}
|
|
} else { // node, pool, storage
|
|
text = info[info.type] || info.id;
|
|
if (info.node && info.type !== 'node') {
|
|
text += " (" + info.node + ")";
|
|
}
|
|
}
|
|
|
|
return text;
|
|
}
|
|
},
|
|
vmid: {
|
|
header: 'VMID',
|
|
type: 'integer',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 80
|
|
},
|
|
name: {
|
|
header: gettext('Name'),
|
|
hidden: true,
|
|
sortable: true,
|
|
type: 'string'
|
|
},
|
|
disk: {
|
|
header: gettext('Disk usage'),
|
|
type: 'integer',
|
|
renderer: PVE.Utils.render_disk_usage,
|
|
sortable: true,
|
|
width: 100,
|
|
hidden: true
|
|
},
|
|
diskuse: {
|
|
header: gettext('Disk usage') + " %",
|
|
type: 'number',
|
|
sortable: true,
|
|
renderer: PVE.Utils.render_disk_usage_percent,
|
|
width: 100,
|
|
calculate: PVE.Utils.calculate_disk_usage,
|
|
sortType: 'asFloat'
|
|
},
|
|
maxdisk: {
|
|
header: gettext('Disk size'),
|
|
type: 'integer',
|
|
renderer: PVE.Utils.render_size,
|
|
sortable: true,
|
|
hidden: true,
|
|
width: 100
|
|
},
|
|
mem: {
|
|
header: gettext('Memory usage'),
|
|
type: 'integer',
|
|
renderer: PVE.Utils.render_mem_usage,
|
|
sortable: true,
|
|
hidden: true,
|
|
width: 100
|
|
},
|
|
memuse: {
|
|
header: gettext('Memory usage') + " %",
|
|
type: 'number',
|
|
renderer: PVE.Utils.render_mem_usage_percent,
|
|
calculate: PVE.Utils.calculate_mem_usage,
|
|
sortType: 'asFloat',
|
|
sortable: true,
|
|
width: 100
|
|
},
|
|
maxmem: {
|
|
header: gettext('Memory size'),
|
|
type: 'integer',
|
|
renderer: PVE.Utils.render_size,
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 100
|
|
},
|
|
cpu: {
|
|
header: gettext('CPU usage'),
|
|
type: 'float',
|
|
renderer: PVE.Utils.render_cpu,
|
|
sortable: true,
|
|
width: 100
|
|
},
|
|
maxcpu: {
|
|
header: gettext('maxcpu'),
|
|
type: 'integer',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 60
|
|
},
|
|
diskread: {
|
|
header: gettext('Total Disk Read'),
|
|
type: 'integer',
|
|
hidden: true,
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
width: 100
|
|
},
|
|
diskwrite: {
|
|
header: gettext('Total Disk Write'),
|
|
type: 'integer',
|
|
hidden: true,
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
width: 100
|
|
},
|
|
netin: {
|
|
header: gettext('Total NetIn'),
|
|
type: 'integer',
|
|
hidden: true,
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
width: 100
|
|
},
|
|
netout: {
|
|
header: gettext('Total NetOut'),
|
|
type: 'integer',
|
|
hidden: true,
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
width: 100
|
|
},
|
|
template: {
|
|
header: gettext('Template'),
|
|
type: 'integer',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 60
|
|
},
|
|
uptime: {
|
|
header: gettext('Uptime'),
|
|
type: 'integer',
|
|
renderer: Proxmox.Utils.render_uptime,
|
|
sortable: true,
|
|
width: 110
|
|
},
|
|
node: {
|
|
header: gettext('Node'),
|
|
type: 'string',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 110
|
|
},
|
|
storage: {
|
|
header: gettext('Storage'),
|
|
type: 'string',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 110
|
|
},
|
|
pool: {
|
|
header: gettext('Pool'),
|
|
type: 'string',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 110
|
|
},
|
|
hastate: {
|
|
header: gettext('HA State'),
|
|
type: 'string',
|
|
defaultValue: 'unmanaged',
|
|
hidden: true,
|
|
sortable: true
|
|
},
|
|
status: {
|
|
header: gettext('Status'),
|
|
type: 'string',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 110
|
|
},
|
|
lock: {
|
|
header: gettext('Lock'),
|
|
type: 'string',
|
|
hidden: true,
|
|
sortable: true,
|
|
width: 110
|
|
}
|
|
};
|
|
|
|
var fields = [];
|
|
var fieldNames = [];
|
|
Ext.Object.each(field_defaults, function(key, value) {
|
|
var field = {name: key, type: value.type};
|
|
if (Ext.isDefined(value.convert)) {
|
|
field.convert = value.convert;
|
|
}
|
|
|
|
if (Ext.isDefined(value.calculate)) {
|
|
field.calculate = value.calculate;
|
|
}
|
|
|
|
if (Ext.isDefined(value.defaultValue)) {
|
|
field.defaultValue = value.defaultValue;
|
|
}
|
|
|
|
fields.push(field);
|
|
fieldNames.push(key);
|
|
});
|
|
|
|
Ext.define('PVEResources', {
|
|
extend: "Ext.data.Model",
|
|
fields: fields,
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/cluster/resources'
|
|
}
|
|
});
|
|
|
|
Ext.define('PVETree', {
|
|
extend: "Ext.data.Model",
|
|
fields: fields,
|
|
proxy: { type: 'memory' }
|
|
});
|
|
|
|
Ext.apply(config, {
|
|
storeid: 'PVEResources',
|
|
model: 'PVEResources',
|
|
defaultColumns: function() {
|
|
var res = [];
|
|
Ext.Object.each(field_defaults, function(field, info) {
|
|
var fi = Ext.apply({ dataIndex: field }, info);
|
|
res.push(fi);
|
|
});
|
|
return res;
|
|
},
|
|
fieldNames: fieldNames
|
|
});
|
|
|
|
me.callParent([config]);
|
|
}
|
|
});
|
|
Ext.define('pve-domains', {
|
|
extend: "Ext.data.Model",
|
|
fields: [
|
|
'realm', 'type', 'comment', 'default', 'tfa',
|
|
{
|
|
name: 'descr',
|
|
// Note: We use this in the RealmComboBox.js (see Bug #125)
|
|
convert: function(value, record) {
|
|
if (value) {
|
|
return value;
|
|
}
|
|
|
|
var info = record.data;
|
|
// return realm if there is no comment
|
|
var text = info.comment || info.realm;
|
|
|
|
if (info.tfa) {
|
|
text += " (+ " + info.tfa + ")";
|
|
}
|
|
|
|
return Ext.String.htmlEncode(text);
|
|
}
|
|
}
|
|
],
|
|
idProperty: 'realm',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/access/domains"
|
|
}
|
|
});
|
|
Ext.define('pve-rrd-node', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
{
|
|
name:'cpu',
|
|
// percentage
|
|
convert: function(value) {
|
|
return value*100;
|
|
}
|
|
},
|
|
{
|
|
name:'iowait',
|
|
// percentage
|
|
convert: function(value) {
|
|
return value*100;
|
|
}
|
|
},
|
|
'loadavg',
|
|
'maxcpu',
|
|
'memtotal',
|
|
'memused',
|
|
'netin',
|
|
'netout',
|
|
'roottotal',
|
|
'rootused',
|
|
'swaptotal',
|
|
'swapused',
|
|
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
|
|
]
|
|
});
|
|
|
|
Ext.define('pve-rrd-guest', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
{
|
|
name:'cpu',
|
|
// percentage
|
|
convert: function(value) {
|
|
return value*100;
|
|
}
|
|
},
|
|
'maxcpu',
|
|
'netin',
|
|
'netout',
|
|
'mem',
|
|
'maxmem',
|
|
'disk',
|
|
'maxdisk',
|
|
'diskread',
|
|
'diskwrite',
|
|
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
|
|
]
|
|
});
|
|
|
|
Ext.define('pve-rrd-storage', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'used',
|
|
'total',
|
|
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
|
|
]
|
|
});
|
|
Ext.define('PVE.form.VlanField', {
|
|
extend: 'Ext.form.field.Number',
|
|
alias: ['widget.pveVlanField'],
|
|
|
|
deleteEmpty: false,
|
|
|
|
emptyText: 'no VLAN',
|
|
|
|
fieldLabel: gettext('VLAN Tag'),
|
|
|
|
allowBlank: true,
|
|
|
|
getSubmitData: function() {
|
|
var me = this,
|
|
data = null,
|
|
val;
|
|
if (!me.disabled && me.submitValue) {
|
|
val = me.getSubmitValue();
|
|
if (val) {
|
|
data = {};
|
|
data[me.getName()] = val;
|
|
} else if (me.deleteEmpty) {
|
|
data = {};
|
|
data['delete'] = me.getName();
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
Ext.apply(me, {
|
|
minValue: 1,
|
|
maxValue: 4094
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
// boolean type including 'Default' (delete property from file)
|
|
Ext.define('PVE.form.Boolean', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.booleanfield'],
|
|
comboItems: [
|
|
['__default__', gettext('Default')],
|
|
[1, gettext('Yes')],
|
|
[0, gettext('No')]
|
|
]
|
|
});
|
|
Ext.define('PVE.form.CompressionSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveCompressionSelector'],
|
|
comboItems: [
|
|
['0', Proxmox.Utils.noneText],
|
|
['lzo', 'LZO (' + gettext('fast') + ')'],
|
|
['gzip', 'GZIP (' + gettext('good') + ')']
|
|
]
|
|
});
|
|
Ext.define('PVE.form.PoolSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pvePoolSelector'],
|
|
|
|
allowBlank: false,
|
|
valueField: 'poolid',
|
|
displayField: 'poolid',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-pools',
|
|
sorters: 'poolid'
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
autoSelect: false,
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Pool'),
|
|
sortable: true,
|
|
dataIndex: 'poolid',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
sortable: false,
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 1
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load();
|
|
}
|
|
|
|
}, function() {
|
|
|
|
Ext.define('pve-pools', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'poolid', 'comment' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/pools"
|
|
},
|
|
idProperty: 'poolid'
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.form.PrivilegesSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
xtype: 'pvePrivilegesSelector',
|
|
|
|
multiSelect: true,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
// So me.store is available.
|
|
me.callParent();
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/access/roles/Administrator',
|
|
method: 'GET',
|
|
success: function(response, options) {
|
|
var data = [], key;
|
|
/*jslint forin: true */
|
|
for (key in response.result.data) {
|
|
data.push([key, key]);
|
|
}
|
|
/*jslint forin: false */
|
|
|
|
me.store.setData(data);
|
|
|
|
me.store.sort({
|
|
property: 'key',
|
|
direction: 'ASC'
|
|
});
|
|
},
|
|
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('pve-groups', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'groupid', 'comment' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/access/groups"
|
|
},
|
|
idProperty: 'groupid'
|
|
});
|
|
|
|
Ext.define('PVE.form.GroupSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
xtype: 'pveGroupSelector',
|
|
|
|
allowBlank: false,
|
|
autoSelect: false,
|
|
valueField: 'groupid',
|
|
displayField: 'groupid',
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Group'),
|
|
sortable: true,
|
|
dataIndex: 'groupid',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
sortable: false,
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 1
|
|
}
|
|
]
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-groups',
|
|
sorters: [{
|
|
property: 'groupid'
|
|
}]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load();
|
|
}
|
|
});
|
|
Ext.define('PVE.form.UserSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveUserSelector'],
|
|
|
|
allowBlank: false,
|
|
autoSelect: false,
|
|
valueField: 'userid',
|
|
displayField: 'userid',
|
|
|
|
editable: true,
|
|
anyMatch: true,
|
|
forceSelection: true,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-users',
|
|
sorters: [{
|
|
property: 'userid'
|
|
}]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('User'),
|
|
sortable: true,
|
|
dataIndex: 'userid',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Name'),
|
|
sortable: true,
|
|
renderer: PVE.Utils.render_full_name,
|
|
dataIndex: 'firstname',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
sortable: false,
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 1
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load({ params: { enabled: 1 }});
|
|
}
|
|
|
|
}, function() {
|
|
|
|
Ext.define('pve-users', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'userid', 'firstname', 'lastname' , 'email', 'comment',
|
|
{ type: 'boolean', name: 'enable' },
|
|
{ type: 'date', dateFormat: 'timestamp', name: 'expire' }
|
|
],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/access/users"
|
|
},
|
|
idProperty: 'userid'
|
|
});
|
|
|
|
});
|
|
|
|
|
|
Ext.define('PVE.form.RoleSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveRoleSelector'],
|
|
|
|
allowBlank: false,
|
|
autoSelect: false,
|
|
valueField: 'roleid',
|
|
displayField: 'roleid',
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-roles',
|
|
sorters: [{
|
|
property: 'roleid'
|
|
}]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Role'),
|
|
sortable: true,
|
|
dataIndex: 'roleid',
|
|
flex: 1
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load();
|
|
}
|
|
|
|
}, function() {
|
|
|
|
Ext.define('pve-roles', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'roleid', 'privs' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/access/roles"
|
|
},
|
|
idProperty: 'roleid'
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.form.GuestIDSelector', {
|
|
extend: 'Ext.form.field.Number',
|
|
alias: 'widget.pveGuestIDSelector',
|
|
|
|
allowBlank: false,
|
|
|
|
minValue: 100,
|
|
|
|
maxValue: 999999999,
|
|
|
|
validateExists: undefined,
|
|
|
|
loadNextFreeID: false,
|
|
|
|
guestType: undefined,
|
|
|
|
validator: function(value) {
|
|
var me = this;
|
|
|
|
if (!Ext.isNumeric(value) ||
|
|
value < me.minValue ||
|
|
value > me.maxValue) {
|
|
// check is done by ExtJS
|
|
return true;
|
|
}
|
|
|
|
if (me.validateExists === true && !me.exists) {
|
|
return me.unknownID;
|
|
}
|
|
|
|
if (me.validateExists === false && me.exists) {
|
|
return me.inUseID;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
var label = '{0} ID';
|
|
var unknownID = gettext('This {0} ID does not exists');
|
|
var inUseID = gettext('This {0} ID is already in use');
|
|
var type = 'CT/VM';
|
|
|
|
if (me.guestType === 'lxc') {
|
|
type = 'CT';
|
|
} else if (me.guestType === 'qemu') {
|
|
type = 'VM';
|
|
}
|
|
|
|
me.label = Ext.String.format(label, type);
|
|
me.unknownID = Ext.String.format(unknownID, type);
|
|
me.inUseID = Ext.String.format(inUseID, type);
|
|
|
|
Ext.apply(me, {
|
|
fieldLabel: me.label,
|
|
listeners: {
|
|
'change': function(field, newValue, oldValue) {
|
|
if (!Ext.isDefined(me.validateExists)) {
|
|
return;
|
|
}
|
|
Proxmox.Utils.API2Request({
|
|
params: { vmid: newValue },
|
|
url: '/cluster/nextid',
|
|
method: 'GET',
|
|
success: function(response, opts) {
|
|
me.exists = false;
|
|
me.validate();
|
|
},
|
|
failure: function(response, opts) {
|
|
me.exists = true;
|
|
me.validate();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (me.loadNextFreeID) {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/cluster/nextid',
|
|
method: 'GET',
|
|
success: function(response, opts) {
|
|
me.setRawValue(response.result.data);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.form.MemoryField', {
|
|
extend: 'Ext.form.field.Number',
|
|
alias: 'widget.pveMemoryField',
|
|
|
|
allowBlank: false,
|
|
|
|
hotplug: false,
|
|
|
|
minValue: 32,
|
|
|
|
maxValue: 4178944,
|
|
|
|
step: 32,
|
|
|
|
value: '512', // qm default
|
|
|
|
allowDecimals: false,
|
|
|
|
allowExponential: false,
|
|
|
|
computeUpDown: function(value) {
|
|
var me = this;
|
|
|
|
if (!me.hotplug) {
|
|
return { up: value + me.step, down: value - me.step };
|
|
}
|
|
|
|
var dimm_size = 512;
|
|
var prev_dimm_size = 0;
|
|
var min_size = 1024;
|
|
var current_size = min_size;
|
|
var value_up = min_size;
|
|
var value_down = min_size;
|
|
var value_start = min_size;
|
|
|
|
var i, j;
|
|
for (j = 0; j < 9; j++) {
|
|
for (i = 0; i < 32; i++) {
|
|
if ((value >= current_size) && (value < (current_size + dimm_size))) {
|
|
value_start = current_size;
|
|
value_up = current_size + dimm_size;
|
|
value_down = current_size - ((i === 0) ? prev_dimm_size : dimm_size);
|
|
}
|
|
current_size += dimm_size;
|
|
}
|
|
prev_dimm_size = dimm_size;
|
|
dimm_size = dimm_size*2;
|
|
}
|
|
|
|
return { up: value_up, down: value_down, start: value_start };
|
|
},
|
|
|
|
onSpinUp: function() {
|
|
var me = this;
|
|
if (!me.readOnly) {
|
|
var res = me.computeUpDown(me.getValue());
|
|
me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue));
|
|
}
|
|
},
|
|
|
|
onSpinDown: function() {
|
|
var me = this;
|
|
if (!me.readOnly) {
|
|
var res = me.computeUpDown(me.getValue());
|
|
me.setValue(Ext.Number.constrain(res.down, me.minValue, me.maxValue));
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (me.hotplug) {
|
|
me.minValue = 1024;
|
|
|
|
me.on('blur', function(field) {
|
|
var value = me.getValue();
|
|
var res = me.computeUpDown(value);
|
|
if (value === res.start || value === res.up || value === res.down) {
|
|
return;
|
|
}
|
|
field.setValue(res.up);
|
|
});
|
|
}
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.form.NetworkCardSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: 'widget.pveNetworkCardSelector',
|
|
comboItems: [
|
|
['e1000', 'Intel E1000'],
|
|
['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'],
|
|
['rtl8139', 'Realtek RTL8139'],
|
|
['vmxnet3', 'VMware vmxnet3']
|
|
]
|
|
});
|
|
Ext.define('PVE.form.DiskFormatSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: 'widget.pveDiskFormatSelector',
|
|
comboItems: [
|
|
['raw', gettext('Raw disk image') + ' (raw)'],
|
|
['qcow2', gettext('QEMU image format') + ' (qcow2)'],
|
|
['vmdk', gettext('VMware image format') + ' (vmdk)']
|
|
]
|
|
});
|
|
Ext.define('PVE.form.DiskSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
xtype: 'pveDiskSelector',
|
|
|
|
// can be
|
|
// undefined: all
|
|
// unused: only unused
|
|
// journal_disk: all disks with gpt
|
|
diskType: undefined,
|
|
|
|
valueField: 'devpath',
|
|
displayField: 'devpath',
|
|
emptyText: gettext('No Disks unused'),
|
|
listConfig: {
|
|
width: 600,
|
|
columns: [
|
|
{
|
|
header: gettext('Device'),
|
|
flex: 3,
|
|
sortable: true,
|
|
dataIndex: 'devpath'
|
|
},
|
|
{
|
|
header: gettext('Size'),
|
|
flex: 2,
|
|
sortable: false,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'size'
|
|
},
|
|
{
|
|
header: gettext('Serial'),
|
|
flex: 5,
|
|
sortable: true,
|
|
dataIndex: 'serial'
|
|
}
|
|
]
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.nodename;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
filterOnLoad: true,
|
|
model: 'pve-disk-list',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + nodename + "/disks/list",
|
|
extraParams: { type: me.diskType }
|
|
},
|
|
sorters: [
|
|
{
|
|
property : 'devpath',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-disk-list', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'devpath', 'used', { name: 'size', type: 'number'},
|
|
{name: 'osdid', type: 'number'},
|
|
'vendor', 'model', 'serial'],
|
|
idProperty: 'devpath'
|
|
});
|
|
});
|
|
Ext.define('PVE.form.BusTypeSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: 'widget.pveBusSelector',
|
|
|
|
noVirtIO: false,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']];
|
|
|
|
if (!me.noVirtIO) {
|
|
me.comboItems.push(['virtio', 'VirtIO Block']);
|
|
}
|
|
|
|
me.comboItems.push(['scsi', 'SCSI']);
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.form.ControllerSelector', {
|
|
extend: 'Ext.form.FieldContainer',
|
|
alias: 'widget.pveControllerSelector',
|
|
|
|
statics: {
|
|
maxIds: {
|
|
ide: 3,
|
|
sata: 5,
|
|
virtio: 15,
|
|
scsi: 13
|
|
}
|
|
},
|
|
|
|
noVirtIO: false,
|
|
|
|
vmconfig: {}, // used to check for existing devices
|
|
|
|
sortByPreviousUsage: function(vmconfig, controllerList) {
|
|
|
|
var usedControllers = Ext.clone(PVE.form.ControllerSelector.maxIds);
|
|
|
|
var type;
|
|
for (type in usedControllers) {
|
|
if(usedControllers.hasOwnProperty(type)) {
|
|
usedControllers[type] = 0;
|
|
}
|
|
}
|
|
|
|
var property;
|
|
for (property in vmconfig) {
|
|
if (vmconfig.hasOwnProperty(property)) {
|
|
if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) {
|
|
var foundController = property.match(PVE.Utils.bus_match)[1];
|
|
usedControllers[foundController]++;
|
|
}
|
|
}
|
|
}
|
|
|
|
var vmDefaults = PVE.qemu.OSDefaults[vmconfig.ostype];
|
|
|
|
var sortPriority = vmDefaults && vmDefaults.busPriority
|
|
? vmDefaults.busPriority : PVE.qemu.OSDefaults.generic;
|
|
|
|
var sortedList = Ext.clone(controllerList);
|
|
sortedList.sort(function(a,b) {
|
|
if (usedControllers[b] == usedControllers[a]) {
|
|
return sortPriority[b] - sortPriority[a];
|
|
}
|
|
return usedControllers[b] - usedControllers[a];
|
|
});
|
|
|
|
return sortedList;
|
|
},
|
|
|
|
setVMConfig: function(vmconfig, autoSelect) {
|
|
var me = this;
|
|
|
|
me.vmconfig = Ext.apply({}, vmconfig);
|
|
|
|
var clist = ['ide', 'virtio', 'scsi', 'sata'];
|
|
var bussel = me.down('field[name=controller]');
|
|
var deviceid = me.down('field[name=deviceid]');
|
|
|
|
if (autoSelect === 'cdrom') {
|
|
clist = ['ide', 'scsi', 'sata'];
|
|
if (!Ext.isDefined(me.vmconfig.ide2)) {
|
|
bussel.setValue('ide');
|
|
deviceid.setValue(2);
|
|
return;
|
|
}
|
|
} else {
|
|
// in most cases we want to add a disk to the same controller
|
|
// we previously used
|
|
clist = me.sortByPreviousUsage(me.vmconfig, clist);
|
|
}
|
|
|
|
Ext.Array.each(clist, function(controller) {
|
|
var confid, i;
|
|
bussel.setValue(controller);
|
|
for (i = 0; i <= PVE.form.ControllerSelector.maxIds[controller]; i++) {
|
|
confid = controller + i.toString();
|
|
if (!Ext.isDefined(me.vmconfig[confid])) {
|
|
deviceid.setValue(i);
|
|
return false; // break
|
|
}
|
|
}
|
|
});
|
|
deviceid.validate();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
Ext.apply(me, {
|
|
fieldLabel: gettext('Bus/Device'),
|
|
layout: 'hbox',
|
|
defaults: {
|
|
hideLabel: true
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'pveBusSelector',
|
|
name: 'controller',
|
|
value: PVE.qemu.OSDefaults.generic.busType,
|
|
noVirtIO: me.noVirtIO,
|
|
allowBlank: false,
|
|
flex: 2,
|
|
listeners: {
|
|
change: function(t, value) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
var field = me.down('field[name=deviceid]');
|
|
field.setMaxValue(PVE.form.ControllerSelector.maxIds[value]);
|
|
field.validate();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'deviceid',
|
|
minValue: 0,
|
|
maxValue: PVE.form.ControllerSelector.maxIds.ide,
|
|
value: '0',
|
|
flex: 1,
|
|
allowBlank: false,
|
|
validator: function(value) {
|
|
/*jslint confusion: true */
|
|
if (!me.rendered) {
|
|
return;
|
|
}
|
|
var field = me.down('field[name=controller]');
|
|
var controller = field.getValue();
|
|
var confid = controller + value;
|
|
if (Ext.isDefined(me.vmconfig[confid])) {
|
|
return "This device is already in use.";
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.form.EmailNotificationSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveEmailNotificationSelector'],
|
|
comboItems: [
|
|
['always', gettext('Always')],
|
|
['failure', gettext('On failure only')]
|
|
]
|
|
});
|
|
/*global Proxmox*/
|
|
Ext.define('PVE.form.RealmComboBox', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: ['widget.pveRealmComboBox'],
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
init: function(view) {
|
|
view.store.on('load', this.onLoad, view);
|
|
},
|
|
|
|
onLoad: function(store, records, success) {
|
|
if (!success) {
|
|
return;
|
|
}
|
|
var me = this;
|
|
var val = me.getValue();
|
|
if (!val || !me.store.findRecord('realm', val)) {
|
|
var def = 'pam';
|
|
Ext.each(records, function(rec) {
|
|
if (rec.data && rec.data['default']) {
|
|
def = rec.data.realm;
|
|
}
|
|
});
|
|
me.setValue(def);
|
|
}
|
|
}
|
|
},
|
|
|
|
fieldLabel: gettext('Realm'),
|
|
name: 'realm',
|
|
queryMode: 'local',
|
|
allowBlank: false,
|
|
editable: false,
|
|
forceSelection: true,
|
|
autoSelect: false,
|
|
triggerAction: 'all',
|
|
valueField: 'realm',
|
|
displayField: 'descr',
|
|
getState: function() {
|
|
return { value: this.getValue() };
|
|
},
|
|
applyState : function(state) {
|
|
if (state && state.value) {
|
|
this.setValue(state.value);
|
|
}
|
|
},
|
|
stateEvents: [ 'select' ],
|
|
stateful: true, // last chosen auth realm is saved between page reloads
|
|
id: 'pveloginrealm', // We need stable ids when using stateful, not autogenerated
|
|
stateID: 'pveloginrealm',
|
|
|
|
needOTP: function(realm) {
|
|
var me = this;
|
|
// use exact match
|
|
var rec = me.store.findRecord('realm', realm, 0, false, false, true);
|
|
return rec && rec.data && rec.data.tfa ? rec.data.tfa : undefined;
|
|
},
|
|
|
|
store: {
|
|
model: 'pve-domains',
|
|
autoLoad: true
|
|
}
|
|
});
|
|
/*
|
|
* Top left combobox, used to select a view of the underneath RessourceTree
|
|
*/
|
|
Ext.define('PVE.form.ViewSelector', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: ['widget.pveViewSelector'],
|
|
|
|
editable: false,
|
|
allowBlank: false,
|
|
forceSelection: true,
|
|
autoSelect: false,
|
|
valueField: 'key',
|
|
displayField: 'value',
|
|
hideLabel: true,
|
|
queryMode: 'local',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var default_views = {
|
|
server: {
|
|
text: gettext('Server View'),
|
|
groups: ['node']
|
|
},
|
|
folder: {
|
|
text: gettext('Folder View'),
|
|
groups: ['type']
|
|
},
|
|
storage: {
|
|
text: gettext('Storage View'),
|
|
groups: ['node'],
|
|
filterfn: function(node) {
|
|
return node.data.type === 'storage' || node.data.type === 'node';
|
|
}
|
|
},
|
|
pool: {
|
|
text: gettext('Pool View'),
|
|
groups: ['pool'],
|
|
// Pool View only lists VMs and Containers
|
|
filterfn: function(node) {
|
|
return node.data.type === 'qemu' || node.data.type === 'lxc' || node.data.type === 'openvz' ||
|
|
node.data.type === 'pool';
|
|
}
|
|
}
|
|
};
|
|
|
|
var groupdef = [];
|
|
Ext.Object.each(default_views, function(viewname, value) {
|
|
groupdef.push([viewname, value.text]);
|
|
});
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
model: 'KeyValue',
|
|
proxy: {
|
|
type: 'memory',
|
|
reader: 'array'
|
|
},
|
|
data: groupdef,
|
|
autoload: true
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
value: groupdef[0][0],
|
|
getViewFilter: function() {
|
|
var view = me.getValue();
|
|
return Ext.apply({ id: view }, default_views[view] || default_views.server);
|
|
},
|
|
|
|
getState: function() {
|
|
return { value: me.getValue() };
|
|
},
|
|
|
|
applyState : function(state, doSelect) {
|
|
var view = me.getValue();
|
|
if (state && state.value && (view != state.value)) {
|
|
var record = store.findRecord('key', state.value);
|
|
if (record) {
|
|
me.setValue(state.value, true);
|
|
if (doSelect) {
|
|
me.fireEvent('select', me, [record]);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
stateEvents: [ 'select' ],
|
|
stateful: true,
|
|
stateId: 'pveview',
|
|
id: 'view'
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
var statechange = function(sp, key, value) {
|
|
if (key === me.id) {
|
|
me.applyState(value, true);
|
|
}
|
|
};
|
|
|
|
var sp = Ext.state.Manager.getProvider();
|
|
me.mon(sp, 'statechange', statechange, me);
|
|
}
|
|
});
|
|
Ext.define('PVE.form.NodeSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveNodeSelector'],
|
|
|
|
// invalidate nodes which are offline
|
|
onlineValidator: false,
|
|
|
|
selectCurNode: false,
|
|
|
|
// do not allow those nodes (array)
|
|
disallowedNodes: undefined,
|
|
|
|
// only allow those nodes (array)
|
|
allowedNodes: undefined,
|
|
// set default value to empty array, else it inits it with
|
|
// null and after the store load it is an empty array,
|
|
// triggering dirtychange
|
|
value: [],
|
|
valueField: 'node',
|
|
displayField: 'node',
|
|
store: {
|
|
fields: [ 'node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes'
|
|
},
|
|
sorters: [
|
|
{
|
|
property : 'node',
|
|
direction: 'ASC'
|
|
},
|
|
{
|
|
property : 'mem',
|
|
direction: 'DESC'
|
|
}
|
|
]
|
|
},
|
|
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Node'),
|
|
dataIndex: 'node',
|
|
sortable: true,
|
|
hideable: false,
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Memory usage') + " %",
|
|
renderer: PVE.Utils.render_mem_usage_percent,
|
|
sortable: true,
|
|
width: 100,
|
|
dataIndex: 'mem'
|
|
},
|
|
{
|
|
header: gettext('CPU usage'),
|
|
renderer: PVE.Utils.render_cpu,
|
|
sortable: true,
|
|
width: 100,
|
|
dataIndex: 'cpu'
|
|
}
|
|
]
|
|
},
|
|
|
|
validator: function(value) {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
if (!me.onlineValidator || (me.allowBlank && !value)) {
|
|
return true;
|
|
}
|
|
|
|
var offline = [];
|
|
var notAllowed = [];
|
|
|
|
Ext.Array.each(value.split(/\s*,\s*/), function(node) {
|
|
var rec = me.store.findRecord(me.valueField, node);
|
|
if (!(rec && rec.data) || 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();
|
|
|
|
// filter out disallowed nodes
|
|
me.getStore().addFilter(new Ext.util.Filter({
|
|
filterFn: function(item) {
|
|
if (Ext.isArray(me.disallowedNodes)) {
|
|
return !Ext.Array.contains(me.disallowedNodes, item.data.node);
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
}));
|
|
|
|
me.mon(me.getStore(), 'load', function(){
|
|
me.isValid();
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.form.FileSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: 'widget.pveFileSelector',
|
|
|
|
editable: true,
|
|
anyMatch: true,
|
|
forceSelection: true,
|
|
|
|
listeners: {
|
|
afterrender: function() {
|
|
var me = this;
|
|
if (!me.disabled) {
|
|
me.setStorage(me.storage, me.nodename);
|
|
}
|
|
}
|
|
},
|
|
|
|
setStorage: function(storage, nodename) {
|
|
var me = this;
|
|
|
|
var change = false;
|
|
if (storage && (me.storage !== storage)) {
|
|
me.storage = storage;
|
|
change = true;
|
|
}
|
|
|
|
if (nodename && (me.nodename !== nodename)) {
|
|
me.nodename = nodename;
|
|
change = true;
|
|
}
|
|
|
|
if (!(me.storage && me.nodename && change)) {
|
|
return;
|
|
}
|
|
|
|
var url = '/api2/json/nodes/' + me.nodename + '/storage/' + me.storage + '/content';
|
|
if (me.storageContent) {
|
|
url += '?content=' + me.storageContent;
|
|
}
|
|
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: url
|
|
});
|
|
|
|
me.store.removeAll();
|
|
me.store.load();
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
this.setStorage(undefined, nodename);
|
|
},
|
|
|
|
store: {
|
|
model: 'pve-storage-content'
|
|
},
|
|
|
|
allowBlank: false,
|
|
autoSelect: false,
|
|
valueField: 'volid',
|
|
displayField: 'text',
|
|
|
|
listConfig: {
|
|
width: 600,
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'text',
|
|
hideable: false,
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Format'),
|
|
width: 60,
|
|
dataIndex: 'format'
|
|
},
|
|
{
|
|
header: gettext('Size'),
|
|
width: 100,
|
|
dataIndex: 'size',
|
|
renderer: Proxmox.Utils.format_size
|
|
}
|
|
]
|
|
}
|
|
});
|
|
Ext.define('PVE.form.StorageSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: 'widget.pveStorageSelector',
|
|
|
|
allowBlank: false,
|
|
valueField: 'storage',
|
|
displayField: 'storage',
|
|
listConfig: {
|
|
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
|
|
},
|
|
{
|
|
header: gettext('Capacity'),
|
|
width: 90,
|
|
dataIndex: 'total',
|
|
renderer: Proxmox.Utils.format_size
|
|
}
|
|
]
|
|
},
|
|
|
|
reloadStorageList: function() {
|
|
var me = this;
|
|
if (!me.nodename) {
|
|
return;
|
|
}
|
|
|
|
var params = {
|
|
format: 1
|
|
};
|
|
var url = '/api2/json/nodes/' + me.nodename + '/storage';
|
|
if (me.storageContent) {
|
|
params.content = me.storageContent;
|
|
}
|
|
if (me.targetNode) {
|
|
params.target = me.targetNode;
|
|
params.enabled = 1; // skip disabled storages
|
|
}
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: url,
|
|
extraParams: params
|
|
});
|
|
|
|
me.store.load();
|
|
|
|
},
|
|
|
|
setTargetNode: function(targetNode) {
|
|
var me = this;
|
|
|
|
if (!targetNode || (me.targetNode === targetNode)) {
|
|
return;
|
|
}
|
|
|
|
me.targetNode = targetNode;
|
|
|
|
me.reloadStorageList();
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
if (!nodename || (me.nodename === nodename)) {
|
|
return;
|
|
}
|
|
|
|
me.nodename = nodename;
|
|
|
|
me.reloadStorageList();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.nodename;
|
|
me.nodename = undefined;
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
model: 'pve-storage-status',
|
|
sorters: {
|
|
property: 'storage',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (nodename) {
|
|
me.setNodename(nodename);
|
|
}
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-storage-status', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'storage', 'active', 'type', 'avail', 'total' ],
|
|
idProperty: 'storage'
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.form.DiskStorageSelector', {
|
|
extend: 'Ext.container.Container',
|
|
alias: 'widget.pveDiskStorageSelector',
|
|
|
|
layout: 'fit',
|
|
defaults: {
|
|
margin: '0 0 5 0'
|
|
},
|
|
|
|
// the fieldLabel for the storageselector
|
|
storageLabel: gettext('Storage'),
|
|
|
|
// the content to show (e.g., images or rootdir)
|
|
storageContent: undefined,
|
|
|
|
// if true, selects the first available storage
|
|
autoSelect: false,
|
|
|
|
allowBlank: false,
|
|
emptyText: '',
|
|
|
|
// hides the selection field
|
|
// this is always hidden on creation,
|
|
// and only shown when the storage needs a selection and
|
|
// hideSelection is not true
|
|
hideSelection: undefined,
|
|
|
|
// hides the size field (e.g, for the efi disk dialog)
|
|
hideSize: false,
|
|
|
|
// sets the 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;
|
|
}
|
|
|
|
var selectformat = false;
|
|
if (rec.data.format) {
|
|
var format = rec.data.format[0]; // 0 is the formats, 1 the default in the backend
|
|
delete format.subvol; // we never need subvol in the gui
|
|
selectformat = (Ext.Object.getSize(format) > 1);
|
|
}
|
|
|
|
var select = !!rec.data.select_existing && !me.hideSelection;
|
|
|
|
formatsel.setDisabled(!selectformat);
|
|
formatsel.setValue(selectformat ? 'qcow2' : 'raw');
|
|
|
|
hdfilesel.setDisabled(!select);
|
|
hdfilesel.setVisible(select);
|
|
if (select) {
|
|
hdfilesel.setStorage(value);
|
|
}
|
|
|
|
hdsizesel.setDisabled(select || me.hideSize);
|
|
hdsizesel.setVisible(!select && !me.hideSize);
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
var hdstorage = me.getComponent('hdstorage');
|
|
var hdfilesel = me.getComponent('hdimage');
|
|
|
|
hdstorage.setNodename(nodename);
|
|
hdfilesel.setNodename(nodename);
|
|
},
|
|
|
|
setDisabled: function(value) {
|
|
var me = this;
|
|
var hdstorage = me.getComponent('hdstorage');
|
|
|
|
// reset on disable
|
|
if (value) {
|
|
hdstorage.setValue();
|
|
}
|
|
hdstorage.setDisabled(value);
|
|
|
|
// disabling does not always fire this event and we do not need
|
|
// the value of the validity
|
|
hdstorage.fireEvent('validitychange');
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.items = [
|
|
{
|
|
xtype: 'pveStorageSelector',
|
|
itemId: 'hdstorage',
|
|
name: 'hdstorage',
|
|
reference: 'hdstorage',
|
|
fieldLabel: me.storageLabel,
|
|
nodename: me.nodename,
|
|
storageContent: me.storageContent,
|
|
disabled: me.disabled,
|
|
autoSelect: me.autoSelect,
|
|
allowBlank: me.allowBlank,
|
|
emptyText: me.emptyText,
|
|
listeners: {
|
|
change: {
|
|
fn: me.changeStorage,
|
|
scope: me
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'pveFileSelector',
|
|
name: 'hdimage',
|
|
reference: 'hdimage',
|
|
itemId: 'hdimage',
|
|
fieldLabel: gettext('Disk image'),
|
|
nodename: me.nodename,
|
|
disabled: true,
|
|
hidden: true
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
itemId: 'disksize',
|
|
reference: 'disksize',
|
|
name: 'disksize',
|
|
fieldLabel: gettext('Disk size') + ' (GiB)',
|
|
hidden: me.hideSize,
|
|
disabled: me.hideSize,
|
|
minValue: 0.001,
|
|
maxValue: 128*1024,
|
|
decimalPrecision: 3,
|
|
value: me.defaultSize,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'pveDiskFormatSelector',
|
|
itemId: 'diskformat',
|
|
reference: 'diskformat',
|
|
name: 'diskformat',
|
|
fieldLabel: gettext('Format'),
|
|
nodename: me.nodename,
|
|
disabled: true,
|
|
hidden: me.storageContent === 'rootdir',
|
|
value: 'qcow2',
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
// use it to disable the children but not ourself
|
|
me.disabled = false;
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.form.BridgeSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.PVE.form.BridgeSelector'],
|
|
|
|
bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge
|
|
|
|
store: {
|
|
fields: [ 'iface', 'active', 'type' ],
|
|
filterOnLoad: true,
|
|
sorters: [
|
|
{
|
|
property : 'iface',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
},
|
|
valueField: 'iface',
|
|
displayField: 'iface',
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Bridge'),
|
|
dataIndex: 'iface',
|
|
hideable: false,
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Active'),
|
|
width: 60,
|
|
dataIndex: 'active',
|
|
renderer: Proxmox.Utils.format_boolean
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comments',
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 1
|
|
}
|
|
]
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
if (!nodename || (me.nodename === nodename)) {
|
|
return;
|
|
}
|
|
|
|
me.nodename = nodename;
|
|
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/network?type=' +
|
|
me.bridgeType
|
|
});
|
|
|
|
me.store.load();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.nodename;
|
|
me.nodename = undefined;
|
|
|
|
me.callParent();
|
|
|
|
me.setNodename(nodename);
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.form.PCISelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
xtype: 'pvePCISelector',
|
|
|
|
store: {
|
|
fields: [ 'id','vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev' ],
|
|
filterOnLoad: true,
|
|
sorters: [
|
|
{
|
|
property : 'id',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
},
|
|
|
|
autoSelect: false,
|
|
valueField: 'id',
|
|
displayField: 'id',
|
|
|
|
// can contain a load callback for the store
|
|
// useful to determine the state of the IOMMU
|
|
onLoadCallBack: undefined,
|
|
|
|
listConfig: {
|
|
width: 800,
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
dataIndex: 'id',
|
|
width: 80
|
|
},
|
|
{
|
|
header: gettext('IOMMU Group'),
|
|
dataIndex: 'iommugroup',
|
|
width: 50
|
|
},
|
|
{
|
|
header: gettext('Vendor'),
|
|
dataIndex: 'vendor_name',
|
|
flex: 2
|
|
},
|
|
{
|
|
header: gettext('Device'),
|
|
dataIndex: 'device_name',
|
|
flex: 6
|
|
},
|
|
{
|
|
header: gettext('Mediated Devices'),
|
|
dataIndex: 'mdev',
|
|
flex: 1,
|
|
renderer: function(val) {
|
|
return Proxmox.Utils.format_boolean(!!val);
|
|
}
|
|
}
|
|
]
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
if (!nodename || (me.nodename === nodename)) {
|
|
return;
|
|
}
|
|
|
|
me.nodename = nodename;
|
|
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/hardware/pci'
|
|
});
|
|
|
|
me.store.load();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.nodename;
|
|
me.nodename = undefined;
|
|
|
|
me.callParent();
|
|
|
|
if (me.onLoadCallBack !== undefined) {
|
|
me.mon(me.getStore(), 'load', me.onLoadCallBack);
|
|
}
|
|
|
|
me.setNodename(nodename);
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.form.MDevSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
xtype: 'pveMDevSelector',
|
|
|
|
store: {
|
|
fields: [ 'type','available', 'description' ],
|
|
filterOnLoad: true,
|
|
sorters: [
|
|
{
|
|
property : 'type',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
},
|
|
autoSelect: false,
|
|
valueField: 'type',
|
|
displayField: 'type',
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Type'),
|
|
dataIndex: 'type',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Available'),
|
|
dataIndex: 'available',
|
|
width: 80
|
|
},
|
|
{
|
|
header: gettext('Description'),
|
|
dataIndex: 'description',
|
|
flex: 1,
|
|
renderer: function(value) {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
|
|
return value.split('\n').join('<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.SecurityGroupsSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveSecurityGroupsSelector'],
|
|
|
|
valueField: 'group',
|
|
displayField: 'group',
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
autoLoad: true,
|
|
fields: [ 'group', 'comment' ],
|
|
idProperty: 'group',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/cluster/firewall/groups"
|
|
},
|
|
sorters: {
|
|
property: 'group',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Security Group'),
|
|
dataIndex: 'group',
|
|
hideable: false,
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 1
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.form.IPRefSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveIPRefSelector'],
|
|
|
|
base_url: undefined,
|
|
|
|
preferredValue: '', // hack: else Form sets dirty flag?
|
|
|
|
ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias']
|
|
|
|
valueField: 'ref',
|
|
displayField: 'ref',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.base_url) {
|
|
throw "no base_url specified";
|
|
}
|
|
|
|
var url = "/api2/json" + me.base_url;
|
|
if (me.ref_type) {
|
|
url += "?type=" + me.ref_type;
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
autoLoad: true,
|
|
fields: [ 'type', 'name', 'ref', 'comment' ],
|
|
idProperty: 'ref',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: url
|
|
},
|
|
sorters: {
|
|
property: 'ref',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var disable_query_for_ips = function(f, value) {
|
|
if (value === null ||
|
|
value.match(/^\d/)) { // IP address starts with \d
|
|
f.queryDelay = 9999999999; // hack: disable with long delay
|
|
} else {
|
|
f.queryDelay = 10;
|
|
}
|
|
};
|
|
|
|
var columns = [];
|
|
|
|
if (!me.ref_type) {
|
|
columns.push({
|
|
header: gettext('Type'),
|
|
dataIndex: 'type',
|
|
hideable: false,
|
|
width: 60
|
|
});
|
|
}
|
|
|
|
columns.push(
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'ref',
|
|
hideable: false,
|
|
width: 140
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 1
|
|
}
|
|
);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: { columns: columns }
|
|
});
|
|
|
|
me.on('change', disable_query_for_ips);
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.form.IPProtocolSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveIPProtocolSelector'],
|
|
valueField: 'p',
|
|
displayField: 'p',
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Protocol'),
|
|
dataIndex: 'p',
|
|
hideable: false,
|
|
sortable: false,
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Number'),
|
|
dataIndex: 'n',
|
|
hideable: false,
|
|
sortable: false,
|
|
width: 50
|
|
},
|
|
{
|
|
header: gettext('Description'),
|
|
dataIndex: 'd',
|
|
hideable: false,
|
|
sortable: false,
|
|
flex: 1
|
|
}
|
|
]
|
|
},
|
|
store: {
|
|
fields: [ 'p', 'd', 'n'],
|
|
data: [
|
|
{ p: 'tcp', n: 6, d: 'Transmission Control Protocol' },
|
|
{ p: 'udp', n: 17, d: 'User Datagram Protocol' },
|
|
{ p: 'icmp', n: 1, d: 'Internet Control Message Protocol' },
|
|
{ p: 'igmp', n: 2, d: 'Internet Group Management' },
|
|
{ p: 'ggp', n: 3, d: 'gateway-gateway protocol' },
|
|
{ p: 'ipencap', n: 4, d: 'IP encapsulated in IP' },
|
|
{ p: 'st', n: 5, d: 'ST datagram mode' },
|
|
{ p: 'egp', n: 8, d: 'exterior gateway protocol' },
|
|
{ p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' },
|
|
{ p: 'pup', n: 12, d: 'PARC universal packet protocol' },
|
|
{ p: 'hmp', n: 20, d: 'host monitoring protocol' },
|
|
{ p: 'xns-idp', n: 22, d: 'Xerox NS IDP' },
|
|
{ p: 'rdp', n: 27, d: '"reliable datagram" protocol' },
|
|
{ p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' },
|
|
{ p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' },
|
|
{ p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' },
|
|
{ p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' },
|
|
{ p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' },
|
|
{ p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' },
|
|
{ p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' },
|
|
{ p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' },
|
|
{ p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' },
|
|
{ p: 'rsvp', n: 46, d: 'Reservation Protocol' },
|
|
{ p: 'gre', n: 47, d: 'General Routing Encapsulation' },
|
|
{ p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' },
|
|
{ p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' },
|
|
{ p: 'skip', n: 57, d: 'SKIP' },
|
|
{ p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' },
|
|
{ p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' },
|
|
{ p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' },
|
|
{ p: 'vmtp', n: 81, d: 'Versatile Message Transport' },
|
|
{ p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' },
|
|
{ p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' },
|
|
{ p: 'ax.25', n: 93, d: 'AX.25 frames' },
|
|
{ p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' },
|
|
{ p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' },
|
|
{ p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' },
|
|
{ p: 'pim', n: 103, d: 'Protocol Independent Multicast' },
|
|
{ p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' },
|
|
{ p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' },
|
|
{ p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' },
|
|
{ p: 'isis', n: 124, d: 'IS-IS over IPv4' },
|
|
{ p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' },
|
|
{ p: 'fc', n: 133, d: 'Fibre Channel' },
|
|
{ p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' },
|
|
{ p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' },
|
|
{ p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' },
|
|
{ p: 'hip', n: 139, d: 'Host Identity Protocol' },
|
|
{ p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' },
|
|
{ p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' },
|
|
{ p: 'rohc', n: 142, d: 'Robust Header Compression' }
|
|
]
|
|
}
|
|
});
|
|
Ext.define('PVE.form.CPUModelSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.CPUModelSelector'],
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText + ' (kvm64)'],
|
|
['486', '486'],
|
|
['athlon', 'athlon'],
|
|
['core2duo', 'core2duo'],
|
|
['coreduo', 'coreduo'],
|
|
['kvm32', 'kvm32'],
|
|
['kvm64', 'kvm64'],
|
|
['pentium', 'pentium'],
|
|
['pentium2', 'pentium2'],
|
|
['pentium3', 'pentium3'],
|
|
['phenom', 'phenom'],
|
|
['qemu32', 'qemu32'],
|
|
['qemu64', 'qemu64'],
|
|
['Conroe', 'Conroe'],
|
|
['Penryn', 'Penryn'],
|
|
['Nehalem', 'Nehalem'],
|
|
['Westmere', 'Westmere'],
|
|
['SandyBridge', 'SandyBridge'],
|
|
['IvyBridge', 'IvyBridge'],
|
|
['Haswell', 'Haswell'],
|
|
['Haswell-noTSX','Haswell-noTSX'],
|
|
['Broadwell', 'Broadwell'],
|
|
['Broadwell-noTSX','Broadwell-noTSX'],
|
|
['Skylake-Client','Skylake-Client'],
|
|
['Opteron_G1', 'Opteron_G1'],
|
|
['Opteron_G2', 'Opteron_G2'],
|
|
['Opteron_G3', 'Opteron_G3'],
|
|
['Opteron_G4', 'Opteron_G4'],
|
|
['Opteron_G5', 'Opteron_G5'],
|
|
['EPYC', 'EPYC'],
|
|
['host', 'host']
|
|
|
|
]
|
|
});
|
|
Ext.define('PVE.form.VNCKeyboardSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.VNCKeyboardSelector'],
|
|
comboItems: PVE.Utils.kvm_keymap_array()
|
|
});
|
|
Ext.define('PVE.form.CacheTypeSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.CacheTypeSelector'],
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText + " (" + gettext('No cache') + ")"],
|
|
['directsync', 'Direct sync'],
|
|
['writethrough', 'Write through'],
|
|
['writeback', 'Write back'],
|
|
['unsafe', 'Write back (' + gettext('unsafe') + ')'],
|
|
['none', gettext('No cache')]
|
|
]
|
|
});
|
|
Ext.define('PVE.form.SnapshotSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.PVE.form.SnapshotSelector'],
|
|
|
|
valueField: 'name',
|
|
displayField: 'name',
|
|
|
|
loadStore: function(nodename, vmid) {
|
|
var me = this;
|
|
|
|
if (!nodename) {
|
|
return;
|
|
}
|
|
|
|
me.nodename = nodename;
|
|
|
|
if (!vmid) {
|
|
return;
|
|
}
|
|
|
|
me.vmid = vmid;
|
|
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid +'/snapshot'
|
|
});
|
|
|
|
me.store.load();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
if (!me.guestType) {
|
|
throw "no guest type specified";
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: [ 'name'],
|
|
filterOnLoad: true
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Snapshot'),
|
|
dataIndex: 'name',
|
|
hideable: false,
|
|
flex: 1
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.loadStore(me.nodename, me.vmid);
|
|
}
|
|
});
|
|
Ext.define('PVE.form.ContentTypeSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveContentTypeSelector'],
|
|
|
|
cts: undefined,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.comboItems = [];
|
|
|
|
if (me.cts === undefined) {
|
|
me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets'];
|
|
}
|
|
|
|
Ext.Array.each(me.cts, function(ct) {
|
|
me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]);
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.form.HotplugFeatureSelector', {
|
|
extend: 'Ext.form.CheckboxGroup',
|
|
alias: 'widget.pveHotplugFeatureSelector',
|
|
|
|
columns: 1,
|
|
vertical: true,
|
|
|
|
defaults: {
|
|
name: 'hotplug',
|
|
submitValue: false
|
|
},
|
|
items: [
|
|
{
|
|
boxLabel: gettext('Disk'),
|
|
inputValue: 'disk',
|
|
checked: true
|
|
},
|
|
{
|
|
boxLabel: gettext('Network'),
|
|
inputValue: 'network',
|
|
checked: true
|
|
},
|
|
{
|
|
boxLabel: 'USB',
|
|
inputValue: 'usb',
|
|
checked: true
|
|
},
|
|
{
|
|
boxLabel: gettext('Memory'),
|
|
inputValue: 'memory'
|
|
},
|
|
{
|
|
boxLabel: gettext('CPU'),
|
|
inputValue: 'cpu'
|
|
}
|
|
],
|
|
|
|
setValue: function(value) {
|
|
var me = this;
|
|
var newVal = [];
|
|
if (value === '1') {
|
|
newVal = ['disk', 'network', 'usb'];
|
|
} else if (value !== '0') {
|
|
newVal = value.split(',');
|
|
}
|
|
me.callParent([{ hotplug: newVal }]);
|
|
},
|
|
|
|
// 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 */
|
|
/*jslint confusion: true*/
|
|
if (data.length === 0) {
|
|
return { 'hotplug':'0' };
|
|
} else {
|
|
return { 'hotplug': data.join(',') };
|
|
}
|
|
}
|
|
|
|
});
|
|
Ext.define('PVE.form.AgentFeatureSelector', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: ['widget.pveAgentFeatureSelector'],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
me.items= [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
boxLabel: gettext('Qemu Agent'),
|
|
name: 'enabled',
|
|
uncheckedValue: 0,
|
|
listeners: {
|
|
change: function(f, value, old) {
|
|
var gtcb = me.down('proxmoxcheckbox[name=fstrim_cloned_disks]');
|
|
if (value) {
|
|
gtcb.setDisabled(false);
|
|
} else {
|
|
gtcb.setDisabled(true);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
boxLabel: gettext('Run guest-trim after clone disk'),
|
|
name: 'fstrim_cloned_disks',
|
|
disabled: true
|
|
}
|
|
];
|
|
me.callParent();
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var agentstr = PVE.Parser.printPropertyString(values, 'enabled');
|
|
return { agent: agentstr };
|
|
},
|
|
|
|
setValues: function(values) {
|
|
var agent = values.agent || '';
|
|
var res = PVE.Parser.parsePropertyString(agent, 'enabled');
|
|
this.callParent([res]);
|
|
}
|
|
});
|
|
Ext.define('PVE.form.iScsiProviderSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveiScsiProviderSelector'],
|
|
comboItems: [
|
|
['comstar', 'Comstar'],
|
|
[ 'istgt', 'istgt'],
|
|
[ 'iet', 'IET'],
|
|
[ 'LIO', 'LIO']
|
|
]
|
|
});
|
|
Ext.define('PVE.form.DayOfWeekSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveDayOfWeekSelector'],
|
|
comboItems:[],
|
|
initComponent: function(){
|
|
var me = this;
|
|
me.comboItems = [
|
|
['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])],
|
|
['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])],
|
|
['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])],
|
|
['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])],
|
|
['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])],
|
|
['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])],
|
|
['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])]
|
|
];
|
|
this.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.form.BackupModeSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveBackupModeSelector'],
|
|
comboItems: [
|
|
['snapshot', gettext('Snapshot')],
|
|
['suspend', gettext('Suspend')],
|
|
['stop', gettext('Stop')]
|
|
]
|
|
});
|
|
Ext.define('PVE.form.ScsiHwSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveScsiHwSelector'],
|
|
comboItems: [
|
|
['__default__', PVE.Utils.render_scsihw('')],
|
|
['lsi', PVE.Utils.render_scsihw('lsi')],
|
|
['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')],
|
|
['megasas', PVE.Utils.render_scsihw('megasas')],
|
|
['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')],
|
|
['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')],
|
|
['pvscsi', PVE.Utils.render_scsihw('pvscsi')]
|
|
]
|
|
});
|
|
Ext.define('PVE.form.FirewallPolicySelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveFirewallPolicySelector'],
|
|
comboItems: [
|
|
['ACCEPT', 'ACCEPT'],
|
|
['REJECT', 'REJECT'],
|
|
[ 'DROP', 'DROP']
|
|
]
|
|
});
|
|
/*
|
|
* This is a global search field
|
|
* it loads the /cluster/resources on focus
|
|
* and displays the result in a floating grid
|
|
*
|
|
* it filters and sorts the objects by the algorithm in
|
|
* the customFilter function
|
|
*
|
|
* also it does accept key up/down and enter for input
|
|
* and it opens to ctrl+shift+f and ctrl+space
|
|
*/
|
|
Ext.define('PVE.form.GlobalSearchField', {
|
|
extend: 'Ext.form.field.Text',
|
|
alias: 'widget.pveGlobalSearchField',
|
|
|
|
emptyText: gettext('Search'),
|
|
enableKeyEvents: true,
|
|
selectOnFocus: true,
|
|
padding: '0 5 0 5',
|
|
|
|
grid: {
|
|
xtype: 'gridpanel',
|
|
focusOnToFront: false,
|
|
floating: true,
|
|
emptyText: Proxmox.Utils.noneText,
|
|
width: 600,
|
|
height: 400,
|
|
scrollable: {
|
|
xtype: 'scroller',
|
|
y: true,
|
|
x:false
|
|
},
|
|
store: {
|
|
model: 'PVEResources',
|
|
proxy:{
|
|
type: 'proxmox',
|
|
url: '/api2/extjs/cluster/resources'
|
|
}
|
|
},
|
|
plugins: {
|
|
ptype: 'bufferedrenderer',
|
|
trailingBufferZone: 20,
|
|
leadingBufferZone: 20
|
|
},
|
|
|
|
hideMe: function() {
|
|
var me = this;
|
|
if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) {
|
|
return;
|
|
}
|
|
me.hasFocus = false;
|
|
if (!me.textfield.hasFocus) {
|
|
me.hide();
|
|
}
|
|
},
|
|
|
|
setFocus: function() {
|
|
var me = this;
|
|
me.hasFocus = true;
|
|
},
|
|
|
|
listeners: {
|
|
rowclick: function(grid, record) {
|
|
var me = this;
|
|
me.textfield.selectAndHide(record.id);
|
|
},
|
|
itemcontextmenu: function(v, record, item, index, event) {
|
|
var me = this;
|
|
me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event);
|
|
},
|
|
/* because of lint */
|
|
focusleave: {
|
|
fn: 'hideMe'
|
|
},
|
|
focusenter: 'setFocus'
|
|
},
|
|
|
|
columns: [
|
|
{
|
|
text: gettext('Type'),
|
|
dataIndex: 'type',
|
|
width: 100,
|
|
renderer: PVE.Utils.render_resource_type
|
|
},
|
|
{
|
|
text: gettext('Description'),
|
|
flex: 1,
|
|
dataIndex: 'text'
|
|
},
|
|
{
|
|
text: gettext('Node'),
|
|
dataIndex: 'node'
|
|
},
|
|
{
|
|
text: gettext('Pool'),
|
|
dataIndex: 'pool'
|
|
}
|
|
]
|
|
},
|
|
|
|
customFilter: function(item) {
|
|
var me = this;
|
|
var match = 0;
|
|
var fieldArr = [];
|
|
var i,j, fields;
|
|
|
|
// different types of objects have different fields to search
|
|
// for example, a node will never have a pool and vice versa
|
|
switch (item.data.type) {
|
|
case 'pool': fieldArr = ['type', 'pool', 'text']; break;
|
|
case 'node': fieldArr = ['type', 'node', 'text']; break;
|
|
case 'storage': fieldArr = ['type', 'pool', 'node', 'storage']; break;
|
|
default: fieldArr = ['name', 'type', 'node', 'pool', 'vmid'];
|
|
}
|
|
if (me.filterVal === '') {
|
|
item.data.relevance = 0;
|
|
return true;
|
|
}
|
|
|
|
// all text is case insensitive and each word is
|
|
// searched alone
|
|
// for every partial match, the row gets
|
|
// 1 match point, for every exact match
|
|
// it gets 2 points
|
|
//
|
|
// results gets sorted by points (descending)
|
|
fields = me.filterVal.split(/\s+/);
|
|
for(i = 0; i < fieldArr.length; i++) {
|
|
var v = item.data[fieldArr[i]];
|
|
if (v !== undefined) {
|
|
v = v.toString().toLowerCase();
|
|
for(j = 0; j < fields.length; j++) {
|
|
if (v.indexOf(fields[j]) !== -1) {
|
|
match++;
|
|
if(v === fields[j]) {
|
|
match++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// give the row the 'relevance' value
|
|
item.data.relevance = match;
|
|
return (match > 0);
|
|
},
|
|
|
|
updateFilter: function(field, newValue, oldValue) {
|
|
var me = this;
|
|
// parse input and filter store,
|
|
// show grid
|
|
me.grid.store.filterVal = newValue.toLowerCase().trim();
|
|
me.grid.store.clearFilter(true);
|
|
me.grid.store.filterBy(me.customFilter);
|
|
me.grid.getSelectionModel().select(0);
|
|
},
|
|
|
|
selectAndHide: function(id) {
|
|
var me = this;
|
|
me.tree.selectById(id);
|
|
me.grid.hide();
|
|
me.setValue('');
|
|
me.blur();
|
|
},
|
|
|
|
onKey: function(field, e) {
|
|
var me = this;
|
|
var key = e.getKey();
|
|
|
|
switch(key) {
|
|
case Ext.event.Event.ENTER:
|
|
// go to first entry if there is one
|
|
if (me.grid.store.getCount() > 0) {
|
|
me.selectAndHide(me.grid.getSelection()[0].data.id);
|
|
}
|
|
break;
|
|
case Ext.event.Event.UP:
|
|
me.grid.getSelectionModel().selectPrevious();
|
|
break;
|
|
case Ext.event.Event.DOWN:
|
|
me.grid.getSelectionModel().selectNext();
|
|
break;
|
|
case Ext.event.Event.ESC:
|
|
me.grid.hide();
|
|
me.blur();
|
|
break;
|
|
}
|
|
},
|
|
|
|
loadValues: function(field) {
|
|
var me = this;
|
|
var records = [];
|
|
|
|
me.hasFocus = true;
|
|
me.grid.textfield = me;
|
|
me.grid.store.load();
|
|
me.grid.showBy(me, 'tl-bl');
|
|
},
|
|
|
|
hideGrid: function() {
|
|
var me = this;
|
|
|
|
me.hasFocus = false;
|
|
if (!me.grid.hasFocus) {
|
|
me.grid.hide();
|
|
}
|
|
},
|
|
|
|
listeners: {
|
|
change: {
|
|
fn: 'updateFilter',
|
|
buffer: 250
|
|
},
|
|
specialkey: 'onKey',
|
|
focusenter: 'loadValues',
|
|
focusleave: {
|
|
fn: 'hideGrid',
|
|
delay: 100
|
|
}
|
|
},
|
|
|
|
toggleFocus: function() {
|
|
var me = this;
|
|
if (!me.hasFocus) {
|
|
me.focus();
|
|
} else {
|
|
me.blur();
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.tree) {
|
|
throw "no tree given";
|
|
}
|
|
|
|
me.grid = Ext.create(me.grid);
|
|
|
|
me.callParent();
|
|
|
|
/*jslint confusion: true*/
|
|
/*because shift is also a function*/
|
|
// bind ctrl+shift+f and ctrl+space
|
|
// to open/close the search
|
|
me.keymap = new Ext.KeyMap({
|
|
target: Ext.get(document),
|
|
binding: [{
|
|
key:'F',
|
|
ctrl: true,
|
|
shift: true,
|
|
fn: me.toggleFocus,
|
|
scope: me
|
|
},{
|
|
key:' ',
|
|
ctrl: true,
|
|
fn: me.toggleFocus,
|
|
scope: me
|
|
}]
|
|
});
|
|
|
|
// always select first item and
|
|
// sort by relevance after load
|
|
me.mon(me.grid.store, 'load', function() {
|
|
me.grid.getSelectionModel().select(0);
|
|
me.grid.store.sort({
|
|
property: 'relevance',
|
|
direction: 'DESC'
|
|
});
|
|
});
|
|
}
|
|
|
|
});
|
|
Ext.define('PVE.form.QemuBiosSelector', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveQemuBiosSelector'],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.comboItems = [
|
|
['__default__', PVE.Utils.render_qemu_bios('')],
|
|
['seabios', PVE.Utils.render_qemu_bios('seabios')],
|
|
['ovmf', PVE.Utils.render_qemu_bios('ovmf')]
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
/* filter is a javascript builtin, but extjs calls it also filter */
|
|
Ext.define('PVE.form.VMSelector', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.vmselector',
|
|
|
|
mixins: {
|
|
field: 'Ext.form.field.Field'
|
|
},
|
|
|
|
allowBlank: true,
|
|
selectAll: false,
|
|
isFormField: true,
|
|
|
|
plugins: 'gridfilters',
|
|
|
|
store: {
|
|
model: 'PVEResources',
|
|
autoLoad: true,
|
|
sorters: 'vmid',
|
|
filters: [{
|
|
property: 'type',
|
|
value: /lxc|qemu/
|
|
}]
|
|
},
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
dataIndex: 'vmid',
|
|
width: 80,
|
|
filter: {
|
|
type: 'number'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Node'),
|
|
dataIndex: 'node'
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'status',
|
|
filter: {
|
|
type: 'list'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1,
|
|
filter: {
|
|
type: 'string'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Pool'),
|
|
dataIndex: 'pool',
|
|
filter: {
|
|
type: 'list'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
dataIndex: 'type',
|
|
width: 120,
|
|
renderer: function(value) {
|
|
if (value === 'qemu') {
|
|
return gettext('Virtual Machine');
|
|
} else if (value === 'lxc') {
|
|
return gettext('LXC Container');
|
|
}
|
|
|
|
return '';
|
|
},
|
|
filter: {
|
|
type: 'list',
|
|
store: {
|
|
data: [
|
|
{id: 'qemu', text: gettext('Virtual Machine')},
|
|
{id: 'lxc', text: gettext('LXC Container')}
|
|
],
|
|
// due to EXTJS-18711
|
|
// we have to do a static list via a store
|
|
// but to avoid creating an object,
|
|
// we have to have a pseudo un function
|
|
un: function(){}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
header: 'HA ' + gettext('Status'),
|
|
dataIndex: 'hastate',
|
|
flex: 1,
|
|
filter: {
|
|
type: 'list'
|
|
}
|
|
}
|
|
],
|
|
|
|
selModel: {
|
|
selType: 'checkboxmodel',
|
|
mode: 'SIMPLE'
|
|
},
|
|
|
|
checkChangeEvents: [
|
|
'selectionchange',
|
|
'change'
|
|
],
|
|
|
|
listeners: {
|
|
selectionchange: function() {
|
|
// to trigger validity and error checks
|
|
this.checkChange();
|
|
}
|
|
},
|
|
|
|
getValue: function() {
|
|
var me = this;
|
|
var sm = me.getSelectionModel();
|
|
var selection = sm.getSelection();
|
|
var values = [];
|
|
var store = me.getStore();
|
|
selection.forEach(function(item) {
|
|
// only add if not filtered
|
|
if (store.findExact('vmid', item.data.vmid) !== -1) {
|
|
values.push(item.data.vmid);
|
|
}
|
|
});
|
|
return values;
|
|
},
|
|
|
|
setValue: function(value) {
|
|
console.log(value);
|
|
var me = this;
|
|
var sm = me.getSelectionModel();
|
|
if (!Ext.isArray(value)) {
|
|
value = value.split(',');
|
|
}
|
|
var selection = [];
|
|
var store = me.getStore();
|
|
|
|
value.forEach(function(item) {
|
|
var rec = store.findRecord('vmid',item, 0, false, true, true);
|
|
console.log(store);
|
|
|
|
if (rec) {
|
|
console.log(rec);
|
|
selection.push(rec);
|
|
}
|
|
});
|
|
|
|
sm.select(selection);
|
|
|
|
return me.mixins.field.setValue.call(me, value);
|
|
},
|
|
|
|
getErrors: function(value) {
|
|
var me = this;
|
|
if (me.allowBlank === false &&
|
|
me.getSelectionModel().getCount() === 0) {
|
|
me.addBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']);
|
|
return [gettext('No VM selected')];
|
|
}
|
|
|
|
me.removeBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']);
|
|
return [];
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.callParent();
|
|
|
|
if (me.nodename) {
|
|
me.store.filters.add({
|
|
property: 'node',
|
|
exactMatch: true,
|
|
value: me.nodename
|
|
});
|
|
}
|
|
|
|
// only show the relevant guests by default
|
|
if (me.action) {
|
|
var statusfilter = '';
|
|
switch (me.action) {
|
|
case 'startall':
|
|
statusfilter = 'stopped';
|
|
break;
|
|
case 'stopall':
|
|
statusfilter = 'running';
|
|
break;
|
|
}
|
|
if (statusfilter !== '') {
|
|
me.store.filters.add({
|
|
property: 'template',
|
|
value: 0
|
|
},{
|
|
id: 'x-gridfilter-status',
|
|
operator: 'in',
|
|
property: 'status',
|
|
value: [statusfilter]
|
|
});
|
|
}
|
|
}
|
|
|
|
var store = me.getStore();
|
|
var sm = me.getSelectionModel();
|
|
|
|
if (me.selectAll) {
|
|
me.mon(store,'load', function(){
|
|
me.getSelectionModel().selectAll(false);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
Ext.define('PVE.form.VMComboSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: 'widget.vmComboSelector',
|
|
|
|
valueField: 'vmid',
|
|
displayField: 'vmid',
|
|
|
|
autoSelect: false,
|
|
editable: true,
|
|
anyMatch: true,
|
|
forceSelection: true,
|
|
|
|
store: {
|
|
model: 'PVEResources',
|
|
autoLoad: true,
|
|
sorters: 'vmid',
|
|
filters: [{
|
|
property: 'type',
|
|
value: /lxc|qemu/
|
|
}]
|
|
},
|
|
|
|
listConfig: {
|
|
width: 600,
|
|
plugins: 'gridfilters',
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
dataIndex: 'vmid',
|
|
width: 80,
|
|
filter: {
|
|
type: 'number'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1,
|
|
filter: {
|
|
type: 'string'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Node'),
|
|
dataIndex: 'node'
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'status',
|
|
filter: {
|
|
type: 'list'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Pool'),
|
|
dataIndex: 'pool',
|
|
hidden: true,
|
|
filter: {
|
|
type: 'list'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
dataIndex: 'type',
|
|
width: 120,
|
|
renderer: function(value) {
|
|
if (value === 'qemu') {
|
|
return gettext('Virtual Machine');
|
|
} else if (value === 'lxc') {
|
|
return gettext('LXC Container');
|
|
}
|
|
|
|
return '';
|
|
},
|
|
filter: {
|
|
type: 'list',
|
|
store: {
|
|
data: [
|
|
{id: 'qemu', text: gettext('Virtual Machine')},
|
|
{id: 'lxc', text: gettext('LXC Container')}
|
|
],
|
|
un: function(){} // due to EXTJS-18711
|
|
}
|
|
}
|
|
},
|
|
{
|
|
header: 'HA ' + gettext('Status'),
|
|
dataIndex: 'hastate',
|
|
hidden: true,
|
|
flex: 1,
|
|
filter: {
|
|
type: 'list'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
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' }
|
|
],
|
|
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);
|
|
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: '-'
|
|
},
|
|
{
|
|
checked: true,
|
|
inputValue: '='
|
|
},
|
|
{
|
|
boxLabel: '+',
|
|
inputValue: '+'
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{
|
|
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);
|
|
}
|
|
});
|
|
Ext.define('PVE.form.USBSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveUSBSelector'],
|
|
allowBlank: false,
|
|
autoSelect: false,
|
|
displayField: 'usbid',
|
|
valueField: 'usbid',
|
|
editable: true,
|
|
|
|
getUSBValue: function() {
|
|
var me = this;
|
|
var rec = me.store.findRecord('usbid', me.value);
|
|
var val = 'host='+ me.value;
|
|
if (rec && rec.data.speed === "5000") {
|
|
val = 'host=' + me.value + ",usb3=1";
|
|
}
|
|
return val;
|
|
},
|
|
|
|
validator: function(value) {
|
|
var me = this;
|
|
if (me.type === 'device') {
|
|
return (/^[a-f0-9]{4}\:[a-f0-9]{4}$/i).test(value);
|
|
} else if (me.type === 'port') {
|
|
return (/^[0-9]+\-[0-9]+(\.[0-9]+)*$/).test(value);
|
|
}
|
|
return false;
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
|
|
if (!nodename) {
|
|
throw "no nodename specified";
|
|
}
|
|
|
|
if (me.type !== 'device' && me.type !== 'port') {
|
|
throw "no valid type specified";
|
|
}
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-usb-' + me.type,
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + nodename + "/scan/usb"
|
|
},
|
|
filters: [
|
|
function (item) {
|
|
return !!item.data.usbpath && !!item.data.prodid && item.data['class'] != 9;
|
|
}
|
|
]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: (me.type === 'device')?gettext('Device'):gettext('Port'),
|
|
sortable: true,
|
|
dataIndex: 'usbid',
|
|
width: 80
|
|
},
|
|
{
|
|
header: gettext('Manufacturer'),
|
|
sortable: true,
|
|
dataIndex: 'manufacturer',
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Product'),
|
|
sortable: true,
|
|
dataIndex: 'product',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Speed'),
|
|
width: 70,
|
|
sortable: true,
|
|
dataIndex: 'speed',
|
|
renderer: function(value) {
|
|
if (value === "5000") {
|
|
return "USB 3.0";
|
|
} else if (value === "480") {
|
|
return "USB 2.0";
|
|
} else {
|
|
return "USB 1.x";
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load();
|
|
}
|
|
|
|
}, function() {
|
|
|
|
Ext.define('pve-usb-device', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
{
|
|
name: 'usbid',
|
|
convert: function(val, data) {
|
|
if (val) {
|
|
return val;
|
|
}
|
|
return data.get('vendid') + ':' + data.get('prodid');
|
|
}
|
|
},
|
|
'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
|
|
{ name: 'port' , type: 'number' },
|
|
{ name: 'level' , type: 'number' },
|
|
{ name: 'class' , type: 'number' },
|
|
{ name: 'devnum' , type: 'number' },
|
|
{ name: 'busnum' , type: 'number' }
|
|
]
|
|
});
|
|
|
|
Ext.define('pve-usb-port', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
{
|
|
name: 'usbid',
|
|
convert: function(val,data) {
|
|
if (val) {
|
|
return val;
|
|
}
|
|
return data.get('busnum') + '-' + data.get('usbpath');
|
|
}
|
|
},
|
|
'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath',
|
|
{ name: 'port' , type: 'number' },
|
|
{ name: 'level' , type: 'number' },
|
|
{ name: 'class' , type: 'number' },
|
|
{ name: 'devnum' , type: 'number' },
|
|
{ name: 'busnum' , type: 'number' }
|
|
]
|
|
});
|
|
});
|
|
Ext.define('PVE.form.CalendarEvent', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
xtype: 'pveCalendarEvent',
|
|
|
|
editable: true,
|
|
|
|
valueField: 'value',
|
|
displayField: 'text',
|
|
queryMode: 'local',
|
|
|
|
store: {
|
|
field: [ 'value', 'text'],
|
|
data: [
|
|
{ value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) },
|
|
{ value: '*/2:00', text: gettext("Every two hours")},
|
|
{ value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30"},
|
|
{ value: 'mon..fri', text: gettext("Monday to Friday") + " 00:00"},
|
|
{ value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly")},
|
|
{ value: 'sun 01:00', text: gettext("Sunday") + " 01:00"}
|
|
]
|
|
},
|
|
|
|
tpl: [
|
|
'<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";
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: ['name'],
|
|
sorters: 'name',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/ceph/pools'
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load({
|
|
callback: function(rec, op, success){
|
|
if (success && rec.length > 0) {
|
|
me.select(rec[0]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
Ext.define('PVE.form.PermPathSelector', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
xtype: 'pvePermPathSelector',
|
|
|
|
valueField: 'value',
|
|
displayField: 'value',
|
|
typeAhead: true,
|
|
queryMode: 'local',
|
|
store: {
|
|
type: 'pvePermPath'
|
|
}
|
|
});
|
|
/* This class defines the "Tasks" tab of the bottom status panel
|
|
* Tasks are jobs with a start, end and log output
|
|
*/
|
|
|
|
Ext.define('PVE.dc.Tasks', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveClusterTasks'],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var taskstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
storeid: 'pve-cluster-tasks',
|
|
model: 'proxmox-tasks',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/cluster/tasks'
|
|
}
|
|
});
|
|
|
|
var store = Ext.create('Proxmox.data.DiffStore', {
|
|
rstore: taskstore,
|
|
sortAfterUpdate: true,
|
|
appendAtStart: true,
|
|
sorters: [
|
|
{
|
|
property : 'pid',
|
|
direction: 'DESC'
|
|
},
|
|
{
|
|
property : 'starttime',
|
|
direction: 'DESC'
|
|
}
|
|
]
|
|
|
|
});
|
|
|
|
var run_task_viewer = function() {
|
|
var sm = me.getSelectionModel();
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: rec.data.upid
|
|
});
|
|
win.show();
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
stateful: false,
|
|
|
|
viewConfig: {
|
|
trackOver: false,
|
|
stripeRows: true, // does not work with getRowClass()
|
|
|
|
getRowClass: function(record, index) {
|
|
var status = record.get('status');
|
|
|
|
if (status && status != 'OK') {
|
|
return "proxmox-invalid-row";
|
|
}
|
|
}
|
|
},
|
|
sortableColumns: false,
|
|
columns: [
|
|
{
|
|
header: gettext("Start Time"),
|
|
dataIndex: 'starttime',
|
|
width: 150,
|
|
renderer: function(value) {
|
|
return Ext.Date.format(value, "M d H:i:s");
|
|
}
|
|
},
|
|
{
|
|
header: gettext("End Time"),
|
|
dataIndex: 'endtime',
|
|
width: 150,
|
|
renderer: function(value, metaData, record) {
|
|
if (record.data.pid) {
|
|
if (record.data.type == "vncproxy" ||
|
|
record.data.type == "vncshell" ||
|
|
record.data.type == "spiceproxy") {
|
|
metaData.tdCls = "x-grid-row-console";
|
|
} else {
|
|
metaData.tdCls = "x-grid-row-loading";
|
|
}
|
|
return "";
|
|
}
|
|
return Ext.Date.format(value, "M d H:i:s");
|
|
}
|
|
},
|
|
{
|
|
header: gettext("Node"),
|
|
dataIndex: 'node',
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext("User name"),
|
|
dataIndex: 'user',
|
|
width: 150
|
|
},
|
|
{
|
|
header: gettext("Description"),
|
|
dataIndex: 'upid',
|
|
flex: 1,
|
|
renderer: Proxmox.Utils.render_upid
|
|
},
|
|
{
|
|
header: gettext("Status"),
|
|
dataIndex: 'status',
|
|
width: 200,
|
|
renderer: function(value, metaData, record) {
|
|
if (record.data.pid) {
|
|
if (record.data.type != "vncproxy") {
|
|
metaData.tdCls = "x-grid-row-loading";
|
|
}
|
|
return "";
|
|
}
|
|
if (value == 'OK') {
|
|
return 'OK';
|
|
}
|
|
// metaData.attr = 'style="color:red;"';
|
|
return Proxmox.Utils.errorText + ': ' + value;
|
|
}
|
|
}
|
|
],
|
|
listeners: {
|
|
itemdblclick: run_task_viewer,
|
|
show: taskstore.startUpdate,
|
|
destroy: taskstore.stopUpdate
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/* This class defines the "Cluster log" tab of the bottom status panel
|
|
* A log entry is a timestamp associated with an action on a cluster
|
|
*/
|
|
|
|
Ext.define('PVE.dc.Log', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveClusterLog'],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var logstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
storeid: 'pve-cluster-log',
|
|
model: 'proxmox-cluster-log',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/cluster/log'
|
|
}
|
|
});
|
|
|
|
var store = Ext.create('Proxmox.data.DiffStore', {
|
|
rstore: logstore,
|
|
appendAtStart: true
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
stateful: false,
|
|
|
|
viewConfig: {
|
|
trackOver: false,
|
|
stripeRows: true,
|
|
|
|
getRowClass: function(record, index) {
|
|
var pri = record.get('pri');
|
|
|
|
if (pri && pri <= 3) {
|
|
return "proxmox-invalid-row";
|
|
}
|
|
}
|
|
},
|
|
sortableColumns: false,
|
|
columns: [
|
|
{
|
|
header: gettext("Time"),
|
|
dataIndex: 'time',
|
|
width: 150,
|
|
renderer: function(value) {
|
|
return Ext.Date.format(value, "M d H:i:s");
|
|
}
|
|
},
|
|
{
|
|
header: gettext("Node"),
|
|
dataIndex: 'node',
|
|
width: 150
|
|
},
|
|
{
|
|
header: gettext("Service"),
|
|
dataIndex: 'tag',
|
|
width: 100
|
|
},
|
|
{
|
|
header: "PID",
|
|
dataIndex: 'pid',
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext("User name"),
|
|
dataIndex: 'user',
|
|
width: 150
|
|
},
|
|
{
|
|
header: gettext("Severity"),
|
|
dataIndex: 'pri',
|
|
renderer: PVE.Utils.render_serverity,
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext("Message"),
|
|
dataIndex: 'msg',
|
|
flex: 1
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: logstore.startUpdate,
|
|
deactivate: logstore.stopUpdate,
|
|
destroy: logstore.stopUpdate
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*
|
|
* This class describes the bottom panel
|
|
*/
|
|
Ext.define('PVE.panel.StatusPanel', {
|
|
extend: 'Ext.tab.Panel',
|
|
alias: 'widget.pveStatusPanel',
|
|
|
|
|
|
//title: "Logs",
|
|
//tabPosition: 'bottom',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var stateid = 'ltab';
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
var state = sp.get(stateid);
|
|
if (state && state.value) {
|
|
me.activeTab = state.value;
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
listeners: {
|
|
tabchange: function() {
|
|
var atab = me.getActiveTab().itemId;
|
|
var state = { value: atab };
|
|
sp.set(stateid, state);
|
|
}
|
|
},
|
|
items: [
|
|
{
|
|
itemId: 'tasks',
|
|
title: gettext('Tasks'),
|
|
xtype: 'pveClusterTasks'
|
|
},
|
|
{
|
|
itemId: 'clog',
|
|
title: gettext('Cluster log'),
|
|
xtype: 'pveClusterLog'
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.items.get(0).fireEvent('show', me.items.get(0));
|
|
|
|
var statechange = function(sp, key, state) {
|
|
if (key === stateid) {
|
|
var atab = me.getActiveTab().itemId;
|
|
var ntab = state.value;
|
|
if (state && ntab && (atab != ntab)) {
|
|
me.setActiveTab(ntab);
|
|
}
|
|
}
|
|
};
|
|
|
|
sp.on('statechange', statechange);
|
|
me.on('destroy', function() {
|
|
sp.un('statechange', statechange);
|
|
});
|
|
|
|
}
|
|
});
|
|
Ext.define('PVE.panel.StatusView', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveStatusView',
|
|
|
|
layout: {
|
|
type: 'column'
|
|
},
|
|
|
|
title: gettext('Status'),
|
|
|
|
getRecordValue: function(key, store) {
|
|
if (!key) {
|
|
throw "no key given";
|
|
}
|
|
var me = this;
|
|
|
|
if (store === undefined) {
|
|
store = me.getStore();
|
|
}
|
|
|
|
var rec = store.getById(key);
|
|
if (rec) {
|
|
return rec.data.value;
|
|
}
|
|
|
|
return '';
|
|
},
|
|
|
|
fieldRenderer: function(val,max) {
|
|
if (max === undefined) {
|
|
return val;
|
|
}
|
|
|
|
if (!Ext.isNumeric(max) || max === 1) {
|
|
return PVE.Utils.render_usage(val);
|
|
}
|
|
return PVE.Utils.render_size_usage(val,max);
|
|
},
|
|
|
|
fieldCalculator: function(used, max) {
|
|
if (!Ext.isNumeric(max) && Ext.isNumeric(used)) {
|
|
return used;
|
|
} else if(!Ext.isNumeric(used)) {
|
|
/* we come here if the field is from a node
|
|
* where the records are not mem and maxmem
|
|
* but mem.used and mem.total
|
|
*/
|
|
if (used.used !== undefined &&
|
|
used.total !== undefined) {
|
|
return used.used/used.total;
|
|
}
|
|
}
|
|
|
|
return used/max;
|
|
},
|
|
|
|
updateField: function(field) {
|
|
var me = this;
|
|
var text = '';
|
|
var renderer = me.fieldRenderer;
|
|
if (Ext.isFunction(field.renderer)) {
|
|
renderer = field.renderer;
|
|
}
|
|
if (field.multiField === true) {
|
|
field.updateValue(renderer.call(field, me.getStore().getRecord()));
|
|
} else if (field.textField !== undefined) {
|
|
field.updateValue(renderer.call(field, me.getRecordValue(field.textField)));
|
|
} else if(field.valueField !== undefined) {
|
|
var used = me.getRecordValue(field.valueField);
|
|
/*jslint confusion: true*/
|
|
/* string and int */
|
|
var max = field.maxField !== undefined ? me.getRecordValue(field.maxField) : 1;
|
|
|
|
var calculate = me.fieldCalculator;
|
|
|
|
if (Ext.isFunction(field.calculate)) {
|
|
calculate = field.calculate;
|
|
}
|
|
field.updateValue(renderer.call(field, used,max), calculate(used,max));
|
|
}
|
|
},
|
|
|
|
getStore: function() {
|
|
var me = this;
|
|
if (!me.rstore) {
|
|
throw "there is no rstore";
|
|
}
|
|
|
|
return me.rstore;
|
|
},
|
|
|
|
updateTitle: function() {
|
|
var me = this;
|
|
me.setTitle(me.getRecordValue('name'));
|
|
},
|
|
|
|
updateValues: function(store, records, success) {
|
|
if (!success) {
|
|
return; // do not update if store load was not successful
|
|
}
|
|
var me = this;
|
|
var itemsToUpdate = me.query('pveInfoWidget');
|
|
|
|
itemsToUpdate.forEach(me.updateField, me);
|
|
|
|
me.updateTitle(store);
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.rstore) {
|
|
throw "no rstore given";
|
|
}
|
|
|
|
if (!me.title) {
|
|
throw "no title given";
|
|
}
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.rstore);
|
|
|
|
me.callParent();
|
|
|
|
me.mon(me.rstore, 'load', 'updateValues');
|
|
}
|
|
|
|
});
|
|
Ext.define('PVE.panel.GuestStatusView', {
|
|
extend: 'PVE.panel.StatusView',
|
|
alias: 'widget.pveGuestStatusView',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
height: 300,
|
|
|
|
cbindData: function (initialConfig) {
|
|
var me = this;
|
|
return {
|
|
isQemu: me.pveSelNode.data.type === 'qemu',
|
|
isLxc: me.pveSelNode.data.type === 'lxc'
|
|
};
|
|
},
|
|
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
|
|
defaults: {
|
|
xtype: 'pveInfoWidget',
|
|
padding: '2 25'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'box',
|
|
height: 20
|
|
},
|
|
{
|
|
itemId: 'status',
|
|
title: gettext('Status'),
|
|
iconCls: 'fa fa-info fa-fw',
|
|
printBar: false,
|
|
multiField: true,
|
|
renderer: function(record) {
|
|
var me = this;
|
|
var text = record.data.status;
|
|
var qmpstatus = record.data.qmpstatus;
|
|
if (qmpstatus && qmpstatus !== record.data.status) {
|
|
text += ' (' + qmpstatus + ')';
|
|
}
|
|
return text;
|
|
}
|
|
},
|
|
{
|
|
itemId: 'hamanaged',
|
|
iconCls: 'fa fa-heartbeat fa-fw',
|
|
title: gettext('HA State'),
|
|
printBar: false,
|
|
textField: 'ha',
|
|
renderer: PVE.Utils.format_ha
|
|
},
|
|
{
|
|
xtype: 'pveInfoWidget',
|
|
itemId: 'node',
|
|
iconCls: 'fa fa-building fa-fw',
|
|
title: gettext('Node'),
|
|
cbind: {
|
|
text: '{pveSelNode.data.node}'
|
|
},
|
|
printBar: false
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
height: 15
|
|
},
|
|
{
|
|
itemId: 'cpu',
|
|
iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon',
|
|
title: gettext('CPU usage'),
|
|
valueField: 'cpu',
|
|
maxField: 'cpus',
|
|
renderer: PVE.Utils.render_cpu_usage,
|
|
// in this specific api call
|
|
// we already have the correct value for the usage
|
|
calculate: Ext.identityFn
|
|
},
|
|
{
|
|
itemId: 'memory',
|
|
iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon',
|
|
title: gettext('Memory usage'),
|
|
valueField: 'mem',
|
|
maxField: 'maxmem'
|
|
},
|
|
{
|
|
itemId: 'swap',
|
|
xtype: 'pveInfoWidget',
|
|
iconCls: 'fa fa-refresh fa-fw',
|
|
title: gettext('SWAP usage'),
|
|
valueField: 'swap',
|
|
maxField: 'maxswap',
|
|
cbind: {
|
|
hidden: '{isQemu}',
|
|
disabled: '{isQemu}'
|
|
}
|
|
},
|
|
{
|
|
itemId: 'rootfs',
|
|
iconCls: 'fa fa-hdd-o fa-fw',
|
|
title: gettext('Bootdisk size'),
|
|
valueField: 'disk',
|
|
maxField: 'maxdisk',
|
|
printBar: false,
|
|
renderer: function(used, max) {
|
|
var me = this;
|
|
me.setPrintBar(used > 0);
|
|
if (used === 0) {
|
|
return PVE.Utils.render_size(max);
|
|
} else {
|
|
return PVE.Utils.render_size_usage(used,max);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
height: 15
|
|
},
|
|
{
|
|
itemId: 'ips',
|
|
xtype: 'pveAgentIPView',
|
|
cbind: {
|
|
rstore: '{rstore}',
|
|
pveSelNode: '{pveSelNode}',
|
|
hidden: '{isLxc}',
|
|
disabled: '{isLxc}'
|
|
}
|
|
}
|
|
],
|
|
|
|
updateTitle: function() {
|
|
var me = this;
|
|
var uptime = me.getRecordValue('uptime');
|
|
|
|
var text = "";
|
|
if (Number(uptime) > 0) {
|
|
text = " (" + gettext('Uptime') + ': ' + Proxmox.Utils.format_duration_long(uptime)
|
|
+ ')';
|
|
}
|
|
|
|
me.setTitle(me.getRecordValue('name') + text);
|
|
}
|
|
});
|
|
/*
|
|
* This is a running chart widget
|
|
* you add time datapoints to it,
|
|
* and we only show the last x of it
|
|
* used for ceph performance charts
|
|
*/
|
|
Ext.define('PVE.widget.RunningChart', {
|
|
extend: 'Ext.container.Container',
|
|
alias: 'widget.pveRunningChart',
|
|
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'center'
|
|
},
|
|
items: [
|
|
{
|
|
width: 80,
|
|
xtype: 'box',
|
|
itemId: 'title',
|
|
data: {
|
|
title: ''
|
|
},
|
|
tpl: '<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: {
|
|
data: {}
|
|
},
|
|
|
|
sprites: [{
|
|
id: 'valueSprite',
|
|
type: 'text',
|
|
text: '0 B/s',
|
|
textAlign: 'end',
|
|
textBaseline: 'middle',
|
|
fontSize: 14
|
|
}],
|
|
|
|
series: [{
|
|
type: 'line',
|
|
xField: 'time',
|
|
yField: 'val',
|
|
fill: 'true',
|
|
colors: ['#cfcfcf'],
|
|
tooltip: {
|
|
trackMouse: true,
|
|
renderer: function( tooltip, record, ctx) {
|
|
var me = this.getChart();
|
|
var date = new Date(record.data.time);
|
|
var value = me.up().renderer(record.data.val);
|
|
tooltip.setHtml(
|
|
me.up().title + ': ' + value + '<br />' +
|
|
Ext.Date.format(date, 'H:i:s')
|
|
);
|
|
}
|
|
},
|
|
style: {
|
|
lineWidth: 1.5,
|
|
opacity: 0.60
|
|
},
|
|
marker: {
|
|
opacity: 0,
|
|
scaling: 0.01,
|
|
fx: {
|
|
duration: 200,
|
|
easing: 'easeOut'
|
|
}
|
|
},
|
|
highlightCfg: {
|
|
opacity: 1,
|
|
scaling: 1.5
|
|
}
|
|
}]
|
|
}
|
|
],
|
|
|
|
// the renderer for the tooltip and last value,
|
|
// default just the value
|
|
renderer: Ext.identityFn,
|
|
|
|
// show the last x seconds
|
|
// default is 5 minutes
|
|
timeFrame: 5*60,
|
|
|
|
addDataPoint: function(value, time) {
|
|
var me = this.chart;
|
|
var panel = me.up();
|
|
var now = new Date();
|
|
var begin = new Date(now.getTime() - (1000*panel.timeFrame));
|
|
|
|
me.store.add({
|
|
time: time || now.getTime(),
|
|
val: value || 0
|
|
});
|
|
|
|
// delete all old records when we have 20 times more datapoints
|
|
// than seconds in our timeframe (so even a subsecond graph does
|
|
// not trigger this often)
|
|
//
|
|
// records in the store do not take much space, but like this,
|
|
// we prevent a memory leak when someone has the site open for a long time
|
|
// with minimal graphical glitches
|
|
if (me.store.count() > panel.timeFrame * 20) {
|
|
var oldData = me.store.getData().createFiltered(function(item) {
|
|
return item.data.time < begin.getTime();
|
|
});
|
|
|
|
me.store.remove(oldData.getRange());
|
|
}
|
|
|
|
me.timeaxis.setMinimum(begin.getTime());
|
|
me.timeaxis.setMaximum(now.getTime());
|
|
me.valuesprite.setText(panel.renderer(value || 0).toString());
|
|
me.valuesprite.setAttributes({
|
|
x: me.getWidth() - 15,
|
|
y: me.getHeight()/2
|
|
}, true);
|
|
me.redraw();
|
|
},
|
|
|
|
setTitle: function(title) {
|
|
this.title = title;
|
|
var me = this.getComponent('title');
|
|
me.update({title: title});
|
|
},
|
|
|
|
initComponent: function(){
|
|
var me = this;
|
|
me.callParent();
|
|
|
|
if (me.title) {
|
|
me.getComponent('title').update({title: me.title});
|
|
}
|
|
me.chart = me.getComponent('chart');
|
|
me.chart.timeaxis = me.chart.getAxes()[1];
|
|
me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite');
|
|
if (me.color) {
|
|
me.chart.series[0].setStyle({
|
|
fill: me.color,
|
|
stroke: me.color
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.widget.Info',{
|
|
extend: 'Ext.container.Container',
|
|
alias: 'widget.pveInfoWidget',
|
|
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
|
|
value: 0,
|
|
maximum: 1,
|
|
printBar: true,
|
|
items: [
|
|
{
|
|
xtype: 'component',
|
|
itemId: 'label',
|
|
data: {
|
|
title: '',
|
|
usage: '',
|
|
iconCls: undefined
|
|
},
|
|
tpl: [
|
|
'<div class="left-aligned">',
|
|
'<tpl if="iconCls">',
|
|
'<i class="{iconCls}"></i> ',
|
|
'</tpl>',
|
|
'{title}</div> <div class="right-aligned">{usage}</div>'
|
|
]
|
|
},
|
|
{
|
|
height: 2,
|
|
border: 0
|
|
},
|
|
{
|
|
xtype: 'progressbar',
|
|
itemId: 'progress',
|
|
height: 5,
|
|
value: 0,
|
|
animate: true
|
|
}
|
|
],
|
|
|
|
warningThreshold: 0.6,
|
|
criticalThreshold: 0.9,
|
|
|
|
setPrintBar: function(enable) {
|
|
var me = this;
|
|
me.printBar = enable;
|
|
me.getComponent('progress').setVisible(enable);
|
|
},
|
|
|
|
setIconCls: function(iconCls) {
|
|
var me = this;
|
|
me.getComponent('label').data.iconCls = iconCls;
|
|
},
|
|
|
|
updateValue: function(text, usage) {
|
|
var me = this;
|
|
var label = me.getComponent('label');
|
|
label.update(Ext.apply(label.data, {title: me.title, usage:text}));
|
|
|
|
if (usage !== undefined &&
|
|
me.printBar &&
|
|
Ext.isNumeric(usage) &&
|
|
usage >= 0) {
|
|
var progressBar = me.getComponent('progress');
|
|
progressBar.updateProgress(usage, '');
|
|
if (usage > me.criticalThreshold) {
|
|
progressBar.removeCls('warning');
|
|
progressBar.addCls('critical');
|
|
} else if (usage > me.warningThreshold) {
|
|
progressBar.removeCls('critical');
|
|
progressBar.addCls('warning');
|
|
} else {
|
|
progressBar.removeCls('warning');
|
|
progressBar.removeCls('critical');
|
|
}
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.title) {
|
|
throw "no title defined";
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
me.getComponent('progress').setVisible(me.printBar);
|
|
|
|
me.updateValue(me.text, me.value);
|
|
me.setIconCls(me.iconCls);
|
|
}
|
|
|
|
});
|
|
Ext.define('PVE.panel.TemplateStatusView',{
|
|
extend: 'PVE.panel.StatusView',
|
|
alias: 'widget.pveTemplateStatusView',
|
|
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
|
|
defaults: {
|
|
xtype: 'pveInfoWidget',
|
|
printBar: false,
|
|
padding: '2 25'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'box',
|
|
height: 20
|
|
},
|
|
{
|
|
itemId: 'hamanaged',
|
|
iconCls: 'fa fa-heartbeat fa-fw',
|
|
title: gettext('HA State'),
|
|
printBar: false,
|
|
textField: 'ha',
|
|
renderer: PVE.Utils.format_ha
|
|
},
|
|
{
|
|
itemId: 'node',
|
|
iconCls: 'fa fa-fw fa-building',
|
|
title: gettext('Node')
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
height: 20
|
|
},
|
|
{
|
|
itemId: 'cpus',
|
|
iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon',
|
|
title: gettext('Processors'),
|
|
textField: 'cpus'
|
|
},
|
|
{
|
|
itemId: 'memory',
|
|
iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon',
|
|
title: gettext('Memory'),
|
|
textField: 'maxmem',
|
|
renderer: PVE.Utils.render_size
|
|
},
|
|
{
|
|
itemId: 'swap',
|
|
iconCls: 'fa fa-refresh fa-fw',
|
|
title: gettext('Swap'),
|
|
textField: 'maxswap',
|
|
renderer: PVE.Utils.render_size
|
|
},
|
|
{
|
|
itemId: 'disk',
|
|
iconCls: 'fa fa-hdd-o fa-fw',
|
|
title: gettext('Bootdisk size'),
|
|
textField: 'maxdisk',
|
|
renderer: PVE.Utils.render_size
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
height: 20
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var name = me.pveSelNode.data.name;
|
|
if (!name) {
|
|
throw "no name specified";
|
|
}
|
|
|
|
me.title = name;
|
|
|
|
me.callParent();
|
|
if (me.pveSelNode.data.type !== 'lxc') {
|
|
me.remove(me.getComponent('swap'));
|
|
}
|
|
me.getComponent('node').updateValue(me.pveSelNode.data.node);
|
|
}
|
|
});
|
|
Ext.define('PVE.widget.HealthWidget', {
|
|
extend: 'Ext.Component',
|
|
alias: 'widget.pveHealthWidget',
|
|
|
|
data: {
|
|
iconCls: PVE.Utils.get_health_icon(undefined, true),
|
|
text: '',
|
|
title: ''
|
|
},
|
|
|
|
style: {
|
|
'text-align':'center'
|
|
},
|
|
|
|
tpl: [
|
|
'<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();
|
|
}
|
|
|
|
});
|
|
/*global u2f*/
|
|
Ext.define('PVE.window.LoginWindow', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
controller: {
|
|
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
onLogon: function() {
|
|
var me = this;
|
|
|
|
var form = this.lookupReference('loginForm');
|
|
var unField = this.lookupReference('usernameField');
|
|
var saveunField = this.lookupReference('saveunField');
|
|
var view = this.getView();
|
|
|
|
if (!form.isValid()) {
|
|
return;
|
|
}
|
|
|
|
view.el.mask(gettext('Please wait...'), 'x-mask-loading');
|
|
|
|
// set or clear username
|
|
var sp = Ext.state.Manager.getProvider();
|
|
if (saveunField.getValue() === true) {
|
|
sp.set(unField.getStateId(), unField.getValue());
|
|
} else {
|
|
sp.clear(unField.getStateId());
|
|
}
|
|
sp.set(saveunField.getStateId(), saveunField.getValue());
|
|
|
|
form.submit({
|
|
failure: function(f, resp){
|
|
me.failure(resp);
|
|
},
|
|
success: function(f, resp){
|
|
view.el.unmask();
|
|
|
|
var data = resp.result.data;
|
|
if (Ext.isDefined(data.NeedTFA)) {
|
|
// Store first factor login information first:
|
|
data.LoggedOut = true;
|
|
Proxmox.Utils.setAuthData(data);
|
|
|
|
if (Ext.isDefined(data.U2FChallenge)) {
|
|
me.perform_u2f(data);
|
|
} else {
|
|
me.perform_otp();
|
|
}
|
|
} else {
|
|
me.success(data);
|
|
}
|
|
}
|
|
});
|
|
|
|
},
|
|
failure: function(resp) {
|
|
var me = this;
|
|
var view = me.getView();
|
|
view.el.unmask();
|
|
var handler = function() {
|
|
var uf = me.lookupReference('usernameField');
|
|
uf.focus(true, true);
|
|
};
|
|
|
|
Ext.MessageBox.alert(gettext('Error'),
|
|
gettext("Login failed. Please try again"),
|
|
handler);
|
|
},
|
|
success: function(data) {
|
|
var me = this;
|
|
var view = me.getView();
|
|
var handler = view.handler || Ext.emptyFn;
|
|
handler.call(me, data);
|
|
view.close();
|
|
},
|
|
|
|
perform_otp: function() {
|
|
var me = this;
|
|
var win = Ext.create('PVE.window.TFALoginWindow', {
|
|
onLogin: function(value) {
|
|
me.finish_tfa(value);
|
|
},
|
|
onCancel: function() {
|
|
Proxmox.LoggedOut = false;
|
|
Proxmox.Utils.authClear();
|
|
me.getView().show();
|
|
}
|
|
});
|
|
win.show();
|
|
},
|
|
|
|
perform_u2f: function(data) {
|
|
var me = this;
|
|
// Show the message:
|
|
var msg = Ext.Msg.show({
|
|
title: 'U2F: '+gettext('Verification'),
|
|
message: gettext('Please press the button on your U2F Device'),
|
|
buttons: []
|
|
});
|
|
var chlg = data.U2FChallenge;
|
|
var key = {
|
|
version: chlg.version,
|
|
keyHandle: chlg.keyHandle
|
|
};
|
|
u2f.sign(chlg.appId, chlg.challenge, [key], function(res) {
|
|
msg.close();
|
|
if (res.errorCode) {
|
|
Proxmox.Utils.authClear();
|
|
Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode));
|
|
return;
|
|
}
|
|
delete res.errorCode;
|
|
me.finish_tfa(JSON.stringify(res));
|
|
});
|
|
},
|
|
finish_tfa: function(res) {
|
|
var me = this;
|
|
var view = me.getView();
|
|
view.el.mask(gettext('Please wait...'), 'x-mask-loading');
|
|
var params = { response: res };
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'POST',
|
|
timeout: 5000, // it'll delay both success & failure
|
|
success: function(resp, opts) {
|
|
view.el.unmask();
|
|
// Fill in what we copy over from the 1st factor:
|
|
var data = resp.result.data;
|
|
data.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
|
|
data.username = Proxmox.UserName;
|
|
// Finish logging in:
|
|
me.success(data);
|
|
},
|
|
failure: function(resp, opts) {
|
|
Proxmox.Utils.authClear();
|
|
me.failure(resp);
|
|
}
|
|
});
|
|
},
|
|
|
|
control: {
|
|
'field[name=username]': {
|
|
specialkey: function(f, e) {
|
|
if (e.getKey() === e.ENTER) {
|
|
var pf = this.lookupReference('passwordField');
|
|
if (!pf.getValue()) {
|
|
pf.focus(false);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'field[name=lang]': {
|
|
change: function(f, value) {
|
|
var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
|
|
Ext.util.Cookies.set('PVELangCookie', value, dt);
|
|
this.getView().mask(gettext('Please wait...'), 'x-mask-loading');
|
|
window.location.reload();
|
|
}
|
|
},
|
|
'button[reference=loginButton]': {
|
|
click: 'onLogon'
|
|
},
|
|
'#': {
|
|
show: function() {
|
|
var sp = Ext.state.Manager.getProvider();
|
|
var checkboxField = this.lookupReference('saveunField');
|
|
var unField = this.lookupReference('usernameField');
|
|
|
|
var checked = sp.get(checkboxField.getStateId());
|
|
checkboxField.setValue(checked);
|
|
|
|
if(checked === true) {
|
|
var username = sp.get(unField.getStateId());
|
|
unField.setValue(username);
|
|
var pwField = this.lookupReference('passwordField');
|
|
pwField.focus();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
width: 400,
|
|
|
|
modal: true,
|
|
|
|
border: false,
|
|
|
|
draggable: true,
|
|
|
|
closable: false,
|
|
|
|
resizable: false,
|
|
|
|
layout: 'auto',
|
|
|
|
title: gettext('Proxmox VE Login'),
|
|
|
|
defaultFocus: 'usernameField',
|
|
|
|
defaultButton: 'loginButton',
|
|
|
|
items: [{
|
|
xtype: 'form',
|
|
layout: 'form',
|
|
url: '/api2/extjs/access/ticket',
|
|
reference: 'loginForm',
|
|
|
|
fieldDefaults: {
|
|
labelAlign: 'right',
|
|
allowBlank: false
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('User name'),
|
|
name: 'username',
|
|
itemId: 'usernameField',
|
|
reference: 'usernameField',
|
|
stateId: 'login-username'
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
inputType: 'password',
|
|
fieldLabel: gettext('Password'),
|
|
name: 'password',
|
|
reference: 'passwordField'
|
|
},
|
|
{
|
|
xtype: 'pveRealmComboBox',
|
|
name: 'realm'
|
|
},
|
|
{
|
|
xtype: 'proxmoxLanguageSelector',
|
|
fieldLabel: gettext('Language'),
|
|
value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en',
|
|
name: 'lang',
|
|
reference: 'langField',
|
|
submitValue: false
|
|
}
|
|
],
|
|
buttons: [
|
|
{
|
|
xtype: 'checkbox',
|
|
fieldLabel: gettext('Save User name'),
|
|
name: 'saveusername',
|
|
reference: 'saveunField',
|
|
stateId: 'login-saveusername',
|
|
labelWidth: 'auto',
|
|
labelAlign: 'right',
|
|
submitValue: false
|
|
},
|
|
{
|
|
text: gettext('Login'),
|
|
reference: 'loginButton'
|
|
}
|
|
]
|
|
}]
|
|
});
|
|
Ext.define('PVE.window.TFALoginWindow', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
modal: true,
|
|
resizable: false,
|
|
title: 'Two-Factor Authentication',
|
|
layout: 'form',
|
|
defaultButton: 'loginButton',
|
|
defaultFocus: 'otpField',
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
login: function() {
|
|
var me = this;
|
|
var view = me.getView();
|
|
view.onLogin(me.lookup('otpField').getValue());
|
|
view.close();
|
|
},
|
|
cancel: function() {
|
|
var me = this;
|
|
var view = me.getView();
|
|
view.onCancel();
|
|
view.close();
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Please enter your OTP verification code:'),
|
|
name: 'otp',
|
|
itemId: 'otpField',
|
|
reference: 'otpField',
|
|
allowBlank: false
|
|
}
|
|
],
|
|
|
|
buttons: [
|
|
{
|
|
text: gettext('Login'),
|
|
reference: 'loginButton',
|
|
handler: 'login'
|
|
},
|
|
{
|
|
text: gettext('Cancel'),
|
|
handler: 'cancel'
|
|
}
|
|
]
|
|
});
|
|
Ext.define('PVE.window.Wizard', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
activeTitle: '', // used for automated testing
|
|
|
|
width: 700,
|
|
height: 510,
|
|
|
|
modal: true,
|
|
border: false,
|
|
|
|
draggable: true,
|
|
closable: true,
|
|
resizable: false,
|
|
|
|
layout: 'border',
|
|
|
|
getValues: function(dirtyOnly) {
|
|
var me = this;
|
|
|
|
var values = {};
|
|
|
|
var form = me.down('form').getForm();
|
|
|
|
form.getFields().each(function(field) {
|
|
if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
|
|
Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
|
|
}
|
|
});
|
|
|
|
Ext.Array.each(me.query('inputpanel'), function(panel) {
|
|
Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
|
|
});
|
|
|
|
return values;
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var tabs = me.items || [];
|
|
delete me.items;
|
|
|
|
/*
|
|
* Items may have the following functions:
|
|
* validator(): per tab custom validation
|
|
* onSubmit(): submit handler
|
|
* onGetValues(): overwrite getValues results
|
|
*/
|
|
|
|
Ext.Array.each(tabs, function(tab) {
|
|
tab.disabled = true;
|
|
});
|
|
tabs[0].disabled = false;
|
|
|
|
var maxidx = 0;
|
|
var curidx = 0;
|
|
|
|
var check_card = function(card) {
|
|
var valid = true;
|
|
var fields = card.query('field, fieldcontainer');
|
|
if (card.isXType('fieldcontainer')) {
|
|
fields.unshift(card);
|
|
}
|
|
Ext.Array.each(fields, function(field) {
|
|
// Note: not all fielcontainer have isValid()
|
|
if (Ext.isFunction(field.isValid) && !field.isValid()) {
|
|
valid = false;
|
|
}
|
|
});
|
|
|
|
if (Ext.isFunction(card.validator)) {
|
|
return card.validator();
|
|
}
|
|
|
|
return valid;
|
|
};
|
|
|
|
var disable_at = function(card) {
|
|
var tp = me.down('#wizcontent');
|
|
var idx = tp.items.indexOf(card);
|
|
for(;idx < tp.items.getCount();idx++) {
|
|
var nc = tp.items.getAt(idx);
|
|
if (nc) {
|
|
nc.disable();
|
|
}
|
|
}
|
|
};
|
|
|
|
var tabchange = function(tp, newcard, oldcard) {
|
|
if (newcard.onSubmit) {
|
|
me.down('#next').setVisible(false);
|
|
me.down('#submit').setVisible(true);
|
|
} else {
|
|
me.down('#next').setVisible(true);
|
|
me.down('#submit').setVisible(false);
|
|
}
|
|
var valid = check_card(newcard);
|
|
me.down('#next').setDisabled(!valid);
|
|
me.down('#submit').setDisabled(!valid);
|
|
me.down('#back').setDisabled(tp.items.indexOf(newcard) == 0);
|
|
|
|
var idx = tp.items.indexOf(newcard);
|
|
if (idx > maxidx) {
|
|
maxidx = idx;
|
|
}
|
|
curidx = idx;
|
|
|
|
var next = idx + 1;
|
|
var ntab = tp.items.getAt(next);
|
|
if (valid && ntab && !newcard.onSubmit) {
|
|
ntab.enable();
|
|
}
|
|
};
|
|
|
|
if (me.subject && !me.title) {
|
|
me.title = Proxmox.Utils.dialog_title(me.subject, true, false);
|
|
}
|
|
|
|
var sp = Ext.state.Manager.getProvider();
|
|
var advchecked = sp.get('proxmox-advanced-cb');
|
|
|
|
Ext.apply(me, {
|
|
items: [
|
|
{
|
|
xtype: 'form',
|
|
region: 'center',
|
|
layout: 'fit',
|
|
border: false,
|
|
margins: '5 5 0 5',
|
|
fieldDefaults: {
|
|
labelWidth: 100,
|
|
anchor: '100%'
|
|
},
|
|
items: [{
|
|
itemId: 'wizcontent',
|
|
xtype: 'tabpanel',
|
|
activeItem: 0,
|
|
bodyPadding: 10,
|
|
listeners: {
|
|
afterrender: function(tp) {
|
|
var atab = this.getActiveTab();
|
|
tabchange(tp, atab);
|
|
},
|
|
tabchange: function(tp, newcard, oldcard) {
|
|
tabchange(tp, newcard, oldcard);
|
|
}
|
|
},
|
|
items: tabs
|
|
}]
|
|
}
|
|
],
|
|
fbar: [
|
|
{
|
|
xtype: 'proxmoxHelpButton',
|
|
itemId: 'help'
|
|
},
|
|
'->',
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
boxLabelAlign: 'before',
|
|
boxLabel: gettext('Advanced'),
|
|
value: advchecked,
|
|
listeners: {
|
|
change: function(cb, val) {
|
|
var tp = me.down('#wizcontent');
|
|
tp.query('inputpanel').forEach(function(ip) {
|
|
ip.setAdvancedVisible(val);
|
|
});
|
|
|
|
sp.set('proxmox-advanced-cb', val);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Back'),
|
|
disabled: true,
|
|
itemId: 'back',
|
|
minWidth: 60,
|
|
handler: function() {
|
|
var tp = me.down('#wizcontent');
|
|
var atab = tp.getActiveTab();
|
|
var prev = tp.items.indexOf(atab) - 1;
|
|
if (prev < 0) {
|
|
return;
|
|
}
|
|
var ntab = tp.items.getAt(prev);
|
|
if (ntab) {
|
|
tp.setActiveTab(ntab);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Next'),
|
|
disabled: true,
|
|
itemId: 'next',
|
|
minWidth: 60,
|
|
handler: function() {
|
|
|
|
var form = me.down('form').getForm();
|
|
|
|
var tp = me.down('#wizcontent');
|
|
var atab = tp.getActiveTab();
|
|
if (!check_card(atab)) {
|
|
return;
|
|
}
|
|
|
|
var next = tp.items.indexOf(atab) + 1;
|
|
var ntab = tp.items.getAt(next);
|
|
if (ntab) {
|
|
ntab.enable();
|
|
tp.setActiveTab(ntab);
|
|
}
|
|
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Finish'),
|
|
minWidth: 60,
|
|
hidden: true,
|
|
itemId: 'submit',
|
|
handler: function() {
|
|
var tp = me.down('#wizcontent');
|
|
var atab = tp.getActiveTab();
|
|
atab.onSubmit();
|
|
}
|
|
}
|
|
]
|
|
});
|
|
me.callParent();
|
|
|
|
Ext.Array.each(me.query('inputpanel'), function(panel) {
|
|
panel.setAdvancedVisible(advchecked);
|
|
});
|
|
|
|
Ext.Array.each(me.query('field'), function(field) {
|
|
var validcheck = function() {
|
|
var tp = me.down('#wizcontent');
|
|
|
|
// check tabs from current to the last enabled for validity
|
|
// since we might have changed a validity on a later one
|
|
var i;
|
|
for (i = curidx; i <= maxidx && i < tp.items.getCount(); i++) {
|
|
var tab = tp.items.getAt(i);
|
|
var valid = check_card(tab);
|
|
|
|
// only set the buttons on the current panel
|
|
if (i === curidx) {
|
|
me.down('#next').setDisabled(!valid);
|
|
me.down('#submit').setDisabled(!valid);
|
|
}
|
|
|
|
// if a panel is invalid, then disable it and all following,
|
|
// else enable it and go to the next
|
|
var ntab = tp.items.getAt(i + 1);
|
|
if (!valid) {
|
|
disable_at(ntab);
|
|
return;
|
|
} else if (ntab && !tab.onSubmit) {
|
|
ntab.enable();
|
|
}
|
|
}
|
|
};
|
|
field.on('change', validcheck);
|
|
field.on('validitychange', validcheck);
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.window.NotesEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
Ext.apply(me, {
|
|
title: gettext('Notes'),
|
|
width: 600,
|
|
height: '400px',
|
|
resizable: true,
|
|
layout: 'fit',
|
|
defaultButton: undefined,
|
|
items: {
|
|
xtype: 'textarea',
|
|
name: 'description',
|
|
height: '100%',
|
|
value: '',
|
|
hideLabel: true
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
Ext.define('PVE.window.Backup', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
resizable: false,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
if (!me.vmtype) {
|
|
throw "no VM type specified";
|
|
}
|
|
|
|
var storagesel = Ext.create('PVE.form.StorageSelector', {
|
|
nodename: me.nodename,
|
|
name: 'storage',
|
|
value: me.storage,
|
|
fieldLabel: gettext('Storage'),
|
|
storageContent: 'backup',
|
|
allowBlank: false
|
|
});
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
fieldDefaults: {
|
|
labelWidth: 100,
|
|
anchor: '100%'
|
|
},
|
|
items: [
|
|
storagesel,
|
|
{
|
|
xtype: 'pveBackupModeSelector',
|
|
fieldLabel: gettext('Mode'),
|
|
value: 'snapshot',
|
|
name: 'mode'
|
|
},
|
|
{
|
|
xtype: 'pveCompressionSelector',
|
|
name: 'compress',
|
|
value: 'lzo',
|
|
fieldLabel: gettext('Compression')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Send email to'),
|
|
name: 'mailto',
|
|
emptyText: Proxmox.Utils.noneText
|
|
}
|
|
]
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Backup'),
|
|
handler: function(){
|
|
var storage = storagesel.getValue();
|
|
var values = form.getValues();
|
|
var params = {
|
|
storage: storage,
|
|
vmid: me.vmid,
|
|
mode: values.mode,
|
|
remove: 0
|
|
};
|
|
|
|
if ( values.mailto ) {
|
|
params.mailto = values.mailto;
|
|
}
|
|
|
|
if (values.compress) {
|
|
params.compress = values.compress;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/vzdump',
|
|
params: params,
|
|
method: 'POST',
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert('Error',response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
// close later so we reload the grid
|
|
// after the task has completed
|
|
me.hide();
|
|
|
|
var upid = response.result.data;
|
|
|
|
var win = Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: upid,
|
|
listeners: {
|
|
close: function() {
|
|
me.close();
|
|
}
|
|
}
|
|
});
|
|
win.show();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
var helpBtn = Ext.create('Proxmox.button.Help', {
|
|
onlineHelp: 'chapter_vzdump',
|
|
listenToGlobalEvent: false,
|
|
hidden: false
|
|
});
|
|
|
|
var title = gettext('Backup') + " " +
|
|
((me.vmtype === 'lxc') ? "CT" : "VM") +
|
|
" " + me.vmid;
|
|
|
|
Ext.apply(me, {
|
|
title: title,
|
|
width: 350,
|
|
modal: true,
|
|
layout: 'auto',
|
|
border: false,
|
|
items: [ me.formPanel ],
|
|
buttons: [ helpBtn, '->', submitBtn ]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.window.Restore', {
|
|
extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit?
|
|
|
|
resizable: false,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.volid) {
|
|
throw "no volume ID specified";
|
|
}
|
|
|
|
if (!me.vmtype) {
|
|
throw "no vmtype specified";
|
|
}
|
|
|
|
var storagesel = Ext.create('PVE.form.StorageSelector', {
|
|
nodename: me.nodename,
|
|
name: 'storage',
|
|
value: '',
|
|
fieldLabel: gettext('Storage'),
|
|
storageContent: (me.vmtype === 'lxc') ? 'rootdir' : 'images',
|
|
allowBlank: true
|
|
});
|
|
|
|
var IDfield;
|
|
if (me.vmid) {
|
|
IDfield = Ext.create('Ext.form.field.Display', {
|
|
name: 'vmid',
|
|
value: me.vmid,
|
|
fieldLabel: (me.vmtype === 'lxc') ? 'CT' : 'VM'
|
|
});
|
|
} else {
|
|
IDfield = Ext.create('PVE.form.GuestIDSelector', {
|
|
name: 'vmid',
|
|
guestType: me.vmtype,
|
|
loadNextFreeID: true,
|
|
validateExists: false
|
|
});
|
|
}
|
|
|
|
var items = [
|
|
{
|
|
xtype: 'displayfield',
|
|
value: me.volidText || me.volid,
|
|
fieldLabel: gettext('Source')
|
|
},
|
|
storagesel,
|
|
IDfield,
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'bwlimit',
|
|
fieldLabel: gettext('Read Limit (MiB/s)'),
|
|
minValue: 0,
|
|
emptyText: gettext('Defaults to target storage restore limit'),
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext("Use '0' to disable all bandwidth limits.")
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'unique',
|
|
fieldLabel: gettext('Unique'),
|
|
hidden: !!me.vmid,
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses')
|
|
},
|
|
checked: false
|
|
}
|
|
];
|
|
|
|
/*jslint confusion: true*/
|
|
if (me.vmtype === 'lxc') {
|
|
items.push({
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'unprivileged',
|
|
value: true,
|
|
fieldLabel: gettext('Unprivileged container')
|
|
});
|
|
}
|
|
/*jslint confusion: false*/
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
fieldDefaults: {
|
|
labelWidth: 100,
|
|
anchor: '100%'
|
|
},
|
|
items: items
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var doRestore = function(url, params) {
|
|
Proxmox.Utils.API2Request({
|
|
url: url,
|
|
params: params,
|
|
method: 'POST',
|
|
waitMsgTarget: me,
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
|
|
var win = Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: upid
|
|
});
|
|
win.show();
|
|
me.close();
|
|
}
|
|
});
|
|
};
|
|
|
|
var submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Restore'),
|
|
handler: function(){
|
|
var storage = storagesel.getValue();
|
|
var values = form.getValues();
|
|
|
|
var params = {
|
|
storage: storage,
|
|
vmid: me.vmid || values.vmid,
|
|
force: me.vmid ? 1 : 0
|
|
};
|
|
if (values.unique) { params.unique = 1; }
|
|
|
|
if (values.bwlimit !== undefined) {
|
|
params.bwlimit = values.bwlimit * 1024;
|
|
}
|
|
|
|
var url;
|
|
var msg;
|
|
if (me.vmtype === 'lxc') {
|
|
url = '/nodes/' + me.nodename + '/lxc';
|
|
params.ostemplate = me.volid;
|
|
params.restore = 1;
|
|
if (values.unprivileged) { params.unprivileged = 1; }
|
|
msg = Proxmox.Utils.format_task_description('vzrestore', params.vmid);
|
|
} else if (me.vmtype === 'qemu') {
|
|
url = '/nodes/' + me.nodename + '/qemu';
|
|
params.archive = me.volid;
|
|
msg = Proxmox.Utils.format_task_description('qmrestore', params.vmid);
|
|
} else {
|
|
throw 'unknown VM type';
|
|
}
|
|
|
|
if (me.vmid) {
|
|
msg += '. ' + gettext('This will permanently erase current VM data.');
|
|
Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
doRestore(url, params);
|
|
});
|
|
} else {
|
|
doRestore(url, params);
|
|
}
|
|
}
|
|
});
|
|
|
|
form.on('validitychange', function(f, valid) {
|
|
submitBtn.setDisabled(!valid);
|
|
});
|
|
|
|
var title = gettext('Restore') + ": " + (
|
|
(me.vmtype === 'lxc') ? 'CT' : 'VM');
|
|
|
|
if (me.vmid) {
|
|
title += " " + me.vmid;
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
title: title,
|
|
width: 500,
|
|
modal: true,
|
|
layout: 'auto',
|
|
border: false,
|
|
items: [ me.formPanel ],
|
|
buttons: [ submitBtn ]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/* Popup a message window
|
|
* where the user has to manually enter the resource ID
|
|
* to enable the destroy button
|
|
*/
|
|
Ext.define('PVE.window.SafeDestroy', {
|
|
extend: 'Ext.window.Window',
|
|
alias: 'widget.pveSafeDestroy',
|
|
|
|
title: gettext('Confirm'),
|
|
modal: true,
|
|
buttonAlign: 'center',
|
|
bodyPadding: 10,
|
|
width: 450,
|
|
layout: { type:'hbox' },
|
|
defaultFocus: 'confirmField',
|
|
showProgress: false,
|
|
|
|
config: {
|
|
item: {
|
|
id: undefined,
|
|
type: undefined
|
|
},
|
|
url: undefined,
|
|
params: {}
|
|
},
|
|
|
|
getParams: function() {
|
|
var me = this;
|
|
if (Ext.Object.isEmpty(me.params)) {
|
|
return '';
|
|
}
|
|
return '?' + Ext.Object.toQueryString(me.params);
|
|
},
|
|
|
|
controller: {
|
|
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
control: {
|
|
'field[name=confirm]': {
|
|
change: function(f, value) {
|
|
var view = this.getView();
|
|
var removeButton = this.lookupReference('removeButton');
|
|
if (value === view.getItem().id.toString()) {
|
|
removeButton.enable();
|
|
} else {
|
|
removeButton.disable();
|
|
}
|
|
},
|
|
specialkey: function (field, event) {
|
|
var removeButton = this.lookupReference('removeButton');
|
|
if (!removeButton.isDisabled() && event.getKey() == event.ENTER) {
|
|
removeButton.fireEvent('click', removeButton, event);
|
|
}
|
|
}
|
|
},
|
|
'button[reference=removeButton]': {
|
|
click: function() {
|
|
var view = this.getView();
|
|
Proxmox.Utils.API2Request({
|
|
url: view.getUrl() + view.getParams(),
|
|
method: 'DELETE',
|
|
waitMsgTarget: view,
|
|
failure: function(response, opts) {
|
|
view.close();
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var hasProgressBar = view.showProgress &&
|
|
response.result.data ? true : false;
|
|
|
|
if (hasProgressBar) {
|
|
// stay around so we can trigger our close events
|
|
// when background action is completed
|
|
view.hide();
|
|
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', {
|
|
upid: upid,
|
|
listeners: {
|
|
destroy: function () {
|
|
view.close();
|
|
}
|
|
}
|
|
});
|
|
win.show();
|
|
} else {
|
|
view.close();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'component',
|
|
cls: [ Ext.baseCSSPrefix + 'message-box-icon',
|
|
Ext.baseCSSPrefix + 'message-box-warning',
|
|
Ext.baseCSSPrefix + 'dlg-icon']
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
flex: 1,
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'component',
|
|
reference: 'messageCmp'
|
|
},
|
|
{
|
|
itemId: 'confirmField',
|
|
reference: 'confirmField',
|
|
xtype: 'textfield',
|
|
name: 'confirm',
|
|
labelWidth: 300,
|
|
hideTrigger: true,
|
|
allowBlank: false
|
|
}
|
|
]
|
|
}
|
|
],
|
|
buttons: [
|
|
{
|
|
reference: 'removeButton',
|
|
text: gettext('Remove'),
|
|
disabled: true
|
|
}
|
|
],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.callParent();
|
|
|
|
var item = me.getItem();
|
|
|
|
if (!Ext.isDefined(item.id)) {
|
|
throw "no ID specified";
|
|
}
|
|
|
|
if (!Ext.isDefined(item.type)) {
|
|
throw "no VM type specified";
|
|
}
|
|
|
|
var messageCmp = me.lookupReference('messageCmp');
|
|
var msg;
|
|
|
|
if (item.type === 'VM') {
|
|
msg = Proxmox.Utils.format_task_description('qmdestroy', item.id);
|
|
} else if (item.type === 'CT') {
|
|
msg = Proxmox.Utils.format_task_description('vzdestroy', item.id);
|
|
} else if (item.type === 'CephPool') {
|
|
msg = Proxmox.Utils.format_task_description('cephdestroypool', item.id);
|
|
} else if (item.type === 'Image') {
|
|
msg = Proxmox.Utils.format_task_description('unknownimgdel', item.id);
|
|
} else {
|
|
throw "unknown item type specified";
|
|
}
|
|
|
|
messageCmp.setHtml(msg);
|
|
|
|
var confirmField = me.lookupReference('confirmField');
|
|
msg = gettext('Please enter the ID to confirm') +
|
|
' (' + item.id + ')';
|
|
confirmField.setFieldLabel(msg);
|
|
}
|
|
});
|
|
Ext.define('PVE.window.BackupConfig', {
|
|
extend: 'Ext.window.Window',
|
|
title: gettext('Configuration'),
|
|
width: 600,
|
|
height: 400,
|
|
layout: 'fit',
|
|
modal: true,
|
|
items: {
|
|
xtype: 'component',
|
|
itemId: 'configtext',
|
|
autoScroll: true,
|
|
style: {
|
|
'background-color': 'white',
|
|
'white-space': 'pre',
|
|
'font-family': 'monospace',
|
|
padding: '5px'
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.volume) {
|
|
throw "no volume specified";
|
|
}
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: "/nodes/" + nodename + "/vzdump/extractconfig",
|
|
method: 'GET',
|
|
params: {
|
|
volume: me.volume
|
|
},
|
|
failure: function(response, opts) {
|
|
me.close();
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response,options) {
|
|
me.show();
|
|
me.down('#configtext').update(Ext.htmlEncode(response.result.data));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.window.Settings', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
width: '800px',
|
|
title: gettext('My Settings'),
|
|
iconCls: 'fa fa-gear',
|
|
modal: true,
|
|
bodyPadding: 10,
|
|
resizable: false,
|
|
|
|
buttons: [
|
|
{
|
|
xtype: 'proxmoxHelpButton',
|
|
onlineHelp: 'gui_my_settings',
|
|
hidden: false
|
|
},
|
|
'->',
|
|
{
|
|
text: gettext('Close'),
|
|
handler: function() {
|
|
this.up('window').close();
|
|
}
|
|
}
|
|
],
|
|
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'top'
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
init: function(view) {
|
|
var me = this;
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
var username = sp.get('login-username') || Proxmox.Utils.noneText;
|
|
me.lookupReference('savedUserName').setValue(username);
|
|
|
|
var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
|
|
settings.forEach(function(setting) {
|
|
var val = localStorage.getItem('pve-xterm-' + setting);
|
|
if (val !== undefined && val !== null) {
|
|
var field = me.lookup(setting);
|
|
field.setValue(val);
|
|
field.resetOriginalValue();
|
|
}
|
|
});
|
|
},
|
|
|
|
set_button_status: function() {
|
|
var me = this;
|
|
|
|
var form = me.lookup('xtermform');
|
|
var valid = form.isValid();
|
|
var dirty = form.isDirty();
|
|
|
|
var hasvalues = false;
|
|
var values = form.getValues();
|
|
Ext.Object.eachValue(values, function(value) {
|
|
if (value) {
|
|
hasvalues = true;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
me.lookup('xtermsave').setDisabled(!dirty || !valid);
|
|
me.lookup('xtermreset').setDisabled(!hasvalues);
|
|
},
|
|
|
|
control: {
|
|
'#xtermjs form': {
|
|
dirtychange: 'set_button_status',
|
|
validitychange: 'set_button_status'
|
|
},
|
|
'#xtermjs button': {
|
|
click: function(button) {
|
|
var me = this;
|
|
var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
|
|
settings.forEach(function(setting) {
|
|
var field = me.lookup(setting);
|
|
if (button.reference === 'xtermsave') {
|
|
var value = field.getValue();
|
|
if (value) {
|
|
localStorage.setItem('pve-xterm-' + setting, value);
|
|
} else {
|
|
localStorage.removeItem('pve-xterm-' + setting);
|
|
}
|
|
} else if (button.reference === 'xtermreset') {
|
|
field.setValue(undefined);
|
|
localStorage.removeItem('pve-xterm-' + setting);
|
|
}
|
|
field.resetOriginalValue();
|
|
});
|
|
me.set_button_status();
|
|
}
|
|
},
|
|
'button[name=reset]': {
|
|
click: function () {
|
|
var blacklist = ['GuiCap', 'login-username', 'dash-storages'];
|
|
var sp = Ext.state.Manager.getProvider();
|
|
var state;
|
|
for (state in sp.state) {
|
|
if (sp.state.hasOwnProperty(state)) {
|
|
if (blacklist.indexOf(state) !== -1) {
|
|
continue;
|
|
}
|
|
|
|
sp.clear(state);
|
|
}
|
|
}
|
|
|
|
window.location.reload();
|
|
}
|
|
},
|
|
'button[name=clear-username]': {
|
|
click: function () {
|
|
var me = this;
|
|
var usernamefield = me.lookupReference('savedUserName');
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
usernamefield.setValue(Proxmox.Utils.noneText);
|
|
sp.clear('login-username');
|
|
}
|
|
},
|
|
'grid[reference=dashboard-storages]': {
|
|
selectionchange: function(grid, selected) {
|
|
var me = this;
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
// saves the selected storageids as
|
|
// "id1,id2,id3,..."
|
|
// or clears the variable
|
|
if (selected.length > 0) {
|
|
sp.set('dash-storages',
|
|
Ext.Array.pluck(selected, 'id').join(','));
|
|
} else {
|
|
sp.clear('dash-storages');
|
|
}
|
|
},
|
|
afterrender: function(grid) {
|
|
var me = grid;
|
|
var sp = Ext.state.Manager.getProvider();
|
|
var store = me.getStore();
|
|
var items = [];
|
|
me.suspendEvent('selectionchange');
|
|
var storages = sp.get('dash-storages') || '';
|
|
storages.split(',').forEach(function(storage){
|
|
// we have to get the records
|
|
// to be able to select them
|
|
if (storage !== '') {
|
|
var item = store.getById(storage);
|
|
if (item) {
|
|
items.push(item);
|
|
}
|
|
}
|
|
});
|
|
me.getSelectionModel().select(items);
|
|
me.resumeEvent('selectionchange');
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
items: [{
|
|
xtype: 'fieldset',
|
|
width: '50%',
|
|
title: gettext('Webinterface Settings'),
|
|
margin: '5',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'left'
|
|
},
|
|
defaults: {
|
|
width: '100%',
|
|
margin: '0 0 10 0'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Dashboard Storages'),
|
|
labelAlign: 'left',
|
|
labelWidth: '50%'
|
|
},
|
|
{
|
|
xtype: 'grid',
|
|
maxHeight: 150,
|
|
reference: 'dashboard-storages',
|
|
selModel: {
|
|
selType: 'checkboxmodel'
|
|
},
|
|
columns: [{
|
|
header: gettext('Name'),
|
|
dataIndex: 'storage',
|
|
flex: 1
|
|
},{
|
|
header: gettext('Node'),
|
|
dataIndex: 'node',
|
|
flex: 1
|
|
}],
|
|
store: {
|
|
type: 'diff',
|
|
field: ['type', 'storage', 'id', 'node'],
|
|
rstore: PVE.data.ResourceStore,
|
|
filters: [{
|
|
property: 'type',
|
|
value: 'storage'
|
|
}],
|
|
sorters: [ 'node','storage']
|
|
}
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
autoEl: { tag: 'hr'}
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Saved User name'),
|
|
labelAlign: 'left',
|
|
labelWidth: '50%',
|
|
stateId: 'login-username',
|
|
reference: 'savedUserName',
|
|
value: ''
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
cls: 'x-btn-default-toolbar-small proxmox-inline-button',
|
|
text: gettext('Clear User name'),
|
|
width: 'auto',
|
|
name: 'clear-username'
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
autoEl: { tag: 'hr'}
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Layout'),
|
|
labelAlign: 'left',
|
|
labelWidth: '50%'
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
cls: 'x-btn-default-toolbar-small proxmox-inline-button',
|
|
text: gettext('Reset Layout'),
|
|
width: 'auto',
|
|
name: 'reset'
|
|
}
|
|
]
|
|
},{
|
|
xtype: 'fieldset',
|
|
itemId: 'xtermjs',
|
|
width: '50%',
|
|
margin: '5',
|
|
title: gettext('xterm.js Settings'),
|
|
items: [{
|
|
xtype: 'form',
|
|
reference: 'xtermform',
|
|
border: false,
|
|
layout: {
|
|
type: 'vbox',
|
|
algin: 'left'
|
|
},
|
|
defaults: {
|
|
width: '100%',
|
|
margin: '0 0 10 0'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'fontFamily',
|
|
reference: 'fontFamily',
|
|
emptyText: Proxmox.Utils.defaultText,
|
|
fieldLabel: gettext('Font-Family')
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
emptyText: Proxmox.Utils.defaultText,
|
|
name: 'fontSize',
|
|
reference: 'fontSize',
|
|
minValue: 1,
|
|
fieldLabel: gettext('Font-Size')
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'letterSpacing',
|
|
reference: 'letterSpacing',
|
|
emptyText: Proxmox.Utils.defaultText,
|
|
fieldLabel: gettext('Letter Spacing')
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'lineHeight',
|
|
minValue: 0.1,
|
|
reference: 'lineHeight',
|
|
emptyText: Proxmox.Utils.defaultText,
|
|
fieldLabel: gettext('Line Height')
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
layout: {
|
|
type: 'hbox',
|
|
pack: 'end'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'button',
|
|
reference: 'xtermreset',
|
|
disabled: true,
|
|
text: gettext('Reset')
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
reference: 'xtermsave',
|
|
disabled: true,
|
|
text: gettext('Save')
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}]
|
|
}],
|
|
|
|
onShow: function() {
|
|
var me = this;
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.panel.StartupInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
onlineHelp: 'qm_startup_and_shutdown',
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var res = PVE.Parser.printStartup(values);
|
|
|
|
if (res === undefined || res === '') {
|
|
return { 'delete': 'startup' };
|
|
}
|
|
|
|
return { startup: res };
|
|
},
|
|
|
|
setStartup: function(value) {
|
|
var me = this;
|
|
|
|
var startup = PVE.Parser.parseStartup(value);
|
|
if (startup) {
|
|
me.setValues(startup);
|
|
}
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.items = [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'order',
|
|
defaultValue: '',
|
|
emptyText: 'any',
|
|
fieldLabel: gettext('Start/Shutdown order')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'up',
|
|
defaultValue: '',
|
|
emptyText: 'default',
|
|
fieldLabel: gettext('Startup delay')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'down',
|
|
defaultValue: '',
|
|
emptyText: 'default',
|
|
fieldLabel: gettext('Shutdown timeout')
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.window.StartupEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: 'widget.pveWindowStartupEdit',
|
|
onlineHelp: undefined,
|
|
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
var ipanelConfig = me.onlineHelp ? {onlineHelp: me.onlineHelp} : {};
|
|
var ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig);
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('Start/Shutdown order'),
|
|
fieldDefaults: {
|
|
labelWidth: 120
|
|
},
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
var i, confid;
|
|
me.vmconfig = response.result.data;
|
|
ipanel.setStartup(me.vmconfig.startup);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.ceph.Install', {
|
|
extend: 'Ext.window.Window',
|
|
xtype: 'pveCephInstallWindow',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
width: 220,
|
|
header: false,
|
|
resizable: false,
|
|
draggable: false,
|
|
modal: true,
|
|
nodename: undefined,
|
|
shadow: false,
|
|
border: false,
|
|
bodyBorder: false,
|
|
closable: false,
|
|
cls: 'install-mask',
|
|
bodyCls: 'install-mask',
|
|
layout: {
|
|
align: 'stretch',
|
|
pack: 'center',
|
|
type: 'vbox'
|
|
},
|
|
viewModel: {
|
|
data: {
|
|
cephVersion: 'nautilus',
|
|
isInstalled: false
|
|
},
|
|
formulas: {
|
|
buttonText: function (get){
|
|
if (get('isInstalled')) {
|
|
return gettext('Configure Ceph');
|
|
} else {
|
|
return gettext('Install Ceph-') + get('cephVersion');
|
|
}
|
|
},
|
|
windowText: function (get) {
|
|
if (get('isInstalled')) {
|
|
return '<p class="install-mask">' +
|
|
Ext.String.format(gettext('{0} is not initialized.'), 'Ceph') + ' '+
|
|
gettext('You need to create a 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() {
|
|
var me = this.up('pveCephInstallWindow');
|
|
var win = Ext.create('PVE.ceph.CephInstallWizard',{
|
|
nodename: me.nodename
|
|
});
|
|
win.getViewModel().set('isInstalled', this.getViewModel().get('isInstalled'));
|
|
win.show();
|
|
me.mon(win,'beforeClose', function(){
|
|
me.fireEvent("cephInstallWindowClosed");
|
|
me.close();
|
|
});
|
|
|
|
}
|
|
}
|
|
]
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.FirewallEnableEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveFirewallEnableEdit'],
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
subject: gettext('Firewall'),
|
|
cbindData: {
|
|
defaultValue: 0
|
|
},
|
|
width: 350,
|
|
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'enable',
|
|
uncheckedValue: 0,
|
|
cbind: {
|
|
defaultValue: '{defaultValue}',
|
|
checked: '{defaultValue}'
|
|
},
|
|
deleteDefaultValue: false,
|
|
fieldLabel: gettext('Firewall')
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
name: 'warning',
|
|
userCls: 'pve-hint',
|
|
value: gettext('Warning: Firewall still disabled at datacenter level!'),
|
|
hidden: true
|
|
}
|
|
],
|
|
|
|
beforeShow: function() {
|
|
var me = this;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/cluster/firewall/options',
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
if (!response.result.data.enable) {
|
|
me.down('displayfield[name=warning]').setVisible(true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.FirewallLograteInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveFirewallLograteInputPanel',
|
|
|
|
viewModel: {},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'enable',
|
|
reference: 'enable',
|
|
fieldLabel: gettext('Enable'),
|
|
value: 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) {
|
|
var me = this;
|
|
|
|
var vals = {};
|
|
vals.enable = values.enable !== undefined ? 1 : 0;
|
|
vals.rate = values.rate + '/' + values.unit;
|
|
vals.burst = values.burst;
|
|
var properties = PVE.Parser.printPropertyString(vals, undefined);
|
|
if (properties == '') {
|
|
return { 'delete': 'log_ratelimit' };
|
|
}
|
|
return { log_ratelimit: properties };
|
|
},
|
|
|
|
setValues: function(values) {
|
|
var me = this;
|
|
|
|
var properties = {};
|
|
if (values.log_ratelimit !== undefined) {
|
|
properties = PVE.Parser.parsePropertyString(values.log_ratelimit, '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
|
|
});
|
|
Ext.define('PVE.panel.NotesView', {
|
|
extend: 'Ext.panel.Panel',
|
|
xtype: 'pveNotesView',
|
|
|
|
title: gettext("Notes"),
|
|
bodyStyle: 'white-space:pre',
|
|
bodyPadding: 10,
|
|
scrollable: true,
|
|
|
|
tbar: {
|
|
itemId: 'tbar',
|
|
hidden: true,
|
|
items: [
|
|
{
|
|
text: gettext('Edit'),
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
me.run_editor();
|
|
}
|
|
}
|
|
]
|
|
},
|
|
|
|
run_editor: function() {
|
|
var me = this;
|
|
var win = Ext.create('PVE.window.NotesEdit', {
|
|
pveSelNode: me.pveSelNode,
|
|
url: me.url
|
|
});
|
|
win.show();
|
|
win.on('destroy', me.load, me);
|
|
},
|
|
|
|
load: function() {
|
|
var me = this;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: me.url,
|
|
waitMsgTarget: me,
|
|
failure: function(response, opts) {
|
|
me.update(gettext('Error') + " " + response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
var data = response.result.data.description || '';
|
|
me.update(Ext.htmlEncode(data));
|
|
}
|
|
});
|
|
},
|
|
|
|
listeners: {
|
|
render: function(c) {
|
|
var me = this;
|
|
me.getEl().on('dblclick', me.run_editor, me);
|
|
}
|
|
},
|
|
|
|
tools: [{
|
|
type: 'gear',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
me.run_editor();
|
|
}
|
|
}],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var type = me.pveSelNode.data.type;
|
|
if (!Ext.Array.contains(['node', 'qemu', 'lxc'], type)) {
|
|
throw 'invalid type specified';
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid && type !== 'node') {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
me.url = '/api2/extjs/nodes/' + nodename + '/';
|
|
|
|
// add the type specific path if qemu/lxc
|
|
if (type === 'qemu' || type === 'lxc') {
|
|
me.url += type + '/' + vmid + '/';
|
|
}
|
|
|
|
me.url += 'config';
|
|
|
|
me.callParent();
|
|
if (type === 'node') {
|
|
me.down('#tbar').setVisible(true);
|
|
}
|
|
me.load();
|
|
}
|
|
});
|
|
Ext.define('PVE.grid.ResourceGrid', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: ['widget.pveResourceGrid'],
|
|
|
|
border: false,
|
|
defaultSorter: {
|
|
property: 'type',
|
|
direction: 'ASC'
|
|
},
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var rstore = PVE.data.ResourceStore;
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
var coldef = rstore.defaultColumns();
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
model: 'PVEResources',
|
|
sorters: me.defaultSorter,
|
|
proxy: { type: 'memory' }
|
|
});
|
|
|
|
var textfilter = '';
|
|
|
|
var textfilter_match = function(item) {
|
|
var match = false;
|
|
Ext.each(['name', 'storage', 'node', 'type', 'text'], function(field) {
|
|
var v = item.data[field];
|
|
if (v !== undefined) {
|
|
v = v.toLowerCase();
|
|
if (v.indexOf(textfilter) >= 0) {
|
|
match = true;
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
return match;
|
|
};
|
|
|
|
var updateGrid = function() {
|
|
|
|
var filterfn = me.viewFilter ? me.viewFilter.filterfn : null;
|
|
|
|
//console.log("START GRID UPDATE " + me.viewFilter);
|
|
|
|
store.suspendEvents();
|
|
|
|
var nodeidx = {};
|
|
var gather_child_nodes = function(cn) {
|
|
if (!cn) {
|
|
return;
|
|
}
|
|
var cs = cn.childNodes;
|
|
if (!cs) {
|
|
return;
|
|
}
|
|
var len = cs.length, i = 0, n, res;
|
|
|
|
for (; i < len; i++) {
|
|
var child = cs[i];
|
|
var orgnode = rstore.data.get(child.data.id);
|
|
if (orgnode) {
|
|
if ((!filterfn || filterfn(child)) &&
|
|
(!textfilter || textfilter_match(child))) {
|
|
nodeidx[child.data.id] = orgnode;
|
|
}
|
|
}
|
|
gather_child_nodes(child);
|
|
}
|
|
};
|
|
gather_child_nodes(me.pveSelNode);
|
|
|
|
// remove vanished items
|
|
var rmlist = [];
|
|
store.each(function(olditem) {
|
|
var item = nodeidx[olditem.data.id];
|
|
if (!item) {
|
|
//console.log("GRID REM UID: " + olditem.data.id);
|
|
rmlist.push(olditem);
|
|
}
|
|
});
|
|
|
|
if (rmlist.length) {
|
|
store.remove(rmlist);
|
|
}
|
|
|
|
// add new items
|
|
var addlist = [];
|
|
var key;
|
|
for (key in nodeidx) {
|
|
if (nodeidx.hasOwnProperty(key)) {
|
|
var item = nodeidx[key];
|
|
|
|
// getById() use find(), which is slow (ExtJS4 DP5)
|
|
//var olditem = store.getById(item.data.id);
|
|
var olditem = store.data.get(item.data.id);
|
|
|
|
if (!olditem) {
|
|
//console.log("GRID ADD UID: " + item.data.id);
|
|
var info = Ext.apply({}, item.data);
|
|
var child = Ext.create(store.model, info);
|
|
addlist.push(item);
|
|
continue;
|
|
}
|
|
// try to detect changes
|
|
var changes = false;
|
|
var fieldkeys = PVE.data.ResourceStore.fieldNames;
|
|
var fieldcount = fieldkeys.length;
|
|
var fieldind;
|
|
for (fieldind = 0; fieldind < fieldcount; fieldind++) {
|
|
var field = fieldkeys[fieldind];
|
|
if (field != 'id' && item.data[field] != olditem.data[field]) {
|
|
changes = true;
|
|
//console.log("changed item " + item.id + " " + field + " " + item.data[field] + " != " + olditem.data[field]);
|
|
olditem.beginEdit();
|
|
olditem.set(field, item.data[field]);
|
|
}
|
|
}
|
|
if (changes) {
|
|
olditem.endEdit(true);
|
|
olditem.commit(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (addlist.length) {
|
|
store.add(addlist);
|
|
}
|
|
|
|
store.sort();
|
|
|
|
store.resumeEvents();
|
|
|
|
store.fireEvent('refresh', store);
|
|
|
|
//console.log("END GRID UPDATE");
|
|
};
|
|
|
|
var filter_task = new Ext.util.DelayedTask(function(){
|
|
updateGrid();
|
|
});
|
|
|
|
var load_cb = function() {
|
|
updateGrid();
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
stateful: true,
|
|
stateId: 'grid-resource',
|
|
tbar: [
|
|
'->',
|
|
gettext('Search') + ':', ' ',
|
|
{
|
|
xtype: 'textfield',
|
|
width: 200,
|
|
value: textfilter,
|
|
enableKeyEvents: true,
|
|
listeners: {
|
|
keyup: function(field, e) {
|
|
var v = field.getValue();
|
|
textfilter = v.toLowerCase();
|
|
filter_task.delay(500);
|
|
}
|
|
}
|
|
}
|
|
],
|
|
viewConfig: {
|
|
stripeRows: true
|
|
},
|
|
listeners: {
|
|
itemcontextmenu: PVE.Utils.createCmdMenu,
|
|
itemdblclick: function(v, record) {
|
|
var ws = me.up('pveStdWorkspace');
|
|
ws.selectById(record.data.id);
|
|
},
|
|
destroy: function() {
|
|
rstore.un("load", load_cb);
|
|
}
|
|
},
|
|
columns: coldef
|
|
});
|
|
me.callParent();
|
|
updateGrid();
|
|
rstore.on("load", load_cb);
|
|
}
|
|
});
|
|
Ext.define('PVE.pool.AddVM', {
|
|
extend: 'Proxmox.window.Edit',
|
|
width: 600,
|
|
height: 400,
|
|
isAdd: true,
|
|
isCreate: true,
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
if (!me.pool) {
|
|
throw "no pool specified";
|
|
}
|
|
|
|
me.url = "/pools/" + me.pool;
|
|
me.method = 'PUT';
|
|
|
|
var vmsField = Ext.create('Ext.form.field.Text', {
|
|
name: 'vms',
|
|
hidden: true,
|
|
allowBlank: false
|
|
});
|
|
|
|
var vmStore = Ext.create('Ext.data.Store', {
|
|
model: 'PVEResources',
|
|
sorters: [
|
|
{
|
|
property: 'vmid',
|
|
order: 'ASC'
|
|
}
|
|
],
|
|
filters: [
|
|
function(item) {
|
|
return ((item.data.type === 'lxc' || item.data.type === 'qemu') && item.data.pool === '');
|
|
}
|
|
]
|
|
});
|
|
|
|
var vmGrid = Ext.create('widget.grid',{
|
|
store: vmStore,
|
|
border: true,
|
|
height: 300,
|
|
scrollable: true,
|
|
selModel: {
|
|
selType: 'checkboxmodel',
|
|
mode: 'SIMPLE',
|
|
listeners: {
|
|
selectionchange: function(model, selected, opts) {
|
|
var selectedVms = [];
|
|
selected.forEach(function(vm) {
|
|
selectedVms.push(vm.data.vmid);
|
|
});
|
|
vmsField.setValue(selectedVms);
|
|
}
|
|
}
|
|
},
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
dataIndex: 'vmid',
|
|
width: 60
|
|
},
|
|
{
|
|
header: gettext('Node'),
|
|
dataIndex: 'node'
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'uptime',
|
|
renderer: function(value) {
|
|
if (value) {
|
|
return Proxmox.Utils.runningText;
|
|
} else {
|
|
return Proxmox.Utils.stoppedText;
|
|
}
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
dataIndex: 'type'
|
|
}
|
|
]
|
|
});
|
|
Ext.apply(me, {
|
|
subject: gettext('Virtual Machine'),
|
|
items: [ vmsField, vmGrid ]
|
|
});
|
|
|
|
me.callParent();
|
|
vmStore.load();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.pool.AddStorage', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
if (!me.pool) {
|
|
throw "no pool specified";
|
|
}
|
|
|
|
me.isCreate = true;
|
|
me.isAdd = true;
|
|
me.url = "/pools/" + me.pool;
|
|
me.method = 'PUT';
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Storage'),
|
|
width: 350,
|
|
items: [
|
|
{
|
|
xtype: 'pveStorageSelector',
|
|
name: 'storage',
|
|
nodename: 'localhost',
|
|
autoSelect: false,
|
|
value: '',
|
|
fieldLabel: gettext("Storage")
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.grid.PoolMembers', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: ['widget.pvePoolMembers'],
|
|
|
|
// fixme: dynamic status update ?
|
|
|
|
stateful: true,
|
|
stateId: 'grid-pool-members',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.pool) {
|
|
throw "no pool specified";
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
model: 'PVEResources',
|
|
sorters: [
|
|
{
|
|
property : 'type',
|
|
direction: 'ASC'
|
|
}
|
|
],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
root: 'data.members',
|
|
url: "/api2/json/pools/" + me.pool
|
|
}
|
|
});
|
|
|
|
var coldef = PVE.data.ResourceStore.defaultColumns();
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var remove_btn = new Proxmox.button.Button({
|
|
text: gettext('Remove'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
confirmMsg: function (rec) {
|
|
return Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
|
|
"'" + rec.data.id + "'");
|
|
},
|
|
handler: function(btn, event, rec) {
|
|
var params = { 'delete': 1 };
|
|
if (rec.data.type === 'storage') {
|
|
params.storage = rec.data.storage;
|
|
} else if (rec.data.type === 'qemu' || rec.data.type === 'lxc' || rec.data.type === 'openvz') {
|
|
params.vms = rec.data.vmid;
|
|
} else {
|
|
throw "unknown resource type";
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/pools/' + me.pool,
|
|
method: 'PUT',
|
|
params: params,
|
|
waitMsgTarget: me,
|
|
callback: function() {
|
|
reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
menu: new Ext.menu.Menu({
|
|
items: [
|
|
{
|
|
text: gettext('Virtual Machine'),
|
|
iconCls: 'pve-itype-icon-qemu',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.pool.AddVM', { pool: me.pool });
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Storage'),
|
|
iconCls: 'pve-itype-icon-storage',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.pool.AddStorage', { pool: me.pool });
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
}
|
|
]
|
|
})
|
|
},
|
|
remove_btn
|
|
],
|
|
viewConfig: {
|
|
stripeRows: true
|
|
},
|
|
columns: coldef,
|
|
listeners: {
|
|
itemcontextmenu: PVE.Utils.createCmdMenu,
|
|
itemdblclick: function(v, record) {
|
|
var ws = me.up('pveStdWorkspace');
|
|
ws.selectById(record.data.id);
|
|
},
|
|
activate: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.form.FWMacroSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: 'widget.pveFWMacroSelector',
|
|
allowBlank: true,
|
|
autoSelect: false,
|
|
valueField: 'macro',
|
|
displayField: 'macro',
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Macro'),
|
|
dataIndex: 'macro',
|
|
hideable: false,
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Description'),
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 1,
|
|
dataIndex: 'descr'
|
|
}
|
|
]
|
|
},
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
autoLoad: true,
|
|
fields: [ 'macro', 'descr' ],
|
|
idProperty: 'macro',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/cluster/firewall/macros"
|
|
},
|
|
sorters: {
|
|
property: 'macro',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.FirewallRulePanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
|
|
allow_iface: false,
|
|
|
|
list_refs_url: undefined,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
// hack: editable ComboGrid returns nothing when empty, so we need to set ''
|
|
// Also, disabled text fields return nothing, so we need to set ''
|
|
|
|
Ext.Array.each(['source', 'dest', 'macro', 'proto', 'sport', 'dport', 'log'], function(key) {
|
|
if (values[key] === undefined) {
|
|
values[key] = '';
|
|
}
|
|
});
|
|
|
|
delete values.modified_marker;
|
|
|
|
return values;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.list_refs_url) {
|
|
throw "no list_refs_url specified";
|
|
}
|
|
|
|
me.column1 = [
|
|
{
|
|
// hack: we use this field to mark the form 'dirty' when the
|
|
// record has errors- so that the user can safe the unmodified
|
|
// form again.
|
|
xtype: 'hiddenfield',
|
|
name: 'modified_marker',
|
|
value: ''
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'type',
|
|
value: 'in',
|
|
comboItems: [['in', 'in'], ['out', 'out']],
|
|
fieldLabel: gettext('Direction'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'action',
|
|
value: 'ACCEPT',
|
|
comboItems: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']],
|
|
fieldLabel: gettext('Action'),
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
if (me.allow_iface) {
|
|
me.column1.push({
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'iface',
|
|
deleteEmpty: !me.isCreate,
|
|
value: '',
|
|
fieldLabel: gettext('Interface')
|
|
});
|
|
} else {
|
|
me.column1.push({
|
|
xtype: 'displayfield',
|
|
fieldLabel: '',
|
|
value: ''
|
|
});
|
|
}
|
|
|
|
me.column1.push(
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: '',
|
|
height: 7,
|
|
value: ''
|
|
},
|
|
{
|
|
xtype: 'pveIPRefSelector',
|
|
name: 'source',
|
|
autoSelect: false,
|
|
editable: true,
|
|
base_url: me.list_refs_url,
|
|
value: '',
|
|
fieldLabel: gettext('Source')
|
|
|
|
},
|
|
{
|
|
xtype: 'pveIPRefSelector',
|
|
name: 'dest',
|
|
autoSelect: false,
|
|
editable: true,
|
|
base_url: me.list_refs_url,
|
|
value: '',
|
|
fieldLabel: gettext('Destination')
|
|
}
|
|
);
|
|
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'enable',
|
|
checked: false,
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Enable')
|
|
},
|
|
{
|
|
xtype: 'pveFWMacroSelector',
|
|
name: 'macro',
|
|
fieldLabel: gettext('Macro'),
|
|
editable: true,
|
|
allowBlank: true,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (value === null) {
|
|
me.down('field[name=proto]').setDisabled(false);
|
|
me.down('field[name=sport]').setDisabled(false);
|
|
me.down('field[name=dport]').setDisabled(false);
|
|
} else {
|
|
me.down('field[name=proto]').setDisabled(true);
|
|
me.down('field[name=proto]').setValue('');
|
|
me.down('field[name=sport]').setDisabled(true);
|
|
me.down('field[name=sport]').setValue('');
|
|
me.down('field[name=dport]').setDisabled(true);
|
|
me.down('field[name=dport]').setValue('');
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'pveIPProtocolSelector',
|
|
name: 'proto',
|
|
autoSelect: false,
|
|
editable: true,
|
|
value: '',
|
|
fieldLabel: gettext('Protocol')
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: '',
|
|
height: 7,
|
|
value: ''
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'sport',
|
|
value: '',
|
|
fieldLabel: gettext('Source port')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'dport',
|
|
value: '',
|
|
fieldLabel: gettext('Dest. port')
|
|
}
|
|
];
|
|
|
|
me.advancedColumn1 = [
|
|
{
|
|
xtype: 'pveFirewallLogLevels'
|
|
}
|
|
];
|
|
|
|
me.columnB = [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
value: '',
|
|
fieldLabel: gettext('Comment')
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.FirewallRuleEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
base_url: undefined,
|
|
list_refs_url: undefined,
|
|
|
|
allow_iface: false,
|
|
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
if (!me.base_url) {
|
|
throw "no base_url specified";
|
|
}
|
|
if (!me.list_refs_url) {
|
|
throw "no list_refs_url specified";
|
|
}
|
|
|
|
me.isCreate = (me.rule_pos === undefined);
|
|
|
|
if (me.isCreate) {
|
|
me.url = '/api2/extjs' + me.base_url;
|
|
me.method = 'POST';
|
|
} else {
|
|
me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
|
|
me.method = 'PUT';
|
|
}
|
|
|
|
var ipanel = Ext.create('PVE.FirewallRulePanel', {
|
|
isCreate: me.isCreate,
|
|
list_refs_url: me.list_refs_url,
|
|
allow_iface: me.allow_iface,
|
|
rule_pos: me.rule_pos
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Rule'),
|
|
isAdd: true,
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var values = response.result.data;
|
|
ipanel.setValues(values);
|
|
if (values.errors) {
|
|
var field = me.query('[isFormField][name=modified_marker]')[0];
|
|
field.setValue(1);
|
|
Ext.Function.defer(function() {
|
|
var form = ipanel.up('form').getForm();
|
|
form.markInvalid(values.errors);
|
|
}, 100);
|
|
}
|
|
}
|
|
});
|
|
} else if (me.rec) {
|
|
ipanel.setValues(me.rec.data);
|
|
}
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.FirewallGroupRuleEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
base_url: undefined,
|
|
|
|
allow_iface: false,
|
|
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
me.isCreate = (me.rule_pos === undefined);
|
|
|
|
if (me.isCreate) {
|
|
me.url = '/api2/extjs' + me.base_url;
|
|
me.method = 'POST';
|
|
} else {
|
|
me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString();
|
|
me.method = 'PUT';
|
|
}
|
|
|
|
var column1 = [
|
|
{
|
|
xtype: 'hiddenfield',
|
|
name: 'type',
|
|
value: 'group'
|
|
},
|
|
{
|
|
xtype: 'pveSecurityGroupsSelector',
|
|
name: 'action',
|
|
value: '',
|
|
fieldLabel: gettext('Security Group'),
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
if (me.allow_iface) {
|
|
column1.push({
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'iface',
|
|
deleteEmpty: !me.isCreate,
|
|
value: '',
|
|
fieldLabel: gettext('Interface')
|
|
});
|
|
}
|
|
|
|
var ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
isCreate: me.isCreate,
|
|
column1: column1,
|
|
column2: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'enable',
|
|
checked: false,
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Enable')
|
|
}
|
|
],
|
|
columnB: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
value: '',
|
|
fieldLabel: gettext('Comment')
|
|
}
|
|
]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Rule'),
|
|
isAdd: true,
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var values = response.result.data;
|
|
ipanel.setValues(values);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.FirewallRules', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pveFirewallRules',
|
|
|
|
onlineHelp: 'chapter_pve_firewall',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-firewall-rules',
|
|
|
|
base_url: undefined,
|
|
list_refs_url: undefined,
|
|
|
|
addBtn: undefined,
|
|
removeBtn: undefined,
|
|
editBtn: undefined,
|
|
groupBtn: undefined,
|
|
|
|
tbar_prefix: undefined,
|
|
|
|
allow_groups: true,
|
|
allow_iface: false,
|
|
|
|
setBaseUrl: function(url) {
|
|
var me = this;
|
|
|
|
me.base_url = url;
|
|
|
|
if (url === undefined) {
|
|
me.addBtn.setDisabled(true);
|
|
if (me.groupBtn) {
|
|
me.groupBtn.setDisabled(true);
|
|
}
|
|
me.store.removeAll();
|
|
} else {
|
|
me.addBtn.setDisabled(false);
|
|
me.removeBtn.baseurl = url + '/';
|
|
if (me.groupBtn) {
|
|
me.groupBtn.setDisabled(false);
|
|
}
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: '/api2/json' + url
|
|
});
|
|
|
|
me.store.load();
|
|
}
|
|
},
|
|
|
|
moveRule: function(from, to) {
|
|
var me = this;
|
|
|
|
if (!me.base_url) {
|
|
return;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: me.base_url + "/" + from,
|
|
method: 'PUT',
|
|
params: { moveto: to },
|
|
waitMsgTarget: me,
|
|
failure: function(response, options) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
callback: function() {
|
|
me.store.load();
|
|
}
|
|
});
|
|
},
|
|
|
|
updateRule: function(rule) {
|
|
var me = this;
|
|
|
|
if (!me.base_url) {
|
|
return;
|
|
}
|
|
|
|
rule.enable = rule.enable ? 1 : 0;
|
|
|
|
var pos = rule.pos;
|
|
delete rule.pos;
|
|
delete rule.errors;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: me.base_url + '/' + pos.toString(),
|
|
method: 'PUT',
|
|
params: rule,
|
|
waitMsgTarget: me,
|
|
failure: function(response, options) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
callback: function() {
|
|
me.store.load();
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
if (!me.list_refs_url) {
|
|
throw "no list_refs_url specified";
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store',{
|
|
model: 'pve-fw-rule'
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var type = rec.data.type;
|
|
|
|
var editor;
|
|
if (type === 'in' || type === 'out') {
|
|
editor = 'PVE.FirewallRuleEdit';
|
|
} else if (type === 'group') {
|
|
editor = 'PVE.FirewallGroupRuleEdit';
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create(editor, {
|
|
digest: rec.data.digest,
|
|
allow_iface: me.allow_iface,
|
|
base_url: me.base_url,
|
|
list_refs_url: me.list_refs_url,
|
|
rule_pos: rec.data.pos
|
|
});
|
|
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
me.editBtn = Ext.create('Proxmox.button.Button',{
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
me.addBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Add'),
|
|
disabled: true,
|
|
handler: function() {
|
|
var win = Ext.create('PVE.FirewallRuleEdit', {
|
|
allow_iface: me.allow_iface,
|
|
base_url: me.base_url,
|
|
list_refs_url: me.list_refs_url
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
var run_copy_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var type = rec.data.type;
|
|
|
|
|
|
if (!(type === 'in' || type === 'out')) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.FirewallRuleEdit', {
|
|
allow_iface: me.allow_iface,
|
|
base_url: me.base_url,
|
|
list_refs_url: me.list_refs_url,
|
|
rec: rec
|
|
});
|
|
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
me.copyBtn = Ext.create('Proxmox.button.Button',{
|
|
text: gettext('Copy'),
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
return (rec.data.type === 'in' || rec.data.type === 'out');
|
|
},
|
|
disabled: true,
|
|
handler: run_copy_editor
|
|
});
|
|
|
|
if (me.allow_groups) {
|
|
me.groupBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Insert') + ': ' +
|
|
gettext('Security Group'),
|
|
disabled: true,
|
|
handler: function() {
|
|
var win = Ext.create('PVE.FirewallGroupRuleEdit', {
|
|
allow_iface: me.allow_iface,
|
|
base_url: me.base_url
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
});
|
|
}
|
|
|
|
me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton',{
|
|
selModel: sm,
|
|
baseurl: me.base_url + '/',
|
|
confirmMsg: false,
|
|
getRecordName: function(rec) {
|
|
var rule = rec.data;
|
|
return rule.pos.toString() +
|
|
'?digest=' + encodeURIComponent(rule.digest);
|
|
},
|
|
callback: function() {
|
|
me.store.load();
|
|
}
|
|
});
|
|
|
|
var tbar = me.tbar_prefix ? [ me.tbar_prefix ] : [];
|
|
tbar.push(me.addBtn, me.copyBtn);
|
|
if (me.groupBtn) {
|
|
tbar.push(me.groupBtn);
|
|
}
|
|
tbar.push(me.removeBtn, me.editBtn);
|
|
|
|
var render_errors = function(name, value, metaData, record) {
|
|
var errors = record.data.errors;
|
|
if (errors && errors[name]) {
|
|
metaData.tdCls = 'proxmox-invalid-row';
|
|
var html = '<p>' + Ext.htmlEncode(errors[name]) + '</p>';
|
|
metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' +
|
|
html.replace(/\"/g,'"') + '"';
|
|
}
|
|
return value;
|
|
};
|
|
|
|
var columns = [
|
|
{
|
|
// similar to xtype: 'rownumberer',
|
|
dataIndex: 'pos',
|
|
resizable: false,
|
|
width: 23,
|
|
sortable: false,
|
|
align: 'right',
|
|
hideable: false,
|
|
menuDisabled: true,
|
|
renderer: function(value, metaData, record, rowIdx, colIdx, store) {
|
|
metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
|
|
if (value >= 0) {
|
|
return value;
|
|
}
|
|
return '';
|
|
}
|
|
},
|
|
{
|
|
xtype: 'checkcolumn',
|
|
header: gettext('Enable'),
|
|
dataIndex: 'enable',
|
|
listeners: {
|
|
checkchange: function(column, recordIndex, checked) {
|
|
var record = me.getStore().getData().items[recordIndex];
|
|
record.commit();
|
|
var data = {};
|
|
Ext.Array.forEach(record.getFields(), function(field) {
|
|
data[field.name] = record.get(field.name);
|
|
});
|
|
if (!me.allow_iface || !data.iface) {
|
|
delete data.iface;
|
|
}
|
|
me.updateRule(data);
|
|
}
|
|
},
|
|
width: 50
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
dataIndex: 'type',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('type', value, metaData, record);
|
|
},
|
|
width: 50
|
|
},
|
|
{
|
|
header: gettext('Action'),
|
|
dataIndex: 'action',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('action', value, metaData, record);
|
|
},
|
|
width: 80
|
|
},
|
|
{
|
|
header: gettext('Macro'),
|
|
dataIndex: 'macro',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('macro', value, metaData, record);
|
|
},
|
|
width: 80
|
|
}
|
|
];
|
|
|
|
if (me.allow_iface) {
|
|
columns.push({
|
|
header: gettext('Interface'),
|
|
dataIndex: 'iface',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('iface', value, metaData, record);
|
|
},
|
|
width: 80
|
|
});
|
|
}
|
|
|
|
columns.push(
|
|
{
|
|
header: gettext('Source'),
|
|
dataIndex: 'source',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('source', value, metaData, record);
|
|
},
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Destination'),
|
|
dataIndex: 'dest',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('dest', value, metaData, record);
|
|
},
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Protocol'),
|
|
dataIndex: 'proto',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('proto', value, metaData, record);
|
|
},
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Dest. port'),
|
|
dataIndex: 'dport',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('dport', value, metaData, record);
|
|
},
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Source port'),
|
|
dataIndex: 'sport',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('sport', value, metaData, record);
|
|
},
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Log level'),
|
|
dataIndex: 'log',
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('log', value, metaData, record);
|
|
},
|
|
width: 100
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
flex: 1,
|
|
renderer: function(value, metaData, record) {
|
|
return render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record);
|
|
}
|
|
}
|
|
);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: tbar,
|
|
viewConfig: {
|
|
plugins: [
|
|
{
|
|
ptype: 'gridviewdragdrop',
|
|
dragGroup: 'FWRuleDDGroup',
|
|
dropGroup: 'FWRuleDDGroup'
|
|
}
|
|
],
|
|
listeners: {
|
|
beforedrop: function(node, data, dropRec, dropPosition) {
|
|
if (!dropRec) {
|
|
return false; // empty view
|
|
}
|
|
var moveto = dropRec.get('pos');
|
|
if (dropPosition === 'after') {
|
|
moveto++;
|
|
}
|
|
var pos = data.records[0].get('pos');
|
|
me.moveRule(pos, moveto);
|
|
return 0;
|
|
},
|
|
itemdblclick: run_editor
|
|
}
|
|
},
|
|
sortableColumns: false,
|
|
columns: columns
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (me.base_url) {
|
|
me.setBaseUrl(me.base_url); // load
|
|
}
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-fw-rule', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ { name: 'enable', type: 'boolean' },
|
|
'type', 'action', 'macro', 'source', 'dest', 'proto', 'iface',
|
|
'dport', 'sport', 'comment', 'pos', 'digest', 'errors' ],
|
|
idProperty: 'pos'
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.FirewallAliasEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
base_url: undefined,
|
|
|
|
alias_name: undefined,
|
|
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
me.isCreate = (me.alias_name === undefined);
|
|
|
|
if (me.isCreate) {
|
|
me.url = '/api2/extjs' + me.base_url;
|
|
me.method = 'POST';
|
|
} else {
|
|
me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name;
|
|
me.method = 'PUT';
|
|
}
|
|
|
|
var items = [
|
|
{
|
|
xtype: 'textfield',
|
|
name: me.isCreate ? 'name' : 'rename',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'cidr',
|
|
fieldLabel: gettext('IP/CIDR'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
fieldLabel: gettext('Comment')
|
|
}
|
|
];
|
|
|
|
var ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
isCreate: me.isCreate,
|
|
items: items
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Alias'),
|
|
isAdd: true,
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var values = response.result.data;
|
|
values.rename = values.name;
|
|
ipanel.setValues(values);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
Ext.define('pve-fw-aliases', {
|
|
extend: 'Ext.data.Model',
|
|
|
|
fields: [ 'name', 'cidr', 'comment', 'digest' ],
|
|
idProperty: 'name'
|
|
});
|
|
|
|
Ext.define('PVE.FirewallAliases', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: ['widget.pveFirewallAliases'],
|
|
|
|
onlineHelp: 'pve_firewall_ip_aliases',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-firewall-aliases',
|
|
|
|
base_url: undefined,
|
|
|
|
title: gettext('Alias'),
|
|
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
if (!me.base_url) {
|
|
throw "missing base_url configuration";
|
|
}
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-fw-aliases',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json" + me.base_url
|
|
},
|
|
sorters: {
|
|
property: 'name',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var reload = function() {
|
|
var oldrec = sm.getSelection()[0];
|
|
store.load(function(records, operation, success) {
|
|
if (oldrec) {
|
|
var rec = store.findRecord('name', oldrec.data.name);
|
|
if (rec) {
|
|
sm.select(rec);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
var run_editor = function() {
|
|
var sm = me.getSelectionModel();
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.FirewallAliasEdit', {
|
|
base_url: me.base_url,
|
|
alias_name: rec.data.name
|
|
});
|
|
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
me.editBtn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
me.addBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Add'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.FirewallAliasEdit', {
|
|
base_url: me.base_url
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: me.base_url + '/',
|
|
callback: reload
|
|
});
|
|
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
tbar: [ me.addBtn, me.removeBtn, me.editBtn ],
|
|
selModel: sm,
|
|
columns: [
|
|
{ header: gettext('Name'), dataIndex: 'name', width: 100 },
|
|
{ header: gettext('IP/CIDR'), dataIndex: 'cidr', width: 100 },
|
|
{ header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 }
|
|
],
|
|
listeners: {
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
me.on('activate', reload);
|
|
}
|
|
});
|
|
Ext.define('PVE.FirewallOptions', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
alias: ['widget.pveFirewallOptions'],
|
|
|
|
fwtype: undefined, // 'dc', 'node' or 'vm'
|
|
|
|
base_url: undefined,
|
|
|
|
initComponent : function() {
|
|
/*jslint confusion: true */
|
|
|
|
var me = this;
|
|
|
|
if (!me.base_url) {
|
|
throw "missing base_url configuration";
|
|
}
|
|
|
|
if (me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm') {
|
|
if (me.fwtype === 'node') {
|
|
me.cwidth1 = 250;
|
|
}
|
|
} else {
|
|
throw "unknown firewall option type";
|
|
}
|
|
|
|
me.rows = {};
|
|
|
|
var add_boolean_row = function(name, text, defaultValue) {
|
|
me.add_boolean_row(name, text, { defaultValue: defaultValue });
|
|
};
|
|
var add_integer_row = function(name, text, minValue, labelWidth) {
|
|
me.add_integer_row(name, text, {
|
|
minValue: minValue,
|
|
deleteEmpty: true,
|
|
labelWidth: labelWidth,
|
|
renderer: function(value) {
|
|
if (value === undefined) {
|
|
return Proxmox.Utils.defaultText;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
});
|
|
};
|
|
|
|
var add_log_row = function(name, labelWidth) {
|
|
me.rows[name] = {
|
|
header: name,
|
|
required: true,
|
|
defaultValue: 'nolog',
|
|
editor: {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: name,
|
|
fieldDefaults: { labelWidth: labelWidth || 100 },
|
|
items: {
|
|
xtype: 'pveFirewallLogLevels',
|
|
name: name,
|
|
fieldLabel: name
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
if (me.fwtype === 'node') {
|
|
me.rows.enable = {
|
|
required: true,
|
|
defaultValue: 1,
|
|
header: gettext('Firewall'),
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: {
|
|
xtype: 'pveFirewallEnableEdit',
|
|
defaultValue: 1
|
|
}
|
|
};
|
|
add_boolean_row('nosmurfs', gettext('SMURFS filter'), 1);
|
|
add_boolean_row('tcpflags', gettext('TCP flags filter'), 0);
|
|
add_boolean_row('ndp', 'NDP', 1);
|
|
add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 32768, 120);
|
|
add_integer_row('nf_conntrack_tcp_timeout_established',
|
|
'nf_conntrack_tcp_timeout_established', 7875, 250);
|
|
add_log_row('log_level_in');
|
|
add_log_row('log_level_out');
|
|
add_log_row('tcp_flags_log_level', 120);
|
|
add_log_row('smurf_log_level');
|
|
} else if (me.fwtype === 'vm') {
|
|
me.rows.enable = {
|
|
required: true,
|
|
defaultValue: 0,
|
|
header: gettext('Firewall'),
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: {
|
|
xtype: 'pveFirewallEnableEdit',
|
|
defaultValue: 0
|
|
}
|
|
};
|
|
add_boolean_row('dhcp', 'DHCP', 1);
|
|
add_boolean_row('ndp', 'NDP', 1);
|
|
add_boolean_row('radv', gettext('Router Advertisement'), 0);
|
|
add_boolean_row('macfilter', gettext('MAC filter'), 1);
|
|
add_boolean_row('ipfilter', gettext('IP filter'), 0);
|
|
add_log_row('log_level_in');
|
|
add_log_row('log_level_out');
|
|
} else if (me.fwtype === 'dc') {
|
|
add_boolean_row('enable', gettext('Firewall'), 0);
|
|
add_boolean_row('ebtables', 'ebtables', 1);
|
|
me.rows.log_ratelimit = {
|
|
header: gettext('Log rate limit'),
|
|
required: true,
|
|
defaultValue: gettext('Default') + ' (enable=1,rate1/second,burst=5)',
|
|
editor: {
|
|
xtype: 'pveFirewallLograteEdit',
|
|
defaultValue: 'enable=1'
|
|
}
|
|
};
|
|
}
|
|
|
|
if (me.fwtype === 'dc' || me.fwtype === 'vm') {
|
|
me.rows.policy_in = {
|
|
header: gettext('Input Policy'),
|
|
required: true,
|
|
defaultValue: 'DROP',
|
|
editor: {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Input Policy'),
|
|
items: {
|
|
xtype: 'pveFirewallPolicySelector',
|
|
name: 'policy_in',
|
|
value: 'DROP',
|
|
fieldLabel: gettext('Input Policy')
|
|
}
|
|
}
|
|
};
|
|
|
|
me.rows.policy_out = {
|
|
header: gettext('Output Policy'),
|
|
required: true,
|
|
defaultValue: 'ACCEPT',
|
|
editor: {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Output Policy'),
|
|
items: {
|
|
xtype: 'pveFirewallPolicySelector',
|
|
name: 'policy_out',
|
|
value: 'ACCEPT',
|
|
fieldLabel: gettext('Output Policy')
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
var edit_btn = new Ext.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
handler: function() { me.run_editor(); }
|
|
});
|
|
|
|
var set_button_status = function() {
|
|
var sm = me.getSelectionModel();
|
|
var rec = sm.getSelection()[0];
|
|
|
|
if (!rec) {
|
|
edit_btn.disable();
|
|
return;
|
|
}
|
|
var rowdef = me.rows[rec.data.key];
|
|
edit_btn.setDisabled(!rowdef.editor);
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: "/api2/json" + me.base_url,
|
|
tbar: [ edit_btn ],
|
|
editorConfig: {
|
|
url: '/api2/extjs/' + me.base_url
|
|
},
|
|
listeners: {
|
|
itemdblclick: me.run_editor,
|
|
selectionchange: set_button_status
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.on('activate', me.rstore.startUpdate);
|
|
me.on('destroy', me.rstore.stopUpdate);
|
|
me.on('deactivate', me.rstore.stopUpdate);
|
|
}
|
|
});
|
|
|
|
|
|
Ext.define('PVE.FirewallLogLevels', {
|
|
extend: 'Proxmox.form.KVComboBox',
|
|
alias: ['widget.pveFirewallLogLevels'],
|
|
|
|
name: 'log',
|
|
fieldLabel: gettext('Log level'),
|
|
value: 'nolog',
|
|
comboItems: [['nolog', 'nolog'], ['emerg', 'emerg'], ['alert', 'alert'],
|
|
['crit', 'crit'], ['err', 'err'], ['warning', 'warning'],
|
|
['notice', 'notice'], ['info', 'info'], ['debug', 'debug']]
|
|
});
|
|
/*
|
|
* 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'],
|
|
|
|
statics: {
|
|
typeDefaults: {
|
|
node: {
|
|
iconCls: 'fa fa-building',
|
|
text: gettext('Nodes')
|
|
},
|
|
pool: {
|
|
iconCls: 'fa fa-tags',
|
|
text: gettext('Resource Pool')
|
|
},
|
|
storage: {
|
|
iconCls: 'fa fa-database',
|
|
text: gettext('Storage')
|
|
},
|
|
qemu: {
|
|
iconCls: 'fa fa-desktop',
|
|
text: gettext('Virtual Machine')
|
|
},
|
|
lxc: {
|
|
//iconCls: 'x-tree-node-lxc',
|
|
iconCls: 'fa fa-cube',
|
|
text: gettext('LXC Container')
|
|
},
|
|
template: {
|
|
iconCls: 'fa fa-file-o'
|
|
}
|
|
}
|
|
},
|
|
|
|
useArrows: true,
|
|
|
|
// private
|
|
nodeSortFn: function(node1, node2) {
|
|
var n1 = node1.data;
|
|
var n2 = node2.data;
|
|
|
|
if ((n1.groupbyid && n2.groupbyid) ||
|
|
!(n1.groupbyid || n2.groupbyid)) {
|
|
|
|
var tcmp;
|
|
|
|
var v1 = n1.type;
|
|
var v2 = n2.type;
|
|
|
|
if ((tcmp = v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)) != 0) {
|
|
return tcmp;
|
|
}
|
|
|
|
// numeric compare for VM IDs
|
|
// sort templates after regular VMs
|
|
if (v1 === 'qemu' || v1 === 'lxc') {
|
|
if (n1.template && !n2.template) {
|
|
return 1;
|
|
} else if (n2.template && !n1.template) {
|
|
return -1;
|
|
}
|
|
v1 = n1.vmid;
|
|
v2 = n2.vmid;
|
|
if ((tcmp = v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)) != 0) {
|
|
return tcmp;
|
|
}
|
|
}
|
|
|
|
return n1.id > n2.id ? 1 : (n1.id < n2.id ? -1 : 0);
|
|
} else if (n1.groupbyid) {
|
|
return -1;
|
|
} else if (n2.groupbyid) {
|
|
return 1;
|
|
}
|
|
},
|
|
|
|
// private: fast binary search
|
|
findInsertIndex: function(node, child, start, end) {
|
|
var me = this;
|
|
|
|
var diff = end - start;
|
|
|
|
var mid = start + (diff>>1);
|
|
|
|
if (diff <= 0) {
|
|
return start;
|
|
}
|
|
|
|
var res = me.nodeSortFn(child, node.childNodes[mid]);
|
|
if (res <= 0) {
|
|
return me.findInsertIndex(node, child, start, mid);
|
|
} else {
|
|
return me.findInsertIndex(node, child, mid + 1, end);
|
|
}
|
|
},
|
|
|
|
setIconCls: function(info) {
|
|
var me = this;
|
|
|
|
var cls = PVE.Utils.get_object_icon_class(info.type, info);
|
|
|
|
if (cls !== '') {
|
|
info.iconCls = cls;
|
|
}
|
|
},
|
|
|
|
// add additional elements to text
|
|
// at the moment only the usage indicator for storages
|
|
setText: function(info) {
|
|
var me = this;
|
|
|
|
var status = '';
|
|
if (info.type === 'storage') {
|
|
var maxdisk = info.maxdisk;
|
|
var disk = info.disk;
|
|
var usage = disk/maxdisk;
|
|
var cls = '';
|
|
if (usage <= 1.0 && usage >= 0.0) {
|
|
var height = (usage*100).toFixed(0);
|
|
var neg_height = (100-usage*100).toFixed(0);
|
|
status = '<div class="usage-wrapper">';
|
|
status += '<div class="usage-negative" style="height: ';
|
|
status += neg_height + '%"></div>';
|
|
status += '<div class="usage" style="height: '+ height +'%"></div>';
|
|
status += '</div> ';
|
|
}
|
|
}
|
|
|
|
info.text = status + info.text;
|
|
},
|
|
|
|
setToolTip: function(info) {
|
|
if (info.type === 'pool' || info.groupbyid !== undefined) {
|
|
return;
|
|
}
|
|
|
|
var qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)];
|
|
if (info.lock) {
|
|
qtips.push('Config locked (' + info.lock + ')');
|
|
}
|
|
if (info.hastate != 'unmanaged') {
|
|
qtips.push(gettext('HA State') + ": " + info.hastate);
|
|
}
|
|
|
|
info.qtip = qtips.join(', ');
|
|
},
|
|
|
|
// private
|
|
addChildSorted: function(node, info) {
|
|
var me = this;
|
|
|
|
me.setIconCls(info);
|
|
me.setText(info);
|
|
me.setToolTip(info);
|
|
|
|
var defaults;
|
|
if (info.groupbyid) {
|
|
info.text = info.groupbyid;
|
|
if (info.type === 'type') {
|
|
defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid];
|
|
if (defaults && defaults.text) {
|
|
info.text = defaults.text;
|
|
}
|
|
}
|
|
}
|
|
var child = Ext.create('PVETree', info);
|
|
|
|
var cs = node.childNodes;
|
|
var pos;
|
|
if (cs) {
|
|
pos = cs[me.findInsertIndex(node, child, 0, cs.length)];
|
|
}
|
|
|
|
node.insertBefore(child, pos);
|
|
|
|
return child;
|
|
},
|
|
|
|
// private
|
|
groupChild: function(node, info, groups, level) {
|
|
var me = this;
|
|
|
|
var groupby = groups[level];
|
|
var v = info[groupby];
|
|
|
|
if (v) {
|
|
var group = node.findChild('groupbyid', v);
|
|
if (!group) {
|
|
var groupinfo;
|
|
if (info.type === groupby) {
|
|
groupinfo = info;
|
|
} else {
|
|
groupinfo = {
|
|
type: groupby,
|
|
id : groupby + "/" + v
|
|
};
|
|
if (groupby !== 'type') {
|
|
groupinfo[groupby] = v;
|
|
}
|
|
}
|
|
groupinfo.leaf = false;
|
|
groupinfo.groupbyid = v;
|
|
group = me.addChildSorted(node, groupinfo);
|
|
}
|
|
if (info.type === groupby) {
|
|
return group;
|
|
}
|
|
if (group) {
|
|
return me.groupChild(group, info, groups, level + 1);
|
|
}
|
|
}
|
|
|
|
return me.addChildSorted(node, info);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var rstore = PVE.data.ResourceStore;
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
if (!me.viewFilter) {
|
|
me.viewFilter = {};
|
|
}
|
|
|
|
var pdata = {
|
|
dataIndex: {},
|
|
updateCount: 0
|
|
};
|
|
|
|
var store = Ext.create('Ext.data.TreeStore', {
|
|
model: 'PVETree',
|
|
root: {
|
|
expanded: true,
|
|
id: 'root',
|
|
text: gettext('Datacenter'),
|
|
iconCls: 'fa fa-server'
|
|
}
|
|
});
|
|
|
|
var stateid = 'rid';
|
|
|
|
var updateTree = function() {
|
|
var tmp;
|
|
|
|
store.suspendEvents();
|
|
|
|
var rootnode = me.store.getRootNode();
|
|
// remember selected node (and all parents)
|
|
var sm = me.getSelectionModel();
|
|
|
|
var lastsel = sm.getSelection()[0];
|
|
var reselect = false;
|
|
var parents = [];
|
|
var p = lastsel;
|
|
while (p && !!(p = p.parentNode)) {
|
|
parents.push(p);
|
|
}
|
|
|
|
var index = pdata.dataIndex;
|
|
|
|
var groups = me.viewFilter.groups || [];
|
|
var filterfn = me.viewFilter.filterfn;
|
|
|
|
// remove vanished or moved items
|
|
// update in place changed items
|
|
var key;
|
|
for (key in index) {
|
|
if (index.hasOwnProperty(key)) {
|
|
var olditem = index[key];
|
|
|
|
// getById() use find(), which is slow (ExtJS4 DP5)
|
|
//var item = rstore.getById(olditem.data.id);
|
|
var item = rstore.data.get(olditem.data.id);
|
|
|
|
var changed = false;
|
|
var moved = false;
|
|
if (item) {
|
|
// test if any grouping attributes changed
|
|
// this will also catch migrated nodes
|
|
// in server view
|
|
var i, len;
|
|
for (i = 0, len = groups.length; i < len; i++) {
|
|
var attr = groups[i];
|
|
if (item.data[attr] != olditem.data[attr]) {
|
|
//console.log("changed " + attr);
|
|
moved = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// explicitly check for node, since
|
|
// in some views, node is not a grouping
|
|
// attribute
|
|
if (!moved && item.data.node !== olditem.data.node) {
|
|
moved = true;
|
|
}
|
|
|
|
// tree item has been updated
|
|
var fields = [
|
|
'text', 'running', 'template', 'status',
|
|
'qmpstatus', 'hastate', 'lock'
|
|
];
|
|
|
|
var field;
|
|
for (i = 0; i < fields.length; i++) {
|
|
field = fields[i];
|
|
if (item.data[field] !== olditem.data[field]) {
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// fixme: also test filterfn()?
|
|
}
|
|
|
|
if (changed) {
|
|
olditem.beginEdit();
|
|
//console.log("REM UPDATE UID: " + key + " ITEM " + item.data.running);
|
|
var info = olditem.data;
|
|
Ext.apply(info, item.data);
|
|
me.setIconCls(info);
|
|
me.setText(info);
|
|
me.setToolTip(info);
|
|
olditem.commit();
|
|
}
|
|
if ((!item || moved) && olditem.isLeaf()) {
|
|
//console.log("REM UID: " + key + " ITEM " + olditem.data.id);
|
|
delete index[key];
|
|
var parentNode = olditem.parentNode;
|
|
// when the selected item disappears,
|
|
// we have to deselect it here, and reselect it
|
|
// later
|
|
if (lastsel && olditem.data.id === lastsel.data.id) {
|
|
reselect = true;
|
|
sm.deselect(olditem);
|
|
}
|
|
// since the store events are suspended, we
|
|
// manually remove the item from the store also
|
|
store.remove(olditem);
|
|
parentNode.removeChild(olditem, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// add new items
|
|
rstore.each(function(item) {
|
|
var olditem = index[item.data.id];
|
|
if (olditem) {
|
|
return;
|
|
}
|
|
|
|
if (filterfn && !filterfn(item)) {
|
|
return;
|
|
}
|
|
|
|
//console.log("ADD UID: " + item.data.id);
|
|
|
|
var info = Ext.apply({ leaf: true }, item.data);
|
|
|
|
var child = me.groupChild(rootnode, info, groups, 0);
|
|
if (child) {
|
|
index[item.data.id] = child;
|
|
}
|
|
});
|
|
|
|
store.resumeEvents();
|
|
store.fireEvent('refresh', store);
|
|
|
|
// select parent node is selection vanished
|
|
if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) {
|
|
lastsel = rootnode;
|
|
while (!!(p = parents.shift())) {
|
|
if (!!(tmp = rootnode.findChild('id', p.data.id, true))) {
|
|
lastsel = tmp;
|
|
break;
|
|
}
|
|
}
|
|
me.selectById(lastsel.data.id);
|
|
} else if (lastsel && reselect) {
|
|
me.selectById(lastsel.data.id);
|
|
}
|
|
|
|
// on first tree load set the selection from the stateful provider
|
|
if (!pdata.updateCount) {
|
|
rootnode.expand();
|
|
me.applyState(sp.get(stateid));
|
|
}
|
|
|
|
pdata.updateCount++;
|
|
};
|
|
|
|
var statechange = function(sp, key, value) {
|
|
if (key === stateid) {
|
|
me.applyState(value);
|
|
}
|
|
};
|
|
|
|
sp.on('statechange', statechange);
|
|
|
|
Ext.apply(me, {
|
|
allowSelection: true,
|
|
store: store,
|
|
viewConfig: {
|
|
// note: animate cause problems with applyState
|
|
animate: false
|
|
},
|
|
//useArrows: true,
|
|
//rootVisible: false,
|
|
//title: 'Resource Tree',
|
|
listeners: {
|
|
itemcontextmenu: PVE.Utils.createCmdMenu,
|
|
destroy: function() {
|
|
rstore.un("load", updateTree);
|
|
},
|
|
beforecellmousedown: function (tree, td, cellIndex, record, tr, rowIndex, ev) {
|
|
var sm = me.getSelectionModel();
|
|
// disable selection when right clicking
|
|
// except the record is already selected
|
|
me.allowSelection = (ev.button !== 2) || sm.isSelected(record);
|
|
},
|
|
beforeselect: function (tree, record, index, eopts) {
|
|
var allow = me.allowSelection;
|
|
me.allowSelection = true;
|
|
return allow;
|
|
},
|
|
itemdblclick: PVE.Utils.openTreeConsole
|
|
},
|
|
setViewFilter: function(view) {
|
|
me.viewFilter = view;
|
|
me.clearTree();
|
|
updateTree();
|
|
},
|
|
setDatacenterText: function(clustername) {
|
|
var rootnode = me.store.getRootNode();
|
|
|
|
var rnodeText = gettext('Datacenter');
|
|
if (clustername !== undefined) {
|
|
rnodeText += ' (' + clustername + ')';
|
|
}
|
|
|
|
rootnode.beginEdit();
|
|
rootnode.data.text = rnodeText;
|
|
rootnode.commit();
|
|
},
|
|
clearTree: function() {
|
|
pdata.updateCount = 0;
|
|
var rootnode = me.store.getRootNode();
|
|
rootnode.collapse();
|
|
rootnode.removeAll();
|
|
pdata.dataIndex = {};
|
|
me.getSelectionModel().deselectAll();
|
|
},
|
|
selectExpand: function(node) {
|
|
var sm = me.getSelectionModel();
|
|
if (!sm.isSelected(node)) {
|
|
sm.select(node);
|
|
var cn = node;
|
|
while (!!(cn = cn.parentNode)) {
|
|
if (!cn.isExpanded()) {
|
|
cn.expand();
|
|
}
|
|
}
|
|
me.getView().focusRow(node);
|
|
}
|
|
},
|
|
selectById: function(nodeid) {
|
|
var rootnode = me.store.getRootNode();
|
|
var sm = me.getSelectionModel();
|
|
var node;
|
|
if (nodeid === 'root') {
|
|
node = rootnode;
|
|
} else {
|
|
node = rootnode.findChild('id', nodeid, true);
|
|
}
|
|
if (node) {
|
|
me.selectExpand(node);
|
|
}
|
|
return node;
|
|
},
|
|
applyState : function(state) {
|
|
var sm = me.getSelectionModel();
|
|
if (state && state.value) {
|
|
me.selectById(state.value);
|
|
} else {
|
|
sm.deselectAll();
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
var sm = me.getSelectionModel();
|
|
sm.on('select', function(sm, n) {
|
|
sp.set(stateid, { value: n.data.id});
|
|
});
|
|
|
|
rstore.on("load", updateTree);
|
|
rstore.startUpdate();
|
|
//rstore.stopUpdate();
|
|
}
|
|
|
|
});
|
|
Ext.define('pve-fw-ipsets', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'name', 'comment', 'digest' ],
|
|
idProperty: 'name'
|
|
});
|
|
|
|
Ext.define('PVE.IPSetList', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pveIPSetList',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-firewall-ipsetlist',
|
|
|
|
ipset_panel: undefined,
|
|
|
|
base_url: undefined,
|
|
|
|
addBtn: undefined,
|
|
removeBtn: undefined,
|
|
editBtn: undefined,
|
|
|
|
initComponent: function() {
|
|
|
|
var me = this;
|
|
|
|
if (me.ipset_panel == undefined) {
|
|
throw "no rule panel specified";
|
|
}
|
|
|
|
if (me.base_url == undefined) {
|
|
throw "no base_url specified";
|
|
}
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-fw-ipsets',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json" + me.base_url
|
|
},
|
|
sorters: {
|
|
property: 'name',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var reload = function() {
|
|
var oldrec = sm.getSelection()[0];
|
|
store.load(function(records, operation, success) {
|
|
if (oldrec) {
|
|
var rec = store.findRecord('name', oldrec.data.name);
|
|
if (rec) {
|
|
sm.select(rec);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var win = Ext.create('Proxmox.window.Edit', {
|
|
subject: "IPSet '" + rec.data.name + "'",
|
|
url: me.base_url,
|
|
method: 'POST',
|
|
digest: rec.data.digest,
|
|
items: [
|
|
{
|
|
xtype: 'hiddenfield',
|
|
name: 'rename',
|
|
value: rec.data.name
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'name',
|
|
value: rec.data.name,
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
value: rec.data.comment,
|
|
fieldLabel: gettext('Comment')
|
|
}
|
|
]
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
me.editBtn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
me.addBtn = new Proxmox.button.Button({
|
|
text: gettext('Create'),
|
|
handler: function() {
|
|
sm.deselectAll();
|
|
var win = Ext.create('Proxmox.window.Edit', {
|
|
subject: 'IPSet',
|
|
url: me.base_url,
|
|
method: 'POST',
|
|
items: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'name',
|
|
value: '',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
value: '',
|
|
fieldLabel: gettext('Comment')
|
|
}
|
|
]
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
|
|
}
|
|
});
|
|
|
|
me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: me.base_url + '/',
|
|
callback: reload
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
tbar: [ '<b>IPSet:</b>', me.addBtn, me.removeBtn, me.editBtn ],
|
|
selModel: sm,
|
|
columns: [
|
|
{ header: 'IPSet', dataIndex: 'name', width: '100' },
|
|
{ header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 }
|
|
],
|
|
listeners: {
|
|
itemdblclick: run_editor,
|
|
select: function(sm, rec) {
|
|
var url = me.base_url + '/' + rec.data.name;
|
|
me.ipset_panel.setBaseUrl(url);
|
|
},
|
|
deselect: function() {
|
|
me.ipset_panel.setBaseUrl(undefined);
|
|
},
|
|
show: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.IPSetCidrEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
cidr: undefined,
|
|
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
me.isCreate = (me.cidr === undefined);
|
|
|
|
|
|
if (me.isCreate) {
|
|
me.url = '/api2/extjs' + me.base_url;
|
|
me.method = 'POST';
|
|
} else {
|
|
me.url = '/api2/extjs' + me.base_url + '/' + me.cidr;
|
|
me.method = 'PUT';
|
|
}
|
|
|
|
var column1 = [];
|
|
|
|
if (me.isCreate) {
|
|
if (!me.list_refs_url) {
|
|
throw "no alias_base_url specified";
|
|
}
|
|
|
|
column1.push({
|
|
xtype: 'pveIPRefSelector',
|
|
name: 'cidr',
|
|
ref_type: 'alias',
|
|
autoSelect: false,
|
|
editable: true,
|
|
base_url: me.list_refs_url,
|
|
value: '',
|
|
fieldLabel: gettext('IP/CIDR')
|
|
});
|
|
} else {
|
|
column1.push({
|
|
xtype: 'displayfield',
|
|
name: 'cidr',
|
|
value: '',
|
|
fieldLabel: gettext('IP/CIDR')
|
|
});
|
|
}
|
|
|
|
var ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
isCreate: me.isCreate,
|
|
column1: column1,
|
|
column2: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'nomatch',
|
|
checked: false,
|
|
uncheckedValue: 0,
|
|
fieldLabel: 'nomatch'
|
|
}
|
|
],
|
|
columnB: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
value: '',
|
|
fieldLabel: gettext('Comment')
|
|
}
|
|
]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('IP/CIDR'),
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var values = response.result.data;
|
|
ipanel.setValues(values);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.IPSetGrid', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pveIPSetGrid',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-firewall-ipsets',
|
|
|
|
base_url: undefined,
|
|
list_refs_url: undefined,
|
|
|
|
addBtn: undefined,
|
|
removeBtn: undefined,
|
|
editBtn: undefined,
|
|
|
|
setBaseUrl: function(url) {
|
|
var me = this;
|
|
|
|
me.base_url = url;
|
|
|
|
if (url === undefined) {
|
|
me.addBtn.setDisabled(true);
|
|
me.store.removeAll();
|
|
} else {
|
|
me.addBtn.setDisabled(false);
|
|
me.removeBtn.baseurl = url + '/';
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: '/api2/json' + url
|
|
});
|
|
|
|
me.store.load();
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
if (!me.list_refs_url) {
|
|
throw "no1 list_refs_url specified";
|
|
}
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-ipset'
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var win = Ext.create('PVE.IPSetCidrEdit', {
|
|
base_url: me.base_url,
|
|
cidr: rec.data.cidr
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
me.editBtn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
me.addBtn = new Proxmox.button.Button({
|
|
text: gettext('Add'),
|
|
disabled: true,
|
|
handler: function() {
|
|
if (!me.base_url) {
|
|
return;
|
|
}
|
|
var win = Ext.create('PVE.IPSetCidrEdit', {
|
|
base_url: me.base_url,
|
|
list_refs_url: me.list_refs_url
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
}
|
|
});
|
|
|
|
me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: me.base_url + '/',
|
|
callback: reload
|
|
});
|
|
|
|
var render_errors = function(value, metaData, record) {
|
|
var errors = record.data.errors;
|
|
if (errors) {
|
|
var msg = errors.cidr || errors.nomatch;
|
|
if (msg) {
|
|
metaData.tdCls = 'proxmox-invalid-row';
|
|
var html = '<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'
|
|
},
|
|
{
|
|
header: gettext('IP/CIDR'),
|
|
dataIndex: 'cidr',
|
|
width: 150,
|
|
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: 1,
|
|
renderer: function(value) {
|
|
return Ext.util.Format.htmlEncode(value);
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (me.base_url) {
|
|
me.setBaseUrl(me.base_url); // load
|
|
}
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-ipset', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ { name: 'nomatch', type: 'boolean' },
|
|
'cidr', 'comment', 'errors' ],
|
|
idProperty: 'cidr'
|
|
});
|
|
|
|
});
|
|
|
|
Ext.define('PVE.IPSet', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveIPSet',
|
|
|
|
title: 'IPSet',
|
|
|
|
onlineHelp: 'pve_firewall_ip_sets',
|
|
|
|
list_refs_url: undefined,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.list_refs_url) {
|
|
throw "no list_refs_url specified";
|
|
}
|
|
|
|
var ipset_panel = Ext.createWidget('pveIPSetGrid', {
|
|
region: 'center',
|
|
list_refs_url: me.list_refs_url,
|
|
border: false
|
|
});
|
|
|
|
var ipset_list = Ext.createWidget('pveIPSetList', {
|
|
region: 'west',
|
|
ipset_panel: ipset_panel,
|
|
base_url: me.base_url,
|
|
width: '50%',
|
|
border: false,
|
|
split: true
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
layout: 'border',
|
|
items: [ ipset_list, ipset_panel ],
|
|
listeners: {
|
|
show: function() {
|
|
ipset_list.fireEvent('show', ipset_list);
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*
|
|
* Base class for all the multitab config panels
|
|
*
|
|
* How to use this:
|
|
*
|
|
* You create a subclass of this, and then define your wanted tabs
|
|
* as items like this:
|
|
*
|
|
* items: [{
|
|
* title: "myTitle",
|
|
* xytpe: "somextype",
|
|
* iconCls: 'fa fa-icon',
|
|
* groups: ['somegroup'],
|
|
* expandedOnInit: true,
|
|
* itemId: 'someId'
|
|
* }]
|
|
*
|
|
* this has to be in the declarative syntax, else we
|
|
* cannot save them for later
|
|
* (so no Ext.create or Ext.apply of an item in the subclass)
|
|
*
|
|
* the groups array expects the itemids of the items
|
|
* which are the parents, which have to come before they
|
|
* are used
|
|
*
|
|
* if you want following the tree:
|
|
*
|
|
* Option1
|
|
* Option2
|
|
* -> SubOption1
|
|
* -> SubSubOption1
|
|
*
|
|
* the suboption1 group array has to look like this:
|
|
* groups: ['itemid-of-option2']
|
|
*
|
|
* and of subsuboption1:
|
|
* groups: ['itemid-of-option2', 'itemid-of-suboption1']
|
|
*
|
|
* setting the expandedOnInit determines if the item/group is expanded
|
|
* initially (false by default)
|
|
*/
|
|
Ext.define('PVE.panel.Config', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pvePanelConfig',
|
|
|
|
showSearch: true, // add a 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: {
|
|
backgroundColor: '#f5f5f5',
|
|
padding: 0,
|
|
margin: 0
|
|
},
|
|
items: {
|
|
xtype: 'treelist',
|
|
itemId: 'menu',
|
|
ui: 'nav',
|
|
expanderOnly: true,
|
|
expanderFirst: false,
|
|
animation: false,
|
|
singleExpand: false,
|
|
listeners: {
|
|
selectionchange: function(treeList, selection) {
|
|
var me = this.up('panel');
|
|
me.suspendLayout = true;
|
|
me.activateCard(selection.data.id);
|
|
me.suspendLayout = false;
|
|
me.updateLayout();
|
|
},
|
|
itemclick: function(treelist, info) {
|
|
var olditem = treelist.getSelection();
|
|
var newitem = info.node;
|
|
|
|
// when clicking on the expand arrow,
|
|
// we don't select items, but still want
|
|
// the original behaviour
|
|
if (info.select === false) {
|
|
return;
|
|
}
|
|
|
|
// if you click on a different item which is open,
|
|
// leave it open
|
|
// else toggle the clicked item
|
|
if (olditem.data.id !== newitem.data.id &&
|
|
newitem.data.expanded === true) {
|
|
info.toggle = false;
|
|
} else {
|
|
info.toggle = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'toolbar',
|
|
itemId: 'toolbar',
|
|
dock: 'top',
|
|
height: 36,
|
|
overflowHandler: 'scroller'
|
|
}],
|
|
|
|
firstItem: '',
|
|
layout: 'card',
|
|
border: 0,
|
|
|
|
// used for automated test
|
|
selectById: function(cardid) {
|
|
var me = this;
|
|
|
|
var root = me.store.getRoot();
|
|
var selection = root.findChild('id', cardid, true);
|
|
|
|
if (selection) {
|
|
selection.expand();
|
|
var menu = me.down('#menu');
|
|
menu.setSelection(selection);
|
|
return cardid;
|
|
}
|
|
},
|
|
|
|
activateCard: function(cardid) {
|
|
var me = this;
|
|
if (me.savedItems[cardid]) {
|
|
var curcard = me.getLayout().getActiveItem();
|
|
var newcard = me.add(me.savedItems[cardid]);
|
|
me.helpButton.setOnlineHelp(newcard.onlineHelp || me.onlineHelp);
|
|
if (curcard) {
|
|
me.setActiveItem(cardid);
|
|
me.remove(curcard, true);
|
|
|
|
// trigger state change
|
|
|
|
var ncard = cardid;
|
|
// Note: '' is alias for first tab.
|
|
// First tab can be 'search' or something else
|
|
if (cardid === me.firstItem) {
|
|
ncard = '';
|
|
}
|
|
if (me.hstateid) {
|
|
me.sp.set(me.hstateid, { value: ncard });
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var stateid = me.hstateid;
|
|
|
|
me.sp = Ext.state.Manager.getProvider();
|
|
|
|
var activeTab; // leaving this undefined means items[0] will be the default tab
|
|
|
|
if (stateid) {
|
|
var state = me.sp.get(stateid);
|
|
if (state && state.value) {
|
|
// if this tab does not exists, it chooses the first
|
|
activeTab = state.value;
|
|
}
|
|
}
|
|
|
|
// get title
|
|
var title = me.title || me.pveSelNode.data.text;
|
|
me.title = undefined;
|
|
|
|
// create toolbar
|
|
var tbar = me.tbar || [];
|
|
me.tbar = undefined;
|
|
|
|
if (!me.onlineHelp) {
|
|
switch(me.pveSelNode.data.id) {
|
|
case 'type/storage':me.onlineHelp = 'chapter-pvesm.html'; break;
|
|
case 'type/qemu':me.onlineHelp = 'chapter-qm.html'; break;
|
|
case 'type/lxc':me.onlineHelp = 'chapter-pct.html'; break;
|
|
case 'type/pool':me.onlineHelp = 'chapter-pveum.html#_pools'; break;
|
|
case 'type/node':me.onlineHelp = 'chapter-sysadmin.html'; break;
|
|
}
|
|
}
|
|
|
|
if (me.tbarSpacing) {
|
|
tbar.unshift('->');
|
|
}
|
|
tbar.unshift({
|
|
xtype: 'tbtext',
|
|
text: title,
|
|
baseCls: 'x-panel-header-text'
|
|
});
|
|
|
|
me.helpButton = Ext.create('Proxmox.button.Help', {
|
|
hidden: false,
|
|
listenToGlobalEvent: false,
|
|
onlineHelp: me.onlineHelp || undefined
|
|
});
|
|
|
|
tbar.push(me.helpButton);
|
|
|
|
me.dockedItems[1].items = tbar;
|
|
|
|
// include search tab
|
|
me.items = me.items || [];
|
|
if (me.showSearch) {
|
|
me.items.unshift({
|
|
itemId: 'search',
|
|
title: gettext('Search'),
|
|
iconCls: 'fa fa-search',
|
|
xtype: 'pveResourceGrid',
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
}
|
|
|
|
me.savedItems = {};
|
|
/*jslint confusion:true*/
|
|
if (me.items[0]) {
|
|
me.firstItem = me.items[0].itemId;
|
|
}
|
|
/*jslint confusion:false*/
|
|
|
|
me.store = Ext.create('Ext.data.TreeStore', {
|
|
root: {
|
|
expanded: true
|
|
}
|
|
});
|
|
var root = me.store.getRoot();
|
|
me.items.forEach(function(item){
|
|
var treeitem = Ext.create('Ext.data.TreeModel',{
|
|
id: item.itemId,
|
|
text: item.title,
|
|
iconCls: item.iconCls,
|
|
leaf: true,
|
|
expanded: item.expandedOnInit
|
|
});
|
|
item.header = false;
|
|
if (me.savedItems[item.itemId] !== undefined) {
|
|
throw "itemId already exists, please use another";
|
|
}
|
|
me.savedItems[item.itemId] = item;
|
|
|
|
var group;
|
|
var curnode = root;
|
|
|
|
// get/create the group items
|
|
while (Ext.isArray(item.groups) && item.groups.length > 0) {
|
|
group = item.groups.shift();
|
|
|
|
var child = curnode.findChild('id', group);
|
|
if (child === null) {
|
|
// did not find the group item
|
|
// so add it where we are
|
|
break;
|
|
}
|
|
curnode = child;
|
|
}
|
|
|
|
// insert the item
|
|
|
|
// lets see if it already exists
|
|
var node = curnode.findChild('id', item.itemId);
|
|
|
|
if (node === null) {
|
|
curnode.appendChild(treeitem);
|
|
} else {
|
|
// should not happen!
|
|
throw "id already exists";
|
|
}
|
|
});
|
|
|
|
delete me.items;
|
|
me.defaults = me.defaults || {};
|
|
Ext.apply(me.defaults, {
|
|
pveSelNode: me.pveSelNode,
|
|
viewFilter: me.viewFilter,
|
|
workspace: me.workspace,
|
|
border: 0
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
var menu = me.down('#menu');
|
|
var selection = root.findChild('id', activeTab, true) || root.firstChild;
|
|
var node = selection;
|
|
while (node !== root) {
|
|
node.expand();
|
|
node = node.parentNode;
|
|
}
|
|
menu.setStore(me.store);
|
|
menu.setSelection(selection);
|
|
|
|
// on a state change,
|
|
// select the new item
|
|
var statechange = function(sp, key, state) {
|
|
// it the state change is for this panel
|
|
if (stateid && (key === stateid) && state) {
|
|
// get active item
|
|
var acard = me.getLayout().getActiveItem().itemId;
|
|
// get the itemid of the new value
|
|
var ncard = state.value || me.firstItem;
|
|
if (ncard && (acard != ncard)) {
|
|
// select the chosen item
|
|
menu.setSelection(root.findChild('id', ncard, true) || root.firstChild);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (stateid) {
|
|
me.mon(me.sp, 'statechange', statechange);
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.grid.BackupView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveBackupView'],
|
|
|
|
onlineHelp: 'chapter_vzdump',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-guest-backup',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var vmtype = me.pveSelNode.data.type;
|
|
if (!vmtype) {
|
|
throw "no VM type specified";
|
|
}
|
|
|
|
var vmtypeFilter;
|
|
if (vmtype === 'openvz') {
|
|
vmtypeFilter = function(item) {
|
|
return item.data.volid.match(':backup/vzdump-openvz-');
|
|
};
|
|
} else if (vmtype === 'lxc') {
|
|
vmtypeFilter = function(item) {
|
|
return item.data.volid.match(':backup/vzdump-lxc-');
|
|
};
|
|
} else if (vmtype === 'qemu') {
|
|
vmtypeFilter = function(item) {
|
|
return item.data.volid.match(':backup/vzdump-qemu-');
|
|
};
|
|
} else {
|
|
throw "unsupported VM type '" + vmtype + "'";
|
|
}
|
|
|
|
var searchFilter = {
|
|
property: 'volid',
|
|
// on initial store display only our vmid backups
|
|
// surround with minus sign to prevent the 2016 VMID bug
|
|
value: vmtype + '-' + vmid + '-',
|
|
anyMatch: true,
|
|
caseSensitive: false
|
|
};
|
|
|
|
me.store = Ext.create('Ext.data.Store', {
|
|
model: 'pve-storage-content',
|
|
sorters: {
|
|
property: 'volid',
|
|
order: 'DESC'
|
|
},
|
|
filters: [
|
|
vmtypeFilter,
|
|
searchFilter
|
|
]
|
|
});
|
|
|
|
var reload = Ext.Function.createBuffered(function() {
|
|
if (me.store) {
|
|
me.store.load();
|
|
}
|
|
}, 100);
|
|
|
|
var setStorage = function(storage) {
|
|
var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content';
|
|
url += '?content=backup';
|
|
|
|
me.store.setProxy({
|
|
type: 'proxmox',
|
|
url: url
|
|
});
|
|
|
|
reload();
|
|
};
|
|
|
|
var storagesel = Ext.create('PVE.form.StorageSelector', {
|
|
nodename: nodename,
|
|
fieldLabel: gettext('Storage'),
|
|
labelAlign: 'right',
|
|
storageContent: 'backup',
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
setStorage(value);
|
|
}
|
|
}
|
|
});
|
|
|
|
var storagefilter = Ext.create('Ext.form.field.Text', {
|
|
fieldLabel: gettext('Search'),
|
|
labelWidth: 50,
|
|
labelAlign: 'right',
|
|
enableKeyEvents: true,
|
|
value: searchFilter.value,
|
|
listeners: {
|
|
buffer: 500,
|
|
keyup: function(field) {
|
|
me.store.clearFilter(true);
|
|
searchFilter.value = field.getValue();
|
|
me.store.filter([
|
|
vmtypeFilter,
|
|
searchFilter
|
|
]);
|
|
}
|
|
}
|
|
});
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var backup_btn = Ext.create('Ext.button.Button', {
|
|
text: gettext('Backup now'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.Backup', {
|
|
nodename: nodename,
|
|
vmid: vmid,
|
|
vmtype: vmtype,
|
|
storage: storagesel.getValue(),
|
|
listeners : {
|
|
close: function() {
|
|
reload();
|
|
}
|
|
}
|
|
});
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
var restore_btn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Restore'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
return !!rec;
|
|
},
|
|
handler: function(b, e, rec) {
|
|
var volid = rec.data.volid;
|
|
|
|
var win = Ext.create('PVE.window.Restore', {
|
|
nodename: nodename,
|
|
vmid: vmid,
|
|
volid: rec.data.volid,
|
|
volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
|
|
vmtype: vmtype
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
}
|
|
});
|
|
|
|
var delete_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
dangerous: true,
|
|
confirmMsg: function(rec) {
|
|
var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
|
|
"'" + rec.data.volid + "'");
|
|
msg += " " + gettext('This will permanently erase all data.');
|
|
|
|
return msg;
|
|
},
|
|
getUrl: function(rec) {
|
|
var storage = storagesel.getValue();
|
|
return '/nodes/' + nodename + '/storage/' + storage + '/content/' + rec.data.volid;
|
|
},
|
|
callback: function() {
|
|
reload();
|
|
}
|
|
});
|
|
|
|
var config_btn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Show Configuration'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
return !!rec;
|
|
},
|
|
handler: function(b, e, rec) {
|
|
var storage = storagesel.getValue();
|
|
if (!storage) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.window.BackupConfig', {
|
|
volume: rec.data.volid,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
selModel: sm,
|
|
tbar: [ backup_btn, restore_btn, delete_btn,config_btn, '->', storagesel, storagefilter ],
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
flex: 1,
|
|
sortable: true,
|
|
renderer: PVE.Utils.render_storage_content,
|
|
dataIndex: 'volid'
|
|
},
|
|
{
|
|
header: gettext('Format'),
|
|
width: 100,
|
|
dataIndex: 'format'
|
|
},
|
|
{
|
|
header: gettext('Size'),
|
|
width: 100,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'size'
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.CephCreateService', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveCephCreateService',
|
|
|
|
showProgress: true,
|
|
|
|
setNode: function(nodename) {
|
|
var me = this;
|
|
|
|
me.nodename = nodename;
|
|
me.url = "/nodes/" + nodename + "/ceph/" + me.type + "/" + nodename;
|
|
},
|
|
|
|
method: 'POST',
|
|
isCreate: true,
|
|
|
|
items: [
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
submitValue: false,
|
|
fieldLabel: gettext('Host'),
|
|
selectCurNode: true,
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
var me = this.up('pveCephCreateService');
|
|
me.setNode(value);
|
|
}
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent : function() {
|
|
var 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.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: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
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.rstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
autoLoad: true,
|
|
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) {
|
|
var regex = new RegExp("not (installed|initialized)", "i");
|
|
PVE.Utils.handleStoreErrorOrMask(view, view.rstore, regex, function(me, error) {
|
|
view.rstore.stopUpdate();
|
|
PVE.Utils.showCephInstallOrMask(view.ownerCt, error.statusText, view.nodename,
|
|
function(win){
|
|
me.mon(win, 'cephInstallWindowClosed', function(){
|
|
view.rstore.startUpdate();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
}
|
|
},
|
|
|
|
service_cmd: function(rec, cmd) {
|
|
var view = this.getView();
|
|
if (!rec.data.host) {
|
|
Ext.Msg.alert(gettext('Error'), "entry has no host");
|
|
return;
|
|
}
|
|
Proxmox.Utils.API2Request({
|
|
url: "/nodes/" + rec.data.host + "/ceph/" + cmd,
|
|
method: 'POST',
|
|
params: { service: view.type + '.' + rec.data.name },
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', {
|
|
upid: upid,
|
|
taskDone: function() {
|
|
view.rstore.load();
|
|
}
|
|
});
|
|
win.show();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
onChangeService: function(btn) {
|
|
var me = this;
|
|
var view = this.getView();
|
|
var cmd = btn.action;
|
|
var rec = view.getSelection()[0];
|
|
me.service_cmd(rec, cmd);
|
|
},
|
|
|
|
showSyslog: function() {
|
|
var view = this.getView();
|
|
var rec = view.getSelection()[0];
|
|
var servicename = 'ceph-' + view.type + '@' + rec.data.name;
|
|
var url = "/api2/extjs/nodes/" + rec.data.host + "/syslog?service=" + encodeURIComponent(servicename);
|
|
var win = Ext.create('Ext.window.Window', {
|
|
title: gettext('Syslog') + ': ' + servicename,
|
|
modal: true,
|
|
width: 800,
|
|
height: 400,
|
|
layout: 'fit',
|
|
items: [{
|
|
xtype: 'proxmoxLogView',
|
|
url: url,
|
|
log_select_timespan: 1
|
|
}]
|
|
});
|
|
win.show();
|
|
},
|
|
|
|
onCreate: function() {
|
|
var view = this.getView();
|
|
var win = Ext.create('PVE.CephCreateService', {
|
|
autoShow: true,
|
|
nodename: view.nodename,
|
|
subject: view.getTitle(),
|
|
type: view.type,
|
|
taskDone: function() {
|
|
view.rstore.load();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
tbar: [
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Start'),
|
|
iconCls: 'fa fa-play',
|
|
action: 'start',
|
|
disabled: true,
|
|
enableFn: function(rec) {
|
|
return rec.data.state === 'stopped' ||
|
|
rec.data.state === 'unknown';
|
|
},
|
|
handler: 'onChangeService'
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Stop'),
|
|
iconCls: 'fa fa-stop',
|
|
action: 'stop',
|
|
enableFn: function(rec) {
|
|
return rec.data.state !== 'stopped';
|
|
},
|
|
disabled: true,
|
|
handler: 'onChangeService'
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Restart'),
|
|
iconCls: 'fa fa-refresh',
|
|
action: 'restart',
|
|
disabled: true,
|
|
enableFn: function(rec) {
|
|
return rec.data.state !== 'stopped';
|
|
},
|
|
handler: 'onChangeService'
|
|
},
|
|
'-',
|
|
{
|
|
text: gettext('Create'),
|
|
reference: 'createButton',
|
|
handler: 'onCreate'
|
|
},
|
|
{
|
|
text: gettext('Destroy'),
|
|
xtype: 'proxmoxStdRemoveButton',
|
|
getUrl: function(rec) {
|
|
var view = this.up('grid');
|
|
if (!rec.data.host) {
|
|
Ext.Msg.alert(gettext('Error'), "entry has no host");
|
|
return;
|
|
}
|
|
return "/nodes/" + rec.data.host + "/ceph/" + view.type + "/" + rec.data.name;
|
|
},
|
|
callback: function(options, success, response) {
|
|
var view = this.up('grid');
|
|
if (!success) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
return;
|
|
}
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', {
|
|
upid: upid,
|
|
taskDone: function() {
|
|
view.rstore.load();
|
|
}
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
'-',
|
|
{
|
|
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,
|
|
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'
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var 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', 'rank', 'host', 'quorum', 'state',
|
|
'ceph_version', 'ceph_version_short',
|
|
{ type: 'string', name: 'version', calculate: function(data) {
|
|
return PVE.Utils.parse_ceph_version(data);
|
|
} }
|
|
],
|
|
idProperty: 'name'
|
|
});
|
|
});
|
|
/*jslint confusion: true */
|
|
Ext.define('PVE.CephCreateFS', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: 'widget.pveCephCreateFS',
|
|
|
|
showTaskViewer: true,
|
|
onlineHelp: 'pveceph_fs_create',
|
|
|
|
subject: 'Ceph FS',
|
|
isCreate: true,
|
|
method: 'POST',
|
|
|
|
setFSName: function(fsName) {
|
|
var me = this;
|
|
|
|
if (fsName === '' || fsName === undefined) {
|
|
fsName = 'cephfs';
|
|
}
|
|
|
|
me.url = "/nodes/" + me.nodename + "/ceph/fs/" + fsName;
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Name'),
|
|
name: 'name',
|
|
value: 'cephfs',
|
|
listeners: {
|
|
change: function(f, value) {
|
|
this.up('pveCephCreateFS').setFSName(value);
|
|
}
|
|
},
|
|
submitValue: false, // already encoded in apicall URL
|
|
emptyText: 'cephfs'
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: 'Placement Groups',
|
|
name: 'pg_num',
|
|
value: 128,
|
|
emptyText: 128,
|
|
minValue: 8,
|
|
maxValue: 32768,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Add 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: {
|
|
cephfsConfigured: false,
|
|
mdsCount: 0
|
|
},
|
|
formulas: {
|
|
canCreateFS: function(get) {
|
|
return (!get('cephfsConfigured') && get('mdsCount') > 0);
|
|
}
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
emptyText: Ext.String.format(gettext('No {0} configured.'), 'CephFS'),
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
init: function(view) {
|
|
view.rstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
autoLoad: true,
|
|
xtype: 'update',
|
|
interval: 5 * 1000,
|
|
autoStart: true,
|
|
storeid: 'pve-ceph-fs',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + view.nodename + '/ceph/fs'
|
|
},
|
|
model: 'pve-ceph-fs'
|
|
});
|
|
view.setStore(Ext.create('Proxmox.data.DiffStore', {
|
|
rstore: view.rstore,
|
|
sorters: {
|
|
property: 'name',
|
|
order: 'DESC'
|
|
}
|
|
}));
|
|
var regex = new RegExp("not (installed|initialized)", "i");
|
|
PVE.Utils.handleStoreErrorOrMask(view, view.rstore, regex, function(me, error){
|
|
me.rstore.stopUpdate();
|
|
PVE.Utils.showCephInstallOrMask(me.ownerCt, error.statusText, view.nodename,
|
|
function(win){
|
|
me.mon(win, 'cephInstallWindowClosed', function(){
|
|
me.rstore.startUpdate();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
view.rstore.on('load', this.onLoad, this);
|
|
view.on('destroy', view.rstore.stopUpdate);
|
|
},
|
|
|
|
onCreate: function() {
|
|
var view = this.getView();
|
|
view.rstore.stopUpdate();
|
|
var win = Ext.create('PVE.CephCreateFS', {
|
|
autoShow: true,
|
|
nodename: view.nodename,
|
|
listeners: {
|
|
destroy: function() {
|
|
view.rstore.startUpdate();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
onLoad: function(store, records, success) {
|
|
var vm = this.getViewModel();
|
|
if (!(success && records && records.length > 0)) {
|
|
vm.set('cephfsConfigured', false);
|
|
return;
|
|
}
|
|
vm.set('cephfsConfigured', true);
|
|
}
|
|
},
|
|
tbar: [
|
|
{
|
|
text: gettext('Create CephFS'),
|
|
reference: 'createButton',
|
|
handler: 'onCreate',
|
|
bind: {
|
|
// only one CephFS per Ceph cluster makes sense for now
|
|
disabled: '{!canCreateFS}'
|
|
}
|
|
}
|
|
],
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
flex: 1,
|
|
dataIndex: 'name'
|
|
},
|
|
{
|
|
header: 'Data Pool',
|
|
flex: 1,
|
|
dataIndex: 'data_pool'
|
|
},
|
|
{
|
|
header: 'Metadata Pool',
|
|
flex: 1,
|
|
dataIndex: 'metadata_pool'
|
|
}
|
|
],
|
|
cbind: {
|
|
nodename: '{nodename}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'pveNodeCephServiceList',
|
|
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;
|
|
}
|
|
vm.set('mdsCount', records.length);
|
|
},
|
|
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.CephCreatePool', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: 'widget.pveCephCreatePool',
|
|
|
|
showProgress: true,
|
|
onlineHelp: 'pve_ceph_pools',
|
|
|
|
subject: 'Ceph Pool',
|
|
isCreate: true,
|
|
method: 'POST',
|
|
items: [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Name'),
|
|
name: 'name',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Size'),
|
|
name: 'size',
|
|
value: 3,
|
|
minValue: 1,
|
|
maxValue: 7,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Min. Size'),
|
|
name: 'min_size',
|
|
value: 2,
|
|
minValue: 1,
|
|
maxValue: 7,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'pveCephRuleSelector',
|
|
fieldLabel: 'Crush Rule', // do not localize
|
|
name: 'crush_rule',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: 'pg_num',
|
|
name: 'pg_num',
|
|
value: 128,
|
|
minValue: 8,
|
|
maxValue: 32768,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Add as Storage'),
|
|
value: true,
|
|
name: 'add_storages',
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Add the new pool to the cluster storage configuration.'),
|
|
},
|
|
}
|
|
],
|
|
initComponent : function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
url: "/nodes/" + me.nodename + "/ceph/pools",
|
|
defaults: {
|
|
nodename: me.nodename
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.CephPoolList', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: 'widget.pveNodeCephPoolList',
|
|
|
|
onlineHelp: 'chapter_pveceph',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-ceph-pools',
|
|
bufferedRenderer: false,
|
|
|
|
features: [ { ftype: 'summary'} ],
|
|
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
width: 120,
|
|
sortable: true,
|
|
dataIndex: 'pool_name'
|
|
},
|
|
{
|
|
header: gettext('Size') + '/min',
|
|
width: 100,
|
|
align: 'right',
|
|
renderer: function(v, meta, rec) {
|
|
return v + '/' + rec.data.min_size;
|
|
},
|
|
dataIndex: 'size'
|
|
},
|
|
{
|
|
text: '# Placement Groups', // pg_num',
|
|
width: 180,
|
|
align: 'right',
|
|
dataIndex: 'pg_num'
|
|
},
|
|
{
|
|
text: 'CRUSH Rule',
|
|
columns: [
|
|
{
|
|
text: 'ID',
|
|
align: 'right',
|
|
width: 50,
|
|
dataIndex: 'crush_rule'
|
|
},
|
|
{
|
|
text: gettext('Name'),
|
|
width: 150,
|
|
dataIndex: 'crush_rule_name',
|
|
},
|
|
]
|
|
},
|
|
{
|
|
text: gettext('Used'),
|
|
columns: [
|
|
{
|
|
text: '%',
|
|
width: 100,
|
|
sortable: true,
|
|
align: 'right',
|
|
renderer: function(val) {
|
|
return Ext.util.Format.percent(val, '0.00');
|
|
},
|
|
dataIndex: 'percent_used',
|
|
summaryType: 'sum',
|
|
summaryRenderer: function(val) {
|
|
return Ext.util.Format.percent(val, '0.00');
|
|
},
|
|
},
|
|
{
|
|
text: gettext('Total'),
|
|
width: 100,
|
|
sortable: true,
|
|
renderer: PVE.Utils.render_size,
|
|
align: 'right',
|
|
dataIndex: 'bytes_used',
|
|
summaryType: 'sum',
|
|
summaryRenderer: PVE.Utils.render_size
|
|
}
|
|
]
|
|
}
|
|
],
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var rstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
interval: 3000,
|
|
storeid: 'ceph-pool-list' + nodename,
|
|
model: 'ceph-pool-list',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + nodename + "/ceph/pools"
|
|
}
|
|
});
|
|
|
|
var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore });
|
|
|
|
var regex = new RegExp("not (installed|initialized)", "i");
|
|
PVE.Utils.handleStoreErrorOrMask(me, rstore, regex, function(me, error){
|
|
me.store.rstore.stopUpdate();
|
|
PVE.Utils.showCephInstallOrMask(me, error.statusText, nodename,
|
|
function(win){
|
|
me.mon(win, 'cephInstallWindowClosed', function(){
|
|
me.store.rstore.startUpdate();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
|
|
var create_btn = new Ext.Button({
|
|
text: gettext('Create'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.CephCreatePool', {
|
|
nodename: nodename
|
|
});
|
|
win.show();
|
|
win.on('destroy', function() {
|
|
rstore.load();
|
|
});
|
|
}
|
|
});
|
|
|
|
var destroy_btn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Destroy'),
|
|
selModel: sm,
|
|
disabled: true,
|
|
handler: function() {
|
|
var rec = sm.getSelection()[0];
|
|
|
|
if (!rec.data.pool_name) {
|
|
return;
|
|
}
|
|
var base_url = '/nodes/' + nodename + '/ceph/pools/' +
|
|
rec.data.pool_name;
|
|
|
|
var win = Ext.create('PVE.window.SafeDestroy', {
|
|
showProgress: true,
|
|
url: base_url,
|
|
params: {
|
|
remove_storages: 1
|
|
},
|
|
item: { type: 'CephPool', id: rec.data.pool_name }
|
|
}).show();
|
|
win.on('destroy', function() {
|
|
rstore.load();
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: [ create_btn, destroy_btn ],
|
|
listeners: {
|
|
activate: rstore.startUpdate,
|
|
destroy: rstore.stopUpdate
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('ceph-pool-list', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'pool_name',
|
|
{ name: 'pool', type: 'integer'},
|
|
{ name: 'size', type: 'integer'},
|
|
{ name: 'min_size', type: 'integer'},
|
|
{ name: 'pg_num', type: 'integer'},
|
|
{ name: 'bytes_used', type: 'integer'},
|
|
{ name: 'percent_used', type: 'number'},
|
|
{ name: 'crush_rule', type: 'integer'},
|
|
{ name: 'crush_rule_name', type: 'string'}
|
|
],
|
|
idProperty: 'pool_name'
|
|
});
|
|
});
|
|
|
|
Ext.define('PVE.form.CephRuleSelector', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveCephRuleSelector',
|
|
|
|
allowBlank: false,
|
|
valueField: 'name',
|
|
displayField: 'name',
|
|
editable: false,
|
|
queryMode: 'local',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no nodename given";
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: ['name'],
|
|
sorters: 'name',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/ceph/rules'
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load({
|
|
callback: function(rec, op, success){
|
|
if (success && rec.length > 0) {
|
|
me.select(rec[0]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
Ext.define('PVE.CephCreateOsd', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveCephCreateOsd',
|
|
|
|
subject: 'Ceph OSD',
|
|
|
|
showProgress: true,
|
|
|
|
onlineHelp: 'pve_ceph_osds',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.isCreate = true;
|
|
|
|
Ext.applyIf(me, {
|
|
url: "/nodes/" + me.nodename + "/ceph/osd",
|
|
method: 'POST',
|
|
items: [
|
|
{
|
|
xtype: 'inputpanel',
|
|
onGetValues: function(values) {
|
|
Object.keys(values || {}).forEach(function(name) {
|
|
if (values[name] === '') {
|
|
delete values[name];
|
|
}
|
|
});
|
|
|
|
return values;
|
|
},
|
|
column1: [
|
|
{
|
|
xtype: 'pveDiskSelector',
|
|
name: 'dev',
|
|
nodename: me.nodename,
|
|
diskType: 'unused',
|
|
fieldLabel: gettext('Disk'),
|
|
allowBlank: false
|
|
}
|
|
],
|
|
column2: [
|
|
{
|
|
xtype: 'pveDiskSelector',
|
|
name: 'db_dev',
|
|
nodename: me.nodename,
|
|
diskType: 'journal_disks',
|
|
fieldLabel: gettext('DB Disk'),
|
|
value: '',
|
|
autoSelect: false,
|
|
allowBlank: true,
|
|
emptyText: 'use OSD disk',
|
|
listeners: {
|
|
change: function(field, val) {
|
|
me.down('field[name=db_size]').setDisabled(!val);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'db_size',
|
|
fieldLabel: gettext('DB size') + ' (GiB)',
|
|
minValue: 1,
|
|
maxValue: 128*1024,
|
|
decimalPrecision: 2,
|
|
allowBlank: true,
|
|
disabled: true,
|
|
emptyText: gettext('Automatic')
|
|
}
|
|
],
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'encrypted',
|
|
fieldLabel: gettext('Encrypt OSD')
|
|
},
|
|
],
|
|
advancedColumn2: [
|
|
{
|
|
xtype: 'pveDiskSelector',
|
|
name: 'wal_dev',
|
|
nodename: me.nodename,
|
|
diskType: 'journal_disks',
|
|
fieldLabel: gettext('WAL Disk'),
|
|
value: '',
|
|
autoSelect: false,
|
|
allowBlank: true,
|
|
emptyText: 'use OSD/DB disk',
|
|
listeners: {
|
|
change: function(field, val) {
|
|
me.down('field[name=wal_size]').setDisabled(!val);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'wal_size',
|
|
fieldLabel: gettext('WAL size') + ' (GiB)',
|
|
minValue: 0.5,
|
|
maxValue: 128*1024,
|
|
decimalPrecision: 2,
|
|
allowBlank: true,
|
|
disabled: true,
|
|
emptyText: gettext('Automatic')
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
padding: '5 0 0 0',
|
|
userCls: 'pve-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')
|
|
}
|
|
],
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
if (me.osdid === undefined || me.osdid < 0) {
|
|
throw "no osdid specified";
|
|
}
|
|
|
|
me.isCreate = true;
|
|
|
|
me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString();
|
|
|
|
Ext.applyIf(me, {
|
|
url: "/nodes/" + me.nodename + "/ceph/osd/" + me.osdid.toString()
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.CephOsdTree', {
|
|
extend: 'Ext.tree.Panel',
|
|
alias: ['widget.pveNodeCephOsdTree'],
|
|
onlineHelp: 'chapter_pveceph',
|
|
|
|
viewModel: {
|
|
data: {
|
|
nodename: '',
|
|
flags: [],
|
|
maxversion: '0',
|
|
versions: {},
|
|
isOsd: false,
|
|
downOsd: false,
|
|
upOsd: false,
|
|
inOsd: false,
|
|
outOsd: false,
|
|
osdid: '',
|
|
osdhost: '',
|
|
}
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
reload: function() {
|
|
var me = this.getView();
|
|
var vm = this.getViewModel();
|
|
var nodename = vm.get('nodename');
|
|
var sm = me.getSelectionModel();
|
|
Proxmox.Utils.API2Request({
|
|
url: "/nodes/" + nodename + "/ceph/osd",
|
|
waitMsgTarget: me,
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
var msg = response.htmlStatus;
|
|
PVE.Utils.showCephInstallOrMask(me, msg, nodename,
|
|
function(win){
|
|
me.mon(win, 'cephInstallWindowClosed', this.reload);
|
|
}
|
|
);
|
|
},
|
|
success: function(response, opts) {
|
|
var data = response.result.data;
|
|
var selected = me.getSelection();
|
|
var name;
|
|
if (selected.length) {
|
|
name = selected[0].data.name;
|
|
}
|
|
vm.set('versions', data.versions);
|
|
// extract max version
|
|
var maxversion = vm.get('maxversion');
|
|
Object.values(data.versions || {}).forEach(function(version) {
|
|
if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
|
|
maxversion = version;
|
|
}
|
|
});
|
|
vm.set('maxversion', maxversion);
|
|
sm.deselectAll();
|
|
me.setRootNode(data.root);
|
|
me.expandAll();
|
|
if (name) {
|
|
var node = me.getRootNode().findChild('name', name, true);
|
|
if (node) {
|
|
me.setSelection([node]);
|
|
}
|
|
}
|
|
|
|
var flags = data.flags.split(',');
|
|
vm.set('flags', flags);
|
|
var noout = flags.includes('noout');
|
|
me.down('#nooutBtn').setText(noout ? gettext("Unset noout") : gettext("Set noout"));
|
|
}
|
|
});
|
|
},
|
|
|
|
osd_cmd: function(comp) {
|
|
var me = this;
|
|
var vm = this.getViewModel();
|
|
var cmd = comp.cmd;
|
|
var params = comp.params || {};
|
|
var osdid = vm.get('osdid');
|
|
|
|
var doRequest = function() {
|
|
Proxmox.Utils.API2Request({
|
|
url: "/nodes/" + vm.get('osdhost') + "/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() {
|
|
var me = this;
|
|
var vm = this.getViewModel();
|
|
Ext.create('PVE.CephCreateOsd', {
|
|
nodename: vm.get('nodename'),
|
|
taskDone: () => { me.reload(); }
|
|
}).show();
|
|
},
|
|
|
|
destroy_osd: function() {
|
|
var me = this;
|
|
var vm = this.getViewModel();
|
|
Ext.create('PVE.CephRemoveOsd', {
|
|
nodename: vm.get('osdhost'),
|
|
osdid: vm.get('osdid'),
|
|
taskDone: () => { me.reload(); }
|
|
}).show();
|
|
},
|
|
|
|
set_flag: function() {
|
|
var me = this;
|
|
var vm = this.getViewModel();
|
|
var flags = vm.get('flags');
|
|
Proxmox.Utils.API2Request({
|
|
url: "/nodes/" + vm.get('nodename') + "/ceph/flags/noout",
|
|
waitMsgTarget: me.getView(),
|
|
method: flags.includes('noout') ? 'DELETE' : 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: () => { me.reload(); }
|
|
});
|
|
},
|
|
|
|
service_cmd: function(comp) {
|
|
var me = this;
|
|
var vm = this.getViewModel();
|
|
var cmd = comp.cmd || comp;
|
|
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) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', {
|
|
upid: upid,
|
|
taskDone: () => { me.reload(); }
|
|
});
|
|
win.show();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
|
|
set_selection_status: function(tp, selection) {
|
|
if (selection.length < 1) {
|
|
return;
|
|
}
|
|
var rec = selection[0];
|
|
var vm = this.getViewModel();
|
|
|
|
var 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;
|
|
}
|
|
var inout = rec.data['in'] ? 'in' : 'out';
|
|
var updownicon = value === 'up' ? 'good fa-arrow-circle-up' :
|
|
'critical fa-arrow-circle-down';
|
|
|
|
var inouticon = rec.data['in'] ? 'good fa-circle' :
|
|
'warning fa-circle-o';
|
|
|
|
var text = value + ' <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) {
|
|
var vm = this.getViewModel();
|
|
var versions = vm.get('versions');
|
|
var icon = "";
|
|
var version = value || "";
|
|
if (value && value != vm.get('maxversion')) {
|
|
icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
|
|
}
|
|
|
|
if (!value && rec.data.type == 'host') {
|
|
version = versions[rec.data.name] || Proxmox.Utils.unknownText;
|
|
}
|
|
|
|
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(PVE.Utils.render_size(value), metaData, rec);
|
|
},
|
|
|
|
control: {
|
|
'#': {
|
|
selectionchange: 'set_selection_status'
|
|
}
|
|
},
|
|
|
|
init: function(view) {
|
|
var me = this;
|
|
var 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,
|
|
|
|
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
|
|
}
|
|
],
|
|
|
|
|
|
tbar: {
|
|
items: [
|
|
{
|
|
text: gettext('Reload'),
|
|
iconCls: 'fa fa-refresh',
|
|
handler: 'reload'
|
|
},
|
|
'-',
|
|
{
|
|
text: gettext('Create') + ': OSD',
|
|
handler: 'create_osd',
|
|
},
|
|
{
|
|
text: gettext('Set noout'),
|
|
itemId: 'nooutBtn',
|
|
handler: 'set_flag',
|
|
},
|
|
'->',
|
|
{
|
|
xtype: 'tbtext',
|
|
data: {
|
|
osd: undefined
|
|
},
|
|
bind: {
|
|
data: {
|
|
osd: "{osdid}"
|
|
}
|
|
},
|
|
tpl: [
|
|
'<tpl if="osd">',
|
|
'osd.{osd}:',
|
|
'<tpl else>',
|
|
gettext('No OSD selected'),
|
|
'</tpl>'
|
|
]
|
|
},
|
|
{
|
|
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) {
|
|
var iconMap = {
|
|
host: 'fa-building',
|
|
osd: 'fa-hdd-o',
|
|
root: 'fa-server',
|
|
};
|
|
return 'fa x-fa-tree ' + iconMap[data.type];
|
|
} },
|
|
{ type: 'number', name: 'crush_weight' }
|
|
],
|
|
});
|
|
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.node.CephCrushMap', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: ['widget.pveNodeCephCrushMap'],
|
|
bodyStyle: 'white-space:pre',
|
|
bodyPadding: 5,
|
|
border: false,
|
|
stateful: true,
|
|
stateId: 'layout-ceph-crush',
|
|
scrollable: true,
|
|
load: function() {
|
|
var me = this;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: me.url,
|
|
waitMsgTarget: me,
|
|
failure: function(response, opts) {
|
|
me.update(gettext('Error') + " " + response.htmlStatus);
|
|
var msg = response.htmlStatus;
|
|
PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node,
|
|
function(win){
|
|
me.mon(win, 'cephInstallWindowClosed', function(){
|
|
me.load();
|
|
});
|
|
}
|
|
);
|
|
},
|
|
success: function(response, opts) {
|
|
var data = response.result.data;
|
|
me.update(Ext.htmlEncode(data));
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
url: '/nodes/' + nodename + '/ceph/crush',
|
|
|
|
listeners: {
|
|
activate: function() {
|
|
me.load();
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
Ext.define('PVE.node.CephStatus', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveNodeCephStatus',
|
|
|
|
onlineHelp: 'chapter_pveceph',
|
|
|
|
scrollable: true,
|
|
|
|
bodyPadding: 5,
|
|
|
|
layout: {
|
|
type: 'column'
|
|
},
|
|
|
|
defaults: {
|
|
padding: 5
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'panel',
|
|
title: gettext('Health'),
|
|
bodyPadding: 10,
|
|
plugins: 'responsive',
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
minHeight: 230,
|
|
columnWidth: 1
|
|
},
|
|
'width >= 1900': {
|
|
minHeight: 500,
|
|
columnWidth: 0.5
|
|
}
|
|
},
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'stretch'
|
|
},
|
|
items: [
|
|
{
|
|
flex: 1,
|
|
itemId: 'overallhealth',
|
|
xtype: 'pveHealthWidget',
|
|
title: gettext('Status')
|
|
},
|
|
{
|
|
flex: 2,
|
|
itemId: 'warnings',
|
|
stateful: true,
|
|
stateId: 'ceph-status-warnings',
|
|
xtype: 'grid',
|
|
// since we load the store manually,
|
|
// to show the emptytext, we have to
|
|
// specify an empty store
|
|
store: { data:[] },
|
|
emptyText: gettext('No Warnings/Errors'),
|
|
columns: [
|
|
{
|
|
dataIndex: 'severity',
|
|
header: gettext('Severity'),
|
|
align: 'center',
|
|
width: 70,
|
|
renderer: function(value) {
|
|
var health = PVE.Utils.map_ceph_health[value];
|
|
var classes = PVE.Utils.get_health_icon(health);
|
|
|
|
return '<i class="fa fa-fw ' + classes + '"></i>';
|
|
},
|
|
sorter: {
|
|
sorterFn: function(a,b) {
|
|
var healthArr = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK'];
|
|
return healthArr.indexOf(b.data.severity) - healthArr.indexOf(a.data.severity);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
dataIndex: 'summary',
|
|
header: gettext('Summary'),
|
|
flex: 1
|
|
},
|
|
{
|
|
xtype: 'actioncolumn',
|
|
width: 40,
|
|
align: 'center',
|
|
tooltip: gettext('Detail'),
|
|
items: [
|
|
{
|
|
iconCls: 'x-fa fa-info-circle',
|
|
handler: function(grid, rowindex, colindex, item, e, record) {
|
|
var win = Ext.create('Ext.window.Window', {
|
|
title: gettext('Detail'),
|
|
resizable: true,
|
|
modal: true,
|
|
width: 650,
|
|
height: 400,
|
|
layout: {
|
|
type: 'fit'
|
|
},
|
|
items: [{
|
|
scrollable: true,
|
|
padding: 10,
|
|
xtype: 'box',
|
|
html: [
|
|
'<span>' + Ext.htmlEncode(record.data.summary) + '</span>',
|
|
'<pre>' + Ext.htmlEncode(record.data.detail) + '</pre>'
|
|
]
|
|
}]
|
|
});
|
|
win.show();
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'pveCephStatusDetail',
|
|
itemId: 'statusdetail',
|
|
plugins: 'responsive',
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: 1,
|
|
minHeight: 250
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: 0.5,
|
|
minHeight: 300
|
|
}
|
|
},
|
|
title: gettext('Status')
|
|
},
|
|
{
|
|
title: gettext('Services'),
|
|
xtype: 'pveCephServices',
|
|
itemId: 'services',
|
|
plugins: 'responsive',
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'stretch'
|
|
},
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: 1,
|
|
minHeight: 200
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: 0.5,
|
|
minHeight: 200
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'panel',
|
|
title: gettext('Performance'),
|
|
columnWidth: 1,
|
|
bodyPadding: 5,
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'center'
|
|
},
|
|
items: [
|
|
{
|
|
flex: 1,
|
|
xtype: 'proxmoxGauge',
|
|
itemId: 'space',
|
|
title: gettext('Usage')
|
|
},
|
|
{
|
|
flex: 2,
|
|
xtype: 'container',
|
|
defaults: {
|
|
padding: 0,
|
|
height: 100
|
|
},
|
|
items: [
|
|
{
|
|
itemId: 'reads',
|
|
xtype: 'pveRunningChart',
|
|
title: gettext('Reads'),
|
|
renderer: PVE.Utils.render_bandwidth
|
|
},
|
|
{
|
|
itemId: 'writes',
|
|
xtype: 'pveRunningChart',
|
|
title: gettext('Writes'),
|
|
renderer: PVE.Utils.render_bandwidth
|
|
},
|
|
{
|
|
itemId: 'iops',
|
|
xtype: 'pveRunningChart',
|
|
hidden: true,
|
|
title: 'IOPS', // do not localize
|
|
renderer: Ext.util.Format.numberRenderer('0,000')
|
|
},
|
|
{
|
|
itemId: 'readiops',
|
|
xtype: 'pveRunningChart',
|
|
hidden: true,
|
|
title: 'IOPS: ' + gettext('Reads'),
|
|
renderer: Ext.util.Format.numberRenderer('0,000')
|
|
},
|
|
{
|
|
itemId: 'writeiops',
|
|
xtype: 'pveRunningChart',
|
|
hidden: true,
|
|
title: 'IOPS: ' + gettext('Writes'),
|
|
renderer: Ext.util.Format.numberRenderer('0,000')
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
],
|
|
|
|
generateCheckData: function(health) {
|
|
var result = [];
|
|
var checks = health.checks || {};
|
|
var keys = Ext.Object.getKeys(checks).sort();
|
|
|
|
Ext.Array.forEach(keys, function(key) {
|
|
var details = checks[key].detail || [];
|
|
result.push({
|
|
id: key,
|
|
summary: checks[key].summary.message,
|
|
detail: Ext.Array.reduce(
|
|
checks[key].detail,
|
|
function(first, second) {
|
|
return first + '\n' + second.message;
|
|
},
|
|
''
|
|
),
|
|
severity: checks[key].severity
|
|
});
|
|
});
|
|
|
|
return result;
|
|
},
|
|
|
|
updateAll: function(store, records, success) {
|
|
if (!success || records.length === 0) {
|
|
return;
|
|
}
|
|
|
|
var me = this;
|
|
var rec = records[0];
|
|
me.status = rec.data;
|
|
|
|
// add health panel
|
|
me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
|
|
// add errors to gridstore
|
|
me.down('#warnings').getStore().loadRawData(me.generateCheckData(rec.data.health || {}), false);
|
|
|
|
// update services
|
|
me.getComponent('services').updateAll(me.metadata || {}, rec.data);
|
|
|
|
// update detailstatus panel
|
|
me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
|
|
|
|
// add performance data
|
|
var used = rec.data.pgmap.bytes_used;
|
|
var total = rec.data.pgmap.bytes_total;
|
|
|
|
var text = Ext.String.format(gettext('{0} of {1}'),
|
|
PVE.Utils.render_size(used),
|
|
PVE.Utils.render_size(total)
|
|
);
|
|
|
|
// update the usage widget
|
|
me.down('#space').updateValue(used/total, text);
|
|
|
|
// TODO: logic for jewel (iops split in read/write)
|
|
|
|
var iops = rec.data.pgmap.op_per_sec;
|
|
var readiops = rec.data.pgmap.read_op_per_sec;
|
|
var writeiops = rec.data.pgmap.write_op_per_sec;
|
|
var reads = rec.data.pgmap.read_bytes_sec || 0;
|
|
var writes = rec.data.pgmap.write_bytes_sec || 0;
|
|
|
|
if (iops !== undefined && me.version !== 'hammer') {
|
|
me.change_version('hammer');
|
|
} else if((readiops !== undefined || writeiops !== undefined) && me.version !== 'jewel') {
|
|
me.change_version('jewel');
|
|
}
|
|
// update the graphs
|
|
me.reads.addDataPoint(reads);
|
|
me.writes.addDataPoint(writes);
|
|
me.iops.addDataPoint(iops);
|
|
me.readiops.addDataPoint(readiops);
|
|
me.writeiops.addDataPoint(writeiops);
|
|
},
|
|
|
|
change_version: function(version) {
|
|
var me = this;
|
|
me.version = version;
|
|
me.sp.set('ceph-version', version);
|
|
me.iops.setVisible(version === 'hammer');
|
|
me.readiops.setVisible(version === 'jewel');
|
|
me.writeiops.setVisible(version === 'jewel');
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
|
|
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');
|
|
|
|
// get ceph version
|
|
me.sp = Ext.state.Manager.getProvider();
|
|
me.version = me.sp.get('ceph-version');
|
|
me.change_version(me.version);
|
|
|
|
var regex = new RegExp("not (installed|initialized)", "i");
|
|
PVE.Utils.handleStoreErrorOrMask(me, me.store, regex, function(me, error){
|
|
me.store.stopUpdate();
|
|
PVE.Utils.showCephInstallOrMask(me, error.statusText, (nodename || 'localhost'),
|
|
function(win){
|
|
me.mon(win, 'cephInstallWindowClosed', function(){
|
|
me.store.startUpdate();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
|
|
me.mon(me.store, 'load', me.updateAll, me);
|
|
me.mon(me.metadatastore, 'load', function(store, records, success) {
|
|
if (!success || records.length < 1) {
|
|
return;
|
|
}
|
|
var rec = records[0];
|
|
me.metadata = rec.data;
|
|
|
|
// update services
|
|
me.getComponent('services').updateAll(rec.data, me.status || {});
|
|
|
|
// update detailstatus panel
|
|
me.getComponent('statusdetail').updateAll(rec.data, me.status || {});
|
|
|
|
}, 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,
|
|
oldosds: []
|
|
},
|
|
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="oldosds.length > 0">',
|
|
'<i class="fa fa-refresh warning"></i> ' + gettext('Outdated OSDs') + "<br>",
|
|
'<div class="osds">',
|
|
'<tpl for="oldosds">',
|
|
'<div class="left-aligned">osd.{id}:</div>',
|
|
'<div class="right-aligned">{version}</div><br />',
|
|
'<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',
|
|
'#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,
|
|
|
|
// working
|
|
activating: 2,
|
|
backfill_wait: 2,
|
|
backfilling: 2,
|
|
creating: 2,
|
|
deep: 2,
|
|
degraded: 2,
|
|
forced_backfill: 2,
|
|
forced_recovery: 2,
|
|
peered: 2,
|
|
peering: 2,
|
|
recovering: 2,
|
|
recovery_wait: 2,
|
|
repair: 2,
|
|
scrubbing: 2,
|
|
snaptrim: 2,
|
|
snaptrim_wait: 2,
|
|
|
|
// error
|
|
backfill_toofull: 3,
|
|
backfill_unfound: 3,
|
|
down: 3,
|
|
incomplete: 3,
|
|
inconsistent: 3,
|
|
recovery_toofull: 3,
|
|
recovery_unfound: 3,
|
|
remapped: 3,
|
|
snaptrim_error: 3,
|
|
stale: 3,
|
|
undersized: 3
|
|
},
|
|
|
|
statecategories: [
|
|
{
|
|
text: gettext('Unknown'),
|
|
count: 0,
|
|
states: [],
|
|
cls: 'faded'
|
|
},
|
|
{
|
|
text: gettext('Clean'),
|
|
cls: 'good'
|
|
},
|
|
{
|
|
text: gettext('Working'),
|
|
cls: 'warning'
|
|
},
|
|
{
|
|
text: gettext('Error'),
|
|
cls: 'critical'
|
|
}
|
|
],
|
|
|
|
updateAll: function(metadata, status) {
|
|
var me = this;
|
|
me.suspendLayout = true;
|
|
|
|
var maxversion = "0";
|
|
Object.values(metadata.version || {}).forEach(function(version) {
|
|
if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
|
|
maxversion = version;
|
|
}
|
|
});
|
|
|
|
var oldosds = [];
|
|
|
|
if (metadata.osd) {
|
|
metadata.osd.forEach(function(osd) {
|
|
var version = PVE.Utils.parse_ceph_version(osd);
|
|
if (version != maxversion) {
|
|
oldosds.push({
|
|
id: osd.id,
|
|
version: version
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
var pgmap = status.pgmap || {};
|
|
var health = status.health || {};
|
|
var osdmap = status.osdmap || { osdmap: {} };
|
|
|
|
|
|
// update pgs sorted
|
|
var pgs_by_state = pgmap.pgs_by_state || [];
|
|
pgs_by_state.sort(function(a,b){
|
|
return (a.state_name < b.state_name)?-1:(a.state_name === b.state_name)?0:1;
|
|
});
|
|
|
|
me.statecategories.forEach(function(cat) {
|
|
cat.count = 0;
|
|
cat.states = [];
|
|
});
|
|
|
|
pgs_by_state.forEach(function(state) {
|
|
var i;
|
|
var states = state.state_name.split(/[^a-z]+/);
|
|
var result = 0;
|
|
for (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.getComponent('pgchart').getStore().setData(me.statecategories);
|
|
me.getComponent('pgs').update({states: pgs_by_state});
|
|
|
|
var downinregex = /(\d+) osds down/;
|
|
var downin_osds = 0;
|
|
|
|
// we collect monitor/osd information from the checks
|
|
Ext.Object.each(health.checks, function(key, value, obj) {
|
|
var found = null;
|
|
if (key === 'OSD_DOWN') {
|
|
found = value.summary.message.match(downinregex);
|
|
if (found !== null) {
|
|
downin_osds = parseInt(found[1],10);
|
|
}
|
|
}
|
|
});
|
|
|
|
// update osds counts
|
|
|
|
var total_osds = osdmap.osdmap.num_osds || 0;
|
|
var in_osds = osdmap.osdmap.num_in_osds || 0;
|
|
var up_osds = osdmap.osdmap.num_up_osds || 0;
|
|
var out_osds = total_osds - in_osds;
|
|
var down_osds = total_osds - up_osds;
|
|
|
|
var downout_osds = down_osds - downin_osds;
|
|
var upin_osds = in_osds - downin_osds;
|
|
var upout_osds = up_osds - upin_osds;
|
|
var osds = {
|
|
total: total_osds,
|
|
upin: upin_osds,
|
|
upout: upout_osds,
|
|
downin: downin_osds,
|
|
downout: downout_osds,
|
|
oldosds: oldosds
|
|
};
|
|
var osdcomponent = me.getComponent('osds');
|
|
osdcomponent.update(Ext.apply(osdcomponent.data, osds));
|
|
|
|
me.suspendLayout = false;
|
|
me.updateLayout();
|
|
}
|
|
});
|
|
|
|
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;
|
|
|
|
var healthstates = {
|
|
'HEALTH_UNKNOWN': 0,
|
|
'HEALTH_ERR': 1,
|
|
'HEALTH_WARN': 2,
|
|
'HEALTH_OLD': 3,
|
|
'HEALTH_OK': 4
|
|
};
|
|
var healthmap = [
|
|
'HEALTH_UNKNOWN',
|
|
'HEALTH_ERR',
|
|
'HEALTH_WARN',
|
|
'HEALTH_OLD',
|
|
'HEALTH_OK'
|
|
];
|
|
var reduceFn = function(first, second) {
|
|
return first + '\n' + second.message;
|
|
};
|
|
var services = ['mon','mgr','mds'];
|
|
var maxversion = "00.0.00";
|
|
Object.values(metadata.version || {}).forEach(function(version) {
|
|
if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
|
|
maxversion = version;
|
|
}
|
|
});
|
|
var i;
|
|
var quorummap = (status && status.quorum_names) ? status.quorum_names : [];
|
|
var monmessages = {};
|
|
var mgrmessages = {};
|
|
var mdsmessages = {};
|
|
if (status) {
|
|
if (status.health) {
|
|
Ext.Object.each(status.health.checks, function(key, value, obj) {
|
|
if (!Ext.String.startsWith(key, "MON_")) {
|
|
return;
|
|
}
|
|
|
|
var i;
|
|
for (i = 0; i < value.detail.length; i++) {
|
|
var match = value.detail[i].message.match(/mon.([a-zA-Z0-9\-\.]+)/);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
var monid = match[1];
|
|
|
|
if (!monmessages[monid]) {
|
|
monmessages[monid] = {
|
|
worstSeverity: healthstates.HEALTH_OK,
|
|
messages: []
|
|
};
|
|
}
|
|
|
|
|
|
monmessages[monid].messages.push(
|
|
PVE.Utils.get_ceph_icon_html(value.severity, true) +
|
|
Ext.Array.reduce(value.detail, reduceFn, '')
|
|
);
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
|
|
var 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 (i = 0; i < services.length; i++) {
|
|
var type = services[i];
|
|
var ids = Object.keys(metadata[type] || {});
|
|
me[type] = {};
|
|
|
|
var j;
|
|
for (j = 0; j < ids.length; j++) {
|
|
var id = ids[j];
|
|
var tmp = id.split('@');
|
|
var name = tmp[0];
|
|
var host = tmp[1];
|
|
var 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') + ": " + result.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 (result.version != maxversion) {
|
|
if (result.health > healthstates.HEALTH_OLD) {
|
|
result.health = healthstates.HEALTH_OLD;
|
|
}
|
|
result.messages.push(
|
|
PVE.Utils.get_ceph_icon_html('HEALTH_OLD', true) +
|
|
gettext('Not Current Version, please upgrade')
|
|
);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
var i;
|
|
list.sort(function(a,b) {
|
|
return a.id > b.id ? 1 : a.id < b.id ? -1 : 0;
|
|
});
|
|
var ids = {};
|
|
if (me.ids) {
|
|
me.ids.forEach(function(id) {
|
|
ids[id] = true;
|
|
});
|
|
}
|
|
for (i = 0; i < list.length; i++) {
|
|
var service = me.getComponent(list[i].id);
|
|
if (!service) {
|
|
// since services are already sorted, and
|
|
// we always have a sorted list
|
|
// we can add it at the service+1 position (because of the title)
|
|
service = me.insert(i+1, {
|
|
xtype: 'pveCephServiceWidget',
|
|
itemId: list[i].id
|
|
});
|
|
if (!me.ids) {
|
|
me.ids = [];
|
|
}
|
|
me.ids.push(list[i].id);
|
|
} else {
|
|
delete ids[list[i].id];
|
|
}
|
|
service.updateService(list[i].title, list[i].text, list[i].health);
|
|
}
|
|
|
|
Object.keys(ids).forEach(function(id) {
|
|
me.remove(id);
|
|
});
|
|
me.suspendLayout = false;
|
|
me.updateLayout();
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
me.callParent();
|
|
me.getComponent('title').update({
|
|
title: me.title
|
|
});
|
|
}
|
|
});
|
|
|
|
/*jslint confusion: true*/
|
|
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() {
|
|
var me = this;
|
|
if (me.tooltip) {
|
|
me.tooltip.destroy();
|
|
delete me.tooltip;
|
|
}
|
|
},
|
|
mouseenter: {
|
|
element: 'el',
|
|
fn: function(events, element) {
|
|
var me = this.component;
|
|
if (!me) {
|
|
return;
|
|
}
|
|
if (!me.tooltip) {
|
|
me.tooltip = Ext.create('Ext.tip.ToolTip', {
|
|
target: me.el,
|
|
trackMouse: true,
|
|
dismissDelay: 0,
|
|
renderTo: Ext.getBody(),
|
|
html: me.data.text
|
|
});
|
|
}
|
|
me.tooltip.show();
|
|
}
|
|
},
|
|
mouseleave: {
|
|
element: 'el',
|
|
fn: function(events, element) {
|
|
var me = this.component;
|
|
if (me.tooltip) {
|
|
me.tooltip.destroy();
|
|
delete me.tooltip;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.node.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/configdb';
|
|
|
|
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/config',
|
|
listeners: {
|
|
activate: function() {
|
|
me.load();
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.CephConfigCrush', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveNodeCephConfigCrush',
|
|
|
|
onlineHelp: 'chapter_pveceph',
|
|
|
|
layout: 'border',
|
|
items: [{
|
|
title: gettext('Configuration'),
|
|
xtype: 'pveNodeCephConfig',
|
|
region: 'center'
|
|
},
|
|
{
|
|
title: 'Crush Map', // do not localize
|
|
xtype: 'pveNodeCephCrushMap',
|
|
region: 'east',
|
|
split: true,
|
|
width: '50%'
|
|
},
|
|
{
|
|
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.ceph.Log', {
|
|
extend: 'Proxmox.panel.LogView',
|
|
xtype: 'cephLogView',
|
|
nodename: undefined,
|
|
failCallback: function(response) {
|
|
var me = this;
|
|
var msg = response.htmlStatus;
|
|
var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename,
|
|
function(win){
|
|
me.mon(win, 'cephInstallWindowClosed', function(){
|
|
me.loadTask.delay(200);
|
|
});
|
|
}
|
|
);
|
|
if (!windowShow) {
|
|
Proxmox.Utils.setErrorMask(me, msg);
|
|
}
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.ceph.CephInstallWizard', {
|
|
extend: 'PVE.window.Wizard',
|
|
alias: 'widget.pveCephInstallWizard',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
resizable: false,
|
|
nodename: undefined,
|
|
viewModel: {
|
|
data: {
|
|
nodename: '',
|
|
configuration: true,
|
|
isInstalled: false
|
|
}
|
|
},
|
|
cbindData: {
|
|
nodename: undefined
|
|
},
|
|
title: gettext('Setup'),
|
|
navigateNext: function() {
|
|
var tp = this.down('#wizcontent');
|
|
var atab = tp.getActiveTab();
|
|
|
|
var next = tp.items.indexOf(atab) + 1;
|
|
var ntab = tp.items.getAt(next);
|
|
if (ntab) {
|
|
ntab.enable();
|
|
tp.setActiveTab(ntab);
|
|
}
|
|
},
|
|
setInitialTab: function (index) {
|
|
var tp = this.down('#wizcontent');
|
|
var initialTab = tp.items.getAt(index);
|
|
initialTab.enable();
|
|
tp.setActiveTab(initialTab);
|
|
},
|
|
onShow: function() {
|
|
this.callParent(arguments);
|
|
var isInstalled = this.getViewModel().get('isInstalled');
|
|
if (isInstalled) {
|
|
this.getViewModel().set('configuration', false);
|
|
this.setInitialTab(2);
|
|
}
|
|
},
|
|
items: [
|
|
{
|
|
title: gettext('Info'),
|
|
xtype: 'panel',
|
|
border: false,
|
|
bodyBorder: false,
|
|
onlineHelp: 'chapter_pveceph',
|
|
html: '<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, click on the next button below to start the installation.'+
|
|
' This wizard will guide you through the necessary steps, after the initial installation you will be offered to create an initial configuration.'+
|
|
' The configuration step is only needed once per cluster and will be skipped if a config is already present.</p>'+
|
|
'<p>Please take a look at our documentation, by clicking the help button below, before starting the installation, '+
|
|
'if you want to gain deeper knowledge about Ceph visit <a target="_blank" href="http://docs.ceph.com/docs/master/">ceph.com</a>.</p>',
|
|
listeners: {
|
|
activate: function() {
|
|
// notify owning container that it should display a help button
|
|
if (this.onlineHelp) {
|
|
Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
|
|
}
|
|
this.up('pveCephInstallWizard').down('#back').hide(true);
|
|
this.up('pveCephInstallWizard').down('#next').setText(gettext('Start installation'));
|
|
},
|
|
deactivate: function() {
|
|
if (this.onlineHelp) {
|
|
Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
|
|
}
|
|
this.up('pveCephInstallWizard').down('#next').setText(gettext('Next'));
|
|
}
|
|
}
|
|
},
|
|
{
|
|
title: gettext('Installation'),
|
|
xtype: 'panel',
|
|
layout: 'fit',
|
|
cbind:{
|
|
nodename: '{nodename}'
|
|
},
|
|
viewModel: {}, // needed to inherit parent viewModel data
|
|
listeners: {
|
|
afterrender: function() {
|
|
var me = this;
|
|
if (this.getViewModel().get('isInstalled')) {
|
|
this.mask("Ceph is already installed, click next to create your configuration.",['pve-static-mask']);
|
|
} else {
|
|
me.down('pveNoVncConsole').fireEvent('activate');
|
|
}
|
|
},
|
|
activate: function() {
|
|
var me = this;
|
|
var nodename = me.nodename;
|
|
me.updateStore = Ext.create('Proxmox.data.UpdateStore', {
|
|
storeid: 'ceph-status-' + nodename,
|
|
interval: 1000,
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + nodename + '/ceph/status'
|
|
},
|
|
listeners: {
|
|
load: function(rec, response, success, operation) {
|
|
|
|
if (success) {
|
|
me.updateStore.stopUpdate();
|
|
me.down('textfield').setValue('success');
|
|
} else if (operation.error.statusText.match("not initialized", "i")) {
|
|
me.updateStore.stopUpdate();
|
|
me.up('pveCephInstallWizard').getViewModel().set('configuration',false);
|
|
me.down('textfield').setValue('success');
|
|
} else if (operation.error.statusText.match("rados_connect failed", "i")) {
|
|
me.updateStore.stopUpdate();
|
|
me.up('pveCephInstallWizard').getViewModel().set('configuration',true);
|
|
me.down('textfield').setValue('success');
|
|
} else if (!operation.error.statusText.match("not installed", "i")) {
|
|
Proxmox.Utils.setErrorMask(me, operation.error.statusText);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
me.updateStore.startUpdate();
|
|
},
|
|
destroy: function() {
|
|
var me = this;
|
|
if (me.updateStore) {
|
|
me.updateStore.stopUpdate();
|
|
}
|
|
}
|
|
},
|
|
items: [
|
|
{
|
|
itemId: 'jsconsole',
|
|
consoleType: 'cmd',
|
|
xtermjs: true,
|
|
xtype: 'pveNoVncConsole',
|
|
cbind:{
|
|
nodename: '{nodename}'
|
|
},
|
|
cmd: 'ceph_install'
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'installSuccess',
|
|
value: '',
|
|
allowBlank: false,
|
|
submitValue: false,
|
|
hidden: true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'inputpanel',
|
|
title: gettext('Configuration'),
|
|
onlineHelp: 'chapter_pveceph',
|
|
cbind: {
|
|
nodename: '{nodename}'
|
|
},
|
|
viewModel: {
|
|
data: {
|
|
replicas: undefined,
|
|
minreplicas: undefined
|
|
}
|
|
},
|
|
listeners: {
|
|
activate: function() {
|
|
this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next'));
|
|
},
|
|
beforeshow: function() {
|
|
if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
|
|
this.mask("Coniguration already initialized",['pve-static-mask']);
|
|
} else {
|
|
this.unmask();
|
|
}
|
|
},
|
|
deactivate: function() {
|
|
this.up('pveCephInstallWizard').down('#submit').setText(gettext('Finish'));
|
|
}
|
|
},
|
|
column1: [
|
|
{
|
|
xtype: 'displayfield',
|
|
value: gettext('Ceph cluster configuration') + ':'
|
|
},
|
|
{
|
|
xtype: 'proxmoxNetworkSelector',
|
|
name: 'network',
|
|
value: '',
|
|
fieldLabel: 'Public Network IP/CIDR',
|
|
bind: {
|
|
allowBlank: '{configuration}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxNetworkSelector',
|
|
name: 'cluster-network',
|
|
fieldLabel: 'Cluster Network IP/CIDR',
|
|
allowBlank: true,
|
|
autoSelect: false,
|
|
emptyText: gettext('Same as Public Network')
|
|
}
|
|
// FIXME: add hint about cluster network and/or reference user to docs??
|
|
],
|
|
column2: [
|
|
{
|
|
xtype: 'displayfield',
|
|
value: gettext('First Ceph monitor') + ':'
|
|
},
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
fieldLabel: gettext('Monitor node'),
|
|
name: 'mon-node',
|
|
selectCurNode: true,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
value: gettext('Additional monitors are recommended. They can be created at any time in the Monitor tab.'),
|
|
userCls: 'pve-hint'
|
|
}
|
|
],
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'size',
|
|
fieldLabel: 'Number of replicas',
|
|
bind: {
|
|
value: '{replicas}'
|
|
},
|
|
maxValue: 7,
|
|
minValue: 2,
|
|
emptyText: '3'
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'min_size',
|
|
fieldLabel: 'Minimum replicas',
|
|
bind: {
|
|
maxValue: '{replicas}',
|
|
value: '{minreplicas}'
|
|
},
|
|
minValue: 2,
|
|
maxValue: 3,
|
|
setMaxValue: function(value) {
|
|
this.maxValue = Ext.Number.from(value, 2);
|
|
// allow enough to avoid split brains with max 'size', but more makes simply no sense
|
|
if (this.maxValue > 4) {
|
|
this.maxValue = 4;
|
|
}
|
|
this.toggleSpinners();
|
|
this.validate();
|
|
},
|
|
emptyText: '2'
|
|
}
|
|
],
|
|
onGetValues: function(values) {
|
|
['cluster-network', 'size', 'min_size'].forEach(function(field) {
|
|
if (!values[field]) {
|
|
delete values[field];
|
|
}
|
|
});
|
|
return values;
|
|
},
|
|
onSubmit: function() {
|
|
var me = this;
|
|
if (!this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
|
|
var wizard = me.up('window');
|
|
var kv = wizard.getValues();
|
|
delete kv['delete'];
|
|
var monNode = kv['mon-node'];
|
|
delete kv['mon-node'];
|
|
var nodename = me.nodename;
|
|
delete kv.nodename;
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + nodename + '/ceph/init',
|
|
waitMsgTarget: wizard,
|
|
method: 'POST',
|
|
params: kv,
|
|
success: function() {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + monNode + '/ceph/mon/' + monNode,
|
|
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 completed, 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.DiskList', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: 'widget.pveNodeDiskList',
|
|
|
|
emptyText: gettext('No Disks found'),
|
|
|
|
stateful: true,
|
|
stateId: 'grid-node-disks',
|
|
|
|
columns: [
|
|
{
|
|
header: gettext('Device'),
|
|
width: 150,
|
|
sortable: true,
|
|
dataIndex: 'devpath'
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
width: 80,
|
|
sortable: true,
|
|
dataIndex: 'type',
|
|
renderer: function(v) {
|
|
if (v === 'ssd') {
|
|
return 'SSD';
|
|
} else if (v === 'hdd') {
|
|
return 'Hard Disk';
|
|
} else if (v === 'usb'){
|
|
return 'USB';
|
|
} else {
|
|
return gettext('Unknown');
|
|
}
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Usage'),
|
|
width: 150,
|
|
sortable: false,
|
|
renderer: function(v, metaData, rec) {
|
|
if (rec) {
|
|
if (rec.data.osdid >= 0) {
|
|
var bluestore = '';
|
|
if (rec.data.bluestore === 1) {
|
|
bluestore = ' (Bluestore)';
|
|
}
|
|
return "Ceph osd." + rec.data.osdid.toString() + bluestore;
|
|
}
|
|
|
|
var types = [];
|
|
if (rec.data.journals > 0) {
|
|
types.push('Journal');
|
|
}
|
|
|
|
if (rec.data.db > 0) {
|
|
types.push('DB');
|
|
}
|
|
|
|
if (rec.data.wal > 0) {
|
|
types.push('WAL');
|
|
}
|
|
|
|
if (types.length > 0) {
|
|
return 'Ceph (' + types.join(', ') + ')';
|
|
}
|
|
}
|
|
|
|
return v || Proxmox.Utils.noText;
|
|
},
|
|
dataIndex: 'used'
|
|
},
|
|
{
|
|
header: gettext('Size'),
|
|
width: 100,
|
|
align: 'right',
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'size'
|
|
},
|
|
{
|
|
header: 'GPT',
|
|
width: 60,
|
|
align: 'right',
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
dataIndex: 'gpt'
|
|
},
|
|
{
|
|
header: gettext('Vendor'),
|
|
width: 100,
|
|
sortable: true,
|
|
hidden: true,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'vendor'
|
|
},
|
|
{
|
|
header: gettext('Model'),
|
|
width: 200,
|
|
sortable: true,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'model'
|
|
},
|
|
{
|
|
header: gettext('Serial'),
|
|
width: 200,
|
|
sortable: true,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'serial'
|
|
},
|
|
{
|
|
header: 'S.M.A.R.T.',
|
|
width: 100,
|
|
sortable: true,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'health'
|
|
},
|
|
{
|
|
header: 'Wearout',
|
|
width: 90,
|
|
sortable: true,
|
|
align: 'right',
|
|
dataIndex: 'wearout',
|
|
renderer: function(value) {
|
|
if (Ext.isNumeric(value)) {
|
|
return (100 - value).toString() + '%';
|
|
}
|
|
return 'N/A';
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
storeid: 'node-disk-list' + nodename,
|
|
model: 'node-disk-list',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + nodename + "/disks/list"
|
|
},
|
|
sorters: [
|
|
{
|
|
property : 'dev',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
});
|
|
|
|
var reloadButton = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Reload'),
|
|
handler: function() {
|
|
me.store.load();
|
|
}
|
|
});
|
|
|
|
var smartButton = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Show S.M.A.R.T. values'),
|
|
selModel: sm,
|
|
enableFn: function() {
|
|
return !!sm.getSelection().length;
|
|
},
|
|
disabled: true,
|
|
handler: function() {
|
|
var rec = sm.getSelection()[0];
|
|
|
|
var win = Ext.create('PVE.DiskSmartWindow', {
|
|
nodename: nodename,
|
|
dev: rec.data.devpath
|
|
});
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
var initButton = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Initialize Disk with GPT'),
|
|
selModel: sm,
|
|
enableFn: function() {
|
|
var selection = sm.getSelection();
|
|
|
|
if (!selection.length || selection[0].data.used) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
},
|
|
disabled: true,
|
|
|
|
handler: function() {
|
|
var rec = sm.getSelection()[0];
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/nodes/' + nodename + '/disks/initgpt',
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
params: { disk: rec.data.devpath},
|
|
failure: function(response, options) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', {
|
|
upid: upid
|
|
});
|
|
win.show();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
me.loadCount = 1; // avoid duplicate loadmask
|
|
Proxmox.Utils.monStoreErrors(me, store);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: [ reloadButton, smartButton, initButton ],
|
|
listeners: {
|
|
itemdblclick: function() {
|
|
var rec = sm.getSelection()[0];
|
|
|
|
var win = Ext.create('PVE.DiskSmartWindow', {
|
|
nodename: nodename,
|
|
dev: rec.data.devpath
|
|
});
|
|
win.show();
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
me.callParent();
|
|
me.store.load();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('node-disk-list', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'devpath', 'used', { name: 'size', type: 'number'},
|
|
{name: 'osdid', type: 'number'},
|
|
'vendor', 'model', 'serial', 'rpm', 'type', 'health', 'wearout' ],
|
|
idProperty: 'devpath'
|
|
});
|
|
});
|
|
|
|
Ext.define('PVE.DiskSmartWindow', {
|
|
extend: 'Ext.window.Window',
|
|
alias: 'widget.pveSmartWindow',
|
|
|
|
modal: true,
|
|
|
|
items: [
|
|
{
|
|
xtype: 'gridpanel',
|
|
layout: {
|
|
type: 'fit'
|
|
},
|
|
emptyText: gettext('No S.M.A.R.T. Values'),
|
|
scrollable: true,
|
|
flex: 1,
|
|
itemId: 'smarts',
|
|
reserveScrollbar: true,
|
|
columns: [
|
|
{ text: 'ID', dataIndex: 'id', width: 50 },
|
|
{ text: gettext('Attribute'), flex: 1, dataIndex: 'name', renderer: Ext.String.htmlEncode },
|
|
{ text: gettext('Value'), dataIndex: 'raw', renderer: Ext.String.htmlEncode },
|
|
{ text: gettext('Normalized'), dataIndex: 'value', width: 60},
|
|
{ text: gettext('Threshold'), dataIndex: 'threshold', width: 60},
|
|
{ text: gettext('Worst'), dataIndex: 'worst', width: 60},
|
|
{ text: gettext('Flags'), dataIndex: 'flags'},
|
|
{ text: gettext('Failing'), dataIndex: 'fail', renderer: Ext.String.htmlEncode }
|
|
]
|
|
},
|
|
{
|
|
xtype: 'component',
|
|
itemId: 'text',
|
|
layout: {
|
|
type: 'fit'
|
|
},
|
|
hidden: true,
|
|
style: {
|
|
'background-color': 'white',
|
|
'white-space': 'pre',
|
|
'font-family': 'monospace'
|
|
}
|
|
}
|
|
],
|
|
|
|
buttons: [
|
|
{
|
|
text: gettext('Reload'),
|
|
name: 'reload',
|
|
handler: function() {
|
|
var me = this;
|
|
me.up('window').store.reload();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Close'),
|
|
name: 'close',
|
|
handler: function() {
|
|
var me = this;
|
|
me.up('window').close();
|
|
}
|
|
}
|
|
],
|
|
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
width: 800,
|
|
height: 500,
|
|
minWidth: 600,
|
|
minHeight: 400,
|
|
bodyPadding: 5,
|
|
title: gettext('S.M.A.R.T. Values'),
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.nodename;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var dev = me.dev;
|
|
if (!dev) {
|
|
throw "no device specified";
|
|
}
|
|
|
|
me.store = Ext.create('Ext.data.Store', {
|
|
model: 'disk-smart',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + nodename + "/disks/smart?disk=" + dev
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
var grid = me.down('#smarts');
|
|
var text = me.down('#text');
|
|
|
|
Proxmox.Utils.monStoreErrors(grid, me.store);
|
|
me.mon(me.store, 'load', function(s, records, success) {
|
|
if (success && records.length > 0) {
|
|
var rec = records[0];
|
|
switch (rec.data.type) {
|
|
case 'text':
|
|
grid.setVisible(false);
|
|
text.setVisible(true);
|
|
text.setHtml(Ext.String.htmlEncode(rec.data.text));
|
|
break;
|
|
default:
|
|
// includes 'ata'
|
|
// cannot use empty case because
|
|
// of jslint
|
|
grid.setVisible(true);
|
|
text.setVisible(false);
|
|
grid.setStore(rec.attributes());
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
me.store.load();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('disk-smart', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
{ name:'health'},
|
|
{ name:'type'},
|
|
{ name:'text'}
|
|
],
|
|
hasMany: {model: 'smart-attribute', name: 'attributes'}
|
|
});
|
|
Ext.define('smart-attribute', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
{ name:'id', type:'number' }, 'name', 'value', 'worst', 'threshold', 'flags', 'fail', 'raw'
|
|
]
|
|
});
|
|
});
|
|
Ext.define('PVE.node.CreateLVM', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveCreateLVM',
|
|
|
|
subject: 'LVM Volume Group',
|
|
|
|
showProgress: true,
|
|
|
|
onlineHelp: 'chapter_lvm',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.isCreate = true;
|
|
|
|
Ext.applyIf(me, {
|
|
url: "/nodes/" + me.nodename + "/disks/lvm",
|
|
method: 'POST',
|
|
items: [
|
|
{
|
|
xtype: 'pveDiskSelector',
|
|
name: 'device',
|
|
nodename: me.nodename,
|
|
diskType: 'unused',
|
|
fieldLabel: gettext('Disk'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'name',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'add_storage',
|
|
fieldLabel: gettext('Add Storage'),
|
|
value: '1'
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.LVMList', {
|
|
extend: 'Ext.tree.Panel',
|
|
xtype: 'pveLVMList',
|
|
emptyText: gettext('No Volume Groups found'),
|
|
stateful: true,
|
|
stateId: 'grid-node-lvm',
|
|
columns: [
|
|
{
|
|
xtype: 'treecolumn',
|
|
text: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1
|
|
},
|
|
{
|
|
text: gettext('Number of LVs'),
|
|
dataIndex: 'lvcount',
|
|
width: 150,
|
|
align: 'right'
|
|
},
|
|
{
|
|
header: gettext('Usage'),
|
|
width: 110,
|
|
dataIndex: 'usage',
|
|
tdCls: 'x-progressbar-default-cell',
|
|
xtype: 'widgetcolumn',
|
|
widget: {
|
|
xtype: 'pveProgressBar'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Size'),
|
|
width: 100,
|
|
align: 'right',
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'size'
|
|
},
|
|
{
|
|
header: gettext('Free'),
|
|
width: 100,
|
|
align: 'right',
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'free'
|
|
}
|
|
],
|
|
|
|
rootVisible: false,
|
|
useArrows: true,
|
|
|
|
tbar: [
|
|
{
|
|
text: gettext('Reload'),
|
|
iconCls: 'fa fa-refresh',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
me.reload();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Create') + ': Volume Group',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
var win = Ext.create('PVE.node.CreateLVM', {
|
|
nodename: me.nodename,
|
|
taskDone: function() {
|
|
me.reload();
|
|
}
|
|
}).show();
|
|
}
|
|
}
|
|
],
|
|
|
|
reload: function() {
|
|
var me = this;
|
|
var sm = me.getSelectionModel();
|
|
Proxmox.Utils.API2Request({
|
|
url: "/nodes/" + me.nodename + "/disks/lvm",
|
|
waitMsgTarget: me,
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(me, response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
sm.deselectAll();
|
|
me.setRootNode(response.result.data);
|
|
me.expandAll();
|
|
}
|
|
});
|
|
},
|
|
|
|
listeners: {
|
|
activate: function() {
|
|
var me = this;
|
|
me.reload();
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var sm = Ext.create('Ext.selection.TreeModel', {});
|
|
|
|
Ext.apply(me, {
|
|
selModel: sm,
|
|
fields: ['name', 'size', 'free',
|
|
{
|
|
type: 'string',
|
|
name: 'iconCls',
|
|
calculate: function(data) {
|
|
var txt = 'fa x-fa-tree fa-';
|
|
txt += (data.leaf) ? 'hdd-o' : 'object-group';
|
|
return txt;
|
|
}
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'usage',
|
|
calculate: function(data) {
|
|
return ((data.size-data.free)/data.size);
|
|
}
|
|
}
|
|
],
|
|
sorters: 'name'
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.reload();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.CreateLVMThin', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveCreateLVMThin',
|
|
|
|
subject: 'LVM Thinpool',
|
|
|
|
showProgress: true,
|
|
|
|
onlineHelp: 'chapter_lvm',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.isCreate = true;
|
|
|
|
Ext.applyIf(me, {
|
|
url: "/nodes/" + me.nodename + "/disks/lvmthin",
|
|
method: 'POST',
|
|
items: [
|
|
{
|
|
xtype: 'pveDiskSelector',
|
|
name: 'device',
|
|
nodename: me.nodename,
|
|
diskType: 'unused',
|
|
fieldLabel: gettext('Disk'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'name',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'add_storage',
|
|
fieldLabel: gettext('Add Storage'),
|
|
value: '1'
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.LVMThinList', {
|
|
extend: 'Ext.grid.Panel',
|
|
xtype: 'pveLVMThinList',
|
|
|
|
emptyText: gettext('No thinpools found'),
|
|
stateful: true,
|
|
stateId: 'grid-node-lvmthin',
|
|
columns: [
|
|
{
|
|
text: gettext('Name'),
|
|
dataIndex: 'lv',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Usage'),
|
|
width: 110,
|
|
dataIndex: 'usage',
|
|
tdCls: 'x-progressbar-default-cell',
|
|
xtype: 'widgetcolumn',
|
|
widget: {
|
|
xtype: 'pveProgressBar'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Size'),
|
|
width: 100,
|
|
align: 'right',
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'lv_size'
|
|
},
|
|
{
|
|
header: gettext('Used'),
|
|
width: 100,
|
|
align: 'right',
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'used'
|
|
},
|
|
{
|
|
header: gettext('Metadata Usage'),
|
|
width: 120,
|
|
dataIndex: 'metadata_usage',
|
|
tdCls: 'x-progressbar-default-cell',
|
|
xtype: 'widgetcolumn',
|
|
widget: {
|
|
xtype: 'pveProgressBar'
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Metadata Size'),
|
|
width: 120,
|
|
align: 'right',
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'metadata_size'
|
|
},
|
|
{
|
|
header: gettext('Metadata Used'),
|
|
width: 125,
|
|
align: 'right',
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'metadata_used'
|
|
}
|
|
],
|
|
|
|
rootVisible: false,
|
|
useArrows: true,
|
|
|
|
tbar: [
|
|
{
|
|
text: gettext('Reload'),
|
|
iconCls: 'fa fa-refresh',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
me.reload();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Create') + ': Thinpool',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
var win = Ext.create('PVE.node.CreateLVMThin', {
|
|
nodename: me.nodename,
|
|
taskDone: function() {
|
|
me.reload();
|
|
}
|
|
}).show();
|
|
}
|
|
}
|
|
],
|
|
|
|
reload: function() {
|
|
var me = this;
|
|
me.store.load();
|
|
me.store.sort();
|
|
},
|
|
|
|
listeners: {
|
|
activate: function() {
|
|
var me = this;
|
|
me.reload();
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
store: {
|
|
fields: ['lv', 'lv_size', 'used', 'metadata_size', 'metadata_used',
|
|
{
|
|
type: 'number',
|
|
name: 'usage',
|
|
calculate: function(data) {
|
|
return data.used/data.lv_size;
|
|
}
|
|
},
|
|
{
|
|
type: 'number',
|
|
name: 'metadata_usage',
|
|
calculate: function(data) {
|
|
return data.metadata_used/data.metadata_size;
|
|
}
|
|
}
|
|
],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + me.nodename + '/disks/lvmthin'
|
|
},
|
|
sorters: 'lv'
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
|
|
me.reload();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.CreateDirectory', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveCreateDirectory',
|
|
|
|
subject: Proxmox.Utils.directoryText,
|
|
|
|
showProgress: true,
|
|
|
|
onlineHelp: 'chapter_storage',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.isCreate = true;
|
|
|
|
Ext.applyIf(me, {
|
|
url: "/nodes/" + me.nodename + "/disks/directory",
|
|
method: 'POST',
|
|
items: [
|
|
{
|
|
xtype: 'pveDiskSelector',
|
|
name: 'device',
|
|
nodename: me.nodename,
|
|
diskType: 'unused',
|
|
fieldLabel: gettext('Disk'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
comboItems: [
|
|
['ext4', 'ext4'],
|
|
['xfs', 'xfs']
|
|
],
|
|
fieldLabel: gettext('Filesystem'),
|
|
name: 'filesystem',
|
|
value: '',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'name',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'add_storage',
|
|
fieldLabel: gettext('Add Storage'),
|
|
value: '1'
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.Directorylist', {
|
|
extend: 'Ext.grid.Panel',
|
|
xtype: 'pveDirectoryList',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-node-directory',
|
|
columns: [
|
|
{
|
|
text: gettext('Path'),
|
|
dataIndex: 'path',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Device'),
|
|
flex: 1,
|
|
dataIndex: 'device'
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
width: 100,
|
|
dataIndex: 'type'
|
|
},
|
|
{
|
|
header: gettext('Options'),
|
|
width: 100,
|
|
dataIndex: 'options'
|
|
},
|
|
{
|
|
header: gettext('Unit File'),
|
|
hidden: true,
|
|
dataIndex: 'unitfile'
|
|
}
|
|
],
|
|
|
|
rootVisible: false,
|
|
useArrows: true,
|
|
|
|
tbar: [
|
|
{
|
|
text: gettext('Reload'),
|
|
iconCls: 'fa fa-refresh',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
me.reload();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Create') + ': Directory',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
var win = Ext.create('PVE.node.CreateDirectory', {
|
|
nodename: me.nodename
|
|
}).show();
|
|
win.on('destroy', function() { me.reload(); });
|
|
}
|
|
}
|
|
],
|
|
|
|
reload: function() {
|
|
var me = this;
|
|
me.store.load();
|
|
me.store.sort();
|
|
},
|
|
|
|
listeners: {
|
|
activate: function() {
|
|
var me = this;
|
|
me.reload();
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
store: {
|
|
fields: ['path', 'device', 'type', 'options', 'unitfile' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + me.nodename + '/disks/directory'
|
|
},
|
|
sorters: 'path'
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
|
|
me.reload();
|
|
}
|
|
});
|
|
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.node.CreateZFS', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveCreateZFS',
|
|
|
|
subject: 'ZFS',
|
|
|
|
showProgress: true,
|
|
|
|
onlineHelp: 'chapter_zfs',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.isCreate = true;
|
|
|
|
var update_disklist = function() {
|
|
var grid = me.down('#disklist');
|
|
var disks = grid.getSelection();
|
|
|
|
var val = [];
|
|
disks.sort(function(a,b) {
|
|
var aorder = a.get('order') || 0;
|
|
var border = b.get('order') || 0;
|
|
return (aorder - border);
|
|
});
|
|
|
|
disks.forEach(function(disk) {
|
|
val.push(disk.get('devpath'));
|
|
});
|
|
|
|
me.down('field[name=devices]').setValue(val.join(','));
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: '/nodes/' + me.nodename + '/disks/zfs',
|
|
method: 'POST',
|
|
items: [
|
|
{
|
|
xtype: 'inputpanel',
|
|
onGetValues: function(values) {
|
|
return values;
|
|
},
|
|
column1: [
|
|
{
|
|
xtype: 'textfield',
|
|
hidden: true,
|
|
name: 'devices',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'name',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'add_storage',
|
|
fieldLabel: gettext('Add Storage'),
|
|
value: '1'
|
|
}
|
|
],
|
|
column2: [
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
fieldLabel: gettext('RAID Level'),
|
|
name: 'raidlevel',
|
|
value: 'single',
|
|
comboItems: [
|
|
['single', gettext('Single Disk')],
|
|
['mirror', 'Mirror'],
|
|
['raid10', 'RAID10'],
|
|
['raidz', 'RAIDZ'],
|
|
['raidz2', 'RAIDZ2'],
|
|
['raidz3', 'RAIDZ3']
|
|
]
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
fieldLabel: gettext('Compression'),
|
|
name: 'compression',
|
|
value: 'on',
|
|
comboItems: [
|
|
['on', 'on'],
|
|
['off', 'off'],
|
|
['gzip', 'gzip'],
|
|
['lz4', 'lz4'],
|
|
['lzjb', 'lzjb'],
|
|
['zle', 'zle']
|
|
]
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('ashift'),
|
|
minValue: 9,
|
|
maxValue: 16,
|
|
value: '12',
|
|
name: 'ashift'
|
|
}
|
|
],
|
|
columnB: [
|
|
{
|
|
xtype: 'grid',
|
|
height: 200,
|
|
emptyText: gettext('No Disks unused'),
|
|
itemId: 'disklist',
|
|
selModel: 'checkboxmodel',
|
|
listeners: {
|
|
selectionchange: update_disklist
|
|
},
|
|
store: {
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/disks/list?type=unused'
|
|
}
|
|
},
|
|
columns: [
|
|
{
|
|
text: gettext('Device'),
|
|
dataIndex: 'devpath',
|
|
flex: 1
|
|
},
|
|
{
|
|
text: gettext('Serial'),
|
|
dataIndex: 'serial'
|
|
},
|
|
{
|
|
text: gettext('Size'),
|
|
dataIndex: 'size',
|
|
renderer: PVE.Utils.render_size
|
|
},
|
|
{
|
|
header: gettext('Order'),
|
|
xtype: 'widgetcolumn',
|
|
dataIndex: 'order',
|
|
sortable: true,
|
|
widget: {
|
|
xtype: 'proxmoxintegerfield',
|
|
minValue: 1,
|
|
isFormField: false,
|
|
listeners: {
|
|
change: function(numberfield, value, old_value) {
|
|
var record = numberfield.getWidgetRecord();
|
|
record.set('order', value);
|
|
update_disklist(record);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
padding: '5 0 0 0',
|
|
userCls: 'pve-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();
|
|
me.down('#disklist').getStore().load();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.ZFSDevices', {
|
|
extend: 'Ext.tree.Panel',
|
|
xtype: 'pveZFSDevices',
|
|
stateful: true,
|
|
stateId: 'grid-node-zfsstatus',
|
|
columns: [
|
|
{
|
|
xtype: 'treecolumn',
|
|
text: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1
|
|
},
|
|
{
|
|
text: gettext('Health'),
|
|
renderer: PVE.Utils.render_zfs_health,
|
|
dataIndex: 'state'
|
|
},
|
|
{
|
|
text: 'READ',
|
|
dataIndex: 'read'
|
|
},
|
|
{
|
|
text: 'WRITE',
|
|
dataIndex: 'write'
|
|
},
|
|
{
|
|
text: 'CKSUM',
|
|
dataIndex: 'cksum'
|
|
},
|
|
{
|
|
text: gettext('Message'),
|
|
dataIndex: 'msg'
|
|
}
|
|
],
|
|
|
|
rootVisible: true,
|
|
|
|
reload: function() {
|
|
var me = this;
|
|
var sm = me.getSelectionModel();
|
|
Proxmox.Utils.API2Request({
|
|
url: "/nodes/" + me.nodename + "/disks/zfs/" + me.zpool,
|
|
waitMsgTarget: me,
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(me, response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
sm.deselectAll();
|
|
me.setRootNode(response.result.data);
|
|
me.expandAll();
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.zpool) {
|
|
throw "no zpool specified";
|
|
}
|
|
|
|
var sm = Ext.create('Ext.selection.TreeModel', {});
|
|
|
|
Ext.apply(me, {
|
|
selModel: sm,
|
|
fields: ['name', 'status',
|
|
{
|
|
type: 'string',
|
|
name: 'iconCls',
|
|
calculate: function(data) {
|
|
var txt = 'fa x-fa-tree fa-';
|
|
if (data.leaf) {
|
|
return txt + 'hdd-o';
|
|
}
|
|
}
|
|
}
|
|
],
|
|
sorters: 'name'
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.reload();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.ZFSStatus', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
xtype: 'pveZFSStatus',
|
|
layout: 'fit',
|
|
border: false,
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.zpool) {
|
|
throw "no zpool specified";
|
|
}
|
|
|
|
me.url = "/api2/extjs/nodes/" + me.nodename + "/disks/zfs/" + me.zpool;
|
|
|
|
me.rows = {
|
|
scan: {
|
|
header: gettext('Scan')
|
|
},
|
|
status: {
|
|
header: gettext('Status')
|
|
},
|
|
action: {
|
|
header: gettext('Action')
|
|
},
|
|
errors: {
|
|
header: gettext('Errors')
|
|
}
|
|
};
|
|
|
|
me.callParent();
|
|
me.reload();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.ZFSList', {
|
|
extend: 'Ext.grid.Panel',
|
|
xtype: 'pveZFSList',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-node-zfs',
|
|
columns: [
|
|
{
|
|
text: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Size'),
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'size'
|
|
},
|
|
{
|
|
header: gettext('Free'),
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'free'
|
|
},
|
|
{
|
|
header: gettext('Allocated'),
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'alloc'
|
|
},
|
|
{
|
|
header: gettext('Fragmentation'),
|
|
renderer: function(value) {
|
|
return value.toString() + '%';
|
|
},
|
|
dataIndex: 'frag'
|
|
},
|
|
{
|
|
header: gettext('Health'),
|
|
renderer: PVE.Utils.render_zfs_health,
|
|
dataIndex: 'health'
|
|
},
|
|
{
|
|
header: gettext('Deduplication'),
|
|
hidden: true,
|
|
renderer: function(value) {
|
|
return value.toFixed(2).toString() + 'x';
|
|
},
|
|
dataIndex: 'dedup'
|
|
}
|
|
],
|
|
|
|
rootVisible: false,
|
|
useArrows: true,
|
|
|
|
tbar: [
|
|
{
|
|
text: gettext('Reload'),
|
|
iconCls: 'fa fa-refresh',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
me.reload();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Create') + ': ZFS',
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
var win = Ext.create('PVE.node.CreateZFS', {
|
|
nodename: me.nodename
|
|
}).show();
|
|
win.on('destroy', function() { me.reload(); });
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Detail'),
|
|
itemId: 'detailbtn',
|
|
disabled: true,
|
|
handler: function() {
|
|
var me = this.up('panel');
|
|
var selection = me.getSelection();
|
|
if (selection.length < 1) {
|
|
return;
|
|
}
|
|
me.show_detail(selection[0].get('name'));
|
|
}
|
|
}
|
|
],
|
|
|
|
show_detail: function(zpool) {
|
|
var me = this;
|
|
|
|
var detailsgrid = Ext.create('PVE.node.ZFSStatus', {
|
|
layout: 'fit',
|
|
nodename: me.nodename,
|
|
flex: 0,
|
|
zpool: zpool
|
|
});
|
|
|
|
var devicetree = Ext.create('PVE.node.ZFSDevices', {
|
|
title: gettext('Devices'),
|
|
nodename: me.nodename,
|
|
flex: 1,
|
|
zpool: zpool
|
|
});
|
|
|
|
|
|
var win = Ext.create('Ext.window.Window', {
|
|
modal: true,
|
|
width: 800,
|
|
height: 400,
|
|
resizable: true,
|
|
layout: 'fit',
|
|
title: gettext('Status') + ': ' + zpool,
|
|
items:[{
|
|
xtype: 'panel',
|
|
region: 'center',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
items: [detailsgrid, devicetree],
|
|
tbar: [{
|
|
text: gettext('Reload'),
|
|
iconCls: 'fa fa-refresh',
|
|
handler: function() {
|
|
|
|
devicetree.reload();
|
|
detailsgrid.reload();
|
|
}
|
|
}]
|
|
}]
|
|
}).show();
|
|
},
|
|
|
|
set_button_status: function() {
|
|
var me = this;
|
|
var selection = me.getSelection();
|
|
me.down('#detailbtn').setDisabled(selection.length === 0);
|
|
},
|
|
|
|
reload: function() {
|
|
var me = this;
|
|
me.store.load();
|
|
me.store.sort();
|
|
},
|
|
|
|
listeners: {
|
|
activate: function() {
|
|
var me = this;
|
|
me.reload();
|
|
},
|
|
selectionchange: function() {
|
|
this.set_button_status();
|
|
},
|
|
itemdblclick: function(grid, record) {
|
|
var me = this;
|
|
me.show_detail(record.get('name'));
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
store: {
|
|
fields: ['name', 'size', 'free', 'alloc', 'dedup', 'frag', 'health'],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/nodes/" + me.nodename + '/disks/zfs'
|
|
},
|
|
sorters: 'name'
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.getStore(), true);
|
|
me.reload();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.StatusView', {
|
|
extend: 'PVE.panel.StatusView',
|
|
alias: 'widget.pveNodeStatus',
|
|
|
|
height: 300,
|
|
bodyPadding: '20 15 20 15',
|
|
|
|
layout: {
|
|
type: 'table',
|
|
columns: 2,
|
|
tableAttrs: {
|
|
style: {
|
|
width: '100%'
|
|
}
|
|
}
|
|
},
|
|
|
|
defaults: {
|
|
xtype: 'pveInfoWidget',
|
|
padding: '0 15 5 15'
|
|
},
|
|
|
|
items: [
|
|
{
|
|
itemId: 'cpu',
|
|
iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon',
|
|
title: gettext('CPU usage'),
|
|
valueField: 'cpu',
|
|
maxField: 'cpuinfo',
|
|
renderer: PVE.Utils.render_node_cpu_usage
|
|
},
|
|
{
|
|
itemId: 'wait',
|
|
iconCls: 'fa fa-fw fa-clock-o',
|
|
title: gettext('IO delay'),
|
|
valueField: 'wait',
|
|
rowspan: 2
|
|
},
|
|
{
|
|
itemId: 'load',
|
|
iconCls: 'fa fa-fw fa-tasks',
|
|
title: gettext('Load average'),
|
|
printBar: false,
|
|
textField: 'loadavg'
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
colspan: 2,
|
|
padding: '0 0 20 0'
|
|
},
|
|
{
|
|
iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon',
|
|
itemId: 'memory',
|
|
title: gettext('RAM usage'),
|
|
valueField: 'memory',
|
|
maxField: 'memory',
|
|
renderer: PVE.Utils.render_node_size_usage
|
|
},
|
|
{
|
|
itemId: 'ksm',
|
|
printBar: false,
|
|
title: gettext('KSM sharing'),
|
|
textField: 'ksm',
|
|
renderer: function(record) {
|
|
return PVE.Utils.render_size(record.shared);
|
|
},
|
|
padding: '0 15 10 15'
|
|
},
|
|
{
|
|
iconCls: 'fa fa-fw fa-hdd-o',
|
|
itemId: 'rootfs',
|
|
title: gettext('HD space') + '(root)',
|
|
valueField: 'rootfs',
|
|
maxField: 'rootfs',
|
|
renderer: PVE.Utils.render_node_size_usage
|
|
},
|
|
{
|
|
iconCls: 'fa fa-fw fa-refresh',
|
|
itemId: 'swap',
|
|
printSize: true,
|
|
title: gettext('SWAP usage'),
|
|
valueField: 'swap',
|
|
maxField: 'swap',
|
|
renderer: PVE.Utils.render_node_size_usage
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
colspan: 2,
|
|
padding: '0 0 20 0'
|
|
},
|
|
{
|
|
itemId: 'cpus',
|
|
colspan: 2,
|
|
printBar: false,
|
|
title: gettext('CPU(s)'),
|
|
textField: 'cpuinfo',
|
|
renderer: function(cpuinfo) {
|
|
return cpuinfo.cpus + " x " + cpuinfo.model + " (" +
|
|
cpuinfo.sockets.toString() + " " +
|
|
(cpuinfo.sockets > 1 ?
|
|
gettext('Sockets') :
|
|
gettext('Socket')
|
|
) + ")";
|
|
},
|
|
value: ''
|
|
},
|
|
{
|
|
itemId: 'kversion',
|
|
colspan: 2,
|
|
title: gettext('Kernel Version'),
|
|
printBar: false,
|
|
textField: 'kversion',
|
|
value: ''
|
|
},
|
|
{
|
|
itemId: 'version',
|
|
colspan: 2,
|
|
printBar: false,
|
|
title: gettext('PVE Manager Version'),
|
|
textField: 'pveversion',
|
|
value: ''
|
|
}
|
|
],
|
|
|
|
updateTitle: function() {
|
|
var me = this;
|
|
var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime'));
|
|
me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')');
|
|
}
|
|
|
|
});
|
|
Ext.define('PVE.node.Summary', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveNodeSummary',
|
|
|
|
scrollable: true,
|
|
bodyPadding: 5,
|
|
|
|
showVersions: function() {
|
|
var me = this;
|
|
|
|
// Note: we use simply text/html here, because ExtJS grid has problems
|
|
// with cut&paste
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
|
|
var view = Ext.createWidget('component', {
|
|
autoScroll: true,
|
|
padding: 5,
|
|
style: {
|
|
'background-color': 'white',
|
|
'white-space': 'pre',
|
|
'font-family': 'monospace'
|
|
}
|
|
});
|
|
|
|
var win = Ext.create('Ext.window.Window', {
|
|
title: gettext('Package versions'),
|
|
width: 600,
|
|
height: 400,
|
|
layout: 'fit',
|
|
modal: true,
|
|
items: [ view ]
|
|
});
|
|
|
|
Proxmox.Utils.API2Request({
|
|
waitMsgTarget: me,
|
|
url: "/nodes/" + nodename + "/apt/versions",
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
win.close();
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
win.show();
|
|
var text = '';
|
|
|
|
Ext.Array.each(response.result.data, function(rec) {
|
|
var version = "not correctly installed";
|
|
var pkg = rec.Package;
|
|
if (rec.OldVersion && rec.CurrentState === 'Installed') {
|
|
version = rec.OldVersion;
|
|
}
|
|
if (rec.RunningKernel) {
|
|
text += pkg + ': ' + version + ' (running kernel: ' +
|
|
rec.RunningKernel + ')\n';
|
|
} else if (rec.ManagerVersion) {
|
|
text += pkg + ': ' + version + ' (running version: ' +
|
|
rec.ManagerVersion + ')\n';
|
|
} else {
|
|
text += pkg + ': ' + version + '\n';
|
|
}
|
|
});
|
|
|
|
view.update(Ext.htmlEncode(text));
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.statusStore) {
|
|
throw "no status storage specified";
|
|
}
|
|
|
|
var rstore = me.statusStore;
|
|
|
|
var version_btn = new Ext.Button({
|
|
text: gettext('Package versions'),
|
|
handler: function(){
|
|
Proxmox.Utils.checked_command(function() { me.showVersions(); });
|
|
}
|
|
});
|
|
|
|
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
|
|
rrdurl: "/api2/json/nodes/" + nodename + "/rrddata",
|
|
model: 'pve-rrd-node'
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' } ],
|
|
items: [
|
|
{
|
|
xtype: 'container',
|
|
layout: 'column',
|
|
defaults: {
|
|
minHeight: 320,
|
|
padding: 5,
|
|
plugins: 'responsive',
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: 1
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: 0.5
|
|
}
|
|
}
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'pveNodeStatus',
|
|
rstore: rstore,
|
|
width: 770,
|
|
pveSelNode: me.pveSelNode
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('CPU usage'),
|
|
fields: ['cpu','iowait'],
|
|
fieldTitles: [gettext('CPU usage'), gettext('IO delay')],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Server load'),
|
|
fields: ['loadavg'],
|
|
fieldTitles: [gettext('Load average')],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Memory usage'),
|
|
fields: ['memtotal','memused'],
|
|
fieldTitles: [gettext('Total'), gettext('RAM usage')],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Network traffic'),
|
|
fields: ['netin','netout'],
|
|
store: rrdstore
|
|
}
|
|
]
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); },
|
|
destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); }
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*global Blob*/
|
|
Ext.define('PVE.node.SubscriptionKeyEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
title: gettext('Upload Subscription Key'),
|
|
width: 300,
|
|
items: {
|
|
xtype: 'textfield',
|
|
name: 'key',
|
|
value: '',
|
|
fieldLabel: gettext('Subscription Key')
|
|
},
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.Subscription', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
|
|
alias: ['widget.pveNodeSubscription'],
|
|
|
|
onlineHelp: 'getting_help',
|
|
|
|
viewConfig: {
|
|
enableTextSelection: true
|
|
},
|
|
|
|
showReport: function() {
|
|
var me = this;
|
|
var nodename = me.pveSelNode.data.node;
|
|
|
|
var getReportFileName = function() {
|
|
var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i');
|
|
return me.nodename + '-report-' + now + '.txt';
|
|
};
|
|
|
|
var view = Ext.createWidget('component', {
|
|
itemId: 'system-report-view',
|
|
scrollable: true,
|
|
style: {
|
|
'background-color': 'white',
|
|
'white-space': 'pre',
|
|
'font-family': 'monospace',
|
|
padding: '5px'
|
|
}
|
|
});
|
|
|
|
var reportWindow = Ext.create('Ext.window.Window', {
|
|
title: gettext('System Report'),
|
|
width: 1024,
|
|
height: 600,
|
|
layout: 'fit',
|
|
modal: true,
|
|
buttons: [
|
|
'->',
|
|
{
|
|
text: gettext('Download'),
|
|
handler: function() {
|
|
var fileContent = reportWindow.getComponent('system-report-view').html;
|
|
var fileName = getReportFileName();
|
|
|
|
// Internet Explorer
|
|
if (window.navigator.msSaveOrOpenBlob) {
|
|
navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName);
|
|
} else {
|
|
var element = document.createElement('a');
|
|
element.setAttribute('href', 'data:text/plain;charset=utf-8,'
|
|
+ encodeURIComponent(fileContent));
|
|
element.setAttribute('download', fileName);
|
|
element.style.display = 'none';
|
|
document.body.appendChild(element);
|
|
element.click();
|
|
document.body.removeChild(element);
|
|
}
|
|
}
|
|
}
|
|
],
|
|
items: view
|
|
});
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/nodes/' + me.nodename + '/report',
|
|
method: 'GET',
|
|
waitMsgTarget: me,
|
|
failure: function(response) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response) {
|
|
var report = Ext.htmlEncode(response.result.data);
|
|
reportWindow.show();
|
|
view.update(report);
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var reload = function() {
|
|
me.rstore.load();
|
|
};
|
|
|
|
var baseurl = '/nodes/' + me.nodename + '/subscription';
|
|
|
|
var render_status = function(value) {
|
|
|
|
var message = me.getObjectValue('message');
|
|
|
|
if (message) {
|
|
return value + ": " + message;
|
|
}
|
|
return value;
|
|
};
|
|
|
|
var rows = {
|
|
productname: {
|
|
header: gettext('Type')
|
|
},
|
|
key: {
|
|
header: gettext('Subscription Key')
|
|
},
|
|
status: {
|
|
header: gettext('Status'),
|
|
renderer: render_status
|
|
},
|
|
message: {
|
|
visible: false
|
|
},
|
|
serverid: {
|
|
header: gettext('Server ID')
|
|
},
|
|
sockets: {
|
|
header: gettext('Sockets')
|
|
},
|
|
checktime: {
|
|
header: gettext('Last checked'),
|
|
renderer: Proxmox.Utils.render_timestamp
|
|
},
|
|
nextduedate: {
|
|
header: gettext('Next due date')
|
|
}
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: '/api2/json' + baseurl,
|
|
cwidth1: 170,
|
|
tbar: [
|
|
{
|
|
text: gettext('Upload Subscription Key'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.node.SubscriptionKeyEdit', {
|
|
url: '/api2/extjs/' + baseurl
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Check'),
|
|
handler: function() {
|
|
Proxmox.Utils.API2Request({
|
|
params: { force: 1 },
|
|
url: baseurl,
|
|
method: 'POST',
|
|
waitMsgTarget: me,
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
callback: reload
|
|
});
|
|
}
|
|
},
|
|
{
|
|
text: gettext('System Report'),
|
|
handler: function() {
|
|
Proxmox.Utils.checked_command(function (){ me.showReport(); });
|
|
}
|
|
}
|
|
],
|
|
rows: rows,
|
|
listeners: {
|
|
activate: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.node.CertificateView', {
|
|
extend: 'Ext.container.Container',
|
|
xtype: 'pveCertificatesView',
|
|
|
|
onlineHelp: 'sysadmin_certificate_management',
|
|
|
|
mixins: ['Proxmox.Mixin.CBind' ],
|
|
|
|
items: [
|
|
{
|
|
xtype: 'pveCertView',
|
|
border: 0,
|
|
cbind: {
|
|
nodename: '{nodename}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'pveACMEView',
|
|
border: 0,
|
|
cbind: {
|
|
nodename: '{nodename}'
|
|
}
|
|
}
|
|
]
|
|
|
|
});
|
|
|
|
Ext.define('PVE.node.CertificateViewer', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
title: gettext('Certificate'),
|
|
|
|
fieldDefaults: {
|
|
labelWidth: 120
|
|
},
|
|
width: 800,
|
|
resizable: true,
|
|
|
|
items: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Name'),
|
|
name: 'filename'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Fingerprint'),
|
|
name: 'fingerprint'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Issuer'),
|
|
name: 'issuer'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Subject'),
|
|
name: 'subject'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Valid Since'),
|
|
renderer: Proxmox.Utils.render_timestamp,
|
|
name: 'notbefore'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Expires'),
|
|
renderer: Proxmox.Utils.render_timestamp,
|
|
name: 'notafter'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Subject Alternative Names'),
|
|
name: 'san',
|
|
renderer: PVE.Utils.render_san
|
|
},
|
|
{
|
|
xtype: 'textarea',
|
|
editable: false,
|
|
grow: true,
|
|
growMax: 200,
|
|
fieldLabel: gettext('Certificate'),
|
|
name: 'pem'
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.cert) {
|
|
throw "no cert given";
|
|
}
|
|
|
|
if (!me.nodename) {
|
|
throw "no nodename given";
|
|
}
|
|
|
|
me.url = '/nodes/' + me.nodename + '/certificates/info';
|
|
me.callParent();
|
|
|
|
// hide OK/Reset button, because we just want to show data
|
|
me.down('toolbar[dock=bottom]').setVisible(false);
|
|
|
|
me.load({
|
|
success: function(response) {
|
|
if (Ext.isArray(response.result.data)) {
|
|
Ext.Array.each(response.result.data, function(item) {
|
|
if (item.filename === me.cert) {
|
|
me.setValues(item);
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.CertUpload', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveCertUpload',
|
|
|
|
title: gettext('Upload Custom Certificate'),
|
|
resizable: false,
|
|
isCreate: true,
|
|
submitText: gettext('Upload'),
|
|
method: 'POST',
|
|
width: 600,
|
|
|
|
apiCallDone: function(success, response, options) {
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
|
|
Ext.getBody().mask(txt, ['pve-static-mask']);
|
|
// reload after 10 seconds automatically
|
|
Ext.defer(function() {
|
|
window.location.reload(true);
|
|
}, 10000);
|
|
},
|
|
|
|
items: [
|
|
{
|
|
fieldLabel: gettext('Private Key (Optional)'),
|
|
labelAlign: 'top',
|
|
emptyText: gettext('No change'),
|
|
name: 'key',
|
|
xtype: 'textarea'
|
|
},
|
|
{
|
|
xtype: 'filebutton',
|
|
text: gettext('From File'),
|
|
listeners: {
|
|
change: function(btn, e, value) {
|
|
var me = this.up('form');
|
|
e = e.event;
|
|
Ext.Array.each(e.target.files, function(file) {
|
|
PVE.Utils.loadSSHKeyFromFile(file, function(res) {
|
|
me.down('field[name=key]').setValue(res);
|
|
});
|
|
});
|
|
btn.reset();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
autoEl: 'hr'
|
|
},
|
|
{
|
|
fieldLabel: gettext('Certificate Chain'),
|
|
labelAlign: 'top',
|
|
allowBlank: false,
|
|
name: 'certificates',
|
|
xtype: 'textarea'
|
|
},
|
|
{
|
|
xtype: 'filebutton',
|
|
text: gettext('From File'),
|
|
listeners: {
|
|
change: function(btn, e, value) {
|
|
var me = this.up('form');
|
|
e = e.event;
|
|
Ext.Array.each(e.target.files, function(file) {
|
|
PVE.Utils.loadSSHKeyFromFile(file, function(res) {
|
|
me.down('field[name=certificates]').setValue(res);
|
|
});
|
|
});
|
|
btn.reset();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'hidden',
|
|
name: 'restart',
|
|
value: '1'
|
|
},
|
|
{
|
|
xtype: 'hidden',
|
|
name: 'force',
|
|
value: '1'
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no nodename given";
|
|
}
|
|
|
|
me.url = '/nodes/' + me.nodename + '/certificates/custom';
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('pve-certificate', {
|
|
extend: 'Ext.data.Model',
|
|
|
|
fields: [ 'filename', 'fingerprint', 'issuer', 'notafter', 'notbefore', 'subject', 'san' ],
|
|
idProperty: 'filename'
|
|
});
|
|
|
|
Ext.define('PVE.node.Certificates', {
|
|
extend: 'Ext.grid.Panel',
|
|
xtype: 'pveCertView',
|
|
|
|
tbar: [
|
|
{
|
|
xtype: 'button',
|
|
text: gettext('Upload Custom Certificate'),
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
var win = Ext.create('PVE.node.CertUpload', {
|
|
nodename: me.nodename
|
|
});
|
|
win.show();
|
|
win.on('destroy', me.reload, me);
|
|
}
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
itemId: 'deletebtn',
|
|
text: gettext('Delete Custom Certificate'),
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/certificates/custom?restart=1',
|
|
method: 'DELETE',
|
|
success: function(response, opt) {
|
|
var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
|
|
Ext.getBody().mask(txt, ['pve-static-mask']);
|
|
// reload after 10 seconds automatically
|
|
Ext.defer(function() {
|
|
window.location.reload(true);
|
|
}, 10000);
|
|
},
|
|
failure: function(response, opt) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
'-',
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
itemId: 'viewbtn',
|
|
disabled: true,
|
|
text: gettext('View Certificate'),
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
me.view_certificate();
|
|
}
|
|
}
|
|
],
|
|
|
|
columns: [
|
|
{
|
|
header: gettext('File'),
|
|
width: 150,
|
|
dataIndex: 'filename'
|
|
},
|
|
{
|
|
header: gettext('Issuer'),
|
|
flex: 1,
|
|
dataIndex: 'issuer'
|
|
},
|
|
{
|
|
header: gettext('Subject'),
|
|
flex: 1,
|
|
dataIndex: 'subject'
|
|
},
|
|
{
|
|
header: gettext('Valid Since'),
|
|
width: 150,
|
|
dataIndex: 'notbefore',
|
|
renderer: Proxmox.Utils.render_timestamp
|
|
},
|
|
{
|
|
header: gettext('Expires'),
|
|
width: 150,
|
|
dataIndex: 'notafter',
|
|
renderer: Proxmox.Utils.render_timestamp
|
|
},
|
|
{
|
|
header: gettext('Subject Alternative Names'),
|
|
flex: 1,
|
|
dataIndex: 'san',
|
|
renderer: PVE.Utils.render_san
|
|
},
|
|
{
|
|
header: gettext('Fingerprint'),
|
|
dataIndex: 'fingerprint',
|
|
hidden: true
|
|
},
|
|
{
|
|
header: gettext('PEM'),
|
|
dataIndex: 'pem',
|
|
hidden: true
|
|
}
|
|
],
|
|
|
|
reload: function() {
|
|
var me = this;
|
|
me.rstore.load();
|
|
},
|
|
|
|
set_button_status: function() {
|
|
var me = this;
|
|
var rec = me.rstore.getById('pveproxy-ssl.pem');
|
|
|
|
me.down('#deletebtn').setDisabled(!rec);
|
|
},
|
|
|
|
view_certificate: function() {
|
|
var me = this;
|
|
var selection = me.getSelection();
|
|
if (!selection || selection.length < 1) {
|
|
return;
|
|
}
|
|
var win = Ext.create('PVE.node.CertificateViewer', {
|
|
cert: selection[0].data.filename,
|
|
nodename : me.nodename
|
|
});
|
|
win.show();
|
|
},
|
|
|
|
listeners: {
|
|
itemdblclick: 'view_certificate'
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no nodename given";
|
|
}
|
|
|
|
me.rstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
storeid: 'certs-' + me.nodename,
|
|
model: 'pve-certificate',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/certificates/info'
|
|
}
|
|
});
|
|
|
|
me.store = {
|
|
type: 'diff',
|
|
rstore: me.rstore
|
|
};
|
|
|
|
me.callParent();
|
|
|
|
me.mon(me.rstore, 'load', me.set_button_status, me);
|
|
me.rstore.startUpdate();
|
|
}
|
|
});
|
|
Ext.define('PVE.node.ACMEEditor', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveACMEEditor',
|
|
|
|
subject: gettext('Domains'),
|
|
items: [
|
|
{
|
|
xtype: 'inputpanel',
|
|
items: [
|
|
{
|
|
xtype: 'textarea',
|
|
fieldLabel: gettext('Domains'),
|
|
emptyText: "domain1.example.com\ndomain2.example.com",
|
|
name: 'domains'
|
|
}
|
|
],
|
|
onGetValues: function(values) {
|
|
if (!values.domains) {
|
|
return {
|
|
'delete': 'acme'
|
|
};
|
|
}
|
|
var domains = values.domains.split(/\n/).join(';');
|
|
return {
|
|
'acme': 'domains=' + domains
|
|
};
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, opts) {
|
|
var res = PVE.Parser.parseACME(response.result.data.acme);
|
|
if (res) {
|
|
res.domains = res.domains.join(' ');
|
|
me.setValues(res);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.ACMEAccountCreate', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
width: 400,
|
|
title: gettext('Register Account'),
|
|
isCreate: true,
|
|
method: 'POST',
|
|
submitText: gettext('Register'),
|
|
url: '/cluster/acme/account',
|
|
showTaskViewer: true,
|
|
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxComboGrid',
|
|
name: 'directory',
|
|
allowBlank: false,
|
|
valueField: 'url',
|
|
displayField: 'name',
|
|
fieldLabel: gettext('ACME Directory'),
|
|
store: {
|
|
autoLoad: true,
|
|
fields: ['name', 'url'],
|
|
idProperty: ['name'],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/cluster/acme/directories'
|
|
},
|
|
sorters: {
|
|
property: 'name',
|
|
order: 'ASC'
|
|
}
|
|
},
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('URL'),
|
|
dataIndex: 'url',
|
|
flex: 1
|
|
}
|
|
]
|
|
},
|
|
listeners: {
|
|
change: function(combogrid, value) {
|
|
var me = this;
|
|
if (!value) {
|
|
return;
|
|
}
|
|
|
|
var disp = me.up('window').down('#tos_url_display');
|
|
var field = me.up('window').down('#tos_url');
|
|
var checkbox = me.up('window').down('#tos_checkbox');
|
|
|
|
disp.setValue(gettext('Loading'));
|
|
field.setValue(undefined);
|
|
checkbox.setValue(undefined);
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/cluster/acme/tos',
|
|
method: 'GET',
|
|
params: {
|
|
directory: value
|
|
},
|
|
success: function(response, opt) {
|
|
me.up('window').down('#tos_url').setValue(response.result.data);
|
|
me.up('window').down('#tos_url_display').setValue(response.result.data);
|
|
},
|
|
failure: function(response, opt) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
itemId: 'tos_url_display',
|
|
fieldLabel: gettext('Terms of Service'),
|
|
renderer: PVE.Utils.render_optional_url,
|
|
name: 'tos_url_display'
|
|
},
|
|
{
|
|
xtype: 'hidden',
|
|
itemId: 'tos_url',
|
|
name: 'tos_url'
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
itemId: 'tos_checkbox',
|
|
fieldLabel: gettext('Accept TOS'),
|
|
submitValue: false,
|
|
validateValue: function(value) {
|
|
if (value && this.checked) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'contact',
|
|
vtype: 'email',
|
|
allowBlank: false,
|
|
fieldLabel: gettext('E-Mail')
|
|
}
|
|
]
|
|
|
|
});
|
|
|
|
Ext.define('PVE.node.ACMEAccountView', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
width: 600,
|
|
fieldDefaults: {
|
|
labelWidth: 140
|
|
},
|
|
|
|
title: gettext('Account'),
|
|
|
|
items: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('E-Mail'),
|
|
name: 'email'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Created'),
|
|
name: 'createdAt'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Status'),
|
|
name: 'status'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Directory'),
|
|
renderer: PVE.Utils.render_optional_url,
|
|
name: 'directory'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Terms of Services'),
|
|
renderer: PVE.Utils.render_optional_url,
|
|
name: 'tos'
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.accountname) {
|
|
throw "no account name defined";
|
|
}
|
|
|
|
me.url = '/cluster/acme/account/' + me.accountname;
|
|
|
|
me.callParent();
|
|
|
|
// hide OK/Reset button, because we just want to show data
|
|
me.down('toolbar[dock=bottom]').setVisible(false);
|
|
|
|
me.load({
|
|
success: function(response) {
|
|
var data = response.result.data;
|
|
data.email = data.account.contact[0];
|
|
data.createdAt = data.account.createdAt;
|
|
data.status = data.account.status;
|
|
me.setValues(data);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.node.ACME', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
xtype: 'pveACMEView',
|
|
|
|
margin: '10 0 0 0',
|
|
title: 'ACME',
|
|
|
|
tbar: [
|
|
{
|
|
xtype: 'button',
|
|
itemId: 'edit',
|
|
text: gettext('Edit Domains'),
|
|
handler: function() {
|
|
this.up('grid').run_editor();
|
|
}
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
itemId: 'createaccount',
|
|
text: gettext('Register Account'),
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
var win = Ext.create('PVE.node.ACMEAccountCreate', {
|
|
taskDone: function() {
|
|
me.load_account();
|
|
me.reload();
|
|
}
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
itemId: 'viewaccount',
|
|
text: gettext('View Account'),
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
var win = Ext.create('PVE.node.ACMEAccountView', {
|
|
accountname: 'default'
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
itemId: 'order',
|
|
text: gettext('Order Certificate'),
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
|
|
Proxmox.Utils.API2Request({
|
|
method: 'POST',
|
|
params: {
|
|
force: 1
|
|
},
|
|
url: '/nodes/' + me.nodename + '/certificates/acme/certificate',
|
|
success: function(response, opt) {
|
|
var win = Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: response.result.data,
|
|
taskDone: function(success) {
|
|
me.certificate_order_finished(success);
|
|
}
|
|
});
|
|
win.show();
|
|
},
|
|
failure: function(response, opt) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
],
|
|
|
|
certificate_order_finished: function(success) {
|
|
if (!success) {
|
|
return;
|
|
}
|
|
var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!');
|
|
Ext.getBody().mask(txt, ['pve-static-mask']);
|
|
// reload after 10 seconds automatically
|
|
Ext.defer(function() {
|
|
window.location.reload(true);
|
|
}, 10000);
|
|
},
|
|
|
|
set_button_status: function() {
|
|
var me = this;
|
|
|
|
var account = !!me.account;
|
|
var acmeObj = PVE.Parser.parseACME(me.getObjectValue('acme'));
|
|
var domains = acmeObj ? acmeObj.domains.length : 0;
|
|
|
|
var order = me.down('#order');
|
|
order.setVisible(account);
|
|
order.setDisabled(!account || !domains);
|
|
|
|
me.down('#createaccount').setVisible(!account);
|
|
me.down('#viewaccount').setVisible(account);
|
|
},
|
|
|
|
load_account: function() {
|
|
var me = this;
|
|
|
|
// for now we only use the 'default' account
|
|
Proxmox.Utils.API2Request({
|
|
url: '/cluster/acme/account/default',
|
|
success: function(response, opt) {
|
|
me.account = response.result.data;
|
|
me.set_button_status();
|
|
},
|
|
failure: function(response, opt) {
|
|
me.account = undefined;
|
|
me.set_button_status();
|
|
}
|
|
});
|
|
},
|
|
|
|
run_editor: function() {
|
|
var me = this;
|
|
var win = Ext.create(me.rows.acme.editor, me.editorConfig);
|
|
win.show();
|
|
win.on('destroy', me.reload, me);
|
|
},
|
|
|
|
listeners: {
|
|
itemdblclick: 'run_editor'
|
|
},
|
|
|
|
// account data gets loaded here
|
|
account: undefined,
|
|
|
|
disableSelection: true,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no nodename given";
|
|
}
|
|
|
|
me.url = '/api2/json/nodes/' + me.nodename + '/config';
|
|
|
|
me.editorConfig = {
|
|
url: '/api2/extjs/nodes/' + me.nodename + '/config'
|
|
};
|
|
/*jslint confusion: true*/
|
|
/*acme is a string above*/
|
|
me.rows = {
|
|
acme: {
|
|
defaultValue: '',
|
|
header: gettext('Domains'),
|
|
editor: 'PVE.node.ACMEEditor',
|
|
renderer: function(value) {
|
|
var acmeObj = PVE.Parser.parseACME(value);
|
|
if (acmeObj) {
|
|
return acmeObj.domains.join('<br>');
|
|
}
|
|
return Proxmox.Utils.noneText;
|
|
}
|
|
}
|
|
};
|
|
/*jslint confusion: false*/
|
|
|
|
me.callParent();
|
|
me.mon(me.rstore, 'load', me.set_button_status, me);
|
|
me.rstore.startUpdate();
|
|
me.load_account();
|
|
}
|
|
});
|
|
Ext.define('PVE.node.Config', {
|
|
extend: 'PVE.panel.Config',
|
|
alias: 'widget.PVE.node.Config',
|
|
|
|
onlineHelp: 'chapter_system_administration',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
|
|
url: "/api2/json/nodes/" + nodename + "/status",
|
|
interval: 1000
|
|
});
|
|
|
|
var node_command = function(cmd) {
|
|
Proxmox.Utils.API2Request({
|
|
params: { command: cmd },
|
|
url: '/nodes/' + nodename + '/status',
|
|
method: 'POST',
|
|
waitMsgTarget: me,
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
var actionBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Bulk Actions'),
|
|
iconCls: 'fa fa-fw fa-ellipsis-v',
|
|
disabled: !caps.nodes['Sys.PowerMgmt'],
|
|
menu: new Ext.menu.Menu({
|
|
items: [
|
|
{
|
|
text: gettext('Bulk Start'),
|
|
iconCls: 'fa fa-fw fa-play',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.BulkAction', {
|
|
nodename: nodename,
|
|
title: gettext('Bulk Start'),
|
|
btnText: gettext('Start'),
|
|
action: 'startall'
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Bulk Stop'),
|
|
iconCls: 'fa fa-fw fa-stop',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.BulkAction', {
|
|
nodename: nodename,
|
|
title: gettext('Bulk Stop'),
|
|
btnText: gettext('Stop'),
|
|
action: 'stopall'
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Bulk Migrate'),
|
|
iconCls: 'fa fa-fw fa-send-o',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.BulkAction', {
|
|
nodename: nodename,
|
|
title: gettext('Bulk Migrate'),
|
|
btnText: gettext('Migrate'),
|
|
action: 'migrateall'
|
|
});
|
|
win.show();
|
|
}
|
|
}
|
|
]
|
|
})
|
|
});
|
|
|
|
var restartBtn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Reboot'),
|
|
disabled: !caps.nodes['Sys.PowerMgmt'],
|
|
dangerous: true,
|
|
confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename),
|
|
handler: function() {
|
|
node_command('reboot');
|
|
},
|
|
iconCls: 'fa fa-undo'
|
|
});
|
|
|
|
var shutdownBtn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Shutdown'),
|
|
disabled: !caps.nodes['Sys.PowerMgmt'],
|
|
dangerous: true,
|
|
confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename),
|
|
handler: function() {
|
|
node_command('shutdown');
|
|
},
|
|
iconCls: 'fa fa-power-off'
|
|
});
|
|
|
|
var shellBtn = Ext.create('PVE.button.ConsoleButton', {
|
|
disabled: !caps.nodes['Sys.Console'],
|
|
text: gettext('Shell'),
|
|
consoleType: 'shell',
|
|
nodename: nodename
|
|
});
|
|
|
|
me.items = [];
|
|
|
|
Ext.apply(me, {
|
|
title: gettext('Node') + " '" + nodename + "'",
|
|
hstateid: 'nodetab',
|
|
defaults: { statusStore: me.statusStore },
|
|
tbar: [ restartBtn, shutdownBtn, shellBtn, actionBtn]
|
|
});
|
|
|
|
if (caps.nodes['Sys.Audit']) {
|
|
me.items.push(
|
|
{
|
|
title: gettext('Summary'),
|
|
iconCls: 'fa fa-book',
|
|
itemId: 'summary',
|
|
xtype: 'pveNodeSummary'
|
|
},
|
|
{
|
|
title: gettext('Notes'),
|
|
iconCls: 'fa fa-sticky-note-o',
|
|
itemId: 'notes',
|
|
xtype: 'pveNotesView'
|
|
}
|
|
);
|
|
}
|
|
|
|
if (caps.nodes['Sys.Console']) {
|
|
me.items.push(
|
|
{
|
|
title: gettext('Shell'),
|
|
iconCls: 'fa fa-terminal',
|
|
itemId: 'jsconsole',
|
|
xtype: 'pveNoVncConsole',
|
|
consoleType: 'shell',
|
|
xtermjs: true,
|
|
nodename: nodename
|
|
}
|
|
);
|
|
}
|
|
|
|
if (caps.nodes['Sys.Audit']) {
|
|
me.items.push(
|
|
{
|
|
title: gettext('System'),
|
|
iconCls: 'fa fa-cogs',
|
|
itemId: 'services',
|
|
expandedOnInit: true,
|
|
startOnlyServices: {
|
|
'pveproxy': true,
|
|
'pvedaemon': true,
|
|
'pve-cluster': true
|
|
},
|
|
nodename: nodename,
|
|
onlineHelp: 'pve_service_daemons',
|
|
xtype: 'proxmoxNodeServiceView'
|
|
},
|
|
{
|
|
title: gettext('Network'),
|
|
iconCls: 'fa fa-exchange',
|
|
itemId: 'network',
|
|
groups: ['services'],
|
|
nodename: nodename,
|
|
onlineHelp: 'sysadmin_network_configuration',
|
|
xtype: 'proxmoxNodeNetworkView'
|
|
},
|
|
{
|
|
title: gettext('Certificates'),
|
|
iconCls: 'fa fa-certificate',
|
|
itemId: 'certificates',
|
|
groups: ['services'],
|
|
nodename: nodename,
|
|
xtype: 'pveCertificatesView'
|
|
},
|
|
{
|
|
title: gettext('DNS'),
|
|
iconCls: 'fa fa-globe',
|
|
groups: ['services'],
|
|
itemId: 'dns',
|
|
nodename: nodename,
|
|
onlineHelp: 'sysadmin_network_configuration',
|
|
xtype: 'proxmoxNodeDNSView'
|
|
},
|
|
{
|
|
title: gettext('Hosts'),
|
|
iconCls: 'fa fa-globe',
|
|
groups: ['services'],
|
|
itemId: 'hosts',
|
|
nodename: nodename,
|
|
onlineHelp: 'sysadmin_network_configuration',
|
|
xtype: 'proxmoxNodeHostsView'
|
|
},
|
|
{
|
|
title: gettext('Time'),
|
|
itemId: 'time',
|
|
groups: ['services'],
|
|
nodename: nodename,
|
|
xtype: 'proxmoxNodeTimeView',
|
|
iconCls: 'fa fa-clock-o'
|
|
});
|
|
}
|
|
|
|
if (caps.nodes['Sys.Syslog']) {
|
|
me.items.push({
|
|
title: 'Syslog',
|
|
iconCls: 'fa fa-list',
|
|
groups: ['services'],
|
|
disabled: !caps.nodes['Sys.Syslog'],
|
|
itemId: 'syslog',
|
|
xtype: 'proxmoxJournalView',
|
|
url: "/api2/extjs/nodes/" + nodename + "/journal"
|
|
});
|
|
|
|
if (caps.nodes['Sys.Modify']) {
|
|
me.items.push({
|
|
title: gettext('Updates'),
|
|
iconCls: 'fa fa-refresh',
|
|
disabled: !caps.nodes['Sys.Console'],
|
|
// do we want to link to system updates instead?
|
|
itemId: 'apt',
|
|
xtype: 'proxmoxNodeAPT',
|
|
upgradeBtn: {
|
|
xtype: 'pveConsoleButton',
|
|
disabled: Proxmox.UserName !== 'root@pam',
|
|
text: gettext('Upgrade'),
|
|
consoleType: 'upgrade',
|
|
nodename: nodename
|
|
},
|
|
nodename: nodename
|
|
});
|
|
}
|
|
}
|
|
|
|
if (caps.nodes['Sys.Audit']) {
|
|
me.items.push(
|
|
{
|
|
xtype: 'pveFirewallRules',
|
|
iconCls: 'fa fa-shield',
|
|
title: gettext('Firewall'),
|
|
allow_iface: true,
|
|
base_url: '/nodes/' + nodename + '/firewall/rules',
|
|
list_refs_url: '/cluster/firewall/refs',
|
|
itemId: 'firewall'
|
|
},
|
|
{
|
|
xtype: 'pveFirewallOptions',
|
|
title: gettext('Options'),
|
|
iconCls: 'fa fa-gear',
|
|
onlineHelp: 'pve_firewall_host_specific_configuration',
|
|
groups: ['firewall'],
|
|
base_url: '/nodes/' + nodename + '/firewall/options',
|
|
fwtype: 'node',
|
|
itemId: 'firewall-options'
|
|
});
|
|
}
|
|
|
|
|
|
if (caps.nodes['Sys.Audit']) {
|
|
me.items.push(
|
|
{
|
|
title: gettext('Disks'),
|
|
itemId: 'storage',
|
|
expandedOnInit: true,
|
|
iconCls: 'fa fa-hdd-o',
|
|
xtype: 'pveNodeDiskList'
|
|
},
|
|
{
|
|
title: 'LVM',
|
|
itemId: 'lvm',
|
|
onlineHelp: 'chapter_lvm',
|
|
iconCls: 'fa fa-square',
|
|
groups: ['storage'],
|
|
xtype: 'pveLVMList'
|
|
},
|
|
{
|
|
title: 'LVM-Thin',
|
|
itemId: 'lvmthin',
|
|
onlineHelp: 'chapter_lvm',
|
|
iconCls: 'fa fa-square-o',
|
|
groups: ['storage'],
|
|
xtype: 'pveLVMThinList'
|
|
},
|
|
{
|
|
title: Proxmox.Utils.directoryText,
|
|
itemId: 'directory',
|
|
onlineHelp: 'chapter_storage',
|
|
iconCls: 'fa fa-folder',
|
|
groups: ['storage'],
|
|
xtype: 'pveDirectoryList'
|
|
},
|
|
{
|
|
title: 'ZFS',
|
|
itemId: 'zfs',
|
|
onlineHelp: 'chapter_zfs',
|
|
iconCls: 'fa fa-th-large',
|
|
groups: ['storage'],
|
|
xtype: 'pveZFSList'
|
|
},
|
|
{
|
|
title: 'Ceph',
|
|
itemId: 'ceph',
|
|
iconCls: 'fa fa-ceph',
|
|
xtype: 'pveNodeCephStatus'
|
|
},
|
|
{
|
|
xtype: 'pveReplicaView',
|
|
iconCls: 'fa fa-retweet',
|
|
title: gettext('Replication'),
|
|
itemId: 'replication'
|
|
},
|
|
{
|
|
xtype: 'pveNodeCephConfigCrush',
|
|
title: gettext('Configuration'),
|
|
iconCls: 'fa fa-gear',
|
|
groups: ['ceph'],
|
|
itemId: 'ceph-config'
|
|
},
|
|
{
|
|
xtype: 'pveNodeCephMonMgr',
|
|
title: gettext('Monitor'),
|
|
iconCls: 'fa fa-tv',
|
|
groups: ['ceph'],
|
|
itemId: 'ceph-monlist'
|
|
},
|
|
{
|
|
xtype: 'pveNodeCephOsdTree',
|
|
title: 'OSD',
|
|
iconCls: 'fa fa-hdd-o',
|
|
groups: ['ceph'],
|
|
itemId: 'ceph-osdtree'
|
|
},
|
|
{
|
|
xtype: 'pveNodeCephFSPanel',
|
|
title: 'CephFS',
|
|
iconCls: 'fa fa-folder',
|
|
groups: ['ceph'],
|
|
nodename: nodename,
|
|
itemId: 'ceph-cephfspanel'
|
|
},
|
|
{
|
|
xtype: 'pveNodeCephPoolList',
|
|
title: 'Pools',
|
|
iconCls: 'fa fa-sitemap',
|
|
groups: ['ceph'],
|
|
itemId: 'ceph-pools'
|
|
}
|
|
);
|
|
}
|
|
|
|
if (caps.nodes['Sys.Syslog']) {
|
|
me.items.push(
|
|
{
|
|
xtype: 'proxmoxLogView',
|
|
title: gettext('Log'),
|
|
iconCls: 'fa fa-list',
|
|
groups: ['firewall'],
|
|
onlineHelp: 'chapter_pve_firewall',
|
|
url: '/api2/extjs/nodes/' + nodename + '/firewall/log',
|
|
itemId: 'firewall-fwlog'
|
|
},
|
|
{
|
|
title: gettext('Log'),
|
|
itemId: 'ceph-log',
|
|
iconCls: 'fa fa-list',
|
|
groups: ['ceph'],
|
|
onlineHelp: 'chapter_pveceph',
|
|
xtype: 'cephLogView',
|
|
url: "/api2/extjs/nodes/" + nodename + "/ceph/log",
|
|
nodename: nodename
|
|
});
|
|
}
|
|
|
|
me.items.push(
|
|
{
|
|
title: gettext('Task History'),
|
|
iconCls: 'fa fa-list',
|
|
itemId: 'tasks',
|
|
nodename: nodename,
|
|
xtype: 'proxmoxNodeTasks'
|
|
},
|
|
{
|
|
title: gettext('Subscription'),
|
|
iconCls: 'fa fa-support',
|
|
itemId: 'support',
|
|
xtype: 'pveNodeSubscription',
|
|
nodename: nodename
|
|
}
|
|
);
|
|
|
|
me.callParent();
|
|
|
|
me.mon(me.statusStore, 'load', function(s, records, success) {
|
|
var uptimerec = s.data.get('uptime');
|
|
var powermgmt = uptimerec ? uptimerec.data.value : false;
|
|
if (!caps.nodes['Sys.PowerMgmt']) {
|
|
powermgmt = false;
|
|
}
|
|
restartBtn.setDisabled(!powermgmt);
|
|
shutdownBtn.setDisabled(!powermgmt);
|
|
shellBtn.setDisabled(!powermgmt);
|
|
});
|
|
|
|
me.on('afterrender', function() {
|
|
me.statusStore.startUpdate();
|
|
});
|
|
|
|
me.on('destroy', function() {
|
|
me.statusStore.stopUpdate();
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.window.Migrate', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
vmtype: undefined,
|
|
nodename: undefined,
|
|
vmid: undefined,
|
|
|
|
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
|
|
}
|
|
|
|
},
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
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.checkMigratePreconditions();
|
|
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;
|
|
}
|
|
//only submit targetstorage if vm is running, storage migration to different storage is only possible online
|
|
if (vm.get('migration.with-local-disks') && vm.get('running')) {
|
|
params.targetstorage = values.targetstorage;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
|
|
waitMsgTarget: view,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target);
|
|
|
|
Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: upid,
|
|
extraTitle: extraTitle
|
|
}).show();
|
|
|
|
view.close();
|
|
}
|
|
});
|
|
|
|
},
|
|
|
|
checkMigratePreconditions: function() {
|
|
var me = this,
|
|
vm = me.getViewModel();
|
|
|
|
|
|
var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'),
|
|
0, false, false, true);
|
|
if (vmrec && vmrec.data && vmrec.data.running) {
|
|
vm.set('running', true);
|
|
}
|
|
|
|
if (vm.get('vmtype') === 'qemu') {
|
|
me.checkQemuPreconditions();
|
|
} else {
|
|
me.checkLxcPreconditions();
|
|
}
|
|
me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
|
|
|
|
// Only allow nodes where the local storage is available in case of offline migration
|
|
// where storage migration is not possible
|
|
me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes');
|
|
|
|
me.lookup('formPanel').isValid();
|
|
|
|
},
|
|
|
|
checkQemuPreconditions: function() {
|
|
var me = this,
|
|
vm = me.getViewModel(),
|
|
migrateStats;
|
|
|
|
if (vm.get('running')) {
|
|
vm.set('migration.mode', 'online');
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
migrateStats = response.result.data;
|
|
if (migrateStats.running) {
|
|
vm.set('running', true);
|
|
}
|
|
// Get migration object from viewmodel to prevent
|
|
// to many bind callbacks
|
|
var migration = vm.get('migration');
|
|
migration.preconditions = [];
|
|
|
|
if (migrateStats.allowed_nodes) {
|
|
migration.allowedNodes = migrateStats.allowed_nodes;
|
|
var target = me.lookup('pveNodeSelector').value;
|
|
if (target.length && !migrateStats.allowed_nodes.includes(target)) {
|
|
let disallowed = migrateStats.not_allowed_nodes[target];
|
|
let missing_storages = disallowed.unavailable_storages.join(', ');
|
|
|
|
migration.possible = false;
|
|
migration.preconditions.push({
|
|
text: 'Storage (' + missing_storages + ') not available on selected target. ' +
|
|
'Start VM to use live storage migration or select other target node',
|
|
severity: 'error'
|
|
});
|
|
}
|
|
}
|
|
|
|
if (migrateStats.local_resources.length) {
|
|
migration.possible = false;
|
|
migration.preconditions.push({
|
|
text: 'Can\'t migrate VM with local resources: '+ migrateStats.local_resources.join(', '),
|
|
severity: 'error'
|
|
});
|
|
}
|
|
|
|
if (migrateStats.local_disks.length) {
|
|
|
|
migrateStats.local_disks.forEach(function (disk) {
|
|
if (disk.cdrom && disk.cdrom === 1) {
|
|
migration.possible = false;
|
|
migration.preconditions.push({
|
|
text: "Can't migrate VM with local CD/DVD",
|
|
severity: 'error'
|
|
});
|
|
|
|
} else if (!disk.referenced_in_config) {
|
|
migration.possible = false;
|
|
migration.preconditions.push({
|
|
text: 'Found not referenced/unused disk via storage: '+ disk.volid,
|
|
severity: 'error'
|
|
});
|
|
} else {
|
|
migration['with-local-disks'] = 1;
|
|
migration.preconditions.push({
|
|
text:'Migration with local disk might take long: ' + disk.volid
|
|
+' (' + PVE.Utils.render_size(disk.size) + ')',
|
|
severity: 'warning'
|
|
});
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
vm.set('migration', migration);
|
|
|
|
}
|
|
});
|
|
},
|
|
checkLxcPreconditions: function() {
|
|
var me = this,
|
|
vm = me.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: {
|
|
type: 'column'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'container',
|
|
columnWidth: 0.5,
|
|
items: [{
|
|
xtype: 'displayfield',
|
|
name: 'source',
|
|
fieldLabel: gettext('Source node'),
|
|
bind: {
|
|
value: '{nodename}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'migrationMode',
|
|
fieldLabel: gettext('Mode'),
|
|
bind: {
|
|
value: '{setMigrationMode}'
|
|
}
|
|
}]
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
columnWidth: 0.5,
|
|
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',
|
|
bind: {
|
|
hidden: '{setStorageselectorHidden}'
|
|
}
|
|
}]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
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}'
|
|
}
|
|
}
|
|
}
|
|
|
|
],
|
|
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.window.BulkAction', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
resizable: true,
|
|
width: 800,
|
|
modal: true,
|
|
layout: {
|
|
type: 'fit'
|
|
},
|
|
border: false,
|
|
|
|
// the action to be set
|
|
// currently there are
|
|
// startall
|
|
// migrateall
|
|
// stopall
|
|
action: undefined,
|
|
|
|
submit: function(params) {
|
|
var me = this;
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + me.nodename + '/' + me.action,
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
|
|
var win = Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: upid
|
|
});
|
|
win.show();
|
|
me.hide();
|
|
win.on('destroy', function() {
|
|
me.close();
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.action) {
|
|
throw "no action specified";
|
|
}
|
|
|
|
if (!me.btnText) {
|
|
throw "no button text specified";
|
|
}
|
|
|
|
if (!me.title) {
|
|
throw "no title specified";
|
|
}
|
|
|
|
var items = [];
|
|
|
|
if (me.action === 'migrateall') {
|
|
/*jslint confusion: true*/
|
|
/*value is string and number*/
|
|
items.push(
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
name: 'target',
|
|
disallowedNodes: [me.nodename],
|
|
fieldLabel: gettext('Target node'),
|
|
allowBlank: false,
|
|
onlineValidator: true
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'maxworkers',
|
|
minValue: 1,
|
|
maxValue: 100,
|
|
value: 1,
|
|
fieldLabel: gettext('Parallel jobs'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
itemId: 'lxcwarning',
|
|
xtype: 'displayfield',
|
|
userCls: 'pve-hint',
|
|
value: 'Warning: Running CTs will be migrated in Restart Mode.',
|
|
hidden: true // only visible if running container chosen
|
|
}
|
|
);
|
|
/*jslint confusion: false*/
|
|
} else if (me.action === 'startall') {
|
|
items.push({
|
|
xtype: 'hiddenfield',
|
|
name: 'force',
|
|
value: 1
|
|
});
|
|
}
|
|
|
|
items.push({
|
|
xtype: 'vmselector',
|
|
itemId: 'vms',
|
|
name: 'vms',
|
|
flex: 1,
|
|
height: 300,
|
|
selectAll: true,
|
|
allowBlank: false,
|
|
nodename: me.nodename,
|
|
action: me.action,
|
|
listeners: {
|
|
selectionchange: function(vmselector, records) {
|
|
if (me.action == 'migrateall') {
|
|
var showWarning = records.some(function(item) {
|
|
return (item.data.type == 'lxc' &&
|
|
item.data.status == 'running');
|
|
});
|
|
me.down('#lxcwarning').setVisible(showWarning);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
fieldDefaults: {
|
|
labelWidth: 300,
|
|
anchor: '100%'
|
|
},
|
|
items: items
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var submitBtn = Ext.create('Ext.Button', {
|
|
text: me.btnText,
|
|
handler: function() {
|
|
form.isValid();
|
|
me.submit(form.getValues());
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: [ me.formPanel ],
|
|
buttons: [ submitBtn ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
form.on('validitychange', function() {
|
|
var valid = form.isValid();
|
|
submitBtn.setDisabled(!valid);
|
|
});
|
|
form.isValid();
|
|
}
|
|
});
|
|
Ext.define('PVE.window.Clone', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
resizable: false,
|
|
|
|
isTemplate: false,
|
|
|
|
onlineHelp: 'qm_copy_and_clone',
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
control: {
|
|
'panel[reference=cloneform]': {
|
|
validitychange: 'disableSubmit'
|
|
}
|
|
},
|
|
disableSubmit: function(form) {
|
|
this.lookupReference('submitBtn').setDisabled(!form.isValid());
|
|
}
|
|
},
|
|
|
|
statics: {
|
|
// display a snapshot selector only if needed
|
|
wrap: function(nodename, vmid, isTemplate, guestType) {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
var snapshotList = response.result.data;
|
|
var hasSnapshots = snapshotList.length === 1 &&
|
|
snapshotList[0].name === 'current' ? false : true;
|
|
|
|
Ext.create('PVE.window.Clone', {
|
|
nodename: nodename,
|
|
guestType: guestType,
|
|
vmid: vmid,
|
|
isTemplate: isTemplate,
|
|
hasSnapshots: hasSnapshots
|
|
}).show();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
create_clone: function(values) {
|
|
var me = this;
|
|
|
|
var params = { newid: values.newvmid };
|
|
|
|
if (values.snapname && values.snapname !== 'current') {
|
|
params.snapname = values.snapname;
|
|
}
|
|
|
|
if (values.pool) {
|
|
params.pool = values.pool;
|
|
}
|
|
|
|
if (values.name) {
|
|
if (me.guestType === 'lxc') {
|
|
params.hostname = values.name;
|
|
} else {
|
|
params.name = values.name;
|
|
}
|
|
}
|
|
|
|
if (values.target) {
|
|
params.target = values.target;
|
|
}
|
|
|
|
if (values.clonemode === 'copy') {
|
|
params.full = 1;
|
|
if (values.hdstorage) {
|
|
params.storage = values.hdstorage;
|
|
if (values.diskformat && me.guestType !== 'lxc') {
|
|
params.format = values.diskformat;
|
|
}
|
|
}
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone',
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
me.close();
|
|
}
|
|
});
|
|
|
|
},
|
|
|
|
// disable the Storage selector when clone mode is linked clone
|
|
updateVisibility: function() {
|
|
var me = this;
|
|
var clonemode = me.lookupReference('clonemodesel').getValue();
|
|
var disksel = me.lookup('diskselector');
|
|
disksel.setDisabled(clonemode === 'clone');
|
|
},
|
|
|
|
// add to the list of valid nodes each node where
|
|
// all the VM disks are available
|
|
verifyFeature: function() {
|
|
var me = this;
|
|
|
|
var snapname = me.lookupReference('snapshotsel').getValue();
|
|
var clonemode = me.lookupReference('clonemodesel').getValue();
|
|
|
|
var params = { feature: clonemode };
|
|
if (snapname !== 'current') {
|
|
params.snapname = snapname;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
waitMsgTarget: me,
|
|
url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature',
|
|
params: params,
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
me.lookupReference('submitBtn').setDisabled(true);
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var res = response.result.data;
|
|
|
|
me.lookupReference('targetsel').allowedNodes = res.nodes;
|
|
me.lookupReference('targetsel').validate();
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
if (!me.snapname) {
|
|
me.snapname = 'current';
|
|
}
|
|
|
|
if (!me.guestType) {
|
|
throw "no Guest Type specified";
|
|
}
|
|
|
|
var titletext = me.guestType === 'lxc' ? 'CT' : 'VM';
|
|
if (me.isTemplate) {
|
|
titletext += ' Template';
|
|
}
|
|
me.title = "Clone " + titletext + " " + me.vmid;
|
|
|
|
var col1 = [];
|
|
var col2 = [];
|
|
|
|
col1.push({
|
|
xtype: 'pveNodeSelector',
|
|
name: 'target',
|
|
reference: 'targetsel',
|
|
fieldLabel: gettext('Target node'),
|
|
selectCurNode: true,
|
|
allowBlank: false,
|
|
onlineValidator: true,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
me.lookupReference('hdstorage').setTargetNode(value);
|
|
}
|
|
}
|
|
});
|
|
|
|
var modelist = [['copy', gettext('Full Clone')]];
|
|
if (me.isTemplate) {
|
|
modelist.push(['clone', gettext('Linked Clone')]);
|
|
}
|
|
|
|
col1.push({
|
|
xtype: 'pveGuestIDSelector',
|
|
name: 'newvmid',
|
|
guestType: me.guestType,
|
|
value: '',
|
|
loadNextFreeID: true,
|
|
validateExists: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'name',
|
|
allowBlank: true,
|
|
fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name')
|
|
},
|
|
{
|
|
xtype: 'pvePoolSelector',
|
|
fieldLabel: gettext('Resource Pool'),
|
|
name: 'pool',
|
|
value: '',
|
|
allowBlank: true
|
|
}
|
|
);
|
|
|
|
col2.push({
|
|
xtype: 'proxmoxKVComboBox',
|
|
fieldLabel: gettext('Mode'),
|
|
name: 'clonemode',
|
|
reference: 'clonemodesel',
|
|
allowBlank: false,
|
|
hidden: !me.isTemplate,
|
|
value: me.isTemplate ? 'clone' : 'copy',
|
|
comboItems: modelist,
|
|
listeners: {
|
|
change: function(t, value) {
|
|
me.updateVisibility();
|
|
me.verifyFeature();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'PVE.form.SnapshotSelector',
|
|
name: 'snapname',
|
|
reference: 'snapshotsel',
|
|
fieldLabel: gettext('Snapshot'),
|
|
nodename: me.nodename,
|
|
guestType: me.guestType,
|
|
vmid: me.vmid,
|
|
hidden: me.isTemplate || !me.hasSnapshots ? true : false,
|
|
disabled: false,
|
|
allowBlank: false,
|
|
value : me.snapname,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
me.verifyFeature();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'pveDiskStorageSelector',
|
|
reference: 'diskselector',
|
|
nodename: me.nodename,
|
|
autoSelect: false,
|
|
hideSize: true,
|
|
hideSelection: true,
|
|
storageLabel: gettext('Target Storage'),
|
|
allowBlank: true,
|
|
storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir',
|
|
emptyText: gettext('Same as source'),
|
|
disabled: me.isTemplate ? true : false // because default mode is clone for templates
|
|
});
|
|
|
|
var formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
reference: 'cloneform',
|
|
border: false,
|
|
layout: 'column',
|
|
defaultType: 'container',
|
|
columns: 2,
|
|
fieldDefaults: {
|
|
labelWidth: 100,
|
|
anchor: '100%'
|
|
},
|
|
items: [
|
|
{
|
|
columnWidth: 0.5,
|
|
padding: '0 10 0 0',
|
|
layout: 'anchor',
|
|
items: col1
|
|
},
|
|
{
|
|
columnWidth: 0.5,
|
|
padding: '0 0 0 10',
|
|
layout: 'anchor',
|
|
items: col2
|
|
}
|
|
]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
modal: true,
|
|
width: 600,
|
|
height: 250,
|
|
border: false,
|
|
layout: 'fit',
|
|
buttons: [ {
|
|
xtype: 'proxmoxHelpButton',
|
|
listenToGlobalEvent: false,
|
|
hidden: false,
|
|
onlineHelp: me.onlineHelp
|
|
},
|
|
'->',
|
|
{
|
|
reference: 'submitBtn',
|
|
text: gettext('Clone'),
|
|
disabled: true,
|
|
handler: function() {
|
|
var cloneForm = me.lookupReference('cloneform');
|
|
if (cloneForm.isValid()) {
|
|
me.create_clone(cloneForm.getValues());
|
|
}
|
|
}
|
|
} ],
|
|
items: [ formPanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.verifyFeature();
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.Monitor', {
|
|
extend: 'Ext.panel.Panel',
|
|
|
|
alias: 'widget.pveQemuMonitor',
|
|
|
|
maxLines: 500,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var history = [];
|
|
var histNum = -1;
|
|
var lines = [];
|
|
|
|
var textbox = Ext.createWidget('panel', {
|
|
region: 'center',
|
|
xtype: 'panel',
|
|
autoScroll: true,
|
|
border: true,
|
|
margins: '5 5 5 5',
|
|
bodyStyle: 'font-family: monospace;'
|
|
});
|
|
|
|
var scrollToEnd = function() {
|
|
var el = textbox.getTargetEl();
|
|
var dom = Ext.getDom(el);
|
|
|
|
var clientHeight = dom.clientHeight;
|
|
// BrowserBug: clientHeight reports 0 in IE9 StrictMode
|
|
// Instead we are using offsetHeight and hardcoding borders
|
|
if (Ext.isIE9 && Ext.isStrict) {
|
|
clientHeight = dom.offsetHeight + 2;
|
|
}
|
|
dom.scrollTop = dom.scrollHeight - clientHeight;
|
|
};
|
|
|
|
var refresh = function() {
|
|
textbox.update('<pre>' + lines.join('\n') + '</pre>');
|
|
scrollToEnd();
|
|
};
|
|
|
|
var addLine = function(line) {
|
|
lines.push(line);
|
|
if (lines.length > me.maxLines) {
|
|
lines.shift();
|
|
}
|
|
};
|
|
|
|
var executeCmd = function(cmd) {
|
|
addLine("# " + Ext.htmlEncode(cmd));
|
|
if (cmd) {
|
|
history.unshift(cmd);
|
|
if (history.length > 20) {
|
|
history.splice(20);
|
|
}
|
|
}
|
|
histNum = -1;
|
|
|
|
refresh();
|
|
Proxmox.Utils.API2Request({
|
|
params: { command: cmd },
|
|
url: '/nodes/' + nodename + '/qemu/' + vmid + "/monitor",
|
|
method: 'POST',
|
|
waitMsgTarget: me,
|
|
success: function(response, opts) {
|
|
var res = response.result.data;
|
|
Ext.Array.each(res.split('\n'), function(line) {
|
|
addLine(Ext.htmlEncode(line));
|
|
});
|
|
refresh();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
layout: { type: 'border' },
|
|
border: false,
|
|
items: [
|
|
textbox,
|
|
{
|
|
region: 'south',
|
|
margins:'0 5 5 5',
|
|
border: false,
|
|
xtype: 'textfield',
|
|
name: 'cmd',
|
|
value: '',
|
|
fieldStyle: 'font-family: monospace;',
|
|
allowBlank: true,
|
|
listeners: {
|
|
afterrender: function(f) {
|
|
f.focus(false);
|
|
addLine("Type 'help' for help.");
|
|
refresh();
|
|
},
|
|
specialkey: function(f, e) {
|
|
var key = e.getKey();
|
|
switch (key) {
|
|
case e.ENTER:
|
|
var cmd = f.getValue();
|
|
f.setValue('');
|
|
executeCmd(cmd);
|
|
break;
|
|
case e.PAGE_UP:
|
|
textbox.scrollBy(0, -0.9*textbox.getHeight(), false);
|
|
break;
|
|
case e.PAGE_DOWN:
|
|
textbox.scrollBy(0, 0.9*textbox.getHeight(), false);
|
|
break;
|
|
case e.UP:
|
|
if (histNum + 1 < history.length) {
|
|
f.setValue(history[++histNum]);
|
|
}
|
|
e.preventDefault();
|
|
break;
|
|
case e.DOWN:
|
|
if (histNum > 0) {
|
|
f.setValue(history[--histNum]);
|
|
}
|
|
e.preventDefault();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
],
|
|
listeners: {
|
|
show: function() {
|
|
var field = me.query('textfield[name="cmd"]')[0];
|
|
field.focus(false, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.Summary', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveQemuSummary',
|
|
|
|
scrollable: true,
|
|
bodyPadding: 5,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
if (!me.workspace) {
|
|
throw "no workspace specified";
|
|
}
|
|
|
|
if (!me.statusStore) {
|
|
throw "no status storage specified";
|
|
}
|
|
|
|
var template = !!me.pveSelNode.data.template;
|
|
var rstore = me.statusStore;
|
|
|
|
var width = template ? 1 : 0.5;
|
|
var items = [
|
|
{
|
|
xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: width
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: width / 2
|
|
}
|
|
},
|
|
itemId: 'gueststatus',
|
|
pveSelNode: me.pveSelNode,
|
|
rstore: rstore
|
|
},
|
|
{
|
|
xtype: 'pveNotesView',
|
|
maxHeight: 330,
|
|
itemId: 'notesview',
|
|
pveSelNode: me.pveSelNode,
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: width
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: width / 2
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
var rrdstore;
|
|
if (!template) {
|
|
|
|
rrdstore = Ext.create('Proxmox.data.RRDStore', {
|
|
rrdurl: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/rrddata",
|
|
model: 'pve-rrd-guest'
|
|
});
|
|
|
|
items.push(
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('CPU usage'),
|
|
pveSelNode: me.pveSelNode,
|
|
fields: ['cpu'],
|
|
fieldTitles: [gettext('CPU usage')],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Memory usage'),
|
|
pveSelNode: me.pveSelNode,
|
|
fields: ['maxmem', 'mem'],
|
|
fieldTitles: [gettext('Total'), gettext('RAM usage')],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Network traffic'),
|
|
pveSelNode: me.pveSelNode,
|
|
fields: ['netin','netout'],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Disk IO'),
|
|
pveSelNode: me.pveSelNode,
|
|
fields: ['diskread','diskwrite'],
|
|
store: rrdstore
|
|
}
|
|
);
|
|
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ],
|
|
items: [
|
|
{
|
|
xtype: 'container',
|
|
layout: {
|
|
type: 'column'
|
|
},
|
|
defaults: {
|
|
minHeight: 330,
|
|
padding: 5,
|
|
plugins: 'responsive',
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: 1
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: 0.5
|
|
}
|
|
}
|
|
},
|
|
items: items
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
if (!template) {
|
|
rrdstore.startUpdate();
|
|
me.on('destroy', rrdstore.stopUpdate);
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.OSTypeInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveQemuOSTypePanel',
|
|
onlineHelp: 'qm_os_settings',
|
|
insideWizard: false,
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
control: {
|
|
'combobox[name=osbase]': {
|
|
change: 'onOSBaseChange'
|
|
},
|
|
'combobox[name=ostype]': {
|
|
afterrender: 'onOSTypeChange',
|
|
change: 'onOSTypeChange'
|
|
}
|
|
},
|
|
onOSBaseChange: function(field, value) {
|
|
this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]);
|
|
},
|
|
onOSTypeChange: function(field) {
|
|
var me = this, ostype = field.getValue();
|
|
if (!me.getView().insideWizard) {
|
|
return;
|
|
}
|
|
var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype);
|
|
|
|
me.setWidget('pveBusSelector', targetValues.busType);
|
|
me.setWidget('pveNetworkCardSelector', targetValues.networkCard);
|
|
var scsihw = targetValues.scsihw || '__default__';
|
|
this.getViewModel().set('current.scsihw', scsihw);
|
|
},
|
|
setWidget: function(widget, newValue) {
|
|
// changing a widget is safe only if ComponentQuery.query returns us
|
|
// a single value array
|
|
var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget);
|
|
if (widgets.length === 1) {
|
|
widgets[0].setValue(newValue);
|
|
} else {
|
|
throw 'non unique widget :' + widget + ' in Wizard';
|
|
}
|
|
}
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
/*jslint confusion: true */
|
|
me.items = [
|
|
{
|
|
xtype: 'displayfield',
|
|
value: gettext('Guest OS') + ':',
|
|
hidden: !me.insideWizard
|
|
},
|
|
{
|
|
xtype: 'combobox',
|
|
submitValue: false,
|
|
name: 'osbase',
|
|
fieldLabel: gettext('Type'),
|
|
editable: false,
|
|
queryMode: 'local',
|
|
value: 'Linux',
|
|
store: Object.keys(PVE.Utils.kvm_ostypes)
|
|
},
|
|
{
|
|
xtype: 'combobox',
|
|
name: 'ostype',
|
|
reference: 'ostype',
|
|
fieldLabel: gettext('Version'),
|
|
value: 'l26',
|
|
allowBlank : false,
|
|
editable: false,
|
|
queryMode: 'local',
|
|
valueField: 'val',
|
|
displayField: 'desc',
|
|
store: {
|
|
fields: ['desc', 'val'],
|
|
data: PVE.Utils.kvm_ostypes.Linux,
|
|
listeners: {
|
|
datachanged: function (store) {
|
|
var ostype = me.lookup('ostype');
|
|
var old_val = ostype.getValue();
|
|
if (!me.insideWizard && old_val && store.find('val', old_val) != -1) {
|
|
ostype.setValue(old_val);
|
|
} else {
|
|
ostype.setValue(store.getAt(0));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
];
|
|
/*jslint confusion: false */
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.OSTypeEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
subject: 'OS Type',
|
|
|
|
items: [{ xtype: 'pveQemuOSTypePanel' }],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
var value = response.result.data.ostype || 'other';
|
|
var osinfo = PVE.Utils.get_kvm_osinfo(value);
|
|
me.setValues({ ostype: value, osbase: osinfo.base });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*
|
|
* This class holds performance *recommended* settings for the PVE Qemu wizards
|
|
* the *mandatory* settings are set in the PVE::QemuServer
|
|
* config_to_command sub
|
|
* We store this here until we get the data from the API server
|
|
*/
|
|
|
|
// this is how you would add an hypothetic FreeBSD > 10 entry
|
|
//
|
|
//virtio-blk is stable but virtIO net still
|
|
// problematic as of 10.3
|
|
// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059
|
|
// addOS({
|
|
// parent: 'generic', // inherits defaults
|
|
// pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js
|
|
// busType: 'virtio' // must match a pveBusController value
|
|
// // networkCard muss match a pveNetworkCardSelector
|
|
|
|
|
|
Ext.define('PVE.qemu.OSDefaults', {
|
|
singleton: true, // will also force creation when loaded
|
|
|
|
constructor: function() {
|
|
var me = this;
|
|
|
|
var addOS = function(settings) {
|
|
if (me.hasOwnProperty(settings.parent)) {
|
|
var child = Ext.clone(me[settings.parent]);
|
|
me[settings.pveOS] = Ext.apply(child, settings);
|
|
|
|
} else {
|
|
throw("Could not find your genitor");
|
|
}
|
|
};
|
|
|
|
// default values
|
|
me.generic = {
|
|
busType: 'ide',
|
|
networkCard: 'e1000',
|
|
busPriority: {
|
|
ide: 4,
|
|
sata: 3,
|
|
scsi: 2,
|
|
virtio: 1
|
|
},
|
|
scsihw: 'virtio-scsi-pci'
|
|
};
|
|
|
|
// virtio-net is in kernel since 2.6.25
|
|
// virtio-scsi since 3.2 but backported in RHEL with 2.6 kernel
|
|
addOS({
|
|
pveOS: 'l26',
|
|
parent : 'generic',
|
|
busType: 'scsi',
|
|
busPriority: {
|
|
scsi: 4,
|
|
virtio: 3,
|
|
sata: 2,
|
|
ide: 1
|
|
},
|
|
networkCard: 'virtio'
|
|
});
|
|
|
|
// recommandation from http://wiki.qemu.org/Windows2000
|
|
addOS({
|
|
pveOS: 'w2k',
|
|
parent : 'generic',
|
|
networkCard: 'rtl8139',
|
|
scsihw: ''
|
|
});
|
|
// https://pve.proxmox.com/wiki/Windows_XP_Guest_Notes
|
|
addOS({
|
|
pveOS: 'wxp',
|
|
parent : 'w2k'
|
|
});
|
|
|
|
me.getDefaults = function(ostype) {
|
|
if (PVE.qemu.OSDefaults[ostype]) {
|
|
return PVE.qemu.OSDefaults[ostype];
|
|
} else {
|
|
return PVE.qemu.OSDefaults.generic;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.ProcessorInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveQemuProcessorPanel',
|
|
onlineHelp: 'qm_cpu',
|
|
|
|
insideWizard: false,
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
updateCores: function() {
|
|
var me = this.getView();
|
|
var sockets = me.down('field[name=sockets]').getValue();
|
|
var cores = me.down('field[name=cores]').getValue();
|
|
me.down('field[name=totalcores]').setValue(sockets*cores);
|
|
var vcpus = me.down('field[name=vcpus]');
|
|
vcpus.setMaxValue(sockets*cores);
|
|
vcpus.setEmptyText(sockets*cores);
|
|
vcpus.validate();
|
|
},
|
|
|
|
control: {
|
|
'field[name=sockets]': {
|
|
change: 'updateCores'
|
|
},
|
|
'field[name=cores]': {
|
|
change: 'updateCores'
|
|
}
|
|
}
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
if (Array.isArray(values['delete'])) {
|
|
values['delete'] = values['delete'].join(',');
|
|
}
|
|
|
|
PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0);
|
|
PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0);
|
|
|
|
// build the cpu options:
|
|
me.cpu.cputype = values.cputype;
|
|
|
|
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;
|
|
},
|
|
|
|
cpu: {},
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'sockets',
|
|
minValue: 1,
|
|
maxValue: 4,
|
|
value: '1',
|
|
fieldLabel: gettext('Sockets'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'cores',
|
|
minValue: 1,
|
|
maxValue: 128,
|
|
value: '1',
|
|
fieldLabel: gettext('Cores'),
|
|
allowBlank: false
|
|
}
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'CPUModelSelector',
|
|
name: 'cputype',
|
|
value: '__default__',
|
|
fieldLabel: gettext('Type')
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Total cores'),
|
|
name: 'totalcores',
|
|
value: '1'
|
|
}
|
|
],
|
|
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'vcpus',
|
|
minValue: 1,
|
|
maxValue: 1,
|
|
value: '',
|
|
fieldLabel: gettext('VCPUs'),
|
|
deleteEmpty: true,
|
|
allowBlank: true,
|
|
emptyText: '1'
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'cpulimit',
|
|
minValue: 0,
|
|
maxValue: 128, // api maximum
|
|
value: '',
|
|
step: 1,
|
|
fieldLabel: gettext('CPU limit'),
|
|
allowBlank: true,
|
|
emptyText: gettext('unlimited')
|
|
}
|
|
],
|
|
|
|
advancedColumn2: [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'cpuunits',
|
|
fieldLabel: gettext('CPU units'),
|
|
minValue: 8,
|
|
maxValue: 500000,
|
|
value: '1024',
|
|
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',
|
|
|
|
width: 700,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel');
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Processors'),
|
|
items: ipanel
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
var data = response.result.data;
|
|
var value = data.cpu;
|
|
if (value) {
|
|
var cpu = PVE.Parser.parseQemuCpu(value);
|
|
ipanel.cpu = cpu;
|
|
data.cputype = cpu.cputype;
|
|
if (cpu.flags) {
|
|
data.flags = cpu.flags;
|
|
}
|
|
}
|
|
me.setValues(data);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.BootOrderPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveQemuBootOrderPanel',
|
|
vmconfig: {}, // store loaded vm config
|
|
|
|
bootdisk: undefined,
|
|
selection: [],
|
|
list: [],
|
|
comboboxes: [],
|
|
|
|
isBootDisk: function(value) {
|
|
return PVE.Utils.bus_match.test(value);
|
|
},
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
var me = this;
|
|
me.vmconfig = vmconfig;
|
|
var order = me.vmconfig.boot || 'cdn';
|
|
me.bootdisk = me.vmconfig.bootdisk || undefined;
|
|
|
|
// get the first 3 characters
|
|
// ignore the rest (there should never be more than 3)
|
|
me.selection = order.split('').slice(0,3);
|
|
|
|
// build bootdev list
|
|
me.list = [];
|
|
Ext.Object.each(me.vmconfig, function(key, value) {
|
|
if (me.isBootDisk(key) &&
|
|
!(/media=cdrom/).test(value)) {
|
|
me.list.push([key, "Disk '" + key + "'"]);
|
|
}
|
|
});
|
|
|
|
me.list.push(['d', 'CD-ROM']);
|
|
me.list.push(['n', gettext('Network')]);
|
|
me.list.push(['__none__', Proxmox.Utils.noneText]);
|
|
|
|
me.recomputeList();
|
|
|
|
me.comboboxes.forEach(function(box) {
|
|
box.resetOriginalValue();
|
|
});
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
var order = me.selection.join('');
|
|
var res = { boot: order };
|
|
|
|
if (me.bootdisk && order.indexOf('c') !== -1) {
|
|
res.bootdisk = me.bootdisk;
|
|
} else {
|
|
res['delete'] = 'bootdisk';
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
recomputeSelection: function(combobox, newVal, oldVal) {
|
|
var me = this.up('#inputpanel');
|
|
me.selection = [];
|
|
me.comboboxes.forEach(function(item) {
|
|
var val = item.getValue();
|
|
|
|
// when selecting an already selected item,
|
|
// switch it around
|
|
if ((val === newVal || (me.isBootDisk(val) && me.isBootDisk(newVal))) &&
|
|
item.name !== combobox.name &&
|
|
newVal !== '__none__') {
|
|
// swap items
|
|
val = oldVal;
|
|
}
|
|
|
|
// push 'c','d' or 'n' in the array
|
|
if (me.isBootDisk(val)) {
|
|
me.selection.push('c');
|
|
me.bootdisk = val;
|
|
} else if (val === 'd' ||
|
|
val === 'n') {
|
|
me.selection.push(val);
|
|
}
|
|
});
|
|
|
|
me.recomputeList();
|
|
},
|
|
|
|
recomputeList: function(){
|
|
var me = this;
|
|
// set the correct values in the kvcomboboxes
|
|
var cnt = 0;
|
|
me.comboboxes.forEach(function(item) {
|
|
if (cnt === 0) {
|
|
// never show 'none' on first combobox
|
|
item.store.loadData(me.list.slice(0, me.list.length-1));
|
|
} else {
|
|
item.store.loadData(me.list);
|
|
}
|
|
item.suspendEvent('change');
|
|
if (cnt < me.selection.length) {
|
|
item.setValue((me.selection[cnt] !== 'c')?me.selection[cnt]:me.bootdisk);
|
|
} else if (cnt === 0){
|
|
item.setValue('');
|
|
} else {
|
|
item.setValue('__none__');
|
|
}
|
|
cnt++;
|
|
item.resumeEvent('change');
|
|
item.validate();
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
// this has to be done here, because of
|
|
// the way our inputPanel class handles items
|
|
me.comboboxes = [
|
|
Ext.createWidget('proxmoxKVComboBox', {
|
|
fieldLabel: gettext('Boot device') + " 1",
|
|
labelWidth: 120,
|
|
name: 'bd1',
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: me.recomputeSelection
|
|
}
|
|
}),
|
|
Ext.createWidget('proxmoxKVComboBox', {
|
|
fieldLabel: gettext('Boot device') + " 2",
|
|
labelWidth: 120,
|
|
name: 'bd2',
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: me.recomputeSelection
|
|
}
|
|
}),
|
|
Ext.createWidget('proxmoxKVComboBox', {
|
|
fieldLabel: gettext('Boot device') + " 3",
|
|
labelWidth: 120,
|
|
name: 'bd3',
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: me.recomputeSelection
|
|
}
|
|
})
|
|
];
|
|
Ext.apply(me, { items: me.comboboxes });
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.BootOrderEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
items: [{
|
|
xtype: 'pveQemuBootOrderPanel',
|
|
itemId: 'inputpanel'
|
|
}],
|
|
|
|
subject: gettext('Boot Order'),
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
me.callParent();
|
|
me.load({
|
|
success: function(response, options) {
|
|
me.down('#inputpanel').setVMConfig(response.result.data);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.MemoryInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveQemuMemoryPanel',
|
|
onlineHelp: 'qm_memory',
|
|
|
|
insideWizard: false,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var res = {};
|
|
|
|
res.memory = values.memory;
|
|
res.balloon = values.balloon;
|
|
|
|
if (!values.ballooning) {
|
|
res.balloon = 0;
|
|
res['delete'] = 'shares';
|
|
} else if (values.memory === values.balloon) {
|
|
delete res.balloon;
|
|
res['delete'] = 'balloon,shares';
|
|
} else if (Ext.isDefined(values.shares) && (values.shares !== "")) {
|
|
res.shares = values.shares;
|
|
} else {
|
|
res['delete'] = "shares";
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
var labelWidth = 160;
|
|
|
|
me.items= [
|
|
{
|
|
xtype: 'pveMemoryField',
|
|
labelWidth: labelWidth,
|
|
fieldLabel: gettext('Memory') + ' (MiB)',
|
|
name: 'memory',
|
|
minValue: 1,
|
|
step: 32,
|
|
hotplug: me.hotplug,
|
|
listeners: {
|
|
change: function(f, value, old) {
|
|
var bf = me.down('field[name=balloon]');
|
|
var balloon = bf.getValue();
|
|
bf.setMaxValue(value);
|
|
if (balloon === old) {
|
|
bf.setValue(value);
|
|
}
|
|
bf.validate();
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
me.advancedItems= [
|
|
{
|
|
xtype: 'pveMemoryField',
|
|
name: 'balloon',
|
|
minValue: 1,
|
|
step: 32,
|
|
fieldLabel: gettext('Minimum memory') + ' (MiB)',
|
|
hotplug: me.hotplug,
|
|
labelWidth: labelWidth,
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
var memory = me.down('field[name=memory]').getValue();
|
|
var shares = me.down('field[name=shares]');
|
|
shares.setDisabled(value === memory);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'shares',
|
|
disabled: true,
|
|
minValue: 0,
|
|
maxValue: 50000,
|
|
value: '',
|
|
step: 10,
|
|
fieldLabel: gettext('Shares'),
|
|
labelWidth: labelWidth,
|
|
allowBlank: true,
|
|
emptyText: Proxmox.Utils.defaultText + ' (1000)',
|
|
submitEmptyText: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
labelWidth: labelWidth,
|
|
value: '1',
|
|
name: 'ballooning',
|
|
fieldLabel: gettext('Ballooning Device'),
|
|
listeners: {
|
|
change: function(f, value) {
|
|
var bf = me.down('field[name=balloon]');
|
|
var shares = me.down('field[name=shares]');
|
|
var memory = me.down('field[name=memory]');
|
|
bf.setDisabled(!value);
|
|
shares.setDisabled(!value || (bf.getValue() === memory.getValue()));
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
if (me.insideWizard) {
|
|
me.column1 = me.items;
|
|
me.items = undefined;
|
|
me.advancedColumn1 = me.advancedItems;
|
|
me.advancedItems = undefined;
|
|
}
|
|
me.callParent();
|
|
}
|
|
|
|
});
|
|
|
|
Ext.define('PVE.qemu.MemoryEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var memoryhotplug;
|
|
if(me.hotplug) {
|
|
Ext.each(me.hotplug.split(','), function(el) {
|
|
if (el === 'memory') {
|
|
memoryhotplug = 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
var ipanel = Ext.create('PVE.qemu.MemoryInputPanel', {
|
|
hotplug: memoryhotplug
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Memory'),
|
|
items: [ ipanel ],
|
|
// uncomment the following to use the async configiguration API
|
|
// backgroundDelay: 5,
|
|
width: 400
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
var data = response.result.data;
|
|
|
|
var values = {
|
|
ballooning: data.balloon === 0 ? '0' : '1',
|
|
shares: data.shares,
|
|
memory: data.memory || '512',
|
|
balloon: data.balloon > 0 ? data.balloon : (data.memory || '512')
|
|
};
|
|
|
|
ipanel.setValues(values);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.NetworkInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveQemuNetworkInputPanel',
|
|
onlineHelp: 'qm_network_device',
|
|
|
|
insideWizard: false,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
me.network.model = values.model;
|
|
if (values.nonetwork) {
|
|
return {};
|
|
} else {
|
|
me.network.bridge = values.bridge;
|
|
me.network.tag = values.tag;
|
|
me.network.firewall = values.firewall;
|
|
}
|
|
me.network.macaddr = values.macaddr;
|
|
me.network.disconnect = values.disconnect;
|
|
me.network.queues = values.queues;
|
|
|
|
if (values.rate) {
|
|
me.network.rate = values.rate;
|
|
} else {
|
|
delete me.network.rate;
|
|
}
|
|
|
|
var params = {};
|
|
|
|
params[me.confid] = PVE.Parser.printQemuNetwork(me.network);
|
|
|
|
return params;
|
|
},
|
|
|
|
setNetwork: function(confid, data) {
|
|
var me = this;
|
|
|
|
me.confid = confid;
|
|
|
|
if (data) {
|
|
data.networkmode = data.bridge ? 'bridge' : 'nat';
|
|
} else {
|
|
data = {};
|
|
data.networkmode = 'bridge';
|
|
}
|
|
me.network = data;
|
|
|
|
me.setValues(me.network);
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
me.bridgesel.setNodename(nodename);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.network = {};
|
|
me.confid = 'net0';
|
|
|
|
me.column1 = [];
|
|
me.column2 = [];
|
|
|
|
me.bridgesel = Ext.create('PVE.form.BridgeSelector', {
|
|
name: 'bridge',
|
|
fieldLabel: gettext('Bridge'),
|
|
nodename: me.nodename,
|
|
autoSelect: true,
|
|
allowBlank: false
|
|
});
|
|
|
|
me.column1 = [
|
|
me.bridgesel,
|
|
{
|
|
xtype: 'pveVlanField',
|
|
name: 'tag',
|
|
value: ''
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Firewall'),
|
|
name: 'firewall',
|
|
checked: (me.insideWizard || me.isCreate)
|
|
}
|
|
];
|
|
|
|
me.advancedColumn1 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Disconnect'),
|
|
name: 'disconnect'
|
|
}
|
|
];
|
|
|
|
if (me.insideWizard) {
|
|
me.column1.unshift({
|
|
xtype: 'checkbox',
|
|
name: 'nonetwork',
|
|
inputValue: 'none',
|
|
boxLabel: gettext('No network device'),
|
|
listeners: {
|
|
change: function(cb, value) {
|
|
var fields = [
|
|
'disconnect',
|
|
'bridge',
|
|
'tag',
|
|
'firewall',
|
|
'model',
|
|
'macaddr',
|
|
'rate',
|
|
'queues'
|
|
];
|
|
fields.forEach(function(fieldname) {
|
|
me.down('field[name='+fieldname+']').setDisabled(value);
|
|
});
|
|
me.down('field[name=bridge]').validate();
|
|
}
|
|
}
|
|
});
|
|
me.column2.unshift({
|
|
xtype: 'displayfield'
|
|
});
|
|
}
|
|
|
|
me.column2.push(
|
|
{
|
|
xtype: 'pveNetworkCardSelector',
|
|
name: 'model',
|
|
fieldLabel: gettext('Model'),
|
|
value: PVE.qemu.OSDefaults.generic.networkCard,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'macaddr',
|
|
fieldLabel: gettext('MAC address'),
|
|
vtype: 'MacAddress',
|
|
allowBlank: true,
|
|
emptyText: 'auto'
|
|
});
|
|
me.advancedColumn2 = [
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'rate',
|
|
fieldLabel: gettext('Rate limit') + ' (MB/s)',
|
|
minValue: 0,
|
|
maxValue: 10*1024,
|
|
value: '',
|
|
emptyText: 'unlimited',
|
|
allowBlank: true
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'queues',
|
|
fieldLabel: 'Multiqueue',
|
|
minValue: 1,
|
|
maxValue: 8,
|
|
value: '',
|
|
allowBlank: true
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.NetworkEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
isAdd: true,
|
|
|
|
initComponent : function() {
|
|
/*jslint confusion: true */
|
|
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.isCreate = me.confid ? false : true;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', {
|
|
confid: me.confid,
|
|
nodename: nodename,
|
|
isCreate: me.isCreate
|
|
});
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('Network Device'),
|
|
items: ipanel
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
var i, confid;
|
|
me.vmconfig = response.result.data;
|
|
if (!me.isCreate) {
|
|
var value = me.vmconfig[me.confid];
|
|
var network = PVE.Parser.parseQemuNetwork(me.confid, value);
|
|
if (!network) {
|
|
Ext.Msg.alert(gettext('Error'), 'Unable to parse network options');
|
|
me.close();
|
|
return;
|
|
}
|
|
ipanel.setNetwork(me.confid, network);
|
|
} else {
|
|
for (i = 0; i < 100; i++) {
|
|
confid = 'net' + i.toString();
|
|
if (!Ext.isDefined(me.vmconfig[confid])) {
|
|
me.confid = confid;
|
|
break;
|
|
}
|
|
}
|
|
ipanel.setNetwork(me.confid);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.Smbios1InputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.PVE.qemu.Smbios1InputPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
smbios1: {},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var params = {
|
|
smbios1: PVE.Parser.printQemuSmbios1(values)
|
|
};
|
|
|
|
return params;
|
|
},
|
|
|
|
setSmbios1: function(data) {
|
|
var me = this;
|
|
|
|
me.smbios1 = data;
|
|
|
|
me.setValues(me.smbios1);
|
|
},
|
|
|
|
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() {
|
|
/*jslint confusion: true */
|
|
|
|
var me = this;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {});
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('SMBIOS settings (type1)'),
|
|
width: 450,
|
|
items: ipanel
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
var i, confid;
|
|
me.vmconfig = response.result.data;
|
|
var value = me.vmconfig.smbios1;
|
|
if (value) {
|
|
var data = PVE.Parser.parseQemuSmbios1(value);
|
|
if (!data) {
|
|
Ext.Msg.alert(gettext('Error'), 'Unable to parse smbios options');
|
|
me.close();
|
|
return;
|
|
}
|
|
ipanel.setSmbios1(data);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.CDInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveQemuCDInputPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var confid = me.confid || (values.controller + values.deviceid);
|
|
|
|
me.drive.media = 'cdrom';
|
|
if (values.mediaType === 'iso') {
|
|
me.drive.file = values.cdimage;
|
|
} else if (values.mediaType === 'cdrom') {
|
|
me.drive.file = 'cdrom';
|
|
} else {
|
|
me.drive.file = 'none';
|
|
}
|
|
|
|
var params = {};
|
|
|
|
params[confid] = PVE.Parser.printQemuDrive(me.drive);
|
|
|
|
return params;
|
|
},
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
var me = this;
|
|
|
|
if (me.bussel) {
|
|
me.bussel.setVMConfig(vmconfig, 'cdrom');
|
|
}
|
|
},
|
|
|
|
setDrive: function(drive) {
|
|
var me = this;
|
|
|
|
var values = {};
|
|
if (drive.file === 'cdrom') {
|
|
values.mediaType = 'cdrom';
|
|
} else if (drive.file === 'none') {
|
|
values.mediaType = 'none';
|
|
} else {
|
|
values.mediaType = 'iso';
|
|
var match = drive.file.match(/^([^:]+):/);
|
|
if (match) {
|
|
values.cdstorage = match[1];
|
|
values.cdimage = drive.file;
|
|
}
|
|
}
|
|
|
|
me.drive = drive;
|
|
|
|
me.setValues(values);
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
me.cdstoragesel.setNodename(nodename);
|
|
me.cdfilesel.setStorage(undefined, nodename);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.drive = {};
|
|
|
|
var items = [];
|
|
|
|
if (!me.confid) {
|
|
me.bussel = Ext.create('PVE.form.ControllerSelector', {
|
|
noVirtIO: true
|
|
});
|
|
items.push(me.bussel);
|
|
}
|
|
|
|
items.push({
|
|
xtype: 'radiofield',
|
|
name: 'mediaType',
|
|
inputValue: 'iso',
|
|
boxLabel: gettext('Use CD/DVD disc image file (iso)'),
|
|
checked: true,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (!me.rendered) {
|
|
return;
|
|
}
|
|
me.down('field[name=cdstorage]').setDisabled(!value);
|
|
me.down('field[name=cdimage]').setDisabled(!value);
|
|
me.down('field[name=cdimage]').validate();
|
|
}
|
|
}
|
|
});
|
|
|
|
me.cdfilesel = Ext.create('PVE.form.FileSelector', {
|
|
name: 'cdimage',
|
|
nodename: me.nodename,
|
|
storageContent: 'iso',
|
|
fieldLabel: gettext('ISO image'),
|
|
labelAlign: 'right',
|
|
allowBlank: false
|
|
});
|
|
|
|
me.cdstoragesel = Ext.create('PVE.form.StorageSelector', {
|
|
name: 'cdstorage',
|
|
nodename: me.nodename,
|
|
fieldLabel: gettext('Storage'),
|
|
labelAlign: 'right',
|
|
storageContent: 'iso',
|
|
allowBlank: false,
|
|
autoSelect: me.insideWizard,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
me.cdfilesel.setStorage(value);
|
|
}
|
|
}
|
|
});
|
|
|
|
items.push(me.cdstoragesel);
|
|
items.push(me.cdfilesel);
|
|
|
|
items.push({
|
|
xtype: 'radiofield',
|
|
name: 'mediaType',
|
|
inputValue: 'cdrom',
|
|
boxLabel: gettext('Use physical CD/DVD Drive')
|
|
});
|
|
|
|
items.push({
|
|
xtype: 'radiofield',
|
|
name: 'mediaType',
|
|
inputValue: 'none',
|
|
boxLabel: gettext('Do not use any media')
|
|
});
|
|
|
|
me.items = items;
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.CDEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
width: 400,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.isCreate = me.confid ? false : true;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.CDInputPanel', {
|
|
confid: me.confid,
|
|
nodename: nodename
|
|
});
|
|
|
|
Ext.applyIf(me, {
|
|
subject: 'CD/DVD Drive',
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
ipanel.setVMConfig(response.result.data);
|
|
if (me.confid) {
|
|
var value = response.result.data[me.confid];
|
|
var drive = PVE.Parser.parseQemuDrive(me.confid, value);
|
|
if (!drive) {
|
|
Ext.Msg.alert('Error', 'Unable to parse drive options');
|
|
me.close();
|
|
return;
|
|
}
|
|
ipanel.setDrive(drive);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true */
|
|
/* 'change' property is assigned a string and then a function */
|
|
Ext.define('PVE.qemu.HDInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveQemuHDInputPanel',
|
|
onlineHelp: 'qm_hard_disk',
|
|
|
|
insideWizard: false,
|
|
|
|
unused: false, // ADD usused disk imaged
|
|
|
|
vmconfig: {}, // used to select usused disks
|
|
|
|
controller: {
|
|
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
onControllerChange: function(field) {
|
|
var value = field.getValue();
|
|
|
|
var allowIOthread = value.match(/^(virtio|scsi)/);
|
|
this.lookup('iothread').setDisabled(!allowIOthread);
|
|
if (!allowIOthread) {
|
|
this.lookup('iothread').setValue(false);
|
|
}
|
|
|
|
var virtio = value.match(/^virtio/);
|
|
this.lookup('discard').setDisabled(virtio);
|
|
this.lookup('ssd').setDisabled(virtio);
|
|
if (virtio) {
|
|
this.lookup('discard').setValue(false);
|
|
this.lookup('ssd').setValue(false);
|
|
}
|
|
|
|
this.lookup('scsiController').setVisible(value.match(/^scsi/));
|
|
},
|
|
|
|
control: {
|
|
'field[name=controller]': {
|
|
change: 'onControllerChange',
|
|
afterrender: 'onControllerChange'
|
|
},
|
|
'field[name=iothread]' : {
|
|
change: function(f, value) {
|
|
if (!this.getView().insideWizard) {
|
|
return;
|
|
}
|
|
var vmScsiType = value ? 'virtio-scsi-single': 'virtio-scsi-pci';
|
|
this.lookupReference('scsiController').setValue(vmScsiType);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var params = {};
|
|
var confid = me.confid || (values.controller + values.deviceid);
|
|
|
|
if (me.unused) {
|
|
me.drive.file = me.vmconfig[values.unusedId];
|
|
confid = values.controller + values.deviceid;
|
|
} else if (me.isCreate) {
|
|
if (values.hdimage) {
|
|
me.drive.file = values.hdimage;
|
|
} else {
|
|
me.drive.file = values.hdstorage + ":" + values.disksize;
|
|
}
|
|
me.drive.format = values.diskformat;
|
|
}
|
|
|
|
if (values.nobackup) {
|
|
me.drive.backup = 'no';
|
|
} else {
|
|
delete me.drive.backup;
|
|
}
|
|
|
|
if (values.noreplicate) {
|
|
me.drive.replicate = 'no';
|
|
} else {
|
|
delete me.drive.replicate;
|
|
}
|
|
|
|
if (values.discard) {
|
|
me.drive.discard = 'on';
|
|
} else {
|
|
delete me.drive.discard;
|
|
}
|
|
|
|
if (values.ssd) {
|
|
me.drive.ssd = 'on';
|
|
} else {
|
|
delete me.drive.ssd;
|
|
}
|
|
|
|
if (values.iothread) {
|
|
me.drive.iothread = 'on';
|
|
} else {
|
|
delete me.drive.iothread;
|
|
}
|
|
|
|
if (values.cache) {
|
|
me.drive.cache = values.cache;
|
|
} else {
|
|
delete me.drive.cache;
|
|
}
|
|
|
|
var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'];
|
|
Ext.Array.each(names, function(name) {
|
|
if (values[name]) {
|
|
me.drive[name] = values[name];
|
|
} else {
|
|
delete me.drive[name];
|
|
}
|
|
var burst_name = name + '_max';
|
|
if (values[burst_name] && values[name]) {
|
|
me.drive[burst_name] = values[burst_name];
|
|
} else {
|
|
delete me.drive[burst_name];
|
|
}
|
|
});
|
|
|
|
|
|
params[confid] = PVE.Parser.printQemuDrive(me.drive);
|
|
|
|
return params;
|
|
},
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
var me = this;
|
|
|
|
me.vmconfig = vmconfig;
|
|
|
|
if (me.bussel) {
|
|
me.bussel.setVMConfig(vmconfig);
|
|
me.scsiController.setValue(vmconfig.scsihw);
|
|
}
|
|
if (me.unusedDisks) {
|
|
var disklist = [];
|
|
Ext.Object.each(vmconfig, function(key, value) {
|
|
if (key.match(/^unused\d+$/)) {
|
|
disklist.push([key, value]);
|
|
}
|
|
});
|
|
me.unusedDisks.store.loadData(disklist);
|
|
me.unusedDisks.setValue(me.confid);
|
|
}
|
|
},
|
|
|
|
setDrive: function(drive) {
|
|
var me = this;
|
|
|
|
me.drive = drive;
|
|
|
|
var values = {};
|
|
var match = drive.file.match(/^([^:]+):/);
|
|
if (match) {
|
|
values.hdstorage = match[1];
|
|
}
|
|
|
|
values.hdimage = drive.file;
|
|
values.nobackup = !PVE.Parser.parseBoolean(drive.backup, 1);
|
|
values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1);
|
|
values.diskformat = drive.format || 'raw';
|
|
values.cache = drive.cache || '__default__';
|
|
values.discard = (drive.discard === 'on');
|
|
values.ssd = PVE.Parser.parseBoolean(drive.ssd);
|
|
values.iothread = PVE.Parser.parseBoolean(drive.iothread);
|
|
|
|
values.mbps_rd = drive.mbps_rd;
|
|
values.mbps_wr = drive.mbps_wr;
|
|
values.iops_rd = drive.iops_rd;
|
|
values.iops_wr = drive.iops_wr;
|
|
values.mbps_rd_max = drive.mbps_rd_max;
|
|
values.mbps_wr_max = drive.mbps_wr_max;
|
|
values.iops_rd_max = drive.iops_rd_max;
|
|
values.iops_wr_max = drive.iops_wr_max;
|
|
|
|
me.setValues(values);
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
me.down('#hdstorage').setNodename(nodename);
|
|
me.down('#hdimage').setStorage(undefined, nodename);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var labelWidth = 140;
|
|
|
|
me.drive = {};
|
|
|
|
me.column1 = [];
|
|
me.column2 = [];
|
|
|
|
me.advancedColumn1 = [];
|
|
me.advancedColumn2 = [];
|
|
|
|
if (!me.confid || me.unused) {
|
|
me.bussel = Ext.create('PVE.form.ControllerSelector', {
|
|
vmconfig: me.insideWizard ? {ide2: 'cdrom'} : {}
|
|
});
|
|
me.column1.push(me.bussel);
|
|
|
|
me.scsiController = Ext.create('Ext.form.field.Display', {
|
|
fieldLabel: gettext('SCSI Controller'),
|
|
reference: 'scsiController',
|
|
bind: me.insideWizard ? {
|
|
value: '{current.scsihw}'
|
|
} : undefined,
|
|
renderer: PVE.Utils.render_scsihw,
|
|
submitValue: false,
|
|
hidden: true
|
|
});
|
|
me.column1.push(me.scsiController);
|
|
}
|
|
|
|
if (me.unused) {
|
|
me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', {
|
|
name: 'unusedId',
|
|
fieldLabel: gettext('Disk image'),
|
|
matchFieldWidth: false,
|
|
listConfig: {
|
|
width: 350
|
|
},
|
|
data: [],
|
|
allowBlank: false
|
|
});
|
|
me.column1.push(me.unusedDisks);
|
|
} else if (me.isCreate) {
|
|
me.column1.push({
|
|
xtype: 'pveDiskStorageSelector',
|
|
storageContent: 'images',
|
|
name: 'disk',
|
|
nodename: me.nodename,
|
|
autoSelect: me.insideWizard
|
|
});
|
|
} else {
|
|
me.column1.push({
|
|
xtype: 'textfield',
|
|
disabled: true,
|
|
submitValue: false,
|
|
fieldLabel: gettext('Disk image'),
|
|
name: 'hdimage'
|
|
});
|
|
}
|
|
|
|
me.column2.push(
|
|
{
|
|
xtype: 'CacheTypeSelector',
|
|
name: 'cache',
|
|
value: '__default__',
|
|
fieldLabel: gettext('Cache')
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Discard'),
|
|
disabled: me.confid && me.confid.match(/^virtio/),
|
|
reference: 'discard',
|
|
name: 'discard'
|
|
}
|
|
);
|
|
|
|
me.advancedColumn1.push(
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
disabled: me.confid && me.confid.match(/^virtio/),
|
|
fieldLabel: gettext('SSD emulation'),
|
|
labelWidth: labelWidth,
|
|
name: 'ssd',
|
|
reference: 'ssd'
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
disabled: me.confid && !me.confid.match(/^(virtio|scsi)/),
|
|
fieldLabel: 'IO thread',
|
|
labelWidth: labelWidth,
|
|
reference: 'iothread',
|
|
name: 'iothread'
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'mbps_rd',
|
|
minValue: 1,
|
|
step: 1,
|
|
fieldLabel: gettext('Read limit') + ' (MB/s)',
|
|
labelWidth: labelWidth,
|
|
emptyText: gettext('unlimited')
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'mbps_wr',
|
|
minValue: 1,
|
|
step: 1,
|
|
fieldLabel: gettext('Write limit') + ' (MB/s)',
|
|
labelWidth: labelWidth,
|
|
emptyText: gettext('unlimited')
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'iops_rd',
|
|
minValue: 10,
|
|
step: 10,
|
|
fieldLabel: gettext('Read limit') + ' (ops/s)',
|
|
labelWidth: labelWidth,
|
|
emptyText: gettext('unlimited')
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'iops_wr',
|
|
minValue: 10,
|
|
step: 10,
|
|
fieldLabel: gettext('Write limit') + ' (ops/s)',
|
|
labelWidth: labelWidth,
|
|
emptyText: gettext('unlimited')
|
|
}
|
|
);
|
|
|
|
me.advancedColumn2.push(
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('No backup'),
|
|
labelWidth: labelWidth,
|
|
name: 'nobackup'
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Skip replication'),
|
|
labelWidth: labelWidth,
|
|
name: 'noreplicate'
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'mbps_rd_max',
|
|
minValue: 1,
|
|
step: 1,
|
|
fieldLabel: gettext('Read max burst') + ' (MB)',
|
|
labelWidth: labelWidth,
|
|
emptyText: gettext('default')
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'mbps_wr_max',
|
|
minValue: 1,
|
|
step: 1,
|
|
fieldLabel: gettext('Write max burst') + ' (MB)',
|
|
labelWidth: labelWidth,
|
|
emptyText: gettext('default')
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'iops_rd_max',
|
|
minValue: 10,
|
|
step: 10,
|
|
fieldLabel: gettext('Read max burst') + ' (ops)',
|
|
labelWidth: labelWidth,
|
|
emptyText: gettext('default')
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'iops_wr_max',
|
|
minValue: 10,
|
|
step: 10,
|
|
fieldLabel: gettext('Write max burst') + ' (ops)',
|
|
labelWidth: labelWidth,
|
|
emptyText: gettext('default')
|
|
}
|
|
);
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*jslint confusion: false */
|
|
|
|
Ext.define('PVE.qemu.HDEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
isAdd: true,
|
|
|
|
backgroundDelay: 5,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var unused = me.confid && me.confid.match(/^unused\d+$/);
|
|
|
|
me.isCreate = me.confid ? unused : true;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.HDInputPanel', {
|
|
confid: me.confid,
|
|
nodename: nodename,
|
|
unused: unused,
|
|
isCreate: me.isCreate
|
|
});
|
|
|
|
var subject;
|
|
if (unused) {
|
|
me.subject = gettext('Unused Disk');
|
|
} else if (me.isCreate) {
|
|
me.subject = gettext('Hard Disk');
|
|
} else {
|
|
me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
|
|
}
|
|
|
|
me.items = [ ipanel ];
|
|
|
|
me.callParent();
|
|
/*jslint confusion: true*/
|
|
/* 'data' is assigned an empty array in same file, and here we
|
|
* use it like an object
|
|
*/
|
|
me.load({
|
|
success: function(response, options) {
|
|
ipanel.setVMConfig(response.result.data);
|
|
if (me.confid) {
|
|
var value = response.result.data[me.confid];
|
|
var drive = PVE.Parser.parseQemuDrive(me.confid, value);
|
|
if (!drive) {
|
|
Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options');
|
|
me.close();
|
|
return;
|
|
}
|
|
ipanel.setDrive(drive);
|
|
me.isValid(); // trigger validation
|
|
}
|
|
}
|
|
});
|
|
/*jslint confusion: false*/
|
|
}
|
|
});
|
|
Ext.define('PVE.window.HDResize', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
resizable: false,
|
|
|
|
resize_disk: function(disk, size) {
|
|
var me = this;
|
|
var params = { disk: disk, size: '+' + size + 'G' };
|
|
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize',
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
me.close();
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var items = [
|
|
{
|
|
xtype: 'displayfield',
|
|
name: 'disk',
|
|
value: me.disk,
|
|
fieldLabel: gettext('Disk'),
|
|
vtype: 'StorageId',
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.hdsizesel = Ext.createWidget('numberfield', {
|
|
name: 'size',
|
|
minValue: 0,
|
|
maxValue: 128*1024,
|
|
decimalPrecision: 3,
|
|
value: '0',
|
|
fieldLabel: gettext('Size Increment') + ' (GiB)',
|
|
allowBlank: false
|
|
});
|
|
|
|
items.push(me.hdsizesel);
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
fieldDefaults: {
|
|
labelWidth: 140,
|
|
anchor: '100%'
|
|
},
|
|
items: items
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var submitBtn;
|
|
|
|
me.title = gettext('Resize disk');
|
|
submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Resize disk'),
|
|
handler: function() {
|
|
if (form.isValid()) {
|
|
var values = form.getValues();
|
|
me.resize_disk(me.disk, values.size);
|
|
}
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
modal: true,
|
|
width: 250,
|
|
height: 150,
|
|
border: false,
|
|
layout: 'fit',
|
|
buttons: [ submitBtn ],
|
|
items: [ me.formPanel ]
|
|
});
|
|
|
|
|
|
me.callParent();
|
|
|
|
if (!me.disk) {
|
|
return;
|
|
}
|
|
|
|
}
|
|
});
|
|
Ext.define('PVE.window.HDMove', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
resizable: false,
|
|
|
|
|
|
move_disk: function(disk, storage, format, delete_disk) {
|
|
var me = this;
|
|
var qemu = (me.type === 'qemu');
|
|
var params = {};
|
|
params.storage = storage;
|
|
params[qemu ? 'disk':'volume'] = disk;
|
|
|
|
if (format && qemu) {
|
|
params.format = format;
|
|
}
|
|
|
|
if (delete_disk) {
|
|
params['delete'] = 1;
|
|
}
|
|
|
|
var url = '/nodes/' + me.nodename + '/' + me.type + '/' + me.vmid + '/';
|
|
url += qemu ? 'move_disk' : 'move_volume';
|
|
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: url,
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: upid
|
|
});
|
|
win.show();
|
|
win.on('destroy', function() { me.close(); });
|
|
}
|
|
});
|
|
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var diskarray = [];
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
if (!me.type) {
|
|
me.type = 'qemu';
|
|
}
|
|
|
|
var qemu = (me.type === 'qemu');
|
|
|
|
var items = [
|
|
{
|
|
xtype: 'displayfield',
|
|
name: qemu ? 'disk' : 'volume',
|
|
value: me.disk,
|
|
fieldLabel: qemu ? gettext('Disk') : gettext('Mount Point'),
|
|
vtype: 'StorageId',
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
items.push({
|
|
xtype: 'pveDiskStorageSelector',
|
|
storageLabel: gettext('Target Storage'),
|
|
nodename: me.nodename,
|
|
storageContent: qemu ? 'images' : 'rootdir',
|
|
hideSize: true
|
|
});
|
|
|
|
items.push({
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Delete source'),
|
|
name: 'deleteDisk',
|
|
uncheckedValue: 0,
|
|
checked: false
|
|
});
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
fieldDefaults: {
|
|
labelWidth: 100,
|
|
anchor: '100%'
|
|
},
|
|
items: items
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var submitBtn;
|
|
|
|
me.title = qemu ? gettext("Move disk") : gettext('Move Volume');
|
|
submitBtn = Ext.create('Ext.Button', {
|
|
text: me.title,
|
|
handler: function() {
|
|
if (form.isValid()) {
|
|
var values = form.getValues();
|
|
me.move_disk(me.disk, values.hdstorage, values.diskformat,
|
|
values.deleteDisk);
|
|
}
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
modal: true,
|
|
width: 350,
|
|
border: false,
|
|
layout: 'fit',
|
|
buttons: [ submitBtn ],
|
|
items: [ me.formPanel ]
|
|
});
|
|
|
|
|
|
me.callParent();
|
|
|
|
me.mon(me.formPanel, 'validitychange', function(fp, isValid) {
|
|
submitBtn.setDisabled(!isValid);
|
|
});
|
|
|
|
me.formPanel.isValid();
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.EFIDiskInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveEFIDiskInputPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
unused: false, // ADD usused disk imaged
|
|
|
|
vmconfig: {}, // used to select usused disks
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var confid = 'efidisk0';
|
|
|
|
if (values.hdimage) {
|
|
me.drive.file = values.hdimage;
|
|
} else {
|
|
// we use 1 here, because for efi the size gets overridden from the backend
|
|
me.drive.file = values.hdstorage + ":1";
|
|
}
|
|
|
|
me.drive.format = values.diskformat;
|
|
var params = {};
|
|
params[confid] = PVE.Parser.printQemuDrive(me.drive);
|
|
return params;
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
me.down('#hdstorage').setNodename(nodename);
|
|
me.down('#hdimage').setStorage(undefined, nodename);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.drive = {};
|
|
|
|
me.items= [];
|
|
|
|
me.items.push({
|
|
xtype: 'pveDiskStorageSelector',
|
|
name: 'efidisk0',
|
|
storageContent: 'images',
|
|
nodename: me.nodename,
|
|
hideSize: true
|
|
});
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.EFIDiskEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
isAdd: true,
|
|
subject: gettext('EFI Disk'),
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.items = [{
|
|
xtype: 'pveEFIDiskInputPanel',
|
|
onlineHelp: 'qm_bios_and_uefi',
|
|
confid: me.confid,
|
|
nodename: nodename,
|
|
isCreate: true
|
|
}];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.DisplayInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveDisplayInputPanel',
|
|
|
|
onGetValues: function(values) {
|
|
var ret = PVE.Parser.printPropertyString(values, 'type');
|
|
if (ret === '') {
|
|
return {
|
|
'delete': 'vga'
|
|
};
|
|
}
|
|
return {
|
|
vga: ret
|
|
};
|
|
},
|
|
|
|
items: [{
|
|
name: 'type',
|
|
xtype: 'proxmoxKVComboBox',
|
|
value: '__default__',
|
|
deleteEmpty: false,
|
|
fieldLabel: gettext('Graphic card'),
|
|
comboItems: PVE.Utils.kvm_vga_driver_array(),
|
|
validator: function() {
|
|
var v = this.getValue();
|
|
var cfg = this.up('proxmoxWindowEdit').vmconfig || {};
|
|
|
|
if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) {
|
|
var fmt = gettext("Serial interface '{0}' is not correctly configured.");
|
|
return Ext.String.format(fmt, v);
|
|
}
|
|
return true;
|
|
},
|
|
listeners: {
|
|
change: function(cb, val) {
|
|
var me = this.up('panel');
|
|
if (!val) {
|
|
return;
|
|
}
|
|
var disable = false;
|
|
var emptyText = Proxmox.Utils.defaultText;
|
|
switch (val) {
|
|
case "cirrus":
|
|
emptyText = "4";
|
|
break;
|
|
case "std":
|
|
emptyText = "16";
|
|
break;
|
|
case "qxl":
|
|
case "qxl2":
|
|
case "qxl3":
|
|
case "qxl4":
|
|
emptyText = "16";
|
|
break;
|
|
case "vmware":
|
|
emptyText = "16";
|
|
break;
|
|
case "none":
|
|
case "serial0":
|
|
case "serial1":
|
|
case "serial2":
|
|
case "serial3":
|
|
emptyText = 'N/A';
|
|
disable = true;
|
|
break;
|
|
case "virtio":
|
|
emptyText = "256";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
var memoryfield = me.down('field[name=memory]');
|
|
memoryfield.setEmptyText(emptyText);
|
|
memoryfield.setDisabled(disable);
|
|
}
|
|
}
|
|
},{
|
|
xtype: 'proxmoxintegerfield',
|
|
emptyText: Proxmox.Utils.defaultText,
|
|
fieldLabel: gettext('Memory') + ' (MiB)',
|
|
minValue: 4,
|
|
maxValue: 512,
|
|
step: 4,
|
|
name: 'memory'
|
|
}]
|
|
});
|
|
|
|
Ext.define('PVE.qemu.DisplayEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
vmconfig: undefined,
|
|
|
|
subject: gettext('Display'),
|
|
width: 350,
|
|
|
|
items: [{
|
|
xtype: 'pveDisplayInputPanel'
|
|
}],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response) {
|
|
me.vmconfig = response.result.data;
|
|
var vga = me.vmconfig.vga || '__default__';
|
|
me.setValues(PVE.Parser.parsePropertyString(vga, 'type'));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.KeyboardEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('Keyboard Layout'),
|
|
items: {
|
|
xtype: 'VNCKeyboardSelector',
|
|
name: 'keyboard',
|
|
value: '__default__',
|
|
fieldLabel: gettext('Keyboard Layout')
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.HardwareView', {
|
|
extend: 'Proxmox.grid.PendingObjectGrid',
|
|
alias: ['widget.PVE.qemu.HardwareView'],
|
|
|
|
onlineHelp: 'qm_virtual_machines_settings',
|
|
|
|
renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
|
|
var me = this;
|
|
var rows = me.rows;
|
|
var rowdef = rows[key] || {};
|
|
var iconCls = rowdef.iconCls;
|
|
var icon = '';
|
|
var txt = (rowdef.header || key);
|
|
|
|
metaData.tdAttr = "valign=middle";
|
|
|
|
if (rowdef.tdCls) {
|
|
metaData.tdCls = rowdef.tdCls;
|
|
if (rowdef.tdCls == 'pve-itype-icon-storage') {
|
|
var value = me.getObjectValue(key, '', false);
|
|
if (value === '') {
|
|
value = me.getObjectValue(key, '', true);
|
|
}
|
|
if (value.match(/vm-.*-cloudinit/)) {
|
|
metaData.tdCls = 'pve-itype-icon-cloud';
|
|
return rowdef.cloudheader;
|
|
} else if (value.match(/media=cdrom/)) {
|
|
metaData.tdCls = 'pve-itype-icon-cdrom';
|
|
return rowdef.cdheader;
|
|
}
|
|
}
|
|
} else if (iconCls) {
|
|
icon = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
|
|
metaData.tdCls += " pve-itype-fa";
|
|
}
|
|
return icon + txt;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
var i, confid;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
var diskCap = caps.vms['VM.Config.Disk'];
|
|
|
|
/*jslint confusion: true */
|
|
var rows = {
|
|
memory: {
|
|
header: gettext('Memory'),
|
|
editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
|
|
never_delete: true,
|
|
defaultValue: '512',
|
|
tdCls: 'pve-itype-icon-memory',
|
|
group: 2,
|
|
multiKey: ['memory', 'balloon', 'shares'],
|
|
renderer: function(value, metaData, record, ri, ci, store, pending) {
|
|
var res = '';
|
|
|
|
var max = me.getObjectValue('memory', 512, pending);
|
|
var balloon = me.getObjectValue('balloon', undefined, pending);
|
|
var shares = me.getObjectValue('shares', undefined, pending);
|
|
|
|
res = Proxmox.Utils.format_size(max*1024*1024);
|
|
|
|
if (balloon !== undefined && balloon > 0) {
|
|
res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res;
|
|
|
|
if (shares) {
|
|
res += ' [shares=' + shares +']';
|
|
}
|
|
} else if (balloon === 0) {
|
|
res += ' [balloon=0]';
|
|
}
|
|
return res;
|
|
}
|
|
},
|
|
sockets: {
|
|
header: gettext('Processors'),
|
|
never_delete: true,
|
|
editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ?
|
|
'PVE.qemu.ProcessorEdit' : undefined,
|
|
tdCls: 'pve-itype-icon-processor',
|
|
group: 3,
|
|
defaultValue: '1',
|
|
multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
|
|
renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
|
|
|
|
var sockets = me.getObjectValue('sockets', 1, pending);
|
|
var model = me.getObjectValue('cpu', undefined, pending);
|
|
var cores = me.getObjectValue('cores', 1, pending);
|
|
var numa = me.getObjectValue('numa', undefined, pending);
|
|
var vcpus = me.getObjectValue('vcpus', undefined, pending);
|
|
var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
|
|
var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
|
|
|
|
var res = Ext.String.format('{0} ({1} sockets, {2} cores)',
|
|
sockets*cores, sockets, cores);
|
|
|
|
if (model) {
|
|
res += ' [' + model + ']';
|
|
}
|
|
|
|
if (numa) {
|
|
res += ' [numa=' + numa +']';
|
|
}
|
|
|
|
if (vcpus) {
|
|
res += ' [vcpus=' + vcpus +']';
|
|
}
|
|
|
|
if (cpulimit) {
|
|
res += ' [cpulimit=' + cpulimit +']';
|
|
}
|
|
|
|
if (cpuunits) {
|
|
res += ' [cpuunits=' + cpuunits +']';
|
|
}
|
|
|
|
return res;
|
|
}
|
|
},
|
|
bios: {
|
|
header: 'BIOS',
|
|
group: 4,
|
|
never_delete: true,
|
|
editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
|
|
defaultValue: '',
|
|
iconCls: 'microchip',
|
|
renderer: PVE.Utils.render_qemu_bios
|
|
},
|
|
vga: {
|
|
header: gettext('Display'),
|
|
editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
|
|
never_delete: true,
|
|
tdCls: 'pve-itype-icon-display',
|
|
group:5,
|
|
defaultValue: '',
|
|
renderer: PVE.Utils.render_kvm_vga_driver
|
|
},
|
|
machine: {
|
|
header: gettext('Machine'),
|
|
editor: caps.vms['VM.Config.HWType'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Machine'),
|
|
width: 350,
|
|
items: [{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'machine',
|
|
value: '__default__',
|
|
fieldLabel: gettext('Machine'),
|
|
comboItems: [
|
|
['__default__', PVE.Utils.render_qemu_machine('')],
|
|
['q35', 'q35']
|
|
]
|
|
}]} : undefined,
|
|
iconCls: 'cogs',
|
|
never_delete: true,
|
|
group: 6,
|
|
defaultValue: '',
|
|
renderer: PVE.Utils.render_qemu_machine
|
|
},
|
|
scsihw: {
|
|
header: gettext('SCSI Controller'),
|
|
iconCls: 'database',
|
|
editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
|
|
renderer: PVE.Utils.render_scsihw,
|
|
group: 7,
|
|
never_delete: true,
|
|
defaultValue: ''
|
|
},
|
|
cores: {
|
|
visible: false
|
|
},
|
|
cpu: {
|
|
visible: false
|
|
},
|
|
numa: {
|
|
visible: false
|
|
},
|
|
balloon: {
|
|
visible: false
|
|
},
|
|
hotplug: {
|
|
visible: false
|
|
},
|
|
vcpus: {
|
|
visible: false
|
|
},
|
|
cpuunits: {
|
|
visible: false
|
|
},
|
|
cpulimit: {
|
|
visible: false
|
|
},
|
|
shares: {
|
|
visible: false
|
|
}
|
|
};
|
|
/*jslint confusion: false */
|
|
|
|
PVE.Utils.forEachBus(undefined, function(type, id) {
|
|
var confid = type + id;
|
|
rows[confid] = {
|
|
group: 10,
|
|
tdCls: 'pve-itype-icon-storage',
|
|
editor: 'PVE.qemu.HDEdit',
|
|
never_delete: caps.vms['VM.Config.Disk'] ? false : true,
|
|
header: gettext('Hard Disk') + ' (' + confid +')',
|
|
cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
|
|
cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')'
|
|
};
|
|
});
|
|
for (i = 0; i < 32; i++) {
|
|
confid = "net" + i.toString();
|
|
rows[confid] = {
|
|
group: 15,
|
|
order: i,
|
|
tdCls: 'pve-itype-icon-network',
|
|
editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
|
|
never_delete: caps.vms['VM.Config.Network'] ? false : true,
|
|
header: gettext('Network Device') + ' (' + confid +')'
|
|
};
|
|
}
|
|
rows.efidisk0 = {
|
|
group: 20,
|
|
tdCls: 'pve-itype-icon-storage',
|
|
editor: null,
|
|
never_delete: caps.vms['VM.Config.Disk'] ? false : true,
|
|
header: gettext('EFI Disk')
|
|
};
|
|
for (i = 0; i < 5; i++) {
|
|
confid = "usb" + i.toString();
|
|
rows[confid] = {
|
|
group: 25,
|
|
order: i,
|
|
tdCls: 'pve-itype-icon-usb',
|
|
editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
|
|
never_delete: caps.nodes['Sys.Console'] ? false : true,
|
|
header: gettext('USB Device') + ' (' + confid + ')'
|
|
};
|
|
}
|
|
for (i = 0; i < 4; i++) {
|
|
confid = "hostpci" + i.toString();
|
|
rows[confid] = {
|
|
group: 30,
|
|
order: i,
|
|
tdCls: 'pve-itype-icon-pci',
|
|
never_delete: caps.nodes['Sys.Console'] ? false : true,
|
|
editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
|
|
header: gettext('PCI Device') + ' (' + confid + ')'
|
|
};
|
|
}
|
|
for (i = 0; i < 4; i++) {
|
|
confid = "serial" + i.toString();
|
|
rows[confid] = {
|
|
group: 35,
|
|
order: i,
|
|
tdCls: 'pve-itype-icon-serial',
|
|
never_delete: caps.nodes['Sys.Console'] ? false : true,
|
|
header: gettext('Serial Port') + ' (' + confid + ')'
|
|
};
|
|
}
|
|
for (i = 0; i < 256; i++) {
|
|
rows["unused" + i.toString()] = {
|
|
group: 99,
|
|
order: i,
|
|
tdCls: 'pve-itype-icon-storage',
|
|
editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
|
|
header: gettext('Unused Disk') + ' ' + i.toString()
|
|
};
|
|
}
|
|
|
|
var sorterFn = function(rec1, rec2) {
|
|
var v1 = rec1.data.key;
|
|
var v2 = rec2.data.key;
|
|
var g1 = rows[v1].group || 0;
|
|
var g2 = rows[v2].group || 0;
|
|
var order1 = rows[v1].order || 0;
|
|
var order2 = rows[v2].order || 0;
|
|
|
|
if ((g1 - g2) !== 0) {
|
|
return g1 - g2;
|
|
}
|
|
|
|
if ((order1 - order2) !== 0) {
|
|
return order1 - order2;
|
|
}
|
|
|
|
if (v1 > v2) {
|
|
return 1;
|
|
} else if (v1 < v2) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
var reload = function() {
|
|
me.rstore.load();
|
|
};
|
|
|
|
var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var rowdef = rows[rec.data.key];
|
|
if (!rowdef.editor) {
|
|
return;
|
|
}
|
|
|
|
var editor = rowdef.editor;
|
|
if (rowdef.tdCls == 'pve-itype-icon-storage') {
|
|
var value = me.getObjectValue(rec.data.key, '', true);
|
|
if (value.match(/vm-.*-cloudinit/)) {
|
|
return;
|
|
} else if (value.match(/media=cdrom/)) {
|
|
editor = 'PVE.qemu.CDEdit';
|
|
} else if (!diskCap) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
var win;
|
|
|
|
if (Ext.isString(editor)) {
|
|
win = Ext.create(editor, {
|
|
pveSelNode: me.pveSelNode,
|
|
confid: rec.data.key,
|
|
url: '/api2/extjs/' + baseurl
|
|
});
|
|
} else {
|
|
var config = Ext.apply({
|
|
pveSelNode: me.pveSelNode,
|
|
confid: rec.data.key,
|
|
url: '/api2/extjs/' + baseurl
|
|
}, rowdef.editor);
|
|
win = Ext.createWidget(rowdef.editor.xtype, config);
|
|
win.load();
|
|
}
|
|
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
var run_resize = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.window.HDResize', {
|
|
disk: rec.data.key,
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
|
|
win.show();
|
|
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
var run_move = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.window.HDMove', {
|
|
disk: rec.data.key,
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
|
|
win.show();
|
|
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
selModel: sm,
|
|
disabled: true,
|
|
handler: run_editor
|
|
});
|
|
|
|
var resize_btn = new Proxmox.button.Button({
|
|
text: gettext('Resize disk'),
|
|
selModel: sm,
|
|
disabled: true,
|
|
handler: run_resize
|
|
});
|
|
|
|
var move_btn = new Proxmox.button.Button({
|
|
text: gettext('Move disk'),
|
|
selModel: sm,
|
|
disabled: true,
|
|
handler: run_move
|
|
});
|
|
|
|
var remove_btn = new Proxmox.button.Button({
|
|
text: gettext('Remove'),
|
|
defaultText: gettext('Remove'),
|
|
altText: gettext('Detach'),
|
|
selModel: sm,
|
|
disabled: true,
|
|
dangerous: true,
|
|
RESTMethod: 'PUT',
|
|
confirmMsg: function(rec) {
|
|
var warn = gettext('Are you sure you want to remove entry {0}');
|
|
if (this.text === this.altText) {
|
|
warn = gettext('Are you sure you want to detach entry {0}');
|
|
}
|
|
|
|
var entry = rec.data.key;
|
|
var msg = Ext.String.format(warn, "'"
|
|
+ me.renderKey(entry, {}, rec) + "'");
|
|
|
|
if (entry.match(/^unused\d+$/)) {
|
|
msg += " " + gettext('This will permanently erase all data.');
|
|
}
|
|
|
|
return msg;
|
|
},
|
|
handler: function(b, e, rec) {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/' + baseurl,
|
|
waitMsgTarget: me,
|
|
method: b.RESTMethod,
|
|
params: {
|
|
'delete': rec.data.key
|
|
},
|
|
callback: function() {
|
|
reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
if (b.RESTMethod === 'POST') {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', {
|
|
upid: upid,
|
|
listeners: {
|
|
destroy: function () {
|
|
me.reload();
|
|
}
|
|
}
|
|
});
|
|
win.show();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
listeners: {
|
|
render: function(btn) {
|
|
// hack: calculate an optimal button width on first display
|
|
// to prevent the whole toolbar to move when we switch
|
|
// between the "Remove" and "Detach" labels
|
|
var def = btn.getSize().width;
|
|
|
|
btn.setText(btn.altText);
|
|
var alt = btn.getSize().width;
|
|
|
|
btn.setText(btn.defaultText);
|
|
|
|
var optimal = alt > def ? alt : def;
|
|
btn.setSize({ width: optimal });
|
|
}
|
|
}
|
|
});
|
|
|
|
var revert_btn = new Proxmox.button.Button({
|
|
text: gettext('Revert'),
|
|
selModel: sm,
|
|
disabled: true,
|
|
handler: function(b, e, rec) {
|
|
var rowdef = me.rows[rec.data.key] || {};
|
|
var keys = rowdef.multiKey || [ rec.data.key ];
|
|
var revert = keys.join(',');
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/' + baseurl,
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
params: {
|
|
'revert': revert
|
|
},
|
|
callback: function() {
|
|
reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert('Error',response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
var efidisk_menuitem = Ext.create('Ext.menu.Item',{
|
|
text: gettext('EFI Disk'),
|
|
iconCls: 'pve-itype-icon-storage',
|
|
disabled: !caps.vms['VM.Config.Disk'],
|
|
handler: function() {
|
|
|
|
var rstoredata = me.rstore.getData().map;
|
|
// check if ovmf is configured
|
|
if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') {
|
|
var win = Ext.create('PVE.qemu.EFIDiskEdit', {
|
|
url: '/api2/extjs/' + baseurl,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
} else {
|
|
Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.'));
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
var set_button_status = function() {
|
|
var sm = me.getSelectionModel();
|
|
var rec = sm.getSelection()[0];
|
|
|
|
// disable button when we have an efidisk already
|
|
// disable is ok in this case, because you can instantly
|
|
// see that there is already one
|
|
efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined);
|
|
// en/disable usb add button
|
|
var usbcount = 0;
|
|
var pcicount = 0;
|
|
var hasCloudInit = false;
|
|
me.rstore.getData().items.forEach(function(item){
|
|
if (/^usb\d+/.test(item.id)) {
|
|
usbcount++;
|
|
} else if (/^hostpci\d+/.test(item.id)) {
|
|
pcicount++;
|
|
}
|
|
if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) {
|
|
hasCloudInit = true;
|
|
}
|
|
});
|
|
|
|
// heuristic only for disabling some stuff, the backend has the final word.
|
|
var noSysConsolePerm = !caps.nodes['Sys.Console'];
|
|
|
|
me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5));
|
|
me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4));
|
|
me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit);
|
|
|
|
if (!rec) {
|
|
remove_btn.disable();
|
|
edit_btn.disable();
|
|
resize_btn.disable();
|
|
move_btn.disable();
|
|
revert_btn.disable();
|
|
return;
|
|
}
|
|
var key = rec.data.key;
|
|
var value = rec.data.value;
|
|
var rowdef = rows[key];
|
|
|
|
var pending = rec.data['delete'] || me.hasPendingChanges(key);
|
|
var isCDRom = (value && !!value.toString().match(/media=cdrom/));
|
|
var isUnusedDisk = key.match(/^unused\d+/);
|
|
var isUsedDisk = !isUnusedDisk &&
|
|
rowdef.tdCls == 'pve-itype-icon-storage' &&
|
|
!isCDRom;
|
|
|
|
var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/));
|
|
|
|
var isEfi = (key === 'efidisk0');
|
|
|
|
remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap));
|
|
remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText);
|
|
remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
|
|
|
|
edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap));
|
|
|
|
resize_btn.setDisabled(pending || !isUsedDisk || !diskCap);
|
|
|
|
move_btn.setDisabled(pending || !isUsedDisk || !diskCap);
|
|
|
|
revert_btn.setDisabled(!pending);
|
|
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending',
|
|
interval: 5000,
|
|
selModel: sm,
|
|
run_editor: run_editor,
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
menu: new Ext.menu.Menu({
|
|
items: [
|
|
{
|
|
text: gettext('Hard Disk'),
|
|
iconCls: 'pve-itype-icon-storage',
|
|
disabled: !caps.vms['VM.Config.Disk'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.qemu.HDEdit', {
|
|
url: '/api2/extjs/' + baseurl,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('CD/DVD Drive'),
|
|
iconCls: 'pve-itype-icon-cdrom',
|
|
disabled: !caps.vms['VM.Config.Disk'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.qemu.CDEdit', {
|
|
url: '/api2/extjs/' + baseurl,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Network Device'),
|
|
iconCls: 'pve-itype-icon-network',
|
|
disabled: !caps.vms['VM.Config.Network'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.qemu.NetworkEdit', {
|
|
url: '/api2/extjs/' + baseurl,
|
|
pveSelNode: me.pveSelNode,
|
|
isCreate: true
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
efidisk_menuitem,
|
|
{
|
|
text: gettext('USB Device'),
|
|
itemId: 'addusb',
|
|
iconCls: 'pve-itype-icon-usb',
|
|
disabled: !caps.nodes['Sys.Console'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.qemu.USBEdit', {
|
|
url: '/api2/extjs/' + baseurl,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('PCI Device'),
|
|
itemId: 'addpci',
|
|
iconCls: 'pve-itype-icon-pci',
|
|
disabled: !caps.nodes['Sys.Console'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.qemu.PCIEdit', {
|
|
url: '/api2/extjs/' + baseurl,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Serial Port'),
|
|
itemId: 'addserial',
|
|
iconCls: 'pve-itype-icon-serial',
|
|
disabled: !caps.vms['VM.Config.Options'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.qemu.SerialEdit', {
|
|
url: '/api2/extjs/' + baseurl
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('CloudInit Drive'),
|
|
itemId: 'addci',
|
|
iconCls: 'pve-itype-icon-cloud',
|
|
disabled: !caps.nodes['Sys.Console'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.qemu.CIDriveEdit', {
|
|
url: '/api2/extjs/' + baseurl,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
}
|
|
]
|
|
})
|
|
},
|
|
remove_btn,
|
|
edit_btn,
|
|
resize_btn,
|
|
move_btn,
|
|
revert_btn
|
|
],
|
|
rows: rows,
|
|
sorterFn: sorterFn,
|
|
listeners: {
|
|
itemdblclick: run_editor,
|
|
selectionchange: set_button_status
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.on('activate', me.rstore.startUpdate);
|
|
me.on('destroy', me.rstore.stopUpdate);
|
|
|
|
me.mon(me.rstore, 'refresh', function() {
|
|
set_button_status();
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.ScsiHwEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('SCSI Controller Type'),
|
|
items: {
|
|
xtype: 'pveScsiHwSelector',
|
|
name: 'scsihw',
|
|
value: '__default__',
|
|
fieldLabel: gettext('Type')
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.BiosEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: 'widget.pveQemuBiosEdit',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var EFIHint = Ext.createWidget({
|
|
xtype: 'displayfield', //submitValue is false, so we don't get submitted
|
|
userCls: 'pve-hint',
|
|
value: 'You need to add an EFI disk for storing the ' +
|
|
'EFI settings. See the online help for details.',
|
|
hidden: true
|
|
});
|
|
|
|
Ext.applyIf(me, {
|
|
subject: 'BIOS',
|
|
items: [ {
|
|
xtype: 'pveQemuBiosSelector',
|
|
onlineHelp: 'qm_bios_and_uefi',
|
|
name: 'bios',
|
|
value: '__default__',
|
|
fieldLabel: 'BIOS',
|
|
listeners: {
|
|
'change' : function(field, newValue) {
|
|
if (newValue == 'ovmf') {
|
|
Proxmox.Utils.API2Request({
|
|
url : me.url,
|
|
method : 'GET',
|
|
failure : function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success : function(response, opts) {
|
|
var vmConfig = response.result.data;
|
|
// there can be only one
|
|
if (!vmConfig.efidisk0) {
|
|
EFIHint.setVisible(true);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
if (EFIHint.isVisible()) {
|
|
EFIHint.setVisible(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
EFIHint
|
|
] });
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
|
|
}
|
|
});
|
|
/*jslint confusion: true */
|
|
Ext.define('PVE.qemu.Options', {
|
|
extend: 'Proxmox.grid.PendingObjectGrid',
|
|
alias: ['widget.PVE.qemu.Options'],
|
|
|
|
onlineHelp: 'qm_options',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
var i;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var rows = {
|
|
name: {
|
|
required: true,
|
|
defaultValue: me.pveSelNode.data.name,
|
|
header: gettext('Name'),
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Name'),
|
|
items: {
|
|
xtype: 'inputpanel',
|
|
items:{
|
|
xtype: 'textfield',
|
|
name: 'name',
|
|
vtype: 'DnsName',
|
|
value: '',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: true
|
|
},
|
|
onGetValues: function(values) {
|
|
var params = values;
|
|
if (values.name === undefined ||
|
|
values.name === null ||
|
|
values.name === '') {
|
|
params = { 'delete':'name'};
|
|
}
|
|
return params;
|
|
}
|
|
}
|
|
} : undefined
|
|
},
|
|
onboot: {
|
|
header: gettext('Start at boot'),
|
|
defaultValue: '',
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Start at boot'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'onboot',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
deleteDefaultValue: true,
|
|
fieldLabel: gettext('Start at boot')
|
|
}
|
|
} : undefined
|
|
},
|
|
startup: {
|
|
header: gettext('Start/Shutdown order'),
|
|
defaultValue: '',
|
|
renderer: PVE.Utils.render_kvm_startup,
|
|
editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ?
|
|
{
|
|
xtype: 'pveWindowStartupEdit',
|
|
onlineHelp: 'qm_startup_and_shutdown'
|
|
} : undefined
|
|
},
|
|
ostype: {
|
|
header: gettext('OS Type'),
|
|
editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined,
|
|
renderer: PVE.Utils.render_kvm_ostype,
|
|
defaultValue: 'other'
|
|
},
|
|
bootdisk: {
|
|
visible: false
|
|
},
|
|
boot: {
|
|
header: gettext('Boot Order'),
|
|
defaultValue: 'cdn',
|
|
editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined,
|
|
multiKey: ['boot', 'bootdisk'],
|
|
renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) {
|
|
var i;
|
|
var text = '';
|
|
var bootdisk = me.getObjectValue('bootdisk', undefined, pending);
|
|
order = order || 'cdn';
|
|
for (i = 0; i < order.length; i++) {
|
|
var sel = order.substring(i, i + 1);
|
|
if (text) {
|
|
text += ', ';
|
|
}
|
|
if (sel === 'c') {
|
|
if (bootdisk) {
|
|
text += "Disk '" + bootdisk + "'";
|
|
} else {
|
|
text += "Disk";
|
|
}
|
|
} else if (sel === 'n') {
|
|
text += 'Network';
|
|
} else if (sel === 'a') {
|
|
text += 'Floppy';
|
|
} else if (sel === 'd') {
|
|
text += 'CD-ROM';
|
|
} else {
|
|
text += sel;
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
},
|
|
tablet: {
|
|
header: gettext('Use tablet for pointer'),
|
|
defaultValue: true,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.Config.HWType'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Use tablet for pointer'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'tablet',
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
defaultValue: 1,
|
|
deleteDefaultValue: true,
|
|
fieldLabel: gettext('Enabled')
|
|
}
|
|
} : undefined
|
|
},
|
|
hotplug: {
|
|
header: gettext('Hotplug'),
|
|
defaultValue: 'disk,network,usb',
|
|
renderer: PVE.Utils.render_hotplug_features,
|
|
editor: caps.vms['VM.Config.HWType'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Hotplug'),
|
|
items: {
|
|
xtype: 'pveHotplugFeatureSelector',
|
|
name: 'hotplug',
|
|
value: '',
|
|
multiSelect: true,
|
|
fieldLabel: gettext('Hotplug'),
|
|
allowBlank: true
|
|
}
|
|
} : undefined
|
|
},
|
|
acpi: {
|
|
header: gettext('ACPI support'),
|
|
defaultValue: true,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.Config.HWType'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('ACPI support'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'acpi',
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
defaultValue: 1,
|
|
deleteDefaultValue: true,
|
|
fieldLabel: gettext('Enabled')
|
|
}
|
|
} : undefined
|
|
},
|
|
kvm: {
|
|
header: gettext('KVM hardware virtualization'),
|
|
defaultValue: true,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.Config.HWType'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('KVM hardware virtualization'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'kvm',
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
defaultValue: 1,
|
|
deleteDefaultValue: true,
|
|
fieldLabel: gettext('Enabled')
|
|
}
|
|
} : undefined
|
|
},
|
|
freeze: {
|
|
header: gettext('Freeze CPU at startup'),
|
|
defaultValue: false,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.PowerMgmt'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Freeze CPU at startup'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'freeze',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
deleteDefaultValue: true,
|
|
labelWidth: 140,
|
|
fieldLabel: gettext('Freeze CPU at startup')
|
|
}
|
|
} : undefined
|
|
},
|
|
localtime: {
|
|
header: gettext('Use local time for RTC'),
|
|
defaultValue: false,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Use local time for RTC'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'localtime',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
deleteDefaultValue: true,
|
|
labelWidth: 140,
|
|
fieldLabel: gettext('Use local time for RTC')
|
|
}
|
|
} : undefined
|
|
},
|
|
startdate: {
|
|
header: gettext('RTC start date'),
|
|
defaultValue: 'now',
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('RTC start date'),
|
|
items: {
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'startdate',
|
|
deleteEmpty: true,
|
|
value: 'now',
|
|
fieldLabel: gettext('RTC start date'),
|
|
vtype: 'QemuStartDate',
|
|
allowBlank: true
|
|
}
|
|
} : undefined
|
|
},
|
|
smbios1: {
|
|
header: gettext('SMBIOS settings (type1)'),
|
|
defaultValue: '',
|
|
renderer: Ext.String.htmlEncode,
|
|
editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined
|
|
},
|
|
agent: {
|
|
header: gettext('Qemu Agent'),
|
|
defaultValue: false,
|
|
renderer: PVE.Utils.render_qga_features,
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Qemu Agent'),
|
|
items: {
|
|
xtype: 'pveAgentFeatureSelector',
|
|
name: 'agent'
|
|
}
|
|
} : undefined
|
|
},
|
|
protection: {
|
|
header: gettext('Protection'),
|
|
defaultValue: false,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Protection'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'protection',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
deleteDefaultValue: true,
|
|
fieldLabel: gettext('Enabled')
|
|
}
|
|
} : undefined
|
|
},
|
|
hookscript: {
|
|
header: gettext('Hookscript')
|
|
}
|
|
};
|
|
|
|
var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
|
|
|
|
var edit_btn = new Ext.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
handler: function() { me.run_editor(); }
|
|
});
|
|
|
|
var revert_btn = new Proxmox.button.Button({
|
|
text: gettext('Revert'),
|
|
disabled: true,
|
|
handler: function() {
|
|
var sm = me.getSelectionModel();
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var rowdef = me.rows[rec.data.key] || {};
|
|
var keys = rowdef.multiKey || [ rec.data.key ];
|
|
var revert = keys.join(',');
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/' + baseurl,
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
params: {
|
|
'revert': revert
|
|
},
|
|
callback: function() {
|
|
me.reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert('Error',response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
var set_button_status = function() {
|
|
var sm = me.getSelectionModel();
|
|
var rec = sm.getSelection()[0];
|
|
|
|
if (!rec) {
|
|
edit_btn.disable();
|
|
return;
|
|
}
|
|
|
|
var key = rec.data.key;
|
|
var pending = rec.data['delete'] || me.hasPendingChanges(key);
|
|
var rowdef = rows[key];
|
|
|
|
edit_btn.setDisabled(!rowdef.editor);
|
|
revert_btn.setDisabled(!pending);
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/pending",
|
|
interval: 5000,
|
|
cwidth1: 250,
|
|
tbar: [ edit_btn, revert_btn ],
|
|
rows: rows,
|
|
editorConfig: {
|
|
url: "/api2/extjs/" + baseurl
|
|
},
|
|
listeners: {
|
|
itemdblclick: me.run_editor,
|
|
selectionchange: set_button_status
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.on('activate', me.rstore.startUpdate);
|
|
me.on('destroy', me.rstore.stopUpdate);
|
|
me.on('deactivate', me.rstore.stopUpdate);
|
|
|
|
me.rstore.on('datachanged', function() {
|
|
set_button_status();
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.window.Snapshot', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
resizable: false,
|
|
|
|
// needed for finding the reference to submitbutton
|
|
// because we do not have a controller
|
|
referenceHolder: true,
|
|
defaultButton: 'submitbutton',
|
|
defaultFocus: 'field',
|
|
|
|
take_snapshot: function(snapname, descr, vmstate) {
|
|
var me = this;
|
|
var params = { snapname: snapname, vmstate: vmstate ? 1 : 0 };
|
|
if (descr) {
|
|
params.description = descr;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot",
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
|
|
win.show();
|
|
me.close();
|
|
}
|
|
});
|
|
},
|
|
|
|
update_snapshot: function(snapname, descr) {
|
|
var me = this;
|
|
Proxmox.Utils.API2Request({
|
|
params: { description: descr },
|
|
url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" +
|
|
snapname + '/config',
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
me.close();
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var summarystore = Ext.create('Ext.data.Store', {
|
|
model: 'KeyValue',
|
|
sorters: [
|
|
{
|
|
property : 'key',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
});
|
|
|
|
var items = [
|
|
{
|
|
xtype: me.snapname ? 'displayfield' : 'textfield',
|
|
name: 'snapname',
|
|
value: me.snapname,
|
|
fieldLabel: gettext('Name'),
|
|
vtype: 'ConfigId',
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
if (me.snapname) {
|
|
items.push({
|
|
xtype: 'displayfield',
|
|
name: 'snaptime',
|
|
renderer: PVE.Utils.render_timestamp_human_readable,
|
|
fieldLabel: gettext('Timestamp')
|
|
});
|
|
} else {
|
|
items.push({
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'vmstate',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
checked: 1,
|
|
fieldLabel: gettext('Include RAM')
|
|
});
|
|
}
|
|
|
|
items.push({
|
|
xtype: 'textareafield',
|
|
grow: true,
|
|
name: 'description',
|
|
fieldLabel: gettext('Description')
|
|
});
|
|
|
|
if (me.snapname) {
|
|
items.push({
|
|
title: gettext('Settings'),
|
|
xtype: 'grid',
|
|
height: 200,
|
|
store: summarystore,
|
|
columns: [
|
|
{header: gettext('Key'), width: 150, dataIndex: 'key'},
|
|
{header: gettext('Value'), flex: 1, dataIndex: 'value'}
|
|
]
|
|
});
|
|
}
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
fieldDefaults: {
|
|
labelWidth: 100,
|
|
anchor: '100%'
|
|
},
|
|
items: items
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var submitBtn;
|
|
|
|
if (me.snapname) {
|
|
me.title = gettext('Edit') + ': ' + gettext('Snapshot');
|
|
submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Update'),
|
|
handler: function() {
|
|
if (form.isValid()) {
|
|
var values = form.getValues();
|
|
me.update_snapshot(me.snapname, values.description);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot');
|
|
submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Take Snapshot'),
|
|
reference: 'submitbutton',
|
|
handler: function() {
|
|
if (form.isValid()) {
|
|
var values = form.getValues();
|
|
me.take_snapshot(values.snapname, values.description, values.vmstate);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
modal: true,
|
|
width: 450,
|
|
border: false,
|
|
layout: 'fit',
|
|
buttons: [ submitBtn ],
|
|
items: [ me.formPanel ]
|
|
});
|
|
|
|
if (me.snapname) {
|
|
Ext.apply(me, {
|
|
width: 620,
|
|
height: 420
|
|
});
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
if (!me.snapname) {
|
|
return;
|
|
}
|
|
|
|
// else load data
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" +
|
|
me.snapname + '/config',
|
|
waitMsgTarget: me,
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
me.close();
|
|
},
|
|
success: function(response, options) {
|
|
var data = response.result.data;
|
|
var kvarray = [];
|
|
Ext.Object.each(data, function(key, value) {
|
|
if (key === 'description' || key === 'snaptime') {
|
|
return;
|
|
}
|
|
kvarray.push({ key: key, value: value });
|
|
});
|
|
|
|
summarystore.suspendEvents();
|
|
summarystore.add(kvarray);
|
|
summarystore.sort();
|
|
summarystore.resumeEvents();
|
|
summarystore.fireEvent('refresh', summarystore);
|
|
|
|
form.findField('snaptime').setValue(data.snaptime);
|
|
form.findField('description').setValue(data.description);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.SnapshotTree', {
|
|
extend: 'Ext.tree.Panel',
|
|
alias: ['widget.pveQemuSnapshotTree'],
|
|
|
|
load_delay: 3000,
|
|
|
|
old_digest: 'invalid',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-qemu-snapshots',
|
|
|
|
sorterFn: function(rec1, rec2) {
|
|
var v1 = rec1.data.snaptime;
|
|
var v2 = rec2.data.snaptime;
|
|
|
|
if (rec1.data.name === 'current') {
|
|
return 1;
|
|
}
|
|
if (rec2.data.name === 'current') {
|
|
return -1;
|
|
}
|
|
|
|
return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0));
|
|
},
|
|
|
|
reload: function(repeat) {
|
|
var me = this;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot',
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(me, response.htmlStatus);
|
|
me.load_task.delay(me.load_delay);
|
|
},
|
|
success: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(me, false);
|
|
var digest = 'invalid';
|
|
var idhash = {};
|
|
var root = { name: '__root', expanded: true, children: [] };
|
|
Ext.Array.each(response.result.data, function(item) {
|
|
item.leaf = true;
|
|
item.children = [];
|
|
if (item.name === 'current') {
|
|
digest = item.digest + item.running;
|
|
if (item.running) {
|
|
item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running';
|
|
} else {
|
|
item.iconCls = 'fa fa-fw fa-desktop x-fa-tree';
|
|
}
|
|
} else {
|
|
item.iconCls = 'fa fa-fw fa-history x-fa-tree';
|
|
}
|
|
idhash[item.name] = item;
|
|
});
|
|
|
|
if (digest !== me.old_digest) {
|
|
me.old_digest = digest;
|
|
|
|
Ext.Array.each(response.result.data, function(item) {
|
|
if (item.parent && idhash[item.parent]) {
|
|
var parent_item = idhash[item.parent];
|
|
parent_item.children.push(item);
|
|
parent_item.leaf = false;
|
|
parent_item.expanded = true;
|
|
parent_item.expandable = false;
|
|
} else {
|
|
root.children.push(item);
|
|
}
|
|
});
|
|
|
|
me.setRootNode(root);
|
|
}
|
|
|
|
me.load_task.delay(me.load_delay);
|
|
}
|
|
});
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/feature',
|
|
params: { feature: 'snapshot' },
|
|
method: 'GET',
|
|
success: function(response, options) {
|
|
var res = response.result.data;
|
|
if (res.hasFeature) {
|
|
var snpBtns = Ext.ComponentQuery.query('#snapshotBtn');
|
|
snpBtns.forEach(function(item){
|
|
item.enable();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
},
|
|
|
|
listeners: {
|
|
beforestatesave: function(grid, state, eopts) {
|
|
// extjs cannot serialize functions,
|
|
// so a the sorter with only the sorterFn will
|
|
// not be a valid sorter when restoring the state
|
|
delete state.storeState.sorters;
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.vmid = me.pveSelNode.data.vmid;
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
me.load_task = new Ext.util.DelayedTask(me.reload, me);
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var valid_snapshot = function(record) {
|
|
return record && record.data && record.data.name &&
|
|
record.data.name !== 'current';
|
|
};
|
|
|
|
var valid_snapshot_rollback = function(record) {
|
|
return record && record.data && record.data.name &&
|
|
record.data.name !== 'current' && !record.data.snapstate;
|
|
};
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (valid_snapshot(rec)) {
|
|
var win = Ext.create('PVE.window.Snapshot', {
|
|
snapname: rec.data.name,
|
|
nodename: me.nodename,
|
|
vmid: me.vmid
|
|
});
|
|
win.show();
|
|
me.mon(win, 'close', me.reload, me);
|
|
}
|
|
};
|
|
|
|
var editBtn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: valid_snapshot,
|
|
handler: run_editor
|
|
});
|
|
|
|
var rollbackBtn = new Proxmox.button.Button({
|
|
text: gettext('Rollback'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: valid_snapshot_rollback,
|
|
confirmMsg: function(rec) {
|
|
return Proxmox.Utils.format_task_description('qmrollback', me.vmid) +
|
|
" '" + rec.data.name + "'";
|
|
},
|
|
handler: function(btn, event) {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var snapname = rec.data.name;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname + '/rollback',
|
|
method: 'POST',
|
|
waitMsgTarget: me,
|
|
callback: function() {
|
|
me.reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
|
|
win.show();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
var removeBtn = new Proxmox.button.Button({
|
|
text: gettext('Remove'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
confirmMsg: function(rec) {
|
|
var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
|
|
"'" + rec.data.name + "'");
|
|
return msg;
|
|
},
|
|
enableFn: valid_snapshot,
|
|
handler: function(btn, event) {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var snapname = rec.data.name;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname,
|
|
method: 'DELETE',
|
|
waitMsgTarget: me,
|
|
callback: function() {
|
|
me.reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
|
|
win.show();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
var snapshotBtn = Ext.create('Ext.Button', {
|
|
itemId: 'snapshotBtn',
|
|
text: gettext('Take Snapshot'),
|
|
disabled: true,
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.Snapshot', {
|
|
nodename: me.nodename,
|
|
vmid: me.vmid
|
|
});
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
layout: 'fit',
|
|
rootVisible: false,
|
|
animate: false,
|
|
sortableColumns: false,
|
|
selModel: sm,
|
|
tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ],
|
|
fields: [
|
|
'name', 'description', 'snapstate', 'vmstate', 'running',
|
|
{ name: 'snaptime', type: 'date', dateFormat: 'timestamp' }
|
|
],
|
|
columns: [
|
|
{
|
|
xtype: 'treecolumn',
|
|
text: gettext('Name'),
|
|
dataIndex: 'name',
|
|
width: 200,
|
|
renderer: function(value, metaData, record) {
|
|
if (value === 'current') {
|
|
return "NOW";
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: gettext('RAM'),
|
|
align: 'center',
|
|
resizable: false,
|
|
dataIndex: 'vmstate',
|
|
width: 50,
|
|
renderer: function(value, metaData, record) {
|
|
if (record.data.name !== 'current') {
|
|
return Proxmox.Utils.format_boolean(value);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Date') + "/" + gettext("Status"),
|
|
dataIndex: 'snaptime',
|
|
width: 150,
|
|
renderer: function(value, metaData, record) {
|
|
if (record.data.snapstate) {
|
|
return record.data.snapstate;
|
|
}
|
|
if (value) {
|
|
return Ext.Date.format(value,'Y-m-d H:i:s');
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Description'),
|
|
dataIndex: 'description',
|
|
flex: 1,
|
|
renderer: function(value, metaData, record) {
|
|
if (record.data.name === 'current') {
|
|
return gettext("You are here!");
|
|
} else {
|
|
return Ext.String.htmlEncode(value);
|
|
}
|
|
}
|
|
}
|
|
],
|
|
columnLines: true, // will work in 4.1?
|
|
listeners: {
|
|
activate: me.reload,
|
|
destroy: me.load_task.cancel,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.store.sorters.add(new Ext.util.Sorter({
|
|
sorterFn: me.sorterFn
|
|
}));
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.Config', {
|
|
extend: 'PVE.panel.Config',
|
|
alias: 'widget.PVE.qemu.Config',
|
|
|
|
onlineHelp: 'chapter_virtual_machines',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var template = !!me.pveSelNode.data.template;
|
|
|
|
var running = !!me.pveSelNode.data.uptime;
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var base_url = '/nodes/' + nodename + "/qemu/" + vmid;
|
|
|
|
me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
|
|
url: '/api2/json' + base_url + '/status/current',
|
|
interval: 1000
|
|
});
|
|
|
|
var vm_command = function(cmd, params) {
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: base_url + '/status/' + cmd,
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
var resumeBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Resume'),
|
|
disabled: !caps.vms['VM.PowerMgmt'],
|
|
hidden: true,
|
|
handler: function() {
|
|
vm_command('resume');
|
|
},
|
|
iconCls: 'fa fa-play'
|
|
});
|
|
|
|
var startBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Start'),
|
|
disabled: !caps.vms['VM.PowerMgmt'] || running,
|
|
hidden: template,
|
|
handler: function() {
|
|
vm_command('start');
|
|
},
|
|
iconCls: 'fa fa-play'
|
|
});
|
|
|
|
var migrateBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Migrate'),
|
|
disabled: !caps.vms['VM.Migrate'],
|
|
hidden: PVE.data.ResourceStore.getNodes().length < 2,
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.Migrate', {
|
|
vmtype: 'qemu',
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
win.show();
|
|
},
|
|
iconCls: 'fa fa-send-o'
|
|
});
|
|
|
|
var moreBtn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('More'),
|
|
menu: { items: [
|
|
{
|
|
text: gettext('Clone'),
|
|
iconCls: 'fa fa-fw fa-clone',
|
|
hidden: caps.vms['VM.Clone'] ? false : true,
|
|
handler: function() {
|
|
PVE.window.Clone.wrap(nodename, vmid, template, 'qemu');
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Convert to template'),
|
|
disabled: template,
|
|
xtype: 'pveMenuItem',
|
|
iconCls: 'fa fa-fw fa-file-o',
|
|
hidden: caps.vms['VM.Allocate'] ? false : true,
|
|
confirmMsg: Proxmox.Utils.format_task_description('qmtemplate', vmid),
|
|
handler: function() {
|
|
Proxmox.Utils.API2Request({
|
|
url: base_url + '/template',
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
{
|
|
iconCls: 'fa fa-heartbeat ',
|
|
hidden: !caps.nodes['Sys.Console'],
|
|
text: gettext('Manage HA'),
|
|
handler: function() {
|
|
var ha = me.pveSelNode.data.hastate;
|
|
Ext.create('PVE.ha.VMResourceEdit', {
|
|
vmid: vmid,
|
|
isCreate: (!ha || ha === 'unmanaged')
|
|
}).show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Remove'),
|
|
itemId: 'removeBtn',
|
|
disabled: !caps.vms['VM.Allocate'],
|
|
handler: function() {
|
|
Ext.create('PVE.window.SafeDestroy', {
|
|
url: base_url,
|
|
item: { type: 'VM', id: vmid }
|
|
}).show();
|
|
},
|
|
iconCls: 'fa fa-trash-o'
|
|
}
|
|
]}
|
|
});
|
|
|
|
var shutdownBtn = Ext.create('PVE.button.Split', {
|
|
text: gettext('Shutdown'),
|
|
disabled: !caps.vms['VM.PowerMgmt'] || !running,
|
|
hidden: template,
|
|
confirmMsg: Proxmox.Utils.format_task_description('qmshutdown', vmid),
|
|
handler: function() {
|
|
vm_command('shutdown');
|
|
},
|
|
menu: {
|
|
items: [{
|
|
text: gettext('Pause'),
|
|
disabled: !caps.vms['VM.PowerMgmt'],
|
|
confirmMsg: Proxmox.Utils.format_task_description('qmpause', vmid),
|
|
handler: function() {
|
|
vm_command("suspend");
|
|
},
|
|
iconCls: 'fa fa-pause'
|
|
},{
|
|
text: gettext('Hibernate'),
|
|
disabled: !caps.vms['VM.PowerMgmt'],
|
|
confirmMsg: Proxmox.Utils.format_task_description('qmsuspend', vmid),
|
|
tooltip: gettext('Suspend to disk'),
|
|
handler: function() {
|
|
vm_command("suspend", { todisk: 1 });
|
|
},
|
|
iconCls: 'fa fa-download'
|
|
},{
|
|
text: gettext('Stop'),
|
|
disabled: !caps.vms['VM.PowerMgmt'],
|
|
dangerous: true,
|
|
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
|
|
confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid),
|
|
handler: function() {
|
|
vm_command("stop", { timeout: 30 });
|
|
},
|
|
iconCls: 'fa fa-stop'
|
|
},{
|
|
text: gettext('Reset'),
|
|
disabled: !caps.vms['VM.PowerMgmt'],
|
|
confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid),
|
|
handler: function() {
|
|
vm_command("reset");
|
|
},
|
|
iconCls: 'fa fa-bolt'
|
|
}]
|
|
},
|
|
iconCls: 'fa fa-power-off'
|
|
});
|
|
|
|
var vm = me.pveSelNode.data;
|
|
|
|
var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
|
|
disabled: !caps.vms['VM.Console'],
|
|
hidden: template,
|
|
consoleType: 'kvm',
|
|
consoleName: vm.name,
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
|
|
var statusTxt = Ext.create('Ext.toolbar.TextItem', {
|
|
data: {
|
|
lock: undefined
|
|
},
|
|
tpl: [
|
|
'<tpl if="lock">',
|
|
'<i class="fa fa-lg fa-lock"></i> ({lock})',
|
|
'</tpl>'
|
|
]
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename),
|
|
hstateid: 'kvmtab',
|
|
tbarSpacing: false,
|
|
tbar: [ statusTxt, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn ],
|
|
defaults: { statusStore: me.statusStore },
|
|
items: [
|
|
{
|
|
title: gettext('Summary'),
|
|
xtype: 'pveQemuSummary',
|
|
iconCls: 'fa fa-book',
|
|
itemId: 'summary'
|
|
}
|
|
]
|
|
});
|
|
|
|
if (caps.vms['VM.Console'] && !template) {
|
|
me.items.push({
|
|
title: gettext('Console'),
|
|
itemId: 'console',
|
|
iconCls: 'fa fa-terminal',
|
|
xtype: 'pveNoVncConsole',
|
|
vmid: vmid,
|
|
consoleType: 'kvm',
|
|
nodename: nodename
|
|
});
|
|
}
|
|
|
|
me.items.push(
|
|
{
|
|
title: gettext('Hardware'),
|
|
itemId: 'hardware',
|
|
iconCls: 'fa fa-desktop',
|
|
xtype: 'PVE.qemu.HardwareView'
|
|
},
|
|
{
|
|
title: 'Cloud-Init',
|
|
itemId: 'cloudinit',
|
|
iconCls: 'fa fa-cloud',
|
|
xtype: 'pveCiPanel'
|
|
},
|
|
{
|
|
title: gettext('Options'),
|
|
iconCls: 'fa fa-gear',
|
|
itemId: 'options',
|
|
xtype: 'PVE.qemu.Options'
|
|
},
|
|
{
|
|
title: gettext('Task History'),
|
|
itemId: 'tasks',
|
|
xtype: 'proxmoxNodeTasks',
|
|
iconCls: 'fa fa-list',
|
|
nodename: nodename,
|
|
vmidFilter: vmid
|
|
}
|
|
);
|
|
|
|
if (caps.vms['VM.Monitor'] && !template) {
|
|
me.items.push({
|
|
title: gettext('Monitor'),
|
|
iconCls: 'fa fa-eye',
|
|
itemId: 'monitor',
|
|
xtype: 'pveQemuMonitor'
|
|
});
|
|
}
|
|
|
|
if (caps.vms['VM.Backup']) {
|
|
me.items.push({
|
|
title: gettext('Backup'),
|
|
iconCls: 'fa fa-floppy-o',
|
|
xtype: 'pveBackupView',
|
|
itemId: 'backup'
|
|
},
|
|
{
|
|
title: gettext('Replication'),
|
|
iconCls: 'fa fa-retweet',
|
|
xtype: 'pveReplicaView',
|
|
itemId: 'replication'
|
|
});
|
|
}
|
|
|
|
if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback']) && !template) {
|
|
me.items.push({
|
|
title: gettext('Snapshots'),
|
|
iconCls: 'fa fa-history',
|
|
xtype: 'pveQemuSnapshotTree',
|
|
itemId: 'snapshot'
|
|
});
|
|
}
|
|
|
|
if (caps.vms['VM.Console']) {
|
|
me.items.push(
|
|
{
|
|
xtype: 'pveFirewallRules',
|
|
title: gettext('Firewall'),
|
|
iconCls: 'fa fa-shield',
|
|
allow_iface: true,
|
|
base_url: base_url + '/firewall/rules',
|
|
list_refs_url: base_url + '/firewall/refs',
|
|
itemId: 'firewall'
|
|
},
|
|
{
|
|
xtype: 'pveFirewallOptions',
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-gear',
|
|
onlineHelp: 'pve_firewall_vm_container_configuration',
|
|
title: gettext('Options'),
|
|
base_url: base_url + '/firewall/options',
|
|
fwtype: 'vm',
|
|
itemId: 'firewall-options'
|
|
},
|
|
{
|
|
xtype: 'pveFirewallAliases',
|
|
title: gettext('Alias'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-external-link',
|
|
base_url: base_url + '/firewall/aliases',
|
|
itemId: 'firewall-aliases'
|
|
},
|
|
{
|
|
xtype: 'pveIPSet',
|
|
title: gettext('IPSet'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-list-ol',
|
|
base_url: base_url + '/firewall/ipset',
|
|
list_refs_url: base_url + '/firewall/refs',
|
|
itemId: 'firewall-ipset'
|
|
},
|
|
{
|
|
title: gettext('Log'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-list',
|
|
onlineHelp: 'chapter_pve_firewall',
|
|
itemId: 'firewall-fwlog',
|
|
xtype: 'proxmoxLogView',
|
|
url: '/api2/extjs' + base_url + '/firewall/log'
|
|
}
|
|
);
|
|
}
|
|
|
|
if (caps.vms['Permissions.Modify']) {
|
|
me.items.push({
|
|
xtype: 'pveACLView',
|
|
title: gettext('Permissions'),
|
|
iconCls: 'fa fa-unlock',
|
|
itemId: 'permissions',
|
|
path: '/vms/' + vmid
|
|
});
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
me.mon(me.statusStore, 'load', function(s, records, success) {
|
|
var status;
|
|
var qmpstatus;
|
|
var spice = false;
|
|
var xtermjs = false;
|
|
var lock;
|
|
|
|
if (!success) {
|
|
status = qmpstatus = 'unknown';
|
|
} else {
|
|
var rec = s.data.get('status');
|
|
status = rec ? rec.data.value : 'unknown';
|
|
rec = s.data.get('qmpstatus');
|
|
qmpstatus = rec ? rec.data.value : 'unknown';
|
|
rec = s.data.get('template');
|
|
template = rec.data.value || false;
|
|
rec = s.data.get('lock');
|
|
lock = rec ? rec.data.value : undefined;
|
|
|
|
spice = s.data.get('spice') ? true : false;
|
|
xtermjs = s.data.get('serial') ? true : false;
|
|
|
|
}
|
|
|
|
if (template) {
|
|
return;
|
|
}
|
|
|
|
var resume = (['prelaunch', 'paused', 'suspended'].indexOf(qmpstatus) !== -1);
|
|
|
|
if (resume || lock === 'suspended') {
|
|
startBtn.setVisible(false);
|
|
resumeBtn.setVisible(true);
|
|
} else {
|
|
startBtn.setVisible(true);
|
|
resumeBtn.setVisible(false);
|
|
}
|
|
|
|
consoleBtn.setEnableSpice(spice);
|
|
consoleBtn.setEnableXtermJS(xtermjs);
|
|
|
|
statusTxt.update({ lock: lock });
|
|
|
|
startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template);
|
|
shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
|
|
me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
|
|
consoleBtn.setDisabled(template);
|
|
});
|
|
|
|
me.on('afterrender', function() {
|
|
me.statusStore.startUpdate();
|
|
});
|
|
|
|
me.on('destroy', function() {
|
|
me.statusStore.stopUpdate();
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.qemu.CreateWizard', {
|
|
extend: 'PVE.window.Wizard',
|
|
alias: 'widget.pveQemuCreateWizard',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
viewModel: {
|
|
data: {
|
|
nodename: '',
|
|
current: {
|
|
scsihw: ''
|
|
}
|
|
}
|
|
},
|
|
|
|
cbindData: {
|
|
nodename: undefined
|
|
},
|
|
|
|
subject: gettext('Virtual Machine'),
|
|
|
|
items: [
|
|
{
|
|
xtype: 'inputpanel',
|
|
title: gettext('General'),
|
|
onlineHelp: 'qm_general_settings',
|
|
column1: [
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
name: 'nodename',
|
|
cbind: {
|
|
selectCurNode: '{!nodename}',
|
|
preferredValue: '{nodename}'
|
|
},
|
|
bind: {
|
|
value: '{nodename}'
|
|
},
|
|
fieldLabel: gettext('Node'),
|
|
allowBlank: false,
|
|
onlineValidator: true
|
|
},
|
|
{
|
|
xtype: 'pveGuestIDSelector',
|
|
name: 'vmid',
|
|
guestType: 'qemu',
|
|
value: '',
|
|
loadNextFreeID: true,
|
|
validateExists: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'name',
|
|
vtype: 'DnsName',
|
|
value: '',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: true
|
|
}
|
|
],
|
|
column2: [
|
|
{
|
|
xtype: 'pvePoolSelector',
|
|
fieldLabel: gettext('Resource Pool'),
|
|
name: 'pool',
|
|
value: '',
|
|
allowBlank: true
|
|
}
|
|
],
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'onboot',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
deleteDefaultValue: true,
|
|
fieldLabel: gettext('Start at boot')
|
|
}
|
|
],
|
|
advancedColumn2: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'order',
|
|
defaultValue: '',
|
|
emptyText: 'any',
|
|
labelWidth: 120,
|
|
fieldLabel: gettext('Start/Shutdown order')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'up',
|
|
defaultValue: '',
|
|
emptyText: 'default',
|
|
labelWidth: 120,
|
|
fieldLabel: gettext('Startup delay')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'down',
|
|
defaultValue: '',
|
|
emptyText: 'default',
|
|
labelWidth: 120,
|
|
fieldLabel: gettext('Shutdown timeout')
|
|
}
|
|
],
|
|
onGetValues: function(values) {
|
|
|
|
['name', 'pool', 'onboot', 'agent'].forEach(function(field) {
|
|
if (!values[field]) {
|
|
delete values[field];
|
|
}
|
|
});
|
|
|
|
var res = PVE.Parser.printStartup({
|
|
order: values.order,
|
|
up: values.up,
|
|
down: values.down
|
|
});
|
|
|
|
if (res) {
|
|
values.startup = res;
|
|
}
|
|
|
|
delete values.order;
|
|
delete values.up;
|
|
delete values.down;
|
|
|
|
return values;
|
|
}
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
layout: 'hbox',
|
|
defaults: {
|
|
flex: 1,
|
|
padding: '0 10'
|
|
},
|
|
title: gettext('OS'),
|
|
items: [
|
|
{
|
|
xtype: 'pveQemuCDInputPanel',
|
|
bind: {
|
|
nodename: '{nodename}'
|
|
},
|
|
confid: 'ide2',
|
|
insideWizard: true
|
|
},
|
|
{
|
|
xtype: 'pveQemuOSTypePanel',
|
|
insideWizard: true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'pveQemuSystemPanel',
|
|
title: gettext('System'),
|
|
isCreate: true,
|
|
insideWizard: true
|
|
},
|
|
{
|
|
xtype: 'pveQemuHDInputPanel',
|
|
bind: {
|
|
nodename: '{nodename}'
|
|
},
|
|
title: gettext('Hard Disk'),
|
|
isCreate: true,
|
|
insideWizard: true
|
|
},
|
|
{
|
|
xtype: 'pveQemuProcessorPanel',
|
|
insideWizard: true,
|
|
title: gettext('CPU')
|
|
},
|
|
{
|
|
xtype: 'pveQemuMemoryPanel',
|
|
insideWizard: true,
|
|
title: gettext('Memory')
|
|
},
|
|
{
|
|
xtype: 'pveQemuNetworkInputPanel',
|
|
bind: {
|
|
nodename: '{nodename}'
|
|
},
|
|
title: gettext('Network'),
|
|
insideWizard: true
|
|
},
|
|
{
|
|
title: gettext('Confirm'),
|
|
layout: 'fit',
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
store: {
|
|
model: 'KeyValue',
|
|
sorters: [{
|
|
property : 'key',
|
|
direction: 'ASC'
|
|
}]
|
|
},
|
|
columns: [
|
|
{header: 'Key', width: 150, dataIndex: 'key'},
|
|
{header: 'Value', flex: 1, dataIndex: 'value'}
|
|
]
|
|
}
|
|
],
|
|
dockedItems: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'start',
|
|
dock: 'bottom',
|
|
margin: '5 0 0 0',
|
|
boxLabel: gettext('Start after created')
|
|
}
|
|
],
|
|
listeners: {
|
|
show: function(panel) {
|
|
var kv = this.up('window').getValues();
|
|
var data = [];
|
|
Ext.Object.each(kv, function(key, value) {
|
|
if (key === 'delete') { // ignore
|
|
return;
|
|
}
|
|
data.push({ key: key, value: value });
|
|
});
|
|
|
|
var summarystore = panel.down('grid').getStore();
|
|
summarystore.suspendEvents();
|
|
summarystore.removeAll();
|
|
summarystore.add(data);
|
|
summarystore.sort();
|
|
summarystore.resumeEvents();
|
|
summarystore.fireEvent('refresh');
|
|
|
|
}
|
|
},
|
|
onSubmit: function() {
|
|
var wizard = this.up('window');
|
|
var kv = wizard.getValues();
|
|
delete kv['delete'];
|
|
|
|
var nodename = kv.nodename;
|
|
delete kv.nodename;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + nodename + '/qemu',
|
|
waitMsgTarget: wizard,
|
|
method: 'POST',
|
|
params: kv,
|
|
success: function(response){
|
|
wizard.close();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
|
|
|
|
|
|
Ext.define('PVE.qemu.USBInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
mixins: ['Proxmox.Mixin.CBind' ],
|
|
|
|
autoComplete: false,
|
|
onlineHelp: 'qm_usb_passthrough',
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
control: {
|
|
'field[name=usb]': {
|
|
change: function(field, newValue, oldValue) {
|
|
var hwidfield = this.lookupReference('hwid');
|
|
var portfield = this.lookupReference('port');
|
|
var usb3field = this.lookupReference('usb3');
|
|
if (field.inputValue === 'hostdevice') {
|
|
hwidfield.setDisabled(!newValue);
|
|
} else if(field.inputValue === 'port') {
|
|
portfield.setDisabled(!newValue);
|
|
} else if(field.inputValue === 'spice') {
|
|
usb3field.setDisabled(newValue);
|
|
}
|
|
}
|
|
},
|
|
'pveUSBSelector': {
|
|
change: function(field, newValue, oldValue) {
|
|
var usbval = field.getUSBValue();
|
|
var usb3field = this.lookupReference('usb3');
|
|
var usb3 = /usb3/.test(usbval);
|
|
if(usb3 && !usb3field.isDisabled()) {
|
|
usb3field.savedVal = usb3field.getValue();
|
|
usb3field.setValue(true);
|
|
usb3field.setDisabled(true);
|
|
} else if(!usb3 && usb3field.isDisabled()){
|
|
var val = (usb3field.savedVal === undefined)?usb3field.originalValue:usb3field.savedVal;
|
|
usb3field.setValue(val);
|
|
usb3field.setDisabled(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
var me = this;
|
|
me.vmconfig = vmconfig;
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
if(!me.confid) {
|
|
var i;
|
|
for (i = 0; i < 6; i++) {
|
|
if (!me.vmconfig['usb' + i.toString()]) {
|
|
me.confid = 'usb' + i.toString();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
var val = "";
|
|
var type = me.down('radiofield').getGroupValue();
|
|
switch (type) {
|
|
case 'spice':
|
|
val = 'spice'; break;
|
|
case 'hostdevice':
|
|
case 'port':
|
|
val = me.down('pveUSBSelector[name=' + type + ']').getUSBValue();
|
|
if (!/usb3/.test(val) && me.down('field[name=usb3]').getValue() === true) {
|
|
val += ',usb3=1';
|
|
}
|
|
break;
|
|
default:
|
|
throw "invalid type selected";
|
|
}
|
|
|
|
values[me.confid] = val;
|
|
return values;
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'fieldcontainer',
|
|
defaultType: 'radiofield',
|
|
items:[
|
|
{
|
|
name: 'usb',
|
|
inputValue: 'spice',
|
|
boxLabel: gettext('Spice Port'),
|
|
submitValue: false,
|
|
checked: true
|
|
},
|
|
{
|
|
name: 'usb',
|
|
inputValue: 'hostdevice',
|
|
boxLabel: gettext('Use USB Vendor/Device ID'),
|
|
submitValue: false
|
|
},
|
|
{
|
|
xtype: 'pveUSBSelector',
|
|
disabled: true,
|
|
type: 'device',
|
|
name: 'hostdevice',
|
|
cbind: { pveSelNode: '{pveSelNode}' },
|
|
editable: true,
|
|
reference: 'hwid',
|
|
allowBlank: false,
|
|
fieldLabel: 'Choose Device',
|
|
labelAlign: 'right',
|
|
submitValue: false
|
|
},
|
|
{
|
|
name: 'usb',
|
|
inputValue: 'port',
|
|
boxLabel: gettext('Use USB Port'),
|
|
submitValue: false
|
|
},
|
|
{
|
|
xtype: 'pveUSBSelector',
|
|
disabled: true,
|
|
name: 'port',
|
|
cbind: { pveSelNode: '{pveSelNode}' },
|
|
editable: true,
|
|
type: 'port',
|
|
reference: 'port',
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Choose Port'),
|
|
labelAlign: 'right',
|
|
submitValue: false
|
|
},
|
|
{
|
|
xtype: 'checkbox',
|
|
name: 'usb3',
|
|
submitValue: false,
|
|
reference: 'usb3',
|
|
fieldLabel: gettext('Use USB3')
|
|
}
|
|
]
|
|
}
|
|
]
|
|
});
|
|
|
|
Ext.define('PVE.qemu.USBEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
vmconfig: undefined,
|
|
|
|
isAdd: true,
|
|
|
|
subject: gettext('USB Device'),
|
|
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.confid;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.USBInputPanel', {
|
|
confid: me.confid,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
ipanel.setVMConfig(response.result.data);
|
|
if (me.confid) {
|
|
var data = response.result.data[me.confid].split(',');
|
|
var port, hostdevice, usb3 = false;
|
|
var type = 'spice';
|
|
var i;
|
|
for (i = 0; i < data.length; i++) {
|
|
if (/^(host=)?(0x)?[a-zA-Z0-9]{4}\:(0x)?[a-zA-Z0-9]{4}$/.test(data[i])) {
|
|
hostdevice = data[i];
|
|
hostdevice = hostdevice.replace('host=', '').replace('0x','');
|
|
type = 'hostdevice';
|
|
} else if (/^(host=)?(\d+)\-(\d+(\.\d+)*)$/.test(data[i])) {
|
|
port = data[i];
|
|
port = port.replace('host=','');
|
|
type = 'port';
|
|
}
|
|
|
|
if (/^usb3=(1|on|true)$/.test(data[i])) {
|
|
usb3 = true;
|
|
}
|
|
}
|
|
var values = {
|
|
usb : type,
|
|
hostdevice: hostdevice,
|
|
port: port,
|
|
usb3: usb3
|
|
};
|
|
|
|
ipanel.setValues(values);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.PCIInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
|
|
onlineHelp: 'qm_pci_passthrough',
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
var me = this;
|
|
me.vmconfig = vmconfig;
|
|
|
|
var hostpci = me.vmconfig[me.confid] || '';
|
|
|
|
var values = PVE.Parser.parsePropertyString(hostpci, 'host');
|
|
if (values.host && values.host.length < 6) { // 00:00 format not 00:00.0
|
|
values.host += ".0";
|
|
values.multifunction = true;
|
|
}
|
|
values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0);
|
|
values.pcie = PVE.Parser.parseBoolean(values.pcie, 0);
|
|
values.rombar = PVE.Parser.parseBoolean(values.rombar, 1);
|
|
|
|
me.setValues(values);
|
|
if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) {
|
|
// machine is not set to some variant of q35, so we disable pcie
|
|
var pcie = me.down('field[name=pcie]');
|
|
pcie.setDisabled(true);
|
|
pcie.setBoxLabel(gettext('Q35 only'));
|
|
}
|
|
|
|
if (values.romfile) {
|
|
me.down('field[name=romfile]').setVisible(true);
|
|
}
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
var ret = {};
|
|
if(!me.confid) {
|
|
var i;
|
|
for (i = 0; i < 5; i++) {
|
|
if (!me.vmconfig['hostpci' + i.toString()]) {
|
|
me.confid = 'hostpci' + i.toString();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (values.multifunction) {
|
|
// modify host to skip the '.X'
|
|
values.host = values.host.substring(0,5);
|
|
delete values.multifunction;
|
|
}
|
|
|
|
if (values.rombar) {
|
|
delete values.rombar;
|
|
} else {
|
|
values.rombar = 0;
|
|
}
|
|
|
|
if (!values.romfile) {
|
|
delete values.romfile;
|
|
}
|
|
|
|
ret[me.confid] = PVE.Parser.printPropertyString(values, 'host');
|
|
return ret;
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: 'pvePCISelector',
|
|
fieldLabel: gettext('Device'),
|
|
name: 'host',
|
|
nodename: me.nodename,
|
|
allowBlank: false,
|
|
onLoadCallBack: function(store, records, success) {
|
|
if (!success || !records.length) {
|
|
return;
|
|
}
|
|
|
|
var first = records[0];
|
|
if (first.data.iommugroup === -1) {
|
|
// no iommu groups
|
|
var warning = Ext.create('Ext.form.field.Display', {
|
|
columnWidth: 1,
|
|
padding: '0 0 10 0',
|
|
value: 'No IOMMU detected, please activate it.' +
|
|
'See Documentation for further information.',
|
|
userCls: 'pve-hint'
|
|
});
|
|
me.items.insert(0, warning);
|
|
me.updateLayout(); // insert does not trigger that
|
|
}
|
|
},
|
|
listeners: {
|
|
change: function(pcisel, value) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
var pcidev = pcisel.getStore().getById(value);
|
|
var mdevfield = me.down('field[name=mdev]');
|
|
mdevfield.setDisabled(!pcidev || !pcidev.data.mdev);
|
|
if (!pcidev) {
|
|
return;
|
|
}
|
|
var id = pcidev.data.id.substring(0,5); // 00:00
|
|
var iommu = pcidev.data.iommugroup;
|
|
// try to find out if there are more devices
|
|
// in that iommu group
|
|
if (iommu !== -1) {
|
|
var count = 0;
|
|
pcisel.getStore().each(function(record) {
|
|
if (record.data.iommugroup === iommu &&
|
|
record.data.id.substring(0,5) !== id)
|
|
{
|
|
count++;
|
|
return false;
|
|
}
|
|
});
|
|
var warning = me.down('#iommuwarning');
|
|
if (count && !warning) {
|
|
warning = Ext.create('Ext.form.field.Display', {
|
|
columnWidth: 1,
|
|
padding: '0 0 10 0',
|
|
itemId: 'iommuwarning',
|
|
value: 'The selected Device is not in a seperate' +
|
|
'IOMMU group, make sure this is intended.',
|
|
userCls: 'pve-hint'
|
|
});
|
|
me.items.insert(0, warning);
|
|
me.updateLayout(); // insert does not trigger that
|
|
} else if (!count && warning) {
|
|
me.remove(warning);
|
|
}
|
|
}
|
|
if (pcidev.data.mdev) {
|
|
mdevfield.setPciID(value);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('All Functions'),
|
|
name: 'multifunction'
|
|
}
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'pveMDevSelector',
|
|
name: 'mdev',
|
|
disabled: true,
|
|
fieldLabel: gettext('MDev Type'),
|
|
nodename: me.nodename,
|
|
listeners: {
|
|
change: function(field, value) {
|
|
var mf = me.down('field[name=multifunction]');
|
|
if (!!value) {
|
|
mf.setValue(false);
|
|
}
|
|
mf.setDisabled(!!value);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Primary GPU'),
|
|
name: 'x-vga'
|
|
}
|
|
];
|
|
|
|
me.advancedColumn1 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: 'ROM-Bar',
|
|
name: 'rombar'
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
submitValue: true,
|
|
hidden: true,
|
|
fieldLabel: 'ROM-File',
|
|
name: 'romfile'
|
|
}
|
|
];
|
|
|
|
me.advancedColumn2 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: 'PCI-Express',
|
|
name: 'pcie'
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.PCIEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
vmconfig: undefined,
|
|
|
|
isAdd: true,
|
|
|
|
subject: gettext('PCI Device'),
|
|
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.confid;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.PCIInputPanel', {
|
|
confid: me.confid,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response) {
|
|
ipanel.setVMConfig(response.result.data);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true */
|
|
Ext.define('PVE.qemu.SerialnputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
|
|
autoComplete: false,
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
var me = this, i;
|
|
me.vmconfig = vmconfig;
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
var port = 'serial' + i.toString();
|
|
if (!me.vmconfig[port]) {
|
|
me.down('field[name=serialid]').setValue(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var id = 'serial' + values.serialid;
|
|
delete values.serialid;
|
|
values[id] = 'socket';
|
|
return values;
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'serialid',
|
|
fieldLabel: gettext('Serial Port'),
|
|
minValue: 0,
|
|
maxValue: 3,
|
|
allowBlank: false,
|
|
validator: function(id) {
|
|
if (!this.rendered) {
|
|
return true;
|
|
}
|
|
var me = this.up('panel');
|
|
if (me.vmconfig !== undefined && Ext.isDefined(me.vmconfig['serial' + id])) {
|
|
return "This device is already in use.";
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
Ext.define('PVE.qemu.SerialEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
vmconfig: undefined,
|
|
|
|
isAdd: true,
|
|
|
|
subject: gettext('Serial Port'),
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
// for now create of (socket) serial port only
|
|
me.isCreate = true;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.SerialnputPanel', {});
|
|
|
|
Ext.apply(me, {
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
ipanel.setVMConfig(response.result.data);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.window.IPInfo', {
|
|
extend: 'Ext.window.Window',
|
|
width: 600,
|
|
title: gettext('Guest Agent Network Information'),
|
|
height: 300,
|
|
layout: {
|
|
type: 'fit'
|
|
},
|
|
modal: true,
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
emptyText: gettext('No network information'),
|
|
columns: [
|
|
{
|
|
dataIndex: 'name',
|
|
text: gettext('Name'),
|
|
flex: 3
|
|
},
|
|
{
|
|
dataIndex: 'hardware-address',
|
|
text: gettext('MAC address'),
|
|
width: 140
|
|
},
|
|
{
|
|
dataIndex: 'ip-addresses',
|
|
text: gettext('IP address'),
|
|
align: 'right',
|
|
flex: 4,
|
|
renderer: function(val) {
|
|
if (!Ext.isArray(val)) {
|
|
return '';
|
|
}
|
|
var ips = [];
|
|
val.forEach(function(ip) {
|
|
var addr = ip['ip-address'];
|
|
var pref = ip.prefix;
|
|
if (addr && pref) {
|
|
ips.push(addr + '/' + pref);
|
|
}
|
|
});
|
|
return ips.join('<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) {
|
|
var me = this.up('pveAgentIPView');
|
|
|
|
var win = Ext.create('PVE.window.IPInfo');
|
|
win.down('grid').getStore().setData(me.nics);
|
|
win.show();
|
|
},
|
|
text: gettext('More')
|
|
}
|
|
]
|
|
}
|
|
],
|
|
|
|
getDefaultIps: function(nics) {
|
|
var me = this;
|
|
var ips = [];
|
|
nics.forEach(function(nic) {
|
|
if (nic['hardware-address'] &&
|
|
nic['hardware-address'] != '00:00:00:00:00:00') {
|
|
|
|
var nic_ips = nic['ip-addresses'] || [];
|
|
nic_ips.forEach(function(ip) {
|
|
var p = ip['ip-address'];
|
|
// show 2 ips at maximum
|
|
if (ips.length < 2) {
|
|
ips.push(p);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return ips;
|
|
},
|
|
|
|
startIPStore: function(store, records, success) {
|
|
var me = this;
|
|
var agentRec = store.getById('agent');
|
|
/*jslint confusion: true*/
|
|
/* value is number and string */
|
|
me.agent = (agentRec && agentRec.data.value === 1);
|
|
me.running = (store.getById('status').data.value === 'running');
|
|
/*jslint confusion: false*/
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
if (!caps.vms['VM.Monitor']) {
|
|
var errorText = gettext("Requires '{0}' Privileges");
|
|
me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor'));
|
|
return;
|
|
}
|
|
|
|
if (me.agent && me.running && me.ipStore.isStopped) {
|
|
me.ipStore.startUpdate();
|
|
} else if (me.ipStore.isStopped) {
|
|
me.updateStatus();
|
|
}
|
|
},
|
|
|
|
updateStatus: function(unsuccessful, defaulttext) {
|
|
var me = this;
|
|
var text = defaulttext || gettext('No network information');
|
|
var more = false;
|
|
if (unsuccessful) {
|
|
text = gettext('Guest Agent not running');
|
|
} else if (me.agent && me.running) {
|
|
if (Ext.isArray(me.nics) && me.nics.length) {
|
|
more = true;
|
|
var ips = me.getDefaultIps(me.nics);
|
|
if (ips.length !== 0) {
|
|
text = ips.join('<br>');
|
|
}
|
|
} else if (me.nics && me.nics.error) {
|
|
var msg = gettext('Cannot get info from Guest Agent<br>Error: {0}');
|
|
text = Ext.String.format(text, me.nics.error.desc);
|
|
}
|
|
} else if (me.agent) {
|
|
text = gettext('Guest Agent not running');
|
|
} else {
|
|
text = gettext('No Guest Agent configured');
|
|
}
|
|
|
|
var ipBox = me.down('#ipBox');
|
|
ipBox.update(text);
|
|
|
|
var moreBtn = me.down('#moreBtn');
|
|
moreBtn.setVisible(more);
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.rstore) {
|
|
throw 'rstore not given';
|
|
}
|
|
|
|
if (!me.pveSelNode) {
|
|
throw 'pveSelNode not given';
|
|
}
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
|
|
me.ipStore = Ext.create('Proxmox.data.UpdateStore', {
|
|
interval: 10000,
|
|
storeid: 'pve-qemu-agent-' + vmid,
|
|
method: 'POST',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/agent/network-get-interfaces'
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.mon(me.ipStore, 'load', function(store, records, success) {
|
|
if (records && records.length) {
|
|
me.nics = records[0].data.result;
|
|
} else {
|
|
me.nics = undefined;
|
|
}
|
|
me.updateStatus(!success);
|
|
});
|
|
|
|
me.on('destroy', me.ipStore.stopUpdate);
|
|
|
|
// if we already have info about the vm, use it immediately
|
|
if (me.rstore.getCount()) {
|
|
me.startIPStore(me.rstore, me.rstore.getData(), false);
|
|
}
|
|
|
|
// check if the guest agent is there on every statusstore load
|
|
me.mon(me.rstore, 'load', me.startIPStore, me);
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.CloudInit', {
|
|
extend: 'Proxmox.grid.PendingObjectGrid',
|
|
xtype: 'pveCiPanel',
|
|
|
|
onlineHelp: 'qm_cloud_init',
|
|
|
|
tbar: [
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
disabled: true,
|
|
dangerous: true,
|
|
confirmMsg: function(rec) {
|
|
var me = this.up('grid');
|
|
var warn = gettext('Are you sure you want to remove entry {0}');
|
|
|
|
var entry = rec.data.key;
|
|
var msg = Ext.String.format(warn, "'"
|
|
+ me.renderKey(entry, {}, rec) + "'");
|
|
|
|
return msg;
|
|
},
|
|
enableFn: function(record) {
|
|
var me = this.up('grid');
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
if (me.rows[record.data.key].never_delete ||
|
|
!caps.vms['VM.Config.Network']) {
|
|
return false;
|
|
}
|
|
|
|
if (record.data.key === 'cipassword' && !record.data.value) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
var records = me.getSelection();
|
|
if (!records || !records.length) {
|
|
return;
|
|
}
|
|
|
|
var id = records[0].data.key;
|
|
var match = id.match(/^net(\d+)$/);
|
|
if (match) {
|
|
id = 'ipconfig' + match[1];
|
|
}
|
|
|
|
var params = {};
|
|
params['delete'] = id;
|
|
Proxmox.Utils.API2Request({
|
|
url: me.baseurl + '/config',
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
params: params,
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
},
|
|
callback: function() {
|
|
me.reload();
|
|
}
|
|
});
|
|
},
|
|
text: gettext('Remove')
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
disabled: true,
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
me.run_editor();
|
|
},
|
|
text: gettext('Edit')
|
|
},
|
|
'-',
|
|
{
|
|
xtype: 'button',
|
|
itemId: 'savebtn',
|
|
text: gettext('Regenerate Image'),
|
|
handler: function() {
|
|
var me = this.up('grid');
|
|
var eject_params = {};
|
|
var insert_params = {};
|
|
var disk = PVE.Parser.parseQemuDrive(me.ciDriveId, me.ciDrive);
|
|
var storage = '';
|
|
var stormatch = disk.file.match(/^([^\:]+)\:/);
|
|
if (stormatch) {
|
|
storage = stormatch[1];
|
|
}
|
|
eject_params[me.ciDriveId] = 'none,media=cdrom';
|
|
insert_params[me.ciDriveId] = storage + ':cloudinit';
|
|
|
|
var failure = function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
};
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: me.baseurl + '/config',
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
params: eject_params,
|
|
failure: failure,
|
|
callback: function() {
|
|
Proxmox.Utils.API2Request({
|
|
url: me.baseurl + '/config',
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
params: insert_params,
|
|
failure: failure,
|
|
callback: function() {
|
|
me.reload();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
],
|
|
|
|
border: false,
|
|
|
|
set_button_status: function(rstore, records, success) {
|
|
if (!success || records.length < 1) {
|
|
return;
|
|
}
|
|
var me = this;
|
|
var found;
|
|
records.forEach(function(record) {
|
|
if (found) {
|
|
return;
|
|
}
|
|
var id = record.data.key;
|
|
var value = record.data.value;
|
|
var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit");
|
|
if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) {
|
|
found = id;
|
|
me.ciDriveId = found;
|
|
me.ciDrive = value;
|
|
}
|
|
});
|
|
|
|
me.down('#savebtn').setDisabled(!found);
|
|
me.setDisabled(!found);
|
|
if (!found) {
|
|
me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']);
|
|
} else {
|
|
me.getView().unmask();
|
|
}
|
|
},
|
|
|
|
renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
|
|
var me = this;
|
|
var rows = me.rows;
|
|
var rowdef = rows[key] || {};
|
|
|
|
var icon = "";
|
|
if (rowdef.iconCls) {
|
|
icon = '<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;
|
|
|
|
/*jslint confusion: true*/
|
|
/* editor is string and object */
|
|
me.rows = {
|
|
ciuser: {
|
|
header: gettext('User'),
|
|
iconCls: 'fa fa-user',
|
|
never_delete: true,
|
|
defaultValue: '',
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('User'),
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
deleteEmpty: true,
|
|
emptyText: Proxmox.Utils.defaultText,
|
|
fieldLabel: gettext('User'),
|
|
name: 'ciuser'
|
|
}
|
|
]
|
|
} : undefined,
|
|
renderer: function(value) {
|
|
return value || Proxmox.Utils.defaultText;
|
|
}
|
|
},
|
|
cipassword: {
|
|
header: gettext('Password'),
|
|
iconCls: 'fa fa-unlock',
|
|
defaultValue: '',
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Password'),
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
inputType: 'password',
|
|
deleteEmpty: true,
|
|
emptyText: Proxmox.Utils.noneText,
|
|
fieldLabel: gettext('Password'),
|
|
name: 'cipassword'
|
|
}
|
|
]
|
|
} : undefined,
|
|
renderer: function(value) {
|
|
return value || Proxmox.Utils.noneText;
|
|
}
|
|
},
|
|
searchdomain: {
|
|
header: gettext('DNS domain'),
|
|
iconCls: 'fa fa-globe',
|
|
editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
|
|
never_delete: true,
|
|
defaultValue: gettext('use host settings')
|
|
},
|
|
nameserver: {
|
|
header: gettext('DNS servers'),
|
|
iconCls: 'fa fa-globe',
|
|
editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
|
|
never_delete: true,
|
|
defaultValue: gettext('use host settings')
|
|
},
|
|
sshkeys: {
|
|
header: gettext('SSH public key'),
|
|
iconCls: 'fa fa-key',
|
|
editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.SSHKeyEdit' : undefined,
|
|
never_delete: true,
|
|
renderer: function(value) {
|
|
value = decodeURIComponent(value);
|
|
var keys = value.split('\n');
|
|
var text = [];
|
|
keys.forEach(function(key) {
|
|
if (key.length) {
|
|
// First erase all quoted strings (eg. command="foo"
|
|
var v = key.replace(/"(?:\\.|[^"\\])*"/g, '');
|
|
// Now try to detect the comment:
|
|
var res = v.match(/^\s*(\S+\s+)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)\s+\S+\s+(.*?)\s*$/, '');
|
|
if (res) {
|
|
key = Ext.String.htmlEncode(res[2]);
|
|
if (res[1]) {
|
|
key += ' <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: ''
|
|
}
|
|
};
|
|
var i;
|
|
var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) {
|
|
var id = record.data.key;
|
|
var match = id.match(/^net(\d+)$/);
|
|
var val = '';
|
|
if (match) {
|
|
val = me.getObjectValue('ipconfig'+match[1], '', pending);
|
|
}
|
|
return val;
|
|
};
|
|
for (i = 0; i < 32; i++) {
|
|
// we want to show an entry for every network device
|
|
// even if it is empty
|
|
me.rows['net' + i.toString()] = {
|
|
multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()],
|
|
header: gettext('IP Config') + ' (net' + i.toString() +')',
|
|
editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined,
|
|
iconCls: 'fa fa-exchange',
|
|
renderer: ipconfig_renderer
|
|
};
|
|
me.rows['ipconfig' + i.toString()] = {
|
|
visible: false
|
|
};
|
|
}
|
|
/*jslint confusion: false*/
|
|
|
|
PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) {
|
|
me.rows[type+id] = {
|
|
visible: false
|
|
};
|
|
});
|
|
me.callParent();
|
|
me.mon(me.rstore, 'load', me.set_button_status, me);
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.CIDriveInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveCIDriveInputPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
vmconfig: {}, // used to select usused disks
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var drive = {};
|
|
var params = {};
|
|
drive.file = values.hdstorage + ":cloudinit";
|
|
drive.format = values.diskformat;
|
|
params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive);
|
|
return params;
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
me.down('#hdstorage').setNodename(nodename);
|
|
me.down('#hdimage').setStorage(undefined, nodename);
|
|
},
|
|
|
|
setVMConfig: function(config) {
|
|
var me = this;
|
|
me.down('#drive').setVMConfig(config, 'cdrom');
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.drive = {};
|
|
|
|
me.items = [
|
|
{
|
|
xtype: 'pveControllerSelector',
|
|
noVirtIO: true,
|
|
itemId: 'drive',
|
|
fieldLabel: gettext('CloudInit Drive'),
|
|
name: 'drive'
|
|
},
|
|
{
|
|
xtype: 'pveDiskStorageSelector',
|
|
itemId: 'storselector',
|
|
storageContent: 'images',
|
|
nodename: me.nodename,
|
|
hideSize: true
|
|
}
|
|
];
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.CIDriveEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveCIDriveEdit',
|
|
|
|
isCreate: true,
|
|
subject: gettext('CloudInit Drive'),
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.items = [{
|
|
xtype: 'pveCIDriveInputPanel',
|
|
itemId: 'cipanel',
|
|
nodename: nodename
|
|
}];
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, opts) {
|
|
me.down('#cipanel').setVMConfig(response.result.data);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.SSHKeyInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveQemuSSHKeyInputPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
if (values.sshkeys) {
|
|
values.sshkeys.trim();
|
|
}
|
|
if (!values.sshkeys.length) {
|
|
values = {};
|
|
values['delete'] = 'sshkeys';
|
|
return values;
|
|
} else {
|
|
values.sshkeys = encodeURIComponent(values.sshkeys);
|
|
}
|
|
return values;
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'textarea',
|
|
itemId: 'sshkeys',
|
|
name: 'sshkeys',
|
|
height: 250
|
|
},
|
|
{
|
|
xtype: 'filebutton',
|
|
itemId: 'filebutton',
|
|
name: 'file',
|
|
text: gettext('Load SSH Key File'),
|
|
fieldLabel: 'test',
|
|
listeners: {
|
|
change: function(btn, e, value) {
|
|
var me = this.up('inputpanel');
|
|
e = e.event;
|
|
Ext.Array.each(e.target.files, function(file) {
|
|
PVE.Utils.loadSSHKeyFromFile(file, function(res) {
|
|
var keysField = me.down('#sshkeys');
|
|
var old = keysField.getValue();
|
|
keysField.setValue(old + res);
|
|
});
|
|
});
|
|
btn.reset();
|
|
}
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.callParent();
|
|
if (!window.FileReader) {
|
|
me.down('#filebutton').setVisible(false);
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.SSHKeyEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
width: 800,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel');
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('SSH Keys'),
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.create) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var data = response.result.data;
|
|
if (data.sshkeys) {
|
|
data.sshkeys = decodeURIComponent(data.sshkeys);
|
|
ipanel.setValues(data);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.qemu.IPConfigPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveIPConfigPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
vmconfig: {},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
if (values.ipv4mode !== 'static') {
|
|
values.ip = values.ipv4mode;
|
|
}
|
|
|
|
if (values.ipv6mode !== 'static') {
|
|
values.ip6 = values.ipv6mode;
|
|
}
|
|
|
|
var params = {};
|
|
|
|
var cfg = PVE.Parser.printIPConfig(values);
|
|
if (cfg === '') {
|
|
params['delete'] = [me.confid];
|
|
} else {
|
|
params[me.confid] = cfg;
|
|
}
|
|
return params;
|
|
},
|
|
|
|
setVMConfig: function(config) {
|
|
var me = this;
|
|
me.vmconfig = config;
|
|
},
|
|
|
|
setIPConfig: function(confid, data) {
|
|
var me = this;
|
|
|
|
me.confid = confid;
|
|
|
|
if (data.ip === 'dhcp') {
|
|
data.ipv4mode = data.ip;
|
|
data.ip = '';
|
|
} else {
|
|
data.ipv4mode = 'static';
|
|
}
|
|
if (data.ip6 === 'dhcp' || data.ip6 === 'auto') {
|
|
data.ipv6mode = data.ip6;
|
|
data.ip6 = '';
|
|
} else {
|
|
data.ipv6mode = 'static';
|
|
}
|
|
|
|
me.ipconfig = data;
|
|
me.setValues(me.ipconfig);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.ipconfig = {};
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Network Device'),
|
|
value: me.netid
|
|
},
|
|
{
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'middle'
|
|
},
|
|
border: false,
|
|
margin: '0 0 5 0',
|
|
items: [
|
|
{
|
|
xtype: 'label',
|
|
text: gettext('IPv4') + ':'
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: gettext('Static'),
|
|
name: 'ipv4mode',
|
|
inputValue: 'static',
|
|
checked: false,
|
|
margin: '0 0 0 10',
|
|
listeners: {
|
|
change: function(cb, value) {
|
|
me.down('field[name=ip]').setDisabled(!value);
|
|
me.down('field[name=gw]').setDisabled(!value);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: gettext('DHCP'),
|
|
name: 'ipv4mode',
|
|
inputValue: 'dhcp',
|
|
checked: false,
|
|
margin: '0 0 0 10'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'ip',
|
|
vtype: 'IPCIDRAddress',
|
|
value: '',
|
|
disabled: true,
|
|
fieldLabel: gettext('IPv4/CIDR')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'gw',
|
|
value: '',
|
|
vtype: 'IPAddress',
|
|
disabled: true,
|
|
fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')'
|
|
}
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'displayfield'
|
|
},
|
|
{
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'middle'
|
|
},
|
|
border: false,
|
|
margin: '0 0 5 0',
|
|
items: [
|
|
{
|
|
xtype: 'label',
|
|
text: gettext('IPv6') + ':'
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: gettext('Static'),
|
|
name: 'ipv6mode',
|
|
inputValue: 'static',
|
|
checked: false,
|
|
margin: '0 0 0 10',
|
|
listeners: {
|
|
change: function(cb, value) {
|
|
me.down('field[name=ip6]').setDisabled(!value);
|
|
me.down('field[name=gw6]').setDisabled(!value);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: gettext('DHCP'),
|
|
name: 'ipv6mode',
|
|
inputValue: 'dhcp',
|
|
checked: false,
|
|
margin: '0 0 0 10'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'ip6',
|
|
value: '',
|
|
vtype: 'IP6CIDRAddress',
|
|
disabled: true,
|
|
fieldLabel: gettext('IPv6/CIDR')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'gw6',
|
|
vtype: 'IP6Address',
|
|
value: '',
|
|
disabled: true,
|
|
fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')'
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.qemu.IPConfigEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
isAdd: true,
|
|
|
|
initComponent : function() {
|
|
/*jslint confusion: true */
|
|
|
|
var me = this;
|
|
|
|
// convert confid from netX to ipconfigX
|
|
var match = me.confid.match(/^net(\d+)$/);
|
|
if (match) {
|
|
me.netid = me.confid;
|
|
me.confid = 'ipconfig' + match[1];
|
|
}
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.isCreate = me.confid ? false : true;
|
|
|
|
var ipanel = Ext.create('PVE.qemu.IPConfigPanel', {
|
|
confid: me.confid,
|
|
netid: me.netid,
|
|
nodename: nodename
|
|
});
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('Network Config'),
|
|
items: ipanel
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
me.vmconfig = response.result.data;
|
|
var ipconfig = {};
|
|
var value = me.vmconfig[me.confid];
|
|
if (value) {
|
|
ipconfig = PVE.Parser.parseIPConfig(me.confid, value);
|
|
if (!ipconfig) {
|
|
Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration'));
|
|
me.close();
|
|
return;
|
|
}
|
|
}
|
|
ipanel.setIPConfig(me.confid, ipconfig);
|
|
ipanel.setVMConfig(me.vmconfig);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.qemu.SystemInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveQemuSystemPanel',
|
|
|
|
onlineHelp: 'qm_system_settings',
|
|
|
|
viewModel: {
|
|
data: {
|
|
efi: false,
|
|
addefi: true
|
|
},
|
|
|
|
formulas: {
|
|
efidisk: function(get) {
|
|
return get('efi') && get('addefi');
|
|
}
|
|
}
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
if (values.vga && values.vga.substr(0,6) === 'serial') {
|
|
values['serial' + values.vga.substr(6,1)] = 'socket';
|
|
}
|
|
|
|
var efidrive = {};
|
|
if (values.hdimage) {
|
|
efidrive.file = values.hdimage;
|
|
} else if (values.hdstorage) {
|
|
efidrive.file = values.hdstorage + ":1";
|
|
}
|
|
|
|
if (values.diskformat) {
|
|
efidrive.format = values.diskformat;
|
|
}
|
|
|
|
delete values.hdimage;
|
|
delete values.hdstorage;
|
|
delete values.diskformat;
|
|
|
|
if (efidrive.file) {
|
|
values.efidisk0 = PVE.Parser.printQemuDrive(efidrive);
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
scsihwChange: function(field, value) {
|
|
var me = this;
|
|
if (me.getView().insideWizard) {
|
|
me.getViewModel().set('current.scsihw', value);
|
|
}
|
|
},
|
|
|
|
biosChange: function(field, value) {
|
|
var me = this;
|
|
if (me.getView().insideWizard) {
|
|
me.getViewModel().set('efi', value === 'ovmf');
|
|
}
|
|
},
|
|
|
|
control: {
|
|
'pveScsiHwSelector': {
|
|
change: 'scsihwChange'
|
|
},
|
|
'pveQemuBiosSelector': {
|
|
change: 'biosChange'
|
|
}
|
|
}
|
|
},
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
value: '__default__',
|
|
deleteEmpty: false,
|
|
fieldLabel: gettext('Graphic card'),
|
|
name: 'vga',
|
|
comboItems: PVE.Utils.kvm_vga_driver_array()
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'agent',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
deleteDefaultValue: true,
|
|
fieldLabel: gettext('Qemu Agent')
|
|
}
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'pveScsiHwSelector',
|
|
name: 'scsihw',
|
|
value: '__default__',
|
|
bind: {
|
|
value: '{current.scsihw}'
|
|
},
|
|
fieldLabel: gettext('SCSI Controller')
|
|
}
|
|
],
|
|
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'pveQemuBiosSelector',
|
|
name: 'bios',
|
|
value: '__default__',
|
|
fieldLabel: 'BIOS'
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
bind: {
|
|
value: '{addefi}',
|
|
hidden: '{!efi}',
|
|
disabled: '{!efi}'
|
|
},
|
|
hidden: true,
|
|
submitValue: false,
|
|
disabled: true,
|
|
fieldLabel: gettext('Add EFI Disk')
|
|
},
|
|
{
|
|
xtype: 'pveDiskStorageSelector',
|
|
name: 'efidisk0',
|
|
storageContent: 'images',
|
|
bind: {
|
|
nodename: '{nodename}',
|
|
hidden: '{!efi}',
|
|
disabled: '{!efidisk}'
|
|
},
|
|
autoSelect: false,
|
|
disabled: true,
|
|
hidden: true,
|
|
hideSize: true
|
|
}
|
|
],
|
|
|
|
advancedColumn2: [
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'machine',
|
|
value: '__default__',
|
|
fieldLabel: gettext('Machine'),
|
|
comboItems: [
|
|
['__default__', PVE.Utils.render_qemu_machine('')],
|
|
['q35', 'q35']
|
|
]
|
|
}
|
|
]
|
|
|
|
});
|
|
Ext.define('PVE.lxc.Summary', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveLxcSummary',
|
|
|
|
scrollable: true,
|
|
bodyPadding: 5,
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
if (!me.workspace) {
|
|
throw "no workspace specified";
|
|
}
|
|
|
|
if (!me.statusStore) {
|
|
throw "no status storage specified";
|
|
}
|
|
|
|
var template = !!me.pveSelNode.data.template;
|
|
var rstore = me.statusStore;
|
|
|
|
var width = template ? 1 : 0.5;
|
|
var items = [
|
|
{
|
|
xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView',
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: width
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: width / 2
|
|
}
|
|
},
|
|
itemId: 'gueststatus',
|
|
pveSelNode: me.pveSelNode,
|
|
rstore: rstore
|
|
},
|
|
{
|
|
xtype: 'pveNotesView',
|
|
maxHeight: 320,
|
|
itemId: 'notesview',
|
|
pveSelNode: me.pveSelNode,
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: width
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: width / 2
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
var rrdstore;
|
|
if (!template) {
|
|
|
|
rrdstore = Ext.create('Proxmox.data.RRDStore', {
|
|
rrdurl: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/rrddata",
|
|
model: 'pve-rrd-guest'
|
|
});
|
|
|
|
items.push(
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('CPU usage'),
|
|
pveSelNode: me.pveSelNode,
|
|
fields: ['cpu'],
|
|
fieldTitles: [gettext('CPU usage')],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Memory usage'),
|
|
pveSelNode: me.pveSelNode,
|
|
fields: ['maxmem', 'mem'],
|
|
fieldTitles: [gettext('Total'), gettext('RAM usage')],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Network traffic'),
|
|
pveSelNode: me.pveSelNode,
|
|
fields: ['netin','netout'],
|
|
store: rrdstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Disk IO'),
|
|
pveSelNode: me.pveSelNode,
|
|
fields: ['diskread','diskwrite'],
|
|
store: rrdstore
|
|
}
|
|
);
|
|
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ],
|
|
items: [
|
|
{
|
|
xtype: 'container',
|
|
layout: {
|
|
type: 'column'
|
|
},
|
|
defaults: {
|
|
minHeight: 320,
|
|
padding: 5,
|
|
plugins: 'responsive',
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: 1
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: 0.5
|
|
}
|
|
}
|
|
},
|
|
items: items
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
if (!template) {
|
|
rrdstore.startUpdate();
|
|
me.on('destroy', rrdstore.stopUpdate);
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.lxc.NetworkInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveLxcNetworkInputPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
onlineHelp: 'pct_container_network',
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
|
|
if (!nodename || (me.nodename === nodename)) {
|
|
return;
|
|
}
|
|
|
|
me.nodename = nodename;
|
|
|
|
var bridgesel = me.query("[isFormField][name=bridge]")[0];
|
|
bridgesel.setNodename(nodename);
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var id;
|
|
if (me.isCreate) {
|
|
id = values.id;
|
|
delete values.id;
|
|
} else {
|
|
id = me.ifname;
|
|
}
|
|
|
|
if (!id) {
|
|
return {};
|
|
}
|
|
|
|
var newdata = {};
|
|
|
|
if (values.ipv6mode !== 'static') {
|
|
values.ip6 = values.ipv6mode;
|
|
}
|
|
if (values.ipv4mode !== 'static') {
|
|
values.ip = values.ipv4mode;
|
|
}
|
|
newdata[id] = PVE.Parser.printLxcNetwork(values);
|
|
return newdata;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var cdata = {};
|
|
|
|
if (me.insideWizard) {
|
|
me.ifname = 'net0';
|
|
cdata.name = 'eth0';
|
|
me.dataCache = {};
|
|
}
|
|
cdata.firewall = (me.insideWizard || me.isCreate);
|
|
|
|
if (!me.dataCache) {
|
|
throw "no dataCache specified";
|
|
}
|
|
|
|
if (!me.isCreate) {
|
|
if (!me.ifname) {
|
|
throw "no interface name specified";
|
|
}
|
|
if (!me.dataCache[me.ifname]) {
|
|
throw "no such interface '" + me.ifname + "'";
|
|
}
|
|
|
|
cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]);
|
|
}
|
|
|
|
var i;
|
|
for (i = 0; i < 10; i++) {
|
|
if (me.isCreate && !me.dataCache['net'+i.toString()]) {
|
|
me.ifname = 'net' + i.toString();
|
|
break;
|
|
}
|
|
}
|
|
|
|
var idselector = {
|
|
xtype: 'hidden',
|
|
name: 'id',
|
|
value: me.ifname
|
|
};
|
|
|
|
me.column1 = [
|
|
idselector,
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'name',
|
|
fieldLabel: gettext('Name'),
|
|
emptyText: '(e.g., eth0)',
|
|
allowBlank: false,
|
|
value: cdata.name,
|
|
validator: function(value) {
|
|
var result = '';
|
|
Ext.Object.each(me.dataCache, function(key, netstr) {
|
|
if (!key.match(/^net\d+/) || key === me.ifname) {
|
|
return; // continue
|
|
}
|
|
var net = PVE.Parser.parseLxcNetwork(netstr);
|
|
if (net.name === value) {
|
|
result = "interface name already in use";
|
|
return false;
|
|
}
|
|
});
|
|
if (result !== '') {
|
|
return result;
|
|
}
|
|
// validator can return bool/string
|
|
/*jslint confusion:true*/
|
|
return true;
|
|
}
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'hwaddr',
|
|
fieldLabel: gettext('MAC address'),
|
|
vtype: 'MacAddress',
|
|
value: cdata.hwaddr,
|
|
allowBlank: true,
|
|
emptyText: 'auto'
|
|
},
|
|
{
|
|
xtype: 'PVE.form.BridgeSelector',
|
|
name: 'bridge',
|
|
nodename: me.nodename,
|
|
fieldLabel: gettext('Bridge'),
|
|
value: cdata.bridge,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'pveVlanField',
|
|
name: 'tag',
|
|
value: cdata.tag
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'rate',
|
|
fieldLabel: gettext('Rate limit') + ' (MB/s)',
|
|
minValue: 0,
|
|
maxValue: 10*1024,
|
|
value: cdata.rate,
|
|
emptyText: 'unlimited',
|
|
allowBlank: true
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Firewall'),
|
|
name: 'firewall',
|
|
value: cdata.firewall
|
|
}
|
|
];
|
|
|
|
var dhcp4 = (cdata.ip === 'dhcp');
|
|
if (dhcp4) {
|
|
cdata.ip = '';
|
|
cdata.gw = '';
|
|
}
|
|
|
|
var auto6 = (cdata.ip6 === 'auto');
|
|
var dhcp6 = (cdata.ip6 === 'dhcp');
|
|
if (auto6 || dhcp6) {
|
|
cdata.ip6 = '';
|
|
cdata.gw6 = '';
|
|
}
|
|
|
|
me.column2 = [
|
|
{
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'middle'
|
|
},
|
|
border: false,
|
|
margin: '0 0 5 0',
|
|
items: [
|
|
{
|
|
xtype: 'label',
|
|
text: 'IPv4:' // do not localize
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: gettext('Static'),
|
|
name: 'ipv4mode',
|
|
inputValue: 'static',
|
|
checked: !dhcp4,
|
|
margin: '0 0 0 10',
|
|
listeners: {
|
|
change: function(cb, value) {
|
|
me.down('field[name=ip]').setDisabled(!value);
|
|
me.down('field[name=gw]').setDisabled(!value);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: 'DHCP', // do not localize
|
|
name: 'ipv4mode',
|
|
inputValue: 'dhcp',
|
|
checked: dhcp4,
|
|
margin: '0 0 0 10'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'ip',
|
|
vtype: 'IPCIDRAddress',
|
|
value: cdata.ip,
|
|
disabled: dhcp4,
|
|
fieldLabel: 'IPv4/CIDR' // do not localize
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'gw',
|
|
value: cdata.gw,
|
|
vtype: 'IPAddress',
|
|
disabled: dhcp4,
|
|
fieldLabel: gettext('Gateway') + ' (IPv4)',
|
|
margin: '0 0 3 0' // override bottom margin to account for the menuseparator
|
|
},
|
|
{
|
|
xtype: 'menuseparator',
|
|
height: '3',
|
|
margin: '0'
|
|
},
|
|
{
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'middle'
|
|
},
|
|
border: false,
|
|
margin: '0 0 5 0',
|
|
items: [
|
|
{
|
|
xtype: 'label',
|
|
text: 'IPv6:' // do not localize
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: gettext('Static'),
|
|
name: 'ipv6mode',
|
|
inputValue: 'static',
|
|
checked: !(auto6 || dhcp6),
|
|
margin: '0 0 0 10',
|
|
listeners: {
|
|
change: function(cb, value) {
|
|
me.down('field[name=ip6]').setDisabled(!value);
|
|
me.down('field[name=gw6]').setDisabled(!value);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: 'DHCP', // do not localize
|
|
name: 'ipv6mode',
|
|
inputValue: 'dhcp',
|
|
checked: dhcp6,
|
|
margin: '0 0 0 10'
|
|
},
|
|
{
|
|
xtype: 'radiofield',
|
|
boxLabel: 'SLAAC', // do not localize
|
|
name: 'ipv6mode',
|
|
inputValue: 'auto',
|
|
checked: auto6,
|
|
margin: '0 0 0 10'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'ip6',
|
|
value: cdata.ip6,
|
|
vtype: 'IP6CIDRAddress',
|
|
disabled: (dhcp6 || auto6),
|
|
fieldLabel: 'IPv6/CIDR' // do not localize
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'gw6',
|
|
vtype: 'IP6Address',
|
|
value: cdata.gw6,
|
|
disabled: (dhcp6 || auto6),
|
|
fieldLabel: gettext('Gateway') + ' (IPv6)'
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
|
|
Ext.define('PVE.lxc.NetworkEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
isAdd: true,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.dataCache) {
|
|
throw "no dataCache specified";
|
|
}
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var ipanel = Ext.create('PVE.lxc.NetworkInputPanel', {
|
|
ifname: me.ifname,
|
|
nodename: me.nodename,
|
|
dataCache: me.dataCache,
|
|
isCreate: me.isCreate
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Network Device') + ' (veth)',
|
|
digest: me.dataCache.digest,
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.lxc.NetworkView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: 'widget.pveLxcNetworkView',
|
|
|
|
onlineHelp: 'pct_container_network',
|
|
|
|
dataCache: {}, // used to store result of last load
|
|
|
|
stateful: true,
|
|
stateId: 'grid-lxc-network',
|
|
|
|
load: function() {
|
|
var me = this;
|
|
|
|
Proxmox.Utils.setErrorMask(me, true);
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: me.url,
|
|
failure: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(me, false);
|
|
var result = Ext.decode(response.responseText);
|
|
var data = result.data || {};
|
|
me.dataCache = data;
|
|
var records = [];
|
|
Ext.Object.each(data, function(key, value) {
|
|
if (!key.match(/^net\d+/)) {
|
|
return; // continue
|
|
}
|
|
var net = PVE.Parser.parseLxcNetwork(value);
|
|
net.id = key;
|
|
records.push(net);
|
|
});
|
|
me.store.loadData(records);
|
|
me.down('button[name=addButton]').setDisabled((records.length >= 10));
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
me.url = '/nodes/' + nodename + '/lxc/' + vmid + '/config';
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-lxc-network',
|
|
sorters: [
|
|
{
|
|
property : 'id',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
});
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var remove_btn = new Proxmox.button.Button({
|
|
text: gettext('Remove'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
return !!caps.vms['VM.Config.Network'];
|
|
},
|
|
confirmMsg: function (rec) {
|
|
return Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
|
|
"'" + rec.data.id + "'");
|
|
},
|
|
handler: function(btn, event, rec) {
|
|
Proxmox.Utils.API2Request({
|
|
url: me.url,
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
params: { 'delete': rec.data.id, digest: me.dataCache.digest },
|
|
callback: function() {
|
|
me.load();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
if (!caps.vms['VM.Config.Network']) {
|
|
return false;
|
|
}
|
|
|
|
var win = Ext.create('PVE.lxc.NetworkEdit', {
|
|
url: me.url,
|
|
nodename: nodename,
|
|
dataCache: me.dataCache,
|
|
ifname: rec.data.id
|
|
});
|
|
win.on('destroy', me.load, me);
|
|
win.show();
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
selModel: sm,
|
|
disabled: true,
|
|
enableFn: function(rec) {
|
|
if (!caps.vms['VM.Config.Network']) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
handler: run_editor
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
name: 'addButton',
|
|
disabled: !caps.vms['VM.Config.Network'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.lxc.NetworkEdit', {
|
|
url: me.url,
|
|
nodename: nodename,
|
|
isCreate: true,
|
|
dataCache: me.dataCache
|
|
});
|
|
win.on('destroy', me.load, me);
|
|
win.show();
|
|
}
|
|
},
|
|
remove_btn,
|
|
edit_btn
|
|
],
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
width: 50,
|
|
dataIndex: 'id'
|
|
},
|
|
{
|
|
header: gettext('Name'),
|
|
width: 80,
|
|
dataIndex: 'name'
|
|
},
|
|
{
|
|
header: gettext('Bridge'),
|
|
width: 80,
|
|
dataIndex: 'bridge'
|
|
},
|
|
{
|
|
header: gettext('Firewall'),
|
|
width: 80,
|
|
dataIndex: 'firewall',
|
|
renderer: Proxmox.Utils.format_boolean
|
|
},
|
|
{
|
|
header: gettext('VLAN Tag'),
|
|
width: 80,
|
|
dataIndex: 'tag'
|
|
},
|
|
{
|
|
header: gettext('MAC address'),
|
|
width: 110,
|
|
dataIndex: 'hwaddr'
|
|
},
|
|
{
|
|
header: gettext('IP address'),
|
|
width: 150,
|
|
dataIndex: 'ip',
|
|
renderer: function(value, metaData, rec) {
|
|
if (rec.data.ip && rec.data.ip6) {
|
|
return rec.data.ip + "<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;
|
|
}
|
|
}
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: me.load,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-lxc-network', {
|
|
extend: "Ext.data.Model",
|
|
proxy: { type: 'memory' },
|
|
fields: [ 'id', 'name', 'hwaddr', 'bridge',
|
|
'ip', 'gw', 'ip6', 'gw6', 'tag', 'firewall' ]
|
|
});
|
|
|
|
});
|
|
|
|
/*jslint confusion: true */
|
|
Ext.define('PVE.lxc.RessourceView', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
alias: ['widget.pveLxcRessourceView'],
|
|
|
|
onlineHelp: 'pct_configuration',
|
|
|
|
renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
|
|
var me = this;
|
|
var rowdef = me.rows[key] || {};
|
|
|
|
metaData.tdAttr = "valign=middle";
|
|
if (rowdef.tdCls) {
|
|
metaData.tdCls = rowdef.tdCls;
|
|
}
|
|
return rowdef.header || key;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
var i, confid;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
var diskCap = caps.vms['VM.Config.Disk'];
|
|
|
|
var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined;
|
|
|
|
var rows = {
|
|
memory: {
|
|
header: gettext('Memory'),
|
|
editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
|
|
defaultValue: 512,
|
|
tdCls: 'pve-itype-icon-memory',
|
|
group: 1,
|
|
renderer: function(value) {
|
|
return Proxmox.Utils.format_size(value*1024*1024);
|
|
}
|
|
},
|
|
swap: {
|
|
header: gettext('Swap'),
|
|
editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined,
|
|
defaultValue: 512,
|
|
tdCls: 'pve-itype-icon-swap',
|
|
group: 2,
|
|
renderer: function(value) {
|
|
return Proxmox.Utils.format_size(value*1024*1024);
|
|
}
|
|
},
|
|
cores: {
|
|
header: gettext('Cores'),
|
|
editor: caps.vms['VM.Config.CPU'] ? 'PVE.lxc.CPUEdit' : undefined,
|
|
defaultValue: '',
|
|
tdCls: 'pve-itype-icon-processor',
|
|
group: 3,
|
|
renderer: function(value) {
|
|
var cpulimit = me.getObjectValue('cpulimit');
|
|
var cpuunits = me.getObjectValue('cpuunits');
|
|
var res;
|
|
if (value) {
|
|
res = value;
|
|
} else {
|
|
res = gettext('unlimited');
|
|
}
|
|
|
|
if (cpulimit) {
|
|
res += ' [cpulimit=' + cpulimit + ']';
|
|
}
|
|
|
|
if (cpuunits) {
|
|
res += ' [cpuunits=' + cpuunits + ']';
|
|
}
|
|
return res;
|
|
}
|
|
},
|
|
rootfs: {
|
|
header: gettext('Root Disk'),
|
|
defaultValue: Proxmox.Utils.noneText,
|
|
editor: mpeditor,
|
|
tdCls: 'pve-itype-icon-storage',
|
|
group: 4
|
|
},
|
|
cpulimit: {
|
|
visible: false
|
|
},
|
|
cpuunits: {
|
|
visible: false
|
|
},
|
|
unprivileged: {
|
|
visible: false
|
|
}
|
|
};
|
|
|
|
PVE.Utils.forEachMP(function(bus, i) {
|
|
confid = bus + i;
|
|
var group = 5;
|
|
var header;
|
|
if (bus === 'mp') {
|
|
header = gettext('Mount Point') + ' (' + confid + ')';
|
|
} else {
|
|
header = gettext('Unused Disk') + ' ' + i;
|
|
group += 1;
|
|
}
|
|
rows[confid] = {
|
|
group: group,
|
|
order: i,
|
|
tdCls: 'pve-itype-icon-storage',
|
|
editor: mpeditor,
|
|
header: header
|
|
};
|
|
}, true);
|
|
|
|
var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
|
|
|
|
me.selModel = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_resize = function() {
|
|
var rec = me.selModel.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.window.MPResize', {
|
|
disk: rec.data.key,
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
|
|
win.show();
|
|
};
|
|
|
|
var run_remove = function(b, e, rec) {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/' + baseurl,
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
params: {
|
|
'delete': rec.data.key
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
var run_move = function(b, e, rec) {
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.window.HDMove', {
|
|
disk: rec.data.key,
|
|
nodename: nodename,
|
|
vmid: vmid,
|
|
type: 'lxc'
|
|
});
|
|
|
|
win.show();
|
|
|
|
win.on('destroy', me.reload, me);
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
selModel: me.selModel,
|
|
disabled: true,
|
|
enableFn: function(rec) {
|
|
if (!rec) {
|
|
return false;
|
|
}
|
|
var rowdef = rows[rec.data.key];
|
|
return !!rowdef.editor;
|
|
},
|
|
handler: function() { me.run_editor(); }
|
|
});
|
|
|
|
var resize_btn = new Proxmox.button.Button({
|
|
text: gettext('Resize disk'),
|
|
selModel: me.selModel,
|
|
disabled: true,
|
|
handler: run_resize
|
|
});
|
|
|
|
var remove_btn = new Proxmox.button.Button({
|
|
text: gettext('Remove'),
|
|
selModel: me.selModel,
|
|
disabled: true,
|
|
dangerous: true,
|
|
confirmMsg: function(rec) {
|
|
var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
|
|
"'" + me.renderKey(rec.data.key, {}, rec) + "'");
|
|
if (rec.data.key.match(/^unused\d+$/)) {
|
|
msg += " " + gettext('This will permanently erase all data.');
|
|
}
|
|
|
|
return msg;
|
|
},
|
|
handler: run_remove
|
|
});
|
|
|
|
var move_btn = new Proxmox.button.Button({
|
|
text: gettext('Move Volume'),
|
|
selModel: me.selModel,
|
|
disabled: true,
|
|
dangerous: true,
|
|
handler: run_move
|
|
});
|
|
|
|
var set_button_status = function() {
|
|
var rec = me.selModel.getSelection()[0];
|
|
|
|
if (!rec) {
|
|
edit_btn.disable();
|
|
remove_btn.disable();
|
|
resize_btn.disable();
|
|
return;
|
|
}
|
|
var key = rec.data.key;
|
|
var value = rec.data.value;
|
|
var rowdef = rows[key];
|
|
|
|
var isDisk = (rowdef.tdCls == 'pve-itype-icon-storage');
|
|
|
|
var noedit = rec.data['delete'] || !rowdef.editor;
|
|
if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) {
|
|
var mp = PVE.Parser.parseLxcMountPoint(value);
|
|
if (mp.type !== 'volume') {
|
|
noedit = true;
|
|
}
|
|
}
|
|
edit_btn.setDisabled(noedit);
|
|
|
|
remove_btn.setDisabled(!isDisk || rec.data.key === 'rootfs' || !diskCap);
|
|
resize_btn.setDisabled(!isDisk || !diskCap);
|
|
move_btn.setDisabled(!isDisk || !diskCap);
|
|
|
|
};
|
|
|
|
var sorterFn = function(rec1, rec2) {
|
|
var v1 = rec1.data.key;
|
|
var v2 = rec2.data.key;
|
|
var g1 = rows[v1].group || 0;
|
|
var g2 = rows[v2].group || 0;
|
|
var order1 = rows[v1].order || 0;
|
|
var order2 = rows[v2].order || 0;
|
|
|
|
if ((g1 - g2) !== 0) {
|
|
return g1 - g2;
|
|
}
|
|
|
|
if ((order1 - order2) !== 0) {
|
|
return order1 - order2;
|
|
}
|
|
|
|
if (v1 > v2) {
|
|
return 1;
|
|
} else if (v1 < v2) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: '/api2/json/' + baseurl,
|
|
selModel: me.selModel,
|
|
interval: 2000,
|
|
cwidth1: 170,
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
menu: new Ext.menu.Menu({
|
|
items: [
|
|
{
|
|
text: gettext('Mount Point'),
|
|
iconCls: 'pve-itype-icon-storage',
|
|
disabled: !caps.vms['VM.Config.Disk'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.lxc.MountPointEdit', {
|
|
url: '/api2/extjs/' + baseurl,
|
|
unprivileged: me.getObjectValue('unprivileged'),
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.show();
|
|
}
|
|
}
|
|
]
|
|
})
|
|
},
|
|
edit_btn,
|
|
remove_btn,
|
|
resize_btn,
|
|
move_btn
|
|
],
|
|
rows: rows,
|
|
sorterFn: sorterFn,
|
|
editorConfig: {
|
|
pveSelNode: me.pveSelNode,
|
|
url: '/api2/extjs/' + baseurl
|
|
},
|
|
listeners: {
|
|
itemdblclick: me.run_editor,
|
|
selectionchange: set_button_status
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.on('activate', me.rstore.startUpdate);
|
|
me.on('destroy', me.rstore.stopUpdate);
|
|
me.on('deactivate', me.rstore.stopUpdate);
|
|
|
|
Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') });
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.lxc.FeaturesInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveLxcFeaturesInputPanel',
|
|
|
|
// used to save the mounts fstypes until sending
|
|
mounts: [],
|
|
|
|
fstypes: ['nfs', 'cifs'],
|
|
|
|
viewModel: {
|
|
parent: null,
|
|
data: {
|
|
unprivileged: false
|
|
},
|
|
formulas: {
|
|
privilegedOnly: function(get) {
|
|
return (get('unprivileged') ? gettext('privileged only') : '');
|
|
},
|
|
unprivilegedOnly: function(get) {
|
|
return (!get('unprivileged') ? gettext('unprivileged only') : '');
|
|
}
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('keyctl'),
|
|
name: 'keyctl',
|
|
bind: {
|
|
disabled: '{!unprivileged}',
|
|
boxLabel: '{unprivilegedOnly}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Nesting'),
|
|
name: 'nesting'
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'nfs',
|
|
fieldLabel: 'NFS',
|
|
bind: {
|
|
disabled: '{unprivileged}',
|
|
boxLabel: '{privilegedOnly}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'cifs',
|
|
fieldLabel: 'CIFS',
|
|
bind: {
|
|
disabled: '{unprivileged}',
|
|
boxLabel: '{privilegedOnly}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'fuse',
|
|
fieldLabel: 'FUSE'
|
|
}
|
|
],
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
var mounts = me.mounts;
|
|
me.fstypes.forEach(function(fs) {
|
|
if (values[fs]) {
|
|
mounts.push(fs);
|
|
}
|
|
delete values[fs];
|
|
});
|
|
|
|
if (mounts.length) {
|
|
values.mount = mounts.join(';');
|
|
}
|
|
|
|
var featuresstring = PVE.Parser.printPropertyString(values, undefined);
|
|
if (featuresstring == '') {
|
|
return { 'delete': 'features' };
|
|
}
|
|
return { features: featuresstring };
|
|
},
|
|
|
|
setValues: function(values) {
|
|
var me = this;
|
|
|
|
me.viewModel.set({ unprivileged: values.unprivileged });
|
|
|
|
if (values.features) {
|
|
var res = PVE.Parser.parsePropertyString(values.features);
|
|
me.mounts = [];
|
|
if (res.mount) {
|
|
res.mount.split(/[; ]/).forEach(function(item) {
|
|
if (me.fstypes.indexOf(item) === -1) {
|
|
me.mounts.push(item);
|
|
} else {
|
|
res[item] = 1;
|
|
}
|
|
});
|
|
}
|
|
this.callParent([res]);
|
|
}
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.lxc.FeaturesEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveLxcFeaturesEdit',
|
|
|
|
subject: gettext('Features'),
|
|
|
|
items: [{
|
|
xtype: 'pveLxcFeaturesInputPanel'
|
|
}],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
/*jslint confusion: true */
|
|
Ext.define('PVE.lxc.Options', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
alias: ['widget.pveLxcOptions'],
|
|
|
|
onlineHelp: 'pct_options',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
var i;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var rows = {
|
|
onboot: {
|
|
header: gettext('Start at boot'),
|
|
defaultValue: '',
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Start at boot'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'onboot',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
fieldLabel: gettext('Start at boot')
|
|
}
|
|
} : undefined
|
|
},
|
|
startup: {
|
|
header: gettext('Start/Shutdown order'),
|
|
defaultValue: '',
|
|
renderer: PVE.Utils.render_kvm_startup,
|
|
editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ?
|
|
{
|
|
xtype: 'pveWindowStartupEdit',
|
|
onlineHelp: 'pct_startup_and_shutdown'
|
|
} : undefined
|
|
},
|
|
ostype: {
|
|
header: gettext('OS Type'),
|
|
defaultValue: Proxmox.Utils.unknownText
|
|
},
|
|
arch: {
|
|
header: gettext('Architecture'),
|
|
defaultValue: Proxmox.Utils.unknownText
|
|
},
|
|
console: {
|
|
header: '/dev/console',
|
|
defaultValue: 1,
|
|
renderer: Proxmox.Utils.format_enabled_toggle,
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: '/dev/console',
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'console',
|
|
uncheckedValue: 0,
|
|
defaultValue: 1,
|
|
deleteDefaultValue: true,
|
|
checked: true,
|
|
fieldLabel: '/dev/console'
|
|
}
|
|
} : undefined
|
|
},
|
|
tty: {
|
|
header: gettext('TTY count'),
|
|
defaultValue: 2,
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('TTY count'),
|
|
items: {
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'tty',
|
|
minValue: 0,
|
|
maxValue: 6,
|
|
value: 2,
|
|
fieldLabel: gettext('TTY count'),
|
|
emptyText: gettext('Default'),
|
|
deleteEmpty: true
|
|
}
|
|
} : undefined
|
|
},
|
|
cmode: {
|
|
header: gettext('Console mode'),
|
|
defaultValue: 'tty',
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Console mode'),
|
|
items: {
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'cmode',
|
|
deleteEmpty: true,
|
|
value: '__default__',
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText + " (tty)"],
|
|
['tty', "/dev/tty[X]"],
|
|
['console', "/dev/console"],
|
|
['shell', "shell"]
|
|
],
|
|
fieldLabel: gettext('Console mode')
|
|
}
|
|
} : undefined
|
|
},
|
|
protection: {
|
|
header: gettext('Protection'),
|
|
defaultValue: false,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
editor: caps.vms['VM.Config.Options'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Protection'),
|
|
items: {
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'protection',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
deleteDefaultValue: true,
|
|
fieldLabel: gettext('Enabled')
|
|
}
|
|
} : undefined
|
|
},
|
|
unprivileged: {
|
|
header: gettext('Unprivileged container'),
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
defaultValue: 0
|
|
},
|
|
features: {
|
|
header: gettext('Features'),
|
|
defaultValue: Proxmox.Utils.noneText,
|
|
editor: Proxmox.UserName === 'root@pam' ?
|
|
'PVE.lxc.FeaturesEdit' : undefined
|
|
},
|
|
hookscript: {
|
|
header: gettext('Hookscript')
|
|
}
|
|
};
|
|
|
|
var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
var rowdef = rows[rec.data.key];
|
|
return !!rowdef.editor;
|
|
},
|
|
handler: function() { me.run_editor(); }
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
url: "/api2/json/" + baseurl,
|
|
selModel: sm,
|
|
interval: 5000,
|
|
tbar: [ edit_btn ],
|
|
rows: rows,
|
|
editorConfig: {
|
|
url: '/api2/extjs/' + baseurl
|
|
},
|
|
listeners: {
|
|
itemdblclick: me.run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.on('activate', me.rstore.startUpdate);
|
|
me.on('destroy', me.rstore.stopUpdate);
|
|
me.on('deactivate', me.rstore.stopUpdate);
|
|
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.lxc.DNSInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveLxcDNSInputPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var deletes = [];
|
|
if (!values.searchdomain && !me.insideWizard) {
|
|
deletes.push('searchdomain');
|
|
}
|
|
|
|
if (values.nameserver) {
|
|
var list = values.nameserver.split(/[\ \,\;]+/);
|
|
values.nameserver = list.join(' ');
|
|
} else if(!me.insideWizard) {
|
|
deletes.push('nameserver');
|
|
}
|
|
|
|
if (deletes.length) {
|
|
values['delete'] = deletes.join(',');
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var items = [
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'searchdomain',
|
|
skipEmptyText: true,
|
|
fieldLabel: gettext('DNS domain'),
|
|
emptyText: gettext('use host settings'),
|
|
allowBlank: true
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
fieldLabel: gettext('DNS servers'),
|
|
vtype: 'IP64AddressList',
|
|
allowBlank: true,
|
|
emptyText: gettext('use host settings'),
|
|
name: 'nameserver',
|
|
itemId: 'nameserver'
|
|
}
|
|
];
|
|
|
|
if (me.insideWizard) {
|
|
me.column1 = items;
|
|
} else {
|
|
me.items = items;
|
|
}
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.lxc.DNSEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var ipanel = Ext.create('PVE.lxc.DNSInputPanel');
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Resources'),
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var values = response.result.data;
|
|
|
|
if (values.nameserver) {
|
|
values.nameserver.replace(/[,;]/, ' ');
|
|
values.nameserver.replace(/^\s+/, '');
|
|
}
|
|
|
|
ipanel.setValues(values);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
/*jslint confusion: true */
|
|
Ext.define('PVE.lxc.DNS', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
alias: ['widget.pveLxcDNS'],
|
|
|
|
onlineHelp: 'pct_container_network',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
var i;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var rows = {
|
|
hostname: {
|
|
required: true,
|
|
defaultValue: me.pveSelNode.data.name,
|
|
header: gettext('Hostname'),
|
|
editor: caps.vms['VM.Config.Network'] ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
subject: gettext('Hostname'),
|
|
items: {
|
|
xtype: 'inputpanel',
|
|
items:{
|
|
fieldLabel: gettext('Hostname'),
|
|
xtype: 'textfield',
|
|
name: 'hostname',
|
|
vtype: 'DnsName',
|
|
allowBlank: true,
|
|
emptyText: 'CT' + vmid.toString()
|
|
},
|
|
onGetValues: function(values) {
|
|
var params = values;
|
|
if (values.hostname === undefined ||
|
|
values.hostname === null ||
|
|
values.hostname === '') {
|
|
params = { hostname: 'CT'+vmid.toString()};
|
|
}
|
|
return params;
|
|
}
|
|
}
|
|
} : undefined
|
|
},
|
|
searchdomain: {
|
|
header: gettext('DNS domain'),
|
|
defaultValue: '',
|
|
editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
|
|
renderer: function(value) {
|
|
return value || gettext('use host settings');
|
|
}
|
|
},
|
|
nameserver: {
|
|
header: gettext('DNS server'),
|
|
defaultValue: '',
|
|
editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined,
|
|
renderer: function(value) {
|
|
return value || gettext('use host settings');
|
|
}
|
|
}
|
|
};
|
|
|
|
var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
|
|
|
|
var reload = function() {
|
|
me.rstore.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var rowdef = rows[rec.data.key];
|
|
if (!rowdef.editor) {
|
|
return;
|
|
}
|
|
|
|
var win;
|
|
if (Ext.isString(rowdef.editor)) {
|
|
win = Ext.create(rowdef.editor, {
|
|
pveSelNode: me.pveSelNode,
|
|
confid: rec.data.key,
|
|
url: '/api2/extjs/' + baseurl
|
|
});
|
|
} else {
|
|
var config = Ext.apply({
|
|
pveSelNode: me.pveSelNode,
|
|
confid: rec.data.key,
|
|
url: '/api2/extjs/' + baseurl
|
|
}, rowdef.editor);
|
|
win = Ext.createWidget(rowdef.editor.xtype, config);
|
|
win.load();
|
|
}
|
|
//win.load();
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
var rowdef = rows[rec.data.key];
|
|
return !!rowdef.editor;
|
|
},
|
|
handler: run_editor
|
|
});
|
|
|
|
var set_button_status = function() {
|
|
var sm = me.getSelectionModel();
|
|
var rec = sm.getSelection()[0];
|
|
|
|
if (!rec) {
|
|
edit_btn.disable();
|
|
return;
|
|
}
|
|
var rowdef = rows[rec.data.key];
|
|
edit_btn.setDisabled(!rowdef.editor);
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/config",
|
|
selModel: sm,
|
|
cwidth1: 150,
|
|
run_editor: run_editor,
|
|
tbar: [ edit_btn ],
|
|
rows: rows,
|
|
listeners: {
|
|
itemdblclick: run_editor,
|
|
selectionchange: set_button_status,
|
|
activate: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.lxc.Config', {
|
|
extend: 'PVE.panel.Config',
|
|
alias: 'widget.PVE.lxc.Config',
|
|
|
|
onlineHelp: 'chapter_pct',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
if (!vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var template = !!me.pveSelNode.data.template;
|
|
|
|
var running = !!me.pveSelNode.data.uptime;
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var base_url = '/nodes/' + nodename + '/lxc/' + vmid;
|
|
|
|
me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
|
|
url: '/api2/json' + base_url + '/status/current',
|
|
interval: 1000
|
|
});
|
|
|
|
var vm_command = function(cmd, params) {
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: base_url + "/status/" + cmd,
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
};
|
|
|
|
var startBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Start'),
|
|
disabled: !caps.vms['VM.PowerMgmt'] || running,
|
|
hidden: template,
|
|
handler: function() {
|
|
vm_command('start');
|
|
},
|
|
iconCls: 'fa fa-play'
|
|
});
|
|
|
|
var stopBtn = Ext.create('Ext.menu.Item',{
|
|
text: gettext('Stop'),
|
|
disabled: !caps.vms['VM.PowerMgmt'],
|
|
confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid),
|
|
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
|
|
dangerous: true,
|
|
handler: function() {
|
|
vm_command("stop");
|
|
},
|
|
iconCls: 'fa fa-stop'
|
|
});
|
|
|
|
var shutdownBtn = Ext.create('PVE.button.Split', {
|
|
text: gettext('Shutdown'),
|
|
disabled: !caps.vms['VM.PowerMgmt'] || !running,
|
|
hidden: template,
|
|
confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid),
|
|
handler: function() {
|
|
vm_command('shutdown');
|
|
},
|
|
menu: {
|
|
items:[stopBtn]
|
|
},
|
|
iconCls: 'fa fa-power-off'
|
|
});
|
|
|
|
var migrateBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Migrate'),
|
|
disabled: !caps.vms['VM.Migrate'],
|
|
hidden: PVE.data.ResourceStore.getNodes().length < 2,
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.Migrate', {
|
|
vmtype: 'lxc',
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
win.show();
|
|
},
|
|
iconCls: 'fa fa-send-o'
|
|
});
|
|
|
|
var moreBtn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('More'),
|
|
menu: { items: [
|
|
{
|
|
text: gettext('Clone'),
|
|
iconCls: 'fa fa-fw fa-clone',
|
|
hidden: caps.vms['VM.Clone'] ? false : true,
|
|
handler: function() {
|
|
PVE.window.Clone.wrap(nodename, vmid, template, 'lxc');
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Convert to template'),
|
|
disabled: template,
|
|
xtype: 'pveMenuItem',
|
|
iconCls: 'fa fa-fw fa-file-o',
|
|
hidden: caps.vms['VM.Allocate'] ? false : true,
|
|
confirmMsg: Proxmox.Utils.format_task_description('vztemplate', vmid),
|
|
handler: function() {
|
|
Proxmox.Utils.API2Request({
|
|
url: base_url + '/template',
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert('Error', response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
{
|
|
iconCls: 'fa fa-heartbeat ',
|
|
hidden: !caps.nodes['Sys.Console'],
|
|
text: gettext('Manage HA'),
|
|
handler: function() {
|
|
var ha = me.pveSelNode.data.hastate;
|
|
Ext.create('PVE.ha.VMResourceEdit', {
|
|
vmid: vmid,
|
|
guestType: 'ct',
|
|
isCreate: (!ha || ha === 'unmanaged')
|
|
}).show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Remove'),
|
|
disabled: !caps.vms['VM.Allocate'],
|
|
itemId: 'removeBtn',
|
|
handler: function() {
|
|
Ext.create('PVE.window.SafeDestroy', {
|
|
url: base_url,
|
|
item: { type: 'CT', id: vmid }
|
|
}).show();
|
|
},
|
|
iconCls: 'fa fa-trash-o'
|
|
}
|
|
]}
|
|
});
|
|
|
|
var vm = me.pveSelNode.data;
|
|
|
|
var consoleBtn = Ext.create('PVE.button.ConsoleButton', {
|
|
disabled: !caps.vms['VM.Console'],
|
|
consoleType: 'lxc',
|
|
consoleName: vm.name,
|
|
hidden: template,
|
|
nodename: nodename,
|
|
vmid: vmid
|
|
});
|
|
|
|
var statusTxt = Ext.create('Ext.toolbar.TextItem', {
|
|
data: {
|
|
lock: undefined
|
|
},
|
|
tpl: [
|
|
'<tpl if="lock">',
|
|
'<i class="fa fa-lg fa-lock"></i> ({lock})',
|
|
'</tpl>'
|
|
]
|
|
});
|
|
|
|
|
|
Ext.apply(me, {
|
|
title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename),
|
|
hstateid: 'lxctab',
|
|
tbarSpacing: false,
|
|
tbar: [ statusTxt, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn ],
|
|
defaults: { statusStore: me.statusStore },
|
|
items: [
|
|
{
|
|
title: gettext('Summary'),
|
|
xtype: 'pveLxcSummary',
|
|
iconCls: 'fa fa-book',
|
|
itemId: 'summary'
|
|
}
|
|
]
|
|
});
|
|
|
|
if (caps.vms['VM.Console'] && !template) {
|
|
me.items.push(
|
|
{
|
|
title: gettext('Console'),
|
|
itemId: 'consolejs',
|
|
iconCls: 'fa fa-terminal',
|
|
xtype: 'pveNoVncConsole',
|
|
vmid: vmid,
|
|
consoleType: 'lxc',
|
|
xtermjs: true,
|
|
nodename: nodename
|
|
}
|
|
);
|
|
}
|
|
|
|
me.items.push(
|
|
{
|
|
title: gettext('Resources'),
|
|
itemId: 'resources',
|
|
expandedOnInit: true,
|
|
iconCls: 'fa fa-cube',
|
|
xtype: 'pveLxcRessourceView'
|
|
},
|
|
{
|
|
title: gettext('Network'),
|
|
iconCls: 'fa fa-exchange',
|
|
itemId: 'network',
|
|
xtype: 'pveLxcNetworkView'
|
|
},
|
|
{
|
|
title: gettext('DNS'),
|
|
iconCls: 'fa fa-globe',
|
|
itemId: 'dns',
|
|
xtype: 'pveLxcDNS'
|
|
},
|
|
{
|
|
title: gettext('Options'),
|
|
itemId: 'options',
|
|
iconCls: 'fa fa-gear',
|
|
xtype: 'pveLxcOptions'
|
|
},
|
|
{
|
|
title: gettext('Task History'),
|
|
itemId: 'tasks',
|
|
iconCls: 'fa fa-list',
|
|
xtype: 'proxmoxNodeTasks',
|
|
nodename: nodename,
|
|
vmidFilter: vmid
|
|
}
|
|
);
|
|
|
|
if (caps.vms['VM.Backup']) {
|
|
me.items.push({
|
|
title: gettext('Backup'),
|
|
iconCls: 'fa fa-floppy-o',
|
|
xtype: 'pveBackupView',
|
|
itemId: 'backup'
|
|
},
|
|
{
|
|
title: gettext('Replication'),
|
|
iconCls: 'fa fa-retweet',
|
|
xtype: 'pveReplicaView',
|
|
itemId: 'replication'
|
|
});
|
|
}
|
|
|
|
if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback']) && !template) {
|
|
me.items.push({
|
|
title: gettext('Snapshots'),
|
|
iconCls: 'fa fa-history',
|
|
xtype: 'pveLxcSnapshotTree',
|
|
itemId: 'snapshot'
|
|
});
|
|
}
|
|
|
|
if (caps.vms['VM.Console']) {
|
|
me.items.push(
|
|
{
|
|
xtype: 'pveFirewallRules',
|
|
title: gettext('Firewall'),
|
|
iconCls: 'fa fa-shield',
|
|
allow_iface: true,
|
|
base_url: base_url + '/firewall/rules',
|
|
list_refs_url: base_url + '/firewall/refs',
|
|
itemId: 'firewall'
|
|
},
|
|
{
|
|
xtype: 'pveFirewallOptions',
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-gear',
|
|
onlineHelp: 'pve_firewall_vm_container_configuration',
|
|
title: gettext('Options'),
|
|
base_url: base_url + '/firewall/options',
|
|
fwtype: 'vm',
|
|
itemId: 'firewall-options'
|
|
},
|
|
{
|
|
xtype: 'pveFirewallAliases',
|
|
title: gettext('Alias'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-external-link',
|
|
base_url: base_url + '/firewall/aliases',
|
|
itemId: 'firewall-aliases'
|
|
},
|
|
{
|
|
xtype: 'pveIPSet',
|
|
title: gettext('IPSet'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-list-ol',
|
|
base_url: base_url + '/firewall/ipset',
|
|
list_refs_url: base_url + '/firewall/refs',
|
|
itemId: 'firewall-ipset'
|
|
},
|
|
{
|
|
title: gettext('Log'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-list',
|
|
onlineHelp: 'chapter_pve_firewall',
|
|
itemId: 'firewall-fwlog',
|
|
xtype: 'proxmoxLogView',
|
|
url: '/api2/extjs' + base_url + '/firewall/log'
|
|
}
|
|
);
|
|
}
|
|
|
|
if (caps.vms['Permissions.Modify']) {
|
|
me.items.push({
|
|
xtype: 'pveACLView',
|
|
title: gettext('Permissions'),
|
|
itemId: 'permissions',
|
|
iconCls: 'fa fa-unlock',
|
|
path: '/vms/' + vmid
|
|
});
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
me.mon(me.statusStore, 'load', function(s, records, success) {
|
|
var status;
|
|
var lock;
|
|
if (!success) {
|
|
status = 'unknown';
|
|
} else {
|
|
var rec = s.data.get('status');
|
|
status = rec ? rec.data.value : 'unknown';
|
|
rec = s.data.get('template');
|
|
template = rec.data.value || false;
|
|
rec = s.data.get('lock');
|
|
lock = rec ? rec.data.value : undefined;
|
|
}
|
|
|
|
statusTxt.update({ lock: lock });
|
|
|
|
startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template);
|
|
shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
|
|
stopBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'stopped');
|
|
me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
|
|
consoleBtn.setDisabled(template);
|
|
});
|
|
|
|
me.on('afterrender', function() {
|
|
me.statusStore.startUpdate();
|
|
});
|
|
|
|
me.on('destroy', function() {
|
|
me.statusStore.stopUpdate();
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.lxc.CreateWizard', {
|
|
extend: 'PVE.window.Wizard',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
viewModel: {
|
|
data: {
|
|
nodename: '',
|
|
storage: '',
|
|
unprivileged: true
|
|
}
|
|
},
|
|
|
|
cbindData: {
|
|
nodename: undefined
|
|
},
|
|
|
|
subject: gettext('LXC Container'),
|
|
|
|
items: [
|
|
{
|
|
xtype: 'inputpanel',
|
|
title: gettext('General'),
|
|
onlineHelp: 'pct_general',
|
|
column1: [
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
name: 'nodename',
|
|
cbind: {
|
|
selectCurNode: '{!nodename}',
|
|
preferredValue: '{nodename}'
|
|
},
|
|
bind: {
|
|
value: '{nodename}'
|
|
},
|
|
fieldLabel: gettext('Node'),
|
|
allowBlank: false,
|
|
onlineValidator: true
|
|
},
|
|
{
|
|
xtype: 'pveGuestIDSelector',
|
|
name: 'vmid', // backend only knows vmid
|
|
guestType: 'lxc',
|
|
value: '',
|
|
loadNextFreeID: true,
|
|
validateExists: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'hostname',
|
|
vtype: 'DnsName',
|
|
value: '',
|
|
fieldLabel: gettext('Hostname'),
|
|
skipEmptyText: true,
|
|
allowBlank: true
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'unprivileged',
|
|
value: true,
|
|
bind: {
|
|
value: '{unprivileged}'
|
|
},
|
|
fieldLabel: gettext('Unprivileged container')
|
|
}
|
|
],
|
|
column2: [
|
|
{
|
|
xtype: 'pvePoolSelector',
|
|
fieldLabel: gettext('Resource Pool'),
|
|
name: 'pool',
|
|
value: '',
|
|
allowBlank: true
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
inputType: 'password',
|
|
name: 'password',
|
|
value: '',
|
|
fieldLabel: gettext('Password'),
|
|
allowBlank: false,
|
|
minLength: 5,
|
|
change: function(f, value) {
|
|
if (f.rendered) {
|
|
f.up().down('field[name=confirmpw]').validate();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
inputType: 'password',
|
|
name: 'confirmpw',
|
|
value: '',
|
|
fieldLabel: gettext('Confirm password'),
|
|
allowBlank: true,
|
|
submitValue: false,
|
|
validator: function(value) {
|
|
var pw = this.up().down('field[name=password]').getValue();
|
|
if (pw !== value) {
|
|
return "Passwords do not match!";
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
name: 'ssh-public-keys',
|
|
value: '',
|
|
fieldLabel: gettext('SSH public key'),
|
|
allowBlank: true,
|
|
validator: function(value) {
|
|
var pwfield = this.up().down('field[name=password]');
|
|
if (value.length) {
|
|
var key = PVE.Parser.parseSSHKey(value);
|
|
if (!key) {
|
|
return "Failed to recognize ssh key";
|
|
}
|
|
pwfield.allowBlank = true;
|
|
} else {
|
|
pwfield.allowBlank = false;
|
|
}
|
|
pwfield.validate();
|
|
return true;
|
|
},
|
|
afterRender: function() {
|
|
if (!window.FileReader) {
|
|
// No FileReader support in this browser
|
|
return;
|
|
}
|
|
var cancel = function(ev) {
|
|
ev = ev.event;
|
|
if (ev.preventDefault) {
|
|
ev.preventDefault();
|
|
}
|
|
};
|
|
var field = this;
|
|
field.inputEl.on('dragover', cancel);
|
|
field.inputEl.on('dragenter', cancel);
|
|
field.inputEl.on('drop', function(ev) {
|
|
ev = ev.event;
|
|
if (ev.preventDefault) {
|
|
ev.preventDefault();
|
|
}
|
|
var files = ev.dataTransfer.files;
|
|
PVE.Utils.loadSSHKeyFromFile(files[0], function(v) {
|
|
field.setValue(v);
|
|
});
|
|
});
|
|
}
|
|
},
|
|
{
|
|
xtype: 'filebutton',
|
|
name: 'file',
|
|
hidden: !window.FileReader,
|
|
text: gettext('Load SSH Key File'),
|
|
listeners: {
|
|
change: function(btn, e, value) {
|
|
e = e.event;
|
|
var field = this.up().down('proxmoxtextfield[name=ssh-public-keys]');
|
|
PVE.Utils.loadSSHKeyFromFile(e.target.files[0], function(v) {
|
|
field.setValue(v);
|
|
});
|
|
btn.reset();
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'inputpanel',
|
|
title: gettext('Template'),
|
|
onlineHelp: 'pct_container_images',
|
|
column1: [
|
|
{
|
|
xtype: 'pveStorageSelector',
|
|
name: 'tmplstorage',
|
|
fieldLabel: gettext('Storage'),
|
|
storageContent: 'vztmpl',
|
|
autoSelect: true,
|
|
allowBlank: false,
|
|
bind: {
|
|
value: '{storage}',
|
|
nodename: '{nodename}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'pveFileSelector',
|
|
name: 'ostemplate',
|
|
storageContent: 'vztmpl',
|
|
fieldLabel: gettext('Template'),
|
|
bind: {
|
|
storage: '{storage}',
|
|
nodename: '{nodename}'
|
|
},
|
|
allowBlank: false
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'pveLxcMountPointInputPanel',
|
|
title: gettext('Root Disk'),
|
|
insideWizard: true,
|
|
isCreate: true,
|
|
unused: false,
|
|
bind: {
|
|
nodename: '{nodename}',
|
|
unprivileged: '{unprivileged}'
|
|
},
|
|
confid: 'rootfs'
|
|
},
|
|
{
|
|
xtype: 'pveLxcCPUInputPanel',
|
|
title: gettext('CPU'),
|
|
insideWizard: true
|
|
},
|
|
{
|
|
xtype: 'pveLxcMemoryInputPanel',
|
|
title: gettext('Memory'),
|
|
insideWizard: true
|
|
},
|
|
{
|
|
xtype: 'pveLxcNetworkInputPanel',
|
|
title: gettext('Network'),
|
|
insideWizard: true,
|
|
bind: {
|
|
nodename: '{nodename}'
|
|
},
|
|
isCreate: true
|
|
},
|
|
{
|
|
xtype: 'pveLxcDNSInputPanel',
|
|
title: gettext('DNS'),
|
|
insideWizard: true
|
|
},
|
|
{
|
|
title: gettext('Confirm'),
|
|
layout: 'fit',
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
store: {
|
|
model: 'KeyValue',
|
|
sorters: [{
|
|
property : 'key',
|
|
direction: 'ASC'
|
|
}]
|
|
},
|
|
columns: [
|
|
{header: 'Key', width: 150, dataIndex: 'key'},
|
|
{header: 'Value', flex: 1, dataIndex: 'value'}
|
|
]
|
|
}
|
|
],
|
|
dockedItems: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'start',
|
|
dock: 'bottom',
|
|
margin: '5 0 0 0',
|
|
boxLabel: gettext('Start after created')
|
|
}
|
|
],
|
|
listeners: {
|
|
show: function(panel) {
|
|
var wizard = this.up('window');
|
|
var kv = wizard.getValues();
|
|
var data = [];
|
|
Ext.Object.each(kv, function(key, value) {
|
|
if (key === 'delete' || key === 'tmplstorage') { // ignore
|
|
return;
|
|
}
|
|
if (key === 'password') { // don't show pw
|
|
return;
|
|
}
|
|
var html = Ext.htmlEncode(Ext.JSON.encode(value));
|
|
data.push({ key: key, value: value });
|
|
});
|
|
|
|
var summarystore = panel.down('grid').getStore();
|
|
summarystore.suspendEvents();
|
|
summarystore.removeAll();
|
|
summarystore.add(data);
|
|
summarystore.sort();
|
|
summarystore.resumeEvents();
|
|
summarystore.fireEvent('refresh');
|
|
}
|
|
},
|
|
onSubmit: function() {
|
|
var wizard = this.up('window');
|
|
var kv = wizard.getValues();
|
|
delete kv['delete'];
|
|
|
|
var nodename = kv.nodename;
|
|
delete kv.nodename;
|
|
delete kv.tmplstorage;
|
|
|
|
if (!kv.pool.length) {
|
|
delete kv.pool;
|
|
}
|
|
|
|
if (!kv.password.length && kv['ssh-public-keys']) {
|
|
delete kv.password;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + nodename + '/lxc',
|
|
waitMsgTarget: wizard,
|
|
method: 'POST',
|
|
params: kv,
|
|
success: function(response, opts){
|
|
var upid = response.result.data;
|
|
|
|
var win = Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: upid
|
|
});
|
|
win.show();
|
|
wizard.close();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
|
|
|
|
Ext.define('PVE.lxc.SnapshotTree', {
|
|
extend: 'Ext.tree.Panel',
|
|
alias: ['widget.pveLxcSnapshotTree'],
|
|
|
|
onlineHelp: 'pct_snapshots',
|
|
|
|
load_delay: 3000,
|
|
|
|
old_digest: 'invalid',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-lxc-snapshots',
|
|
|
|
sorterFn: function(rec1, rec2) {
|
|
var v1 = rec1.data.snaptime;
|
|
var v2 = rec2.data.snaptime;
|
|
|
|
if (rec1.data.name === 'current') {
|
|
return 1;
|
|
}
|
|
if (rec2.data.name === 'current') {
|
|
return -1;
|
|
}
|
|
|
|
return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0));
|
|
},
|
|
|
|
reload: function(repeat) {
|
|
var me = this;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot',
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(me, response.htmlStatus);
|
|
me.load_task.delay(me.load_delay);
|
|
},
|
|
success: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(me, false);
|
|
var digest = 'invalid';
|
|
var idhash = {};
|
|
var root = { name: '__root', expanded: true, children: [] };
|
|
Ext.Array.each(response.result.data, function(item) {
|
|
item.leaf = true;
|
|
item.children = [];
|
|
if (item.name === 'current') {
|
|
digest = item.digest + item.running;
|
|
if (item.running) {
|
|
item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running';
|
|
} else {
|
|
item.iconCls = 'fa fa-fw fa-desktop x-fa-tree';
|
|
}
|
|
} else {
|
|
item.iconCls = 'fa fa-fw fa-history x-fa-tree';
|
|
}
|
|
idhash[item.name] = item;
|
|
});
|
|
|
|
if (digest !== me.old_digest) {
|
|
me.old_digest = digest;
|
|
|
|
Ext.Array.each(response.result.data, function(item) {
|
|
if (item.parent && idhash[item.parent]) {
|
|
var parent_item = idhash[item.parent];
|
|
parent_item.children.push(item);
|
|
parent_item.leaf = false;
|
|
parent_item.expanded = true;
|
|
parent_item.expandable = false;
|
|
} else {
|
|
root.children.push(item);
|
|
}
|
|
});
|
|
|
|
me.setRootNode(root);
|
|
}
|
|
|
|
me.load_task.delay(me.load_delay);
|
|
}
|
|
});
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/feature',
|
|
params: { feature: 'snapshot' },
|
|
method: 'GET',
|
|
success: function(response, options) {
|
|
var res = response.result.data;
|
|
if (res.hasFeature) {
|
|
var snpBtns = Ext.ComponentQuery.query('#snapshotBtn');
|
|
snpBtns.forEach(function(item){
|
|
item.enable();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
},
|
|
|
|
listeners: {
|
|
beforestatesave: function(grid, state, eopts) {
|
|
// extjs cannot serialize functions,
|
|
// so a the sorter with only the sorterFn will
|
|
// not be a valid sorter when restoring the state
|
|
delete state.storeState.sorters;
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
me.vmid = me.pveSelNode.data.vmid;
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
me.load_task = new Ext.util.DelayedTask(me.reload, me);
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var valid_snapshot = function(record) {
|
|
return record && record.data && record.data.name &&
|
|
record.data.name !== 'current';
|
|
};
|
|
|
|
var valid_snapshot_rollback = function(record) {
|
|
return record && record.data && record.data.name &&
|
|
record.data.name !== 'current' && !record.data.snapstate;
|
|
};
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (valid_snapshot(rec)) {
|
|
var win = Ext.create('PVE.window.LxcSnapshot', {
|
|
snapname: rec.data.name,
|
|
nodename: me.nodename,
|
|
vmid: me.vmid
|
|
});
|
|
win.show();
|
|
me.mon(win, 'close', me.reload, me);
|
|
}
|
|
};
|
|
|
|
var editBtn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: valid_snapshot,
|
|
handler: run_editor
|
|
});
|
|
|
|
var rollbackBtn = new Proxmox.button.Button({
|
|
text: gettext('Rollback'),
|
|
disabled: true,
|
|
dangerous: true,
|
|
selModel: sm,
|
|
enableFn: valid_snapshot_rollback,
|
|
confirmMsg: function(rec) {
|
|
var taskdescription = Proxmox.Utils.format_task_description('vzrollback', me.vmid);
|
|
var snaptime = Ext.Date.format(rec.data.snaptime,'Y-m-d H:i:s');
|
|
var snapname = rec.data.name;
|
|
|
|
var msg = Ext.String.format(gettext('{0} to {1} ({2})'),
|
|
taskdescription, snapname, snaptime);
|
|
msg += '<p>' + gettext('Note: Rollback stops CT') + '</p>';
|
|
|
|
return msg;
|
|
},
|
|
handler: function(btn, event) {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var snapname = rec.data.name;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot/' + snapname + '/rollback',
|
|
method: 'POST',
|
|
waitMsgTarget: me,
|
|
callback: function() {
|
|
me.reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
|
|
win.show();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
var removeBtn = new Proxmox.button.Button({
|
|
text: gettext('Remove'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
confirmMsg: function(rec) {
|
|
var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
|
|
"'" + rec.data.name + "'");
|
|
return msg;
|
|
},
|
|
enableFn: valid_snapshot,
|
|
handler: function(btn, event) {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var snapname = rec.data.name;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot/' + snapname,
|
|
method: 'DELETE',
|
|
waitMsgTarget: me,
|
|
callback: function() {
|
|
me.reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
|
|
win.show();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
var snapshotBtn = Ext.create('Ext.Button', {
|
|
itemId: 'snapshotBtn',
|
|
text: gettext('Take Snapshot'),
|
|
disabled: true,
|
|
handler: function() {
|
|
var win = Ext.create('PVE.window.LxcSnapshot', {
|
|
nodename: me.nodename,
|
|
vmid: me.vmid
|
|
});
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
layout: 'fit',
|
|
rootVisible: false,
|
|
animate: false,
|
|
sortableColumns: false,
|
|
selModel: sm,
|
|
tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ],
|
|
fields: [
|
|
'name', 'description', 'snapstate', 'vmstate', 'running',
|
|
{ name: 'snaptime', type: 'date', dateFormat: 'timestamp' }
|
|
],
|
|
columns: [
|
|
{
|
|
xtype: 'treecolumn',
|
|
text: gettext('Name'),
|
|
dataIndex: 'name',
|
|
width: 200,
|
|
renderer: function(value, metaData, record) {
|
|
if (value === 'current') {
|
|
return "NOW";
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
},
|
|
// {
|
|
// text: gettext('RAM'),
|
|
// align: 'center',
|
|
// resizable: false,
|
|
// dataIndex: 'vmstate',
|
|
// width: 50,
|
|
// renderer: function(value, metaData, record) {
|
|
// if (record.data.name !== 'current') {
|
|
// return Proxmox.Utils.format_boolean(value);
|
|
// }
|
|
// }
|
|
// },
|
|
{
|
|
text: gettext('Date') + "/" + gettext("Status"),
|
|
dataIndex: 'snaptime',
|
|
resizable: false,
|
|
width: 150,
|
|
renderer: function(value, metaData, record) {
|
|
if (record.data.snapstate) {
|
|
return record.data.snapstate;
|
|
}
|
|
if (value) {
|
|
return Ext.Date.format(value,'Y-m-d H:i:s');
|
|
}
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Description'),
|
|
dataIndex: 'description',
|
|
flex: 1,
|
|
renderer: function(value, metaData, record) {
|
|
if (record.data.name === 'current') {
|
|
return gettext("You are here!");
|
|
} else {
|
|
return Ext.String.htmlEncode(value);
|
|
}
|
|
}
|
|
}
|
|
],
|
|
columnLines: true,
|
|
listeners: {
|
|
activate: me.reload,
|
|
destroy: me.load_task.cancel,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.store.sorters.add(new Ext.util.Sorter({
|
|
sorterFn: me.sorterFn
|
|
}));
|
|
}
|
|
});
|
|
Ext.define('PVE.window.LxcSnapshot', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
resizable: false,
|
|
|
|
// needed for finding the reference to submitbutton
|
|
// because we do not have a controller
|
|
referenceHolder: true,
|
|
defaultButton: 'submitbutton',
|
|
defaultFocus: 'field',
|
|
|
|
take_snapshot: function(snapname, descr, vmstate) {
|
|
var me = this;
|
|
var params = { snapname: snapname };
|
|
if (descr) {
|
|
params.description = descr;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot",
|
|
waitMsgTarget: me,
|
|
method: 'POST',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
|
|
win.show();
|
|
me.close();
|
|
}
|
|
});
|
|
},
|
|
|
|
update_snapshot: function(snapname, descr) {
|
|
var me = this;
|
|
Proxmox.Utils.API2Request({
|
|
params: { description: descr },
|
|
url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot/" +
|
|
snapname + '/config',
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
me.close();
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var summarystore = Ext.create('Ext.data.Store', {
|
|
model: 'KeyValue',
|
|
sorters: [
|
|
{
|
|
property : 'key',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
});
|
|
|
|
var items = [
|
|
{
|
|
xtype: me.snapname ? 'displayfield' : 'textfield',
|
|
name: 'snapname',
|
|
value: me.snapname,
|
|
fieldLabel: gettext('Name'),
|
|
vtype: 'ConfigId',
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
if (me.snapname) {
|
|
items.push({
|
|
xtype: 'displayfield',
|
|
name: 'snaptime',
|
|
renderer: PVE.Utils.render_timestamp_human_readable,
|
|
fieldLabel: gettext('Timestamp')
|
|
});
|
|
}
|
|
|
|
items.push({
|
|
xtype: 'textareafield',
|
|
grow: true,
|
|
name: 'description',
|
|
fieldLabel: gettext('Description')
|
|
});
|
|
|
|
if (me.snapname) {
|
|
items.push({
|
|
title: gettext('Settings'),
|
|
xtype: 'grid',
|
|
height: 200,
|
|
store: summarystore,
|
|
columns: [
|
|
{header: gettext('Key'), width: 150, dataIndex: 'key'},
|
|
{header: gettext('Value'), flex: 1, dataIndex: 'value'}
|
|
]
|
|
});
|
|
}
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
fieldDefaults: {
|
|
labelWidth: 100,
|
|
anchor: '100%'
|
|
},
|
|
items: items
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var submitBtn;
|
|
|
|
if (me.snapname) {
|
|
me.title = gettext('Edit') + ': ' + gettext('Snapshot');
|
|
submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Update'),
|
|
handler: function() {
|
|
if (form.isValid()) {
|
|
var values = form.getValues();
|
|
me.update_snapshot(me.snapname, values.description);
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot');
|
|
submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Take Snapshot'),
|
|
reference: 'submitbutton',
|
|
handler: function() {
|
|
if (form.isValid()) {
|
|
var values = form.getValues();
|
|
me.take_snapshot(values.snapname, values.description);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
modal: true,
|
|
width: 450,
|
|
border: false,
|
|
layout: 'fit',
|
|
buttons: [ submitBtn ],
|
|
items: [ me.formPanel ]
|
|
});
|
|
|
|
if (me.snapname) {
|
|
Ext.apply(me, {
|
|
width: 620,
|
|
height: 420
|
|
});
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
if (!me.snapname) {
|
|
return;
|
|
}
|
|
|
|
// else load data
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot/" +
|
|
me.snapname + '/config',
|
|
waitMsgTarget: me,
|
|
method: 'GET',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
me.close();
|
|
},
|
|
success: function(response, options) {
|
|
var data = response.result.data;
|
|
var kvarray = [];
|
|
Ext.Object.each(data, function(key, value) {
|
|
if (key === 'description' || key === 'snaptime') {
|
|
return;
|
|
}
|
|
kvarray.push({ key: key, value: value });
|
|
});
|
|
|
|
summarystore.suspendEvents();
|
|
summarystore.add(kvarray);
|
|
summarystore.sort();
|
|
summarystore.resumeEvents();
|
|
summarystore.fireEvent('refresh', summarystore);
|
|
|
|
form.findField('snaptime').setValue(data.snaptime);
|
|
form.findField('description').setValue(data.description);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
/*jslint confusion: true */
|
|
var labelWidth = 120;
|
|
|
|
Ext.define('PVE.lxc.MemoryEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Memory'),
|
|
items: Ext.create('PVE.lxc.MemoryInputPanel')
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
|
|
|
|
Ext.define('PVE.lxc.CPUEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('CPU'),
|
|
items: Ext.create('PVE.lxc.CPUInputPanel')
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.lxc.CPUInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveLxcCPUInputPanel',
|
|
|
|
onlineHelp: 'pct_cpu',
|
|
|
|
insideWizard: false,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
PVE.Utils.delete_if_default(values, 'cores', '', me.insideWizard);
|
|
// cpu{limit,unit} aren't in the wizard so create is always false
|
|
PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0);
|
|
PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0);
|
|
|
|
return values;
|
|
},
|
|
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'numberfield',
|
|
name: 'cpulimit',
|
|
minValue: 0,
|
|
value: '',
|
|
step: 1,
|
|
fieldLabel: gettext('CPU limit'),
|
|
allowBlank: true,
|
|
emptyText: gettext('unlimited')
|
|
}
|
|
],
|
|
|
|
advancedColumn2: [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'cpuunits',
|
|
fieldLabel: gettext('CPU units'),
|
|
value: 1024,
|
|
minValue: 8,
|
|
maxValue: 500000,
|
|
labelWidth: labelWidth,
|
|
allowBlank: false
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'cores',
|
|
minValue: 1,
|
|
maxValue: 128,
|
|
value: me.insideWizard ? 1 : '',
|
|
fieldLabel: gettext('Cores'),
|
|
allowBlank: true,
|
|
deleteEmpty: true,
|
|
emptyText: gettext('unlimited')
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.lxc.MemoryInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
alias: 'widget.pveLxcMemoryInputPanel',
|
|
|
|
onlineHelp: 'pct_memory',
|
|
|
|
insideWizard: false,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var items = [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'memory',
|
|
minValue: 16,
|
|
value: '512',
|
|
step: 32,
|
|
fieldLabel: gettext('Memory') + ' (MiB)',
|
|
labelWidth: labelWidth,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'swap',
|
|
minValue: 0,
|
|
value: '512',
|
|
step: 32,
|
|
fieldLabel: gettext('Swap') + ' (MiB)',
|
|
labelWidth: labelWidth,
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
if (me.insideWizard) {
|
|
me.column1 = items;
|
|
} else {
|
|
me.items = items;
|
|
}
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.window.MPResize', {
|
|
extend: 'Ext.window.Window',
|
|
|
|
resizable: false,
|
|
|
|
resize_disk: function(disk, size) {
|
|
var me = this;
|
|
var params = { disk: disk, size: '+' + size + 'G' };
|
|
|
|
Proxmox.Utils.API2Request({
|
|
params: params,
|
|
url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/resize',
|
|
waitMsgTarget: me,
|
|
method: 'PUT',
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
var upid = response.result.data;
|
|
var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid });
|
|
win.show();
|
|
me.close();
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.vmid) {
|
|
throw "no VM ID specified";
|
|
}
|
|
|
|
var items = [
|
|
{
|
|
xtype: 'displayfield',
|
|
name: 'disk',
|
|
value: me.disk,
|
|
fieldLabel: gettext('Disk'),
|
|
vtype: 'StorageId',
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.hdsizesel = Ext.createWidget('numberfield', {
|
|
name: 'size',
|
|
minValue: 0,
|
|
maxValue: 128*1024,
|
|
decimalPrecision: 3,
|
|
value: '0',
|
|
fieldLabel: gettext('Size Increment') + ' (GiB)',
|
|
allowBlank: false
|
|
});
|
|
|
|
items.push(me.hdsizesel);
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
bodyPadding: 10,
|
|
border: false,
|
|
fieldDefaults: {
|
|
labelWidth: 120,
|
|
anchor: '100%'
|
|
},
|
|
items: items
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var submitBtn;
|
|
|
|
me.title = gettext('Resize disk');
|
|
submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Resize disk'),
|
|
handler: function() {
|
|
if (form.isValid()) {
|
|
var values = form.getValues();
|
|
me.resize_disk(me.disk, values.size);
|
|
}
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
modal: true,
|
|
border: false,
|
|
layout: 'fit',
|
|
buttons: [ submitBtn ],
|
|
items: [ me.formPanel ]
|
|
});
|
|
|
|
|
|
me.callParent();
|
|
|
|
if (!me.disk) {
|
|
return;
|
|
}
|
|
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
/* hidden: boolean and string
|
|
* bind: function and object
|
|
* disabled: boolean and string
|
|
*/
|
|
Ext.define('PVE.lxc.MountPointInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
xtype: 'pveLxcMountPointInputPanel',
|
|
|
|
insideWizard: false,
|
|
|
|
onlineHelp: 'pct_container_storage',
|
|
|
|
unused: false, // add unused disk imaged
|
|
|
|
unprivileged: false,
|
|
|
|
vmconfig: {}, // used to select unused disks
|
|
|
|
setUnprivileged: function(unprivileged) {
|
|
var me = this;
|
|
var vm = me.getViewModel();
|
|
me.unprivileged = unprivileged;
|
|
vm.set('unpriv', unprivileged);
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
var confid = me.confid || "mp"+values.mpid;
|
|
values.file = me.down('field[name=file]').getValue();
|
|
if (values.mountoptions) {
|
|
values.mountoptions = values.mountoptions.join(';');
|
|
}
|
|
|
|
if (me.unused) {
|
|
confid = "mp"+values.mpid;
|
|
} else if (me.isCreate) {
|
|
values.file = values.hdstorage + ':' + values.disksize;
|
|
}
|
|
|
|
// delete unnecessary fields
|
|
delete values.mpid;
|
|
delete values.hdstorage;
|
|
delete values.disksize;
|
|
delete values.diskformat;
|
|
|
|
var res = {};
|
|
res[confid] = PVE.Parser.printLxcMountPoint(values);
|
|
return res;
|
|
},
|
|
|
|
|
|
setMountPoint: function(mp) {
|
|
var me = this;
|
|
var vm = this.getViewModel();
|
|
vm.set('mptype', mp.type);
|
|
if (mp.mountoptions) {
|
|
mp.mountoptions = mp.mountoptions.split(';');
|
|
}
|
|
|
|
if (this.confid === 'rootfs') {
|
|
var field = me.down('field[name=mountoptions]');
|
|
var forbidden = ['nodev', 'noexec'];
|
|
var filtered = field.comboItems.filter(e => !forbidden.includes(e[0]));
|
|
field.setComboItems(filtered);
|
|
}
|
|
|
|
me.setValues(mp);
|
|
},
|
|
|
|
setVMConfig: function(vmconfig) {
|
|
var me = this;
|
|
var vm = me.getViewModel();
|
|
me.vmconfig = vmconfig;
|
|
vm.set('unpriv', vmconfig.unprivileged);
|
|
|
|
PVE.Utils.forEachMP(function(bus, i) {
|
|
var name = "mp" + i.toString();
|
|
if (!Ext.isDefined(vmconfig[name])) {
|
|
me.down('field[name=mpid]').setValue(i);
|
|
return false;
|
|
}
|
|
});
|
|
},
|
|
|
|
setNodename: function(nodename) {
|
|
var me = this;
|
|
var vm = me.getViewModel();
|
|
vm.set('node', nodename);
|
|
me.down('#diskstorage').setNodename(nodename);
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
control: {
|
|
'field[name=mpid]': {
|
|
change: function(field, value) {
|
|
field.validate();
|
|
}
|
|
},
|
|
'#hdstorage': {
|
|
change: function(field, newValue) {
|
|
var me = this;
|
|
if (!newValue) {
|
|
return;
|
|
}
|
|
|
|
var rec = field.store.getById(newValue);
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var vm = me.getViewModel();
|
|
vm.set('type', rec.data.type);
|
|
}
|
|
}
|
|
},
|
|
|
|
init: function(view) {
|
|
var me = this;
|
|
var vm = this.getViewModel();
|
|
vm.set('confid', view.confid);
|
|
vm.set('unused', view.unused);
|
|
vm.set('node', view.nodename);
|
|
vm.set('unpriv', view.unprivileged);
|
|
vm.set('hideStorSelector', view.unused || !view.isCreate);
|
|
}
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
unpriv: false,
|
|
unused: false,
|
|
showStorageSelector: false,
|
|
mptype: '',
|
|
type: '',
|
|
confid: '',
|
|
node: ''
|
|
},
|
|
|
|
formulas: {
|
|
quota: function(get) {
|
|
return !(get('type') === 'zfs' ||
|
|
get('type') === 'zfspool' ||
|
|
get('unpriv') ||
|
|
get('isBind'));
|
|
},
|
|
hasMP: function(get) {
|
|
return !!get('confid') && !get('unused');
|
|
},
|
|
isRoot: function(get) {
|
|
return get('confid') === 'rootfs';
|
|
},
|
|
isBind: function(get) {
|
|
return get('mptype') === 'bind';
|
|
},
|
|
isBindOrRoot: function(get) {
|
|
return get('isBind') || get('isRoot');
|
|
}
|
|
}
|
|
},
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'mpid',
|
|
fieldLabel: gettext('Mount Point ID'),
|
|
minValue: 0,
|
|
maxValue: PVE.Utils.mp_counts.mps - 1,
|
|
hidden: true,
|
|
allowBlank: false,
|
|
disabled: true,
|
|
bind: {
|
|
hidden: '{hasMP}',
|
|
disabled: '{hasMP}'
|
|
},
|
|
validator: function(value) {
|
|
var me = this.up('inputpanel');
|
|
if (!me.rendered) {
|
|
return;
|
|
}
|
|
if (Ext.isDefined(me.vmconfig["mp"+value])) {
|
|
return "Mount point is already in use.";
|
|
}
|
|
/*jslint confusion: true*/
|
|
/* returns a string above */
|
|
return true;
|
|
}
|
|
},
|
|
{
|
|
xtype: 'pveDiskStorageSelector',
|
|
itemId: 'diskstorage',
|
|
storageContent: 'rootdir',
|
|
hidden: true,
|
|
autoSelect: true,
|
|
selectformat: false,
|
|
defaultSize: 8,
|
|
bind: {
|
|
hidden: '{hideStorSelector}',
|
|
disabled: '{hideStorSelector}',
|
|
nodename: '{node}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
disabled: true,
|
|
submitValue: false,
|
|
fieldLabel: gettext('Disk image'),
|
|
name: 'file',
|
|
bind: {
|
|
hidden: '{!hideStorSelector}'
|
|
}
|
|
}
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'mp',
|
|
value: '',
|
|
emptyText: gettext('/some/path'),
|
|
allowBlank: false,
|
|
disabled: true,
|
|
fieldLabel: gettext('Path'),
|
|
bind: {
|
|
hidden: '{isRoot}',
|
|
disabled: '{isRoot}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'backup',
|
|
fieldLabel: gettext('Backup'),
|
|
bind: {
|
|
hidden: '{isRoot}',
|
|
disabled: '{isBindOrRoot}'
|
|
}
|
|
}
|
|
],
|
|
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'quota',
|
|
defaultValue: 0,
|
|
bind: {
|
|
disabled: '{!quota}'
|
|
},
|
|
fieldLabel: gettext('Enable quota'),
|
|
listeners: {
|
|
disable: function() {
|
|
this.reset();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'ro',
|
|
defaultValue: 0,
|
|
bind: {
|
|
hidden: '{isRoot}',
|
|
disabled: '{isRoot}'
|
|
},
|
|
fieldLabel: gettext('Read-only')
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'mountoptions',
|
|
fieldLabel: gettext('Mount options'),
|
|
deleteEmpty: false,
|
|
comboItems: [
|
|
['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() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var unused = me.confid && me.confid.match(/^unused\d+$/);
|
|
|
|
me.isCreate = me.confid ? unused : true;
|
|
|
|
var ipanel = Ext.create('PVE.lxc.MountPointInputPanel', {
|
|
confid: me.confid,
|
|
nodename: nodename,
|
|
unused: unused,
|
|
unprivileged: me.unprivileged,
|
|
isCreate: me.isCreate
|
|
});
|
|
|
|
var subject;
|
|
if (unused) {
|
|
subject = gettext('Unused Disk');
|
|
} else if (me.isCreate) {
|
|
subject = gettext('Mount Point');
|
|
} else {
|
|
subject = gettext('Mount Point') + ' (' + me.confid + ')';
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
subject: subject,
|
|
defaultFocus: me.confid !== 'rootfs' ? 'textfield[name=mp]' : 'tool',
|
|
items: ipanel
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.load({
|
|
success: function(response, options) {
|
|
ipanel.setVMConfig(response.result.data);
|
|
if (me.confid) {
|
|
/*jslint confusion: true*/
|
|
/*data is defined as array above*/
|
|
var value = response.result.data[me.confid];
|
|
/*jslint confusion: false*/
|
|
var mp = PVE.Parser.parseLxcMountPoint(value);
|
|
|
|
if (!mp) {
|
|
Ext.Msg.alert(gettext('Error'), 'Unable to parse mount point options');
|
|
me.close();
|
|
return;
|
|
}
|
|
|
|
ipanel.setMountPoint(mp);
|
|
me.isValid(); // trigger validation
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
Ext.define('PVE.pool.StatusView', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
alias: ['widget.pvePoolStatusView'],
|
|
disabled: true,
|
|
|
|
title: gettext('Status'),
|
|
cwidth1: 150,
|
|
interval: 30000,
|
|
//height: 195,
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var pool = me.pveSelNode.data.pool;
|
|
if (!pool) {
|
|
throw "no pool specified";
|
|
}
|
|
|
|
var rows = {
|
|
comment: {
|
|
header: gettext('Comment'),
|
|
renderer: Ext.String.htmlEncode,
|
|
required: true
|
|
}
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
url: "/api2/json/pools/" + pool,
|
|
rows: rows
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.pool.Summary', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pvePoolSummary',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var pool = me.pveSelNode.data.pool;
|
|
if (!pool) {
|
|
throw "no pool specified";
|
|
}
|
|
|
|
var statusview = Ext.create('PVE.pool.StatusView', {
|
|
pveSelNode: me.pveSelNode,
|
|
style: 'padding-top:0px'
|
|
});
|
|
|
|
var rstore = statusview.rstore;
|
|
|
|
Ext.apply(me, {
|
|
autoScroll: true,
|
|
bodyStyle: 'padding:10px',
|
|
defaults: {
|
|
style: 'padding-top:10px',
|
|
width: 800
|
|
},
|
|
items: [ statusview ]
|
|
});
|
|
|
|
me.on('activate', rstore.startUpdate);
|
|
me.on('destroy', rstore.stopUpdate);
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.pool.Config', {
|
|
extend: 'PVE.panel.Config',
|
|
alias: 'widget.pvePoolConfig',
|
|
|
|
onlineHelp: 'pveum_pools',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var pool = me.pveSelNode.data.pool;
|
|
if (!pool) {
|
|
throw "no pool specified";
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
title: Ext.String.format(gettext("Resource Pool") + ': ' + pool),
|
|
hstateid: 'pooltab',
|
|
items: [
|
|
{
|
|
title: gettext('Summary'),
|
|
iconCls: 'fa fa-book',
|
|
xtype: 'pvePoolSummary',
|
|
itemId: 'summary'
|
|
},
|
|
{
|
|
title: gettext('Members'),
|
|
xtype: 'pvePoolMembers',
|
|
iconCls: 'fa fa-th',
|
|
pool: pool,
|
|
itemId: 'members'
|
|
},
|
|
{
|
|
xtype: 'pveACLView',
|
|
title: gettext('Permissions'),
|
|
iconCls: 'fa fa-unlock',
|
|
itemId: 'permissions',
|
|
path: '/pool/' + pool
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.panel.StorageBase', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
controller: 'storageEdit',
|
|
|
|
type: '',
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
if (me.isCreate) {
|
|
values.type = me.type;
|
|
} else {
|
|
delete values.storage;
|
|
}
|
|
|
|
values.disable = values.enable ? 0 : 1;
|
|
delete values.enable;
|
|
|
|
return values;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.column1.unshift({
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'storage',
|
|
value: me.storageId || '',
|
|
fieldLabel: 'ID',
|
|
vtype: 'StorageId',
|
|
allowBlank: false
|
|
});
|
|
|
|
me.column2.unshift(
|
|
{
|
|
xtype: 'pveNodeSelector',
|
|
name: 'nodes',
|
|
disabled: me.storageId === 'local',
|
|
fieldLabel: gettext('Nodes'),
|
|
emptyText: gettext('All') + ' (' + gettext('No restrictions') +')',
|
|
multiSelect: true,
|
|
autoSelect: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'enable',
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Enable')
|
|
}
|
|
);
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.BaseEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.storageId;
|
|
|
|
if (me.isCreate) {
|
|
me.url = '/api2/extjs/storage';
|
|
me.method = 'POST';
|
|
} else {
|
|
me.url = '/api2/extjs/storage/' + me.storageId;
|
|
me.method = 'PUT';
|
|
}
|
|
|
|
var ipanel = Ext.create(me.paneltype, {
|
|
type: me.type,
|
|
isCreate: me.isCreate,
|
|
storageId: me.storageId
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: PVE.Utils.format_storage_type(me.type),
|
|
isAdd: true,
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var values = response.result.data;
|
|
var ctypes = values.content || '';
|
|
|
|
values.content = ctypes.split(',');
|
|
|
|
if (values.nodes) {
|
|
values.nodes = values.nodes.split(',');
|
|
}
|
|
values.enable = values.disable ? 0 : 1;
|
|
|
|
ipanel.setValues(values);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.grid.TemplateSelector', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: 'widget.pveTemplateSelector',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-template-selector',
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var baseurl = "/nodes/" + me.nodename + "/aplinfo";
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-aplinfo',
|
|
groupField: 'section',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json' + baseurl
|
|
}
|
|
});
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{
|
|
groupHeaderTpl: '{[ "Section: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})'
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
Proxmox.Utils.monStoreErrors(me, store);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: [
|
|
'->',
|
|
gettext('Search'),
|
|
{
|
|
xtype: 'textfield',
|
|
width: 200,
|
|
enableKeyEvents: true,
|
|
listeners: {
|
|
buffer: 500,
|
|
keyup: function(field) {
|
|
var value = field.getValue().toLowerCase();
|
|
store.clearFilter(true);
|
|
store.filterBy(function(rec) {
|
|
return (rec.data['package'].toLowerCase().indexOf(value) !== -1)
|
|
|| (rec.data.headline.toLowerCase().indexOf(value) !== -1);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
],
|
|
features: [ groupingFeature ],
|
|
columns: [
|
|
{
|
|
header: gettext('Type'),
|
|
width: 80,
|
|
dataIndex: 'type'
|
|
},
|
|
{
|
|
header: gettext('Package'),
|
|
flex: 1,
|
|
dataIndex: 'package'
|
|
},
|
|
{
|
|
header: gettext('Version'),
|
|
width: 80,
|
|
dataIndex: 'version'
|
|
},
|
|
{
|
|
header: gettext('Description'),
|
|
flex: 1.5,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'headline'
|
|
}
|
|
],
|
|
listeners: {
|
|
afterRender: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
|
|
}, function() {
|
|
|
|
Ext.define('pve-aplinfo', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'template', 'type', 'package', 'version', 'headline', 'infopage',
|
|
'description', 'os', 'section'
|
|
],
|
|
idProperty: 'template'
|
|
});
|
|
|
|
});
|
|
|
|
Ext.define('PVE.storage.TemplateDownload', {
|
|
extend: 'Ext.window.Window',
|
|
alias: 'widget.pveTemplateDownload',
|
|
|
|
modal: true,
|
|
title: gettext('Templates'),
|
|
layout: 'fit',
|
|
width: 900,
|
|
height: 600,
|
|
initComponent : function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
var grid = Ext.create('PVE.grid.TemplateSelector', {
|
|
border: false,
|
|
scrollable: true,
|
|
nodename: me.nodename
|
|
});
|
|
|
|
var sm = grid.getSelectionModel();
|
|
|
|
var submitBtn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Download'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: function(button, event, rec) {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + me.nodename + '/aplinfo',
|
|
params: {
|
|
storage: me.storage,
|
|
template: rec.data.template
|
|
},
|
|
method: 'POST',
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response, options) {
|
|
var upid = response.result.data;
|
|
|
|
Ext.create('Proxmox.window.TaskViewer', {
|
|
upid: upid,
|
|
listeners: {
|
|
destroy: me.reloadGrid
|
|
}
|
|
}).show();
|
|
|
|
me.close();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: grid,
|
|
buttons: [ submitBtn ]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.Upload', {
|
|
extend: 'Ext.window.Window',
|
|
alias: 'widget.pveStorageUpload',
|
|
|
|
resizable: false,
|
|
|
|
modal: true,
|
|
|
|
initComponent : function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
var xhr;
|
|
|
|
if (!me.nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
if (!me.storage) {
|
|
throw "no storage ID specified";
|
|
}
|
|
|
|
var baseurl = "/nodes/" + me.nodename + "/storage/" + me.storage + "/upload";
|
|
|
|
var pbar = Ext.create('Ext.ProgressBar', {
|
|
text: 'Ready',
|
|
hidden: true
|
|
});
|
|
|
|
me.formPanel = Ext.create('Ext.form.Panel', {
|
|
method: 'POST',
|
|
waitMsgTarget: true,
|
|
bodyPadding: 10,
|
|
border: false,
|
|
width: 300,
|
|
fieldDefaults: {
|
|
labelWidth: 100,
|
|
anchor: '100%'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'pveContentTypeSelector',
|
|
cts: me.contents,
|
|
fieldLabel: gettext('Content'),
|
|
name: 'content',
|
|
value: me.contents[0] || '',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'filefield',
|
|
name: 'filename',
|
|
buttonText: gettext('Select File...'),
|
|
allowBlank: false
|
|
},
|
|
pbar
|
|
]
|
|
});
|
|
|
|
var form = me.formPanel.getForm();
|
|
|
|
var doStandardSubmit = function() {
|
|
form.submit({
|
|
url: "/api2/htmljs" + baseurl,
|
|
waitMsg: gettext('Uploading file...'),
|
|
success: function(f, action) {
|
|
me.close();
|
|
},
|
|
failure: function(f, action) {
|
|
var msg = PVE.Utils.extractFormActionError(action);
|
|
Ext.Msg.alert(gettext('Error'), msg);
|
|
}
|
|
});
|
|
};
|
|
|
|
var updateProgress = function(per, bytes) {
|
|
var text = (per * 100).toFixed(2) + '%';
|
|
if (bytes) {
|
|
text += " (" + Proxmox.Utils.format_size(bytes) + ')';
|
|
}
|
|
pbar.updateProgress(per, text);
|
|
};
|
|
|
|
var abortBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Abort'),
|
|
disabled: true,
|
|
handler: function() {
|
|
me.close();
|
|
}
|
|
});
|
|
|
|
var submitBtn = Ext.create('Ext.Button', {
|
|
text: gettext('Upload'),
|
|
disabled: true,
|
|
handler: function(button) {
|
|
var fd;
|
|
try {
|
|
fd = new FormData();
|
|
} catch (err) {
|
|
doStandardSubmit();
|
|
return;
|
|
}
|
|
|
|
button.setDisabled(true);
|
|
abortBtn.setDisabled(false);
|
|
|
|
var field = form.findField('content');
|
|
fd.append("content", field.getValue());
|
|
field.setDisabled(true);
|
|
|
|
field = form.findField('filename');
|
|
var file = field.fileInputEl.dom;
|
|
fd.append("filename", file.files[0]);
|
|
field.setDisabled(true);
|
|
|
|
pbar.setVisible(true);
|
|
updateProgress(0);
|
|
|
|
xhr = new XMLHttpRequest();
|
|
|
|
xhr.addEventListener("load", function(e) {
|
|
if (xhr.status == 200) {
|
|
me.close();
|
|
} else {
|
|
var msg = gettext('Error') + " " + xhr.status.toString() + ": " + Ext.htmlEncode(xhr.statusText);
|
|
if (xhr.responseText !== "") {
|
|
var result = Ext.decode(xhr.responseText);
|
|
result.message = msg;
|
|
msg = Proxmox.Utils.extractRequestError(result, true);
|
|
}
|
|
Ext.Msg.alert(gettext('Error'), msg, function(btn) {
|
|
me.close();
|
|
});
|
|
}
|
|
}, false);
|
|
|
|
xhr.addEventListener("error", function(e) {
|
|
var msg = "Error " + e.target.status.toString() + " occurred while receiving the document.";
|
|
Ext.Msg.alert(gettext('Error'), msg, function(btn) {
|
|
me.close();
|
|
});
|
|
});
|
|
|
|
xhr.upload.addEventListener("progress", function(evt) {
|
|
if (evt.lengthComputable) {
|
|
var percentComplete = evt.loaded / evt.total;
|
|
updateProgress(percentComplete, evt.loaded);
|
|
}
|
|
}, false);
|
|
|
|
xhr.open("POST", "/api2/json" + baseurl, true);
|
|
xhr.send(fd);
|
|
}
|
|
});
|
|
|
|
form.on('validitychange', function(f, valid) {
|
|
submitBtn.setDisabled(!valid);
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
title: gettext('Upload'),
|
|
items: me.formPanel,
|
|
buttons: [ abortBtn, submitBtn ],
|
|
listeners: {
|
|
close: function() {
|
|
if (xhr) {
|
|
xhr.abort();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.ContentView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: 'widget.pveStorageContentView',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-storage-content',
|
|
viewConfig: {
|
|
trackOver: false,
|
|
loadMask: false
|
|
},
|
|
features: [
|
|
{
|
|
ftype: 'grouping',
|
|
groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})'
|
|
}
|
|
],
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var storage = me.pveSelNode.data.storage;
|
|
if (!storage) {
|
|
throw "no storage ID specified";
|
|
}
|
|
|
|
var baseurl = "/nodes/" + nodename + "/storage/" + storage + "/content";
|
|
var store = Ext.create('Ext.data.Store',{
|
|
model: 'pve-storage-content',
|
|
groupField: 'content',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json' + baseurl
|
|
},
|
|
sorters: {
|
|
property: 'volid',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
me.statusStore.load();
|
|
};
|
|
|
|
Proxmox.Utils.monStoreErrors(me, store);
|
|
|
|
var templateButton = Ext.create('Proxmox.button.Button',{
|
|
itemId: 'tmpl-btn',
|
|
text: gettext('Templates'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.storage.TemplateDownload', {
|
|
nodename: nodename,
|
|
storage: storage,
|
|
reloadGrid: reload
|
|
});
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
var uploadButton = Ext.create('Proxmox.button.Button', {
|
|
contents : ['iso','vztmpl'],
|
|
text: gettext('Upload'),
|
|
handler: function() {
|
|
var me = this;
|
|
var win = Ext.create('PVE.storage.Upload', {
|
|
nodename: nodename,
|
|
storage: storage,
|
|
contents: me.contents
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
}
|
|
});
|
|
|
|
var imageRemoveButton;
|
|
var removeButton = Ext.create('Proxmox.button.StdRemoveButton',{
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
if (rec && rec.data.content !== 'images') {
|
|
imageRemoveButton.setVisible(false);
|
|
removeButton.setVisible(true);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
callback: function() {
|
|
reload();
|
|
},
|
|
baseurl: baseurl + '/'
|
|
});
|
|
|
|
imageRemoveButton = Ext.create('Proxmox.button.Button',{
|
|
selModel: sm,
|
|
hidden: true,
|
|
text: gettext('Remove'),
|
|
enableFn: function(rec) {
|
|
if (rec && rec.data.content === 'images') {
|
|
removeButton.setVisible(false);
|
|
imageRemoveButton.setVisible(true);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
handler: function(btn, event, rec) {
|
|
me = this;
|
|
|
|
var url = baseurl + '/' + rec.data.volid;
|
|
var vmid = rec.data.vmid;
|
|
|
|
var store = PVE.data.ResourceStore;
|
|
|
|
if (vmid && store.findVMID(vmid)) {
|
|
var guest_node = store.guestNode(vmid);
|
|
var storage_path = 'storage/' + nodename + '/' + storage;
|
|
|
|
// allow to delete local backed images if a VMID exists on another node.
|
|
if (store.storageIsShared(storage_path) || guest_node == nodename) {
|
|
var msg = Ext.String.format(
|
|
gettext("Cannot remove image, a guest with VMID '{0}' exists!"), vmid);
|
|
msg += '<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('PVE.window.SafeDestroy', {
|
|
title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid),
|
|
showProgress: true,
|
|
url: url,
|
|
item: { type: 'Image', id: vmid }
|
|
}).show();
|
|
win.on('destroy', function() {
|
|
me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
|
|
url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status'
|
|
});
|
|
reload();
|
|
|
|
});
|
|
}
|
|
});
|
|
|
|
me.statusStore = Ext.create('Proxmox.data.ObjectStore', {
|
|
url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status'
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: [
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Restore'),
|
|
selModel: sm,
|
|
disabled: true,
|
|
enableFn: function(rec) {
|
|
return rec && rec.data.content === 'backup';
|
|
},
|
|
handler: function(b, e, rec) {
|
|
var vmtype;
|
|
if (rec.data.volid.match(/vzdump-qemu-/)) {
|
|
vmtype = 'qemu';
|
|
} else if (rec.data.volid.match(/vzdump-openvz-/) || rec.data.volid.match(/vzdump-lxc-/)) {
|
|
vmtype = 'lxc';
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.window.Restore', {
|
|
nodename: nodename,
|
|
volid: rec.data.volid,
|
|
volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
|
|
vmtype: vmtype
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
}
|
|
},
|
|
removeButton,
|
|
imageRemoveButton,
|
|
templateButton,
|
|
uploadButton,
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Show Configuration'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
return rec && rec.data.content === 'backup';
|
|
},
|
|
handler: function(b,e,rec) {
|
|
var win = Ext.create('PVE.window.BackupConfig', {
|
|
volume: rec.data.volid,
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
|
|
win.show();
|
|
}
|
|
},
|
|
'->',
|
|
gettext('Search') + ':', ' ',
|
|
{
|
|
xtype: 'textfield',
|
|
width: 200,
|
|
enableKeyEvents: true,
|
|
listeners: {
|
|
buffer: 500,
|
|
keyup: function(field) {
|
|
store.clearFilter(true);
|
|
store.filter([
|
|
{
|
|
property: 'text',
|
|
value: field.getValue(),
|
|
anyMatch: true,
|
|
caseSensitive: false
|
|
}
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
],
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
flex: 1,
|
|
sortable: true,
|
|
renderer: PVE.Utils.render_storage_content,
|
|
dataIndex: 'text'
|
|
},
|
|
{
|
|
header: gettext('Format'),
|
|
width: 100,
|
|
dataIndex: 'format'
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
width: 100,
|
|
dataIndex: 'content',
|
|
renderer: PVE.Utils.format_content_types
|
|
},
|
|
{
|
|
header: gettext('Size'),
|
|
width: 100,
|
|
renderer: Proxmox.Utils.format_size,
|
|
dataIndex: 'size'
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
// disable the buttons/restrict the upload window
|
|
// if templates or uploads are not allowed
|
|
me.mon(me.statusStore, 'load', function(s, records, success) {
|
|
var availcontent = [];
|
|
Ext.Array.each(records, function(item){
|
|
if (item.id === 'content') {
|
|
availcontent = item.data.value.split(',');
|
|
}
|
|
});
|
|
var templ = false;
|
|
var upload = false;
|
|
var cts = [];
|
|
|
|
Ext.Array.each(availcontent, function(content) {
|
|
if (content === 'vztmpl') {
|
|
templ = true;
|
|
cts.push('vztmpl');
|
|
} else if (content === 'iso') {
|
|
upload = true;
|
|
cts.push('iso');
|
|
}
|
|
});
|
|
|
|
if (templ !== upload) {
|
|
uploadButton.contents = cts;
|
|
}
|
|
|
|
templateButton.setDisabled(!templ);
|
|
uploadButton.setDisabled(!upload && !templ);
|
|
});
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-storage-content', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'volid', 'content', 'format', 'size', 'used', 'vmid',
|
|
'channel', 'id', 'lun',
|
|
{
|
|
name: 'text',
|
|
convert: function(value, record) {
|
|
// check for volid, because if you click on a grouping header,
|
|
// it calls convert (but with an empty volid)
|
|
if (value || record.data.volid === null) {
|
|
return value;
|
|
}
|
|
return PVE.Utils.render_storage_content(value, {}, record);
|
|
}
|
|
}
|
|
],
|
|
idProperty: 'volid'
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.storage.StatusView', {
|
|
extend: 'PVE.panel.StatusView',
|
|
alias: 'widget.pveStorageStatusView',
|
|
|
|
height: 230,
|
|
title: gettext('Status'),
|
|
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
|
|
defaults: {
|
|
xtype: 'pveInfoWidget',
|
|
padding: '0 30 5 30'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'box',
|
|
height: 30
|
|
},
|
|
{
|
|
itemId: 'enabled',
|
|
title: gettext('Enabled'),
|
|
printBar: false,
|
|
textField: 'disabled',
|
|
renderer: Proxmox.Utils.format_neg_boolean
|
|
},
|
|
{
|
|
itemId: 'active',
|
|
title: gettext('Active'),
|
|
printBar: false,
|
|
textField: 'active',
|
|
renderer: Proxmox.Utils.format_boolean
|
|
},
|
|
{
|
|
itemId: 'content',
|
|
title: gettext('Content'),
|
|
printBar: false,
|
|
textField: 'content',
|
|
renderer: PVE.Utils.format_content_types
|
|
},
|
|
{
|
|
itemId: 'type',
|
|
title: gettext('Type'),
|
|
printBar: false,
|
|
textField: 'type',
|
|
renderer: PVE.Utils.format_storage_type
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
height: 10
|
|
},
|
|
{
|
|
itemId: 'usage',
|
|
title: gettext('Usage'),
|
|
valueField: 'used',
|
|
maxField: 'total'
|
|
}
|
|
],
|
|
|
|
updateTitle: function() {
|
|
return;
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.Summary', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveStorageSummary',
|
|
scrollable: true,
|
|
bodyPadding: 5,
|
|
tbar: [
|
|
'->',
|
|
{
|
|
xtype: 'proxmoxRRDTypeSelector'
|
|
}
|
|
],
|
|
layout: {
|
|
type: 'column'
|
|
},
|
|
defaults: {
|
|
padding: 5,
|
|
columnWidth: 1
|
|
},
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var storage = me.pveSelNode.data.storage;
|
|
if (!storage) {
|
|
throw "no storage ID specified";
|
|
}
|
|
|
|
var rstore = Ext.create('Proxmox.data.ObjectStore', {
|
|
url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status",
|
|
interval: 1000
|
|
});
|
|
|
|
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
|
|
rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata",
|
|
model: 'pve-rrd-storage'
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: [
|
|
{
|
|
xtype: 'pveStorageStatusView',
|
|
pveSelNode: me.pveSelNode,
|
|
rstore: rstore
|
|
},
|
|
{
|
|
xtype: 'proxmoxRRDChart',
|
|
title: gettext('Usage'),
|
|
fields: ['total','used'],
|
|
fieldTitles: ['Total Size', 'Used Size'],
|
|
store: rrdstore
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); },
|
|
destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); }
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.Browser', {
|
|
extend: 'PVE.panel.Config',
|
|
alias: 'widget.PVE.storage.Browser',
|
|
|
|
onlineHelp: 'chapter_storage',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var nodename = me.pveSelNode.data.node;
|
|
if (!nodename) {
|
|
throw "no node name specified";
|
|
}
|
|
|
|
var storeid = me.pveSelNode.data.storage;
|
|
if (!storeid) {
|
|
throw "no storage ID specified";
|
|
}
|
|
|
|
|
|
me.items = [
|
|
{
|
|
title: gettext('Summary'),
|
|
xtype: 'pveStorageSummary',
|
|
iconCls: 'fa fa-book',
|
|
itemId: 'summary'
|
|
}
|
|
];
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
Ext.apply(me, {
|
|
title: Ext.String.format(gettext("Storage {0} on node {1}"),
|
|
"'" + storeid + "'", "'" + nodename + "'"),
|
|
hstateid: 'storagetab'
|
|
});
|
|
|
|
if (caps.storage['Datastore.Allocate'] ||
|
|
caps.storage['Datastore.AllocateSpace'] ||
|
|
caps.storage['Datastore.Audit']) {
|
|
me.items.push({
|
|
xtype: 'pveStorageContentView',
|
|
title: gettext('Content'),
|
|
iconCls: 'fa fa-th',
|
|
itemId: 'content'
|
|
});
|
|
}
|
|
|
|
if (caps.storage['Permissions.Modify']) {
|
|
me.items.push({
|
|
xtype: 'pveACLView',
|
|
title: gettext('Permissions'),
|
|
iconCls: 'fa fa-unlock',
|
|
itemId: 'permissions',
|
|
path: '/storage/' + storeid
|
|
});
|
|
}
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.DirInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
onlineHelp: 'storage_directory',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'path',
|
|
value: '',
|
|
fieldLabel: gettext('Directory'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'pveContentTypeSelector',
|
|
name: 'content',
|
|
value: 'images',
|
|
multiSelect: true,
|
|
fieldLabel: gettext('Content'),
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'shared',
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Shared')
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Max Backups'),
|
|
disabled: true,
|
|
name: 'maxfiles',
|
|
reference: 'maxfiles',
|
|
minValue: 0,
|
|
maxValue: 365,
|
|
value: me.isCreate ? '1' : undefined,
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.storage.NFSScan', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveNFSScan',
|
|
|
|
queryParam: 'server',
|
|
|
|
valueField: 'path',
|
|
displayField: 'path',
|
|
matchFieldWidth: false,
|
|
listConfig: {
|
|
loadingText: gettext('Scanning...'),
|
|
width: 350
|
|
},
|
|
doRawQuery: function() {
|
|
},
|
|
|
|
onTriggerClick: function() {
|
|
var me = this;
|
|
|
|
if (!me.queryCaching || me.lastQuery !== me.nfsServer) {
|
|
me.store.removeAll();
|
|
}
|
|
|
|
me.allQuery = me.nfsServer;
|
|
|
|
me.callParent();
|
|
},
|
|
|
|
setServer: function(server) {
|
|
var me = this;
|
|
|
|
me.nfsServer = server;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: [ 'path', 'options' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/scan/nfs'
|
|
}
|
|
});
|
|
|
|
store.sort('path', 'ASC');
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.NFSInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
onlineHelp: 'storage_nfs',
|
|
|
|
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) {
|
|
var res = 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.column2 = [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Max Backups'),
|
|
disabled: true,
|
|
name: 'maxfiles',
|
|
reference: 'maxfiles',
|
|
minValue: 0,
|
|
maxValue: 365,
|
|
value: me.isCreate ? '1' : undefined,
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.advancedColumn1 = [
|
|
{
|
|
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();
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.CIFSScan', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveCIFSScan',
|
|
|
|
queryParam: 'server',
|
|
|
|
valueField: 'share',
|
|
displayField: 'share',
|
|
matchFieldWidth: false,
|
|
listConfig: {
|
|
loadingText: gettext('Scanning...'),
|
|
width: 350
|
|
},
|
|
doRawQuery: function() {
|
|
},
|
|
|
|
onTriggerClick: function() {
|
|
var me = this;
|
|
|
|
if (!me.queryCaching || me.lastQuery !== me.cifsServer) {
|
|
me.store.removeAll();
|
|
}
|
|
|
|
var params = {};
|
|
if (me.cifsUsername && me.cifsPassword) {
|
|
params.username = me.cifsUsername;
|
|
params.password = me.cifsPassword;
|
|
}
|
|
|
|
if (me.cifsDomain) {
|
|
params.domain = me.cifsDomain;
|
|
}
|
|
|
|
me.store.getProxy().setExtraParams(params);
|
|
me.allQuery = me.cifsServer;
|
|
|
|
me.callParent();
|
|
},
|
|
|
|
setServer: function(server) {
|
|
this.cifsServer = server;
|
|
},
|
|
|
|
setUsername: function(username) {
|
|
this.cifsUsername = username;
|
|
},
|
|
|
|
setPassword: function(password) {
|
|
this.cifsPassword = password;
|
|
},
|
|
|
|
setDomain: function(domain) {
|
|
this.cifsDomain = domain;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: ['description', 'share'],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/scan/cifs'
|
|
}
|
|
});
|
|
store.sort('share', 'ASC');
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.CIFSInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
onlineHelp: 'storage_cifs',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var passwordfield = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
|
|
inputType: 'password',
|
|
name: 'password',
|
|
value: me.isCreate ? '' : '********',
|
|
fieldLabel: gettext('Password'),
|
|
allowBlank: false,
|
|
disabled: me.isCreate,
|
|
minLength: 1,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
|
|
if (me.isCreate) {
|
|
var exportField = me.down('field[name=share]');
|
|
exportField.setPassword(value);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'server',
|
|
value: '',
|
|
fieldLabel: gettext('Server'),
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (me.isCreate) {
|
|
var exportField = me.down('field[name=share]');
|
|
exportField.setServer(value);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'username',
|
|
value: '',
|
|
fieldLabel: gettext('Username'),
|
|
emptyText: gettext('Guest user'),
|
|
allowBlank: true,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (!me.isCreate) {
|
|
return;
|
|
}
|
|
var exportField = me.down('field[name=share]');
|
|
exportField.setUsername(value);
|
|
|
|
if (value == "") {
|
|
passwordfield.disable();
|
|
} else {
|
|
passwordfield.enable();
|
|
}
|
|
passwordfield.validate();
|
|
}
|
|
}
|
|
},
|
|
passwordfield,
|
|
{
|
|
xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield',
|
|
name: 'share',
|
|
value: '',
|
|
fieldLabel: 'Share',
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Max Backups'),
|
|
name: 'maxfiles',
|
|
reference: 'maxfiles',
|
|
minValue: 0,
|
|
maxValue: 365,
|
|
value: me.isCreate ? '1' : undefined,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'pveContentTypeSelector',
|
|
name: 'content',
|
|
value: 'images',
|
|
multiSelect: true,
|
|
fieldLabel: gettext('Content'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'domain',
|
|
value: me.isCreate ? '' : undefined,
|
|
fieldLabel: gettext('Domain'),
|
|
allowBlank: true,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (me.isCreate) {
|
|
|
|
var exportField = me.down('field[name=share]');
|
|
exportField.setDomain(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.GlusterFsScan', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveGlusterFsScan',
|
|
|
|
queryParam: 'server',
|
|
|
|
valueField: 'volname',
|
|
displayField: 'volname',
|
|
matchFieldWidth: false,
|
|
listConfig: {
|
|
loadingText: 'Scanning...',
|
|
width: 350
|
|
},
|
|
doRawQuery: function() {
|
|
},
|
|
|
|
onTriggerClick: function() {
|
|
var me = this;
|
|
|
|
if (!me.queryCaching || me.lastQuery !== me.glusterServer) {
|
|
me.store.removeAll();
|
|
}
|
|
|
|
me.allQuery = me.glusterServer;
|
|
|
|
me.callParent();
|
|
},
|
|
|
|
setServer: function(server) {
|
|
var me = this;
|
|
|
|
me.glusterServer = server;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: [ 'volname' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs'
|
|
}
|
|
});
|
|
|
|
store.sort('volname', 'ASC');
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.GlusterFsInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
onlineHelp: 'storage_glusterfs',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'server',
|
|
value: '',
|
|
fieldLabel: gettext('Server'),
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (me.isCreate) {
|
|
var volumeField = me.down('field[name=volume]');
|
|
volumeField.setServer(value);
|
|
volumeField.setValue('');
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
|
|
name: 'server2',
|
|
value: '',
|
|
fieldLabel: gettext('Second Server'),
|
|
allowBlank: true
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'pveGlusterFsScan' : 'displayfield',
|
|
name: 'volume',
|
|
value: '',
|
|
fieldLabel: 'Volume name',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'pveContentTypeSelector',
|
|
cts: ['images', 'iso', 'backup', 'vztmpl', 'snippets'],
|
|
name: 'content',
|
|
value: 'images',
|
|
multiSelect: true,
|
|
fieldLabel: gettext('Content'),
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Max Backups'),
|
|
disabled: true,
|
|
name: 'maxfiles',
|
|
reference: 'maxfiles',
|
|
minValue: 0,
|
|
maxValue: 365,
|
|
value: me.isCreate ? '1' : undefined,
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.IScsiScan', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveIScsiScan',
|
|
|
|
queryParam: 'portal',
|
|
valueField: 'target',
|
|
displayField: 'target',
|
|
matchFieldWidth: false,
|
|
listConfig: {
|
|
loadingText: gettext('Scanning...'),
|
|
width: 350
|
|
},
|
|
doRawQuery: function() {
|
|
},
|
|
|
|
onTriggerClick: function() {
|
|
var me = this;
|
|
|
|
if (!me.queryCaching || me.lastQuery !== me.portal) {
|
|
me.store.removeAll();
|
|
}
|
|
|
|
me.allQuery = me.portal;
|
|
|
|
me.callParent();
|
|
},
|
|
|
|
setPortal: function(portal) {
|
|
var me = this;
|
|
|
|
me.portal = portal;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: [ 'target', 'portal' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/scan/iscsi'
|
|
}
|
|
});
|
|
|
|
store.sort('target', 'ASC');
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.IScsiInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
onlineHelp: 'storage_open_iscsi',
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
values.content = values.luns ? 'images' : 'none';
|
|
delete values.luns;
|
|
|
|
return me.callParent([values]);
|
|
},
|
|
|
|
setValues: function(values) {
|
|
values.luns = (values.content.indexOf('images') !== -1) ? true : false;
|
|
this.callParent([values]);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'portal',
|
|
value: '',
|
|
fieldLabel: 'Portal',
|
|
allowBlank: false,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (me.isCreate) {
|
|
var exportField = me.down('field[name=target]');
|
|
exportField.setPortal(value);
|
|
exportField.setValue('');
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
readOnly: !me.isCreate,
|
|
xtype: me.isCreate ? 'pveIScsiScan' : 'displayfield',
|
|
name: 'target',
|
|
value: '',
|
|
fieldLabel: 'Target',
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'checkbox',
|
|
name: 'luns',
|
|
checked: true,
|
|
fieldLabel: gettext('Use LUNs directly')
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.VgSelector', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveVgSelector',
|
|
valueField: 'vg',
|
|
displayField: 'vg',
|
|
queryMode: 'local',
|
|
editable: false,
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
autoLoad: {}, // true,
|
|
fields: [ 'vg', 'size', 'free' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/scan/lvm'
|
|
}
|
|
});
|
|
|
|
store.sort('vg', 'ASC');
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
loadingText: gettext('Scanning...')
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.BaseStorageSelector', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveBaseStorageSelector',
|
|
|
|
existingGroupsText: gettext("Existing volume groups"),
|
|
queryMode: 'local',
|
|
editable: false,
|
|
value: '',
|
|
valueField: 'storage',
|
|
displayField: 'text',
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
autoLoad: {
|
|
addRecords: true,
|
|
params: {
|
|
type: 'iscsi'
|
|
}
|
|
},
|
|
fields: [ 'storage', 'type', 'content',
|
|
{
|
|
name: 'text',
|
|
convert: function(value, record) {
|
|
if (record.data.storage) {
|
|
return record.data.storage + " (iSCSI)";
|
|
} else {
|
|
return me.existingGroupsText;
|
|
}
|
|
}
|
|
}],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/storage/'
|
|
}
|
|
});
|
|
|
|
store.loadData([{ storage: '' }], true);
|
|
|
|
store.sort('storage', 'ASC');
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.LVMInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
onlineHelp: 'storage_lvm',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.column1 = [];
|
|
|
|
var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
|
|
name: 'vgname',
|
|
hidden: !!me.isCreate,
|
|
disabled: !!me.isCreate,
|
|
value: '',
|
|
fieldLabel: gettext('Volume group'),
|
|
allowBlank: false
|
|
});
|
|
|
|
if (me.isCreate) {
|
|
var vgField = Ext.create('PVE.storage.VgSelector', {
|
|
name: 'vgname',
|
|
fieldLabel: gettext('Volume group'),
|
|
allowBlank: false
|
|
});
|
|
|
|
var baseField = Ext.createWidget('pveFileSelector', {
|
|
name: 'base',
|
|
hidden: true,
|
|
disabled: true,
|
|
nodename: 'localhost',
|
|
storageContent: 'images',
|
|
fieldLabel: gettext('Base volume'),
|
|
allowBlank: false
|
|
});
|
|
|
|
me.column1.push({
|
|
xtype: 'pveBaseStorageSelector',
|
|
name: 'basesel',
|
|
fieldLabel: gettext('Base storage'),
|
|
submitValue: false,
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (value) {
|
|
vgnameField.setVisible(true);
|
|
vgnameField.setDisabled(false);
|
|
vgField.setVisible(false);
|
|
vgField.setDisabled(true);
|
|
baseField.setVisible(true);
|
|
baseField.setDisabled(false);
|
|
} else {
|
|
vgnameField.setVisible(false);
|
|
vgnameField.setDisabled(true);
|
|
vgField.setVisible(true);
|
|
vgField.setDisabled(false);
|
|
baseField.setVisible(false);
|
|
baseField.setDisabled(true);
|
|
}
|
|
baseField.setStorage(value);
|
|
}
|
|
}
|
|
});
|
|
|
|
me.column1.push(baseField);
|
|
|
|
me.column1.push(vgField);
|
|
}
|
|
|
|
me.column1.push(vgnameField);
|
|
|
|
// here value is an array,
|
|
// while before it was a string
|
|
/*jslint confusion: true*/
|
|
me.column1.push({
|
|
xtype: 'pveContentTypeSelector',
|
|
cts: ['images', 'rootdir'],
|
|
fieldLabel: gettext('Content'),
|
|
name: 'content',
|
|
value: ['images', 'rootdir'],
|
|
multiSelect: true,
|
|
allowBlank: false
|
|
});
|
|
/*jslint confusion: false*/
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'shared',
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Shared')
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.TPoolSelector', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveTPSelector',
|
|
|
|
queryParam: 'vg',
|
|
valueField: 'lv',
|
|
displayField: 'lv',
|
|
editable: false,
|
|
|
|
doRawQuery: function() {
|
|
},
|
|
|
|
onTriggerClick: function() {
|
|
var me = this;
|
|
|
|
if (!me.queryCaching || me.lastQuery !== me.vg) {
|
|
me.store.removeAll();
|
|
}
|
|
|
|
me.allQuery = me.vg;
|
|
|
|
me.callParent();
|
|
},
|
|
|
|
setVG: function(myvg) {
|
|
var me = this;
|
|
|
|
me.vg = myvg;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: [ 'lv' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/scan/lvmthin'
|
|
}
|
|
});
|
|
|
|
store.sort('lv', 'ASC');
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
loadingText: gettext('Scanning...')
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.BaseVGSelector', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveBaseVGSelector',
|
|
|
|
valueField: 'vg',
|
|
displayField: 'vg',
|
|
queryMode: 'local',
|
|
editable: false,
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
autoLoad: {},
|
|
fields: [ 'vg', 'size', 'free'],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/scan/lvm'
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
listConfig: {
|
|
loadingText: gettext('Scanning...')
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.LvmThinInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
onlineHelp: 'storage_lvmthin',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.column1 = [];
|
|
|
|
var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
|
|
name: 'vgname',
|
|
hidden: !!me.isCreate,
|
|
disabled: !!me.isCreate,
|
|
value: '',
|
|
fieldLabel: gettext('Volume group'),
|
|
allowBlank: false
|
|
});
|
|
|
|
var thinpoolField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', {
|
|
name: 'thinpool',
|
|
hidden: !!me.isCreate,
|
|
disabled: !!me.isCreate,
|
|
value: '',
|
|
fieldLabel: gettext('Thin Pool'),
|
|
allowBlank: false
|
|
});
|
|
|
|
if (me.isCreate) {
|
|
var vgField = Ext.create('PVE.storage.TPoolSelector', {
|
|
name: 'thinpool',
|
|
fieldLabel: gettext('Thin Pool'),
|
|
allowBlank: false
|
|
});
|
|
|
|
me.column1.push({
|
|
xtype: 'pveBaseVGSelector',
|
|
name: 'vgname',
|
|
fieldLabel: gettext('Volume group'),
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (me.isCreate) {
|
|
vgField.setVG(value);
|
|
vgField.setValue('');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
me.column1.push(vgField);
|
|
}
|
|
|
|
me.column1.push(vgnameField);
|
|
|
|
me.column1.push(thinpoolField);
|
|
|
|
// here value is an array,
|
|
// while before it was a string
|
|
/*jslint confusion: true*/
|
|
me.column1.push({
|
|
xtype: 'pveContentTypeSelector',
|
|
cts: ['images', 'rootdir'],
|
|
fieldLabel: gettext('Content'),
|
|
name: 'content',
|
|
value: ['images', 'rootdir'],
|
|
multiSelect: true,
|
|
allowBlank: false
|
|
});
|
|
/*jslint confusion: false*/
|
|
|
|
me.column2 = [];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.storage.CephFSInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
controller: 'cephstorage',
|
|
|
|
onlineHelp: 'storage_cephfs',
|
|
|
|
viewModel: {
|
|
type: 'cephstorage'
|
|
},
|
|
|
|
setValues: function(values) {
|
|
if (values.monhost) {
|
|
this.viewModel.set('pveceph', false);
|
|
this.lookupReference('pvecephRef').setValue(false);
|
|
this.lookupReference('pvecephRef').resetOriginalValue();
|
|
}
|
|
this.callParent([values]);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
me.type = 'cephfs';
|
|
|
|
me.column1 = [];
|
|
|
|
me.column1.push(
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'monhost',
|
|
vtype: 'HostList',
|
|
value: '',
|
|
bind: {
|
|
disabled: '{pveceph}',
|
|
submitValue: '{!pveceph}',
|
|
hidden: '{pveceph}'
|
|
},
|
|
fieldLabel: 'Monitor(s)',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'monhost',
|
|
bind: {
|
|
disabled: '{!pveceph}',
|
|
hidden: '{!pveceph}'
|
|
},
|
|
value: '',
|
|
fieldLabel: 'Monitor(s)'
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'username',
|
|
value: 'admin',
|
|
bind: {
|
|
disabled: '{pveceph}',
|
|
submitValue: '{!pveceph}'
|
|
},
|
|
fieldLabel: gettext('User name'),
|
|
allowBlank: true
|
|
}
|
|
);
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'pveContentTypeSelector',
|
|
cts: ['backup', 'iso', 'vztmpl', 'snippets'],
|
|
fieldLabel: gettext('Content'),
|
|
name: 'content',
|
|
value: 'backup',
|
|
multiSelect: true,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
fieldLabel: gettext('Max Backups'),
|
|
name: 'maxfiles',
|
|
reference: 'maxfiles',
|
|
minValue: 0,
|
|
maxValue: 365,
|
|
value: me.isCreate ? '1' : undefined,
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
me.columnB = [{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'pveceph',
|
|
reference: 'pvecephRef',
|
|
bind : {
|
|
disabled: '{!pvecephPossible}',
|
|
value: '{pveceph}'
|
|
},
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
submitValue: false,
|
|
hidden: !me.isCreate,
|
|
boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS')
|
|
}];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.storage.Ceph.Model', {
|
|
extend: 'Ext.app.ViewModel',
|
|
alias: 'viewmodel.cephstorage',
|
|
|
|
data: {
|
|
pveceph: true,
|
|
pvecephPossible: true
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.Ceph.Controller', {
|
|
extend: 'PVE.controller.StorageEdit',
|
|
alias: 'controller.cephstorage',
|
|
|
|
control: {
|
|
'#': {
|
|
afterrender: 'queryMonitors'
|
|
},
|
|
'textfield[name=username]': {
|
|
disable: 'resetField'
|
|
},
|
|
'displayfield[name=monhost]': {
|
|
enable: 'queryMonitors'
|
|
},
|
|
'textfield[name=monhost]': {
|
|
disable: 'resetField',
|
|
enable: 'resetField'
|
|
}
|
|
},
|
|
resetField: function(field) {
|
|
field.reset();
|
|
},
|
|
queryMonitors: function(field, newVal, oldVal) {
|
|
// we get called with two signatures, the above one for a field
|
|
// change event and the afterrender from the view, this check only
|
|
// can be true for the field change one and omit the API request if
|
|
// pveceph got unchecked - as it's not needed there.
|
|
if (field && !newVal && oldVal) {
|
|
return;
|
|
}
|
|
var view = this.getView();
|
|
var vm = this.getViewModel();
|
|
if (!(view.isCreate || vm.get('pveceph'))) {
|
|
return; // only query on create or if editing a pveceph store
|
|
}
|
|
|
|
var monhostField = this.lookupReference('monhost');
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/json/nodes/localhost/ceph/mon',
|
|
method: 'GET',
|
|
scope: this,
|
|
callback: function(options, success, response) {
|
|
var data = response.result.data;
|
|
if (response.status === 200) {
|
|
if (data.length > 0) {
|
|
var monhost = Ext.Array.pluck(data, 'name').sort().join(',');
|
|
monhostField.setValue(monhost);
|
|
monhostField.resetOriginalValue();
|
|
if (view.isCreate) {
|
|
vm.set('pvecephPossible', true);
|
|
}
|
|
} else {
|
|
vm.set('pveceph', false);
|
|
}
|
|
} else {
|
|
vm.set('pveceph', false);
|
|
vm.set('pvecephPossible', false);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.RBDInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
controller: 'cephstorage',
|
|
|
|
onlineHelp: 'ceph_rados_block_devices',
|
|
|
|
viewModel: {
|
|
type: 'cephstorage'
|
|
},
|
|
|
|
setValues: function(values) {
|
|
if (values.monhost) {
|
|
this.viewModel.set('pveceph', false);
|
|
this.lookupReference('pvecephRef').setValue(false);
|
|
this.lookupReference('pvecephRef').resetOriginalValue();
|
|
}
|
|
this.callParent([values]);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
me.type = 'rbd';
|
|
|
|
me.column1 = [];
|
|
|
|
if (me.isCreate) {
|
|
me.column1.push({
|
|
xtype: 'pveCephPoolSelector',
|
|
nodename: me.nodename,
|
|
name: 'pool',
|
|
bind: {
|
|
disabled: '{!pveceph}',
|
|
submitValue: '{pveceph}',
|
|
hidden: '{!pveceph}'
|
|
},
|
|
fieldLabel: gettext('Pool'),
|
|
allowBlank: false
|
|
},{
|
|
xtype: 'textfield',
|
|
name: 'pool',
|
|
value: 'rbd',
|
|
bind: {
|
|
disabled: '{pveceph}',
|
|
submitValue: '{!pveceph}',
|
|
hidden: '{pveceph}'
|
|
},
|
|
fieldLabel: gettext('Pool'),
|
|
allowBlank: false
|
|
});
|
|
} else {
|
|
me.column1.push({
|
|
xtype: 'displayfield',
|
|
nodename: me.nodename,
|
|
name: 'pool',
|
|
fieldLabel: gettext('Pool'),
|
|
allowBlank: false
|
|
});
|
|
}
|
|
|
|
me.column1.push(
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'monhost',
|
|
vtype: 'HostList',
|
|
bind: {
|
|
disabled: '{pveceph}',
|
|
submitValue: '{!pveceph}',
|
|
hidden: '{pveceph}'
|
|
},
|
|
value: '',
|
|
fieldLabel: 'Monitor(s)',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
reference: 'monhost',
|
|
bind: {
|
|
disabled: '{!pveceph}',
|
|
hidden: '{!pveceph}'
|
|
},
|
|
value: '',
|
|
fieldLabel: 'Monitor(s)'
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'username',
|
|
bind: {
|
|
disabled: '{pveceph}',
|
|
submitValue: '{!pveceph}'
|
|
},
|
|
value: 'admin',
|
|
fieldLabel: gettext('User name'),
|
|
allowBlank: true
|
|
}
|
|
);
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'pveContentTypeSelector',
|
|
cts: ['images', 'rootdir'],
|
|
fieldLabel: gettext('Content'),
|
|
name: 'content',
|
|
value: ['images'],
|
|
multiSelect: true,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'krbd',
|
|
uncheckedValue: 0,
|
|
fieldLabel: 'KRBD'
|
|
}
|
|
];
|
|
|
|
me.columnB = [{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'pveceph',
|
|
reference: 'pvecephRef',
|
|
bind : {
|
|
disabled: '{!pvecephPossible}',
|
|
value: '{pveceph}'
|
|
},
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
submitValue: false,
|
|
hidden: !me.isCreate,
|
|
boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool')
|
|
}];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.storage.ZFSInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
viewModel: {
|
|
parent: null,
|
|
data: {
|
|
isLIO: false,
|
|
isComstar: true,
|
|
hasWriteCacheOption: true
|
|
}
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
control: {
|
|
'field[name=iscsiprovider]': {
|
|
change: 'changeISCSIProvider'
|
|
}
|
|
},
|
|
changeISCSIProvider: function(f, newVal, oldVal) {
|
|
var vm = this.getViewModel();
|
|
vm.set('isLIO', newVal === 'LIO');
|
|
vm.set('isComstar', newVal === 'comstar');
|
|
vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt');
|
|
}
|
|
},
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
if (me.isCreate) {
|
|
values.content = 'images';
|
|
}
|
|
|
|
values.nowritecache = values.writecache ? 0 : 1;
|
|
delete values.writecache;
|
|
|
|
return me.callParent([values]);
|
|
},
|
|
|
|
setValues: function diff(values) {
|
|
values.writecache = values.nowritecache ? 0 : 1;
|
|
this.callParent([values]);
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'portal',
|
|
value: '',
|
|
fieldLabel: gettext('Portal'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'pool',
|
|
value: '',
|
|
fieldLabel: gettext('Pool'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'blocksize',
|
|
value: '4k',
|
|
fieldLabel: gettext('Block Size'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'target',
|
|
value: '',
|
|
fieldLabel: gettext('Target'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'comstar_tg',
|
|
value: '',
|
|
fieldLabel: gettext('Target group'),
|
|
bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
|
|
allowBlank: true
|
|
}
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield',
|
|
name: 'iscsiprovider',
|
|
value: 'comstar',
|
|
fieldLabel: gettext('iSCSI Provider'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'sparse',
|
|
checked: false,
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Thin provision')
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'writecache',
|
|
checked: true,
|
|
bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' },
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Write cache')
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'comstar_hg',
|
|
value: '',
|
|
bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' },
|
|
fieldLabel: gettext('Host group'),
|
|
allowBlank: true
|
|
},
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'lio_tpg',
|
|
value: '',
|
|
bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' },
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Target portal group')
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.storage.ZFSPoolSelector', {
|
|
extend: 'Ext.form.field.ComboBox',
|
|
alias: 'widget.pveZFSPoolSelector',
|
|
valueField: 'pool',
|
|
displayField: 'pool',
|
|
queryMode: 'local',
|
|
editable: false,
|
|
listConfig: {
|
|
loadingText: gettext('Scanning...')
|
|
},
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.nodename) {
|
|
me.nodename = 'localhost';
|
|
}
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
autoLoad: {}, // true,
|
|
fields: [ 'pool', 'size', 'free' ],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodename + '/scan/zfs'
|
|
}
|
|
});
|
|
|
|
store.sort('pool', 'ASC');
|
|
|
|
Ext.apply(me, {
|
|
store: store
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.storage.ZFSPoolInputPanel', {
|
|
extend: 'PVE.panel.StorageBase',
|
|
|
|
onlineHelp: 'storage_zfspool',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.column1 = [];
|
|
|
|
if (me.isCreate) {
|
|
me.column1.push(Ext.create('PVE.storage.ZFSPoolSelector', {
|
|
name: 'pool',
|
|
fieldLabel: gettext('ZFS Pool'),
|
|
allowBlank: false
|
|
}));
|
|
} else {
|
|
me.column1.push(Ext.createWidget('displayfield', {
|
|
name: 'pool',
|
|
value: '',
|
|
fieldLabel: gettext('ZFS Pool'),
|
|
allowBlank: false
|
|
}));
|
|
}
|
|
|
|
// value is an array,
|
|
// while before it was a string
|
|
/*jslint confusion: true*/
|
|
me.column1.push(
|
|
{xtype: 'pveContentTypeSelector',
|
|
cts: ['images', 'rootdir'],
|
|
fieldLabel: gettext('Content'),
|
|
name: 'content',
|
|
value: ['images', 'rootdir'],
|
|
multiSelect: true,
|
|
allowBlank: false
|
|
});
|
|
/*jslint confusion: false*/
|
|
me.column2 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'sparse',
|
|
checked: false,
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Thin provision')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'blocksize',
|
|
emptyText: '8k',
|
|
fieldLabel: gettext('Block Size'),
|
|
allowBlank: true
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.ha.StatusView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: ['widget.pveHAStatusView'],
|
|
|
|
onlineHelp: 'chapter_ha_manager',
|
|
|
|
sortPriority: {
|
|
quorum: 1,
|
|
master: 2,
|
|
lrm: 3,
|
|
service: 4
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (!me.rstore) {
|
|
throw "no rstore given";
|
|
}
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.rstore);
|
|
|
|
var store = Ext.create('Proxmox.data.DiffStore', {
|
|
rstore: me.rstore,
|
|
sortAfterUpdate: true,
|
|
sorters: [{
|
|
sorterFn: function(rec1, rec2) {
|
|
var p1 = me.sortPriority[rec1.data.type];
|
|
var p2 = me.sortPriority[rec2.data.type];
|
|
return (p1 !== p2) ? ((p1 > p2) ? 1 : -1) : 0;
|
|
}
|
|
}],
|
|
filters: {
|
|
property: 'type',
|
|
value: 'service',
|
|
operator: '!='
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
stateful: false,
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
columns: [
|
|
{
|
|
header: gettext('Type'),
|
|
width: 80,
|
|
dataIndex: 'type'
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
width: 80,
|
|
flex: 1,
|
|
dataIndex: 'status'
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.on('activate', me.rstore.startUpdate);
|
|
me.on('destroy', me.rstore.stopUpdate);
|
|
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-ha-status', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'id', 'type', 'node', 'status', 'sid',
|
|
'state', 'group', 'comment',
|
|
'max_restart', 'max_relocate', 'type',
|
|
'crm_state', 'request_state'
|
|
],
|
|
idProperty: 'id'
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.ha.Status', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveHAStatus',
|
|
|
|
onlineHelp: 'chapter_ha_manager',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.rstore = Ext.create('Proxmox.data.ObjectStore', {
|
|
interval: me.interval,
|
|
model: 'pve-ha-status',
|
|
storeid: 'pve-store-' + (++Ext.idSeed),
|
|
groupField: 'type',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/cluster/ha/status/current'
|
|
}
|
|
});
|
|
|
|
me.items = [{
|
|
xtype: 'pveHAStatusView',
|
|
title: gettext('Status'),
|
|
rstore: me.rstore,
|
|
border: 0,
|
|
collapsible: true,
|
|
padding: '0 0 20 0'
|
|
},{
|
|
xtype: 'pveHAResourcesView',
|
|
flex: 1,
|
|
collapsible: true,
|
|
title: gettext('Resources'),
|
|
border: 0,
|
|
rstore: me.rstore
|
|
}];
|
|
|
|
me.callParent();
|
|
me.on('activate', me.rstore.startUpdate);
|
|
}
|
|
});
|
|
Ext.define('PVE.ha.GroupSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: ['widget.pveHAGroupSelector'],
|
|
|
|
value: [],
|
|
autoSelect: false,
|
|
valueField: 'group',
|
|
displayField: 'group',
|
|
listConfig: {
|
|
columns: [
|
|
{
|
|
header: gettext('Group'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'group'
|
|
},
|
|
{
|
|
header: gettext('Nodes'),
|
|
width: 100,
|
|
sortable: false,
|
|
dataIndex: 'nodes'
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
flex: 1,
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode
|
|
}
|
|
]
|
|
},
|
|
store: {
|
|
model: 'pve-ha-groups',
|
|
sorters: {
|
|
property: 'group',
|
|
order: 'DESC'
|
|
}
|
|
},
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
me.callParent();
|
|
me.getStore().load();
|
|
}
|
|
|
|
}, function() {
|
|
|
|
Ext.define('pve-ha-groups', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'group', 'type', 'digest', 'nodes', 'comment',
|
|
{
|
|
name : 'restricted',
|
|
type: 'boolean'
|
|
},
|
|
{
|
|
name : 'nofailback',
|
|
type: 'boolean'
|
|
}
|
|
],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/cluster/ha/groups"
|
|
},
|
|
idProperty: 'group'
|
|
});
|
|
});
|
|
Ext.define('PVE.ha.VMResourceInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
onlineHelp: 'ha_manager_resource_config',
|
|
vmid: undefined,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
if (values.vmid) {
|
|
values.sid = values.vmid;
|
|
}
|
|
delete values.vmid;
|
|
|
|
PVE.Utils.delete_if_default(values, 'group', '', me.isCreate);
|
|
PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate);
|
|
PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate);
|
|
|
|
return values;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
var MIN_QUORUM_VOTES = 3;
|
|
|
|
var disabledHint = Ext.createWidget({
|
|
xtype: 'displayfield', // won't get submitted by default
|
|
userCls: 'pve-hint',
|
|
value: 'Disabling the resource will stop the guest system. ' +
|
|
'See the online help for details.',
|
|
hidden: true
|
|
});
|
|
|
|
var fewVotesHint = Ext.createWidget({
|
|
itemId: 'fewVotesHint',
|
|
xtype: 'displayfield',
|
|
userCls: 'pve-hint',
|
|
value: 'At least three quorum votes are recommended for reliable HA.',
|
|
hidden: true
|
|
});
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/cluster/config/nodes',
|
|
method: 'GET',
|
|
failure: function(response) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
success: function(response) {
|
|
var nodes = response.result.data;
|
|
var votes = 0;
|
|
Ext.Array.forEach(nodes, function(node) {
|
|
var vote = parseInt(node.quorum_votes, 10); // parse as base 10
|
|
votes += vote || 0; // parseInt might return NaN, which is false
|
|
});
|
|
|
|
if (votes < MIN_QUORUM_VOTES) {
|
|
fewVotesHint.setVisible(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
/*jslint confusion: true */
|
|
var vmidStore = (me.vmid) ? {} : {
|
|
model: 'PVEResources',
|
|
autoLoad: true,
|
|
sorters: 'vmid',
|
|
filters: [
|
|
{
|
|
property: 'type',
|
|
value: /lxc|qemu/
|
|
},
|
|
{
|
|
property: 'hastate',
|
|
value: /unmanaged/
|
|
}
|
|
]
|
|
};
|
|
|
|
// value is a string above, but a number below
|
|
me.column1 = [
|
|
{
|
|
xtype: me.vmid ? 'displayfield' : 'vmComboSelector',
|
|
submitValue: me.isCreate,
|
|
name: 'vmid',
|
|
fieldLabel: (me.vmid && me.guestType === 'ct') ? 'CT' : 'VM',
|
|
value: me.vmid,
|
|
store: vmidStore,
|
|
validateExists: true
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'max_restart',
|
|
fieldLabel: gettext('Max. Restart'),
|
|
value: 1,
|
|
minValue: 0,
|
|
maxValue: 10,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'max_relocate',
|
|
fieldLabel: gettext('Max. Relocate'),
|
|
value: 1,
|
|
minValue: 0,
|
|
maxValue: 10,
|
|
allowBlank: false
|
|
}
|
|
];
|
|
/*jslint confusion: false */
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'pveHAGroupSelector',
|
|
name: 'group',
|
|
fieldLabel: gettext('Group')
|
|
},
|
|
{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'state',
|
|
value: 'started',
|
|
fieldLabel: gettext('Request State'),
|
|
comboItems: [
|
|
['started', 'started'],
|
|
['stopped', 'stopped'],
|
|
['ignored', 'ignored'],
|
|
['disabled', 'disabled']
|
|
],
|
|
listeners: {
|
|
'change': function(field, newValue) {
|
|
if (newValue === 'disabled') {
|
|
disabledHint.setVisible(true);
|
|
}
|
|
else {
|
|
if (disabledHint.isVisible()) {
|
|
disabledHint.setVisible(false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
disabledHint
|
|
];
|
|
|
|
me.columnB = [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
fieldLabel: gettext('Comment')
|
|
},
|
|
fewVotesHint
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.ha.VMResourceEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
vmid: undefined,
|
|
guestType: undefined,
|
|
isCreate: undefined,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
if (me.isCreate === undefined) {
|
|
me.isCreate = !me.vmid;
|
|
}
|
|
|
|
if (me.isCreate) {
|
|
me.url = '/api2/extjs/cluster/ha/resources';
|
|
me.method = 'POST';
|
|
} else {
|
|
me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid;
|
|
me.method = 'PUT';
|
|
}
|
|
|
|
var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', {
|
|
isCreate: me.isCreate,
|
|
vmid: me.vmid,
|
|
guestType: me.guestType
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('Resource') + ': ' + gettext('Container') +
|
|
'/' + gettext('Virtual Machine'),
|
|
isAdd: true,
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var values = response.result.data;
|
|
|
|
var regex = /^(\S+):(\S+)$/;
|
|
var res = regex.exec(values.sid);
|
|
|
|
if (res[1] !== 'vm' && res[1] !== 'ct') {
|
|
throw "got unexpected resource type";
|
|
}
|
|
|
|
values.vmid = res[2];
|
|
|
|
ipanel.setValues(values);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.ha.ResourcesView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: ['widget.pveHAResourcesView'],
|
|
|
|
onlineHelp: 'ha_manager_resources',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-ha-resources',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
if (!me.rstore) {
|
|
throw "no store given";
|
|
}
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.rstore);
|
|
|
|
var store = Ext.create('Proxmox.data.DiffStore', {
|
|
rstore: me.rstore,
|
|
filters: {
|
|
property: 'type',
|
|
value: 'service'
|
|
}
|
|
});
|
|
|
|
var reload = function() {
|
|
me.rstore.load();
|
|
};
|
|
|
|
var render_error = function(dataIndex, value, metaData, record) {
|
|
var errors = record.data.errors;
|
|
if (errors) {
|
|
var msg = errors[dataIndex];
|
|
if (msg) {
|
|
metaData.tdCls = 'proxmox-invalid-row';
|
|
var html = '<p>' + Ext.htmlEncode(msg) + '</p>';
|
|
metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' +
|
|
html.replace(/\"/g,'"') + '"';
|
|
}
|
|
}
|
|
return value;
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
var sid = rec.data.sid;
|
|
|
|
var regex = /^(\S+):(\S+)$/;
|
|
var res = regex.exec(sid);
|
|
|
|
if (res[1] !== 'vm' && res[1] !== 'ct') {
|
|
return;
|
|
}
|
|
var guestType = res[1];
|
|
var vmid = res[2];
|
|
|
|
var win = Ext.create('PVE.ha.VMResourceEdit',{
|
|
guestType: guestType,
|
|
vmid: vmid
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: '/cluster/ha/resources/',
|
|
getUrl: function(rec) {
|
|
var me = this;
|
|
return me.baseurl + '/' + rec.get('sid');
|
|
},
|
|
callback: function() {
|
|
reload();
|
|
}
|
|
});
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
disabled: !caps.nodes['Sys.Console'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.ha.VMResourceEdit',{});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
edit_btn, remove_btn
|
|
],
|
|
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'sid'
|
|
},
|
|
{
|
|
header: gettext('State'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'state'
|
|
},
|
|
{
|
|
header: gettext('Node'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'node'
|
|
},
|
|
{
|
|
header: gettext('Request State'),
|
|
width: 100,
|
|
hidden: true,
|
|
sortable: true,
|
|
renderer: function(v) {
|
|
return v || 'started';
|
|
},
|
|
dataIndex: 'request_state'
|
|
},
|
|
{
|
|
header: gettext('CRM State'),
|
|
width: 100,
|
|
hidden: true,
|
|
sortable: true,
|
|
dataIndex: 'crm_state'
|
|
},
|
|
{
|
|
header: gettext('Max. Restart'),
|
|
width: 100,
|
|
sortable: true,
|
|
renderer: (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, record) {
|
|
return render_error('group', value, metaData, record);
|
|
},
|
|
dataIndex: 'group'
|
|
},
|
|
{
|
|
header: gettext('Description'),
|
|
flex: 1,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'comment'
|
|
}
|
|
],
|
|
listeners: {
|
|
beforeselect: function(grid, record, index, eOpts) {
|
|
if (!caps.nodes['Sys.Console']) {
|
|
return false;
|
|
}
|
|
},
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-ha-resources', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'sid', 'state', 'digest', 'errors', 'group', 'comment',
|
|
'max_restart', 'max_relocate', 'type', 'status', 'node',
|
|
'crm_state', 'request_state'
|
|
],
|
|
idProperty: 'sid'
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.ha.GroupInputPanel', {
|
|
extend: 'Proxmox.panel.InputPanel',
|
|
onlineHelp: 'ha_manager_groups',
|
|
|
|
groupId: undefined,
|
|
|
|
onGetValues: function(values) {
|
|
var me = this;
|
|
|
|
if (me.isCreate) {
|
|
values.type = 'group';
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var update_nodefield, update_node_selection;
|
|
|
|
var sm = Ext.create('Ext.selection.CheckboxModel', {
|
|
mode: 'SIMPLE',
|
|
listeners: {
|
|
selectionchange: function(model, selected) {
|
|
update_nodefield(selected);
|
|
}
|
|
}
|
|
});
|
|
|
|
// use already cached data to avoid an API call
|
|
var data = PVE.data.ResourceStore.getNodes();
|
|
|
|
var store = Ext.create('Ext.data.Store', {
|
|
fields: [ 'node', 'mem', 'cpu', 'priority' ],
|
|
data: data,
|
|
proxy: {
|
|
type: 'memory',
|
|
reader: {type: 'json'}
|
|
},
|
|
sorters: [
|
|
{
|
|
property : 'node',
|
|
direction: 'ASC'
|
|
}
|
|
]
|
|
});
|
|
|
|
var nodegrid = Ext.createWidget('grid', {
|
|
store: store,
|
|
border: true,
|
|
height: 300,
|
|
selModel: sm,
|
|
columns: [
|
|
{
|
|
header: gettext('Node'),
|
|
flex: 1,
|
|
dataIndex: 'node'
|
|
},
|
|
{
|
|
header: gettext('Memory usage') + " %",
|
|
renderer: PVE.Utils.render_mem_usage_percent,
|
|
sortable: true,
|
|
width: 150,
|
|
dataIndex: 'mem'
|
|
},
|
|
{
|
|
header: gettext('CPU usage'),
|
|
renderer: PVE.Utils.render_cpu,
|
|
sortable: true,
|
|
width: 150,
|
|
dataIndex: 'cpu'
|
|
},
|
|
{
|
|
header: 'Priority',
|
|
xtype: 'widgetcolumn',
|
|
dataIndex: 'priority',
|
|
sortable: true,
|
|
stopSelection: true,
|
|
widget: {
|
|
xtype: 'proxmoxintegerfield',
|
|
minValue: 0,
|
|
maxValue: 1000,
|
|
isFormField: false,
|
|
listeners: {
|
|
change: function(numberfield, value, old_value) {
|
|
var record = numberfield.getWidgetRecord();
|
|
record.set('priority', value);
|
|
update_nodefield(sm.getSelection());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
var nodefield = Ext.create('Ext.form.field.Hidden', {
|
|
name: 'nodes',
|
|
value: '',
|
|
listeners: {
|
|
change: function (nodefield, value) {
|
|
update_node_selection(value);
|
|
}
|
|
},
|
|
isValid: function () {
|
|
var value = nodefield.getValue();
|
|
return (value && 0 !== value.length);
|
|
}
|
|
});
|
|
|
|
update_node_selection = function(string) {
|
|
sm.deselectAll(true);
|
|
|
|
string.split(',').forEach(function (e, idx, array) {
|
|
var res = e.split(':');
|
|
|
|
store.each(function(record) {
|
|
var node = record.get('node');
|
|
|
|
if (node == res[0]) {
|
|
sm.select(record, true);
|
|
record.set('priority', res[1]);
|
|
record.commit();
|
|
}
|
|
});
|
|
});
|
|
nodegrid.reconfigure(store);
|
|
|
|
};
|
|
|
|
update_nodefield = function(selected) {
|
|
var nodes = '';
|
|
var first_iteration = true;
|
|
Ext.Array.each(selected, function(record) {
|
|
if (!first_iteration) {
|
|
nodes += ',';
|
|
}
|
|
first_iteration = false;
|
|
|
|
nodes += record.data.node;
|
|
if (record.data.priority) {
|
|
nodes += ':' + record.data.priority;
|
|
}
|
|
});
|
|
|
|
// nodefield change listener calls us again, which results in a
|
|
// endless recursion, suspend the event temporary to avoid this
|
|
nodefield.suspendEvent('change');
|
|
nodefield.setValue(nodes);
|
|
nodefield.resumeEvent('change');
|
|
};
|
|
|
|
me.column1 = [
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'group',
|
|
value: me.groupId || '',
|
|
fieldLabel: 'ID',
|
|
vtype: 'StorageId',
|
|
allowBlank: false
|
|
},
|
|
nodefield
|
|
];
|
|
|
|
me.column2 = [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'restricted',
|
|
uncheckedValue: 0,
|
|
fieldLabel: 'restricted'
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'nofailback',
|
|
uncheckedValue: 0,
|
|
fieldLabel: 'nofailback'
|
|
}
|
|
];
|
|
|
|
me.columnB = [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
fieldLabel: gettext('Comment')
|
|
},
|
|
nodegrid
|
|
];
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.ha.GroupEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
groupId: undefined,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.groupId;
|
|
|
|
if (me.isCreate) {
|
|
me.url = '/api2/extjs/cluster/ha/groups';
|
|
me.method = 'POST';
|
|
} else {
|
|
me.url = '/api2/extjs/cluster/ha/groups/' + me.groupId;
|
|
me.method = 'PUT';
|
|
}
|
|
|
|
var ipanel = Ext.create('PVE.ha.GroupInputPanel', {
|
|
isCreate: me.isCreate,
|
|
groupId: me.groupId
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
subject: gettext('HA Group'),
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var values = response.result.data;
|
|
|
|
ipanel.setValues(values);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.ha.GroupsView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: ['widget.pveHAGroupsView'],
|
|
|
|
onlineHelp: 'ha_manager_groups',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-ha-groups',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-ha-groups',
|
|
sorters: {
|
|
property: 'group',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
|
|
var win = Ext.create('PVE.ha.GroupEdit',{
|
|
groupId: rec.data.group
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: '/cluster/ha/groups/',
|
|
callback: function() {
|
|
reload();
|
|
}
|
|
});
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
tbar: [
|
|
{
|
|
text: gettext('Create'),
|
|
disabled: !caps.nodes['Sys.Console'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.ha.GroupEdit',{});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
edit_btn, remove_btn
|
|
],
|
|
columns: [
|
|
{
|
|
header: gettext('Group'),
|
|
width: 150,
|
|
sortable: true,
|
|
dataIndex: 'group'
|
|
},
|
|
{
|
|
header: 'restricted',
|
|
width: 100,
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
dataIndex: 'restricted'
|
|
},
|
|
{
|
|
header: 'nofailback',
|
|
width: 100,
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
dataIndex: 'nofailback'
|
|
},
|
|
{
|
|
header: gettext('Nodes'),
|
|
flex: 1,
|
|
sortable: false,
|
|
dataIndex: 'nodes'
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
flex: 1,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'comment'
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: reload,
|
|
beforeselect: function(grid, record, index, eOpts) {
|
|
if (!caps.nodes['Sys.Console']) {
|
|
return false;
|
|
}
|
|
},
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.ha.FencingView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: ['widget.pveFencingView'],
|
|
|
|
onlineHelp: 'ha_manager_fencing',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-ha-fencing',
|
|
data: []
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
stateful: false,
|
|
viewConfig: {
|
|
trackOver: false,
|
|
deferEmptyText: false,
|
|
emptyText: 'Use watchdog based fencing.'
|
|
},
|
|
columns: [
|
|
{
|
|
header: 'Node',
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'node'
|
|
},
|
|
{
|
|
header: gettext('Command'),
|
|
flex: 1,
|
|
dataIndex: 'command'
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-ha-fencing', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'node', 'command', 'digest'
|
|
]
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.dc.Summary', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveDcSummary',
|
|
|
|
scrollable: true,
|
|
|
|
bodyPadding: 5,
|
|
|
|
layout: 'column',
|
|
|
|
defaults: {
|
|
padding: 5,
|
|
plugins: 'responsive',
|
|
responsiveConfig: {
|
|
'width < 1900': {
|
|
columnWidth: 1
|
|
},
|
|
'width >= 1900': {
|
|
columnWidth: 0.5
|
|
}
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
itemId: 'dcHealth',
|
|
xtype: 'pveDcHealth'
|
|
},
|
|
{
|
|
itemId: 'dcGuests',
|
|
xtype: 'pveDcGuests'
|
|
},
|
|
{
|
|
title: gettext('Resources'),
|
|
xtype: 'panel',
|
|
minHeight: 250,
|
|
bodyPadding: 5,
|
|
layout: 'hbox',
|
|
defaults: {
|
|
xtype: 'proxmoxGauge',
|
|
flex: 1
|
|
},
|
|
items:[
|
|
{
|
|
title: gettext('CPU'),
|
|
itemId: 'cpu'
|
|
},
|
|
{
|
|
title: gettext('Memory'),
|
|
itemId: 'memory'
|
|
},
|
|
{
|
|
title: gettext('Storage'),
|
|
itemId: 'storage'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
itemId: 'nodeview',
|
|
xtype: 'pveDcNodeView',
|
|
height: 250
|
|
},
|
|
{
|
|
title: gettext('Subscriptions'),
|
|
height: 220,
|
|
items: [
|
|
{
|
|
itemId: 'subscriptions',
|
|
xtype: 'pveHealthWidget',
|
|
userCls: 'pointer',
|
|
listeners: {
|
|
element: 'el',
|
|
click: function() {
|
|
if (this.component.userCls === 'pointer') {
|
|
window.open('https://www.proxmox.com/en/proxmox-ve/pricing', '_blank');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var rstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
interval: 3000,
|
|
storeid: 'pve-cluster-status',
|
|
model: 'pve-dc-nodes',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/cluster/status"
|
|
}
|
|
});
|
|
|
|
var gridstore = Ext.create('Proxmox.data.DiffStore', {
|
|
rstore: rstore,
|
|
filters: {
|
|
property: 'type',
|
|
value: 'node'
|
|
},
|
|
sorters: {
|
|
property: 'id',
|
|
direction: 'ASC'
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.getComponent('nodeview').setStore(gridstore);
|
|
|
|
var gueststatus = me.getComponent('dcGuests');
|
|
|
|
var cpustat = me.down('#cpu');
|
|
var memorystat = me.down('#memory');
|
|
var storagestat = me.down('#storage');
|
|
var sp = Ext.state.Manager.getProvider();
|
|
|
|
me.mon(PVE.data.ResourceStore, 'load', function(curstore, results) {
|
|
me.suspendLayout = true;
|
|
|
|
var cpu = 0;
|
|
var maxcpu = 0;
|
|
|
|
var nodes = 0;
|
|
|
|
var memory = 0;
|
|
var maxmem = 0;
|
|
|
|
var countedStorages = {};
|
|
var used = 0;
|
|
var total = 0;
|
|
var usableStorages = {};
|
|
var storages = sp.get('dash-storages') || '';
|
|
storages.split(',').forEach(function(storage){
|
|
if (storage !== '') {
|
|
usableStorages[storage] = true;
|
|
}
|
|
});
|
|
|
|
var qemu = {
|
|
running: 0,
|
|
paused: 0,
|
|
stopped: 0,
|
|
template: 0
|
|
};
|
|
var lxc = {
|
|
running: 0,
|
|
paused: 0,
|
|
stopped: 0,
|
|
template: 0
|
|
};
|
|
var error = 0;
|
|
|
|
var i;
|
|
|
|
for (i = 0; i < results.length; i++) {
|
|
var item = results[i];
|
|
switch(item.data.type) {
|
|
case 'node':
|
|
cpu += (item.data.cpu * item.data.maxcpu);
|
|
maxcpu += item.data.maxcpu || 0;
|
|
memory += item.data.mem || 0;
|
|
maxmem += item.data.maxmem || 0;
|
|
nodes++;
|
|
|
|
// update grid also
|
|
var griditem = gridstore.getById(item.data.id);
|
|
if (griditem) {
|
|
griditem.set('cpuusage', item.data.cpu);
|
|
var max = item.data.maxmem || 1;
|
|
var val = item.data.mem || 0;
|
|
griditem.set('memoryusage', val/max);
|
|
griditem.set('uptime', item.data.uptime);
|
|
griditem.commit(); //else it marks the fields as dirty
|
|
}
|
|
break;
|
|
case 'storage':
|
|
if (!Ext.Object.isEmpty(usableStorages)) {
|
|
if (usableStorages[item.data.id] === true) {
|
|
used += item.data.disk;
|
|
total += item.data.maxdisk;
|
|
}
|
|
break;
|
|
}
|
|
if (!countedStorages[item.data.storage] ||
|
|
(item.data.storage === 'local' &&
|
|
!countedStorages[item.data.id])) {
|
|
used += item.data.disk;
|
|
total += item.data.maxdisk;
|
|
|
|
countedStorages[item.data.storage === 'local'?item.data.id:item.data.storage] = true;
|
|
}
|
|
break;
|
|
case 'qemu':
|
|
qemu[item.data.template ? 'template' : item.data.status]++;
|
|
if (item.data.hastate === 'error') {
|
|
error++;
|
|
}
|
|
break;
|
|
case 'lxc':
|
|
lxc[item.data.template ? 'template' : item.data.status]++;
|
|
if (item.data.hastate === 'error') {
|
|
error++;
|
|
}
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
var text = Ext.String.format(gettext('of {0} CPU(s)'), maxcpu);
|
|
cpustat.updateValue((cpu/maxcpu), text);
|
|
|
|
text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(memory), PVE.Utils.render_size(maxmem));
|
|
memorystat.updateValue((memory/maxmem), text);
|
|
|
|
text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(used), PVE.Utils.render_size(total));
|
|
storagestat.updateValue((used/total), text);
|
|
|
|
gueststatus.updateValues(qemu,lxc,error);
|
|
|
|
me.suspendLayout = false;
|
|
me.updateLayout(true);
|
|
});
|
|
|
|
var dcHealth = me.getComponent('dcHealth');
|
|
me.mon(rstore, 'load', dcHealth.updateStatus, dcHealth);
|
|
|
|
var subs = me.down('#subscriptions');
|
|
me.mon(rstore, 'load', function(store, records, success) {
|
|
var i;
|
|
var level;
|
|
var mixed = false;
|
|
for (i = 0; i < records.length; i++) {
|
|
if (records[i].get('type') !== 'node') {
|
|
continue;
|
|
}
|
|
var node = records[i];
|
|
if (node.get('status') === 'offline') {
|
|
continue;
|
|
}
|
|
|
|
var curlevel = node.get('level');
|
|
|
|
if (curlevel === '') { // no subscription trumps all, set and break
|
|
level = '';
|
|
break;
|
|
}
|
|
|
|
if (level === undefined) { // save level
|
|
level = curlevel;
|
|
} else if (level !== curlevel) { // detect different levels
|
|
mixed = true;
|
|
}
|
|
}
|
|
|
|
var 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();
|
|
});
|
|
|
|
rstore.startUpdate();
|
|
}
|
|
|
|
});
|
|
Ext.define('PVE.window.ReplicaEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveReplicaEdit',
|
|
|
|
subject: gettext('Replication Job'),
|
|
|
|
|
|
url: '/cluster/replication',
|
|
method: 'POST',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var vmid = me.pveSelNode.data.vmid;
|
|
var nodename = me.pveSelNode.data.node;
|
|
|
|
var items = [];
|
|
|
|
items.push({
|
|
xtype: (me.isCreate && !vmid)?'pveGuestIDSelector':'displayfield',
|
|
name: 'guest',
|
|
fieldLabel: 'CT/VM ID',
|
|
value: vmid || ''
|
|
});
|
|
|
|
items.push(
|
|
{
|
|
xtype: me.isCreate ? 'pveNodeSelector':'displayfield',
|
|
name: 'target',
|
|
disallowedNodes: [nodename],
|
|
allowBlank: false,
|
|
onlineValidator: true,
|
|
fieldLabel: gettext("Target")
|
|
},
|
|
{
|
|
xtype: 'pveCalendarEvent',
|
|
fieldLabel: gettext('Schedule'),
|
|
emptyText: '*/15 - ' + Ext.String.format(gettext('Every {0} minutes'), 15),
|
|
name: 'schedule'
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
fieldLabel: gettext('Rate limit') + ' (MB/s)',
|
|
step: 1,
|
|
minValue: 1,
|
|
emptyText: gettext('unlimited'),
|
|
name: 'rate'
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Comment'),
|
|
name: 'comment'
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'enabled',
|
|
defaultValue: 'on',
|
|
checked: true,
|
|
fieldLabel: gettext('Enabled')
|
|
}
|
|
);
|
|
|
|
me.items = [
|
|
{
|
|
xtype: 'inputpanel',
|
|
itemId: 'ipanel',
|
|
onlineHelp: 'pvesr_schedule_time_format',
|
|
|
|
onGetValues: function(values) {
|
|
var me = this.up('window');
|
|
|
|
values.disable = values.enabled ? 0 : 1;
|
|
delete values.enabled;
|
|
|
|
PVE.Utils.delete_if_default(values, 'rate', '', me.isCreate);
|
|
PVE.Utils.delete_if_default(values, 'disable', 0, me.isCreate);
|
|
PVE.Utils.delete_if_default(values, 'schedule', '*/15', me.isCreate);
|
|
PVE.Utils.delete_if_default(values, 'comment', '', me.isCreate);
|
|
|
|
if (me.isCreate) {
|
|
values.type = 'local';
|
|
var vm = vmid || values.guest;
|
|
var id = -1;
|
|
if (me.highestids[vm] !== undefined) {
|
|
id = me.highestids[vm];
|
|
}
|
|
id++;
|
|
values.id = vm + '-' + id.toString();
|
|
delete values.guest;
|
|
}
|
|
return values;
|
|
},
|
|
items: items
|
|
}
|
|
];
|
|
|
|
me.callParent();
|
|
|
|
if (me.isCreate) {
|
|
me.load({
|
|
success: function(response) {
|
|
var jobs = response.result.data;
|
|
var highestids = {};
|
|
Ext.Array.forEach(jobs, function(job) {
|
|
var match = /^([0-9]+)\-([0-9]+)$/.exec(job.id);
|
|
if (match) {
|
|
var vmid = parseInt(match[1],10);
|
|
var id = parseInt(match[2],10);
|
|
if (highestids[vmid] < id ||
|
|
highestids[vmid] === undefined) {
|
|
highestids[vmid] = id;
|
|
}
|
|
}
|
|
});
|
|
|
|
me.highestids = highestids;
|
|
}
|
|
});
|
|
|
|
} else {
|
|
me.load({
|
|
success: function(response, options) {
|
|
response.result.data.enabled = !response.result.data.disable;
|
|
me.setValues(response.result.data);
|
|
me.digest = response.result.data.digest;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
/*jslint confusion: true */
|
|
/* callback is a function and string */
|
|
Ext.define('PVE.grid.ReplicaView', {
|
|
extend: 'Ext.grid.Panel',
|
|
xtype: 'pveReplicaView',
|
|
|
|
onlineHelp: 'chapter_pvesr',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-pve-replication-status',
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
addJob: function(button,event,rec) {
|
|
var me = this.getView();
|
|
var controller = this;
|
|
var win = Ext.create('PVE.window.ReplicaEdit', {
|
|
isCreate: true,
|
|
method: 'POST',
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.on('destroy', function() { controller.reload(); });
|
|
win.show();
|
|
},
|
|
|
|
editJob: function(button,event,rec) {
|
|
var me = this.getView();
|
|
var controller = this;
|
|
var data = rec.data;
|
|
var win = Ext.create('PVE.window.ReplicaEdit', {
|
|
url: '/cluster/replication/' + data.id,
|
|
method: 'PUT',
|
|
pveSelNode: me.pveSelNode
|
|
});
|
|
win.on('destroy', function() { controller.reload(); });
|
|
win.show();
|
|
},
|
|
|
|
scheduleJobNow: function(button,event,rec) {
|
|
var me = this.getView();
|
|
var controller = this;
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/schedule_now",
|
|
method: 'POST',
|
|
waitMsgTarget: me,
|
|
callback: function() { controller.reload(); },
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
|
|
showLog: function(button, event, rec) {
|
|
var me = this.getView();
|
|
var controller = this;
|
|
var logView = Ext.create('Proxmox.panel.LogView', {
|
|
border: false,
|
|
url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/log"
|
|
});
|
|
var win = Ext.create('Ext.window.Window', {
|
|
items: [ logView ],
|
|
layout: 'fit',
|
|
width: 800,
|
|
height: 400,
|
|
modal: true,
|
|
title: gettext("Replication Log")
|
|
});
|
|
var task = {
|
|
run: function() {
|
|
logView.requestUpdate();
|
|
},
|
|
interval: 1000
|
|
};
|
|
Ext.TaskManager.start(task);
|
|
win.on('destroy', function() {
|
|
Ext.TaskManager.stop(task);
|
|
controller.reload();
|
|
});
|
|
win.show();
|
|
},
|
|
|
|
reload: function() {
|
|
var me = this.getView();
|
|
me.rstore.load();
|
|
},
|
|
|
|
dblClick: function(grid, record, item) {
|
|
var me = this;
|
|
me.editJob(undefined, undefined, record);
|
|
},
|
|
|
|
// check for cluster
|
|
// currently replication is for cluster only, so we disable the whole
|
|
// component
|
|
checkPrerequisites: function() {
|
|
var me = this.getView();
|
|
if (PVE.data.ResourceStore.getNodes().length < 2) {
|
|
me.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']);
|
|
}
|
|
},
|
|
|
|
control: {
|
|
'#': {
|
|
itemdblclick: 'dblClick',
|
|
afterlayout: 'checkPrerequisites'
|
|
}
|
|
}
|
|
},
|
|
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
itemId: 'addButton',
|
|
handler: 'addJob'
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Edit'),
|
|
itemId: 'editButton',
|
|
handler: 'editJob',
|
|
disabled: true
|
|
},
|
|
{
|
|
xtype: 'proxmoxStdRemoveButton',
|
|
itemId: 'removeButton',
|
|
baseurl: '/api2/extjs/cluster/replication/',
|
|
dangerous: true,
|
|
callback: 'reload'
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Log'),
|
|
itemId: 'logButton',
|
|
handler: 'showLog',
|
|
disabled: true
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Schedule now'),
|
|
itemId: 'scheduleNowButton',
|
|
handler: 'scheduleJobNow',
|
|
disabled: true
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
var mode = '';
|
|
var url = '/cluster/replication';
|
|
|
|
me.nodename = me.pveSelNode.data.node;
|
|
me.vmid = me.pveSelNode.data.vmid;
|
|
|
|
me.columns = [
|
|
{
|
|
text: gettext('Enabled'),
|
|
dataIndex: 'enabled',
|
|
xtype: 'checkcolumn',
|
|
sortable: true,
|
|
disabled: true
|
|
},
|
|
{
|
|
text: 'ID',
|
|
dataIndex: 'id',
|
|
width: 60,
|
|
hidden: true
|
|
},
|
|
{
|
|
text: gettext('Guest'),
|
|
dataIndex: 'guest',
|
|
width: 75
|
|
},
|
|
{
|
|
text: gettext('Job'),
|
|
dataIndex: 'jobnum',
|
|
width: 60
|
|
},
|
|
{
|
|
text: gettext('Target'),
|
|
dataIndex: 'target'
|
|
}
|
|
];
|
|
|
|
if (!me.nodename) {
|
|
mode = 'dc';
|
|
me.stateId = 'grid-pve-replication-dc';
|
|
} else if (!me.vmid) {
|
|
mode = 'node';
|
|
url = '/nodes/' + me.nodename + '/replication';
|
|
} else {
|
|
mode = 'vm';
|
|
url = '/nodes/' + me.nodename + '/replication' + '?guest=' + me.vmid;
|
|
}
|
|
|
|
if (mode !== 'dc') {
|
|
me.columns.push(
|
|
{
|
|
text: gettext('Status'),
|
|
dataIndex: 'state',
|
|
minWidth: 160,
|
|
flex: 1,
|
|
renderer: function(value, metadata, record) {
|
|
|
|
if (record.data.pid) {
|
|
metadata.tdCls = 'x-grid-row-loading';
|
|
return '';
|
|
}
|
|
|
|
var icons = [];
|
|
var states = [];
|
|
|
|
if (record.data.remove_job) {
|
|
icons.push('<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: PVE.Utils.render_duration
|
|
},
|
|
{
|
|
text: gettext('Next Sync'),
|
|
dataIndex: 'next_sync',
|
|
width: 150,
|
|
renderer: function(value) {
|
|
if (!value) {
|
|
return '-';
|
|
}
|
|
|
|
var now = new Date();
|
|
var next = new Date(value*1000);
|
|
|
|
if (next < now) {
|
|
return gettext('pending');
|
|
}
|
|
|
|
return Proxmox.Utils.render_timestamp(value);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
me.columns.push(
|
|
{
|
|
text: gettext('Schedule'),
|
|
width: 75,
|
|
dataIndex: 'schedule'
|
|
},
|
|
{
|
|
text: gettext('Rate limit'),
|
|
dataIndex: 'rate',
|
|
renderer: function(value) {
|
|
if (!value) {
|
|
return gettext('unlimited');
|
|
}
|
|
|
|
return value.toString() + ' MB/s';
|
|
},
|
|
hidden: true
|
|
},
|
|
{
|
|
text: gettext('Comment'),
|
|
dataIndex: 'comment',
|
|
renderer: Ext.htmlEncode
|
|
}
|
|
);
|
|
|
|
me.rstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
storeid: 'pve-replica-' + me.nodename + me.vmid,
|
|
model: (mode === 'dc')? 'pve-replication' : 'pve-replication-state',
|
|
interval: 3000,
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json" + url
|
|
}
|
|
});
|
|
|
|
me.store = Ext.create('Proxmox.data.DiffStore', {
|
|
rstore: me.rstore,
|
|
sorters: [
|
|
{
|
|
property: 'guest'
|
|
},
|
|
{
|
|
property: 'jobnum'
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
// we cannot access the log and scheduleNow button
|
|
// in the datacenter, because
|
|
// we do not know where/if the jobs runs
|
|
if (mode === 'dc') {
|
|
me.down('#logButton').setHidden(true);
|
|
me.down('#scheduleNowButton').setHidden(true);
|
|
}
|
|
|
|
// if we set the warning mask, we do not want to load
|
|
// or set the mask on store errors
|
|
if (PVE.data.ResourceStore.getNodes().length < 2) {
|
|
return;
|
|
}
|
|
|
|
Proxmox.Utils.monStoreErrors(me, me.rstore);
|
|
|
|
me.on('destroy', me.rstore.stopUpdate);
|
|
me.rstore.startUpdate();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-replication', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'id', 'target', 'comment', 'rate', 'type',
|
|
{ name: 'guest', type: 'integer' },
|
|
{ name: 'jobnum', type: 'integer' },
|
|
{ name: 'schedule', defaultValue: '*/15' },
|
|
{ name: 'disable', defaultValue: '' },
|
|
{ name: 'enabled', calculate: function(data) { return !data.disable; } }
|
|
]
|
|
});
|
|
|
|
Ext.define('pve-replication-state', {
|
|
extend: 'pve-replication',
|
|
fields: [
|
|
'last_sync', 'next_sync', 'error', 'duration', 'state',
|
|
'fail_count', 'remove_job', 'pid'
|
|
]
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.dc.Health', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveDcHealth',
|
|
|
|
title: gettext('Health'),
|
|
|
|
bodyPadding: 10,
|
|
height: 220,
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'stretch'
|
|
},
|
|
|
|
defaults: {
|
|
flex: 1,
|
|
xtype: 'box',
|
|
style: {
|
|
'text-align':'center'
|
|
}
|
|
},
|
|
|
|
nodeList: [],
|
|
nodeIndex: 0,
|
|
|
|
updateStatus: function(store, records, success) {
|
|
var me = this;
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
var cluster = {
|
|
iconCls: PVE.Utils.get_health_icon('good', true),
|
|
text: gettext("Standalone node - no cluster defined")
|
|
};
|
|
|
|
var nodes = {
|
|
online: 0,
|
|
offline: 0
|
|
};
|
|
|
|
// by default we have one node
|
|
var numNodes = 1;
|
|
var i;
|
|
|
|
for (i = 0; i < records.length; i++) {
|
|
var item = records[i];
|
|
if (item.data.type === 'node') {
|
|
nodes[item.data.online === 1 ? 'online':'offline']++;
|
|
} else if(item.data.type === 'cluster') {
|
|
cluster.text = gettext("Cluster") + ": ";
|
|
cluster.text += item.data.name + ", ";
|
|
cluster.text += gettext("Quorate") + ": ";
|
|
cluster.text += Proxmox.Utils.format_boolean(item.data.quorate);
|
|
if (item.data.quorate != 1) {
|
|
cluster.iconCls = PVE.Utils.get_health_icon('critical', true);
|
|
}
|
|
|
|
numNodes = item.data.nodes;
|
|
}
|
|
}
|
|
|
|
if (numNodes !== (nodes.online + nodes.offline)) {
|
|
nodes.offline = numNodes - nodes.online;
|
|
}
|
|
|
|
me.getComponent('clusterstatus').updateHealth(cluster);
|
|
me.getComponent('nodestatus').update(nodes);
|
|
},
|
|
|
|
updateCeph: function(store, records, success) {
|
|
var me = this;
|
|
var cephstatus = me.getComponent('ceph');
|
|
if (!success || records.length < 1) {
|
|
|
|
// if ceph status is already visible
|
|
// don't stop to update
|
|
if (cephstatus.isVisible()) {
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
var state = PVE.Utils.render_ceph_health(records[0].data.health || {});
|
|
cephstatus.updateHealth(state);
|
|
cephstatus.setVisible(true);
|
|
},
|
|
|
|
listeners: {
|
|
destroy: function() {
|
|
var me = this;
|
|
me.cephstore.stopUpdate();
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
itemId: 'clusterstatus',
|
|
xtype: 'pveHealthWidget',
|
|
title: gettext('Status')
|
|
},
|
|
{
|
|
itemId: 'nodestatus',
|
|
data: {
|
|
online: 0,
|
|
offline: 0
|
|
},
|
|
tpl: [
|
|
'<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() {
|
|
var sp = Ext.state.Manager.getProvider();
|
|
sp.set('dctab', {value:'ceph'}, true);
|
|
}
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
me.nodeList = PVE.data.ResourceStore.getNodes();
|
|
me.nodeIndex = 0;
|
|
me.cephstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
interval: 3000,
|
|
storeid: 'pve-cluster-ceph',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/nodes/' + me.nodeList[me.nodeIndex].node + '/ceph/status'
|
|
}
|
|
});
|
|
me.callParent();
|
|
me.mon(me.cephstore, 'load', me.updateCeph, me);
|
|
me.cephstore.startUpdate();
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.Guests', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveDcGuests',
|
|
|
|
|
|
title: gettext('Guests'),
|
|
height: 220,
|
|
layout: {
|
|
type: 'table',
|
|
columns: 2,
|
|
tableAttrs: {
|
|
style: {
|
|
width: '100%'
|
|
}
|
|
}
|
|
},
|
|
bodyPadding: '0 20 20 20',
|
|
|
|
defaults: {
|
|
xtype: 'box',
|
|
padding: '0 50 0 50',
|
|
style: {
|
|
'text-align':'center',
|
|
'line-height':'1.2'
|
|
}
|
|
},
|
|
items: [{
|
|
itemId: 'qemu',
|
|
data: {
|
|
running: 0,
|
|
paused: 0,
|
|
stopped: 0,
|
|
template: 0
|
|
},
|
|
tpl: [
|
|
'<h3>' + gettext("Virtual Machines") + '</h3>',
|
|
'<div class="left-aligned">',
|
|
'<i class="good fa fa-fw fa-play-circle"> </i>',
|
|
gettext('Running'),
|
|
'</div>',
|
|
'<div class="right-aligned">{running}</div>' + '<br />',
|
|
'<tpl if="paused > 0">',
|
|
'<div class="left-aligned">',
|
|
'<i class="warning fa fa-fw fa-pause-circle"> </i>',
|
|
gettext('Paused'),
|
|
'</div>',
|
|
'<div class="right-aligned">{paused}</div>' + '<br />',
|
|
'</tpl>',
|
|
'<div class="left-aligned">',
|
|
'<i class="faded fa fa-fw fa-stop-circle"> </i>',
|
|
gettext('Stopped'),
|
|
'</div>',
|
|
'<div class="right-aligned">{stopped}</div>' + '<br />',
|
|
'<tpl if="template > 0">',
|
|
'<div class="left-aligned">',
|
|
'<i class="fa fa-fw fa-circle-o"> </i>',
|
|
gettext('Templates'),
|
|
'</div>',
|
|
'<div class="right-aligned">{template}</div>',
|
|
'</tpl>'
|
|
]
|
|
},{
|
|
itemId: 'lxc',
|
|
data: {
|
|
running: 0,
|
|
paused: 0,
|
|
stopped: 0,
|
|
template: 0
|
|
},
|
|
tpl: [
|
|
'<h3>' + gettext("LXC Container") + '</h3>',
|
|
'<div class="left-aligned">',
|
|
'<i class="good fa fa-fw fa-play-circle"> </i>',
|
|
gettext('Running'),
|
|
'</div>',
|
|
'<div class="right-aligned">{running}</div>' + '<br />',
|
|
'<tpl if="paused > 0">',
|
|
'<div class="left-aligned">',
|
|
'<i class="warning fa fa-fw fa-pause-circle"> </i>',
|
|
gettext('Paused'),
|
|
'</div>',
|
|
'<div class="right-aligned">{paused}</div>' + '<br />',
|
|
'</tpl>',
|
|
'<div class="left-aligned">',
|
|
'<i class="faded fa fa-fw fa-stop-circle"> </i>',
|
|
gettext('Stopped'),
|
|
'</div>',
|
|
'<div class="right-aligned">{stopped}</div>' + '<br />',
|
|
'<tpl if="template > 0">',
|
|
'<div class="left-aligned">',
|
|
'<i class="fa fa-fw fa-circle-o"> </i>',
|
|
gettext('Templates'),
|
|
'</div>',
|
|
'<div class="right-aligned">{template}</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) {
|
|
var me = this;
|
|
me.getComponent('qemu').update(qemu);
|
|
me.getComponent('lxc').update(lxc);
|
|
me.getComponent('error').update({num: error});
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.dc.OptionView', {
|
|
extend: 'Proxmox.grid.ObjectGrid',
|
|
alias: ['widget.pveDcOptionView'],
|
|
|
|
onlineHelp: 'datacenter_configuration_file',
|
|
|
|
monStoreErrors: true,
|
|
|
|
add_inputpanel_row: function(name, text, opts) {
|
|
var me = this;
|
|
|
|
opts = opts || {};
|
|
me.rows = me.rows || {};
|
|
|
|
var canEdit = (opts.caps === undefined || opts.caps);
|
|
me.rows[name] = {
|
|
required: true,
|
|
defaultValue: opts.defaultValue,
|
|
header: text,
|
|
renderer: opts.renderer,
|
|
editor: canEdit ? {
|
|
xtype: 'proxmoxWindowEdit',
|
|
width: 350,
|
|
subject: text,
|
|
fieldDefaults: {
|
|
labelWidth: opts.labelWidth || 100
|
|
},
|
|
setValues: function(values) {
|
|
// FIXME: run through parsePropertyString if not an object?
|
|
var edit_value = values[name];
|
|
Ext.Array.each(this.query('inputpanel'), function(panel) {
|
|
panel.setValues(edit_value);
|
|
});
|
|
},
|
|
url: opts.url,
|
|
items: [{
|
|
xtype: 'inputpanel',
|
|
onGetValues: function(values) {
|
|
if (values === undefined || Object.keys(values).length === 0) {
|
|
return { 'delete': name };
|
|
}
|
|
var ret_val = {};
|
|
ret_val[name] = PVE.Parser.printPropertyString(values);
|
|
return ret_val;
|
|
},
|
|
items: opts.items
|
|
}]
|
|
} : undefined
|
|
};
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
me.add_combobox_row('keyboard', gettext('Keyboard Layout'), {
|
|
renderer: PVE.Utils.render_kvm_language,
|
|
comboItems: PVE.Utils.kvm_keymap_array(),
|
|
defaultValue: '__default__',
|
|
deleteEmpty: true
|
|
});
|
|
me.add_text_row('http_proxy', gettext('HTTP proxy'), {
|
|
defaultValue: Proxmox.Utils.noneText,
|
|
vtype: 'HttpProxy',
|
|
deleteEmpty: true
|
|
});
|
|
me.add_combobox_row('console', gettext('Console Viewer'), {
|
|
renderer: PVE.Utils.render_console_viewer,
|
|
comboItems: PVE.Utils.console_viewer_array(),
|
|
defaultValue: '__default__',
|
|
deleteEmpty: true
|
|
});
|
|
me.add_text_row('email_from', gettext('Email from address'), {
|
|
deleteEmpty: true,
|
|
vtype: 'proxmoxMail',
|
|
defaultValue: 'root@$hostname'
|
|
});
|
|
me.add_text_row('mac_prefix', gettext('MAC address prefix'), {
|
|
deleteEmpty: true,
|
|
vtype: 'MacPrefix',
|
|
defaultValue: Proxmox.Utils.noneText
|
|
});
|
|
me.add_inputpanel_row('migration', gettext('Migration Settings'), {
|
|
renderer: PVE.Utils.render_dc_ha_opts,
|
|
caps: caps.vms['Sys.Modify'],
|
|
labelWidth: 120,
|
|
url: "/api2/extjs/cluster/options",
|
|
defaultKey: 'type',
|
|
items: [{
|
|
xtype: 'displayfield',
|
|
name: 'type',
|
|
fieldLabel: gettext('Type'),
|
|
value: 'secure',
|
|
submitValue: true,
|
|
}, {
|
|
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,
|
|
caps: caps.vms['Sys.Modify'],
|
|
labelWidth: 120,
|
|
url: "/api2/extjs/cluster/options",
|
|
items: [{
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'shutdown_policy',
|
|
fieldLabel: gettext('Shutdown Policy'),
|
|
deleteEmpty: false,
|
|
value: '__default__',
|
|
comboItems: [
|
|
['__default__', Proxmox.Utils.defaultText + ' (conditional)' ],
|
|
['freeze', 'freeze'],
|
|
['failover', 'failover'],
|
|
['conditional', 'conditional']
|
|
],
|
|
defaultValue: '__default__'
|
|
}]
|
|
});
|
|
|
|
// TODO: bwlimits, u2f?
|
|
|
|
me.selModel = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
Ext.apply(me, {
|
|
tbar: [{
|
|
text: gettext('Edit'),
|
|
xtype: 'proxmoxButton',
|
|
disabled: true,
|
|
handler: function() { me.run_editor(); },
|
|
selModel: me.selModel
|
|
}],
|
|
url: "/api2/json/cluster/options",
|
|
editorConfig: {
|
|
url: "/api2/extjs/cluster/options"
|
|
},
|
|
interval: 5000,
|
|
cwidth1: 200,
|
|
listeners: {
|
|
itemdblclick: me.run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
// set the new value for the default console
|
|
me.mon(me.rstore, 'load', function(store, records, success) {
|
|
if (!success) {
|
|
return;
|
|
}
|
|
|
|
var rec = store.getById('console');
|
|
PVE.VersionInfo.console = rec.data.value;
|
|
if (rec.data.value === '__default__') {
|
|
delete PVE.VersionInfo.console;
|
|
}
|
|
});
|
|
|
|
me.on('activate', me.rstore.startUpdate);
|
|
me.on('destroy', me.rstore.stopUpdate);
|
|
me.on('deactivate', me.rstore.stopUpdate);
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.StorageView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveStorageView'],
|
|
|
|
onlineHelp: 'chapter_storage',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-dc-storage',
|
|
|
|
createStorageEditWindow: function(type, sid) {
|
|
var schema = PVE.Utils.storageSchema[type];
|
|
if (!schema || !schema.ipanel) {
|
|
throw "no editor registered for storage type: " + type;
|
|
}
|
|
|
|
Ext.create('PVE.storage.BaseEdit', {
|
|
paneltype: 'PVE.storage.' + schema.ipanel,
|
|
type: type,
|
|
storageId: sid,
|
|
autoShow: true,
|
|
listeners: {
|
|
destroy: this.reloadStore
|
|
}
|
|
});
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-storage',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/storage"
|
|
},
|
|
sorters: {
|
|
property: 'storage',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var type = rec.data.type,
|
|
sid = rec.data.storage;
|
|
|
|
me.createStorageEditWindow(type, sid);
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: '/storage/',
|
|
callback: reload
|
|
});
|
|
|
|
// else we cannot dynamically generate the add menu handlers
|
|
var addHandleGenerator = function(type) {
|
|
return function() { me.createStorageEditWindow(type); };
|
|
};
|
|
var addMenuItems = [], type;
|
|
/*jslint forin: true */
|
|
for (type in PVE.Utils.storageSchema) {
|
|
var storage = PVE.Utils.storageSchema[type];
|
|
if (storage.hideAdd) {
|
|
continue;
|
|
}
|
|
addMenuItems.push({
|
|
text: PVE.Utils.format_storage_type(type),
|
|
iconCls: 'fa fa-fw fa-' + storage.faIcon,
|
|
handler: addHandleGenerator(type)
|
|
});
|
|
}
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
reloadStore: reload,
|
|
selModel: sm,
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
menu: new Ext.menu.Menu({
|
|
items: addMenuItems
|
|
})
|
|
},
|
|
remove_btn,
|
|
edit_btn
|
|
],
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
flex: 2,
|
|
sortable: true,
|
|
dataIndex: 'storage'
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
flex: 1,
|
|
sortable: true,
|
|
dataIndex: 'type',
|
|
renderer: PVE.Utils.format_storage_type
|
|
},
|
|
{
|
|
header: gettext('Content'),
|
|
flex: 3,
|
|
sortable: true,
|
|
dataIndex: 'content',
|
|
renderer: PVE.Utils.format_content_types
|
|
},
|
|
{
|
|
header: gettext('Path') + '/' + gettext('Target'),
|
|
flex: 2,
|
|
sortable: true,
|
|
dataIndex: 'path',
|
|
renderer: function(value, metaData, record) {
|
|
if (record.data.target) {
|
|
return record.data.target;
|
|
}
|
|
return value;
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Shared'),
|
|
flex: 1,
|
|
sortable: true,
|
|
dataIndex: 'shared',
|
|
renderer: Proxmox.Utils.format_boolean
|
|
},
|
|
{
|
|
header: gettext('Enabled'),
|
|
flex: 1,
|
|
sortable: true,
|
|
dataIndex: 'disable',
|
|
renderer: Proxmox.Utils.format_neg_boolean
|
|
},
|
|
{
|
|
header: gettext('Bandwidth Limit'),
|
|
flex: 2,
|
|
sortable: true,
|
|
dataIndex: 'bwlimit'
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: reload,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-storage', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'path', 'type', 'content', 'server', 'portal', 'target', 'export', 'storage',
|
|
{ name: 'shared', type: 'boolean'},
|
|
{ name: 'disable', type: 'boolean'}
|
|
],
|
|
idProperty: 'storage'
|
|
});
|
|
|
|
});
|
|
/*global u2f,QRCode,Uint8Array*/
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.window.TFAEdit', {
|
|
extend: 'Ext.window.Window',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
onlineHelp: 'pveum_tfa_auth', // fake to ensure this gets a link target
|
|
|
|
modal: true,
|
|
resizable: false,
|
|
title: gettext('Two Factor Authentication'),
|
|
subject: 'TFA',
|
|
url: '/api2/extjs/access/tfa',
|
|
width: 512,
|
|
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
|
|
updateQrCode: function() {
|
|
var me = this;
|
|
var values = me.lookup('totp_form').getValues();
|
|
var algorithm = values.algorithm;
|
|
if (!algorithm) {
|
|
algorithm = 'SHA1';
|
|
}
|
|
|
|
me.qrcode.makeCode(
|
|
'otpauth://totp/' + encodeURIComponent(me.userid) +
|
|
'?secret=' + values.secret +
|
|
'&period=' + values.step +
|
|
'&digits=' + values.digits +
|
|
'&algorithm=' + algorithm +
|
|
'&issuer=' + encodeURIComponent(values.issuer)
|
|
);
|
|
|
|
me.lookup('challenge').setVisible(true);
|
|
me.down('#qrbox').setVisible(true);
|
|
},
|
|
|
|
showError: function(error) {
|
|
Ext.Msg.alert(
|
|
gettext('Error'),
|
|
PVE.Utils.render_u2f_error(error)
|
|
);
|
|
},
|
|
|
|
doU2FChallenge: function(response) {
|
|
var me = this;
|
|
|
|
var data = response.result.data;
|
|
me.lookup('password').setDisabled(true);
|
|
var msg = Ext.Msg.show({
|
|
title: 'U2F: '+gettext('Setup'),
|
|
message: gettext('Please press the button on your U2F Device'),
|
|
buttons: []
|
|
});
|
|
Ext.Function.defer(function() {
|
|
u2f.register(data.appId, [data], [], function(data) {
|
|
msg.close();
|
|
if (data.errorCode) {
|
|
me.showError(data.errorCode);
|
|
} else {
|
|
me.respondToU2FChallenge(data);
|
|
}
|
|
});
|
|
}, 500, me);
|
|
},
|
|
|
|
respondToU2FChallenge: function(data) {
|
|
var me = this;
|
|
var params = {
|
|
userid: me.userid,
|
|
action: 'confirm',
|
|
response: JSON.stringify(data)
|
|
};
|
|
if (Proxmox.UserName !== 'root@pam') {
|
|
params.password = me.lookup('password').value;
|
|
}
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'PUT',
|
|
success: function() {
|
|
me.close();
|
|
Ext.Msg.show({
|
|
title: gettext('Success'),
|
|
message: gettext('U2F Device successfully connected.'),
|
|
buttons: Ext.Msg.OK
|
|
});
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
in_totp_tab: true,
|
|
tfa_required: false,
|
|
tfa_type: null, // dependencies of formulas should not be undefined
|
|
valid: false,
|
|
u2f_available: true
|
|
},
|
|
formulas: {
|
|
canDeleteTFA: function(get) {
|
|
return (get('tfa_type') !== null && !get('tfa_required'));
|
|
},
|
|
canSetupTOTP: function(get) {
|
|
var tfa = get('tfa_type');
|
|
return (tfa === null || tfa === 'totp' || tfa === 1);
|
|
},
|
|
canSetupU2F: function(get) {
|
|
var tfa = get('tfa_type');
|
|
return (get('u2f_available') && (tfa === null || tfa === 'u2f' || tfa === 1));
|
|
}
|
|
}
|
|
},
|
|
|
|
afterLoading: function(realm_tfa_type, user_tfa_type) {
|
|
var me = this;
|
|
var viewmodel = me.getViewModel();
|
|
if (user_tfa_type === 'oath') {
|
|
user_tfa_type = 'totp';
|
|
}
|
|
viewmodel.set('tfa_type', user_tfa_type || null);
|
|
if (!realm_tfa_type) {
|
|
// There's no TFA enforced by the realm, everything works.
|
|
viewmodel.set('u2f_available', true);
|
|
viewmodel.set('tfa_required', false);
|
|
} else if (realm_tfa_type === 'oath') {
|
|
// The realm explicitly requires TOTP
|
|
if (user_tfa_type !== 'totp' && user_tfa_type !== null) {
|
|
// user had a different tfa method, so
|
|
// we have to change back to the totp tab and
|
|
// generate a secret
|
|
viewmodel.set('tfa_type', null);
|
|
me.lookup('tfatabs').setActiveTab(me.lookup('totp_panel'));
|
|
me.getController().randomizeSecret();
|
|
}
|
|
viewmodel.set('tfa_required', true);
|
|
viewmodel.set('u2f_available', false);
|
|
} else {
|
|
// The realm enforces some other TFA type (yubico)
|
|
me.close();
|
|
Ext.Msg.alert(
|
|
gettext('Error'),
|
|
Ext.String.format(
|
|
gettext("Custom 2nd factor configuration is not supported on realms with '{0}' TFA."),
|
|
realm_tfa_type
|
|
)
|
|
);
|
|
}
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
control: {
|
|
'field[qrupdate=true]': {
|
|
change: function() {
|
|
var me = this.getView();
|
|
me.updateQrCode();
|
|
}
|
|
},
|
|
'field': {
|
|
validitychange: function(field, valid) {
|
|
var me = this;
|
|
var viewModel = me.getViewModel();
|
|
var form = me.lookup('totp_form');
|
|
var challenge = me.lookup('challenge');
|
|
var password = me.lookup('password');
|
|
viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
|
|
}
|
|
},
|
|
'#': {
|
|
show: function() {
|
|
var me = this.getView();
|
|
var viewmodel = this.getViewModel();
|
|
|
|
var loadMaskContainer = me.down('#tfatabs');
|
|
Proxmox.Utils.API2Request({
|
|
url: '/access/users/' + encodeURIComponent(me.userid) + '/tfa',
|
|
waitMsgTarget: loadMaskContainer,
|
|
method: 'GET',
|
|
success: function(response, opts) {
|
|
var data = response.result.data;
|
|
me.afterLoading(data.realm, data.user);
|
|
},
|
|
failure: function(response, opts) {
|
|
Proxmox.Utils.setErrorMask(loadMaskContainer, response.htmlStatus);
|
|
}
|
|
});
|
|
|
|
me.qrdiv = document.createElement('center');
|
|
me.qrcode = new QRCode(me.qrdiv, {
|
|
width: 256,
|
|
height: 256,
|
|
correctLevel: QRCode.CorrectLevel.M
|
|
});
|
|
me.down('#qrbox').getEl().appendChild(me.qrdiv);
|
|
|
|
viewmodel.set('tfa_type', me.tfa_type || null);
|
|
if (!me.tfa_type) {
|
|
this.randomizeSecret();
|
|
} else {
|
|
me.down('#qrbox').setVisible(false);
|
|
me.lookup('challenge').setVisible(false);
|
|
if (me.tfa_type === 'u2f') {
|
|
var u2f_panel = me.lookup('u2f_panel');
|
|
me.lookup('tfatabs').setActiveTab(u2f_panel);
|
|
}
|
|
}
|
|
|
|
if (Proxmox.UserName === 'root@pam') {
|
|
me.lookup('password').setVisible(false);
|
|
me.lookup('password').setDisabled(true);
|
|
}
|
|
}
|
|
},
|
|
'#tfatabs': {
|
|
tabchange: function(panel, newcard) {
|
|
var viewmodel = this.getViewModel();
|
|
viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel');
|
|
}
|
|
}
|
|
},
|
|
|
|
applySettings: function() {
|
|
var me = this;
|
|
var values = me.lookup('totp_form').getValues();
|
|
var params = {
|
|
userid: me.getView().userid,
|
|
action: 'new',
|
|
key: values.secret,
|
|
config: PVE.Parser.printPropertyString({
|
|
type: 'oath',
|
|
digits: values.digits,
|
|
step: values.step
|
|
}),
|
|
// this is used to verify that the client generates the correct codes:
|
|
response: me.lookup('challenge').value
|
|
};
|
|
|
|
if (Proxmox.UserName !== 'root@pam') {
|
|
params.password = me.lookup('password').value;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'PUT',
|
|
waitMsgTarget: me.getView(),
|
|
success: function(response, opts) {
|
|
me.getView().close();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
|
|
deleteTFA: function() {
|
|
var me = this;
|
|
var values = me.lookup('totp_form').getValues();
|
|
var params = {
|
|
userid: me.getView().userid,
|
|
action: 'delete'
|
|
};
|
|
|
|
if (Proxmox.UserName !== 'root@pam') {
|
|
params.password = me.lookup('password').value;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'PUT',
|
|
waitMsgTarget: me.getView(),
|
|
success: function(response, opts) {
|
|
me.getView().close();
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
},
|
|
|
|
randomizeSecret: function() {
|
|
var me = this;
|
|
var rnd = new Uint8Array(16);
|
|
window.crypto.getRandomValues(rnd);
|
|
var data = '';
|
|
rnd.forEach(function(b) {
|
|
// secret must be base32, so just use the first 5 bits
|
|
b = b & 0x1f;
|
|
if (b < 26) {
|
|
// A..Z
|
|
data += String.fromCharCode(b + 0x41);
|
|
} else {
|
|
// 2..7
|
|
data += String.fromCharCode(b-26 + 0x32);
|
|
}
|
|
});
|
|
me.lookup('tfa_secret').setValue(data);
|
|
},
|
|
|
|
startU2FRegistration: function() {
|
|
var me = this;
|
|
|
|
var params = {
|
|
userid: me.getView().userid,
|
|
action: 'new'
|
|
};
|
|
|
|
if (Proxmox.UserName !== 'root@pam') {
|
|
params.password = me.lookup('password').value;
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/api2/extjs/access/tfa',
|
|
params: params,
|
|
method: 'PUT',
|
|
waitMsgTarget: me.getView(),
|
|
success: function(response) {
|
|
me.getView().doU2FChallenge(response);
|
|
},
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'tabpanel',
|
|
itemId: 'tfatabs',
|
|
reference: 'tfatabs',
|
|
border: false,
|
|
items: [
|
|
{
|
|
xtype: 'panel',
|
|
title: 'TOTP',
|
|
itemId: 'totp-panel',
|
|
reference: 'totp_panel',
|
|
tfa_type: 'totp',
|
|
border: false,
|
|
bind: {
|
|
disabled: '{!canSetupTOTP}'
|
|
},
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'stretch'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'form',
|
|
layout: 'anchor',
|
|
border: false,
|
|
reference: 'totp_form',
|
|
fieldDefaults: {
|
|
anchor: '100%',
|
|
padding: '0 5'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('User name'),
|
|
cbind: {
|
|
value: '{userid}'
|
|
}
|
|
},
|
|
{
|
|
layout: 'hbox',
|
|
border: false,
|
|
padding: '0 0 5 0',
|
|
items: [{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Secret'),
|
|
emptyText: gettext('Unchanged'),
|
|
name: 'secret',
|
|
reference: 'tfa_secret',
|
|
regex: /^[A-Z2-7=]+$/,
|
|
regexText: 'Must be base32 [A-Z2-7=]',
|
|
maskRe: /[A-Z2-7=]/,
|
|
qrupdate: true,
|
|
flex: 4
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
text: gettext('Randomize'),
|
|
reference: 'randomize_button',
|
|
handler: 'randomizeSecret',
|
|
flex: 1
|
|
}]
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
fieldLabel: gettext('Time period'),
|
|
name: 'step',
|
|
// Google Authenticator ignores this and generates bogus data
|
|
hidden: true,
|
|
value: 30,
|
|
minValue: 10,
|
|
qrupdate: true
|
|
},
|
|
{
|
|
xtype: 'numberfield',
|
|
fieldLabel: gettext('Digits'),
|
|
name: 'digits',
|
|
value: 6,
|
|
// Google Authenticator ignores this and generates bogus data
|
|
hidden: true,
|
|
minValue: 6,
|
|
maxValue: 8,
|
|
qrupdate: true
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Issuer Name'),
|
|
name: 'issuer',
|
|
value: 'Proxmox Web UI',
|
|
qrupdate: true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'box',
|
|
itemId: 'qrbox',
|
|
visible: false, // will be enabled when generating a qr code
|
|
style: {
|
|
'background-color': 'white',
|
|
padding: '5px',
|
|
width: '266px',
|
|
height: '266px'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Verification Code'),
|
|
allowBlank: false,
|
|
reference: 'challenge',
|
|
padding: '0 5',
|
|
emptyText: gettext('Scan QR code and enter TOTP auth. code to verify')
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: 'U2F',
|
|
itemId: 'u2f-panel',
|
|
reference: 'u2f_panel',
|
|
tfa_type: 'u2f',
|
|
border: false,
|
|
padding: '5 5',
|
|
layout: {
|
|
type: 'vbox',
|
|
align: 'middle'
|
|
},
|
|
bind: {
|
|
disabled: '{!canSetupU2F}'
|
|
},
|
|
items: [
|
|
{
|
|
xtype: 'label',
|
|
width: 500,
|
|
text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.')
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
inputType: 'password',
|
|
fieldLabel: gettext('Password'),
|
|
minLength: 5,
|
|
reference: 'password',
|
|
allowBlank: false,
|
|
validateBlank: true,
|
|
padding: '0 0 5 5',
|
|
emptyText: gettext('verify current password')
|
|
}
|
|
],
|
|
|
|
buttons: [
|
|
{
|
|
xtype: 'proxmoxHelpButton'
|
|
},
|
|
'->',
|
|
{
|
|
text: gettext('Apply'),
|
|
handler: 'applySettings',
|
|
bind: {
|
|
hidden: '{!in_totp_tab}',
|
|
disabled: '{!valid}'
|
|
}
|
|
},
|
|
{
|
|
xtype: 'button',
|
|
text: gettext('Register U2F Device'),
|
|
handler: 'startU2FRegistration',
|
|
bind: {
|
|
hidden: '{in_totp_tab}',
|
|
disabled: '{tfa_type}'
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Delete'),
|
|
reference: 'delete_button',
|
|
disabled: true,
|
|
handler: 'deleteTFA',
|
|
bind: {
|
|
disabled: '{!canDeleteTFA}'
|
|
}
|
|
}
|
|
],
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
if (!me.userid) {
|
|
throw "no userid given";
|
|
}
|
|
|
|
me.callParent();
|
|
|
|
Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth');
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.UserEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveDcUserEdit'],
|
|
|
|
isAdd: true,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.userid;
|
|
|
|
var url;
|
|
var method;
|
|
var realm;
|
|
|
|
if (me.isCreate) {
|
|
url = '/api2/extjs/access/users';
|
|
method = 'POST';
|
|
} else {
|
|
url = '/api2/extjs/access/users/' + encodeURIComponent(me.userid);
|
|
method = 'PUT';
|
|
}
|
|
|
|
var verifypw;
|
|
var pwfield;
|
|
|
|
var validate_pw = function() {
|
|
if (verifypw.getValue() !== pwfield.getValue()) {
|
|
return gettext("Passwords do not match");
|
|
}
|
|
return true;
|
|
};
|
|
|
|
verifypw = Ext.createWidget('textfield', {
|
|
inputType: 'password',
|
|
fieldLabel: gettext('Confirm password'),
|
|
name: 'verifypassword',
|
|
submitValue: false,
|
|
disabled: true,
|
|
hidden: true,
|
|
validator: validate_pw
|
|
});
|
|
|
|
pwfield = Ext.createWidget('textfield', {
|
|
inputType: 'password',
|
|
fieldLabel: gettext('Password'),
|
|
minLength: 5,
|
|
name: 'password',
|
|
disabled: true,
|
|
hidden: true,
|
|
validator: validate_pw
|
|
});
|
|
|
|
var update_passwd_field = function(realm) {
|
|
if (realm === 'pve') {
|
|
pwfield.setVisible(true);
|
|
pwfield.setDisabled(false);
|
|
verifypw.setVisible(true);
|
|
verifypw.setDisabled(false);
|
|
} else {
|
|
pwfield.setVisible(false);
|
|
pwfield.setDisabled(true);
|
|
verifypw.setVisible(false);
|
|
verifypw.setDisabled(true);
|
|
}
|
|
|
|
};
|
|
|
|
var column1 = [
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'userid',
|
|
fieldLabel: gettext('User name'),
|
|
value: me.userid,
|
|
allowBlank: false,
|
|
submitValue: me.isCreate ? true : false
|
|
},
|
|
pwfield, verifypw,
|
|
{
|
|
xtype: 'pveGroupSelector',
|
|
name: 'groups',
|
|
multiSelect: true,
|
|
allowBlank: true,
|
|
fieldLabel: gettext('Group')
|
|
},
|
|
{
|
|
xtype: 'datefield',
|
|
name: 'expire',
|
|
emptyText: 'never',
|
|
format: 'Y-m-d',
|
|
submitFormat: 'U',
|
|
fieldLabel: gettext('Expire')
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Enabled'),
|
|
name: 'enable',
|
|
uncheckedValue: 0,
|
|
defaultValue: 1,
|
|
checked: true
|
|
}
|
|
];
|
|
|
|
var column2 = [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'firstname',
|
|
fieldLabel: gettext('First Name')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'lastname',
|
|
fieldLabel: gettext('Last Name')
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'email',
|
|
fieldLabel: gettext('E-Mail'),
|
|
vtype: 'proxmoxMail'
|
|
}
|
|
];
|
|
|
|
if (me.isCreate) {
|
|
column1.splice(1,0,{
|
|
xtype: 'pveRealmComboBox',
|
|
name: 'realm',
|
|
fieldLabel: gettext('Realm'),
|
|
allowBlank: false,
|
|
matchFieldWidth: false,
|
|
listConfig: { width: 300 },
|
|
listeners: {
|
|
change: function(combo, newValue){
|
|
realm = newValue;
|
|
update_passwd_field(realm);
|
|
}
|
|
},
|
|
submitValue: false
|
|
});
|
|
}
|
|
|
|
var ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
column1: column1,
|
|
column2: column2,
|
|
columnB: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
fieldLabel: gettext('Comment')
|
|
}
|
|
],
|
|
advancedItems: [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'keys',
|
|
fieldLabel: gettext('Key IDs')
|
|
}
|
|
],
|
|
onGetValues: function(values) {
|
|
// hack: ExtJS datefield does not submit 0, so we need to set that
|
|
if (!values.expire) {
|
|
values.expire = 0;
|
|
}
|
|
|
|
if (realm) {
|
|
values.userid = values.userid + '@' + realm;
|
|
}
|
|
|
|
if (!values.password) {
|
|
delete values.password;
|
|
}
|
|
|
|
return values;
|
|
}
|
|
});
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('User'),
|
|
url: url,
|
|
method: method,
|
|
fieldDefaults: {
|
|
labelWidth: 110 // for spanish translation
|
|
},
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var data = response.result.data;
|
|
if (Ext.isDefined(data.expire)) {
|
|
if (data.expire) {
|
|
data.expire = new Date(data.expire * 1000);
|
|
} else {
|
|
// display 'never' instead of '1970-01-01'
|
|
data.expire = null;
|
|
}
|
|
}
|
|
me.setValues(data);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
/*jslint confusion: true */
|
|
Ext.define('PVE.dc.UserView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveUserView'],
|
|
|
|
onlineHelp: 'pveum_users',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-users',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
var store = new Ext.data.Store({
|
|
id: "users",
|
|
model: 'pve-users',
|
|
sorters: {
|
|
property: 'userid',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: '/access/users/',
|
|
enableFn: function(rec) {
|
|
if (!caps.access['User.Modify']) {
|
|
return false;
|
|
}
|
|
return rec.data.userid !== 'root@pam';
|
|
},
|
|
callback: function() {
|
|
reload();
|
|
}
|
|
});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec || !caps.access['User.Modify']) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.dc.UserEdit',{
|
|
userid: rec.data.userid
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
enableFn: function(rec) {
|
|
return !!caps.access['User.Modify'];
|
|
},
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
var pwchange_btn = new Proxmox.button.Button({
|
|
text: gettext('Password'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: function(btn, event, rec) {
|
|
var win = Ext.create('Proxmox.window.PasswordEdit', {
|
|
userid: rec.data.userid
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
var tfachange_btn = new Proxmox.button.Button({
|
|
text: 'TFA',
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: function(btn, event, rec) {
|
|
var d = rec.data;
|
|
var tfa_type = PVE.Parser.parseTfaType(d.keys);
|
|
var win = Ext.create('PVE.window.TFAEdit',{
|
|
tfa_type: tfa_type,
|
|
userid: d.userid
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
});
|
|
|
|
var tbar = [
|
|
{
|
|
text: gettext('Add'),
|
|
disabled: !caps.access['User.Modify'],
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.UserEdit',{
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
edit_btn, remove_btn, pwchange_btn, tfachange_btn
|
|
];
|
|
|
|
var render_username = function(userid) {
|
|
return userid.match(/^(.+)(@[^@]+)$/)[1];
|
|
};
|
|
|
|
var render_realm = function(userid) {
|
|
return userid.match(/@([^@]+)$/)[1];
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: tbar,
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
columns: [
|
|
{
|
|
header: gettext('User name'),
|
|
width: 200,
|
|
sortable: true,
|
|
renderer: render_username,
|
|
dataIndex: 'userid'
|
|
},
|
|
{
|
|
header: gettext('Realm'),
|
|
width: 100,
|
|
sortable: true,
|
|
renderer: render_realm,
|
|
dataIndex: 'userid'
|
|
},
|
|
{
|
|
header: gettext('Enabled'),
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_boolean,
|
|
dataIndex: 'enable'
|
|
},
|
|
{
|
|
header: gettext('Expire'),
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: Proxmox.Utils.format_expire,
|
|
dataIndex: 'expire'
|
|
},
|
|
{
|
|
header: gettext('Name'),
|
|
width: 150,
|
|
sortable: true,
|
|
renderer: PVE.Utils.render_full_name,
|
|
dataIndex: 'firstname'
|
|
},
|
|
{
|
|
header: 'TFA',
|
|
width: 50,
|
|
sortable: true,
|
|
renderer: function(v) {
|
|
var tfa_type = PVE.Parser.parseTfaType(v);
|
|
if (tfa_type === undefined) {
|
|
return Proxmox.Utils.noText;
|
|
} else if (tfa_type === 1) {
|
|
return Proxmox.Utils.yesText;
|
|
} else {
|
|
return tfa_type;
|
|
}
|
|
},
|
|
dataIndex: 'keys'
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
sortable: false,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'comment',
|
|
flex: 1
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: reload,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.PoolView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pvePoolView'],
|
|
|
|
onlineHelp: 'pveum_pools',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-pools',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-pools',
|
|
sorters: {
|
|
property: 'poolid',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: '/pools/',
|
|
callback: function () {
|
|
reload();
|
|
}
|
|
});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.dc.PoolEdit',{
|
|
poolid: rec.data.poolid
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
var tbar = [
|
|
{
|
|
text: gettext('Create'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.PoolEdit', {});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
edit_btn, remove_btn
|
|
];
|
|
|
|
Proxmox.Utils.monStoreErrors(me, store);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: tbar,
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
width: 200,
|
|
sortable: true,
|
|
dataIndex: 'poolid'
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
sortable: false,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'comment',
|
|
flex: 1
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: reload,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.PoolEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveDcPoolEdit'],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.poolid;
|
|
|
|
var url;
|
|
var method;
|
|
|
|
if (me.isCreate) {
|
|
url = '/api2/extjs/pools';
|
|
method = 'POST';
|
|
} else {
|
|
url = '/api2/extjs/pools/' + me.poolid;
|
|
method = 'PUT';
|
|
}
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('Pool'),
|
|
url: url,
|
|
method: method,
|
|
items: [
|
|
{
|
|
xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
|
|
fieldLabel: gettext('Name'),
|
|
name: 'poolid',
|
|
value: me.poolid,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Comment'),
|
|
name: 'comment',
|
|
allowBlank: true
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load();
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.GroupView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveGroupView'],
|
|
|
|
onlineHelp: 'pveum_groups',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-groups',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-groups',
|
|
sorters: {
|
|
property: 'groupid',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
callback: function() {
|
|
reload();
|
|
},
|
|
baseurl: '/access/groups/'
|
|
});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.dc.GroupEdit',{
|
|
groupid: rec.data.groupid
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
var tbar = [
|
|
{
|
|
text: gettext('Create'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.GroupEdit', {});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
edit_btn, remove_btn
|
|
];
|
|
|
|
Proxmox.Utils.monStoreErrors(me, store);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: tbar,
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
width: 200,
|
|
sortable: true,
|
|
dataIndex: 'groupid'
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
sortable: false,
|
|
renderer: Ext.String.htmlEncode,
|
|
dataIndex: 'comment',
|
|
flex: 1
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: reload,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.GroupEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveDcGroupEdit'],
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.groupid;
|
|
|
|
var url;
|
|
var method;
|
|
|
|
if (me.isCreate) {
|
|
url = '/api2/extjs/access/groups';
|
|
method = 'POST';
|
|
} else {
|
|
url = '/api2/extjs/access/groups/' + me.groupid;
|
|
method = 'PUT';
|
|
}
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('Group'),
|
|
url: url,
|
|
method: method,
|
|
items: [
|
|
{
|
|
xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
|
|
fieldLabel: gettext('Name'),
|
|
name: 'groupid',
|
|
value: me.groupid,
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Comment'),
|
|
name: 'comment',
|
|
allowBlank: true
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load();
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.RoleView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveRoleView'],
|
|
|
|
onlineHelp: 'pveum_roles',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-roles',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-roles',
|
|
sorters: {
|
|
property: 'roleid',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var render_privs = function(value, metaData) {
|
|
|
|
if (!value) {
|
|
return '-';
|
|
}
|
|
|
|
// allow word wrap
|
|
metaData.style = 'white-space:normal;';
|
|
|
|
return value.replace(/\,/g, ' ');
|
|
};
|
|
|
|
Proxmox.Utils.monStoreErrors(me, store);
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
if (rec.data.special === "1") {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.dc.RoleEdit',{
|
|
roleid: rec.data.roleid,
|
|
privs: rec.data.privs
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
columns: [
|
|
{
|
|
header: gettext('Built-In'),
|
|
width: 65,
|
|
sortable: true,
|
|
dataIndex: 'special',
|
|
renderer: Proxmox.Utils.format_boolean
|
|
},
|
|
{
|
|
header: gettext('Name'),
|
|
width: 150,
|
|
sortable: true,
|
|
dataIndex: 'roleid'
|
|
},
|
|
{
|
|
itemid: 'privs',
|
|
header: gettext('Privileges'),
|
|
sortable: false,
|
|
renderer: render_privs,
|
|
dataIndex: 'privs',
|
|
flex: 1
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: function() {
|
|
store.load();
|
|
},
|
|
itemdblclick: run_editor
|
|
},
|
|
tbar: [
|
|
{
|
|
text: gettext('Create'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.RoleEdit', {});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxButton',
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor,
|
|
enableFn: function(record) {
|
|
return record.data.special !== '1';
|
|
}
|
|
},
|
|
{
|
|
xtype: 'proxmoxStdRemoveButton',
|
|
selModel: sm,
|
|
callback: function() {
|
|
reload();
|
|
},
|
|
baseurl: '/access/roles/',
|
|
enableFn: function(record) {
|
|
return record.data.special !== '1';
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.RoleEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveDcRoleEdit',
|
|
|
|
width: 400,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.roleid;
|
|
|
|
var url;
|
|
var method;
|
|
|
|
if (me.isCreate) {
|
|
url = '/api2/extjs/access/roles';
|
|
method = 'POST';
|
|
} else {
|
|
url = '/api2/extjs/access/roles/' + me.roleid;
|
|
method = 'PUT';
|
|
}
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext('Role'),
|
|
url: url,
|
|
method: method,
|
|
items: [
|
|
{
|
|
xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield',
|
|
name: 'roleid',
|
|
value: me.roleid,
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Name')
|
|
},
|
|
{
|
|
xtype: 'pvePrivilegesSelector',
|
|
name: 'privs',
|
|
value: me.privs,
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Privileges')
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response) {
|
|
var data = response.result.data;
|
|
var keys = Ext.Object.getKeys(data);
|
|
|
|
me.setValues({
|
|
privs: keys,
|
|
roleid: me.roleid
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.ACLAdd', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveACLAdd'],
|
|
url: '/access/acl',
|
|
method: 'PUT',
|
|
isAdd: true,
|
|
initComponent : function() {
|
|
|
|
var me = this;
|
|
|
|
me.isCreate = true;
|
|
|
|
var items = [
|
|
{
|
|
xtype: me.path ? 'hiddenfield' : 'pvePermPathSelector',
|
|
name: 'path',
|
|
value: me.path,
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Path')
|
|
}
|
|
];
|
|
|
|
if (me.aclType === 'group') {
|
|
me.subject = gettext("Group Permission");
|
|
items.push({
|
|
xtype: 'pveGroupSelector',
|
|
name: 'groups',
|
|
fieldLabel: gettext('Group')
|
|
});
|
|
} else if (me.aclType === 'user') {
|
|
me.subject = gettext("User Permission");
|
|
items.push({
|
|
xtype: 'pveUserSelector',
|
|
name: 'users',
|
|
fieldLabel: gettext('User')
|
|
});
|
|
} else {
|
|
throw "unknown ACL type";
|
|
}
|
|
|
|
items.push({
|
|
xtype: 'pveRoleSelector',
|
|
name: 'roles',
|
|
value: 'NoAccess',
|
|
fieldLabel: gettext('Role')
|
|
});
|
|
|
|
if (!me.path) {
|
|
items.push({
|
|
xtype: 'proxmoxcheckbox',
|
|
name: 'propagate',
|
|
checked: true,
|
|
uncheckedValue: 0,
|
|
fieldLabel: gettext('Propagate')
|
|
});
|
|
}
|
|
|
|
var ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
items: items,
|
|
onlineHelp: 'pveum_permission_management'
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.dc.ACLView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveACLView'],
|
|
|
|
onlineHelp: 'chapter_user_management',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-acls',
|
|
|
|
// use fixed path
|
|
path: undefined,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = Ext.create('Ext.data.Store',{
|
|
model: 'pve-acl',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/access/acl"
|
|
},
|
|
sorters: {
|
|
property: 'path',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
if (me.path) {
|
|
store.addFilter(Ext.create('Ext.util.Filter',{
|
|
filterFn: function(item) {
|
|
if (item.data.path === me.path) {
|
|
return true;
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
var render_ugid = function(ugid, metaData, record) {
|
|
if (record.data.type == 'group') {
|
|
return '@' + ugid;
|
|
}
|
|
|
|
return ugid;
|
|
};
|
|
|
|
var columns = [
|
|
{
|
|
header: gettext('User') + '/' + gettext('Group'),
|
|
flex: 1,
|
|
sortable: true,
|
|
renderer: render_ugid,
|
|
dataIndex: 'ugid'
|
|
},
|
|
{
|
|
header: gettext('Role'),
|
|
flex: 1,
|
|
sortable: true,
|
|
dataIndex: 'roleid'
|
|
}
|
|
];
|
|
|
|
if (!me.path) {
|
|
columns.unshift({
|
|
header: gettext('Path'),
|
|
flex: 1,
|
|
sortable: true,
|
|
dataIndex: 'path'
|
|
});
|
|
columns.push({
|
|
header: gettext('Propagate'),
|
|
width: 80,
|
|
sortable: true,
|
|
dataIndex: 'propagate'
|
|
});
|
|
}
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var remove_btn = new Proxmox.button.Button({
|
|
text: gettext('Remove'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
confirmMsg: gettext('Are you sure you want to remove this entry'),
|
|
handler: function(btn, event, rec) {
|
|
var params = {
|
|
'delete': 1,
|
|
path: rec.data.path,
|
|
roles: rec.data.roleid
|
|
};
|
|
if (rec.data.type === 'group') {
|
|
params.groups = rec.data.ugid;
|
|
} else if (rec.data.type === 'user') {
|
|
params.users = rec.data.ugid;
|
|
} else {
|
|
throw 'unknown data type';
|
|
}
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/access/acl',
|
|
params: params,
|
|
method: 'PUT',
|
|
waitMsgTarget: me,
|
|
callback: function() {
|
|
reload();
|
|
},
|
|
failure: function (response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Proxmox.Utils.monStoreErrors(me, store);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
menu: {
|
|
xtype: 'menu',
|
|
items: [
|
|
{
|
|
text: gettext('Group Permission'),
|
|
iconCls: 'fa fa-fw fa-group',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.ACLAdd',{
|
|
aclType: 'group',
|
|
path: me.path
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('User Permission'),
|
|
iconCls: 'fa fa-fw fa-user',
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.ACLAdd',{
|
|
aclType: 'user',
|
|
path: me.path
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
}
|
|
]
|
|
}
|
|
},
|
|
remove_btn
|
|
],
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
columns: columns,
|
|
listeners: {
|
|
activate: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-acl', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'path', 'type', 'ugid', 'roleid',
|
|
{
|
|
name: 'propagate',
|
|
type: 'boolean'
|
|
}
|
|
]
|
|
});
|
|
|
|
});
|
|
Ext.define('PVE.dc.AuthView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveAuthView'],
|
|
|
|
onlineHelp: 'pveum_authentication_realms',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-authrealms',
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-domains',
|
|
sorters: {
|
|
property: 'realm',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.dc.AuthEdit',{
|
|
realm: rec.data.realm,
|
|
authType: rec.data.type
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
baseurl: '/access/domains/',
|
|
selModel: sm,
|
|
enableFn: function(rec) {
|
|
return !(rec.data.type === 'pve' || rec.data.type === 'pam');
|
|
},
|
|
callback: function() {
|
|
reload();
|
|
}
|
|
});
|
|
|
|
var tbar = [
|
|
{
|
|
text: gettext('Add'),
|
|
menu: new Ext.menu.Menu({
|
|
items: [
|
|
{
|
|
text: gettext('Active Directory Server'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.AuthEdit', {
|
|
authType: 'ad'
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: gettext('LDAP Server'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.AuthEdit',{
|
|
authType: 'ldap'
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
}
|
|
]
|
|
})
|
|
},
|
|
edit_btn, remove_btn
|
|
];
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
tbar: tbar,
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
columns: [
|
|
{
|
|
header: gettext('Realm'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'realm'
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'type'
|
|
},
|
|
{
|
|
header: gettext('TFA'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'tfa'
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
sortable: false,
|
|
dataIndex: 'comment',
|
|
renderer: Ext.String.htmlEncode,
|
|
flex: 1
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: reload,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.AuthEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveDcAuthEdit'],
|
|
|
|
isAdd: true,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.realm;
|
|
|
|
var url;
|
|
var method;
|
|
var serverlist;
|
|
|
|
if (me.isCreate) {
|
|
url = '/api2/extjs/access/domains';
|
|
method = 'POST';
|
|
} else {
|
|
url = '/api2/extjs/access/domains/' + me.realm;
|
|
method = 'PUT';
|
|
}
|
|
|
|
var column1 = [
|
|
{
|
|
xtype: me.isCreate ? 'textfield' : 'displayfield',
|
|
name: 'realm',
|
|
fieldLabel: gettext('Realm'),
|
|
value: me.realm,
|
|
allowBlank: false
|
|
}
|
|
];
|
|
|
|
if (me.authType === 'ad') {
|
|
|
|
me.subject = gettext('Active Directory Server');
|
|
|
|
column1.push({
|
|
xtype: 'textfield',
|
|
name: 'domain',
|
|
fieldLabel: gettext('Domain'),
|
|
emptyText: 'company.net',
|
|
allowBlank: false
|
|
});
|
|
|
|
} else if (me.authType === 'ldap') {
|
|
|
|
me.subject = gettext('LDAP Server');
|
|
|
|
column1.push({
|
|
xtype: 'textfield',
|
|
name: 'base_dn',
|
|
fieldLabel: gettext('Base Domain Name'),
|
|
emptyText: 'CN=Users,DC=Company,DC=net',
|
|
allowBlank: false
|
|
});
|
|
|
|
column1.push({
|
|
xtype: 'textfield',
|
|
name: 'user_attr',
|
|
emptyText: 'uid / sAMAccountName',
|
|
fieldLabel: gettext('User Attribute Name'),
|
|
allowBlank: false
|
|
});
|
|
} else if (me.authType === 'pve') {
|
|
|
|
if (me.isCreate) {
|
|
throw 'unknown auth type';
|
|
}
|
|
|
|
me.subject = 'Proxmox VE authentication server';
|
|
|
|
} else if (me.authType === 'pam') {
|
|
|
|
if (me.isCreate) {
|
|
throw 'unknown auth type';
|
|
}
|
|
|
|
me.subject = 'linux PAM';
|
|
|
|
} else {
|
|
throw 'unknown auth type ';
|
|
}
|
|
|
|
column1.push({
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Default'),
|
|
name: 'default',
|
|
uncheckedValue: 0
|
|
});
|
|
|
|
var column2 = [];
|
|
|
|
if (me.authType === 'ldap' || me.authType === 'ad') {
|
|
column2.push(
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Server'),
|
|
name: 'server1',
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxtextfield',
|
|
fieldLabel: gettext('Fallback Server'),
|
|
deleteEmpty: !me.isCreate,
|
|
name: 'server2'
|
|
},
|
|
{
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'port',
|
|
fieldLabel: gettext('Port'),
|
|
minValue: 1,
|
|
maxValue: 65535,
|
|
emptyText: gettext('Default'),
|
|
submitEmptyText: false
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: 'SSL',
|
|
name: 'secure',
|
|
uncheckedValue: 0
|
|
}
|
|
);
|
|
}
|
|
|
|
// Two Factor Auth settings
|
|
|
|
column2.push({
|
|
xtype: 'proxmoxKVComboBox',
|
|
name: 'tfa',
|
|
deleteEmpty: !me.isCreate,
|
|
value: '',
|
|
fieldLabel: gettext('TFA'),
|
|
comboItems: [ ['__default__', Proxmox.Utils.noneText], ['oath', 'OATH'], ['yubico', 'Yubico']],
|
|
listeners: {
|
|
change: function(f, value) {
|
|
if (!me.rendered) {
|
|
return;
|
|
}
|
|
me.down('field[name=oath_step]').setVisible(value === 'oath');
|
|
me.down('field[name=oath_digits]').setVisible(value === 'oath');
|
|
me.down('field[name=yubico_api_id]').setVisible(value === 'yubico');
|
|
me.down('field[name=yubico_api_key]').setVisible(value === 'yubico');
|
|
me.down('field[name=yubico_url]').setVisible(value === 'yubico');
|
|
}
|
|
}
|
|
});
|
|
|
|
column2.push({
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'oath_step',
|
|
value: '',
|
|
minValue: 10,
|
|
emptyText: Proxmox.Utils.defaultText + ' (30)',
|
|
submitEmptyText: false,
|
|
hidden: true,
|
|
fieldLabel: 'OATH time step'
|
|
});
|
|
|
|
column2.push({
|
|
xtype: 'proxmoxintegerfield',
|
|
name: 'oath_digits',
|
|
value: '',
|
|
minValue: 6,
|
|
maxValue: 8,
|
|
emptyText: Proxmox.Utils.defaultText + ' (6)',
|
|
submitEmptyText: false,
|
|
hidden: true,
|
|
fieldLabel: 'OATH password length'
|
|
});
|
|
|
|
column2.push({
|
|
xtype: 'textfield',
|
|
name: 'yubico_api_id',
|
|
hidden: true,
|
|
fieldLabel: 'Yubico API Id'
|
|
});
|
|
|
|
column2.push({
|
|
xtype: 'textfield',
|
|
name: 'yubico_api_key',
|
|
hidden: true,
|
|
fieldLabel: 'Yubico API Key'
|
|
});
|
|
|
|
column2.push({
|
|
xtype: 'textfield',
|
|
name: 'yubico_url',
|
|
hidden: true,
|
|
fieldLabel: 'Yubico URL'
|
|
});
|
|
|
|
var ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
column1: column1,
|
|
column2: column2,
|
|
columnB: [{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
fieldLabel: gettext('Comment')
|
|
}],
|
|
onGetValues: function(values) {
|
|
if (!values.port) {
|
|
if (!me.isCreate) {
|
|
Proxmox.Utils.assemble_field_data(values, { 'delete': 'port' });
|
|
}
|
|
delete values.port;
|
|
}
|
|
|
|
if (me.isCreate) {
|
|
values.type = me.authType;
|
|
}
|
|
|
|
if (values.tfa === 'oath') {
|
|
values.tfa = "type=oath";
|
|
if (values.oath_step) {
|
|
values.tfa += ",step=" + values.oath_step;
|
|
}
|
|
if (values.oath_digits) {
|
|
values.tfa += ",digits=" + values.oath_digits;
|
|
}
|
|
} else if (values.tfa === 'yubico') {
|
|
values.tfa = "type=yubico";
|
|
values.tfa += ",id=" + values.yubico_api_id;
|
|
values.tfa += ",key=" + values.yubico_api_key;
|
|
if (values.yubico_url) {
|
|
values.tfa += ",url=" + values.yubico_url;
|
|
}
|
|
} else {
|
|
delete values.tfa;
|
|
}
|
|
|
|
delete values.oath_step;
|
|
delete values.oath_digits;
|
|
delete values.yubico_api_id;
|
|
delete values.yubico_api_key;
|
|
delete values.yubico_url;
|
|
|
|
return values;
|
|
}
|
|
});
|
|
|
|
Ext.applyIf(me, {
|
|
url: url,
|
|
method: method,
|
|
fieldDefaults: {
|
|
labelWidth: 120
|
|
},
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (!me.isCreate) {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var data = response.result.data || {};
|
|
// just to be sure (should not happen)
|
|
if (data.type !== me.authType) {
|
|
me.close();
|
|
throw "got wrong auth type";
|
|
}
|
|
|
|
if (data.tfa) {
|
|
var tfacfg = PVE.Parser.parseTfaConfig(data.tfa);
|
|
data.tfa = tfacfg.type;
|
|
if (tfacfg.type === 'yubico') {
|
|
data.yubico_api_key = tfacfg.key;
|
|
data.yubico_api_id = tfacfg.id;
|
|
data.yubico_url = tfacfg.url;
|
|
} else if (tfacfg.type === 'oath') {
|
|
// step is a number before
|
|
/*jslint confusion: true*/
|
|
data.oath_step = tfacfg.step;
|
|
data.oath_digits = tfacfg.digits;
|
|
/*jslint confusion: false*/
|
|
}
|
|
}
|
|
|
|
me.setValues(data);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.BackupEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveDcBackupEdit'],
|
|
|
|
defaultFocus: undefined,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = !me.jobid;
|
|
|
|
var url;
|
|
var method;
|
|
|
|
if (me.isCreate) {
|
|
url = '/api2/extjs/cluster/backup';
|
|
method = 'POST';
|
|
} else {
|
|
url = '/api2/extjs/cluster/backup/' + me.jobid;
|
|
method = 'PUT';
|
|
}
|
|
|
|
var vmidField = Ext.create('Ext.form.field.Hidden', {
|
|
name: 'vmid'
|
|
});
|
|
|
|
/*jslint confusion: true*/
|
|
// 'value' can be assigned a string or an array
|
|
var selModeField = Ext.create('Proxmox.form.KVComboBox', {
|
|
xtype: 'proxmoxKVComboBox',
|
|
comboItems: [
|
|
['include', gettext('Include selected VMs')],
|
|
['all', gettext('All')],
|
|
['exclude', gettext('Exclude selected VMs')],
|
|
['pool', gettext('Pool based')]
|
|
],
|
|
fieldLabel: gettext('Selection mode'),
|
|
name: 'selMode',
|
|
value: ''
|
|
});
|
|
|
|
var sm = Ext.create('Ext.selection.CheckboxModel', {
|
|
mode: 'SIMPLE',
|
|
listeners: {
|
|
selectionchange: function(model, selected) {
|
|
var sel = [];
|
|
Ext.Array.each(selected, function(record) {
|
|
sel.push(record.data.vmid);
|
|
});
|
|
|
|
// to avoid endless recursion suspend the vmidField change
|
|
// event temporary as it calls us again
|
|
vmidField.suspendEvent('change');
|
|
vmidField.setValue(sel);
|
|
vmidField.resumeEvent('change');
|
|
}
|
|
}
|
|
});
|
|
|
|
var storagesel = Ext.create('PVE.form.StorageSelector', {
|
|
fieldLabel: gettext('Storage'),
|
|
nodename: 'localhost',
|
|
storageContent: 'backup',
|
|
allowBlank: false,
|
|
name: 'storage'
|
|
});
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'PVEResources',
|
|
sorters: {
|
|
property: 'vmid',
|
|
order: 'ASC'
|
|
}
|
|
});
|
|
|
|
var vmgrid = Ext.createWidget('grid', {
|
|
store: store,
|
|
border: true,
|
|
height: 300,
|
|
selModel: sm,
|
|
disabled: true,
|
|
columns: [
|
|
{
|
|
header: 'ID',
|
|
dataIndex: 'vmid',
|
|
width: 60
|
|
},
|
|
{
|
|
header: gettext('Node'),
|
|
dataIndex: 'node'
|
|
},
|
|
{
|
|
header: gettext('Status'),
|
|
dataIndex: 'uptime',
|
|
renderer: function(value) {
|
|
if (value) {
|
|
return Proxmox.Utils.runningText;
|
|
} else {
|
|
return Proxmox.Utils.stoppedText;
|
|
}
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
dataIndex: 'type'
|
|
}
|
|
]
|
|
});
|
|
|
|
var selectPoolMembers = function(poolid) {
|
|
if (!poolid) {
|
|
return;
|
|
}
|
|
sm.deselectAll(true);
|
|
store.filter([
|
|
{
|
|
id: 'poolFilter',
|
|
property: 'pool',
|
|
value: poolid
|
|
}
|
|
]);
|
|
sm.selectAll(true);
|
|
};
|
|
|
|
var selPool = Ext.create('PVE.form.PoolSelector', {
|
|
fieldLabel: gettext('Pool to backup'),
|
|
hidden: true,
|
|
allowBlank: true,
|
|
name: 'pool',
|
|
listeners: {
|
|
change: function( selpool, newValue, oldValue) {
|
|
selectPoolMembers(newValue);
|
|
}
|
|
}
|
|
});
|
|
|
|
var nodesel = Ext.create('PVE.form.NodeSelector', {
|
|
name: 'node',
|
|
fieldLabel: gettext('Node'),
|
|
allowBlank: true,
|
|
editable: true,
|
|
autoSelect: false,
|
|
emptyText: '-- ' + gettext('All') + ' --',
|
|
listeners: {
|
|
change: function(f, value) {
|
|
storagesel.setNodename(value || 'localhost');
|
|
var mode = selModeField.getValue();
|
|
store.clearFilter();
|
|
store.filterBy(function(rec) {
|
|
return (!value || rec.get('node') === value);
|
|
});
|
|
if (mode === 'all') {
|
|
sm.selectAll(true);
|
|
}
|
|
|
|
if (mode === 'pool') {
|
|
selectPoolMembers(selPool.value);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
var column1 = [
|
|
nodesel,
|
|
storagesel,
|
|
{
|
|
xtype: 'pveDayOfWeekSelector',
|
|
name: 'dow',
|
|
fieldLabel: gettext('Day of week'),
|
|
multiSelect: true,
|
|
value: ['sat'],
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'timefield',
|
|
fieldLabel: gettext('Start Time'),
|
|
name: 'starttime',
|
|
format: 'H:i',
|
|
formatText: 'HH:MM',
|
|
value: '00:00',
|
|
allowBlank: false
|
|
},
|
|
selModeField,
|
|
selPool
|
|
];
|
|
|
|
var column2 = [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Send email to'),
|
|
name: 'mailto'
|
|
},
|
|
{
|
|
xtype: 'pveEmailNotificationSelector',
|
|
fieldLabel: gettext('Email notification'),
|
|
name: 'mailnotification',
|
|
deleteEmpty: me.isCreate ? false : true,
|
|
value: me.isCreate ? 'always' : ''
|
|
},
|
|
{
|
|
xtype: 'pveCompressionSelector',
|
|
fieldLabel: gettext('Compression'),
|
|
name: 'compress',
|
|
deleteEmpty: me.isCreate ? false : true,
|
|
value: 'lzo'
|
|
},
|
|
{
|
|
xtype: 'pveBackupModeSelector',
|
|
fieldLabel: gettext('Mode'),
|
|
value: 'snapshot',
|
|
name: 'mode'
|
|
},
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Enable'),
|
|
name: 'enabled',
|
|
uncheckedValue: 0,
|
|
defaultValue: 1,
|
|
checked: true
|
|
},
|
|
vmidField
|
|
];
|
|
/*jslint confusion: false*/
|
|
|
|
var ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
onlineHelp: 'chapter_vzdump',
|
|
column1: column1,
|
|
column2: column2,
|
|
onGetValues: function(values) {
|
|
if (!values.node) {
|
|
if (!me.isCreate) {
|
|
Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
|
|
}
|
|
delete values.node;
|
|
}
|
|
|
|
var selMode = values.selMode;
|
|
delete values.selMode;
|
|
|
|
if (selMode === 'all') {
|
|
values.all = 1;
|
|
values.exclude = '';
|
|
delete values.vmid;
|
|
} else if (selMode === 'exclude') {
|
|
values.all = 1;
|
|
values.exclude = values.vmid;
|
|
delete values.vmid;
|
|
} else if (selMode === 'pool') {
|
|
delete values.vmid;
|
|
}
|
|
|
|
if (selMode !== 'pool') {
|
|
delete values.pool;
|
|
}
|
|
return values;
|
|
}
|
|
});
|
|
|
|
var update_vmid_selection = function(list, mode) {
|
|
if (mode !== 'all' && mode !== 'pool') {
|
|
sm.deselectAll(true);
|
|
if (list) {
|
|
Ext.Array.each(list.split(','), function(vmid) {
|
|
var rec = store.findRecord('vmid', vmid);
|
|
if (rec) {
|
|
sm.select(rec, true);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
vmidField.on('change', function(f, value) {
|
|
var mode = selModeField.getValue();
|
|
update_vmid_selection(value, mode);
|
|
});
|
|
|
|
selModeField.on('change', function(f, value, oldValue) {
|
|
if (oldValue === 'pool') {
|
|
store.removeFilter('poolFilter');
|
|
}
|
|
|
|
if (oldValue === 'all') {
|
|
sm.deselectAll(true);
|
|
vmidField.setValue('');
|
|
}
|
|
|
|
if (value === 'all') {
|
|
sm.selectAll(true);
|
|
vmgrid.setDisabled(true);
|
|
} else {
|
|
vmgrid.setDisabled(false);
|
|
}
|
|
|
|
if (value === 'pool') {
|
|
vmgrid.setDisabled(true);
|
|
vmidField.setValue('');
|
|
selPool.setVisible(true);
|
|
selPool.allowBlank = false;
|
|
selectPoolMembers(selPool.value);
|
|
|
|
} else {
|
|
selPool.setVisible(false);
|
|
selPool.allowBlank = true;
|
|
}
|
|
var list = vmidField.getValue();
|
|
update_vmid_selection(list, value);
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load({
|
|
params: { type: 'vm' },
|
|
callback: function() {
|
|
var node = nodesel.getValue();
|
|
store.clearFilter();
|
|
store.filterBy(function(rec) {
|
|
return (!node || node.length === 0 || rec.get('node') === node);
|
|
});
|
|
var list = vmidField.getValue();
|
|
var mode = selModeField.getValue();
|
|
if (mode === 'all') {
|
|
sm.selectAll(true);
|
|
} else if (mode === 'pool'){
|
|
selectPoolMembers(selPool.value);
|
|
} else {
|
|
update_vmid_selection(list, mode);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
Ext.applyIf(me, {
|
|
subject: gettext("Backup Job"),
|
|
url: url,
|
|
method: method,
|
|
items: [ ipanel, vmgrid ]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
if (me.isCreate) {
|
|
selModeField.setValue('include');
|
|
} else {
|
|
me.load({
|
|
success: function(response, options) {
|
|
var data = response.result.data;
|
|
|
|
data.dow = data.dow.split(',');
|
|
|
|
if (data.all || data.exclude) {
|
|
if (data.exclude) {
|
|
data.vmid = data.exclude;
|
|
data.selMode = 'exclude';
|
|
} else {
|
|
data.vmid = '';
|
|
data.selMode = 'all';
|
|
}
|
|
} else if (data.pool) {
|
|
data.selMode = 'pool';
|
|
data.selPool = data.pool;
|
|
} else {
|
|
data.selMode = 'include';
|
|
}
|
|
|
|
me.setValues(data);
|
|
}
|
|
});
|
|
}
|
|
|
|
reload();
|
|
}
|
|
});
|
|
|
|
|
|
Ext.define('PVE.dc.BackupView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveDcBackupView'],
|
|
|
|
onlineHelp: 'chapter_vzdump',
|
|
|
|
allText: '-- ' + gettext('All') + ' --',
|
|
allExceptText: gettext('All except {0}'),
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-cluster-backup',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/cluster/backup"
|
|
}
|
|
});
|
|
|
|
var reload = function() {
|
|
store.load();
|
|
};
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
var win = Ext.create('PVE.dc.BackupEdit',{
|
|
jobid: rec.data.id
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: '/cluster/backup',
|
|
callback: function() {
|
|
reload();
|
|
}
|
|
});
|
|
|
|
Proxmox.Utils.monStoreErrors(me, store);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
stateful: true,
|
|
stateId: 'grid-dc-backup',
|
|
viewConfig: {
|
|
trackOver: false
|
|
},
|
|
tbar: [
|
|
{
|
|
text: gettext('Add'),
|
|
handler: function() {
|
|
var win = Ext.create('PVE.dc.BackupEdit',{});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
}
|
|
},
|
|
remove_btn,
|
|
edit_btn
|
|
],
|
|
columns: [
|
|
{
|
|
header: gettext('Enabled'),
|
|
width: 80,
|
|
dataIndex: 'enabled',
|
|
xtype: 'checkcolumn',
|
|
sortable: true,
|
|
disabled: true,
|
|
disabledCls: 'x-item-enabled',
|
|
stopSelection: false
|
|
},
|
|
{
|
|
header: gettext('Node'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'node',
|
|
renderer: function(value) {
|
|
if (value) {
|
|
return value;
|
|
}
|
|
return me.allText;
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Day of week'),
|
|
width: 200,
|
|
sortable: false,
|
|
dataIndex: 'dow',
|
|
renderer: function(val) {
|
|
var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
var selected = [];
|
|
var cur = -1;
|
|
val.split(',').forEach(function(day){
|
|
cur++;
|
|
var dow = (dows.indexOf(day)+6)%7;
|
|
if (cur === dow) {
|
|
if (selected.length === 0 || selected[selected.length-1] === 0) {
|
|
selected.push(1);
|
|
} else {
|
|
selected[selected.length-1]++;
|
|
}
|
|
} else {
|
|
while (cur < dow) {
|
|
cur++;
|
|
selected.push(0);
|
|
}
|
|
selected.push(1);
|
|
}
|
|
});
|
|
|
|
cur = -1;
|
|
var days = [];
|
|
selected.forEach(function(item) {
|
|
cur++;
|
|
if (item > 2) {
|
|
days.push(Ext.Date.dayNames[(cur+1)] + '-' + Ext.Date.dayNames[(cur+item)%7]);
|
|
cur += item-1;
|
|
} else if (item == 2) {
|
|
days.push(Ext.Date.dayNames[cur+1]);
|
|
days.push(Ext.Date.dayNames[(cur+2)%7]);
|
|
cur++;
|
|
} else if (item == 1) {
|
|
days.push(Ext.Date.dayNames[(cur+1)%7]);
|
|
}
|
|
});
|
|
return days.join(', ');
|
|
}
|
|
},
|
|
{
|
|
header: gettext('Start Time'),
|
|
width: 60,
|
|
sortable: true,
|
|
dataIndex: 'starttime'
|
|
},
|
|
{
|
|
header: gettext('Storage'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'storage'
|
|
},
|
|
{
|
|
header: gettext('Selection'),
|
|
flex: 1,
|
|
sortable: false,
|
|
dataIndex: 'vmid',
|
|
renderer: function(value, metaData, record) {
|
|
/*jslint confusion: true */
|
|
if (record.data.all) {
|
|
if (record.data.exclude) {
|
|
return Ext.String.format(me.allExceptText, record.data.exclude);
|
|
}
|
|
return me.allText;
|
|
}
|
|
if (record.data.vmid) {
|
|
return record.data.vmid;
|
|
}
|
|
|
|
if (record.data.pool) {
|
|
return "Pool '"+ record.data.pool + "'";
|
|
}
|
|
|
|
return "-";
|
|
}
|
|
}
|
|
],
|
|
listeners: {
|
|
activate: reload,
|
|
itemdblclick: run_editor
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
}, function() {
|
|
|
|
Ext.define('pve-cluster-backup', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'id', 'starttime', 'dow',
|
|
'storage', 'node', 'vmid', 'exclude',
|
|
'mailto', 'pool',
|
|
{ name: 'enabled', type: 'boolean' },
|
|
{ name: 'all', type: 'boolean' },
|
|
{ name: 'snapshot', type: 'boolean' },
|
|
{ name: 'stop', type: 'boolean' },
|
|
{ name: 'suspend', type: 'boolean' },
|
|
{ name: 'compress', type: 'boolean' }
|
|
]
|
|
});
|
|
});
|
|
Ext.define('PVE.dc.Support', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveDcSupport',
|
|
pveGuidePath: '/pve-docs/index.html',
|
|
onlineHelp: 'getting_help',
|
|
|
|
invalidHtml: '<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() {
|
|
var me = this;
|
|
|
|
var reload = function() {
|
|
Proxmox.Utils.API2Request({
|
|
url: '/nodes/localhost/subscription',
|
|
method: 'GET',
|
|
waitMsgTarget: me,
|
|
failure: function(response, opts) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
me.update('Unable to load subscription status' + ": " + response.htmlStatus);
|
|
},
|
|
success: function(response, opts) {
|
|
var data = response.result.data;
|
|
|
|
if (data.status === 'Active') {
|
|
if (data.level === 'c') {
|
|
me.updateCommunity(data);
|
|
} else {
|
|
me.updateActive(data);
|
|
}
|
|
} else {
|
|
me.updateInactive(data);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
autoScroll: true,
|
|
bodyStyle: 'padding:10px',
|
|
listeners: {
|
|
activate: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('pve-security-groups', {
|
|
extend: 'Ext.data.Model',
|
|
|
|
fields: [ 'group', 'comment', 'digest' ],
|
|
idProperty: 'group'
|
|
});
|
|
|
|
Ext.define('PVE.SecurityGroupEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
|
|
base_url: "/cluster/firewall/groups",
|
|
|
|
allow_iface: false,
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
me.isCreate = (me.group_name === undefined);
|
|
|
|
var subject;
|
|
|
|
me.url = '/api2/extjs' + me.base_url;
|
|
me.method = 'POST';
|
|
|
|
var items = [
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'group',
|
|
value: me.group_name || '',
|
|
fieldLabel: gettext('Name'),
|
|
allowBlank: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
name: 'comment',
|
|
value: me.group_comment || '',
|
|
fieldLabel: gettext('Comment')
|
|
}
|
|
];
|
|
|
|
if (me.isCreate) {
|
|
subject = gettext('Security Group');
|
|
} else {
|
|
subject = gettext('Security Group') + " '" + me.group_name + "'";
|
|
items.push({
|
|
xtype: 'hiddenfield',
|
|
name: 'rename',
|
|
value: me.group_name
|
|
});
|
|
}
|
|
|
|
var ipanel = Ext.create('Proxmox.panel.InputPanel', {
|
|
// InputPanel does not have a 'create' property, does it need a 'isCreate'
|
|
isCreate: me.isCreate,
|
|
items: items
|
|
});
|
|
|
|
|
|
Ext.apply(me, {
|
|
subject: subject,
|
|
items: [ ipanel ]
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.SecurityGroupList', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pveSecurityGroupList',
|
|
|
|
stateful: true,
|
|
stateId: 'grid-securitygroups',
|
|
|
|
rule_panel: undefined,
|
|
|
|
addBtn: undefined,
|
|
removeBtn: undefined,
|
|
editBtn: undefined,
|
|
|
|
base_url: "/cluster/firewall/groups",
|
|
|
|
initComponent: function() {
|
|
/*jslint confusion: true */
|
|
var me = this;
|
|
|
|
if (me.rule_panel == undefined) {
|
|
throw "no rule panel specified";
|
|
}
|
|
|
|
if (me.base_url == undefined) {
|
|
throw "no base_url specified";
|
|
}
|
|
|
|
var store = new Ext.data.Store({
|
|
model: 'pve-security-groups',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json' + me.base_url
|
|
},
|
|
sorters: {
|
|
property: 'group',
|
|
order: 'DESC'
|
|
}
|
|
});
|
|
|
|
var sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
var reload = function() {
|
|
var oldrec = sm.getSelection()[0];
|
|
store.load(function(records, operation, success) {
|
|
if (oldrec) {
|
|
var rec = store.findRecord('group', oldrec.data.group);
|
|
if (rec) {
|
|
sm.select(rec);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
var run_editor = function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
var win = Ext.create('PVE.SecurityGroupEdit', {
|
|
digest: rec.data.digest,
|
|
group_name: rec.data.group,
|
|
group_comment: rec.data.comment
|
|
});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
};
|
|
|
|
me.editBtn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor
|
|
});
|
|
|
|
me.addBtn = new Proxmox.button.Button({
|
|
text: gettext('Create'),
|
|
handler: function() {
|
|
sm.deselectAll();
|
|
var win = Ext.create('PVE.SecurityGroupEdit', {});
|
|
win.show();
|
|
win.on('destroy', reload);
|
|
}
|
|
});
|
|
|
|
me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: me.base_url + '/',
|
|
enableFn: function(rec) {
|
|
return (rec && me.base_url);
|
|
},
|
|
callback: function() {
|
|
reload();
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
tbar: [ '<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) {
|
|
var url = '/cluster/firewall/groups/' + rec.data.group;
|
|
me.rule_panel.setBaseUrl(url);
|
|
},
|
|
deselect: function() {
|
|
me.rule_panel.setBaseUrl(undefined);
|
|
},
|
|
show: reload
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
store.load();
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.SecurityGroups', {
|
|
extend: 'Ext.panel.Panel',
|
|
alias: 'widget.pveSecurityGroups',
|
|
|
|
title: 'Security Groups',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var rule_panel = Ext.createWidget('pveFirewallRules', {
|
|
region: 'center',
|
|
allow_groups: false,
|
|
list_refs_url: '/cluster/firewall/refs',
|
|
tbar_prefix: '<b>' + gettext('Rules') + ':</b>',
|
|
border: false
|
|
});
|
|
|
|
var sglist = Ext.createWidget('pveSecurityGroupList', {
|
|
region: 'west',
|
|
rule_panel: rule_panel,
|
|
width: '25%',
|
|
border: false,
|
|
split: true
|
|
});
|
|
|
|
|
|
Ext.apply(me, {
|
|
layout: 'border',
|
|
items: [ sglist, rule_panel ],
|
|
listeners: {
|
|
show: function() {
|
|
sglist.fireEvent('show', sglist);
|
|
}
|
|
}
|
|
});
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
/*
|
|
* Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected
|
|
*/
|
|
|
|
Ext.define('PVE.dc.Config', {
|
|
extend: 'PVE.panel.Config',
|
|
alias: 'widget.PVE.dc.Config',
|
|
|
|
onlineHelp: 'pve_admin_guide',
|
|
|
|
initComponent: function() {
|
|
var me = this;
|
|
|
|
var caps = Ext.state.Manager.get('GuiCap');
|
|
|
|
me.items = [];
|
|
|
|
Ext.apply(me, {
|
|
title: gettext("Datacenter"),
|
|
hstateid: 'dctab'
|
|
});
|
|
|
|
if (caps.dc['Sys.Audit']) {
|
|
me.items.push({
|
|
title: gettext('Summary'),
|
|
xtype: 'pveDcSummary',
|
|
iconCls: 'fa fa-book',
|
|
itemId: 'summary'
|
|
},
|
|
{
|
|
title: gettext('Cluster'),
|
|
xtype: 'pveClusterAdministration',
|
|
iconCls: 'fa fa-server',
|
|
itemId: 'cluster'
|
|
},
|
|
{
|
|
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'
|
|
});
|
|
|
|
if (caps.dc['Sys.Audit']) {
|
|
me.items.push({
|
|
xtype: 'pveGroupView',
|
|
title: gettext('Groups'),
|
|
iconCls: 'fa fa-users',
|
|
groups: ['permissions'],
|
|
itemId: 'groups'
|
|
},
|
|
{
|
|
xtype: 'pvePoolView',
|
|
title: gettext('Pools'),
|
|
iconCls: 'fa fa-tags',
|
|
groups: ['permissions'],
|
|
itemId: 'pools'
|
|
},
|
|
{
|
|
xtype: 'pveRoleView',
|
|
title: gettext('Roles'),
|
|
iconCls: 'fa fa-male',
|
|
groups: ['permissions'],
|
|
itemId: 'roles'
|
|
},
|
|
{
|
|
xtype: 'pveAuthView',
|
|
title: gettext('Authentication'),
|
|
groups: ['permissions'],
|
|
iconCls: 'fa fa-key',
|
|
itemId: 'domains'
|
|
},
|
|
{
|
|
xtype: 'pveHAStatus',
|
|
title: 'HA',
|
|
iconCls: 'fa fa-heartbeat',
|
|
itemId: 'ha'
|
|
},
|
|
{
|
|
title: gettext('Groups'),
|
|
groups: ['ha'],
|
|
xtype: 'pveHAGroupsView',
|
|
iconCls: 'fa fa-object-group',
|
|
itemId: 'ha-groups'
|
|
},
|
|
{
|
|
title: gettext('Fencing'),
|
|
groups: ['ha'],
|
|
iconCls: 'fa fa-bolt',
|
|
xtype: 'pveFencingView',
|
|
itemId: 'ha-fencing'
|
|
},
|
|
{
|
|
xtype: 'pveFirewallRules',
|
|
title: gettext('Firewall'),
|
|
allow_iface: true,
|
|
base_url: '/cluster/firewall/rules',
|
|
list_refs_url: '/cluster/firewall/refs',
|
|
iconCls: 'fa fa-shield',
|
|
itemId: 'firewall'
|
|
},
|
|
{
|
|
xtype: 'pveFirewallOptions',
|
|
title: gettext('Options'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-gear',
|
|
base_url: '/cluster/firewall/options',
|
|
onlineHelp: 'pve_firewall_cluster_wide_setup',
|
|
fwtype: 'dc',
|
|
itemId: 'firewall-options'
|
|
},
|
|
{
|
|
xtype: 'pveSecurityGroups',
|
|
title: gettext('Security Group'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-group',
|
|
itemId: 'firewall-sg'
|
|
},
|
|
{
|
|
xtype: 'pveFirewallAliases',
|
|
title: gettext('Alias'),
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-external-link',
|
|
base_url: '/cluster/firewall/aliases',
|
|
itemId: 'firewall-aliases'
|
|
},
|
|
{
|
|
xtype: 'pveIPSet',
|
|
title: 'IPSet',
|
|
groups: ['firewall'],
|
|
iconCls: 'fa fa-list-ol',
|
|
base_url: '/cluster/firewall/ipset',
|
|
list_refs_url: '/cluster/firewall/refs',
|
|
itemId: 'firewall-ipset'
|
|
},
|
|
{
|
|
xtype: 'pveDcSupport',
|
|
title: gettext('Support'),
|
|
itemId: 'support',
|
|
iconCls: 'fa fa-comments-o'
|
|
});
|
|
}
|
|
|
|
me.callParent();
|
|
}
|
|
});
|
|
Ext.define('PVE.dc.NodeView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
alias: 'widget.pveDcNodeView',
|
|
|
|
title: gettext('Nodes'),
|
|
disableSelection: true,
|
|
scrollable: true,
|
|
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
flex: 1,
|
|
sortable: true,
|
|
dataIndex: 'name'
|
|
},
|
|
{
|
|
header: 'ID',
|
|
width: 40,
|
|
sortable: true,
|
|
dataIndex: 'nodeid'
|
|
},
|
|
{
|
|
header: gettext('Online'),
|
|
width: 60,
|
|
sortable: true,
|
|
dataIndex: 'online',
|
|
renderer: function(value) {
|
|
var cls = (value)?'good':'critical';
|
|
return '<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(){
|
|
var me = this.up('grid');
|
|
var height = Math.max(me.getHeight()-50, 250);
|
|
me.setHeight(height);
|
|
}
|
|
},
|
|
{
|
|
type: 'down',
|
|
handler: function(){
|
|
var me = this.up('grid');
|
|
var height = me.getHeight()+50;
|
|
me.setHeight(height);
|
|
}
|
|
}
|
|
]
|
|
}, function() {
|
|
|
|
Ext.define('pve-dc-nodes', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [ 'id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'],
|
|
idProperty: 'id'
|
|
});
|
|
|
|
});
|
|
|
|
Ext.define('PVE.widget.ProgressBar',{
|
|
extend: 'Ext.Progress',
|
|
alias: 'widget.pveProgressBar',
|
|
|
|
animate: true,
|
|
textTpl: [
|
|
'{percent}%'
|
|
],
|
|
|
|
setValue: function(value){
|
|
var me = this;
|
|
me.callParent([value]);
|
|
|
|
me.removeCls(['warning', 'critical']);
|
|
|
|
if (value > 0.89) {
|
|
me.addCls('critical');
|
|
} else if (value > 0.59) {
|
|
me.addCls('warning');
|
|
}
|
|
}
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('pve-cluster-nodes', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr',
|
|
{ type: 'integer', name: 'quorum_votes' }
|
|
],
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/cluster/config/nodes"
|
|
},
|
|
idProperty: 'nodeid'
|
|
});
|
|
|
|
Ext.define('pve-cluster-info', {
|
|
extend: 'Ext.data.Model',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/cluster/config/join"
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.ClusterAdministration', {
|
|
extend: 'Ext.panel.Panel',
|
|
xtype: 'pveClusterAdministration',
|
|
|
|
title: gettext('Cluster Administration'),
|
|
onlineHelp: 'chapter_pvecm',
|
|
|
|
border: false,
|
|
defaults: { border: false },
|
|
|
|
viewModel: {
|
|
parent: null,
|
|
data: {
|
|
totem: {},
|
|
nodelist: [],
|
|
preferred_node: {
|
|
name: '',
|
|
fp: '',
|
|
addr: ''
|
|
},
|
|
isInCluster: false,
|
|
nodecount: 0
|
|
}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'panel',
|
|
title: gettext('Cluster Information'),
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
init: function(view) {
|
|
view.store = Ext.create('Proxmox.data.UpdateStore', {
|
|
autoStart: true,
|
|
interval: 15 * 1000,
|
|
storeid: 'pve-cluster-info',
|
|
model: 'pve-cluster-info'
|
|
});
|
|
view.store.on('load', this.onLoad, this);
|
|
view.on('destroy', view.store.stopUpdate);
|
|
},
|
|
|
|
onLoad: function(store, records, success) {
|
|
var vm = this.getViewModel();
|
|
if (!success || !records || !records[0].data) {
|
|
vm.set('totem', {});
|
|
vm.set('isInCluster', false);
|
|
vm.set('nodelist', []);
|
|
vm.set('preferred_node', {
|
|
name: '',
|
|
addr: '',
|
|
fp: ''
|
|
});
|
|
return;
|
|
}
|
|
var data = records[0].data;
|
|
vm.set('totem', data.totem);
|
|
vm.set('isInCluster', !!data.totem.cluster_name);
|
|
vm.set('nodelist', data.nodelist);
|
|
|
|
var nodeinfo = Ext.Array.findBy(data.nodelist, function (el) {
|
|
return el.name === data.preferred_node;
|
|
});
|
|
|
|
vm.set('preferred_node', {
|
|
name: data.preferred_node,
|
|
addr: nodeinfo.pve_addr,
|
|
ring_addr: [ nodeinfo.ring0_addr, nodeinfo.ring1_addr ],
|
|
fp: nodeinfo.pve_fp
|
|
});
|
|
},
|
|
|
|
onCreate: function() {
|
|
var view = this.getView();
|
|
view.store.stopUpdate();
|
|
var win = Ext.create('PVE.ClusterCreateWindow', {
|
|
autoShow: true,
|
|
listeners: {
|
|
destroy: function() {
|
|
view.store.startUpdate();
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
onClusterInfo: function() {
|
|
var vm = this.getViewModel();
|
|
var win = Ext.create('PVE.ClusterInfoWindow', {
|
|
joinInfo: {
|
|
ipAddress: vm.get('preferred_node.addr'),
|
|
fingerprint: vm.get('preferred_node.fp'),
|
|
ring_addr: vm.get('preferred_node.ring_addr'),
|
|
totem: vm.get('totem')
|
|
}
|
|
});
|
|
win.show();
|
|
},
|
|
|
|
onJoin: function() {
|
|
var view = this.getView();
|
|
view.store.stopUpdate();
|
|
var win = Ext.create('PVE.ClusterJoinNodeWindow', {
|
|
autoShow: true,
|
|
listeners: {
|
|
destroy: function() {
|
|
view.store.startUpdate();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
},
|
|
tbar: [
|
|
{
|
|
text: gettext('Create Cluster'),
|
|
reference: 'createButton',
|
|
handler: 'onCreate',
|
|
bind: {
|
|
disabled: '{isInCluster}'
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Join Information'),
|
|
reference: 'addButton',
|
|
handler: 'onClusterInfo',
|
|
bind: {
|
|
disabled: '{!isInCluster}'
|
|
}
|
|
},
|
|
{
|
|
text: gettext('Join Cluster'),
|
|
reference: 'joinButton',
|
|
handler: 'onJoin',
|
|
bind: {
|
|
disabled: '{isInCluster}'
|
|
}
|
|
}
|
|
],
|
|
layout: 'hbox',
|
|
bodyPadding: 5,
|
|
items: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Cluster Name'),
|
|
bind: {
|
|
value: '{totem.cluster_name}',
|
|
hidden: '{!isInCluster}'
|
|
},
|
|
flex: 1
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Config Version'),
|
|
bind: {
|
|
value: '{totem.config_version}',
|
|
hidden: '{!isInCluster}'
|
|
},
|
|
flex: 1
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Number of Nodes'),
|
|
labelWidth: 120,
|
|
bind: {
|
|
value: '{nodecount}',
|
|
hidden: '{!isInCluster}'
|
|
},
|
|
flex: 1
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
value: gettext('Standalone node - no cluster defined'),
|
|
bind: {
|
|
hidden: '{isInCluster}'
|
|
},
|
|
flex: 1
|
|
}
|
|
]
|
|
},
|
|
{
|
|
xtype: 'grid',
|
|
title: gettext('Cluster Nodes'),
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
init: function(view) {
|
|
view.rstore = Ext.create('Proxmox.data.UpdateStore', {
|
|
autoLoad: true,
|
|
xtype: 'update',
|
|
interval: 5 * 1000,
|
|
autoStart: true,
|
|
storeid: 'pve-cluster-nodes',
|
|
model: 'pve-cluster-nodes'
|
|
});
|
|
view.setStore(Ext.create('Proxmox.data.DiffStore', {
|
|
rstore: view.rstore,
|
|
sorters: {
|
|
property: 'nodeid',
|
|
order: 'DESC'
|
|
}
|
|
}));
|
|
Proxmox.Utils.monStoreErrors(view, view.rstore);
|
|
view.rstore.on('load', this.onLoad, this);
|
|
view.on('destroy', view.rstore.stopUpdate);
|
|
},
|
|
|
|
onLoad: function(store, records, success) {
|
|
var vm = this.getViewModel();
|
|
if (!success || !records) {
|
|
vm.set('nodecount', 0);
|
|
return;
|
|
}
|
|
vm.set('nodecount', records.length);
|
|
}
|
|
},
|
|
columns: [
|
|
{
|
|
header: gettext('Nodename'),
|
|
flex: 2,
|
|
dataIndex: 'name'
|
|
},
|
|
{
|
|
header: gettext('ID'),
|
|
flex: 1,
|
|
dataIndex: 'nodeid'
|
|
},
|
|
{
|
|
header: gettext('Votes'),
|
|
flex: 1,
|
|
dataIndex: 'quorum_votes'
|
|
},
|
|
{
|
|
header: Ext.String.format(gettext('Link {0}'), 0),
|
|
flex: 2,
|
|
dataIndex: 'ring0_addr'
|
|
},
|
|
{
|
|
header: Ext.String.format(gettext('Link {0}'), 1),
|
|
flex: 2,
|
|
dataIndex: 'ring1_addr'
|
|
}
|
|
]
|
|
}
|
|
]
|
|
});
|
|
/*jslint confusion: true*/
|
|
Ext.define('PVE.ClusterCreateWindow', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveClusterCreateWindow',
|
|
|
|
title: gettext('Create Cluster'),
|
|
width: 600,
|
|
|
|
method: 'POST',
|
|
url: '/cluster/config',
|
|
|
|
isCreate: true,
|
|
subject: gettext('Cluster'),
|
|
showTaskViewer: true,
|
|
|
|
onlineHelp: 'pvecm_create_cluster',
|
|
|
|
items: {
|
|
xtype: 'inputpanel',
|
|
items: [{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Cluster Name'),
|
|
allowBlank: false,
|
|
name: 'clustername'
|
|
},
|
|
{
|
|
xtype: 'proxmoxNetworkSelector',
|
|
fieldLabel: Ext.String.format(gettext('Link {0}'), 0),
|
|
emptyText: gettext("Optional, defaults to IP resolved by node's hostname"),
|
|
name: 'link0',
|
|
autoSelect: false,
|
|
valueField: 'address',
|
|
displayField: 'address',
|
|
skipEmptyText: true
|
|
}],
|
|
advancedItems: [{
|
|
xtype: 'proxmoxNetworkSelector',
|
|
fieldLabel: Ext.String.format(gettext('Link {0}'), 1),
|
|
emptyText: gettext("Optional second link for redundancy"),
|
|
name: 'link1',
|
|
autoSelect: false,
|
|
valueField: 'address',
|
|
displayField: 'address',
|
|
skipEmptyText: true
|
|
}]
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.ClusterInfoWindow', {
|
|
extend: 'Ext.window.Window',
|
|
xtype: 'pveClusterInfoWindow',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
width: 800,
|
|
modal: true,
|
|
resizable: false,
|
|
title: gettext('Cluster Join Information'),
|
|
|
|
joinInfo: {
|
|
ipAddress: undefined,
|
|
fingerprint: undefined,
|
|
totem: {}
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'component',
|
|
border: false,
|
|
padding: '10 10 10 10',
|
|
html: gettext("Copy the Join Information here and use it on the node you want to add.")
|
|
},
|
|
{
|
|
xtype: 'container',
|
|
layout: 'form',
|
|
border: false,
|
|
padding: '0 10 10 10',
|
|
items: [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('IP Address'),
|
|
cbind: { value: '{joinInfo.ipAddress}' },
|
|
editable: false
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Fingerprint'),
|
|
cbind: { value: '{joinInfo.fingerprint}' },
|
|
editable: false
|
|
},
|
|
{
|
|
xtype: 'textarea',
|
|
inputId: 'pveSerializedClusterInfo',
|
|
fieldLabel: gettext('Join Information'),
|
|
grow: true,
|
|
cbind: { joinInfo: '{joinInfo}' },
|
|
editable: false,
|
|
listeners: {
|
|
afterrender: function(field) {
|
|
if (!field.joinInfo) {
|
|
return;
|
|
}
|
|
var jsons = Ext.JSON.encode(field.joinInfo);
|
|
var base64s = Ext.util.Base64.encode(jsons);
|
|
field.setValue(base64s);
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
],
|
|
dockedItems: [{
|
|
dock: 'bottom',
|
|
xtype: 'toolbar',
|
|
items: [{
|
|
xtype: 'button',
|
|
handler: function(b) {
|
|
var el = document.getElementById('pveSerializedClusterInfo');
|
|
el.select();
|
|
document.execCommand("copy");
|
|
},
|
|
text: gettext('Copy Information')
|
|
}]
|
|
}]
|
|
});
|
|
|
|
Ext.define('PVE.ClusterJoinNodeWindow', {
|
|
extend: 'Proxmox.window.Edit',
|
|
xtype: 'pveClusterJoinNodeWindow',
|
|
|
|
title: gettext('Cluster Join'),
|
|
width: 800,
|
|
|
|
method: 'POST',
|
|
url: '/cluster/config/join',
|
|
|
|
defaultFocus: 'textarea[name=serializedinfo]',
|
|
isCreate: true,
|
|
submitText: gettext('Join'),
|
|
showTaskViewer: true,
|
|
|
|
onlineHelp: 'chapter_pvecm',
|
|
|
|
viewModel: {
|
|
parent: null,
|
|
data: {
|
|
info: {
|
|
fp: '',
|
|
ip: '',
|
|
ring0Needed: false,
|
|
ring1Possible: false,
|
|
ring1Needed: false
|
|
}
|
|
},
|
|
formulas: {
|
|
ring0EmptyText: function(get) {
|
|
if (get('info.ring0Needed')) {
|
|
return gettext("Cannot use default address safely");
|
|
} else {
|
|
return gettext("Default: IP resolved by node's hostname");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
control: {
|
|
'#': {
|
|
close: function() {
|
|
delete PVE.Utils.silenceAuthFailures;
|
|
}
|
|
},
|
|
'proxmoxcheckbox[name=assistedEntry]': {
|
|
change: 'onInputTypeChange'
|
|
},
|
|
'textarea[name=serializedinfo]': {
|
|
change: 'recomputeSerializedInfo',
|
|
enable: 'resetField'
|
|
},
|
|
'proxmoxtextfield[name=ring1_addr]': {
|
|
enable: 'ring1Needed'
|
|
},
|
|
'textfield': {
|
|
disable: 'resetField'
|
|
}
|
|
},
|
|
resetField: function(field) {
|
|
field.reset();
|
|
},
|
|
ring1Needed: function(f) {
|
|
var vm = this.getViewModel();
|
|
f.allowBlank = !vm.get('info.ring1Needed');
|
|
},
|
|
onInputTypeChange: function(field, assistedInput) {
|
|
var vm = this.getViewModel();
|
|
if (!assistedInput) {
|
|
vm.set('info.ring1Possible', true);
|
|
}
|
|
},
|
|
recomputeSerializedInfo: function(field, value) {
|
|
var vm = this.getViewModel();
|
|
var jsons = Ext.util.Base64.decode(value);
|
|
var joinInfo = Ext.JSON.decode(jsons, true);
|
|
|
|
var info = {
|
|
fp: '',
|
|
ring1Needed: false,
|
|
ring1Possible: false,
|
|
ip: ''
|
|
};
|
|
|
|
var totem = {};
|
|
if (!(joinInfo && joinInfo.totem)) {
|
|
field.valid = false;
|
|
} else {
|
|
var ring0Needed = false;
|
|
if (joinInfo.ring_addr !== undefined) {
|
|
ring0Needed = joinInfo.ring_addr[0] !== joinInfo.ipAddress;
|
|
}
|
|
|
|
info = {
|
|
ip: joinInfo.ipAddress,
|
|
fp: joinInfo.fingerprint,
|
|
ring0Needed: ring0Needed,
|
|
ring1Possible: !!joinInfo.totem['interface']['1'],
|
|
ring1Needed: !!joinInfo.totem['interface']['1']
|
|
};
|
|
totem = joinInfo.totem;
|
|
field.valid = true;
|
|
}
|
|
|
|
vm.set('info', info);
|
|
}
|
|
},
|
|
|
|
submit: function() {
|
|
// joining may produce temporarily auth failures, ignore as long the task runs
|
|
PVE.Utils.silenceAuthFailures = true;
|
|
this.callParent();
|
|
},
|
|
|
|
taskDone: function(success) {
|
|
delete PVE.Utils.silenceAuthFailures;
|
|
if (success) {
|
|
var txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!');
|
|
// ensure user cannot do harm
|
|
Ext.getBody().mask(txt, ['pve-static-mask']);
|
|
// TaskView may hide above mask, so tell him directly
|
|
Ext.Msg.show({
|
|
title: gettext('Join Task Finished'),
|
|
icon: Ext.Msg.INFO,
|
|
msg: txt
|
|
});
|
|
// reload always (if user wasn't faster), but wait a bit for pveproxy
|
|
Ext.defer(function() {
|
|
window.location.reload(true);
|
|
}, 5000);
|
|
}
|
|
},
|
|
|
|
items: [{
|
|
xtype: 'proxmoxcheckbox',
|
|
reference: 'assistedEntry',
|
|
name: 'assistedEntry',
|
|
submitValue: false,
|
|
value: true,
|
|
autoEl: {
|
|
tag: 'div',
|
|
'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering')
|
|
},
|
|
boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.')
|
|
},
|
|
{
|
|
xtype: 'textarea',
|
|
name: 'serializedinfo',
|
|
submitValue: false,
|
|
allowBlank: false,
|
|
fieldLabel: gettext('Information'),
|
|
emptyText: gettext('Paste encoded Cluster Information here'),
|
|
validator: function(val) {
|
|
return val === '' || this.valid ||
|
|
gettext('Does not seem like a valid encoded Cluster Information!');
|
|
},
|
|
bind: {
|
|
disabled: '{!assistedEntry.checked}',
|
|
hidden: '{!assistedEntry.checked}'
|
|
},
|
|
value: ''
|
|
},
|
|
{
|
|
xtype: 'inputpanel',
|
|
column1: [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Peer Address'),
|
|
allowBlank: false,
|
|
bind: {
|
|
value: '{info.ip}',
|
|
readOnly: '{assistedEntry.checked}'
|
|
},
|
|
name: 'hostname'
|
|
},
|
|
{
|
|
xtype: 'textfield',
|
|
inputType: 'password',
|
|
emptyText: gettext("Peer's root password"),
|
|
fieldLabel: gettext('Password'),
|
|
allowBlank: false,
|
|
name: 'password'
|
|
}
|
|
],
|
|
column2: [
|
|
{
|
|
xtype: 'proxmoxNetworkSelector',
|
|
fieldLabel: Ext.String.format(gettext('Link {0}'), 0),
|
|
bind: {
|
|
emptyText: '{ring0EmptyText}',
|
|
allowBlank: '{!info.ring0Needed}'
|
|
},
|
|
skipEmptyText: true,
|
|
autoSelect: false,
|
|
valueField: 'address',
|
|
displayField: 'address',
|
|
name: 'link0'
|
|
},
|
|
{
|
|
xtype: 'proxmoxNetworkSelector',
|
|
fieldLabel: Ext.String.format(gettext('Link {0}'), 1),
|
|
skipEmptyText: true,
|
|
autoSelect: false,
|
|
valueField: 'address',
|
|
displayField: 'address',
|
|
bind: {
|
|
disabled: '{!info.ring1Possible}',
|
|
allowBlank: '{!info.ring1Needed}',
|
|
},
|
|
name: 'link1'
|
|
}
|
|
],
|
|
columnB: [
|
|
{
|
|
xtype: 'textfield',
|
|
fieldLabel: gettext('Fingerprint'),
|
|
allowBlank: false,
|
|
bind: {
|
|
value: '{info.fp}',
|
|
readOnly: '{assistedEntry.checked}'
|
|
},
|
|
name: 'fingerprint'
|
|
}
|
|
]
|
|
}]
|
|
});
|
|
/*
|
|
* Workspace base class
|
|
*
|
|
* popup login window when auth fails (call onLogin handler)
|
|
* update (re-login) ticket every 15 minutes
|
|
*
|
|
*/
|
|
|
|
Ext.define('PVE.Workspace', {
|
|
extend: 'Ext.container.Viewport',
|
|
|
|
title: 'Proxmox Virtual Environment',
|
|
|
|
loginData: null, // Data from last login call
|
|
|
|
onLogin: function(loginData) {},
|
|
|
|
// private
|
|
updateLoginData: function(loginData) {
|
|
var me = this;
|
|
me.loginData = loginData;
|
|
Proxmox.Utils.setAuthData(loginData);
|
|
|
|
var rt = me.down('pveResourceTree');
|
|
rt.setDatacenterText(loginData.clustername);
|
|
|
|
if (loginData.cap) {
|
|
Ext.state.Manager.set('GuiCap', loginData.cap);
|
|
}
|
|
me.response401count = 0;
|
|
|
|
me.onLogin(loginData);
|
|
},
|
|
|
|
// private
|
|
showLogin: function() {
|
|
var me = this;
|
|
|
|
Proxmox.Utils.authClear();
|
|
Proxmox.UserName = null;
|
|
me.loginData = null;
|
|
|
|
if (!me.login) {
|
|
me.login = Ext.create('PVE.window.LoginWindow', {
|
|
handler: function(data) {
|
|
me.login = null;
|
|
me.updateLoginData(data);
|
|
Proxmox.Utils.checked_command(function() {}); // display subscription status
|
|
}
|
|
});
|
|
}
|
|
me.onLogin(null);
|
|
me.login.show();
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
Ext.tip.QuickTipManager.init();
|
|
|
|
// fixme: what about other errors
|
|
Ext.Ajax.on('requestexception', function(conn, response, options) {
|
|
if (response.status == 401 && !PVE.Utils.silenceAuthFailures) { // auth failure
|
|
// 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() {
|
|
var ticket = Proxmox.Utils.authOK();
|
|
if (!ticket || !Proxmox.UserName) {
|
|
return;
|
|
}
|
|
|
|
Ext.Ajax.request({
|
|
params: {
|
|
username: Proxmox.UserName,
|
|
password: ticket
|
|
},
|
|
url: '/api2/json/access/ticket',
|
|
method: 'POST',
|
|
success: function(response, opts) {
|
|
var obj = Ext.decode(response.responseText);
|
|
me.updateLoginData(obj.data);
|
|
}
|
|
});
|
|
},
|
|
interval: 15*60*1000
|
|
});
|
|
|
|
}
|
|
});
|
|
|
|
Ext.define('PVE.StdWorkspace', {
|
|
extend: 'PVE.Workspace',
|
|
|
|
alias: ['widget.pveStdWorkspace'],
|
|
|
|
// private
|
|
setContent: function(comp) {
|
|
var me = this;
|
|
|
|
var cont = me.child('#content');
|
|
|
|
var lay = cont.getLayout();
|
|
|
|
var cur = lay.getActiveItem();
|
|
|
|
if (comp) {
|
|
Proxmox.Utils.setErrorMask(cont, false);
|
|
comp.border = false;
|
|
cont.add(comp);
|
|
if (cur !== null && lay.getNext()) {
|
|
lay.next();
|
|
var task = Ext.create('Ext.util.DelayedTask', function(){
|
|
cont.remove(cur);
|
|
});
|
|
task.delay(10);
|
|
}
|
|
}
|
|
else {
|
|
// helper for cleaning the content when logging out
|
|
cont.removeAll();
|
|
}
|
|
},
|
|
|
|
selectById: function(nodeid) {
|
|
var me = this;
|
|
var tree = me.down('pveResourceTree');
|
|
tree.selectById(nodeid);
|
|
},
|
|
|
|
onLogin: function(loginData) {
|
|
var me = this;
|
|
|
|
me.updateUserInfo();
|
|
|
|
if (loginData) {
|
|
PVE.data.ResourceStore.startUpdate();
|
|
|
|
Proxmox.Utils.API2Request({
|
|
url: '/version',
|
|
method: 'GET',
|
|
success: function(response) {
|
|
PVE.VersionInfo = response.result.data;
|
|
me.updateVersionInfo();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
updateUserInfo: function() {
|
|
var me = this;
|
|
var ui = me.query('#userinfo')[0];
|
|
ui.setText(Proxmox.UserName || '');
|
|
ui.updateLayout();
|
|
},
|
|
|
|
updateVersionInfo: function() {
|
|
var me = this;
|
|
|
|
var ui = me.query('#versioninfo')[0];
|
|
|
|
if (PVE.VersionInfo) {
|
|
var version = PVE.VersionInfo.version;
|
|
ui.update('Virtual Environment ' + version);
|
|
} else {
|
|
ui.update('Virtual Environment');
|
|
}
|
|
ui.updateLayout();
|
|
},
|
|
|
|
initComponent : function() {
|
|
var me = this;
|
|
|
|
Ext.History.init();
|
|
|
|
var sprovider = Ext.create('PVE.StateProvider');
|
|
Ext.state.Manager.setProvider(sprovider);
|
|
|
|
var selview = Ext.create('PVE.form.ViewSelector');
|
|
|
|
var rtree = Ext.createWidget('pveResourceTree', {
|
|
viewFilter: selview.getViewFilter(),
|
|
flex: 1,
|
|
selModel: {
|
|
selType: 'treemodel',
|
|
listeners: {
|
|
selectionchange: function(sm, selected) {
|
|
if (selected.length > 0) {
|
|
var n = selected[0];
|
|
var tlckup = {
|
|
root: 'PVE.dc.Config',
|
|
node: 'PVE.node.Config',
|
|
qemu: 'PVE.qemu.Config',
|
|
lxc: 'PVE.lxc.Config',
|
|
storage: 'PVE.storage.Browser',
|
|
pool: 'pvePoolConfig'
|
|
};
|
|
var comp = {
|
|
xtype: tlckup[n.data.type || 'root'] ||
|
|
'pvePanelConfig',
|
|
showSearch: (n.data.id === 'root') ||
|
|
Ext.isDefined(n.data.groupbyid),
|
|
pveSelNode: n,
|
|
workspace: me,
|
|
viewFilter: selview.getViewFilter()
|
|
};
|
|
PVE.curSelectedNode = n;
|
|
me.setContent(comp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
selview.on('select', function(combo, records) {
|
|
if (records) {
|
|
var view = combo.getViewFilter();
|
|
rtree.setViewFilter(view);
|
|
}
|
|
});
|
|
|
|
var caps = sprovider.get('GuiCap');
|
|
|
|
var createVM = Ext.createWidget('button', {
|
|
pack: 'end',
|
|
margin: '3 5 0 0',
|
|
baseCls: 'x-btn',
|
|
iconCls: 'fa fa-desktop',
|
|
text: gettext("Create VM"),
|
|
disabled: !caps.vms['VM.Allocate'],
|
|
handler: function() {
|
|
var wiz = Ext.create('PVE.qemu.CreateWizard', {});
|
|
wiz.show();
|
|
}
|
|
});
|
|
|
|
var createCT = Ext.createWidget('button', {
|
|
pack: 'end',
|
|
margin: '3 5 0 0',
|
|
baseCls: 'x-btn',
|
|
iconCls: 'fa fa-cube',
|
|
text: gettext("Create CT"),
|
|
disabled: !caps.vms['VM.Allocate'],
|
|
handler: function() {
|
|
var wiz = Ext.create('PVE.lxc.CreateWizard', {});
|
|
wiz.show();
|
|
}
|
|
});
|
|
|
|
sprovider.on('statechange', function(sp, key, value) {
|
|
if (key === 'GuiCap' && value) {
|
|
caps = value;
|
|
createVM.setDisabled(!caps.vms['VM.Allocate']);
|
|
createCT.setDisabled(!caps.vms['VM.Allocate']);
|
|
}
|
|
});
|
|
|
|
Ext.apply(me, {
|
|
layout: { type: 'border' },
|
|
border: false,
|
|
items: [
|
|
{
|
|
region: 'north',
|
|
layout: {
|
|
type: 'hbox',
|
|
align: 'middle'
|
|
},
|
|
baseCls: 'x-plain',
|
|
defaults: {
|
|
baseCls: 'x-plain'
|
|
},
|
|
border: false,
|
|
margin: '2 0 2 5',
|
|
items: [
|
|
{
|
|
html: '<a class="x-unselectable" target=_blank href="https://www.proxmox.com">' +
|
|
'<img style="padding-top:4px;padding-right:5px" src="/pve2/images/proxmox_logo.png"/></a>'
|
|
},
|
|
{
|
|
minWidth: 150,
|
|
id: 'versioninfo',
|
|
html: 'Virtual Environment'
|
|
},
|
|
{
|
|
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'),
|
|
iconCls: 'fa fa-fw fa-key',
|
|
handler: function() {
|
|
var win = Ext.create('Proxmox.window.PasswordEdit', {
|
|
userid: Proxmox.UserName
|
|
});
|
|
win.show();
|
|
}
|
|
},
|
|
{
|
|
text: 'TFA',
|
|
iconCls: 'fa fa-fw fa-lock',
|
|
handler: function(btn, event, rec) {
|
|
var win = Ext.create('PVE.window.TFAEdit',{
|
|
userid: Proxmox.UserName
|
|
});
|
|
win.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: 200,
|
|
items: [ selview, rtree ],
|
|
listeners: {
|
|
resize: function(panel, width, height) {
|
|
var viewWidth = me.getSize().width;
|
|
if (width > viewWidth - 100) {
|
|
panel.setWidth(viewWidth - 100);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
xtype: 'pveStatusPanel',
|
|
stateful: true,
|
|
stateId: 'pvesouth',
|
|
itemId: 'south',
|
|
region: 'south',
|
|
margin:'0 5 5 5',
|
|
title: gettext('Logs'),
|
|
collapsible: true,
|
|
header: false,
|
|
height: 200,
|
|
split:true,
|
|
listeners: {
|
|
resize: function(panel, width, height) {
|
|
var viewHeight = me.getSize().height;
|
|
if (height > (viewHeight - 150)) {
|
|
panel.setHeight(viewHeight - 150);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
me.callParent();
|
|
|
|
me.updateUserInfo();
|
|
|
|
// on resize, center all modal windows
|
|
Ext.on('resize', function(){
|
|
var wins = Ext.ComponentQuery.query('window[modal]');
|
|
if (wins.length > 0) {
|
|
wins.forEach(function(win){
|
|
win.alignTo(me, 'c-c');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
|