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

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>&nbsp;<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,'&quot;') + '"';
}
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,'&quot;') + '"';
}
}
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 &gt; 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,'&quot;') + '"';
}
}
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">&nbsp;</i>',
gettext('Online'),
'</div>',
'<div class="right-aligned">{online}</div>',
'<br /><br />',
'<div class="left-aligned">',
'<i class="critical fa fa-fw fa-times">&nbsp;</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">&nbsp;</i>',
gettext('Running'),
'</div>',
'<div class="right-aligned">{running}</div>' + '<br />',
'<tpl if="paused &gt; 0">',
'<div class="left-aligned">',
'<i class="warning fa fa-fw fa-pause-circle">&nbsp;</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">&nbsp;</i>',
gettext('Stopped'),
'</div>',
'<div class="right-aligned">{stopped}</div>' + '<br />',
'<tpl if="template &gt; 0">',
'<div class="left-aligned">',
'<i class="fa fa-fw fa-circle-o">&nbsp;</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">&nbsp;</i>',
gettext('Running'),
'</div>',
'<div class="right-aligned">{running}</div>' + '<br />',
'<tpl if="paused &gt; 0">',
'<div class="left-aligned">',
'<i class="warning fa fa-fw fa-pause-circle">&nbsp;</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">&nbsp;</i>',
gettext('Stopped'),
'</div>',
'<div class="right-aligned">{stopped}</div>' + '<br />',
'<tpl if="template &gt; 0">',
'<div class="left-aligned">',
'<i class="fa fa-fw fa-circle-o">&nbsp;</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 &gt; 0">',
'<div class="left-aligned">',
'<i class="critical fa fa-fw fa-times-circle">&nbsp;</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');
});
}
});
}
});