Created
July 14, 2016 20:59
-
-
Save ppmathis/c44608af6bfe52141e147e9a4e17bd1a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 = { | |
| dir: function() {}, | |
| log: function() {} | |
| }; | |
| } | |
| console.log("Starting PVE Manager"); | |
| Ext.Ajax.defaultHeaders = { | |
| 'Accept': 'application/json' | |
| }; | |
| Ext.Ajax.on('beforerequest', function(conn, options) { | |
| if (PVE.CSRFPreventionToken) { | |
| if (!options.headers) { | |
| options.headers = {}; | |
| } | |
| options.headers.CSRFPreventionToken = PVE.CSRFPreventionToken; | |
| } | |
| }); | |
| var IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])"; | |
| var IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")"; | |
| var IPV6_H16 = "(?:[0-9a-fA-F]{1,4})"; | |
| var IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")"; | |
| var IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$"); | |
| var IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")\/([0-9]{1,2})$"); | |
| var IPV6_REGEXP = "(?:" + | |
| "(?:(?:" + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" + | |
| "(?:(?:" + "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" + | |
| "(?:(?:(?:" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" + | |
| "(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" + | |
| "(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" + | |
| "(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" + | |
| "(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" + ")" + IPV6_LS32 + ")|" + | |
| "(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" + ")" + IPV6_H16 + ")|" + | |
| "(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" + ")" + ")" + | |
| ")"; | |
| var IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$"); | |
| var IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")\/([0-9]{1,3})$"); | |
| var IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]"); | |
| var IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$"); | |
| Ext.define('PVE.Utils', { statics: { | |
| // this class only contains static functions | |
| toolkit: undefined, // (extjs|touch), set inside Toolkit.js | |
| 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="http://www.proxmox.com/products/proxmox-ve/subscription-service-plans">www.proxmox.com</a> to get a list of available options.', | |
| kvm_ostypes: { | |
| other: gettext('Other OS types'), | |
| wxp: 'Microsoft Windows XP/2003', | |
| w2k: 'Microsoft Windows 2000', | |
| w2k8: 'Microsoft Windows Vista/2008', | |
| win7: 'Microsoft Windows 7/2008r2', | |
| win8: 'Microsoft Windows 8.x/10/2012/r2', | |
| l24: 'Linux 2.4 Kernel', | |
| l26: 'Linux 4.X/3.X/2.6 Kernel', | |
| solaris: 'Solaris Kernel' | |
| }, | |
| render_kvm_ostype: function (value) { | |
| if (!value) { | |
| return gettext('Other OS types'); | |
| } | |
| var text = PVE.Utils.kvm_ostypes[value]; | |
| if (text) { | |
| return text; | |
| } | |
| return value; | |
| }, | |
| 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(gettext('USB')); | |
| } else if (el === 'memory') { | |
| fa.push(gettext('Memory')); | |
| } else if (el === 'cpu') { | |
| fa.push(gettext('CPU')); | |
| } else { | |
| fa.push(el); | |
| } | |
| }); | |
| return fa.join(', '); | |
| }, | |
| network_iface_types: { | |
| eth: gettext("Network Device"), | |
| bridge: 'Linux Bridge', | |
| bond: 'Linux Bond', | |
| OVSBridge: 'OVS Bridge', | |
| OVSBond: 'OVS Bond', | |
| OVSPort: 'OVS Port', | |
| OVSIntPort: 'OVS IntPort' | |
| }, | |
| render_network_iface_type: function(value) { | |
| return PVE.Utils.network_iface_types[value] || | |
| PVE.Utils.unknownText; | |
| }, | |
| render_qemu_bios: function(value) { | |
| if (!value) { | |
| return PVE.Utils.defaultText + ' (SeaBIOS)'; | |
| } else if (value === 'seabios') { | |
| return "SeaBIOS"; | |
| } else if (value === 'ovmf') { | |
| return "OVMF (UEFI)"; | |
| } else { | |
| return value; | |
| } | |
| }, | |
| render_scsihw: function(value) { | |
| if (!value) { | |
| return PVE.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'), | |
| cirrus: 'Cirrus Logic GD5446', | |
| 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' | |
| }, | |
| render_kvm_language: function (value) { | |
| if (!value) { | |
| return PVE.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; | |
| }, | |
| render_console_viewer: function(value) { | |
| if (!value || value === '__default__') { | |
| return PVE.Utils.defaultText + ' (HTML5)'; | |
| } else if (value === 'vv') { | |
| return 'SPICE (remote-viewer)'; | |
| } else if (value === 'html5') { | |
| return 'HTML5 (noVNC)'; | |
| } else { | |
| return value; | |
| } | |
| }, | |
| language_map: { | |
| zh_CN: 'Chinese', | |
| ca: 'Catalan', | |
| da: 'Danish', | |
| en: 'English', | |
| eu: 'Euskera (Basque)', | |
| fr: 'French', | |
| de: 'German', | |
| it: 'Italian', | |
| ja: 'Japanese', | |
| nb: 'Norwegian (Bokmal)', | |
| nn: 'Norwegian (Nynorsk)', | |
| fa: 'Persian (Farsi)', | |
| pl: 'Polish', | |
| pt_BR: 'Portuguese (Brazil)', | |
| ru: 'Russian', | |
| sl: 'Slovenian', | |
| es: 'Spanish', | |
| sv: 'Swedish', | |
| tr: 'Turkish' | |
| }, | |
| render_language: function (value) { | |
| if (!value) { | |
| return PVE.Utils.defaultText + ' (English)'; | |
| } | |
| var text = PVE.Utils.language_map[value]; | |
| if (text) { | |
| return text + ' (' + value + ')'; | |
| } | |
| return value; | |
| }, | |
| language_array: function() { | |
| var data = [['__default__', PVE.Utils.render_language('')]]; | |
| Ext.Object.each(PVE.Utils.language_map, function(key, value) { | |
| data.push([key, PVE.Utils.render_language(value)]); | |
| }); | |
| return data; | |
| }, | |
| render_kvm_vga_driver: function (value) { | |
| if (!value) { | |
| return PVE.Utils.defaultText; | |
| } | |
| var text = PVE.Utils.kvm_vga_drivers[value]; | |
| 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; | |
| }, | |
| authOK: function() { | |
| return Ext.util.Cookies.get('PVEAuthCookie'); | |
| }, | |
| authClear: function() { | |
| Ext.util.Cookies.clear("PVEAuthCookie"); | |
| }, | |
| // fixme: remove - not needed? | |
| gridLineHeigh: function() { | |
| return 21; | |
| //if (Ext.isGecko) | |
| //return 23; | |
| //return 21; | |
| }, | |
| extractRequestError: function(result, verbose) { | |
| var msg = gettext('Successful'); | |
| if (!result.success) { | |
| msg = gettext("Unknown error"); | |
| if (result.message) { | |
| msg = result.message; | |
| if (result.status) { | |
| msg += ' (' + result.status + ')'; | |
| } | |
| } | |
| if (verbose && Ext.isObject(result.errors)) { | |
| msg += "<br>"; | |
| Ext.Object.each(result.errors, function(prop, desc) { | |
| msg += "<br><b>" + Ext.htmlEncode(prop) + "</b>: " + | |
| Ext.htmlEncode(desc); | |
| }); | |
| } | |
| } | |
| return msg; | |
| }, | |
| 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 = PVE.Utils.extractRequestError(action.result, true); | |
| break; | |
| } | |
| return msg; | |
| }, | |
| // Ext.Ajax.request | |
| API2Request: function(reqOpts) { | |
| var newopts = Ext.apply({ | |
| waitMsg: gettext('Please wait...') | |
| }, reqOpts); | |
| if (!newopts.url.match(/^\/api2/)) { | |
| newopts.url = '/api2/extjs' + newopts.url; | |
| } | |
| delete newopts.callback; | |
| var createWrapper = function(successFn, callbackFn, failureFn) { | |
| Ext.apply(newopts, { | |
| success: function(response, options) { | |
| if (options.waitMsgTarget) { | |
| if (PVE.Utils.toolkit === 'touch') { | |
| options.waitMsgTarget.setMasked(false); | |
| } else { | |
| options.waitMsgTarget.setLoading(false); | |
| } | |
| } | |
| var result = Ext.decode(response.responseText); | |
| response.result = result; | |
| if (!result.success) { | |
| response.htmlStatus = PVE.Utils.extractRequestError(result, true); | |
| Ext.callback(callbackFn, options.scope, [options, false, response]); | |
| Ext.callback(failureFn, options.scope, [response, options]); | |
| return; | |
| } | |
| Ext.callback(callbackFn, options.scope, [options, true, response]); | |
| Ext.callback(successFn, options.scope, [response, options]); | |
| }, | |
| failure: function(response, options) { | |
| if (options.waitMsgTarget) { | |
| if (PVE.Utils.toolkit === 'touch') { | |
| options.waitMsgTarget.setMasked(false); | |
| } else { | |
| options.waitMsgTarget.setLoading(false); | |
| } | |
| } | |
| response.result = {}; | |
| try { | |
| response.result = Ext.decode(response.responseText); | |
| } catch(e) {} | |
| var msg = gettext('Connection error') + ' - server offline?'; | |
| if (response.aborted) { | |
| msg = gettext('Connection error') + ' - aborted.'; | |
| } else if (response.timedout) { | |
| msg = gettext('Connection error') + ' - Timeout.'; | |
| } else if (response.status && response.statusText) { | |
| msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText; | |
| } | |
| response.htmlStatus = msg; | |
| Ext.callback(callbackFn, options.scope, [options, false, response]); | |
| Ext.callback(failureFn, options.scope, [response, options]); | |
| } | |
| }); | |
| }; | |
| createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure); | |
| var target = newopts.waitMsgTarget; | |
| if (target) { | |
| if (PVE.Utils.toolkit === 'touch') { | |
| target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg} ); | |
| } else { | |
| // Note: ExtJS bug - this does not work when component is not rendered | |
| target.setLoading(newopts.waitMsg); | |
| } | |
| } | |
| Ext.Ajax.request(newopts); | |
| }, | |
| assemble_field_data: function(values, data) { | |
| if (Ext.isObject(data)) { | |
| Ext.Object.each(data, function(name, val) { | |
| if (values.hasOwnProperty(name)) { | |
| var bucket = values[name]; | |
| if (!Ext.isArray(bucket)) { | |
| bucket = values[name] = [bucket]; | |
| } | |
| if (Ext.isArray(val)) { | |
| values[name] = bucket.concat(val); | |
| } else { | |
| bucket.push(val); | |
| } | |
| } else { | |
| values[name] = val; | |
| } | |
| }); | |
| } | |
| }, | |
| checked_command: function(orig_cmd) { | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/localhost/subscription', | |
| method: 'GET', | |
| //waitMsgTarget: me, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| }, | |
| success: function(response, opts) { | |
| var data = response.result.data; | |
| if (data.status !== 'Active') { | |
| Ext.Msg.show({ | |
| title: gettext('No valid subscription'), | |
| icon: Ext.Msg.WARNING, | |
| msg: PVE.Utils.noSubKeyHtml, | |
| buttons: Ext.Msg.OK, | |
| callback: function(btn) { | |
| if (btn !== 'ok') { | |
| return; | |
| } | |
| orig_cmd(); | |
| } | |
| }); | |
| } else { | |
| orig_cmd(); | |
| } | |
| } | |
| }); | |
| }, | |
| task_desc_table: { | |
| vncproxy: [ 'VM/CT', gettext('Console') ], | |
| spiceproxy: [ 'VM/CT', gettext('Console') + ' (Spice)' ], | |
| vncshell: [ '', gettext('Shell') ], | |
| spiceshell: [ '', gettext('Shell') + ' (Spice)' ], | |
| qmsnapshot: [ 'VM', gettext('Snapshot') ], | |
| qmrollback: [ 'VM', gettext('Rollback') ], | |
| qmdelsnapshot: [ 'VM', gettext('Delete Snapshot') ], | |
| qmcreate: [ 'VM', gettext('Create') ], | |
| qmrestore: [ 'VM', gettext('Restore') ], | |
| qmdestroy: [ 'VM', gettext('Destroy') ], | |
| qmigrate: [ 'VM', gettext('Migrate') ], | |
| qmclone: [ 'VM', gettext('Clone') ], | |
| qmmove: [ 'VM', gettext('Move disk') ], | |
| qmtemplate: [ 'VM', gettext('Convert to template') ], | |
| qmstart: [ 'VM', gettext('Start') ], | |
| qmstop: [ 'VM', gettext('Stop') ], | |
| qmreset: [ 'VM', gettext('Reset') ], | |
| qmshutdown: [ 'VM', gettext('Shutdown') ], | |
| qmsuspend: [ 'VM', gettext('Suspend') ], | |
| qmresume: [ 'VM', gettext('Resume') ], | |
| qmconfig: [ 'VM', gettext('Configure') ], | |
| vzsnapshot: [ 'CT', gettext('Snapshot') ], | |
| vzrollback: [ 'CT', gettext('Rollback') ], | |
| vzdelsnapshot: [ 'CT', gettext('Delete Snapshot') ], | |
| vzcreate: ['CT', gettext('Create') ], | |
| vzrestore: ['CT', gettext('Restore') ], | |
| vzdestroy: ['CT', gettext('Destroy') ], | |
| vzmigrate: [ 'CT', gettext('Migrate') ], | |
| vzclone: [ 'CT', gettext('Clone') ], | |
| vztemplate: [ 'CT', gettext('Convert to template') ], | |
| vzstart: ['CT', gettext('Start') ], | |
| vzstop: ['CT', gettext('Stop') ], | |
| vzmount: ['CT', gettext('Mount') ], | |
| vzumount: ['CT', gettext('Unmount') ], | |
| vzshutdown: ['CT', gettext('Shutdown') ], | |
| vzsuspend: [ 'CT', gettext('Suspend') ], | |
| vzresume: [ 'CT', gettext('Resume') ], | |
| hamigrate: [ 'HA', gettext('Migrate') ], | |
| hastart: [ 'HA', gettext('Start') ], | |
| hastop: [ 'HA', gettext('Stop') ], | |
| srvstart: ['SRV', gettext('Start') ], | |
| srvstop: ['SRV', gettext('Stop') ], | |
| srvrestart: ['SRV', gettext('Restart') ], | |
| srvreload: ['SRV', gettext('Reload') ], | |
| cephcreatemon: ['Ceph Monitor', gettext('Create') ], | |
| cephdestroymon: ['Ceph Monitor', gettext('Destroy') ], | |
| cephcreateosd: ['Ceph OSD', gettext('Create') ], | |
| cephdestroyosd: ['Ceph OSD', gettext('Destroy') ], | |
| imgcopy: ['', gettext('Copy data') ], | |
| imgdel: ['', gettext('Erase data') ], | |
| download: ['', gettext('Download') ], | |
| vzdump: ['', gettext('Backup') ], | |
| aptupdate: ['', gettext('Update package database') ], | |
| startall: [ '', gettext('Start all VMs and Containers') ], | |
| stopall: [ '', gettext('Stop all VMs and Containers') ], | |
| migrateall: [ '', gettext('Migrate all VMs and Containers') ] | |
| }, | |
| format_task_description: function(type, id) { | |
| var farray = PVE.Utils.task_desc_table[type]; | |
| if (!farray) { | |
| return type; | |
| } | |
| var prefix = farray[0]; | |
| var text = farray[1]; | |
| if (prefix) { | |
| return prefix + ' ' + id + ' - ' + text; | |
| } | |
| return text; | |
| }, | |
| parse_task_upid: function(upid) { | |
| var task = {}; | |
| var res = upid.match(/^UPID:(\S+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/); | |
| if (!res) { | |
| throw "unable to parse upid '" + upid + "'"; | |
| } | |
| task.node = res[1]; | |
| task.pid = parseInt(res[2], 16); | |
| task.pstart = parseInt(res[3], 16); | |
| task.starttime = parseInt(res[4], 16); | |
| task.type = res[5]; | |
| task.id = res[6]; | |
| task.user = res[7]; | |
| task.desc = PVE.Utils.format_task_description(task.type, task.id); | |
| return task; | |
| }, | |
| format_size: function(size) { | |
| /*jslint confusion: true */ | |
| var units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']; | |
| var num = 0; | |
| while (size >= 1024 && ((num++)+1) < units.length) { | |
| size = size / 1024; | |
| } | |
| return size.toFixed((num > 0)?2:0) + " " + units[num] + "B"; | |
| }, | |
| format_html_bar: function(per, text) { | |
| return "<div class='pve-bar-wrap'>" + text + "<div class='pve-bar-border'>" + | |
| "<div class='pve-bar-inner' style='width:" + per + "%;'></div>" + | |
| "</div></div>"; | |
| }, | |
| format_cpu_bar: function(per1, per2, text) { | |
| return "<div class='pve-bar-border'>" + | |
| "<div class='pve-bar-inner' style='width:" + per1 + "%;'></div>" + | |
| "<div class='pve-bar-inner2' style='width:" + per2 + "%;'></div>" + | |
| "<div class='pve-bar-text'>" + text + "</div>" + | |
| "</div>"; | |
| }, | |
| format_large_bar: function(per, text) { | |
| if (!text) { | |
| text = per.toFixed(1) + "%"; | |
| } | |
| return "<div class='pve-largebar-border'>" + | |
| "<div class='pve-largebar-inner' style='width:" + per + "%;'></div>" + | |
| "<div class='pve-largebar-text'>" + text + "</div>" + | |
| "</div>"; | |
| }, | |
| format_duration_long: function(ut) { | |
| var days = Math.floor(ut / 86400); | |
| ut -= days*86400; | |
| var hours = Math.floor(ut / 3600); | |
| ut -= hours*3600; | |
| var mins = Math.floor(ut / 60); | |
| ut -= mins*60; | |
| var hours_str = '00' + hours.toString(); | |
| hours_str = hours_str.substr(hours_str.length - 2); | |
| var mins_str = "00" + mins.toString(); | |
| mins_str = mins_str.substr(mins_str.length - 2); | |
| var ut_str = "00" + ut.toString(); | |
| ut_str = ut_str.substr(ut_str.length - 2); | |
| if (days) { | |
| var ds = days > 1 ? PVE.Utils.daysText : PVE.Utils.dayText; | |
| return days.toString() + ' ' + ds + ' ' + | |
| hours_str + ':' + mins_str + ':' + ut_str; | |
| } else { | |
| return hours_str + ':' + mins_str + ':' + ut_str; | |
| } | |
| }, | |
| format_duration_short: function(ut) { | |
| if (ut < 60) { | |
| return ut.toString() + 's'; | |
| } | |
| if (ut < 3600) { | |
| var mins = ut / 60; | |
| return mins.toFixed(0) + 'm'; | |
| } | |
| if (ut < 86400) { | |
| var hours = ut / 3600; | |
| return hours.toFixed(0) + 'h'; | |
| } | |
| var days = ut / 86400; | |
| return days.toFixed(0) + 'd'; | |
| }, | |
| yesText: gettext('Yes'), | |
| noText: gettext('No'), | |
| noneText: gettext('none'), | |
| errorText: gettext('Error'), | |
| unknownText: gettext('Unknown'), | |
| defaultText: gettext('Default'), | |
| daysText: gettext('days'), | |
| dayText: gettext('day'), | |
| runningText: gettext('running'), | |
| stoppedText: gettext('stopped'), | |
| neverText: gettext('never'), | |
| totalText: gettext('Total'), | |
| usedText: gettext('Used'), | |
| directoryText: gettext('Directory'), | |
| imagesText: gettext('Disk image'), | |
| backupFileText: gettext('VZDump backup file'), | |
| vztmplText: gettext('Container template'), | |
| isoImageText: gettext('ISO image'), | |
| containersText: gettext('Container'), | |
| stateText: gettext('State'), | |
| groupText: gettext('Group'), | |
| format_expire: function(date) { | |
| if (!date) { | |
| return PVE.Utils.neverText; | |
| } | |
| return Ext.Date.format(date, "Y-m-d"); | |
| }, | |
| format_storage_type: function(value) { | |
| if (value === 'dir') { | |
| return PVE.Utils.directoryText; | |
| } else if (value === 'nfs') { | |
| return 'NFS'; | |
| } else if (value === 'glusterfs') { | |
| return 'GlusterFS'; | |
| } else if (value === 'lvm') { | |
| return 'LVM'; | |
| } else if (value === 'lvmthin') { | |
| return 'LVM-Thin'; | |
| } else if (value === 'iscsi') { | |
| return 'iSCSI'; | |
| } else if (value === 'rbd') { | |
| return 'RBD'; | |
| } else if (value === 'sheepdog') { | |
| return 'Sheepdog'; | |
| } else if (value === 'zfs') { | |
| return 'ZFS over iSCSI'; | |
| } else if (value === 'zfspool') { | |
| return 'ZFS'; | |
| } else if (value === 'iscsidirect') { | |
| return 'iSCSIDirect'; | |
| } else if (value === 'drbd') { | |
| return 'DRBD'; | |
| } else { | |
| return PVE.Utils.unknownText; | |
| } | |
| }, | |
| format_boolean_with_default: function(value) { | |
| if (Ext.isDefined(value) && value !== '__default__') { | |
| return value ? PVE.Utils.yesText : PVE.Utils.noText; | |
| } | |
| return PVE.Utils.defaultText; | |
| }, | |
| format_boolean: function(value) { | |
| return value ? PVE.Utils.yesText : PVE.Utils.noText; | |
| }, | |
| format_neg_boolean: function(value) { | |
| return !value ? PVE.Utils.yesText : PVE.Utils.noText; | |
| }, | |
| format_ha: function(value) { | |
| var text = PVE.Utils.format_boolean(value.managed); | |
| if (value.managed) { | |
| text += ', ' + PVE.Utils.stateText + ': '; | |
| text += value.state || PVE.Utils.noneText; | |
| text += ', ' + PVE.Utils.groupText + ': '; | |
| text += value.group || PVE.Utils.noneText; | |
| } | |
| return text; | |
| }, | |
| format_content_types: function(value) { | |
| var cta = []; | |
| Ext.each(value.split(',').sort(), function(ct) { | |
| if (ct === 'images') { | |
| cta.push(PVE.Utils.imagesText); | |
| } else if (ct === 'backup') { | |
| cta.push(PVE.Utils.backupFileText); | |
| } else if (ct === 'vztmpl') { | |
| cta.push(PVE.Utils.vztmplText); | |
| } else if (ct === 'iso') { | |
| cta.push(PVE.Utils.isoImageText); | |
| } else if (ct === 'rootdir') { | |
| cta.push(PVE.Utils.containersText); | |
| } | |
| }); | |
| return cta.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 PVE.Utils.format_size(value); | |
| }, | |
| render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) { | |
| var servertime = new Date(value * 1000); | |
| return Ext.Date.format(servertime, 'Y-m-d H:i:s'); | |
| }, | |
| 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); | |
| }, | |
| render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) { | |
| var icon = ''; | |
| var gridcls = ''; | |
| switch (value) { | |
| case 'lxc': icon = 'cube'; | |
| gridcls = '-stopped'; | |
| break; | |
| case 'qemu': icon = 'desktop'; | |
| gridcls = '-stopped'; | |
| break; | |
| case 'node': icon = 'building'; | |
| gridcls = '-offline'; | |
| break; | |
| case 'storage': icon = 'database'; break; | |
| case 'pool': icon = 'tags'; break; | |
| default: icon = 'file'; | |
| } | |
| if (value === 'lxc' || value === 'qemu') { | |
| if (record.data.running && record.data.status !== 'paused') { | |
| gridcls = '-running'; | |
| } else if (record.data.running) { | |
| gridcls = '-paused'; | |
| } | |
| if (record.data.template) { | |
| icon = 'file-o'; | |
| gridcls = '-template-' + value; | |
| } | |
| } else if (value === 'node') { | |
| if (record.data.running) { | |
| gridcls = '-online'; | |
| } | |
| } | |
| var fa = '<i class="fa fa-fw x-fa-grid' + gridcls + ' fa-' + icon + '"></i> '; | |
| return fa + value; | |
| }, | |
| render_uptime: function(value, metaData, record, rowIndex, colIndex, store) { | |
| var uptime = value; | |
| if (uptime === undefined) { | |
| return ''; | |
| } | |
| if (uptime <= 0) { | |
| return '-'; | |
| } | |
| return PVE.Utils.format_duration_long(uptime); | |
| }, | |
| 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 PVE.Utils.format_task_description(type, id); | |
| }, | |
| dialog_title: function(subject, create, isAdd) { | |
| if (create) { | |
| if (isAdd) { | |
| return gettext('Add') + ': ' + subject; | |
| } else { | |
| return gettext('Create') + ': ' + subject; | |
| } | |
| } else { | |
| return gettext('Edit') + ': ' + subject; | |
| } | |
| }, | |
| windowHostname: function() { | |
| return window.location.hostname.replace(IP6_bracket_match, | |
| function(m, addr, offset, original) { return addr; }); | |
| }, | |
| openDefaultConsoleWindow: function(allowSpice, vmtype, vmid, nodename, vmname) { | |
| var dv = PVE.Utils.defaultViewer(allowSpice); | |
| PVE.Utils.openConsoleWindow(dv, vmtype, vmid, nodename, vmname); | |
| }, | |
| openConsoleWindow: function(viewer, vmtype, vmid, nodename, vmname) { | |
| // 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); | |
| } 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 { | |
| throw "unknown viewer type"; | |
| } | |
| }, | |
| defaultViewer: function(allowSpice) { | |
| var vncdefault = 'html5'; | |
| var dv = PVE.VersionInfo.console || vncdefault; | |
| if (dv === 'vv' && !allowSpice) { | |
| dv = vncdefault; | |
| } | |
| return dv; | |
| }, | |
| openVNCViewer: function(vmtype, vmid, nodename, vmname) { | |
| var url = Ext.urlEncode({ | |
| console: vmtype, // kvm, lxc, upgrade or shell | |
| novnc: 1, | |
| vmid: vmid, | |
| vmname: vmname, | |
| node: nodename | |
| }); | |
| var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427"); | |
| 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); | |
| } | |
| }; | |
| PVE.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"); | |
| } | |
| }); | |
| }, | |
| // comp.setLoading() is buggy in ExtJS 4.0.7, so we | |
| // use el.mask() instead | |
| setErrorMask: function(comp, msg) { | |
| var el = comp.el; | |
| if (!el) { | |
| return; | |
| } | |
| if (!msg) { | |
| el.unmask(); | |
| } else { | |
| if (msg === true) { | |
| el.mask(gettext("Loading...")); | |
| } else { | |
| el.mask(msg); | |
| } | |
| } | |
| }, | |
| monStoreErrors: function(me, store) { | |
| me.mon(store, 'beforeload', function(s, operation, eOpts) { | |
| if (!me.loadCount) { | |
| me.loadCount = 0; // make sure it is numeric | |
| PVE.Utils.setErrorMask(me, true); | |
| } | |
| }); | |
| // only works with 'pve' proxy | |
| me.mon(store.proxy, 'afterload', function(proxy, request, success) { | |
| me.loadCount++; | |
| if (success) { | |
| PVE.Utils.setErrorMask(me, false); | |
| return; | |
| } | |
| var msg; | |
| /*jslint nomen: true */ | |
| var operation = request._operation; | |
| var error = operation.getError(); | |
| if (error.statusText) { | |
| msg = error.statusText + ' (' + error.status + ')'; | |
| } else { | |
| msg = gettext('Connection error'); | |
| } | |
| PVE.Utils.setErrorMask(me, msg); | |
| }); | |
| }, | |
| createCmdMenu: function(v, record, item, index, event) { | |
| event.stopEvent(); | |
| if (!(v instanceof Ext.tree.View)) { | |
| v.select(record); | |
| } | |
| var menu; | |
| if (record.data.type === 'qemu' && !record.data.template) { | |
| menu = Ext.create('PVE.qemu.CmdMenu', { | |
| pveSelNode: record | |
| }); | |
| } else if (record.data.type === 'qemu' && record.data.template) { | |
| menu = Ext.create('PVE.qemu.TemplateMenu', { | |
| pveSelNode: record | |
| }); | |
| } else if (record.data.type === 'lxc' && !record.data.template) { | |
| menu = Ext.create('PVE.lxc.CmdMenu', { | |
| pveSelNode: record | |
| }); | |
| } else if (record.data.type === 'lxc' && record.data.template) { | |
| /* since clone does not work reliably, disable for now | |
| menu = Ext.create('PVE.lxc.TemplateMenu', { | |
| pveSelNode: record | |
| }); | |
| */ | |
| return; | |
| } else { | |
| return; | |
| } | |
| menu.showAt(event.getXY()); | |
| } | |
| }}); | |
| /*global IP4_match, IP4_cidr_match, IP6_match, IP6_cidr_match, IP64_match*/ | |
| // ExtJS related things | |
| PVE.Utils.toolkit = 'extjs'; | |
| // do not send '_dc' parameter | |
| Ext.Ajax.disableCaching = false; | |
| // custom Vtypes | |
| Ext.apply(Ext.form.field.VTypes, { | |
| IPAddress: function(v) { | |
| return IP4_match.test(v); | |
| }, | |
| IPAddressText: gettext('Example') + ': 192.168.1.1', | |
| IPAddressMask: /[\d\.]/i, | |
| IPCIDRAddress: function(v) { | |
| var result = IP4_cidr_match.exec(v); | |
| // limits according to JSON Schema see | |
| // pve-common/src/PVE/JSONSchema.pm | |
| return (result !== null && result[1] >= 8 && result[1] <= 32); | |
| }, | |
| IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24' + "<br>" + gettext('Valid CIDR Range') + ': 8-32', | |
| IPCIDRAddressMask: /[\d\.\/]/i, | |
| IP6Address: function(v) { | |
| return IP6_match.test(v); | |
| }, | |
| IP6AddressText: gettext('Example') + ': 2001:DB8::42', | |
| IP6AddressMask: /[A-Fa-f0-9:]/, | |
| IP6CIDRAddress: function(v) { | |
| var result = IP6_cidr_match.exec(v); | |
| // limits according to JSON Schema see | |
| // pve-common/src/PVE/JSONSchema.pm | |
| return (result !== null && result[1] >= 8 && result[1] <= 120); | |
| }, | |
| IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64' + "<br>" + gettext('Valid CIDR Range') + ': 8-120', | |
| IP6CIDRAddressMask: /[A-Fa-f0-9:\/]/, | |
| IP6PrefixLength: function(v) { | |
| return v >= 0 && v <= 128; | |
| }, | |
| IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128', | |
| IP6PrefixLengthMask: /[0-9]/, | |
| IP64Address: function(v) { | |
| return IP64_match.test(v); | |
| }, | |
| IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42', | |
| IP64AddressMask: /[A-Fa-f0-9\.:]/, | |
| MacAddress: function(v) { | |
| return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v); | |
| }, | |
| MacAddressMask: /[a-fA-F0-9:]/, | |
| MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab', | |
| BridgeName: function(v) { | |
| return (/^vmbr\d{1,4}$/).test(v); | |
| }, | |
| BridgeNameText: gettext('Format') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999', | |
| BondName: function(v) { | |
| return (/^bond\d{1,4}$/).test(v); | |
| }, | |
| BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999', | |
| InterfaceName: function(v) { | |
| return (/^[a-z][a-z0-9_]{1,20}$/).test(v); | |
| }, | |
| InterfaceNameText: gettext('Format') + ': [a-z][a-z0-9_]{1,20}', | |
| 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"', | |
| StorageId: function(v) { | |
| return (/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i).test(v); | |
| }, | |
| StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'", | |
| ConfigId: function(v) { | |
| return (/^[a-z][a-z0-9\_]+$/i).test(v); | |
| }, | |
| ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'", | |
| HttpProxy: function(v) { | |
| return (/^http:\/\/.*$/).test(v); | |
| }, | |
| HttpProxyText: gettext('Example') + ": http://username:password@host:port/", | |
| DnsName: function(v) { | |
| return (/^(([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)\.)*([A-Za-z0-9]([A-Za-z0-9\-]*[A-Za-z0-9])?)$/).test(v); | |
| }, | |
| DnsNameText: gettext('This is not a valid DNS name'), | |
| // workaround for https://www.sencha.com/forum/showthread.php?302150 | |
| pveMail: function(v) { | |
| return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v); | |
| }, | |
| pveMailText: gettext('Example') + ": [email protected]" | |
| }); | |
| // ExtJs 5-6 has an issue with caching | |
| // see https://www.sencha.com/forum/showthread.php?308989 | |
| Ext.define('PVE.UnderlayPool', { | |
| override: 'Ext.dom.UnderlayPool', | |
| checkOut: function () { | |
| var cache = this.cache, | |
| len = cache.length, | |
| el; | |
| // do cleanup because some of the objects might have been destroyed | |
| while (len--) { | |
| if (cache[len].destroyed) { | |
| cache.splice(len, 1); | |
| } | |
| } | |
| // end do cleanup | |
| el = cache.shift(); | |
| if (!el) { | |
| el = Ext.Element.create(this.elementConfig); | |
| el.setVisibilityMode(2); | |
| //<debug> | |
| // tell the spec runner to ignore this element when checking if the dom is clean | |
| el.dom.setAttribute('data-sticky', true); | |
| //</debug> | |
| } | |
| return el; | |
| } | |
| }); | |
| // if the order of the values are not the same in originalValue and value | |
| // extjs will not overwrite value, but marks the field dirty and thus | |
| // the reset button will be enabled (but clicking it changes nothing) | |
| // so if the arrays are not the same after resetting, we | |
| // clear and set it | |
| Ext.define('PVE.form.ComboBox', { | |
| override: 'Ext.form.field.ComboBox', | |
| reset: function() { | |
| // copied from combobox | |
| var me = this; | |
| me.callParent(); | |
| me.applyEmptyText(); | |
| // clear and set when not the same | |
| if (Ext.isArray(me.originalValue) && !Ext.Array.equals(me.getValue(), me.originalValue)) { | |
| me.clearValue(); | |
| me.setValue(me.originalValue); | |
| } | |
| } | |
| }); | |
| // should be fixed with ExtJS 6.0.2, see: | |
| // https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll | |
| Ext.define('PVE.Datepicker', { | |
| override: 'Ext.picker.Date', | |
| hideMode: 'visibility' | |
| }); | |
| // force alert boxes to be rendered with an Error Icon | |
| // since Ext.Msg is an object and not a prototype, we need to override it | |
| // after the framework has been initiated | |
| Ext.onReady(function() { | |
| /*jslint confusion: true */ | |
| Ext.override(Ext.Msg, { | |
| alert: function(title, message, fn, scope) { | |
| if (Ext.isString(title)) { | |
| var config = { | |
| title: title, | |
| message: message, | |
| icon: this.ERROR, | |
| buttons: this.OK, | |
| fn: fn, | |
| scope : scope, | |
| minWidth: this.minWidth | |
| }; | |
| return this.show(config); | |
| } | |
| } | |
| }); | |
| /*jslint confusion: false */ | |
| }); | |
| Ext.define('Ext.ux.IFrame', { | |
| extend: 'Ext.Component', | |
| alias: 'widget.uxiframe', | |
| loadMask: 'Loading...', | |
| src: 'about:blank', | |
| renderTpl: [ | |
| '<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>' | |
| ], | |
| childEls: ['iframeEl'], | |
| initComponent: function () { | |
| this.callParent(); | |
| this.frameName = this.frameName || this.id + '-frame'; | |
| }, | |
| initEvents : function() { | |
| var me = this; | |
| me.callParent(); | |
| me.iframeEl.on('load', me.onLoad, me); | |
| }, | |
| initRenderData: function() { | |
| return Ext.apply(this.callParent(), { | |
| src: this.src, | |
| frameName: this.frameName | |
| }); | |
| }, | |
| getBody: function() { | |
| var doc = this.getDoc(); | |
| return doc.body || doc.documentElement; | |
| }, | |
| getDoc: function() { | |
| try { | |
| return this.getWin().document; | |
| } catch (ex) { | |
| return null; | |
| } | |
| }, | |
| getWin: function() { | |
| var me = this, | |
| name = me.frameName, | |
| win = Ext.isIE | |
| ? me.iframeEl.dom.contentWindow | |
| : window.frames[name]; | |
| return win; | |
| }, | |
| getFrame: function() { | |
| var me = this; | |
| return me.iframeEl.dom; | |
| }, | |
| beforeDestroy: function () { | |
| this.cleanupListeners(true); | |
| this.callParent(); | |
| }, | |
| cleanupListeners: function(destroying){ | |
| var doc, prop; | |
| if (this.rendered) { | |
| try { | |
| doc = this.getDoc(); | |
| if (doc) { | |
| /*jslint nomen: true*/ | |
| Ext.get(doc).un(this._docListeners); | |
| /*jslint nomen: false*/ | |
| if (destroying && doc.hasOwnProperty) { | |
| for (prop in doc) { | |
| if (doc.hasOwnProperty(prop)) { | |
| delete doc[prop]; | |
| } | |
| } | |
| } | |
| } | |
| } catch(e) { } | |
| } | |
| }, | |
| onLoad: function() { | |
| var me = this, | |
| doc = me.getDoc(), | |
| fn = me.onRelayedEvent; | |
| if (doc) { | |
| try { | |
| // These events need to be relayed from the inner document (where they stop | |
| // bubbling) up to the outer document. This has to be done at the DOM level so | |
| // the event reaches listeners on elements like the document body. The effected | |
| // mechanisms that depend on this bubbling behavior are listed to the right | |
| // of the event. | |
| /*jslint nomen: true*/ | |
| Ext.get(doc).on( | |
| me._docListeners = { | |
| mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront) | |
| mousemove: fn, // window resize drag detection | |
| mouseup: fn, // window resize termination | |
| click: fn, // not sure, but just to be safe | |
| dblclick: fn, // not sure again | |
| scope: me | |
| } | |
| ); | |
| /*jslint nomen: false*/ | |
| } catch(e) { | |
| // cannot do this xss | |
| } | |
| // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK! | |
| Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me); | |
| this.el.unmask(); | |
| this.fireEvent('load', this); | |
| } else if (me.src) { | |
| this.el.unmask(); | |
| this.fireEvent('error', this); | |
| } | |
| }, | |
| onRelayedEvent: function (event) { | |
| // relay event from the iframe's document to the document that owns the iframe... | |
| var iframeEl = this.iframeEl, | |
| // Get the left-based iframe position | |
| iframeXY = iframeEl.getTrueXY(), | |
| originalEventXY = event.getXY(), | |
| // Get the left-based XY position. | |
| // This is because the consumer of the injected event will | |
| // perform its own RTL normalization. | |
| eventXY = event.getTrueXY(); | |
| // the event from the inner document has XY relative to that document's origin, | |
| // so adjust it to use the origin of the iframe in the outer document: | |
| event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]]; | |
| event.injectEvent(iframeEl); // blame the iframe for the event... | |
| event.xy = originalEventXY; // restore the original XY (just for safety) | |
| }, | |
| load: function (src) { | |
| var me = this, | |
| text = me.loadMask, | |
| frame = me.getFrame(); | |
| if (me.fireEvent('beforeload', me, src) !== false) { | |
| if (text && me.el) { | |
| me.el.mask(text); | |
| } | |
| frame.src = me.src = (src || me.src); | |
| } | |
| } | |
| }); | |
| // Some configuration values are complex strings - | |
| // so we need parsers/generators for them. | |
| Ext.define('PVE.Parser', { statics: { | |
| // this class only contains static functions | |
| parseBoolean: function(value, default_value) { | |
| if (!Ext.isDefined(value)) { | |
| return default_value; | |
| } | |
| value = value.toLowerCase(); | |
| return value === '1' || | |
| value === 'on' || | |
| value === 'yes' || | |
| value === 'true'; | |
| }, | |
| 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; | |
| }, | |
| 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|firewall|tag|rate)=(\S+)$/); | |
| if (!match_res) { | |
| // todo: simply ignore errors ? | |
| return; // continue | |
| } | |
| data[match_res[1]] = match_res[2]; | |
| }); | |
| 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_]+)=(\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]; | |
| res[k] = v; | |
| }); | |
| if (errors || !res.file) { | |
| return; | |
| } | |
| var m = res.file.match(/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):/i); | |
| if (m) { | |
| res.storage = m[1]; | |
| res.type = 'volume'; | |
| } else if (res.file.match(/^\/dev\//)) { | |
| res.type = 'device'; | |
| } else { | |
| res.type = 'bind'; | |
| } | |
| return res; | |
| }, | |
| printLxcMountPoint: function(mp) { | |
| var drivestr = mp.file; | |
| Ext.Object.each(mp, function(key, value) { | |
| if (!Ext.isDefined(value) || key === 'file' || | |
| key === 'type' || key === 'storage') { | |
| return; // continue | |
| } | |
| drivestr += ',' + key + '=' + value; | |
| }); | |
| return drivestr; | |
| }, | |
| parseStartup: function(value) { | |
| if (value === undefined) { | |
| return; | |
| } | |
| var res = {}; | |
| var errors = false; | |
| Ext.Array.each(value.split(','), function(p) { | |
| if (!p || p.match(/^\s*$/)) { | |
| return; // continue | |
| } | |
| var match_res; | |
| if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) { | |
| res.order = match_res[2]; | |
| } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) { | |
| res.up = match_res[1]; | |
| } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) { | |
| res.down = match_res[1]; | |
| } else { | |
| errors = true; | |
| return false; // break | |
| } | |
| }); | |
| if (errors) { | |
| return; | |
| } | |
| return res; | |
| }, | |
| printStartup: function(startup) { | |
| var arr = []; | |
| if (startup.order !== undefined && startup.order !== '') { | |
| arr.push('order=' + startup.order); | |
| } | |
| if (startup.up !== undefined && startup.up !== '') { | |
| arr.push('up=' + startup.up); | |
| } | |
| if (startup.down !== undefined && startup.down !== '') { | |
| arr.push('down=' + startup.down); | |
| } | |
| return arr.join(','); | |
| }, | |
| parseQemuSmbios1: function(value) { | |
| var res = {}; | |
| Ext.Array.each(value.split(','), function(p) { | |
| var kva = p.split('=', 2); | |
| res[kva[0]] = kva[1]; | |
| }); | |
| return res; | |
| }, | |
| printQemuSmbios1: function(data) { | |
| var datastr = ''; | |
| Ext.Object.each(data, function(key, value) { | |
| if (value === '') { return; } | |
| datastr += (datastr !== '' ? ',' : '') + key + '=' + value; | |
| }); | |
| return datastr; | |
| }, | |
| parseTfaConfig: function(value) { | |
| var res = {}; | |
| Ext.Array.each(value.split(','), function(p) { | |
| var kva = p.split('=', 2); | |
| res[kva[0]] = kva[1]; | |
| }); | |
| return res; | |
| }, | |
| parseQemuCpu: function(value) { | |
| if (!value) { | |
| return {}; | |
| } | |
| var res = {}; | |
| var errors = false; | |
| Ext.Array.each(value.split(','), function(p) { | |
| if (!p || p.match(/^\s*$/)) { | |
| return; // continue | |
| } | |
| if (!p.match(/\=/)) { | |
| if (Ext.isDefined(res.cpu)) { | |
| errors = true; | |
| return false; // break | |
| } | |
| res.cputype = p; | |
| return; // continue | |
| } | |
| var match_res = p.match(/^([a-z_]+)=(\S+)$/); | |
| if (!match_res) { | |
| errors = true; | |
| return false; // break | |
| } | |
| var k = match_res[1]; | |
| if (Ext.isDefined(res[k])) { | |
| errors = true; | |
| return false; // break | |
| } | |
| res[k] = match_res[2]; | |
| }); | |
| if (errors || !res.cputype) { | |
| return; | |
| } | |
| return res; | |
| }, | |
| printQemuCpu: function(cpu) { | |
| var cpustr = cpu.cputype; | |
| var optstr = ''; | |
| Ext.Object.each(cpu, function(key, value) { | |
| if (!Ext.isDefined(value) || key === 'cputype') { | |
| return; // continue | |
| } | |
| optstr += ',' + key + '=' + value; | |
| }); | |
| if (!cpustr) { | |
| if (optstr) { | |
| return 'kvm64' + optstr; | |
| } | |
| return; | |
| } | |
| return cpustr + optstr; | |
| } | |
| }}); | |
| /* 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: { | |
| monitor: 49, | |
| 'ha-fencing': 48, | |
| 'ha-groups': 47, | |
| 'ha-resources': 46, | |
| 'ceph-log': 45, | |
| 'ceph-crushmap':44, | |
| 'ceph-pools': 43, | |
| 'ceph-osdtree': 42, | |
| 'ceph-disklist': 41, | |
| 'ceph-monlist': 40, | |
| 'ceph-config': 39, | |
| ceph: 38, | |
| 'firewall-fwlog': 37, | |
| 'firewall-options': 36, | |
| 'firewall-ipset': 35, | |
| 'firewall-aliases': 34, | |
| 'firewall-sg': 33, | |
| firewall: 32, | |
| apt: 31, | |
| members: 30, | |
| snapshot: 29, | |
| ha: 28, | |
| support: 27, | |
| pools: 26, | |
| syslog: 25, | |
| ubc: 24, | |
| initlog: 23, | |
| openvz: 22, | |
| backup: 21, | |
| resources: 20, | |
| content: 19, | |
| root: 18, | |
| domains: 17, | |
| roles: 16, | |
| groups: 15, | |
| users: 14, | |
| time: 13, | |
| dns: 12, | |
| network: 11, | |
| services: 10, | |
| options: 9, | |
| console: 8, | |
| hardware: 7, | |
| permissions: 6, | |
| summary: 5, | |
| tasks: 4, | |
| clog: 3, | |
| storage: 2, | |
| folder: 1, | |
| server: 0 | |
| }, | |
| decodeHToken: function(token) { | |
| var me = this; | |
| var state = {}; | |
| if (!token) { | |
| Ext.Array.each(me.hslist, function(rec) { | |
| state[rec[0]] = rec[1]; | |
| }); | |
| return state; | |
| } | |
| // return Ext.urlDecode(token); | |
| var items = token.split(':'); | |
| var prefix = items.shift(); | |
| if (prefix != me.hprefix) { | |
| return me.decodeHToken(); | |
| } | |
| Ext.Array.each(me.hslist, function(rec) { | |
| var value = items.shift(); | |
| if (value) { | |
| if (value[0] === '=') { | |
| value = decodeURIComponent(value.slice(1)); | |
| } else { | |
| Ext.Object.each(me.compDict, function(key, cv) { | |
| if (value == cv) { | |
| value = key; | |
| return false; | |
| } | |
| }); | |
| } | |
| } | |
| state[rec[0]] = value; | |
| }); | |
| return state; | |
| }, | |
| encodeHToken: function(state) { | |
| var me = this; | |
| // return Ext.urlEncode(state); | |
| var ctoken = me.hprefix; | |
| Ext.Array.each(me.hslist, function(rec) { | |
| var value = state[rec[0]]; | |
| if (!Ext.isDefined(value)) { | |
| value = rec[1]; | |
| } | |
| value = encodeURIComponent(value); | |
| if (!value) { | |
| ctoken += ':'; | |
| } else { | |
| var comp = me.compDict[value]; | |
| if (Ext.isDefined(comp)) { | |
| ctoken += ":" + comp; | |
| } else { | |
| ctoken += ":=" + value; | |
| } | |
| } | |
| }); | |
| return ctoken; | |
| }, | |
| constructor: function(config){ | |
| var me = this; | |
| me.callParent([config]); | |
| me.UIState = me.decodeHToken(); // set default | |
| var history_change_cb = function(token) { | |
| //console.log("HC " + token); | |
| if (!token) { | |
| var res = window.confirm(gettext('Are you sure you want to navigate away from this page?')); | |
| if (res){ | |
| // process text value and close... | |
| Ext.History.back(); | |
| } else { | |
| Ext.History.forward(); | |
| } | |
| return; | |
| } | |
| var newstate = me.decodeHToken(token); | |
| Ext.Array.each(me.hslist, function(rec) { | |
| if (typeof newstate[rec[0]] == "undefined") { | |
| return; | |
| } | |
| me.setHV(rec[0], newstate[rec[0]], true); | |
| }); | |
| }; | |
| var start_token = Ext.History.getToken(); | |
| if (start_token) { | |
| history_change_cb(start_token); | |
| } else { | |
| var htext = me.encodeHToken(me.UIState); | |
| Ext.History.add(htext); | |
| } | |
| Ext.History.on('change', history_change_cb); | |
| }, | |
| get: function(name, defaultValue){ | |
| /*jslint confusion: true */ | |
| var me = this; | |
| var data; | |
| if (typeof me.UIState[name] != "undefined") { | |
| data = { value: me.UIState[name] }; | |
| } else { | |
| data = me.callParent(arguments); | |
| if (!data && name === 'GuiCap') { | |
| data = { vms: {}, storage: {}, access: {}, nodes: {}, dc: {} }; | |
| } | |
| } | |
| //console.log("GET " + name + " " + Ext.encode(data)); | |
| return data; | |
| }, | |
| clear: function(name){ | |
| var me = this; | |
| if (typeof me.UIState[name] != "undefined") { | |
| me.UIState[name] = null; | |
| } | |
| me.callParent(arguments); | |
| }, | |
| set: function(name, value){ | |
| var me = this; | |
| //console.log("SET " + name + " " + Ext.encode(value)); | |
| if (typeof me.UIState[name] != "undefined") { | |
| var newvalue = value ? value.value : null; | |
| if (me.setHV(name, newvalue, false)) { | |
| var htext = me.encodeHToken(me.UIState); | |
| Ext.History.add(htext); | |
| } | |
| } else { | |
| me.callParent(arguments); | |
| } | |
| } | |
| }); | |
| /* Button features: | |
| * - observe selection changes to enable/disable the button using enableFn() | |
| * - pop up confirmation dialog using confirmMsg() | |
| */ | |
| Ext.define('PVE.button.Button', { | |
| extend: 'Ext.button.Button', | |
| alias: 'widget.pveButton', | |
| // 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, | |
| initComponent: function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (me.handler) { | |
| me.realHandler = me.handler; | |
| me.handler = function(button, event) { | |
| var rec, msg; | |
| if (me.selModel) { | |
| rec = me.selModel.getSelection()[0]; | |
| if (!rec || (me.enableFn(rec) === false)) { | |
| return; | |
| } | |
| } | |
| if (me.confirmMsg) { | |
| msg = me.confirmMsg; | |
| if (Ext.isFunction(me.confirmMsg)) { | |
| msg = me.confirmMsg(rec); | |
| } | |
| Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; | |
| Ext.Msg.show({ | |
| title: gettext('Confirm'), | |
| icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, | |
| msg: msg, | |
| buttons: Ext.Msg.YESNO, | |
| callback: function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| me.realHandler(button, event, rec); | |
| } | |
| }); | |
| } else { | |
| me.realHandler(button, event, rec); | |
| } | |
| }; | |
| } | |
| 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.button.ConsoleButton', { | |
| extend: 'Ext.button.Split', | |
| alias: 'widget.pveConsoleButton', | |
| consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade' | |
| consoleName: undefined, | |
| enableSpice: true, | |
| nodename: undefined, | |
| vmid: 0, | |
| setEnableSpice: function(enable){ | |
| var me = this; | |
| me.enableSpice = enable; | |
| me.spiceMenu.setDisabled(!enable); | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| me.spiceMenu = Ext.create('Ext.menu.Item', { | |
| text: 'SPICE', | |
| iconCls: 'pve-itype-icon-virt-viewer', | |
| handler: function() { | |
| PVE.Utils.openConsoleWindow('vv', me.consoleType, me.vmid, me.nodename, me.consoleName); | |
| } | |
| }); | |
| var noVncMenu = Ext.create('Ext.menu.Item', { | |
| text: 'noVNC', | |
| iconCls: 'pve-itype-icon-novnc', | |
| handler: function() { | |
| PVE.Utils.openConsoleWindow('html5', me.consoleType, me.vmid, me.nodename, me.consoleName); | |
| } | |
| }); | |
| if (me.text === null) { | |
| me.text = gettext('Console'); | |
| } | |
| Ext.apply(me, { | |
| handler: function() { | |
| PVE.Utils.openDefaultConsoleWindow(me.enableSpice, me.consoleType, me.vmid, | |
| me.nodename, me.consoleName); | |
| }, | |
| menu: new Ext.menu.Menu({ | |
| items: [ noVncMenu, me.spiceMenu ] | |
| }) | |
| }); | |
| 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); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| /* help button pointing to an online documentation | |
| for components contained in a modal window | |
| */ | |
| Ext.define('PVE.button.Help', { | |
| extend: 'Ext.button.Button', | |
| alias: 'widget.pveHelpButton', | |
| text: gettext('Help'), | |
| // make help button less flashy by styling it like toolbar buttons | |
| iconCls: ' x-btn-icon-el-default-toolbar-small fa fa-question-circle', | |
| cls: 'x-btn-default-toolbar-small pve-help-button', | |
| hidden: true, | |
| controller: { | |
| xclass: 'Ext.app.ViewController', | |
| listen: { | |
| global: { | |
| pveShowHelp: 'onPveShowHelp', | |
| pveHideHelp: 'onPveHideHelp' | |
| } | |
| }, | |
| onPveShowHelp: function(helpLink) { | |
| this.getView().setHandler(function() { | |
| var docsURI = window.location.origin + | |
| '/pve-docs/' + helpLink; | |
| window.open(docsURI); | |
| }); | |
| this.getView().show(); | |
| }, | |
| onPveHideHelp: function() { | |
| this.getView().hide(); | |
| } | |
| } | |
| });Ext.define('PVE.qemu.SendKeyMenu', { | |
| extend: 'Ext.button.Button', | |
| alias: ['widget.pveQemuSendKeyMenu'], | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| if (!me.vmid) { | |
| throw "no VM ID specified"; | |
| } | |
| var sendKey = function(key) { | |
| PVE.Utils.API2Request({ | |
| params: { key: key }, | |
| url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/sendkey", | |
| method: 'PUT', | |
| waitMsgTarget: me, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| } | |
| }); | |
| }; | |
| Ext.apply(me, { | |
| text: 'SendKey', | |
| menu: new Ext.menu.Menu({ | |
| height: 200, | |
| items: [ | |
| { | |
| text: 'Tab', handler: function() { | |
| sendKey('tab'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-Delete', handler: function() { | |
| sendKey('ctrl-alt-delete'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-Backspace', handler: function() { | |
| sendKey('ctrl-alt-backspace'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F1', handler: function() { | |
| sendKey('ctrl-alt-f1'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F2', handler: function() { | |
| sendKey('ctrl-alt-f2'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F3', handler: function() { | |
| sendKey('ctrl-alt-f3'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F4', handler: function() { | |
| sendKey('ctrl-alt-f4'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F5', handler: function() { | |
| sendKey('ctrl-alt-f5'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F6', handler: function() { | |
| sendKey('ctrl-alt-f6'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F7', handler: function() { | |
| sendKey('ctrl-alt-f7'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F8', handler: function() { | |
| sendKey('ctrl-alt-f8'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F9', handler: function() { | |
| sendKey('ctrl-alt-f9'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F10', handler: function() { | |
| sendKey('ctrl-alt-f10'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F11', handler: function() { | |
| sendKey('ctrl-alt-f11'); | |
| } | |
| }, | |
| { | |
| text: 'Ctrl-Alt-F12', handler: function() { | |
| sendKey('ctrl-alt-f12'); | |
| } | |
| } | |
| ] | |
| }) | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| 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) { | |
| PVE.Utils.API2Request({ | |
| params: params, | |
| url: '/nodes/' + nodename + '/qemu/' + vmid + "/status/" + cmd, | |
| method: 'POST', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| } | |
| }); | |
| }; | |
| var running = false; | |
| var stopped = true; | |
| var suspended = false; | |
| switch (me.pveSelNode.data.status) { | |
| case 'running': | |
| running = true; | |
| stopped = false; | |
| 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', | |
| disabled: running || suspended, | |
| handler: function() { | |
| vm_command('start'); | |
| } | |
| }, | |
| { | |
| text: gettext('Suspend'), | |
| iconCls: 'fa fa-fw fa-pause', | |
| hidden: suspended, | |
| disabled: stopped || suspended, | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('qmsuspend', 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 = PVE.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, | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('qmstop', vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| vm_command("stop"); | |
| }); | |
| } | |
| }, | |
| { xtype: 'menuseparator' }, | |
| { | |
| text: gettext('Migrate'), | |
| iconCls: 'fa fa-fw fa-send-o', | |
| 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', | |
| handler: function() { | |
| var win = Ext.create('PVE.window.Clone', { | |
| nodename: nodename, | |
| vmid: vmid | |
| }); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: gettext('Convert to template'), | |
| iconCls: 'fa fa-fw fa-file-o', | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('qmtemplate', vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| PVE.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() { | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| }, | |
| success: function(response, opts) { | |
| var allowSpice = response.result.data.spice; | |
| PVE.Utils.openDefaultConsoleWindow(allowSpice, 'kvm', vmid, nodename, vmname); | |
| } | |
| }); | |
| } | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.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 vmname = me.pveSelNode.data.name; | |
| var template = me.pveSelNode.data.template; | |
| var vm_command = function(cmd, params) { | |
| PVE.Utils.API2Request({ | |
| params: params, | |
| url: '/nodes/' + nodename + '/qemu/' + vmid + "/status/" + cmd, | |
| method: 'POST', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| }; | |
| me.title = "VM " + vmid; | |
| me.items = [ | |
| { | |
| text: gettext('Migrate'), | |
| iconCls: 'fa fa-fw fa-send-o', | |
| 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', | |
| handler: function() { | |
| var win = Ext.create('PVE.window.Clone', { | |
| nodename: nodename, | |
| vmid: vmid, | |
| isTemplate: template | |
| }); | |
| win.show(); | |
| } | |
| } | |
| ]; | |
| 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) { | |
| PVE.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 running = false; | |
| var stopped = true; | |
| var suspended = false; | |
| 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 = PVE.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 = PVE.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, | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('vzstop', vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| vm_command("stop"); | |
| }); | |
| } | |
| }, | |
| { xtype: 'menuseparator' }, | |
| { | |
| text: gettext('Migrate'), | |
| iconCls: 'fa fa-fw fa-send-o', | |
| handler: function() { | |
| var win = Ext.create('PVE.window.Migrate', { | |
| vmtype: 'lxc', | |
| nodename: nodename, | |
| vmid: vmid | |
| }); | |
| win.show(); | |
| } | |
| }, | |
| // { | |
| // text: gettext('Convert to template'), | |
| // icon: '/pve2/images/forward.png', | |
| // handler: function() { | |
| // var msg = PVE.Utils.format_task_description('vztemplate', vmid); | |
| // Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| // if (btn !== 'yes') { | |
| // return; | |
| // } | |
| // | |
| // PVE.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.noVncConsole', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pveNoVncConsole', | |
| nodename: undefined, | |
| vmid: undefined, | |
| consoleType: undefined, // lxc or kvm | |
| layout: 'fit', | |
| border: false, | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| if (!me.consoleType) { | |
| throw "no console type specified"; | |
| } | |
| if (!me.vmid && me.consoleType !== 'shell') { | |
| 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" }); | |
| Ext.apply(me, { | |
| items: box, | |
| listeners: { | |
| activate: function() { | |
| var url = '/?console=' + me.consoleType + '&novnc=1&node=' + me.nodename + '&resize=scale'; | |
| if (me.vmid) { | |
| url += '&vmid='+ me.vmid; | |
| } | |
| box.load(url); | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.VNCConsole', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pveVNCConsole', | |
| last_novnc_state: undefined, | |
| last_novnc_msg: undefined, | |
| layout: 'fit', | |
| border: false, | |
| detectMigratedVM: function() { | |
| var me = this; | |
| if (!me.vmid) { | |
| return; | |
| } | |
| // try to detect migrated VM | |
| PVE.Utils.API2Request({ | |
| url: '/cluster/resources', | |
| method: 'GET', | |
| success: function(response) { | |
| var list = response.result.data; | |
| Ext.Array.each(list, function(item) { | |
| if (item.type === 'qemu' && item.vmid == me.vmid) { | |
| if (item.node !== me.nodename) { | |
| me.nodename = item.node; | |
| me.url = "/nodes/" + me.nodename + "/" + item.type + "/" + me.vmid + "/vncproxy"; | |
| me.wsurl = "/nodes/" + me.nodename + "/" + item.type + "/" + me.vmid + "/vncwebsocket"; | |
| me.reloadApplet(); | |
| } | |
| return false; // break | |
| } | |
| }); | |
| } | |
| }); | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.url) { | |
| throw "no url specified"; | |
| } | |
| var myid = me.id + "-vncapp"; | |
| me.appletID = myid; | |
| var box; | |
| if (!me.wsurl) { | |
| throw "no web socket url specified"; | |
| } | |
| box = Ext.create('Ext.ux.IFrame', { id: myid }); | |
| var resize_window = function() { | |
| //console.log("resize"); | |
| var aw; | |
| var ah; | |
| var novnciframe = box.getFrame(); | |
| // noVNC_canvas | |
| var innerDoc = novnciframe.contentDocument || novnciframe.contentWindow.document; | |
| aw = innerDoc.getElementById('noVNC_canvas').width; | |
| ah = innerDoc.getElementById('noVNC_canvas').height + 8; | |
| var novnc_state = innerDoc.getElementById('noVNC_status_state').innerHTML; | |
| var novnc_msg = innerDoc.getElementById('noVNC_status_msg').innerHTML; | |
| if (novnc_state !== me.last_novnc_state || novnc_msg !== me.last_novnc_msg) { | |
| me.last_novnc_state = novnc_state; | |
| me.last_novnc_msg = novnc_msg; | |
| if (novnc_state !== 'normal') { | |
| PVE.Utils.setErrorMask(box, novnc_msg || 'unknown'); | |
| } else { | |
| PVE.Utils.setErrorMask(box); // clear mask | |
| } | |
| if (novnc_state === 'disconnected') { | |
| me.detectMigratedVM(); | |
| } | |
| } | |
| if (aw < 640) { aw = 640; } | |
| if (ah < 400) { ah = 400; } | |
| var tbar = me.getDockedItems("[dock=top]")[0]; | |
| var tbh = tbar ? tbar.getHeight() : 0; | |
| var oh; | |
| var ow; | |
| //console.log("size0 " + aw + " " + ah + " tbh " + tbh); | |
| if (window.innerHeight) { | |
| oh = window.innerHeight; | |
| ow = window.innerWidth; | |
| } else if (document.documentElement && | |
| document.documentElement.clientHeight) { | |
| oh = document.documentElement.clientHeight; | |
| ow = document.documentElement.clientWidth; | |
| } else if (document.body) { | |
| oh = document.body.clientHeight; | |
| ow = document.body.clientWidth; | |
| } else { | |
| throw "can't get window size"; | |
| } | |
| var offsetw = aw - ow; | |
| var offseth = ah + tbh - oh; | |
| if (offsetw !== 0 || offseth !== 0) { | |
| //console.log("try resize by " + offsetw + " " + offseth); | |
| try { window.resizeBy(offsetw, offseth); } catch (e) {} | |
| } | |
| Ext.Function.defer(resize_window, 1000); | |
| }; | |
| var start_vnc_viewer = function(param) { | |
| var pveparams = Ext.urlEncode({ | |
| port: param.port, | |
| vncticket: param.ticket | |
| }); | |
| var urlparams = Ext.urlEncode({ | |
| encrypt: 1, | |
| path: "api2/json" + me.wsurl + "?" + pveparams, | |
| password: param.ticket | |
| }); | |
| box.load('/novnc/vnc_pve.html?' + urlparams); | |
| Ext.Function.defer(resize_window, 1000); | |
| }; | |
| Ext.apply(me, { | |
| scrollable: me.toplevel ? false : true, | |
| items: box, | |
| reloadApplet: function() { | |
| var params = Ext.apply({}, me.params); | |
| params.websocket = 1; | |
| PVE.Utils.API2Request({ | |
| url: me.url, | |
| params: params, | |
| method: me.method || 'POST', | |
| failure: function(response, opts) { | |
| box.update(gettext('Error') + ' ' + response.htmlStatus); | |
| }, | |
| success: function(response, opts) { | |
| start_vnc_viewer(response.result.data); | |
| } | |
| }); | |
| } | |
| }); | |
| me.callParent(); | |
| if (me.toplevel) { | |
| me.on("render", me.reloadApplet); | |
| } else { | |
| me.on("activate", me.reloadApplet); | |
| me.on("hide", function() { box.update(""); }); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.KVMConsole', { | |
| extend: 'PVE.VNCConsole', | |
| alias: 'widget.pveKVMConsole', | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| if (!me.vmid) { | |
| throw "no VM ID specified"; | |
| } | |
| var baseUrl = "/nodes/" + me.nodename + "/qemu/" + me.vmid; | |
| var vm_command = function(cmd, params, reload_applet) { | |
| PVE.Utils.API2Request({ | |
| params: params, | |
| url: baseUrl + "/status/" + cmd, | |
| method: 'POST', | |
| waitMsgTarget: me, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| }, | |
| success: function() { | |
| if (reload_applet) { | |
| Ext.Function.defer(me.reloadApplet, 1000, me); | |
| } | |
| } | |
| }); | |
| }; | |
| var tbar = [ | |
| { | |
| text: gettext('Start'), | |
| handler: function() { | |
| vm_command("start", {}, 1); | |
| } | |
| }, | |
| { | |
| text: gettext('Shutdown'), | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('qmshutdown', me.vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| vm_command('shutdown'); | |
| }); | |
| } | |
| }, | |
| { | |
| text: gettext('Stop'), | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('qmstop', me.vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| vm_command("stop"); | |
| }); | |
| } | |
| }, | |
| { | |
| xtype: 'pveQemuSendKeyMenu', | |
| nodename: me.nodename, | |
| vmid: me.vmid | |
| }, | |
| { | |
| text: gettext('Reset'), | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('qmreset', me.vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| vm_command("reset"); | |
| }); | |
| } | |
| }, | |
| { | |
| text: gettext('Suspend'), | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('qmsuspend', me.vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| vm_command("suspend"); | |
| }); | |
| } | |
| }, | |
| { | |
| text: gettext('Resume'), | |
| handler: function() { | |
| vm_command("resume"); | |
| } | |
| }, | |
| // Note: no migrate here, because we can't display migrate log | |
| { | |
| text: gettext('Console'), | |
| handler: function() { | |
| PVE.Utils.openVNCViewer('kvm', me.vmid, me.nodename, me.vmname); | |
| } | |
| }, | |
| '->', | |
| { | |
| text: gettext('Reload'), | |
| handler: function () { | |
| me.reloadApplet(); | |
| } | |
| } | |
| ]; | |
| Ext.apply(me, { | |
| tbar: tbar, | |
| url: baseUrl + "/vncproxy", | |
| wsurl: baseUrl + "/vncwebsocket" | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.LxcConsole', { | |
| extend: 'PVE.VNCConsole', | |
| alias: 'widget.pveLxcConsole', | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| if (!me.vmid) { | |
| throw "no VM ID specified"; | |
| } | |
| var baseUrl = "/nodes/" + me.nodename + "/lxc/" + me.vmid; | |
| var vm_command = function(cmd, params, reload_applet) { | |
| PVE.Utils.API2Request({ | |
| params: params, | |
| url: baseUrl + "/status/" + cmd, | |
| waitMsgTarget: me, | |
| method: 'POST', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| }, | |
| success: function() { | |
| if (reload_applet) { | |
| Ext.Function.defer(me.reloadApplet, 1000, me); | |
| } | |
| } | |
| }); | |
| }; | |
| var tbar = [ | |
| { | |
| text: gettext('Start'), | |
| handler: function() { | |
| vm_command("start"); | |
| } | |
| }, | |
| { | |
| text: gettext('Shutdown'), | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('vzshutdown', me.vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| vm_command("shutdown"); | |
| }); | |
| } | |
| }, | |
| { | |
| text: gettext('Stop'), | |
| handler: function() { | |
| var msg = PVE.Utils.format_task_description('vzstop', me.vmid); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| vm_command("stop"); | |
| }); | |
| } | |
| }, | |
| // Note: no migrate here, because we can't display migrate log | |
| '->', | |
| { | |
| text: gettext('Reload'), | |
| handler: function () { | |
| me.reloadApplet(); | |
| } | |
| } | |
| ]; | |
| Ext.apply(me, { | |
| tbar: tbar, | |
| url: baseUrl + "/vncproxy", | |
| wsurl: baseUrl + "/vncwebsocket" | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.Shell', { | |
| extend: 'PVE.VNCConsole', | |
| alias: 'widget.pveShell', | |
| ugradeSystem: false, // set to true to run "apt-get dist-upgrade" | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| var tbar = [ '->' ]; | |
| if (!me.ugradeSystem) { | |
| // we dont want to restart the upgrade script | |
| tbar.push({ | |
| text: gettext('Reload'), | |
| handler: function () { me.reloadApplet(); } | |
| }); | |
| } | |
| tbar.push({ | |
| text: gettext('Shell'), | |
| handler: function() { | |
| PVE.Utils.openVNCViewer('shell', undefined, me.nodename, undefined); | |
| } | |
| }); | |
| var baseUrl = "/nodes/" + me.nodename; | |
| Ext.apply(me, { | |
| tbar: tbar, | |
| url: baseUrl + "/vncshell", | |
| wsurl: baseUrl + "/vncwebsocket" | |
| }); | |
| if (me.ugradeSystem) { | |
| me.params = { upgrade: 1 }; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('Timezone', { | |
| extend: 'Ext.data.Model', | |
| fields: ['zone'] | |
| }); | |
| Ext.define('PVE.data.TimezoneStore', { | |
| extend: 'Ext.data.Store', | |
| model: 'Timezone', | |
| data: [ | |
| ['Africa/Abidjan'], | |
| ['Africa/Accra'], | |
| ['Africa/Addis_Ababa'], | |
| ['Africa/Algiers'], | |
| ['Africa/Asmara'], | |
| ['Africa/Bamako'], | |
| ['Africa/Bangui'], | |
| ['Africa/Banjul'], | |
| ['Africa/Bissau'], | |
| ['Africa/Blantyre'], | |
| ['Africa/Brazzaville'], | |
| ['Africa/Bujumbura'], | |
| ['Africa/Cairo'], | |
| ['Africa/Casablanca'], | |
| ['Africa/Ceuta'], | |
| ['Africa/Conakry'], | |
| ['Africa/Dakar'], | |
| ['Africa/Dar_es_Salaam'], | |
| ['Africa/Djibouti'], | |
| ['Africa/Douala'], | |
| ['Africa/El_Aaiun'], | |
| ['Africa/Freetown'], | |
| ['Africa/Gaborone'], | |
| ['Africa/Harare'], | |
| ['Africa/Johannesburg'], | |
| ['Africa/Kampala'], | |
| ['Africa/Khartoum'], | |
| ['Africa/Kigali'], | |
| ['Africa/Kinshasa'], | |
| ['Africa/Lagos'], | |
| ['Africa/Libreville'], | |
| ['Africa/Lome'], | |
| ['Africa/Luanda'], | |
| ['Africa/Lubumbashi'], | |
| ['Africa/Lusaka'], | |
| ['Africa/Malabo'], | |
| ['Africa/Maputo'], | |
| ['Africa/Maseru'], | |
| ['Africa/Mbabane'], | |
| ['Africa/Mogadishu'], | |
| ['Africa/Monrovia'], | |
| ['Africa/Nairobi'], | |
| ['Africa/Ndjamena'], | |
| ['Africa/Niamey'], | |
| ['Africa/Nouakchott'], | |
| ['Africa/Ouagadougou'], | |
| ['Africa/Porto-Novo'], | |
| ['Africa/Sao_Tome'], | |
| ['Africa/Tripoli'], | |
| ['Africa/Tunis'], | |
| ['Africa/Windhoek'], | |
| ['America/Adak'], | |
| ['America/Anchorage'], | |
| ['America/Anguilla'], | |
| ['America/Antigua'], | |
| ['America/Araguaina'], | |
| ['America/Argentina/Buenos_Aires'], | |
| ['America/Argentina/Catamarca'], | |
| ['America/Argentina/Cordoba'], | |
| ['America/Argentina/Jujuy'], | |
| ['America/Argentina/La_Rioja'], | |
| ['America/Argentina/Mendoza'], | |
| ['America/Argentina/Rio_Gallegos'], | |
| ['America/Argentina/Salta'], | |
| ['America/Argentina/San_Juan'], | |
| ['America/Argentina/San_Luis'], | |
| ['America/Argentina/Tucuman'], | |
| ['America/Argentina/Ushuaia'], | |
| ['America/Aruba'], | |
| ['America/Asuncion'], | |
| ['America/Atikokan'], | |
| ['America/Bahia'], | |
| ['America/Bahia_Banderas'], | |
| ['America/Barbados'], | |
| ['America/Belem'], | |
| ['America/Belize'], | |
| ['America/Blanc-Sablon'], | |
| ['America/Boa_Vista'], | |
| ['America/Bogota'], | |
| ['America/Boise'], | |
| ['America/Cambridge_Bay'], | |
| ['America/Campo_Grande'], | |
| ['America/Cancun'], | |
| ['America/Caracas'], | |
| ['America/Cayenne'], | |
| ['America/Cayman'], | |
| ['America/Chicago'], | |
| ['America/Chihuahua'], | |
| ['America/Costa_Rica'], | |
| ['America/Cuiaba'], | |
| ['America/Curacao'], | |
| ['America/Danmarkshavn'], | |
| ['America/Dawson'], | |
| ['America/Dawson_Creek'], | |
| ['America/Denver'], | |
| ['America/Detroit'], | |
| ['America/Dominica'], | |
| ['America/Edmonton'], | |
| ['America/Eirunepe'], | |
| ['America/El_Salvador'], | |
| ['America/Fortaleza'], | |
| ['America/Glace_Bay'], | |
| ['America/Godthab'], | |
| ['America/Goose_Bay'], | |
| ['America/Grand_Turk'], | |
| ['America/Grenada'], | |
| ['America/Guadeloupe'], | |
| ['America/Guatemala'], | |
| ['America/Guayaquil'], | |
| ['America/Guyana'], | |
| ['America/Halifax'], | |
| ['America/Havana'], | |
| ['America/Hermosillo'], | |
| ['America/Indiana/Indianapolis'], | |
| ['America/Indiana/Knox'], | |
| ['America/Indiana/Marengo'], | |
| ['America/Indiana/Petersburg'], | |
| ['America/Indiana/Tell_City'], | |
| ['America/Indiana/Vevay'], | |
| ['America/Indiana/Vincennes'], | |
| ['America/Indiana/Winamac'], | |
| ['America/Inuvik'], | |
| ['America/Iqaluit'], | |
| ['America/Jamaica'], | |
| ['America/Juneau'], | |
| ['America/Kentucky/Louisville'], | |
| ['America/Kentucky/Monticello'], | |
| ['America/La_Paz'], | |
| ['America/Lima'], | |
| ['America/Los_Angeles'], | |
| ['America/Maceio'], | |
| ['America/Managua'], | |
| ['America/Manaus'], | |
| ['America/Marigot'], | |
| ['America/Martinique'], | |
| ['America/Matamoros'], | |
| ['America/Mazatlan'], | |
| ['America/Menominee'], | |
| ['America/Merida'], | |
| ['America/Mexico_City'], | |
| ['America/Miquelon'], | |
| ['America/Moncton'], | |
| ['America/Monterrey'], | |
| ['America/Montevideo'], | |
| ['America/Montreal'], | |
| ['America/Montserrat'], | |
| ['America/Nassau'], | |
| ['America/New_York'], | |
| ['America/Nipigon'], | |
| ['America/Nome'], | |
| ['America/Noronha'], | |
| ['America/North_Dakota/Center'], | |
| ['America/North_Dakota/New_Salem'], | |
| ['America/Ojinaga'], | |
| ['America/Panama'], | |
| ['America/Pangnirtung'], | |
| ['America/Paramaribo'], | |
| ['America/Phoenix'], | |
| ['America/Port-au-Prince'], | |
| ['America/Port_of_Spain'], | |
| ['America/Porto_Velho'], | |
| ['America/Puerto_Rico'], | |
| ['America/Rainy_River'], | |
| ['America/Rankin_Inlet'], | |
| ['America/Recife'], | |
| ['America/Regina'], | |
| ['America/Resolute'], | |
| ['America/Rio_Branco'], | |
| ['America/Santa_Isabel'], | |
| ['America/Santarem'], | |
| ['America/Santiago'], | |
| ['America/Santo_Domingo'], | |
| ['America/Sao_Paulo'], | |
| ['America/Scoresbysund'], | |
| ['America/Shiprock'], | |
| ['America/St_Barthelemy'], | |
| ['America/St_Johns'], | |
| ['America/St_Kitts'], | |
| ['America/St_Lucia'], | |
| ['America/St_Thomas'], | |
| ['America/St_Vincent'], | |
| ['America/Swift_Current'], | |
| ['America/Tegucigalpa'], | |
| ['America/Thule'], | |
| ['America/Thunder_Bay'], | |
| ['America/Tijuana'], | |
| ['America/Toronto'], | |
| ['America/Tortola'], | |
| ['America/Vancouver'], | |
| ['America/Whitehorse'], | |
| ['America/Winnipeg'], | |
| ['America/Yakutat'], | |
| ['America/Yellowknife'], | |
| ['Antarctica/Casey'], | |
| ['Antarctica/Davis'], | |
| ['Antarctica/DumontDUrville'], | |
| ['Antarctica/Macquarie'], | |
| ['Antarctica/Mawson'], | |
| ['Antarctica/McMurdo'], | |
| ['Antarctica/Palmer'], | |
| ['Antarctica/Rothera'], | |
| ['Antarctica/South_Pole'], | |
| ['Antarctica/Syowa'], | |
| ['Antarctica/Vostok'], | |
| ['Arctic/Longyearbyen'], | |
| ['Asia/Aden'], | |
| ['Asia/Almaty'], | |
| ['Asia/Amman'], | |
| ['Asia/Anadyr'], | |
| ['Asia/Aqtau'], | |
| ['Asia/Aqtobe'], | |
| ['Asia/Ashgabat'], | |
| ['Asia/Baghdad'], | |
| ['Asia/Bahrain'], | |
| ['Asia/Baku'], | |
| ['Asia/Bangkok'], | |
| ['Asia/Beirut'], | |
| ['Asia/Bishkek'], | |
| ['Asia/Brunei'], | |
| ['Asia/Choibalsan'], | |
| ['Asia/Chongqing'], | |
| ['Asia/Colombo'], | |
| ['Asia/Damascus'], | |
| ['Asia/Dhaka'], | |
| ['Asia/Dili'], | |
| ['Asia/Dubai'], | |
| ['Asia/Dushanbe'], | |
| ['Asia/Gaza'], | |
| ['Asia/Harbin'], | |
| ['Asia/Ho_Chi_Minh'], | |
| ['Asia/Hong_Kong'], | |
| ['Asia/Hovd'], | |
| ['Asia/Irkutsk'], | |
| ['Asia/Jakarta'], | |
| ['Asia/Jayapura'], | |
| ['Asia/Jerusalem'], | |
| ['Asia/Kabul'], | |
| ['Asia/Kamchatka'], | |
| ['Asia/Karachi'], | |
| ['Asia/Kashgar'], | |
| ['Asia/Kathmandu'], | |
| ['Asia/Kolkata'], | |
| ['Asia/Krasnoyarsk'], | |
| ['Asia/Kuala_Lumpur'], | |
| ['Asia/Kuching'], | |
| ['Asia/Kuwait'], | |
| ['Asia/Macau'], | |
| ['Asia/Magadan'], | |
| ['Asia/Makassar'], | |
| ['Asia/Manila'], | |
| ['Asia/Muscat'], | |
| ['Asia/Nicosia'], | |
| ['Asia/Novokuznetsk'], | |
| ['Asia/Novosibirsk'], | |
| ['Asia/Omsk'], | |
| ['Asia/Oral'], | |
| ['Asia/Phnom_Penh'], | |
| ['Asia/Pontianak'], | |
| ['Asia/Pyongyang'], | |
| ['Asia/Qatar'], | |
| ['Asia/Qyzylorda'], | |
| ['Asia/Rangoon'], | |
| ['Asia/Riyadh'], | |
| ['Asia/Sakhalin'], | |
| ['Asia/Samarkand'], | |
| ['Asia/Seoul'], | |
| ['Asia/Shanghai'], | |
| ['Asia/Singapore'], | |
| ['Asia/Taipei'], | |
| ['Asia/Tashkent'], | |
| ['Asia/Tbilisi'], | |
| ['Asia/Tehran'], | |
| ['Asia/Thimphu'], | |
| ['Asia/Tokyo'], | |
| ['Asia/Ulaanbaatar'], | |
| ['Asia/Urumqi'], | |
| ['Asia/Vientiane'], | |
| ['Asia/Vladivostok'], | |
| ['Asia/Yakutsk'], | |
| ['Asia/Yekaterinburg'], | |
| ['Asia/Yerevan'], | |
| ['Atlantic/Azores'], | |
| ['Atlantic/Bermuda'], | |
| ['Atlantic/Canary'], | |
| ['Atlantic/Cape_Verde'], | |
| ['Atlantic/Faroe'], | |
| ['Atlantic/Madeira'], | |
| ['Atlantic/Reykjavik'], | |
| ['Atlantic/South_Georgia'], | |
| ['Atlantic/St_Helena'], | |
| ['Atlantic/Stanley'], | |
| ['Australia/Adelaide'], | |
| ['Australia/Brisbane'], | |
| ['Australia/Broken_Hill'], | |
| ['Australia/Currie'], | |
| ['Australia/Darwin'], | |
| ['Australia/Eucla'], | |
| ['Australia/Hobart'], | |
| ['Australia/Lindeman'], | |
| ['Australia/Lord_Howe'], | |
| ['Australia/Melbourne'], | |
| ['Australia/Perth'], | |
| ['Australia/Sydney'], | |
| ['Europe/Amsterdam'], | |
| ['Europe/Andorra'], | |
| ['Europe/Athens'], | |
| ['Europe/Belgrade'], | |
| ['Europe/Berlin'], | |
| ['Europe/Bratislava'], | |
| ['Europe/Brussels'], | |
| ['Europe/Bucharest'], | |
| ['Europe/Budapest'], | |
| ['Europe/Chisinau'], | |
| ['Europe/Copenhagen'], | |
| ['Europe/Dublin'], | |
| ['Europe/Gibraltar'], | |
| ['Europe/Guernsey'], | |
| ['Europe/Helsinki'], | |
| ['Europe/Isle_of_Man'], | |
| ['Europe/Istanbul'], | |
| ['Europe/Jersey'], | |
| ['Europe/Kaliningrad'], | |
| ['Europe/Kiev'], | |
| ['Europe/Lisbon'], | |
| ['Europe/Ljubljana'], | |
| ['Europe/London'], | |
| ['Europe/Luxembourg'], | |
| ['Europe/Madrid'], | |
| ['Europe/Malta'], | |
| ['Europe/Mariehamn'], | |
| ['Europe/Minsk'], | |
| ['Europe/Monaco'], | |
| ['Europe/Moscow'], | |
| ['Europe/Oslo'], | |
| ['Europe/Paris'], | |
| ['Europe/Podgorica'], | |
| ['Europe/Prague'], | |
| ['Europe/Riga'], | |
| ['Europe/Rome'], | |
| ['Europe/Samara'], | |
| ['Europe/San_Marino'], | |
| ['Europe/Sarajevo'], | |
| ['Europe/Simferopol'], | |
| ['Europe/Skopje'], | |
| ['Europe/Sofia'], | |
| ['Europe/Stockholm'], | |
| ['Europe/Tallinn'], | |
| ['Europe/Tirane'], | |
| ['Europe/Uzhgorod'], | |
| ['Europe/Vaduz'], | |
| ['Europe/Vatican'], | |
| ['Europe/Vienna'], | |
| ['Europe/Vilnius'], | |
| ['Europe/Volgograd'], | |
| ['Europe/Warsaw'], | |
| ['Europe/Zagreb'], | |
| ['Europe/Zaporozhye'], | |
| ['Europe/Zurich'], | |
| ['Indian/Antananarivo'], | |
| ['Indian/Chagos'], | |
| ['Indian/Christmas'], | |
| ['Indian/Cocos'], | |
| ['Indian/Comoro'], | |
| ['Indian/Kerguelen'], | |
| ['Indian/Mahe'], | |
| ['Indian/Maldives'], | |
| ['Indian/Mauritius'], | |
| ['Indian/Mayotte'], | |
| ['Indian/Reunion'], | |
| ['Pacific/Apia'], | |
| ['Pacific/Auckland'], | |
| ['Pacific/Chatham'], | |
| ['Pacific/Chuuk'], | |
| ['Pacific/Easter'], | |
| ['Pacific/Efate'], | |
| ['Pacific/Enderbury'], | |
| ['Pacific/Fakaofo'], | |
| ['Pacific/Fiji'], | |
| ['Pacific/Funafuti'], | |
| ['Pacific/Galapagos'], | |
| ['Pacific/Gambier'], | |
| ['Pacific/Guadalcanal'], | |
| ['Pacific/Guam'], | |
| ['Pacific/Honolulu'], | |
| ['Pacific/Johnston'], | |
| ['Pacific/Kiritimati'], | |
| ['Pacific/Kosrae'], | |
| ['Pacific/Kwajalein'], | |
| ['Pacific/Majuro'], | |
| ['Pacific/Marquesas'], | |
| ['Pacific/Midway'], | |
| ['Pacific/Nauru'], | |
| ['Pacific/Niue'], | |
| ['Pacific/Norfolk'], | |
| ['Pacific/Noumea'], | |
| ['Pacific/Pago_Pago'], | |
| ['Pacific/Palau'], | |
| ['Pacific/Pitcairn'], | |
| ['Pacific/Pohnpei'], | |
| ['Pacific/Port_Moresby'], | |
| ['Pacific/Rarotonga'], | |
| ['Pacific/Saipan'], | |
| ['Pacific/Tahiti'], | |
| ['Pacific/Tarawa'], | |
| ['Pacific/Tongatapu'], | |
| ['Pacific/Wake'], | |
| ['Pacific/Wallis'] | |
| ] | |
| }); | |
| /* A reader to store a single JSON Object (hash) into a storage. | |
| * Also accepts an array containing a single hash. | |
| * | |
| * So it can read: | |
| * | |
| * example1: {data1: "xyz", data2: "abc"} | |
| * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}] | |
| * | |
| * example2: [ {data1: "xyz", data2: "abc"} ] | |
| * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}] | |
| * | |
| * If you set 'readArray', the reader expexts the object as array: | |
| * | |
| * example3: [ { key: "data1", value: "xyz", p2: "cde" }, { key: "data2", value: "abc", p2: "efg" }] | |
| * returns [{key: "data1", value: "xyz", p2: "cde}, {key: "data2", value: "abc", p2: "efg"}] | |
| * | |
| * Note: The records can contain additional properties (like 'p2' above) when you use 'readArray' | |
| * | |
| * Additional feature: specify allowed properties with default values with 'rows' object | |
| * | |
| * var rows = { | |
| * memory: { | |
| * required: true, | |
| * defaultValue: 512 | |
| * } | |
| * } | |
| * | |
| */ | |
| Ext.define('PVE.data.reader.JsonObject', { | |
| extend: 'Ext.data.reader.Json', | |
| alias : 'reader.jsonobject', | |
| readArray: false, | |
| rows: undefined, | |
| constructor: function(config) { | |
| var me = this; | |
| Ext.apply(me, config || {}); | |
| me.callParent([config]); | |
| }, | |
| getResponseData: function(response) { | |
| var me = this; | |
| var data = []; | |
| try { | |
| var result = Ext.decode(response.responseText); | |
| // get our data items inside the server response | |
| var root = result[me.getRootProperty()]; | |
| if (me.readArray) { | |
| var rec_hash = {}; | |
| Ext.Array.each(root, function(rec) { | |
| if (Ext.isDefined(rec.key)) { | |
| rec_hash[rec.key] = rec; | |
| } | |
| }); | |
| if (me.rows) { | |
| Ext.Object.each(me.rows, function(key, rowdef) { | |
| var rec = rec_hash[key]; | |
| if (Ext.isDefined(rec)) { | |
| if (!Ext.isDefined(rec.value)) { | |
| rec.value = rowdef.defaultValue; | |
| } | |
| data.push(rec); | |
| } else if (Ext.isDefined(rowdef.defaultValue)) { | |
| data.push({key: key, value: rowdef.defaultValue} ); | |
| } else if (rowdef.required) { | |
| data.push({key: key, value: undefined }); | |
| } | |
| }); | |
| } else { | |
| Ext.Array.each(root, function(rec) { | |
| if (Ext.isDefined(rec.key)) { | |
| data.push(rec); | |
| } | |
| }); | |
| } | |
| } else { | |
| var org_root = root; | |
| if (Ext.isArray(org_root)) { | |
| if (root.length == 1) { | |
| root = org_root[0]; | |
| } else { | |
| root = {}; | |
| } | |
| } | |
| if (me.rows) { | |
| Ext.Object.each(me.rows, function(key, rowdef) { | |
| if (Ext.isDefined(root[key])) { | |
| data.push({key: key, value: root[key]}); | |
| } else if (Ext.isDefined(rowdef.defaultValue)) { | |
| data.push({key: key, value: rowdef.defaultValue}); | |
| } else if (rowdef.required) { | |
| data.push({key: key, value: undefined}); | |
| } | |
| }); | |
| } else { | |
| Ext.Object.each(root, function(key, value) { | |
| data.push({key: key, value: value }); | |
| }); | |
| } | |
| } | |
| } | |
| catch (ex) { | |
| Ext.Error.raise({ | |
| response: response, | |
| json: response.responseText, | |
| parseError: ex, | |
| msg: 'Unable to parse the JSON returned by the server: ' + ex.toString() | |
| }); | |
| } | |
| return data; | |
| } | |
| }); | |
| Ext.define('PVE.RestProxy', { | |
| extend: 'Ext.data.RestProxy', | |
| alias : 'proxy.pve', | |
| pageParam : null, | |
| startParam: null, | |
| limitParam: null, | |
| groupParam: null, | |
| sortParam: null, | |
| filterParam: null, | |
| noCache : false, | |
| afterRequest: function(request, success) { | |
| this.fireEvent('afterload', this, request, success); | |
| return; | |
| }, | |
| constructor: function(config) { | |
| Ext.applyIf(config, { | |
| reader: { | |
| type: 'json', | |
| rootProperty: config.root || 'data' | |
| } | |
| }); | |
| this.callParent([config]); | |
| } | |
| }, function() { | |
| 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) { | |
| var info = record.data; | |
| var text; | |
| if (value) { | |
| return value; | |
| } | |
| // return realm if there is no comment | |
| text = info.comment || info.realm; | |
| if (info.tfa) { | |
| text += " (+ " + info.tfa + ")"; | |
| } | |
| return Ext.String.htmlEncode(text); | |
| } | |
| } | |
| ], | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/access/domains" | |
| } | |
| }); | |
| Ext.define('KeyValue', { | |
| extend: "Ext.data.Model", | |
| fields: [ 'key', 'value' ], | |
| idProperty: 'key' | |
| }); | |
| Ext.define('KeyValuePendingDelete', { | |
| extend: "Ext.data.Model", | |
| fields: [ 'key', 'value', 'pending', 'delete' ], | |
| idProperty: 'key' | |
| }); | |
| Ext.define('pve-string-list', { | |
| extend: 'Ext.data.Model', | |
| fields: [ 'n', 't' ], | |
| idProperty: 'n' | |
| }); | |
| Ext.define('pve-tasks', { | |
| extend: 'Ext.data.Model', | |
| fields: [ | |
| { name: 'starttime', type : 'date', dateFormat: 'timestamp' }, | |
| { name: 'endtime', type : 'date', dateFormat: 'timestamp' }, | |
| { name: 'pid', type: 'int' }, | |
| 'node', 'upid', 'user', 'status', 'type', 'id' | |
| ], | |
| idProperty: 'upid' | |
| }); | |
| Ext.define('pve-cluster-log', { | |
| extend: 'Ext.data.Model', | |
| fields: [ | |
| { name: 'uid' , type: 'int' }, | |
| { name: 'time', type : 'date', dateFormat: 'timestamp' }, | |
| { name: 'pri', type: 'int' }, | |
| { name: 'pid', type: 'int' }, | |
| 'node', 'user', 'tag', 'msg', | |
| { | |
| name: 'id', | |
| convert: function(value, record) { | |
| var info = record.data; | |
| var text; | |
| if (value) { | |
| return value; | |
| } | |
| // compute unique ID | |
| return info.uid + ':' + info.node; | |
| } | |
| } | |
| ], | |
| idProperty: 'id' | |
| }); | |
| }); | |
| // Serialize load (avoid too many parallel connections) | |
| Ext.define('PVE.data.UpdateQueue', { | |
| singleton: true, | |
| constructor : function(){ | |
| var me = this; | |
| var queue = []; | |
| var queue_idx = {}; | |
| var idle = true; | |
| var start_update = function() { | |
| if (!idle) { | |
| return; | |
| } | |
| var storeid = queue.shift(); | |
| if (!storeid) { | |
| return; | |
| } | |
| var info = queue_idx[storeid]; | |
| queue_idx[storeid] = null; | |
| info.updatestart = new Date(); | |
| idle = false; | |
| info.store.load({ | |
| callback: function(records, operation, success) { | |
| idle = true; | |
| if (info.callback) { | |
| var runtime = (new Date()).getTime() - info.updatestart.getTime(); | |
| info.callback(runtime, success); | |
| } | |
| start_update(); | |
| } | |
| }); | |
| }; | |
| Ext.apply(me, { | |
| queue: function(store, cb) { | |
| var storeid = store.storeid; | |
| if (!storeid) { | |
| throw "unable to queue store without storeid"; | |
| } | |
| if (!queue_idx[storeid]) { | |
| queue_idx[storeid] = { | |
| store: store, | |
| callback: cb | |
| }; | |
| queue.push(storeid); | |
| } | |
| start_update(); | |
| }, | |
| unqueue: function(store) { | |
| var storeid = store.storeid; | |
| if (!storeid) { | |
| throw "unabel to unqueue store without storeid"; | |
| } | |
| if (queue_idx[storeid]) { | |
| Ext.Array.remove(queue,storeid); | |
| queue_idx[storeid] = null; | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| /* Extends the Ext.data.Store type | |
| * with startUpdate() and stopUpdate() methods | |
| * to refresh the store data in the background | |
| * Components using this store directly will flicker | |
| * due to the redisplay of the element ater 'config.interval' ms | |
| * | |
| * Note that you have to call yourself startUpdate() for the background load | |
| * to begin | |
| */ | |
| Ext.define('PVE.data.UpdateStore', { | |
| extend: 'Ext.data.Store', | |
| constructor: function(config) { | |
| var me = this; | |
| config = config || {}; | |
| if (!config.interval) { | |
| config.interval = 3000; | |
| } | |
| if (!config.storeid) { | |
| throw "no storeid specified"; | |
| } | |
| var load_task = new Ext.util.DelayedTask(); | |
| var run_load_task = function() { | |
| if (PVE.Utils.authOK()) { | |
| PVE.data.UpdateQueue.queue(me, function(runtime, success) { | |
| var interval = config.interval + runtime*2; | |
| load_task.delay(interval, run_load_task); | |
| }); | |
| } else { | |
| load_task.delay(200, run_load_task); | |
| } | |
| }; | |
| Ext.apply(config, { | |
| startUpdate: function() { | |
| run_load_task(); | |
| }, | |
| stopUpdate: function() { | |
| load_task.cancel(); | |
| PVE.data.UpdateQueue.unqueue(me); | |
| } | |
| }); | |
| me.callParent([config]); | |
| me.on('destroy', function() { | |
| load_task.cancel(); | |
| PVE.data.UpdateQueue.unqueue(me); | |
| }); | |
| } | |
| }); | |
| /* | |
| * The DiffStore is a in-memory store acting as proxy between a real store | |
| * instance and a component. | |
| * Its purpose is to redisplay the component *only* if the data has been changed | |
| * inside the real store, to avoid the annoying visual flickering of using | |
| * the real store directly. | |
| * | |
| * Implementation: | |
| * The DiffStore monitors via mon() the 'load' events sent by the real store. | |
| * On each 'load' event, the DiffStore compares its own content with the target | |
| * store (call to cond_add_item()) and then fires a 'refresh' event. | |
| * The 'refresh' event will automatically trigger a view refresh on the component | |
| * who binds to this store. | |
| */ | |
| /* Config properties: | |
| * rstore: the realstore which will autorefresh its content from the API | |
| * Only works if rstore has a model and use 'idProperty' | |
| * sortAfterUpdate: sort the diffstore before rendering the view | |
| */ | |
| Ext.define('PVE.data.DiffStore', { | |
| extend: 'Ext.data.Store', | |
| sortAfterUpdate: false, | |
| constructor: function(config) { | |
| var me = this; | |
| config = config || {}; | |
| if (!config.rstore) { | |
| throw "no rstore specified"; | |
| } | |
| if (!config.rstore.model) { | |
| throw "no rstore model specified"; | |
| } | |
| var rstore = config.rstore; | |
| Ext.apply(config, { | |
| model: rstore.model, | |
| proxy: { type: 'memory' } | |
| }); | |
| me.callParent([config]); | |
| var first_load = true; | |
| var cond_add_item = function(data, id) { | |
| var olditem = me.getById(id); | |
| if (olditem) { | |
| olditem.beginEdit(); | |
| Ext.Array.each(me.model.prototype.fields, function(field) { | |
| if (olditem.data[field.name] !== data[field.name]) { | |
| olditem.set(field.name, data[field.name]); | |
| } | |
| }); | |
| olditem.endEdit(true); | |
| olditem.commit(); | |
| } else { | |
| var newrec = Ext.create(me.model, data); | |
| var pos = (me.appendAtStart && !first_load) ? 0 : me.data.length; | |
| me.insert(pos, newrec); | |
| } | |
| }; | |
| me.mon(rstore, 'load', function(s, records, success) { | |
| if (!success) { | |
| return; | |
| } | |
| me.suspendEvents(); | |
| // getSource returns null if data is not filtered | |
| // if it is filtered it returns all records | |
| var allItems = me.getData().getSource() || me.getData(); | |
| // remove vanished items | |
| allItems.each(function(olditem) { | |
| var item = rstore.getById(olditem.getId()); | |
| if (!item) { | |
| me.remove(olditem); | |
| } | |
| }); | |
| rstore.each(function(item) { | |
| cond_add_item(item.data, item.getId()); | |
| }); | |
| me.filter(); | |
| if (me.sortAfterUpdate) { | |
| me.sort(); | |
| } | |
| first_load = false; | |
| me.resumeEvents(); | |
| me.fireEvent('refresh', me); | |
| me.fireEvent('datachanged', me); | |
| }); | |
| } | |
| }); | |
| /* This store encapsulates data items which are organized as an Array of key-values Objects | |
| * ie data[0] contains something like {key: "keyboard", value: "da"} | |
| * | |
| * Designed to work with the KeyValue model and the JsonObject data reader | |
| */ | |
| Ext.define('PVE.data.ObjectStore', { | |
| extend: 'PVE.data.UpdateStore', | |
| constructor: function(config) { | |
| var me = this; | |
| config = config || {}; | |
| if (!config.storeid) { | |
| config.storeid = 'pve-store-' + (++Ext.idSeed); | |
| } | |
| Ext.applyIf(config, { | |
| model: 'KeyValue', | |
| proxy: { | |
| type: 'pve', | |
| url: config.url, | |
| extraParams: config.extraParams, | |
| reader: { | |
| type: 'jsonobject', | |
| rows: config.rows, | |
| readArray: config.readArray, | |
| rootProperty: config.root || 'data' | |
| } | |
| } | |
| }); | |
| me.callParent([config]); | |
| } | |
| }); | |
| Ext.define('PVE.data.ResourceStore', { | |
| extend: 'PVE.data.UpdateStore', | |
| singleton: true, | |
| findVMID: function(vmid) { | |
| var me = this, i; | |
| return (me.findExact('vmid', parseInt(vmid, 10)) >= 0); | |
| }, | |
| 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: PVE.Utils.format_boolean, | |
| hidden: true, | |
| convert: function(value, record) { | |
| var info = record.data; | |
| if (info.type === 'qemu' || info.type === 'lxc' || info.type === 'node') { | |
| return (Ext.isNumeric(info.uptime) && (info.uptime > 0)); | |
| } else { | |
| return false; | |
| } | |
| } | |
| }, | |
| 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 (info.type === 'node') { | |
| text = info.node; | |
| } else if (info.type === 'pool') { | |
| text = info.pool; | |
| } else if (info.type === 'storage') { | |
| text = info.storage + ' (' + info.node + ')'; | |
| } else if (info.type === 'qemu' || info.type === 'lxc') { | |
| text = String(info.vmid); | |
| if (info.name) { | |
| text += " (" + info.name + ')'; | |
| } | |
| } else { | |
| text = info.id; | |
| } | |
| 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: PVE.Utils.format_size, | |
| width: 100 | |
| }, | |
| diskwrite: { | |
| header: gettext('Total Disk Write'), | |
| type: 'integer', | |
| hidden: true, | |
| sortable: true, | |
| renderer: PVE.Utils.format_size, | |
| width: 100 | |
| }, | |
| netin: { | |
| header: gettext('Total NetIn'), | |
| type: 'integer', | |
| hidden: true, | |
| sortable: true, | |
| renderer: PVE.Utils.format_size, | |
| width: 100 | |
| }, | |
| netout: { | |
| header: gettext('Total NetOut'), | |
| type: 'integer', | |
| hidden: true, | |
| sortable: true, | |
| renderer: PVE.Utils.format_size, | |
| width: 100 | |
| }, | |
| template: { | |
| header: gettext('Template'), | |
| type: 'integer', | |
| hidden: true, | |
| sortable: true, | |
| width: 60 | |
| }, | |
| uptime: { | |
| header: gettext('Uptime'), | |
| type: 'integer', | |
| renderer: PVE.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 | |
| } | |
| }; | |
| var fields = []; | |
| var fieldNames = []; | |
| Ext.Object.each(field_defaults, function(key, value) { | |
| if (!Ext.isDefined(value.convert) && !Ext.isDefined(value.calculate)) { | |
| fields.push({name: key, type: value.type}); | |
| fieldNames.push(key); | |
| } else if (key === 'text' || key === 'running') { | |
| fields.push({name: key, type: value.type, convert: value.convert}); | |
| fieldNames.push(key); | |
| } else { | |
| value.name = key; | |
| fields.push(value); | |
| } | |
| }); | |
| Ext.define('PVEResources', { | |
| extend: "Ext.data.Model", | |
| fields: fields, | |
| proxy: { | |
| type: 'pve', | |
| url: '/api2/json/cluster/resources' | |
| } | |
| }); | |
| Ext.define('PVETree', { | |
| extend: "Ext.data.Model", | |
| fields: fields, | |
| proxy: { type: 'memory' } | |
| }); | |
| Ext.apply(config, { | |
| storeid: 'PVEResources', | |
| model: 'PVEResources', | |
| defaultColums: 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]); | |
| } | |
| }); | |
| /* Extends the PVE.data.UpdateStore type | |
| * | |
| * | |
| */ | |
| Ext.define('PVE.data.RRDStore', { | |
| extend: 'PVE.data.UpdateStore', | |
| alias: 'store.pveRRDStore', | |
| setRRDUrl: function(timeframe, cf) { | |
| var me = this; | |
| if (!timeframe) { | |
| timeframe = me.timeframe; | |
| } | |
| if (!cf) { | |
| cf = me.cf; | |
| } | |
| me.proxy.url = me.rrdurl + "?timeframe=" + timeframe + "&cf=" + cf; | |
| }, | |
| proxy: { | |
| type: 'pve' | |
| }, | |
| fields: [ | |
| // node rrd 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', | |
| 'time', | |
| // missing qemu/lxc fields | |
| 'maxmem', | |
| 'mem', | |
| 'disk', | |
| 'diskread', | |
| 'diskwrite', | |
| 'maxdisk', | |
| // missing storage fields | |
| 'used', | |
| 'total', | |
| // for time we generate unix timestamps, javascript uses milliseconds instead of seconds | |
| { name:'time', convert: function(value) { return value*1000; }} | |
| ], | |
| sorters: 'time', | |
| timeframe: 'hour', | |
| cf: 'AVERAGE', | |
| constructor: function(config) { | |
| var me = this; | |
| config = config || {}; | |
| // set default interval to 30seconds | |
| if (!config.interval) { | |
| config.interval = 30000; | |
| } | |
| // set a new storeid | |
| if (!config.storeid) { | |
| config.storeid = 'rrdstore-' + (++Ext.idSeed); | |
| } | |
| // rrdurl is required | |
| if (!config.rrdurl) { | |
| throw "no rrdurl specified"; | |
| } | |
| var stateid = 'pveRRDTypeSelection'; | |
| var sp = Ext.state.Manager.getProvider(); | |
| var stateinit = sp.get(stateid); | |
| if (stateinit) { | |
| if(stateinit.timeframe !== me.timeframe || stateinit.cf !== me.rrdcffn){ | |
| me.timeframe = stateinit.timeframe; | |
| me.rrdcffn = stateinit.cf; | |
| } | |
| } | |
| me.callParent([config]); | |
| me.setRRDUrl(); | |
| me.mon(sp, 'statechange', function(prov, key, state){ | |
| if (key === stateid) { | |
| if (state && state.id) { | |
| if (state.timeframe !== me.timeframe || state.cf !== me.cf) { | |
| me.timeframe = state.timeframe; | |
| me.cf = state.cf; | |
| me.setRRDUrl(); | |
| me.reload(); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.form.VlanField', { | |
| extend: 'Ext.form.field.Number', | |
| alias: ['widget.pveVlanField'], | |
| deleteEmpty: false, | |
| emptyText: 'no VLAN', | |
| fieldLabel: gettext('VLAN Tag'), | |
| allowBlank: true, | |
| getSubmitData: function() { | |
| var me = this, | |
| data = null, | |
| val; | |
| if (!me.disabled && me.submitValue) { | |
| val = me.getSubmitValue(); | |
| if (val) { | |
| data = {}; | |
| data[me.getName()] = val; | |
| } else if (me.deleteEmpty) { | |
| data = {}; | |
| data['delete'] = me.getName(); | |
| } | |
| } | |
| return data; | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| Ext.apply(me, { | |
| minValue: 1, | |
| maxValue: 4094 | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.form.Checkbox', { | |
| extend: 'Ext.form.field.Checkbox', | |
| alias: ['widget.pvecheckbox'], | |
| defaultValue: undefined, | |
| deleteDefaultValue: false, | |
| deleteEmpty: false, | |
| inputValue: '1', | |
| getSubmitData: function() { | |
| var me = this, | |
| data = null, | |
| val; | |
| if (!me.disabled && me.submitValue) { | |
| val = me.getSubmitValue(); | |
| if (val !== null) { | |
| data = {}; | |
| if ((val == me.defaultValue) && me.deleteDefaultValue) { | |
| data['delete'] = me.getName(); | |
| } else { | |
| data[me.getName()] = val; | |
| } | |
| } else if (me.deleteEmpty) { | |
| data = {}; | |
| data['delete'] = me.getName(); | |
| } | |
| } | |
| return data; | |
| }, | |
| // also accept integer 1 as true | |
| setRawValue: function(value) { | |
| var me = this; | |
| if (value === 1) { | |
| me.callParent([true]); | |
| } else { | |
| me.callParent([value]); | |
| } | |
| } | |
| });Ext.define('PVE.form.Textfield', { | |
| extend: 'Ext.form.field.Text', | |
| alias: ['widget.pvetextfield'], | |
| skipEmptyText: true, | |
| deleteEmpty: false, | |
| getSubmitData: function() { | |
| var me = this, | |
| data = null, | |
| val; | |
| if (!me.disabled && me.submitValue && !me.isFileUpload()) { | |
| val = me.getSubmitValue(); | |
| if (val !== null) { | |
| data = {}; | |
| data[me.getName()] = val; | |
| } else if (me.deleteEmpty) { | |
| data = {}; | |
| data['delete'] = me.getName(); | |
| } | |
| } | |
| return data; | |
| }, | |
| getSubmitValue: function() { | |
| var me = this; | |
| var value = this.processRawValue(this.getRawValue()); | |
| if (value !== '') { | |
| return value; | |
| } | |
| return me.skipEmptyText ? null: value; | |
| } | |
| });Ext.define('PVE.form.RRDTypeSelector', { | |
| extend: 'Ext.form.field.ComboBox', | |
| alias: ['widget.pveRRDTypeSelector'], | |
| displayField: 'text', | |
| valueField: 'id', | |
| editable: false, | |
| queryMode: 'local', | |
| value: 'hour', | |
| stateEvents: [ 'select' ], | |
| stateful: true, | |
| stateId: 'pveRRDTypeSelection', | |
| store: { | |
| type: 'array', | |
| fields: [ 'id', 'timeframe', 'cf', 'text' ], | |
| data : [ | |
| [ 'hour', 'hour', 'AVERAGE', "Hour (average)" ], | |
| [ 'hourmax', 'hour', 'MAX', "Hour (max)" ], | |
| [ 'day', 'day', 'AVERAGE', "Day (average)" ], | |
| [ 'daymax', 'day', 'MAX', "Day (max)" ], | |
| [ 'week', 'week', 'AVERAGE', "Week (average)" ], | |
| [ 'weekmax', 'week', 'MAX', "Week (max)" ], | |
| [ 'month', 'month', 'AVERAGE', "Month (average)" ], | |
| [ 'monthmax', 'month', 'MAX', "Month (max)" ], | |
| [ 'year', 'year', 'AVERAGE', "Year (average)" ], | |
| [ 'yearmax', 'year', 'MAX', "Year (max)" ] | |
| ] | |
| }, | |
| // save current selection in the state Provider so RRDView can read it | |
| getState: function() { | |
| var ind = this.getStore().findExact('id', this.getValue()); | |
| var rec = this.getStore().getAt(ind); | |
| if (!rec) { | |
| return; | |
| } | |
| return { | |
| id: rec.data.id, | |
| timeframe: rec.data.timeframe, | |
| cf: rec.data.cf | |
| }; | |
| }, | |
| // set selection based on last saved state | |
| applyState : function(state) { | |
| if (state && state.id) { | |
| this.setValue(state.id); | |
| } | |
| } | |
| }); | |
| /* | |
| * ComboGrid component: a ComboBox where the dropdown menu (the | |
| * "Picker") is a Grid with Rows and Columns expects a listConfig | |
| * object with a columns property roughly based on the GridPicker from | |
| * https://www.sencha.com/forum/showthread.php?299909 | |
| * | |
| */ | |
| Ext.define('PVE.form.ComboGrid', { | |
| extend: 'Ext.form.field.ComboBox', | |
| alias: ['widget.PVE.form.ComboGrid'], | |
| // this value is used as default value after load() | |
| preferredValue: undefined, | |
| // hack: allow to select empty value | |
| // seems extjs does not allow that when 'editable == false' | |
| onKeyUp: function(e, t) { | |
| var me = this; | |
| var key = e.getKey(); | |
| if (!me.editable && me.allowBlank && !me.multiSelect && | |
| (key == e.BACKSPACE || key == e.DELETE)) { | |
| me.setValue(''); | |
| } | |
| me.callParent(arguments); | |
| }, | |
| // needed to trigger onKeyUp etc. | |
| enableKeyEvents: true, | |
| // override ExtJS method | |
| // if the field has multiSelect enabled, the store is not loaded, and | |
| // the displayfield == valuefield, it saves the rawvalue as an array | |
| // but the getRawValue method is only defined in the textfield class | |
| // (which has not to deal with arrays) an returns the string in the | |
| // field (not an array) | |
| // | |
| // so if we have multiselect enabled, return the rawValue (which | |
| // should be an array) and else we do callParent so | |
| // it should not impact any other use of the class | |
| getRawValue: function() { | |
| var me = this; | |
| if (me.multiSelect) { | |
| return me.rawValue; | |
| } else { | |
| return me.callParent(); | |
| } | |
| }, | |
| // override ExtJS protected method | |
| onBindStore: function(store, initial) { | |
| var me = this, | |
| picker = me.picker, | |
| extraKeySpec, | |
| valueCollectionConfig; | |
| // We're being bound, not unbound... | |
| if (store) { | |
| // If store was created from a 2 dimensional array with generated field names 'field1' and 'field2' | |
| if (store.autoCreated) { | |
| me.queryMode = 'local'; | |
| me.valueField = me.displayField = 'field1'; | |
| if (!store.expanded) { | |
| me.displayField = 'field2'; | |
| } | |
| // displayTpl config will need regenerating with the autogenerated displayField name 'field1' | |
| me.setDisplayTpl(null); | |
| } | |
| if (!Ext.isDefined(me.valueField)) { | |
| me.valueField = me.displayField; | |
| } | |
| // Add a byValue index to the store so that we can efficiently look up records by the value field | |
| // when setValue passes string value(s). | |
| // The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys | |
| // are found, they are all returned by the get call. | |
| // This is so that findByText and findByValue are able to return the *FIRST* matching value. By default, | |
| // if unique is true, CollectionKey keeps the *last* matching value. | |
| extraKeySpec = { | |
| byValue: { | |
| rootProperty: 'data', | |
| unique: false | |
| } | |
| }; | |
| extraKeySpec.byValue.property = me.valueField; | |
| store.setExtraKeys(extraKeySpec); | |
| if (me.displayField === me.valueField) { | |
| store.byText = store.byValue; | |
| } else { | |
| extraKeySpec.byText = { | |
| rootProperty: 'data', | |
| unique: false | |
| }; | |
| extraKeySpec.byText.property = me.displayField; | |
| store.setExtraKeys(extraKeySpec); | |
| } | |
| // We hold a collection of the values which have been selected, keyed by this field's valueField. | |
| // This collection also functions as the selected items collection for the BoundList's selection model | |
| valueCollectionConfig = { | |
| rootProperty: 'data', | |
| extraKeys: { | |
| byInternalId: { | |
| property: 'internalId' | |
| }, | |
| byValue: { | |
| property: me.valueField, | |
| rootProperty: 'data' | |
| } | |
| }, | |
| // Whenever this collection is changed by anyone, whether by this field adding to it, | |
| // or the BoundList operating, we must refresh our value. | |
| listeners: { | |
| beginupdate: me.onValueCollectionBeginUpdate, | |
| endupdate: me.onValueCollectionEndUpdate, | |
| scope: me | |
| } | |
| }; | |
| // This becomes our collection of selected records for the Field. | |
| me.valueCollection = new Ext.util.Collection(valueCollectionConfig); | |
| // We use the selected Collection as our value collection and the basis | |
| // for rendering the tag list. | |
| //pve override: since the picker is represented by a grid panel, | |
| // we changed here the selection to RowModel | |
| me.pickerSelectionModel = new Ext.selection.RowModel({ | |
| mode: me.multiSelect ? 'SIMPLE' : 'SINGLE', | |
| // There are situations when a row is selected on mousedown but then the mouse is dragged to another row | |
| // and released. In these situations, the event target for the click event won't be the row where the mouse | |
| // was released but the boundview. The view will then determine that it should fire a container click, and | |
| // the DataViewModel will then deselect all prior selections. Setting `deselectOnContainerClick` here will | |
| // prevent the model from deselecting. | |
| deselectOnContainerClick: false, | |
| enableInitialSelection: false, | |
| pruneRemoved: false, | |
| selected: me.valueCollection, | |
| store: store, | |
| listeners: { | |
| scope: me, | |
| lastselectedchanged: me.updateBindSelection | |
| } | |
| }); | |
| if (!initial) { | |
| me.resetToDefault(); | |
| } | |
| if (picker) { | |
| picker.setSelectionModel(me.pickerSelectionModel); | |
| if (picker.getStore() !== store) { | |
| picker.bindStore(store); | |
| } | |
| } | |
| } | |
| }, | |
| // copied from ComboBox | |
| createPicker: function() { | |
| var me = this; | |
| var picker; | |
| var pickerCfg = Ext.apply({ | |
| // pve overrides: display a grid for selection | |
| xtype: 'gridpanel', | |
| id: me.pickerId, | |
| pickerField: me, | |
| floating: true, | |
| hidden: true, | |
| store: me.store, | |
| displayField: me.displayField, | |
| preserveScrollOnRefresh: true, | |
| pageSize: me.pageSize, | |
| tpl: me.tpl, | |
| selModel: me.pickerSelectionModel, | |
| focusOnToFront: false | |
| }, me.listConfig, me.defaultListConfig); | |
| picker = me.picker || Ext.widget(pickerCfg); | |
| if (picker.getStore() !== me.store) { | |
| picker.bindStore(me.store); | |
| } | |
| if (me.pageSize) { | |
| picker.pagingToolbar.on('beforechange', me.onPageChange, me); | |
| } | |
| // pve overrides: pass missing method in gridPanel to its view | |
| picker.refresh = function() { | |
| picker.getSelectionModel().select(me.valueCollection.getRange()); | |
| picker.getView().refresh(); | |
| }; | |
| picker.getNodeByRecord = function() { | |
| picker.getView().getNodeByRecord(arguments); | |
| }; | |
| // We limit the height of the picker to fit in the space above | |
| // or below this field unless the picker has its own ideas about that. | |
| if (!picker.initialConfig.maxHeight) { | |
| picker.on({ | |
| beforeshow: me.onBeforePickerShow, | |
| scope: me | |
| }); | |
| } | |
| picker.getSelectionModel().on({ | |
| beforeselect: me.onBeforeSelect, | |
| beforedeselect: me.onBeforeDeselect, | |
| focuschange: me.onFocusChange, | |
| selectionChange: function (sm, selectedRecords) { | |
| var me = this; | |
| if (selectedRecords.length) { | |
| me.setValue(selectedRecords); | |
| me.fireEvent('select', me, selectedRecords); | |
| } | |
| }, | |
| scope: me | |
| }); | |
| // hack for extjs6 | |
| // when the clicked item is the same as the previously selected, | |
| // it does not select the item | |
| // instead we hide the picker | |
| if (!me.multiSelect) { | |
| picker.on('itemclick', function (sm,record) { | |
| if (picker.getSelection()[0] === record) { | |
| picker.hide(); | |
| } | |
| }); | |
| } | |
| // when our store is not yet loaded, we increase | |
| // the height of the gridpanel, so that we can see | |
| // the loading mask | |
| // | |
| // we save the minheight to reset it after the load | |
| picker.on('show', function() { | |
| if (me.enableLoadMask) { | |
| me.savedMinHeight = picker.getMinHeight(); | |
| picker.setMinHeight(100); | |
| } | |
| }); | |
| picker.getNavigationModel().navigateOnSpace = false; | |
| return picker; | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| if (me.initialConfig.editable === undefined) { | |
| me.editable = false; | |
| } | |
| Ext.apply(me, { | |
| queryMode: 'local', | |
| matchFieldWidth: false | |
| }); | |
| Ext.applyIf(me, { value: ''}); // hack: avoid ExtJS validate() bug | |
| Ext.applyIf(me.listConfig, { width: 400 }); | |
| me.callParent(); | |
| // Create the picker at an early stage, so it is available to store the previous selection | |
| if (!me.picker) { | |
| me.createPicker(); | |
| } | |
| me.mon(me.store, 'beforeload', function() { | |
| if (!me.isDisabled()) { | |
| me.enableLoadMask = true; | |
| } | |
| }); | |
| // hack: autoSelect does not work | |
| me.mon(me.store, 'load', function(store, r, success, o) { | |
| if (success) { | |
| me.clearInvalid(); | |
| if (me.enableLoadMask) { | |
| delete me.enableLoadMask; | |
| // if the picker exists, | |
| // we reset its minheight to the saved var/0 | |
| // we have to update the layout, otherwise the height | |
| // gets not recalculated | |
| if (me.picker) { | |
| me.picker.setMinHeight(me.savedMinHeight || 0); | |
| delete me.savedMinHeight; | |
| me.picker.updateLayout(); | |
| } | |
| } | |
| var def = me.getValue() || me.preferredValue; | |
| if (def) { | |
| me.setValue(def, true); // sync with grid | |
| } | |
| var found = false; | |
| if (def) { | |
| if (Ext.isArray(def)) { | |
| Ext.Array.each(def, function(v) { | |
| if (store.findRecord(me.valueField, v)) { | |
| found = true; | |
| return false; // break | |
| } | |
| }); | |
| } else { | |
| found = store.findRecord(me.valueField, def); | |
| } | |
| } | |
| if (!found) { | |
| var rec = me.store.first(); | |
| if (me.autoSelect && rec && rec.data) { | |
| def = rec.data[me.valueField]; | |
| me.setValue(def, true); | |
| } else { | |
| me.setValue(me.editable ? def : '', true); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| /* Key-Value ComboBox | |
| * | |
| * config properties: | |
| * comboItems: an array of Key - Value pairs | |
| * deleteEmpty: if set to true (default), an empty value received from the | |
| * comboBox will reset the property to its default value | |
| */ | |
| Ext.define('PVE.form.KVComboBox', { | |
| extend: 'Ext.form.field.ComboBox', | |
| alias: 'widget.pveKVComboBox', | |
| deleteEmpty: true, | |
| comboItems: undefined, | |
| displayField: 'value', | |
| valueField: 'key', | |
| queryMode: 'local', | |
| // overide framework function to implement deleteEmpty behaviour | |
| getSubmitData: function() { | |
| var me = this, | |
| data = null, | |
| val; | |
| if (!me.disabled && me.submitValue) { | |
| val = me.getSubmitValue(); | |
| if (val !== null && val !== '' && val !== '__default__') { | |
| data = {}; | |
| data[me.getName()] = val; | |
| } else if (me.deleteEmpty) { | |
| data = {}; | |
| data['delete'] = me.getName(); | |
| } | |
| } | |
| return data; | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| me.store = Ext.create('Ext.data.ArrayStore', { | |
| model: 'KeyValue', | |
| data : me.comboItems | |
| }); | |
| if (me.initialConfig.editable === undefined) { | |
| me.editable = false; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| // boolean type including 'Default' (delete property from file) | |
| Ext.define('PVE.form.Boolean', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.booleanfield'], | |
| comboItems: [ | |
| ['__default__', gettext('Default')], | |
| [1, gettext('Yes')], | |
| [0, gettext('No')] | |
| ] | |
| }); | |
| Ext.define('PVE.form.CompressionSelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.pveCompressionSelector'], | |
| comboItems: [ | |
| ['0', PVE.Utils.noneText], | |
| ['lzo', 'LZO (' + gettext('fast') + ')'], | |
| ['gzip', 'GZIP (' + gettext('good') + ')'] | |
| ] | |
| }); | |
| Ext.define('PVE.form.PoolSelector', { | |
| extend: 'PVE.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' | |
| }); | |
| 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: 'pve', | |
| url: "/api2/json/pools" | |
| }, | |
| idProperty: 'poolid' | |
| }); | |
| }); | |
| Ext.define('PVE.form.GroupSelector', { | |
| extend: 'PVE.form.ComboGrid', | |
| alias: ['widget.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' | |
| }); | |
| Ext.apply(me, { | |
| store: store | |
| }); | |
| me.callParent(); | |
| store.load(); | |
| } | |
| }, function() { | |
| Ext.define('pve-groups', { | |
| extend: 'Ext.data.Model', | |
| fields: [ 'groupid', 'comment' ], | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/access/groups" | |
| }, | |
| idProperty: 'groupid' | |
| }); | |
| }); | |
| Ext.define('PVE.form.UserSelector', { | |
| extend: 'PVE.form.ComboGrid', | |
| alias: ['widget.pveUserSelector'], | |
| allowBlank: false, | |
| autoSelect: false, | |
| valueField: 'userid', | |
| displayField: 'userid', | |
| initComponent: function() { | |
| var me = this; | |
| var store = new Ext.data.Store({ | |
| model: 'pve-users' | |
| }); | |
| var render_full_name = function(firstname, metaData, record) { | |
| var first = firstname || ''; | |
| var last = record.data.lastname || ''; | |
| return first + " " + last; | |
| }; | |
| Ext.apply(me, { | |
| store: store, | |
| listConfig: { | |
| columns: [ | |
| { | |
| header: gettext('User'), | |
| sortable: true, | |
| dataIndex: 'userid', | |
| flex: 1 | |
| }, | |
| { | |
| header: gettext('Name'), | |
| sortable: true, | |
| renderer: 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: 'pve', | |
| url: "/api2/json/access/users" | |
| }, | |
| idProperty: 'userid' | |
| }); | |
| }); | |
| Ext.define('PVE.form.RoleSelector', { | |
| extend: 'PVE.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' | |
| }); | |
| 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: 'pve', | |
| url: "/api2/json/access/roles" | |
| }, | |
| idProperty: 'roleid' | |
| }); | |
| }); | |
| Ext.define('PVE.form.VMIDSelector', { | |
| extend: 'Ext.form.field.Number', | |
| alias: 'widget.pveVMIDSelector', | |
| allowBlank: false, | |
| minValue: 100, | |
| maxValue: 999999999, | |
| validateExists: undefined, | |
| loadNextFreeVMID: false, | |
| initComponent: function() { | |
| var me = this; | |
| Ext.applyIf(me, { | |
| fieldLabel: 'VM ID', | |
| listeners: { | |
| 'change': function(field, newValue, oldValue) { | |
| if (!Ext.isDefined(me.validateExists)) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| params: { vmid: newValue }, | |
| url: '/cluster/nextid', | |
| method: 'GET', | |
| success: function(response, opts) { | |
| if (me.validateExists === true) { | |
| me.markInvalid(gettext('This VM ID does not exists')); | |
| } | |
| }, | |
| failure: function(response, opts) { | |
| if (me.validateExists === false) { | |
| me.markInvalid(gettext('This VM ID is already in use')); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| if (me.loadNextFreeVMID) { | |
| PVE.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 | |
| 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: 'PVE.form.KVComboBox', | |
| alias: ['widget.PVE.form.NetworkCardSelector'], | |
| comboItems: [ | |
| ['e1000', 'Intel E1000'], | |
| ['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'], | |
| ['rtl8139', 'Realtek RTL8139'], | |
| ['vmxnet3', 'VMware vmxnet3'] | |
| ] | |
| }); | |
| Ext.define('PVE.form.DiskFormatSelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.PVE.form.DiskFormatSelector'], | |
| comboItems: [ | |
| ['raw', gettext('Raw disk image') + ' (raw)'], | |
| ['qcow2', gettext('QEMU image format') + ' (qcow2)'], | |
| ['vmdk', gettext('VMware image format') + ' (vmdk)'] | |
| ] | |
| }); | |
| Ext.define('PVE.form.BusTypeSelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.PVE.form.BusTypeSelector'], | |
| noVirtIO: false, | |
| noScsi: false, | |
| initComponent: function() { | |
| var me = this; | |
| me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']]; | |
| if (!me.noVirtIO) { | |
| me.comboItems.push(['virtio', 'VirtIO']); | |
| } | |
| if (!me.noScsi) { | |
| me.comboItems.push(['scsi', 'SCSI']); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.form.ControllerSelector', { | |
| extend: 'Ext.form.FieldContainer', | |
| alias: ['widget.PVE.form.ControllerSelector'], | |
| statics: { | |
| maxIds: { | |
| ide: 3, | |
| sata: 5, | |
| virtio: 15, | |
| scsi: 13 | |
| } | |
| }, | |
| noVirtIO: false, | |
| noScsi: false, | |
| vmconfig: {}, // used to check for existing devices | |
| setVMConfig: function(vmconfig, autoSelect) { | |
| var me = this; | |
| me.vmconfig = Ext.apply({}, vmconfig); | |
| if (autoSelect) { | |
| var clist = ['ide', 'virtio', 'scsi', 'sata']; | |
| if (autoSelect === 'cdrom') { | |
| clist = ['ide', 'scsi', 'sata']; | |
| if (!Ext.isDefined(me.vmconfig.ide2)) { | |
| me.down('field[name=controller]').setValue('ide'); | |
| me.down('field[name=deviceid]').setValue(2); | |
| return; | |
| } | |
| } else if (me.vmconfig.ostype === 'l26') { | |
| clist = ['virtio', 'ide', 'scsi', 'sata']; | |
| } | |
| Ext.Array.each(clist, function(controller) { | |
| var confid, i; | |
| if ((controller === 'virtio' && me.noVirtIO) || | |
| (controller === 'scsi' && me.noScsi)) { | |
| return; //continue | |
| } | |
| me.down('field[name=controller]').setValue(controller); | |
| for (i = 0; i <= PVE.form.ControllerSelector.maxIds[controller]; i++) { | |
| confid = controller + i.toString(); | |
| if (!Ext.isDefined(me.vmconfig[confid])) { | |
| me.down('field[name=deviceid]').setValue(i); | |
| return false; // break | |
| } | |
| } | |
| }); | |
| } | |
| me.down('field[name=deviceid]').validate(); | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| Ext.apply(me, { | |
| fieldLabel: gettext('Bus/Device'), | |
| layout: 'hbox', | |
| defaults: { | |
| flex: 1, | |
| hideLabel: true | |
| }, | |
| items: [ | |
| { | |
| xtype: 'PVE.form.BusTypeSelector', | |
| name: 'controller', | |
| value: 'ide', | |
| noVirtIO: me.noVirtIO, | |
| noScsi: me.noScsi, | |
| allowBlank: false, | |
| listeners: { | |
| change: function(t, value) { | |
| if (!me.rendered || !value) { | |
| return; | |
| } | |
| var field = me.down('field[name=deviceid]'); | |
| field.setMaxValue(PVE.form.ControllerSelector.maxIds[value]); | |
| field.validate(); | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'deviceid', | |
| minValue: 0, | |
| maxValue: PVE.form.ControllerSelector.maxIds.ide, | |
| value: '0', | |
| 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: 'PVE.form.KVComboBox', | |
| alias: ['widget.pveEmailNotificationSelector'], | |
| comboItems: [ | |
| ['always', gettext('Always')], | |
| ['failure', gettext('On failure only')] | |
| ] | |
| }); | |
| Ext.define('PVE.form.RealmComboBox', { | |
| extend: 'Ext.form.field.ComboBox', | |
| alias: ['widget.pveRealmComboBox'], | |
| fieldLabel: gettext('Realm'), | |
| name: 'realm', | |
| queryMode: 'local', | |
| allowBlank: 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; | |
| var rec = me.store.findRecord('realm', realm); | |
| return rec && rec.data && rec.data.tfa ? rec.data.tfa : undefined; | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| me.store = Ext.create('Ext.data.Store', { | |
| model: 'pve-domains' | |
| }); | |
| me.callParent(); | |
| me.store.load({ | |
| callback: function(r, o, success) { | |
| if (success) { | |
| var def = me.getValue(); | |
| if (!def || !me.store.findRecord('realm', def)) { | |
| def = 'pam'; | |
| Ext.each(r, function(record) { | |
| if (record.data && record.data["default"]) { | |
| def = record.data.realm; | |
| } | |
| }); | |
| } | |
| if (def) { | |
| me.setValue(def); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.form.BondModeSelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.bondModeSelector'], | |
| openvswitch: false, | |
| initComponent: function() { | |
| var me = this; | |
| if (me.openvswitch) { | |
| me.comboItems = [ | |
| ['active-backup', 'active-backup'], | |
| ['balance-slb', 'balance-slb'], | |
| ['lacp-balance-slb', 'LACP (balance-slb)'], | |
| ['lacp-balance-tcp', 'LACP (balance-tcp)'] | |
| ]; | |
| } else { | |
| me.comboItems = [ | |
| ['balance-rr', 'balance-rr'], | |
| ['active-backup', 'active-backup'], | |
| ['balance-xor', 'balance-xor'], | |
| ['broadcast', 'broadcast'], | |
| ['802.3ad', 'LACP (802.3ad)'], | |
| ['balance-tlb', 'balance-tlb'], | |
| ['balance-alb', 'balance-alb'] | |
| ]; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.form.BondPolicySelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.bondPolicySelector'], | |
| comboItems: [ | |
| ['layer2', 'layer2'], | |
| ['layer2+3', 'layer2+3'], | |
| ['layer3+4', 'layer3+4'] | |
| ] | |
| }); | |
| /* | |
| * 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: 'PVE.form.ComboGrid', | |
| alias: ['widget.pveNodeSelector'], | |
| // invalidate nodes which are offline | |
| onlineValidator: false, | |
| selectCurNode: false, | |
| // 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: 'pve', | |
| url: '/api2/json/nodes' | |
| }, | |
| sorters: [ | |
| { | |
| property : 'node', | |
| direction: 'ASC' | |
| }, | |
| { | |
| property : 'mem', | |
| direction: 'DESC' | |
| } | |
| ] | |
| }, | |
| listConfig: { | |
| columns: [ | |
| { | |
| header: gettext('Node'), | |
| dataIndex: 'node', | |
| sortable: true, | |
| hideable: false, | |
| flex: 1 | |
| }, | |
| { | |
| header: gettext('Memory usage') + " %", | |
| renderer: PVE.Utils.render_mem_usage_percent, | |
| sortable: true, | |
| width: 100, | |
| dataIndex: 'mem' | |
| }, | |
| { | |
| header: gettext('CPU usage'), | |
| renderer: PVE.Utils.render_cpu, | |
| sortable: true, | |
| width: 100, | |
| dataIndex: 'cpu' | |
| } | |
| ] | |
| }, | |
| validator: function(value) { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.onlineValidator || (me.allowBlank && !value)) { | |
| return true; | |
| } | |
| var offline = []; | |
| var notAllowed = []; | |
| Ext.Array.each(value.split(/\s*,\s*/), function(node) { | |
| var rec = me.store.findRecord(me.valueField, node); | |
| if (!(rec && rec.data) || !Ext.isNumeric(rec.data.mem)) { | |
| offline.push(node); | |
| } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) { | |
| notAllowed.push(node); | |
| } | |
| }); | |
| if (notAllowed.length !== 0) { | |
| return "Node " + notAllowed.join(', ') + " is not allowed for this action!"; | |
| } | |
| if (offline.length !== 0) { | |
| return "Node " + offline.join(', ') + " seems to be offline!"; | |
| } | |
| return true; | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| if (me.selectCurNode && PVE.curSelectedNode.data.node) { | |
| me.preferredValue = PVE.curSelectedNode.data.node; | |
| } | |
| me.callParent(); | |
| me.getStore().load(); | |
| } | |
| }); | |
| Ext.define('PVE.form.FileSelector', { | |
| extend: 'PVE.form.ComboGrid', | |
| alias: ['widget.pveFileSelector'], | |
| 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: 'pve', | |
| url: url | |
| }); | |
| me.store.load(); | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| var store = Ext.create('Ext.data.Store', { | |
| model: 'pve-storage-content' | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| allowBlank: false, | |
| autoSelect: false, | |
| valueField: 'volid', | |
| displayField: 'text', | |
| listConfig: { | |
| columns: [ | |
| { | |
| header: gettext('Name'), | |
| dataIndex: 'text', | |
| hideable: false, | |
| flex: 1 | |
| }, | |
| { | |
| header: gettext('Format'), | |
| width: 60, | |
| dataIndex: 'format' | |
| }, | |
| { | |
| header: gettext('Size'), | |
| width: 60, | |
| dataIndex: 'size', | |
| renderer: PVE.Utils.format_size | |
| } | |
| ] | |
| } | |
| }); | |
| me.callParent(); | |
| me.setStorage(me.storage, me.nodename); | |
| } | |
| }); | |
| Ext.define('PVE.form.StorageSelector', { | |
| extend: 'PVE.form.ComboGrid', | |
| alias: ['widget.PVE.form.StorageSelector'], | |
| allowBlank: false, | |
| valueField: 'storage', | |
| displayField: 'storage', | |
| listConfig: { | |
| columns: [ | |
| { | |
| header: gettext('Name'), | |
| dataIndex: 'storage', | |
| hideable: false, | |
| flex: 1 | |
| }, | |
| { | |
| header: gettext('Type'), | |
| width: 60, | |
| dataIndex: 'type' | |
| }, | |
| { | |
| header: gettext('Avail'), | |
| width: 80, | |
| dataIndex: 'avail', | |
| renderer: PVE.Utils.format_size | |
| }, | |
| { | |
| header: gettext('Capacity'), | |
| width: 80, | |
| dataIndex: 'total', | |
| renderer: PVE.Utils.format_size | |
| } | |
| ] | |
| }, | |
| reloadStorageList: function() { | |
| var me = this; | |
| if (!me.nodename) { | |
| return; | |
| } | |
| var params = {}; | |
| 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: 'pve', | |
| 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.BridgeSelector', { | |
| extend: 'PVE.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, | |
| flex: 1 | |
| }, | |
| { | |
| header: gettext('Active'), | |
| width: 60, | |
| dataIndex: 'active', | |
| renderer: PVE.Utils.format_boolean | |
| } | |
| ] | |
| }, | |
| setNodename: function(nodename) { | |
| var me = this; | |
| if (!nodename || (me.nodename === nodename)) { | |
| return; | |
| } | |
| me.nodename = nodename; | |
| me.store.setProxy({ | |
| type: 'pve', | |
| 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.SecurityGroupsSelector', { | |
| extend: 'PVE.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: 'pve', | |
| 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: 'PVE.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: 'pve', | |
| url: url | |
| }, | |
| sorters: { | |
| property: 'ref', | |
| order: 'DESC' | |
| } | |
| }); | |
| var disable_query_for_ips = function(f, value) { | |
| if (value === null || | |
| value.match(/^\d/)) { // IP address starts with \d | |
| f.queryDelay = 9999999999; // hack: disbale with long delay | |
| } else { | |
| f.queryDelay = 10; | |
| } | |
| }; | |
| var columns = []; | |
| if (!me.ref_type) { | |
| columns.push({ | |
| header: gettext('Type'), | |
| dataIndex: 'type', | |
| hideable: false, | |
| width: 60 | |
| }); | |
| } | |
| columns.push( | |
| { | |
| header: gettext('Name'), | |
| dataIndex: 'ref', | |
| hideable: false, | |
| width: 140 | |
| }, | |
| { | |
| header: gettext('Comment'), | |
| dataIndex: 'comment', | |
| renderer: Ext.String.htmlEncode, | |
| flex: 1 | |
| } | |
| ); | |
| Ext.apply(me, { | |
| store: store, | |
| listConfig: { columns: columns } | |
| }); | |
| me.on('change', disable_query_for_ips); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.form.IPProtocolSelector', { | |
| extend: 'PVE.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: 'PVE.form.KVComboBox', | |
| alias: ['widget.CPUModelSelector'], | |
| comboItems: [ | |
| ['__default__', PVE.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'], | |
| ['Opteron_G1', 'Opteron_G1'], | |
| ['Opteron_G2', 'Opteron_G2'], | |
| ['Opteron_G3', 'Opteron_G3'], | |
| ['Opteron_G4', 'Opteron_G4'], | |
| ['Opteron_G5', 'Opteron_G5'], | |
| ['host', 'host'] | |
| ] | |
| }); | |
| Ext.define('PVE.form.VNCKeyboardSelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.VNCKeyboardSelector'], | |
| comboItems: PVE.Utils.kvm_keymap_array() | |
| }); | |
| Ext.define('PVE.form.LanguageSelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.pveLanguageSelector'], | |
| comboItems: PVE.Utils.language_array() | |
| }); | |
| Ext.define('PVE.form.DisplaySelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.DisplaySelector'], | |
| comboItems: PVE.Utils.kvm_vga_driver_array() | |
| }); | |
| Ext.define('PVE.form.CacheTypeSelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.CacheTypeSelector'], | |
| comboItems: [ | |
| ['__default__', PVE.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: 'PVE.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: 'pve', | |
| url: '/api2/json/nodes/' + me.nodename + '/qemu/' + 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"; | |
| } | |
| 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: 'PVE.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']; | |
| } | |
| 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, | |
| items: [ | |
| { boxLabel: gettext('Disk'), name: 'hotplug', inputValue: 'disk', submitValue: false, checked: true }, | |
| { boxLabel: gettext('Network'), name: 'hotplug', inputValue: 'network',submitValue: false, checked: true }, | |
| { boxLabel: gettext('USB'), name: 'hotplug', inputValue: 'usb', submitValue: false, checked: true }, | |
| { boxLabel: gettext('Memory'), name: 'hotplug', inputValue: 'memory', submitValue: false }, | |
| { boxLabel: gettext('CPU'), name: 'hotplug', inputValue: 'cpu', submitValue: false } | |
| ], | |
| setValue: function(value) { | |
| var me = this; | |
| var newVal = []; | |
| if (value === '1') { | |
| newVal = ['disk', 'network', 'usb']; | |
| } else if (value !== '0') { | |
| newVal = value.split(','); | |
| } | |
| me.callParent([{ hotplug: newVal }]); | |
| }, | |
| // overide framework function to | |
| // assemble the hotplug value | |
| getSubmitData: function() { | |
| var me = this, | |
| boxes = me.getBoxes(), | |
| data = []; | |
| Ext.Array.forEach(boxes, function(box){ | |
| if (box.getValue()) { | |
| data.push(box.inputValue); | |
| } | |
| }); | |
| /* because above is hotplug an array */ | |
| /*jslint confusion: true*/ | |
| if (data.length === 0) { | |
| return { 'hotplug':'0' }; | |
| } else { | |
| return { 'hotplug': data.join(',') }; | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.form.iScsiProviderSelector', { | |
| extend: 'PVE.form.KVComboBox', | |
| alias: ['widget.pveiScsiProviderSelector'], | |
| comboItems: [ | |
| ['comstar', 'Comstar'], | |
| [ 'istgt', 'istgt'], | |
| [ 'iet', 'IET'] | |
| ] | |
| }); | |
| Ext.define('PVE.form.DayOfWeekSelector', { | |
| extend: 'PVE.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: 'PVE.form.KVComboBox', | |
| alias: ['widget.pveBackupModeSelector'], | |
| comboItems: [ | |
| ['snapshot', gettext('Snapshot')], | |
| ['suspend', gettext('Suspend')], | |
| ['stop', gettext('Stop')] | |
| ] | |
| }); | |
| Ext.define('PVE.form.ScsiHwSelector', { | |
| extend: 'PVE.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: 'PVE.form.KVComboBox', | |
| alias: ['widget.pveFirewallPolicySelector'], | |
| comboItems: [ | |
| ['ACCEPT', 'ACCEPT'], | |
| ['REJECT', 'REJECT'], | |
| [ 'DROP', 'DROP'] | |
| ] | |
| }); | |
| Ext.define('PVE.form.QemuBiosSelector', { | |
| extend: 'PVE.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(); | |
| } | |
| }); | |
| /* 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 = new PVE.data.UpdateStore({ | |
| storeid: 'pve-cluster-tasks', | |
| model: 'pve-tasks', | |
| proxy: { | |
| type: 'pve', | |
| url: '/api2/json/cluster/tasks' | |
| } | |
| }); | |
| var store = Ext.create('PVE.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('PVE.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 "x-form-invalid-field"; | |
| } | |
| } | |
| }, | |
| 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: PVE.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 PVE.Utils.errorText + ': ' + value; | |
| } | |
| } | |
| ], | |
| listeners: { | |
| itemdblclick: run_task_viewer, | |
| show: taskstore.startUpdate, | |
| hide: taskstore.stopUpdate, | |
| 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 = new PVE.data.UpdateStore({ | |
| storeid: 'pve-cluster-log', | |
| model: 'pve-cluster-log', | |
| proxy: { | |
| type: 'pve', | |
| url: '/api2/json/cluster/log' | |
| } | |
| }); | |
| var store = Ext.create('PVE.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 "x-form-invalid-field"; | |
| } | |
| } | |
| }, | |
| 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.RRDView', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pveRRDView', | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.datasource) { | |
| throw "no datasource specified"; | |
| } | |
| if (!me.rrdurl) { | |
| throw "no rrdurl specified"; | |
| } | |
| var stateid = 'pveRRDTypeSelection'; | |
| var sp = Ext.state.Manager.getProvider(); | |
| var stateinit = sp.get(stateid); | |
| if (stateinit) { | |
| if(stateinit.timeframe !== me.timeframe || stateinit.cf !== me.rrdcffn){ | |
| me.timeframe = stateinit.timeframe; | |
| me.rrdcffn = stateinit.cf; | |
| } | |
| } | |
| if (!me.timeframe) { | |
| if(stateinit && stateinit.timeframe){ | |
| me.timeframe = stateinit.timeframe; | |
| }else{ | |
| me.timeframe = 'hour'; | |
| } | |
| } | |
| if (!me.rrdcffn) { | |
| if(stateinit && stateinit.rrdcffn){ | |
| me.rrdcffn = stateinit.cf; | |
| }else{ | |
| me.rrdcffn = 'AVERAGE'; | |
| } | |
| } | |
| var datasource = me.datasource; | |
| // fixme: dcindex?? | |
| var dcindex = 0; | |
| var create_url = function() { | |
| var url = me.rrdurl + "?ds=" + datasource + | |
| "&timeframe=" + me.timeframe + "&cf=" + me.rrdcffn + | |
| "&_dc=" + dcindex.toString(); | |
| dcindex++; | |
| return url; | |
| }; | |
| Ext.apply(me, { | |
| layout: 'fit', | |
| html: { | |
| tag: 'img', | |
| width: 800, | |
| height: 200, | |
| src: create_url() | |
| }, | |
| applyState : function(state) { | |
| if (state && state.id) { | |
| if(state.timeframe !== me.timeframe || state.cf !== me.rrdcffn){ | |
| me.timeframe = state.timeframe; | |
| me.rrdcffn = state.cf; | |
| me.reload_task.delay(10); | |
| } | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| me.reload_task = new Ext.util.DelayedTask(function() { | |
| if (me.rendered) { | |
| try { | |
| var html = { | |
| tag: 'img', | |
| width: 800, | |
| height: 200, | |
| src: create_url() | |
| }; | |
| me.update(html); | |
| } catch (e) { | |
| // fixme: | |
| console.log(e); | |
| } | |
| me.reload_task.delay(30000); | |
| } else { | |
| me.reload_task.delay(1000); | |
| } | |
| }); | |
| me.reload_task.delay(30000); | |
| me.on('destroy', function() { | |
| me.reload_task.cancel(); | |
| }); | |
| var state_change_fn = function(prov, key, value) { | |
| if (key == stateid) { | |
| me.applyState(value); | |
| } | |
| }; | |
| me.mon(sp, 'statechange', state_change_fn); | |
| } | |
| }); | |
| Ext.define('PVE.widget.RRDChart', { | |
| extend: 'Ext.chart.CartesianChart', | |
| alias: 'widget.pveRRDChart', | |
| width: 800, | |
| height: 300, | |
| animation: false, | |
| interactions: [{ | |
| type: 'crosszoom' | |
| }], | |
| axes: [{ | |
| type: 'numeric', | |
| position: 'left', | |
| grid: true, | |
| renderer: 'leftAxisRenderer', | |
| minimum: 0 | |
| }, { | |
| type: 'time', | |
| position: 'bottom', | |
| grid: true, | |
| fields: ['time'] | |
| }], | |
| legend: { | |
| docked: 'right', | |
| // we set this that all graphs have same width | |
| width: 140 | |
| }, | |
| listeners: { | |
| afterrender: 'onAfterRender', | |
| animationend: 'onAfterAnimation' | |
| }, | |
| bytesArr : [ | |
| 'memtotal', | |
| 'memused', | |
| 'roottotal', | |
| 'rootused', | |
| 'swaptotal', | |
| 'swapused', | |
| 'maxmem', | |
| 'mem', | |
| 'disk', | |
| 'maxdisk', | |
| 'total', | |
| 'used' | |
| ], | |
| bytespersArr: [ | |
| 'netin', | |
| 'netout', | |
| 'diskread', | |
| 'diskwrite' | |
| ], | |
| percentArr: [ | |
| 'cpu', | |
| 'iowait' | |
| ], | |
| convertToUnits: function(value) { | |
| var units = ['', 'k','M','G','T', 'P']; | |
| var si = 0; | |
| while(value >= 1000 && si < (units.length -1)){ | |
| value = value / 1000; | |
| si++; | |
| } | |
| // javascript floating point weirdness | |
| value = Ext.Number.correctFloat(value); | |
| // limit to 2 decimal points | |
| value = Ext.util.Format.number(value, "0.##"); | |
| return value.toString() + " " + units[si]; | |
| }, | |
| leftAxisRenderer: function(axis, label, layoutContext) { | |
| var me = this; | |
| return me.convertToUnits(label); | |
| }, | |
| onSeriesTooltipRender: function (tooltip, record, item) { | |
| var me = this; | |
| var suffix = ''; | |
| if (me.percentArr.indexOf(item.field) != -1) { | |
| suffix = '%'; | |
| } else if (me.bytesArr.indexOf(item.field) != -1) { | |
| suffix = 'B'; | |
| } else if (me.bytespersArr.indexOf(item.field) != -1) { | |
| suffix = 'B/s'; | |
| } | |
| var prefix = item.field; | |
| if (me.fieldTitles && me.fieldTitles[me.fields.indexOf(item.field)]) { | |
| prefix = me.fieldTitles[me.fields.indexOf(item.field)]; | |
| } | |
| tooltip.setHtml(prefix + ': ' + this.convertToUnits(record.get(item.field)) + suffix + | |
| '<br>' + new Date(record.get('time'))); | |
| }, | |
| onAfterRender: function(){ | |
| var me = this; | |
| // add correct label for left axis | |
| var axisTitle = ""; | |
| if (me.percentArr.indexOf(me.fields[0]) != -1) { | |
| axisTitle = "%"; | |
| } else if (me.bytesArr.indexOf(me.fields[0]) != -1) { | |
| axisTitle = "Bytes"; | |
| } else if (me.bytespersArr.indexOf(me.fields[0]) != -1) { | |
| axisTitle = "Bytes/s"; | |
| } | |
| me.axes[0].setTitle(axisTitle); | |
| me.addTool({ | |
| type: 'minus', | |
| disabled: true, | |
| tooltip: gettext('Undo Zoom'), | |
| handler: function(){ | |
| var undoButton = me.interactions[0].getUndoButton(); | |
| if (undoButton.handler) { | |
| undoButton.handler(); | |
| } | |
| } | |
| }); | |
| // add a series for each field we get | |
| me.fields.forEach(function(item, index){ | |
| var title = item; | |
| if (me.fieldTitles && me.fieldTitles[index]) { | |
| title = me.fieldTitles[index]; | |
| } | |
| me.addSeries({ | |
| type: 'line', | |
| xField: 'time', | |
| yField: item, | |
| title: title, | |
| fill: true, | |
| style: { | |
| lineWidth: 1.5, | |
| opacity: 0.60 | |
| }, | |
| marker: { | |
| opacity: 0, | |
| scaling: 0.01, | |
| fx: { | |
| duration: 200, | |
| easing: 'easeOut' | |
| } | |
| }, | |
| highlightCfg: { | |
| opacity: 1, | |
| scaling: 1.5 | |
| }, | |
| tooltip: { | |
| trackMouse: true, | |
| renderer: 'onSeriesTooltipRender' | |
| } | |
| }); | |
| }); | |
| }, | |
| onAfterAnimation: function(chart, eopts) { | |
| // if the undobuton is disabled, | |
| // disable our tool | |
| var ourUndoZoomButton = chart.tools[0]; | |
| var undoButton = chart.interactions[0].getUndoButton(); | |
| ourUndoZoomButton.setDisabled(undoButton.isDisabled()); | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| if (!me.store) { | |
| throw "cannot work without store"; | |
| } | |
| if (!me.fields) { | |
| throw "cannot work without fields"; | |
| } | |
| me.callParent(); | |
| // enable animation after the store is loaded | |
| me.store.onAfter('load', function() { | |
| me.setAnimation(true); | |
| }, this, {single: true}); | |
| } | |
| }); | |
| Ext.define('PVE.panel.InputPanel', { | |
| extend: 'Ext.panel.Panel', | |
| alias: ['widget.inputpanel'], | |
| listeners: { | |
| activate: function() { | |
| // notify owning container that it should display a help button | |
| if (this.onlineHelp) { | |
| Ext.GlobalEvents.fireEvent('pveShowHelp', this.onlineHelp); | |
| } | |
| }, | |
| deactivate: function() { | |
| if (this.onlineHelp) { | |
| Ext.GlobalEvents.fireEvent('pveHideHelp', this.onlineHelp); | |
| } | |
| } | |
| }, | |
| border: false, | |
| // override this with an URL to a relevant chapter of the pve manual | |
| // setting this will display a help button in our parent panel | |
| onlineHelp: undefined, | |
| // overwrite this to modify submit data | |
| onGetValues: function(values) { | |
| return values; | |
| }, | |
| getValues: function(dirtyOnly) { | |
| var me = this; | |
| if (Ext.isFunction(me.onGetValues)) { | |
| dirtyOnly = false; | |
| } | |
| var values = {}; | |
| Ext.Array.each(me.query('[isFormField]'), function(field) { | |
| if (!dirtyOnly || field.isDirty()) { | |
| PVE.Utils.assemble_field_data(values, field.getSubmitData()); | |
| } | |
| }); | |
| return me.onGetValues(values); | |
| }, | |
| setValues: function(values) { | |
| var me = this; | |
| var form = me.up('form'); | |
| Ext.iterate(values, function(fieldId, val) { | |
| var field = me.query('[isFormField][name=' + fieldId + ']')[0]; | |
| if (field) { | |
| field.setValue(val); | |
| if (form.trackResetOnLoad) { | |
| field.resetOriginalValue(); | |
| } | |
| } | |
| }); | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| var items; | |
| if (me.items) { | |
| me.columns = 1; | |
| items = [ | |
| { | |
| columnWidth: 1, | |
| layout: 'anchor', | |
| items: me.items | |
| } | |
| ]; | |
| me.items = undefined; | |
| } else if (me.column1) { | |
| me.columns = 2; | |
| items = [ | |
| { | |
| columnWidth: 0.5, | |
| padding: '0 10 0 0', | |
| layout: 'anchor', | |
| items: me.column1 | |
| }, | |
| { | |
| columnWidth: 0.5, | |
| padding: '0 0 0 10', | |
| layout: 'anchor', | |
| items: me.column2 || [] // allow empty column | |
| } | |
| ]; | |
| if (me.columnB) { | |
| items.push({ | |
| columnWidth: 1, | |
| padding: '10 0 0 0', | |
| layout: 'anchor', | |
| items: me.columnB | |
| }); | |
| } | |
| } else { | |
| throw "unsupported config"; | |
| } | |
| if (me.useFieldContainer) { | |
| Ext.apply(me, { | |
| layout: 'fit', | |
| items: Ext.apply(me.useFieldContainer, { | |
| layout: 'column', | |
| defaultType: 'container', | |
| items: items | |
| }) | |
| }); | |
| } else { | |
| Ext.apply(me, { | |
| layout: 'column', | |
| defaultType: 'container', | |
| items: items | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| // fixme: how can we avoid those lint errors? | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.window.Edit', { | |
| extend: 'Ext.window.Window', | |
| alias: 'widget.pveWindowEdit', | |
| resizable: false, | |
| // use this tio atimatically generate a title like | |
| // Create: <subject> | |
| subject: undefined, | |
| // set create to true if you want a Create button (instead | |
| // OK and RESET) | |
| create: false, | |
| // set to true if you want an Add button (instead of Create) | |
| isAdd: false, | |
| // set to true if you want an Remove button (instead of Create) | |
| isRemove: false, | |
| backgroundDelay: 0, | |
| showProgress: false, | |
| isValid: function() { | |
| var me = this; | |
| var form = me.formPanel.getForm(); | |
| return form.isValid(); | |
| }, | |
| getValues: function(dirtyOnly) { | |
| var me = this; | |
| var values = {}; | |
| var form = me.formPanel.getForm(); | |
| form.getFields().each(function(field) { | |
| if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { | |
| PVE.Utils.assemble_field_data(values, field.getSubmitData()); | |
| } | |
| }); | |
| Ext.Array.each(me.query('inputpanel'), function(panel) { | |
| PVE.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); | |
| }); | |
| return values; | |
| }, | |
| setValues: function(values) { | |
| var me = this; | |
| var form = me.formPanel.getForm(); | |
| Ext.iterate(values, function(fieldId, val) { | |
| var field = form.findField(fieldId); | |
| if (field && !field.up('inputpanel')) { | |
| field.setValue(val); | |
| if (form.trackResetOnLoad) { | |
| field.resetOriginalValue(); | |
| } | |
| } | |
| }); | |
| Ext.Array.each(me.query('inputpanel'), function(panel) { | |
| panel.setValues(values); | |
| }); | |
| }, | |
| submit: function() { | |
| var me = this; | |
| var form = me.formPanel.getForm(); | |
| var values = me.getValues(); | |
| Ext.Object.each(values, function(name, val) { | |
| if (values.hasOwnProperty(name)) { | |
| if (Ext.isArray(val) && !val.length) { | |
| values[name] = ''; | |
| } | |
| } | |
| }); | |
| if (me.digest) { | |
| values.digest = me.digest; | |
| } | |
| if (me.backgroundDelay) { | |
| values.background_delay = me.backgroundDelay; | |
| } | |
| var url = me.url; | |
| if (me.method === 'DELETE') { | |
| url = url + "?" + Ext.Object.toQueryString(values); | |
| values = undefined; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: url, | |
| waitMsgTarget: me, | |
| method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'), | |
| params: values, | |
| failure: function(response, options) { | |
| if (response.result && response.result.errors) { | |
| form.markInvalid(response.result.errors); | |
| } | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| }, | |
| success: function(response, options) { | |
| me.close(); | |
| if ((me.backgroundDelay || me.showProgress) && | |
| response.result.data) { | |
| var upid = response.result.data; | |
| var win = Ext.create('PVE.window.TaskProgress', { | |
| upid: upid | |
| }); | |
| win.show(); | |
| } | |
| } | |
| }); | |
| }, | |
| load: function(options) { | |
| var me = this; | |
| var form = me.formPanel.getForm(); | |
| options = options || {}; | |
| var newopts = Ext.apply({ | |
| waitMsgTarget: me | |
| }, options); | |
| var createWrapper = function(successFn) { | |
| Ext.apply(newopts, { | |
| url: me.url, | |
| method: 'GET', | |
| success: function(response, opts) { | |
| form.clearInvalid(); | |
| me.digest = response.result.data.digest; | |
| if (successFn) { | |
| successFn(response, opts); | |
| } else { | |
| me.setValues(response.result.data); | |
| } | |
| // hack: fix ExtJS bug | |
| Ext.Array.each(me.query('radiofield'), function(f) { | |
| f.resetOriginalValue(); | |
| }); | |
| }, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus, function() { | |
| me.close(); | |
| }); | |
| } | |
| }); | |
| }; | |
| createWrapper(options.success); | |
| PVE.Utils.API2Request(newopts); | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.url) { | |
| throw "no url specified"; | |
| } | |
| var items = Ext.isArray(me.items) ? me.items : [ me.items ]; | |
| me.items = undefined; | |
| me.formPanel = Ext.create('Ext.form.Panel', { | |
| url: me.url, | |
| method: me.method || 'PUT', | |
| trackResetOnLoad: true, | |
| bodyPadding: 10, | |
| border: false, | |
| defaults: { | |
| border: false | |
| }, | |
| fieldDefaults: Ext.apply({}, me.fieldDefaults, { | |
| labelWidth: 100, | |
| anchor: '100%' | |
| }), | |
| items: items | |
| }); | |
| var form = me.formPanel.getForm(); | |
| var submitBtn = Ext.create('Ext.Button', { | |
| text: me.create ? (me.isAdd ? gettext('Add') : ( me.isRemove ? gettext('Remove') : gettext('Create'))) : gettext('OK'), | |
| disabled: !me.create, | |
| handler: function() { | |
| me.submit(); | |
| } | |
| }); | |
| var resetBtn = Ext.create('Ext.Button', { | |
| text: 'Reset', | |
| disabled: true, | |
| handler: function(){ | |
| form.reset(); | |
| } | |
| }); | |
| var set_button_status = function() { | |
| var valid = form.isValid(); | |
| var dirty = form.isDirty(); | |
| submitBtn.setDisabled(!valid || !(dirty || me.create)); | |
| resetBtn.setDisabled(!dirty); | |
| }; | |
| form.on('dirtychange', set_button_status); | |
| form.on('validitychange', set_button_status); | |
| var colwidth = 300; | |
| if (me.fieldDefaults && me.fieldDefaults.labelWidth) { | |
| colwidth += me.fieldDefaults.labelWidth - 100; | |
| } | |
| var twoColumn = items[0].column1 || items[0].column2; | |
| if (me.subject && !me.title) { | |
| me.title = PVE.Utils.dialog_title(me.subject, me.create, me.isAdd); | |
| } | |
| if (me.create) { | |
| me.buttons = [ submitBtn ] ; | |
| } else { | |
| me.buttons = [ submitBtn, resetBtn ]; | |
| } | |
| if (items[0].onlineHelp) { | |
| var helpButton = Ext.create('PVE.button.Help'); | |
| me.buttons.unshift(helpButton, '->'); | |
| Ext.GlobalEvents.fireEvent('pveShowHelp', items[0].onlineHelp); | |
| } | |
| Ext.applyIf(me, { | |
| modal: true, | |
| width: twoColumn ? colwidth*2 : colwidth, | |
| border: false, | |
| items: [ me.formPanel ] | |
| }); | |
| me.callParent(); | |
| // always mark invalid fields | |
| me.on('afterlayout', function() { | |
| me.isValid(); | |
| }); | |
| } | |
| }); | |
| 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 view = this.getView(); | |
| if(form.isValid()){ | |
| view.el.mask(gettext('Please wait...'), 'x-mask-loading'); | |
| form.submit({ | |
| failure: function(f, resp){ | |
| 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(f, resp){ | |
| view.el.unmask(); | |
| var handler = view.handler || Ext.emptyFn; | |
| handler.call(me, resp.result.data); | |
| view.close(); | |
| } | |
| }); | |
| } | |
| }, | |
| control: { | |
| 'field[name=username]': { | |
| specialkey: function(f, e) { | |
| if (e.getKey() === e.ENTER) { | |
| var pf = this.lookupReference('passwordField'); | |
| if (pf.getValue()) { | |
| this.onLogon(); | |
| } else { | |
| pf.focus(false); | |
| } | |
| } | |
| } | |
| }, | |
| 'field[name=password]': { | |
| specialkey: function(f, e) { | |
| if (e.getKey() === e.ENTER) { | |
| this.onLogon(); | |
| } | |
| } | |
| }, | |
| 'field[name=realm]': { | |
| change: function(f, value) { | |
| var otp_field = this.lookupReference('otpField'); | |
| if (f.needOTP(value)) { | |
| otp_field.setVisible(true); | |
| otp_field.setDisabled(false); | |
| } else { | |
| otp_field.setVisible(false); | |
| otp_field.setDisabled(true); | |
| } | |
| } | |
| }, | |
| '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' | |
| } | |
| } | |
| }, | |
| width: 400, | |
| modal: true, | |
| border: false, | |
| draggable: true, | |
| closable: false, | |
| resizable: false, | |
| layout: 'auto', | |
| title: gettext('Proxmox VE Login'), | |
| defaultFocus: 'usernameField', | |
| 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', | |
| blankText: gettext("Enter your user name") | |
| }, | |
| { | |
| xtype: 'textfield', | |
| inputType: 'password', | |
| fieldLabel: gettext('Password'), | |
| name: 'password', | |
| reference: 'passwordField', | |
| blankText: gettext("Enter your password") | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('OTP'), | |
| name: 'otp', | |
| reference: 'otpField', | |
| allowBlank: false, | |
| hidden: true | |
| }, | |
| { | |
| xtype: 'pveRealmComboBox', | |
| name: 'realm' | |
| }, | |
| { | |
| xtype: 'pveLanguageSelector', | |
| fieldLabel: gettext('Language'), | |
| value: Ext.util.Cookies.get('PVELangCookie') || 'en', | |
| name: 'lang', | |
| reference: 'langField', | |
| submitValue: false | |
| } | |
| ], | |
| buttons: [ | |
| { | |
| text: gettext('Login'), | |
| reference: 'loginButton' | |
| } | |
| ] | |
| }] | |
| }); | |
| Ext.define('PVE.window.TaskProgress', { | |
| extend: 'Ext.window.Window', | |
| alias: 'widget.pveTaskProgress', | |
| initComponent: function() { | |
| var me = this; | |
| if (!me.upid) { | |
| throw "no task specified"; | |
| } | |
| var task = PVE.Utils.parse_task_upid(me.upid); | |
| var statstore = Ext.create('PVE.data.ObjectStore', { | |
| url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", | |
| interval: 1000, | |
| rows: { | |
| status: { defaultValue: 'unknown' }, | |
| exitstatus: { defaultValue: 'unknown' } | |
| } | |
| }); | |
| me.on('destroy', statstore.stopUpdate); | |
| var getObjectValue = function(key, defaultValue) { | |
| var rec = statstore.getById(key); | |
| if (rec) { | |
| return rec.data.value; | |
| } | |
| return defaultValue; | |
| }; | |
| var pbar = Ext.create('Ext.ProgressBar', { text: 'running...' }); | |
| me.mon(statstore, 'load', function() { | |
| var status = getObjectValue('status'); | |
| if (status === 'stopped') { | |
| var exitstatus = getObjectValue('exitstatus'); | |
| if (exitstatus == 'OK') { | |
| pbar.reset(); | |
| pbar.updateText("Done!"); | |
| Ext.Function.defer(me.close, 1000, me); | |
| } else { | |
| me.close(); | |
| Ext.Msg.alert('Task failed', exitstatus); | |
| } | |
| } | |
| }); | |
| var descr = PVE.Utils.format_task_description(task.type, task.id); | |
| Ext.apply(me, { | |
| title: "Task: " + descr, | |
| width: 300, | |
| layout: 'auto', | |
| modal: true, | |
| bodyPadding: 5, | |
| items: pbar, | |
| buttons: [ | |
| { | |
| text: gettext('Details'), | |
| handler: function() { | |
| var win = Ext.create('PVE.window.TaskViewer', { | |
| upid: me.upid | |
| }); | |
| win.show(); | |
| me.close(); | |
| } | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| statstore.startUpdate(); | |
| pbar.wait(); | |
| } | |
| }); | |
| // fixme: how can we avoid those lint errors? | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.window.TaskViewer', { | |
| extend: 'Ext.window.Window', | |
| alias: 'widget.pveTaskViewer', | |
| initComponent: function() { | |
| var me = this; | |
| if (!me.upid) { | |
| throw "no task specified"; | |
| } | |
| var task = PVE.Utils.parse_task_upid(me.upid); | |
| var statgrid; | |
| var rows = { | |
| status: { | |
| header: gettext('Status'), | |
| defaultValue: 'unknown', | |
| renderer: function(value) { | |
| if (value != 'stopped') { | |
| return value; | |
| } | |
| var es = statgrid.getObjectValue('exitstatus'); | |
| if (es) { | |
| return value + ': ' + es; | |
| } | |
| } | |
| }, | |
| exitstatus: { | |
| visible: false | |
| }, | |
| type: { | |
| header: gettext('Task type'), | |
| required: true | |
| }, | |
| user: { | |
| header: gettext('User name'), | |
| required: true | |
| }, | |
| node: { | |
| header: gettext('Node'), | |
| required: true | |
| }, | |
| pid: { | |
| header: gettext('Process ID'), | |
| required: true | |
| }, | |
| starttime: { | |
| header: gettext('Start Time'), | |
| required: true, | |
| renderer: PVE.Utils.render_timestamp | |
| }, | |
| upid: { | |
| header: gettext('Unique task ID') | |
| } | |
| }; | |
| var statstore = Ext.create('PVE.data.ObjectStore', { | |
| url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", | |
| interval: 1000, | |
| rows: rows | |
| }); | |
| me.on('destroy', statstore.stopUpdate); | |
| var stop_task = function() { | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + task.node + "/tasks/" + me.upid, | |
| waitMsgTarget: me, | |
| method: 'DELETE', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| }; | |
| var stop_btn1 = new Ext.Button({ | |
| text: gettext('Stop'), | |
| disabled: true, | |
| handler: stop_task | |
| }); | |
| var stop_btn2 = new Ext.Button({ | |
| text: gettext('Stop'), | |
| disabled: true, | |
| handler: stop_task | |
| }); | |
| statgrid = Ext.create('PVE.grid.ObjectGrid', { | |
| title: gettext('Status'), | |
| layout: 'fit', | |
| tbar: [ stop_btn1 ], | |
| rstore: statstore, | |
| rows: rows, | |
| border: false | |
| }); | |
| var logView = Ext.create('PVE.panel.LogView', { | |
| title: gettext('Output'), | |
| tbar: [ stop_btn2 ], | |
| border: false, | |
| url: "/api2/extjs/nodes/" + task.node + "/tasks/" + me.upid + "/log" | |
| }); | |
| me.mon(statstore, 'load', function() { | |
| var status = statgrid.getObjectValue('status'); | |
| if (status === 'stopped') { | |
| logView.requestUpdate(undefined, true); | |
| logView.scrollToEnd = false; | |
| statstore.stopUpdate(); | |
| } | |
| stop_btn1.setDisabled(status !== 'running'); | |
| stop_btn2.setDisabled(status !== 'running'); | |
| }); | |
| statstore.startUpdate(); | |
| Ext.apply(me, { | |
| title: "Task viewer: " + task.desc, | |
| width: 800, | |
| height: 400, | |
| layout: 'fit', | |
| modal: true, | |
| items: [{ | |
| xtype: 'tabpanel', | |
| region: 'center', | |
| items: [ logView, statgrid ] | |
| }] | |
| }); | |
| me.callParent(); | |
| logView.fireEvent('show', logView); | |
| } | |
| }); | |
| Ext.define('PVE.window.Wizard', { | |
| extend: 'Ext.window.Window', | |
| 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())) { | |
| PVE.Utils.assemble_field_data(values, field.getSubmitData()); | |
| } | |
| }); | |
| Ext.Array.each(me.query('inputpanel'), function(panel) { | |
| PVE.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 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 tbar = Ext.create('Ext.toolbar.Toolbar', { | |
| ui: 'footer', | |
| region: 'south', | |
| margins: '0 5 5 5', | |
| items: [ | |
| { | |
| xtype: 'pveHelpButton', | |
| itemId: 'help' | |
| }, | |
| '->', | |
| { | |
| 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(); | |
| } | |
| } | |
| ] | |
| }); | |
| var display_header = function(newcard) { | |
| var html = '<h1>' + newcard.title + '</h1>'; | |
| if (newcard.descr) { | |
| html += newcard.descr; | |
| } | |
| me.down('#header').update(html); | |
| }; | |
| 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 next = tp.items.indexOf(newcard) + 1; | |
| var ntab = tp.items.getAt(next); | |
| if (valid && ntab && !newcard.onSubmit) { | |
| ntab.enable(); | |
| } | |
| }; | |
| if (me.subject && !me.title) { | |
| me.title = PVE.Utils.dialog_title(me.subject, true, false); | |
| } | |
| Ext.apply(me, { | |
| width: 620, | |
| height: 450, | |
| modal: true, | |
| border: false, | |
| draggable: true, | |
| closable: true, | |
| resizable: false, | |
| layout: 'border', | |
| items: [ | |
| { | |
| // disabled for now - not really needed | |
| hidden: true, | |
| region: 'north', | |
| itemId: 'header', | |
| layout: 'fit', | |
| margins: '5 5 0 5', | |
| bodyPadding: 10, | |
| html: '' | |
| }, | |
| { | |
| 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) { | |
| display_header(newcard); | |
| tabchange(tp, newcard, oldcard); | |
| } | |
| }, | |
| items: tabs | |
| }] | |
| }, | |
| tbar | |
| ] | |
| }); | |
| me.callParent(); | |
| display_header(tabs[0]); | |
| Ext.Array.each(me.query('field'), function(field) { | |
| field.on('change', function(f) { | |
| var tp = me.down('#wizcontent'); | |
| var atab = tp.getActiveTab(); | |
| var valid = check_card(atab); | |
| me.down('#next').setDisabled(!valid); | |
| me.down('#submit').setDisabled(!valid); | |
| var next = tp.items.indexOf(atab) + 1; | |
| var ntab = tp.items.getAt(next); | |
| if (!valid) { | |
| disable_at(ntab); | |
| } else if (ntab && !atab.onSubmit) { | |
| ntab.enable(); | |
| } | |
| }); | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.window.NotesEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| Ext.apply(me, { | |
| title: gettext('Notes'), | |
| width: 600, | |
| layout: 'fit', | |
| items: { | |
| xtype: 'textarea', | |
| name: 'description', | |
| height: 200, | |
| 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') | |
| } | |
| ] | |
| }); | |
| 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.compress) { | |
| params.compress = values.compress; | |
| } | |
| PVE.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) { | |
| var upid = response.result.data; | |
| var win = Ext.create('PVE.window.TaskViewer', { | |
| upid: upid | |
| }); | |
| win.show(); | |
| me.close(); | |
| } | |
| }); | |
| } | |
| }); | |
| var title = gettext('Backup') + " " + | |
| ((me.vmtype === 'openvz') ? "CT" : "VM") + | |
| " " + me.vmid; | |
| Ext.apply(me, { | |
| title: title, | |
| width: 350, | |
| modal: true, | |
| layout: 'auto', | |
| border: false, | |
| items: [ me.formPanel ], | |
| buttons: [ submitBtn ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.window.Restore', { | |
| extend: 'Ext.window.Window', // fixme: PVE.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 | |
| }); | |
| me.formPanel = Ext.create('Ext.form.Panel', { | |
| bodyPadding: 10, | |
| border: false, | |
| fieldDefaults: { | |
| labelWidth: 100, | |
| anchor: '100%' | |
| }, | |
| items: [ | |
| { | |
| xtype: 'displayfield', | |
| value: me.volidText || me.volid, | |
| fieldLabel: gettext('Source') | |
| }, | |
| storagesel, | |
| { | |
| xtype: me.vmid ? 'displayfield' : 'pveVMIDSelector', | |
| name: 'vmid', | |
| fieldLabel: 'VM ID', | |
| value: me.vmid, | |
| loadNextFreeVMID: me.vmid ? false: true, | |
| validateExists: false | |
| } | |
| ] | |
| }); | |
| var form = me.formPanel.getForm(); | |
| var doRestore = function(url, params) { | |
| PVE.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('PVE.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 | |
| }; | |
| var url; | |
| var msg; | |
| if (me.vmtype === 'lxc') { | |
| url = '/nodes/' + me.nodename + '/lxc'; | |
| params.ostemplate = me.volid; | |
| params.restore = 1; | |
| msg = PVE.Utils.format_task_description('vzrestore', params.vmid); | |
| } else if (me.vmtype === 'qemu') { | |
| url = '/nodes/' + me.nodename + '/qemu'; | |
| params.archive = me.volid; | |
| msg = PVE.Utils.format_task_description('qmrestore', params.vmid); | |
| } else { | |
| throw 'unknown VM type'; | |
| } | |
| if (me.vmid) { | |
| msg += '. ' + gettext('This will permanently erase current VM data.'); | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| doRestore(url, params); | |
| }); | |
| } else { | |
| doRestore(url, params); | |
| } | |
| } | |
| }); | |
| form.on('validitychange', function(f, valid) { | |
| submitBtn.setDisabled(!valid); | |
| }); | |
| var title = gettext('Restore') + ": " + ( | |
| (me.vmtype === 'lxc') ? 'CT' : 'VM'); | |
| if (me.vmid) { | |
| title += " " + me.vmid; | |
| } | |
| Ext.apply(me, { | |
| title: title, | |
| width: 500, | |
| modal: true, | |
| layout: 'auto', | |
| border: false, | |
| items: [ me.formPanel ], | |
| buttons: [ submitBtn ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| /* Popup a message window | |
| * where the user has to manually enter the ressource ID | |
| * to enable the destroy button | |
| */ | |
| Ext.define('PVE.window.SafeDestroy', { | |
| extend: 'Ext.window.Window', | |
| alias: 'widget.pveSafeDestroy', | |
| title: gettext('Confirm'), | |
| modal: true, | |
| buttonAlign: 'center', | |
| bodyPadding: 10, | |
| width: 450, | |
| layout: { type:'hbox' }, | |
| defaultFocus: 'confirmField', | |
| config: { | |
| item: { | |
| id: undefined, | |
| type: undefined | |
| }, | |
| url: undefined | |
| }, | |
| 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) { | |
| 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(); | |
| PVE.Utils.API2Request({ | |
| url: view.getUrl(), | |
| method: 'DELETE', | |
| waitMsgTarget: view, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| }, | |
| callback: function() { | |
| 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: 'numberfield', | |
| 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 = PVE.Utils.format_task_description('qmdestroy', item.id); | |
| } else if (item.type === 'CT') { | |
| msg = PVE.Utils.format_task_description('vzdestroy', item.id); | |
| } else { | |
| throw "unknown VM 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.panel.NotesView', { | |
| extend: 'Ext.panel.Panel', | |
| title: gettext("Notes"), | |
| bodyStyle: 'white-space:pre', | |
| bodyPadding: 10, | |
| scrollable: true, | |
| load: function() { | |
| var me = this; | |
| PVE.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)); | |
| } | |
| }); | |
| }, | |
| 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; | |
| var url; | |
| if (vmtype === 'qemu') { | |
| me.url = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid + '/config'; | |
| } else if (vmtype === 'lxc') { | |
| me.url = '/api2/extjs/nodes/' + nodename + '/lxc/' + vmid + '/config'; | |
| } else { | |
| throw "unknown vm type '" + vmtype + "'"; | |
| } | |
| Ext.apply(me, { | |
| listeners: { | |
| render: function(c) { | |
| c.el.on('dblclick', function() { | |
| var win = Ext.create('PVE.window.NotesEdit', { | |
| pveSelNode: me.pveSelNode, | |
| url: me.url | |
| }); | |
| win.show(); | |
| win.on('destroy', me.load, me); | |
| }); | |
| } | |
| }, | |
| tools: [{ | |
| type: 'gear', | |
| handler: function() { | |
| var win = Ext.create('PVE.window.NotesEdit', { | |
| pveSelNode: me.pveSelNode, | |
| url: me.url | |
| }); | |
| win.show(); | |
| win.on('destroy', me.load, me); | |
| } | |
| }] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.override(Ext.view.Table, { | |
| afterRender: function() { | |
| var me = this; | |
| me.callParent(); | |
| // EXT5DEBUG | |
| // me.mon(me.el, { | |
| // scroll: me.fireBodyScroll, | |
| // scope: me | |
| // }); | |
| // if (!me.featuresMC || | |
| // (me.featuresMC.findIndex('ftype', 'selectable') < 0)) { | |
| // me.el.unselectable(); | |
| // } | |
| // | |
| // me.attachEventsForFeatures(); | |
| } | |
| }); | |
| Ext.define('PVE.grid.SelectFeature', { | |
| extend: 'Ext.grid.feature.Feature', | |
| alias: 'feature.selectable', | |
| mutateMetaRowTpl: function(metaRowTpl) { | |
| var tpl, i, | |
| ln = metaRowTpl.length; | |
| for (i = 0; i < ln; i++) { | |
| tpl = metaRowTpl[i]; | |
| tpl = tpl.replace(/x-grid-row/, 'x-grid-row x-selectable'); | |
| tpl = tpl.replace(/x-grid-cell-inner x-unselectable/g, 'x-grid-cell-inner'); | |
| tpl = tpl.replace(/unselectable="on"/g, ''); | |
| metaRowTpl[i] = tpl; | |
| } | |
| } | |
| }); | |
| /* Renders a list of key values objets | |
| mandatory config parameters: | |
| rows: an object container where each propery is a key-value object we want to render | |
| var rows = { | |
| keyboard: { | |
| header: gettext('Keyboard Layout'), | |
| editor: 'PVE.dc.KeyboardEdit', | |
| renderer: PVE.Utils.render_kvm_language, | |
| required: true | |
| }, | |
| optional: | |
| disabled: setting this parameter to true will disable selection and focus on the | |
| pveObjectGrid as well as greying out input elements. | |
| Useful for a readonly tabular display | |
| */ | |
| Ext.define('PVE.grid.ObjectGrid', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveObjectGrid'], | |
| disabled: false, | |
| getObjectValue: function(key, defaultValue) { | |
| var me = this; | |
| var rec = me.store.getById(key); | |
| if (rec) { | |
| return rec.data.value; | |
| } | |
| return defaultValue; | |
| }, | |
| renderKey: function(key, metaData, record, rowIndex, colIndex, store) { | |
| var me = this; | |
| var rows = me.rows; | |
| var rowdef = (rows && rows[key]) ? rows[key] : {}; | |
| return rowdef.header || key; | |
| }, | |
| renderValue: function(value, metaData, record, rowIndex, colIndex, store) { | |
| var me = this; | |
| var rows = me.rows; | |
| var key = record.data.key; | |
| var rowdef = (rows && rows[key]) ? rows[key] : {}; | |
| var renderer = rowdef.renderer; | |
| if (renderer) { | |
| return renderer(value, metaData, record, rowIndex, colIndex, store); | |
| } | |
| return value; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| var rows = me.rows; | |
| if (!me.rstore) { | |
| if (!me.url) { | |
| throw "no url specified"; | |
| } | |
| me.rstore = Ext.create('PVE.data.ObjectStore', { | |
| url: me.url, | |
| interval: me.interval, | |
| extraParams: me.extraParams, | |
| rows: me.rows | |
| }); | |
| } | |
| var rstore = me.rstore; | |
| var store = Ext.create('PVE.data.DiffStore', { rstore: rstore, | |
| sorters: [], | |
| filters: [] | |
| }); | |
| if (rows) { | |
| Ext.Object.each(rows, function(key, rowdef) { | |
| if (Ext.isDefined(rowdef.defaultValue)) { | |
| store.add({ key: key, value: rowdef.defaultValue }); | |
| } else if (rowdef.required) { | |
| store.add({ key: key, value: undefined }); | |
| } | |
| }); | |
| } | |
| if (me.sorterFn) { | |
| store.sorters.add(Ext.create('Ext.util.Sorter', { | |
| sorterFn: me.sorterFn | |
| })); | |
| } | |
| store.filters.add(Ext.create('Ext.util.Filter', { | |
| filterFn: function(item) { | |
| if (rows) { | |
| var rowdef = rows[item.data.key]; | |
| if (!rowdef || (rowdef.visible === false)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| })); | |
| PVE.Utils.monStoreErrors(me, rstore); | |
| Ext.applyIf(me, { | |
| store: store, | |
| hideHeaders: true, | |
| stateful: false, | |
| columns: [ | |
| { | |
| header: gettext('Name'), | |
| width: me.cwidth1 || 200, | |
| dataIndex: 'key', | |
| renderer: me.renderKey | |
| }, | |
| { | |
| flex: 1, | |
| header: gettext('Value'), | |
| dataIndex: 'value', | |
| renderer: me.renderValue | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.grid.PendingObjectGrid', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pvePendingObjectGrid'], | |
| getObjectValue: function(key, defaultValue, pending) { | |
| var me = this; | |
| var rec = me.store.getById(key); | |
| if (rec) { | |
| var value = (pending && Ext.isDefined(rec.data.pending) && (rec.data.pending !== '')) ? | |
| rec.data.pending : rec.data.value; | |
| if (Ext.isDefined(value) && (value !== '')) { | |
| return value; | |
| } else { | |
| return defaultValue; | |
| } | |
| } | |
| return defaultValue; | |
| }, | |
| hasPendingChanges: function(key) { | |
| var me = this; | |
| var rows = me.rows; | |
| var rowdef = (rows && rows[key]) ? rows[key] : {}; | |
| var keys = rowdef.multiKey || [ key ]; | |
| var pending = false; | |
| Ext.Array.each(keys, function(k) { | |
| var rec = me.store.getById(k); | |
| if (rec && rec.data && Ext.isDefined(rec.data.pending) && (rec.data.pending !== '')) { | |
| pending = true; | |
| return false; // break | |
| } | |
| }); | |
| return pending; | |
| }, | |
| renderValue: function(value, metaData, record, rowIndex, colIndex, store) { | |
| var me = this; | |
| var rows = me.rows; | |
| var key = record.data.key; | |
| var rowdef = (rows && rows[key]) ? rows[key] : {}; | |
| var renderer = rowdef.renderer; | |
| var current = ''; | |
| var pendingdelete = ''; | |
| var pending = ''; | |
| if (renderer) { | |
| current = renderer(value, metaData, record, rowIndex, colIndex, store, false); | |
| if (me.hasPendingChanges(key)) { | |
| pending = renderer(record.data.pending, metaData, record, rowIndex, colIndex, store, true); | |
| } | |
| if (pending == current) { | |
| pending = undefined; | |
| } | |
| } else { | |
| current = value || ''; | |
| pending = record.data.pending; | |
| } | |
| if (record.data['delete']) { | |
| pendingdelete = '<div style="text-decoration: line-through;">'+ current +'</div>'; | |
| } | |
| if (pending || pendingdelete) { | |
| return current + '<div style="color:red">' + (pending || '') + pendingdelete + '</div>'; | |
| } else { | |
| return current; | |
| } | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| var rows = me.rows; | |
| if (!me.rstore) { | |
| if (!me.url) { | |
| throw "no url specified"; | |
| } | |
| me.rstore = Ext.create('PVE.data.ObjectStore', { | |
| model: 'KeyValuePendingDelete', | |
| readArray: true, | |
| url: me.url, | |
| interval: me.interval, | |
| extraParams: me.extraParams, | |
| rows: me.rows | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.grid.ResourceGrid', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveResourceGrid'], | |
| border: false, | |
| saveCurrentState: function(){ | |
| var me = this; | |
| me.saveState(); | |
| }, | |
| defaultSorter: { | |
| property: 'type', | |
| direction: 'ASC' | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| var rstore = PVE.data.ResourceStore; | |
| var sp = Ext.state.Manager.getProvider(); | |
| var coldef = rstore.defaultColums(); | |
| 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: 'resourcegrid', | |
| tbar: [ | |
| { | |
| xtype: 'button', | |
| itemId: 'resetbutton', | |
| tooltip: gettext("Reset Columns"), | |
| iconCls: 'fa fa-fw fa-columns x-button-reset', | |
| handler: function(button, e, opts) { | |
| var sp = Ext.state.Manager.getProvider(); | |
| me.getStore().sort(me.defaultSorter); | |
| me.reconfigure(null, PVE.data.ResourceStore.defaultColums()); | |
| sp.clear(me.stateId); | |
| } | |
| }, | |
| '->', | |
| 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); | |
| }, | |
| columnschanged: 'saveCurrentState', | |
| columnresize: 'saveCurrentState', | |
| columnmove: 'saveCurrentState', | |
| sortchange: 'saveCurrentState' | |
| }, | |
| columns: coldef | |
| }); | |
| me.callParent(); | |
| updateGrid(); | |
| rstore.on("load", load_cb); | |
| } | |
| }); | |
| // Ext.create is a function | |
| // but we defined create as a bool in PVE.window.Edit | |
| /*jslint confusion: true*/ | |
| Ext.define('PVE.pool.AddVM', { | |
| extend: 'PVE.window.Edit', | |
| width: 600, | |
| height: 400, | |
| isAdd: true, | |
| create: true, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| 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 PVE.Utils.runningText; | |
| } else { | |
| return PVE.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: 'PVE.window.Edit', | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.pool) { | |
| throw "no pool specified"; | |
| } | |
| me.create = true; | |
| me.isAdd = true; | |
| me.url = "/pools/" + me.pool; | |
| me.method = 'PUT'; | |
| Ext.apply(me, { | |
| subject: gettext('Storage'), | |
| width: 350, | |
| items: [ | |
| { | |
| xtype: 'PVE.form.StorageSelector', | |
| 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 ? | |
| 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: 'pve', | |
| root: 'data.members', | |
| url: "/api2/json/pools/" + me.pool | |
| } | |
| }); | |
| var coldef = PVE.data.ResourceStore.defaultColums(); | |
| var reload = function() { | |
| store.load(); | |
| }; | |
| var sm = Ext.create('Ext.selection.RowModel', {}); | |
| var remove_btn = new PVE.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"; | |
| } | |
| PVE.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, | |
| activate: reload | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.form.FWMacroSelector', { | |
| extend: 'PVE.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: 'pve', | |
| url: "/api2/json/cluster/firewall/macros" | |
| }, | |
| sorters: { | |
| property: 'macro', | |
| order: 'DESC' | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.FirewallRulePanel', { | |
| extend: 'PVE.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'], 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: 'pveKVComboBox', | |
| name: 'type', | |
| value: 'in', | |
| comboItems: [['in', 'in'], ['out', 'out']], | |
| fieldLabel: gettext('Direction'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'pveKVComboBox', | |
| name: 'action', | |
| value: 'ACCEPT', | |
| comboItems: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']], | |
| fieldLabel: gettext('Action'), | |
| allowBlank: false | |
| } | |
| ]; | |
| if (me.allow_iface) { | |
| me.column1.push({ | |
| xtype: 'pvetextfield', | |
| name: 'iface', | |
| deleteEmpty: !me.create, | |
| 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: 'pvecheckbox', | |
| 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.columnB = [ | |
| { | |
| xtype: 'textfield', | |
| name: 'comment', | |
| value: '', | |
| fieldLabel: gettext('Comment') | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.FirewallRuleEdit', { | |
| extend: 'PVE.window.Edit', | |
| base_url: undefined, | |
| list_refs_url: undefined, | |
| allow_iface: false, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.base_url) { | |
| throw "no base_url specified"; | |
| } | |
| if (!me.list_refs_url) { | |
| throw "no list_refs_url specified"; | |
| } | |
| me.create = (me.rule_pos === undefined); | |
| if (me.create) { | |
| 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', { | |
| create: me.create, | |
| 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.create) { | |
| 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); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.FirewallGroupRuleEdit', { | |
| extend: 'PVE.window.Edit', | |
| base_url: undefined, | |
| allow_iface: false, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| me.create = (me.rule_pos === undefined); | |
| if (me.create) { | |
| 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: 'pvetextfield', | |
| name: 'iface', | |
| deleteEmpty: !me.create, | |
| value: '', | |
| fieldLabel: gettext('Interface') | |
| }); | |
| } | |
| var ipanel = Ext.create('PVE.panel.InputPanel', { | |
| create: me.create, | |
| column1: column1, | |
| column2: [ | |
| { | |
| xtype: 'pvecheckbox', | |
| 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.create) { | |
| 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', | |
| 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); | |
| if (me.groupBtn) { | |
| me.groupBtn.setDisabled(false); | |
| } | |
| me.store.setProxy({ | |
| type: 'pve', | |
| url: '/api2/json' + url | |
| }); | |
| me.store.load(); | |
| } | |
| }, | |
| moveRule: function(from, to) { | |
| var me = this; | |
| if (!me.base_url) { | |
| return; | |
| } | |
| PVE.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; | |
| PVE.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(); | |
| } | |
| }); | |
| }, | |
| deleteRule: function(rule) { | |
| var me = this; | |
| if (!me.base_url) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: me.base_url + '/' + rule.pos.toString() + | |
| '?digest=' + encodeURIComponent(rule.digest), | |
| method: 'DELETE', | |
| 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('PVE.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(); | |
| } | |
| }); | |
| 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('PVE.button.Button',{ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| return; | |
| } | |
| me.deleteRule(rec.data); | |
| } | |
| }); | |
| var tbar = me.tbar_prefix ? [ me.tbar_prefix ] : []; | |
| tbar.push(me.addBtn); | |
| 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 = 'x-form-invalid-field'; | |
| var html = '<p>' + Ext.htmlEncode(errors[name]) + '</p>'; | |
| metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + | |
| html.replace(/\"/g,'"') + '"'; | |
| } | |
| return value; | |
| }; | |
| var columns = [ | |
| { | |
| // similar to xtype: 'rownumberer', | |
| dataIndex: 'pos', | |
| resizable: false, | |
| width: 23, | |
| sortable: false, | |
| align: 'right', | |
| hideable: false, | |
| menuDisabled: true, | |
| renderer: function(value, metaData, record, rowIdx, colIdx, store) { | |
| metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; | |
| if (value >= 0) { | |
| return value; | |
| } | |
| return ''; | |
| } | |
| }, | |
| { | |
| xtype: 'checkcolumn', | |
| header: gettext('Enable'), | |
| dataIndex: 'enable', | |
| listeners: { | |
| checkchange: function(column, recordIndex, checked) { | |
| var record = me.getStore().getData().items[recordIndex]; | |
| record.commit(); | |
| var data = {}; | |
| Ext.Array.forEach(record.getFields(), function(field) { | |
| data[field.name] = record.get(field.name); | |
| }); | |
| if (!me.allow_iface || !data.iface) { | |
| delete data.iface; | |
| } | |
| me.updateRule(data); | |
| } | |
| }, | |
| width: 50 | |
| }, | |
| { | |
| header: gettext('Type'), | |
| dataIndex: 'type', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('type', value, metaData, record); | |
| }, | |
| width: 50 | |
| }, | |
| { | |
| header: gettext('Action'), | |
| dataIndex: 'action', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('action', value, metaData, record); | |
| }, | |
| width: 80 | |
| }, | |
| { | |
| header: gettext('Macro'), | |
| dataIndex: 'macro', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('macro', value, metaData, record); | |
| }, | |
| width: 80 | |
| } | |
| ]; | |
| if (me.allow_iface) { | |
| columns.push({ | |
| header: gettext('Interface'), | |
| dataIndex: 'iface', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('iface', value, metaData, record); | |
| }, | |
| width: 80 | |
| }); | |
| } | |
| columns.push( | |
| { | |
| header: gettext('Source'), | |
| dataIndex: 'source', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('source', value, metaData, record); | |
| }, | |
| width: 100 | |
| }, | |
| { | |
| header: gettext('Destination'), | |
| dataIndex: 'dest', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('dest', value, metaData, record); | |
| }, | |
| width: 100 | |
| }, | |
| { | |
| header: gettext('Protocol'), | |
| dataIndex: 'proto', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('proto', value, metaData, record); | |
| }, | |
| width: 100 | |
| }, | |
| { | |
| header: gettext('Dest. port'), | |
| dataIndex: 'dport', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('dport', value, metaData, record); | |
| }, | |
| width: 100 | |
| }, | |
| { | |
| header: gettext('Source port'), | |
| dataIndex: 'sport', | |
| renderer: function(value, metaData, record) { | |
| return render_errors('sport', value, metaData, record); | |
| }, | |
| width: 100 | |
| }, | |
| { | |
| header: gettext('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: 'PVE.window.Edit', | |
| base_url: undefined, | |
| alias_name: undefined, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| me.create = (me.alias_name === undefined); | |
| if (me.create) { | |
| 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.create ? '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('PVE.panel.InputPanel', { | |
| create: me.create, | |
| items: items | |
| }); | |
| Ext.apply(me, { | |
| subject: gettext('Alias'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| me.load({ | |
| success: function(response, options) { | |
| var values = response.result.data; | |
| values.rename = values.name; | |
| ipanel.setValues(values); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.FirewallAliases', { | |
| extend: 'Ext.grid.Panel', | |
| alias: ['widget.pveFirewallAliases'], | |
| base_url: undefined, | |
| title: gettext('Alias'), | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.base_url) { | |
| throw "missing base_url configuration"; | |
| } | |
| var store = new Ext.data.Store({ | |
| fields: [ 'name', 'cidr', 'comment', 'digest' ], | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json" + me.base_url | |
| }, | |
| idProperty: 'name', | |
| 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 PVE.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 = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: me.base_url + '/' + rec.data.name, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| failure: function(response, options) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| }, | |
| 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('show', reload); | |
| } | |
| }); | |
| Ext.define('PVE.FirewallOptions', { | |
| extend: 'PVE.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"; | |
| } | |
| var rows = {}; | |
| var add_boolean_row = function(name, text, defaultValue, labelWidth) { | |
| rows[name] = { | |
| header: text, | |
| required: true, | |
| defaultValue: defaultValue || 0, | |
| renderer: PVE.Utils.format_boolean, | |
| editor: { | |
| xtype: 'pveWindowEdit', | |
| subject: text, | |
| fieldDefaults: { labelWidth: labelWidth || 100 }, | |
| items: { | |
| xtype: 'pvecheckbox', | |
| defaultValue: defaultValue || 0, | |
| checked: defaultValue ? true : false, | |
| name: name, | |
| uncheckedValue: 0, | |
| fieldLabel: text | |
| } | |
| } | |
| }; | |
| }; | |
| var add_integer_row = function(name, text, labelWidth, minValue) { | |
| rows[name] = { | |
| header: text, | |
| required: true, | |
| renderer: function(value) { | |
| return value || PVE.Utils.defaultText; | |
| }, | |
| editor: { | |
| xtype: 'pveWindowEdit', | |
| subject: text, | |
| fieldDefaults: { labelWidth: labelWidth || 100 }, | |
| items: { | |
| xtype: 'numberfield', | |
| name: name, | |
| minValue: minValue, | |
| decimalPrecision: 0, | |
| fieldLabel: text, | |
| emptyText: gettext('Default'), | |
| getSubmitData: function() { | |
| var me = this; | |
| var val = me.getSubmitValue(); | |
| if (val !== null && val !== '') { | |
| var data = {}; | |
| data[name] = val; | |
| return data; | |
| } else { | |
| return { 'delete' : name }; | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| }; | |
| var add_log_row = function(name, labelWidth) { | |
| rows[name] = { | |
| header: name, | |
| required: true, | |
| defaultValue: 'nolog', | |
| editor: { | |
| xtype: 'pveWindowEdit', | |
| subject: name, | |
| fieldDefaults: { labelWidth: labelWidth || 100 }, | |
| items: { | |
| xtype: 'pveKVComboBox', | |
| name: name, | |
| fieldLabel: name, | |
| comboItems: [['nolog', 'nolog'], ['info', 'info'], ['err', 'err'], | |
| ['warning', 'warning'], ['crit', 'crit'], ['alert', 'alert'], | |
| ['emerg', 'emerg'], ['debug', 'debug']] | |
| } | |
| } | |
| }; | |
| }; | |
| if (me.fwtype === 'node') { | |
| add_boolean_row('enable', gettext('Enable Firewall'), 1); | |
| add_boolean_row('nosmurfs', gettext('SMURFS filter'), 1); | |
| add_boolean_row('tcpflags', gettext('TCP flags filter'), 0); | |
| add_boolean_row('ndp', gettext('Enable NDP'), 1); | |
| add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 120, 32768); | |
| add_integer_row('nf_conntrack_tcp_timeout_established', | |
| 'nf_conntrack_tcp_timeout_established', 250, 7875); | |
| 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') { | |
| add_boolean_row('enable', gettext('Enable Firewall'), 0); | |
| add_boolean_row('dhcp', gettext('Enable DHCP'), 0); | |
| add_boolean_row('ndp', gettext('Enable NDP'), 1); | |
| add_boolean_row('radv', gettext('Allow 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('Enable Firewall'), 0); | |
| } | |
| if (me.fwtype === 'dc' || me.fwtype === 'vm') { | |
| rows.policy_in = { | |
| header: gettext('Input Policy'), | |
| required: true, | |
| defaultValue: 'DROP', | |
| editor: { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Input Policy'), | |
| items: { | |
| xtype: 'pveFirewallPolicySelector', | |
| name: 'policy_in', | |
| value: 'DROP', | |
| fieldLabel: gettext('Input Policy') | |
| } | |
| } | |
| }; | |
| rows.policy_out = { | |
| header: gettext('Output Policy'), | |
| required: true, | |
| defaultValue: 'ACCEPT', | |
| editor: { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Output Policy'), | |
| items: { | |
| xtype: 'pveFirewallPolicySelector', | |
| name: 'policy_out', | |
| value: 'ACCEPT', | |
| fieldLabel: gettext('Output Policy') | |
| } | |
| } | |
| }; | |
| } | |
| var reload = function() { | |
| me.rstore.load(); | |
| }; | |
| var run_editor = function() { | |
| var sm = me.getSelectionModel(); | |
| 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' + me.base_url | |
| }); | |
| } else { | |
| var config = Ext.apply({ | |
| pveSelNode: me.pveSelNode, | |
| confid: rec.data.key, | |
| url: '/api2/extjs' + me.base_url | |
| }, rowdef.editor); | |
| win = Ext.createWidget(rowdef.editor.xtype, config); | |
| win.load(); | |
| } | |
| win.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var edit_btn = new Ext.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| 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" + me.base_url, | |
| tbar: [ edit_btn ], | |
| rows: rows, | |
| listeners: { | |
| itemdblclick: run_editor, | |
| selectionchange: set_button_status | |
| } | |
| }); | |
| me.callParent(); | |
| me.on('activate', reload); | |
| } | |
| }); | |
| /* | |
| * Left Treepanel, containing all the ressources we manage in this datacenter: server nodes, server storages, VMs and Containers | |
| */ | |
| Ext.define('PVE.tree.ResourceTree', { | |
| extend: 'Ext.tree.TreePanel', | |
| alias: ['widget.pveResourceTree'], | |
| statics: { | |
| typeDefaults: { | |
| node: { | |
| iconCls: 'fa fa-building x-fa-tree', | |
| text: gettext('Nodes') | |
| }, | |
| pool: { | |
| iconCls: 'fa fa-tags fa-dark x-fa-tree', | |
| text: gettext('Resource Pool') | |
| }, | |
| storage: { | |
| iconCls: 'fa fa-database fa-dark x-fa-tree', | |
| text: gettext('Storage') | |
| }, | |
| qemu: { | |
| iconCls: 'fa fa-desktop x-fa-tree', | |
| text: gettext('Virtual Machine') | |
| }, | |
| lxc: { | |
| //iconCls: 'x-tree-node-lxc', | |
| iconCls: 'fa fa-cube x-fa-tree', | |
| text: gettext('LXC Container') | |
| }, | |
| template: { | |
| iconCls: 'fa fa-file-o fa-dark x-fa-tree-template' | |
| }, | |
| datacenter: { | |
| iconCls: 'fa fa-server x-fa-tree-datacenter' | |
| } | |
| } | |
| }, | |
| 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.text > n2.text ? 1 : (n1.text < n2.text ? -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 defaults = PVE.tree.ResourceTree.typeDefaults[info.type]; | |
| if (info.id === 'root') { | |
| defaults = PVE.tree.ResourceTree.typeDefaults.datacenter; | |
| } else if (info.type === 'type') { | |
| defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid]; | |
| } | |
| if (defaults && defaults.iconCls) { | |
| var iconClsAdd = ''; | |
| if (info.running && info.type === 'node') { | |
| iconClsAdd = '-online'; | |
| } else if (info.running) { | |
| iconClsAdd = '-running'; | |
| if (info.status === 'paused') { | |
| iconClsAdd = '-paused'; | |
| } | |
| } else if (info.type === 'lxc' || info.type === 'qemu') { | |
| iconClsAdd = '-stopped'; | |
| } else if (info.type === 'node') { | |
| iconClsAdd = '-offline'; | |
| } | |
| info.iconCls = defaults.iconCls + iconClsAdd; | |
| if (info.template) { | |
| iconClsAdd = '-template'; | |
| info.iconCls = PVE.tree.ResourceTree.typeDefaults.template.iconCls + '-' + info.type; | |
| } | |
| } | |
| }, | |
| // private | |
| addChildSorted: function(node, info) { | |
| var me = this; | |
| me.setIconCls(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); | |
| // fixme: remove when EXTJS has fixed those bugs?! | |
| group.expand(); group.collapse(); | |
| } | |
| 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') | |
| } | |
| }); | |
| var stateid = 'rid'; | |
| var updateTree = function() { | |
| var tmp; | |
| // fixme: suspend events ? | |
| var rootnode = me.store.getRootNode(); | |
| me.setIconCls(rootnode.data); | |
| // remember selected node (and all parents) | |
| var sm = me.getSelectionModel(); | |
| var lastsel = sm.getSelection()[0]; | |
| var parents = []; | |
| var p = lastsel; | |
| while (p && !!(p = p.parentNode)) { | |
| parents.push(p); | |
| } | |
| var index = pdata.dataIndex; | |
| var groups = me.viewFilter.groups || []; | |
| var filterfn = me.viewFilter.filterfn; | |
| // remove vanished or moved items | |
| // update in place changed items | |
| var key; | |
| for (key in index) { | |
| if (index.hasOwnProperty(key)) { | |
| var olditem = index[key]; | |
| // getById() use find(), which is slow (ExtJS4 DP5) | |
| //var item = rstore.getById(olditem.data.id); | |
| var item = rstore.data.get(olditem.data.id); | |
| var changed = false; | |
| var moved = false; | |
| if (item) { | |
| // test if any grouping attributes changed | |
| // this will also catch migrated nodes | |
| // in server view | |
| var i, len; | |
| for (i = 0, len = groups.length; i < len; i++) { | |
| var attr = groups[i]; | |
| if (item.data[attr] != olditem.data[attr]) { | |
| //console.log("changed " + attr); | |
| moved = true; | |
| break; | |
| } | |
| } | |
| // explicitely check for node, since | |
| // in some views, node is not a grouping | |
| // attribute | |
| if (!moved && item.data.node !== olditem.data.node) { | |
| moved = true; | |
| } | |
| // tree item has been updated | |
| if ((item.data.text !== olditem.data.text) || | |
| (item.data.running !== olditem.data.running) || | |
| (item.data.template !== olditem.data.template) || | |
| (item.data.status !== olditem.data.status)) { | |
| //console.log("changed node/text/running " + olditem.data.id); | |
| changed = true; | |
| } | |
| // fixme: also test filterfn()? | |
| } | |
| if (changed) { | |
| olditem.beginEdit(); | |
| //console.log("REM UPDATE UID: " + key + " ITEM " + item.data.running); | |
| var info = olditem.data; | |
| Ext.apply(info, item.data); | |
| me.setIconCls(info); | |
| olditem.commit(); | |
| } | |
| if ((!item || moved) && olditem.isLeaf()) { | |
| //console.log("REM UID: " + key + " ITEM " + olditem.data.id); | |
| delete index[key]; | |
| var parentNode = olditem.parentNode; | |
| 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; | |
| } | |
| }); | |
| // 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); | |
| } | |
| // on first tree load set the selection from the stateful provider | |
| if (!pdata.updateCount) { | |
| rootnode.collapse(); | |
| 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) { | |
| // disable selection when right clicking | |
| me.allowSelection = (ev.button !== 2); | |
| }, | |
| beforeselect: function (tree, record, index, eopts) { | |
| var allow = me.allowSelection; | |
| me.allowSelection = true; | |
| return allow; | |
| } | |
| }, | |
| setViewFilter: function(view) { | |
| me.viewFilter = view; | |
| me.clearTree(); | |
| updateTree(); | |
| }, | |
| 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(); | |
| } | |
| } | |
| } | |
| }, | |
| 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); | |
| } | |
| }, | |
| checkVmMigration: function(record) { | |
| if (!(record.data.type === 'qemu' || record.data.type === 'lxc')) { | |
| throw "not a vm type"; | |
| } | |
| var rootnode = me.store.getRootNode(); | |
| var node = rootnode.findChild('id', record.data.id, true); | |
| if (node && node.data.type === record.data.type && | |
| node.data.node !== record.data.node) { | |
| // defer select (else we get strange errors) | |
| Ext.defer(function() { me.selectExpand(node); }, 100, me); | |
| } | |
| }, | |
| 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.IPSetList', { | |
| extend: 'Ext.grid.Panel', | |
| alias: 'widget.pveIPSetList', | |
| ipset_panel: undefined, | |
| base_url: undefined, | |
| addBtn: undefined, | |
| removeBtn: undefined, | |
| editBtn: undefined, | |
| initComponent: function() { | |
| /*jslint confusion: true */ | |
| 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({ | |
| fields: [ 'name', 'comment', 'digest' ], | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json" + me.base_url | |
| }, | |
| idProperty: 'name', | |
| 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('PVE.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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| me.addBtn = new PVE.button.Button({ | |
| text: gettext('Create'), | |
| handler: function() { | |
| sm.deselectAll(); | |
| var win = Ext.create('PVE.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 = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec || !me.base_url) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: me.base_url + '/' + rec.data.name, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| failure: function(response, options) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| }, | |
| 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: 'PVE.window.Edit', | |
| cidr: undefined, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| me.create = (me.cidr === undefined); | |
| if (me.create) { | |
| 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.create) { | |
| 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('PVE.panel.InputPanel', { | |
| create: me.create, | |
| column1: column1, | |
| column2: [ | |
| { | |
| xtype: 'pvecheckbox', | |
| 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.create) { | |
| 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', | |
| 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.store.setProxy({ | |
| type: 'pve', | |
| 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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| me.addBtn = new PVE.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 = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec || !me.base_url) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: me.base_url + '/' + rec.data.cidr, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| failure: function(response, options) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| }, | |
| 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 = 'x-form-invalid-field'; | |
| var html = '<p>' + Ext.htmlEncode(msg) + '</p>'; | |
| metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + | |
| html.replace(/\"/g,'"') + '"'; | |
| } | |
| } | |
| return value; | |
| }; | |
| Ext.apply(me, { | |
| tbar: [ '<b>IP/CIDR:</b>', me.addBtn, me.removeBtn, me.editBtn ], | |
| store: store, | |
| selModel: sm, | |
| listeners: { | |
| itemdblclick: run_editor | |
| }, | |
| columns: [ | |
| { | |
| xtype: 'rownumberer' | |
| }, | |
| { | |
| header: gettext('IP/CIDR'), | |
| dataIndex: 'cidr', | |
| width: 150, | |
| renderer: function(value, metaData, record) { | |
| value = render_errors(value, metaData, record); | |
| if (record.data.nomatch) { | |
| return '<b>! </b>' + value; | |
| } | |
| return value; | |
| } | |
| }, | |
| { | |
| header: gettext('Comment'), | |
| dataIndex: 'comment', | |
| flex: 1, | |
| renderer: function(value) { | |
| return Ext.util.Format.htmlEncode(value); | |
| } | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| if (me.base_url) { | |
| me.setBaseUrl(me.base_url); // load | |
| } | |
| } | |
| }, function() { | |
| Ext.define('pve-ipset', { | |
| extend: 'Ext.data.Model', | |
| fields: [ { name: 'nomatch', type: 'boolean' }, | |
| 'cidr', 'comment', 'errors' ], | |
| idProperty: 'cidr' | |
| }); | |
| }); | |
| Ext.define('PVE.IPSet', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pveIPSet', | |
| title: 'IPSet', | |
| 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, | |
| flex: 0.5, | |
| border: false | |
| }); | |
| var ipset_list = Ext.createWidget('pveIPSetList', { | |
| region: 'west', | |
| ipset_panel: ipset_panel, | |
| base_url: me.base_url, | |
| flex: 0.5, | |
| 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 | |
| */ | |
| Ext.define('PVE.panel.Config', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pvePanelConfig', | |
| showSearch: true, // add a ressource grid with a search button as first tab | |
| viewFilter: undefined, // a filter to pass to that ressource grid | |
| initComponent: function() { | |
| var me = this; | |
| var stateid = me.hstateid; | |
| var sp = Ext.state.Manager.getProvider(); | |
| var activeTab; // leaving this undefined means items[0] will be the default tab | |
| var hsregex = /^([^\-\s]+)(-\S+)?$/; | |
| if (stateid) { | |
| var state = sp.get(stateid); | |
| if (state && state.value) { | |
| var res = hsregex.exec(state.value); | |
| if (res && res[1] && Ext.isArray(me.items)) { | |
| me.items.forEach(function(item) { | |
| if (item.itemId === res[1]) { | |
| activeTab = res[1]; | |
| } | |
| }); | |
| } else if (res && res[1] && me.items && me.items.itemId === res[1]) { | |
| activeTab = res[1]; | |
| } | |
| } | |
| } | |
| var items = me.items || []; | |
| me.items = undefined; | |
| var tbar = me.tbar || []; | |
| me.tbar = undefined; | |
| var title = me.title || me.pveSelNode.data.text; | |
| me.title = undefined; | |
| tbar.unshift('->'); | |
| tbar.unshift({ | |
| xtype: 'tbtext', | |
| text: title, | |
| baseCls: 'x-panel-header-text' | |
| }); | |
| if (me.showSearch) { | |
| items.unshift({ | |
| itemId: 'search', | |
| title: gettext('Search'), | |
| layout: { type:'fit' }, | |
| plugins: [{ | |
| ptype: 'lazyitems', | |
| items: [{ | |
| xtype: 'pveResourceGrid', | |
| pveSelNode: me.pveSelNode | |
| }] | |
| }] | |
| }); | |
| } | |
| var toolbar = Ext.create('Ext.toolbar.Toolbar', { | |
| items: tbar, | |
| border: false, | |
| height: 36 | |
| }); | |
| var tab = Ext.create('Ext.tab.Panel', { | |
| flex: 1, | |
| border: true, | |
| activeTab: activeTab, | |
| defaults: Ext.apply(me.defaults || {}, { | |
| pveSelNode: me.pveSelNode, | |
| viewFilter: me.viewFilter, | |
| workspace: me.workspace, | |
| border: false | |
| }), | |
| items: items, | |
| listeners: { | |
| tabchange: function(tp, newcard, oldcard) { | |
| var ntab = newcard.itemId; | |
| // Note: '' is alias for first tab. | |
| // First tab can be 'search' or something else | |
| if (newcard.itemId === items[0].itemId) { | |
| ntab = ''; | |
| } | |
| if (stateid) { | |
| if (newcard.phstateid) { | |
| sp.set(newcard.phstateid, newcard.getHState()); | |
| } else { | |
| sp.set(stateid, { value: ntab }); | |
| } | |
| } | |
| // if we have a tabpanel which we declared lazy (with ptype: lazyitems) | |
| // then we have the actual item in items.items[0] | |
| // and there we need to fire the event hide | |
| // because some tabs use this event (which is not fired in this case) | |
| if (oldcard.plugins && oldcard.plugins[0] && oldcard.plugins[0].ptype == 'lazyitems') { | |
| oldcard.items.items[0].fireEvent('hide'); | |
| } | |
| } | |
| } | |
| }); | |
| Ext.apply(me, { | |
| layout: { type: 'vbox', align: 'stretch' }, | |
| items: [ toolbar, tab] | |
| }); | |
| me.callParent(); | |
| var statechange = function(sp, key, state) { | |
| if (stateid && (key === stateid) && state) { | |
| var atab = tab.getActiveTab().itemId; | |
| var res = hsregex.exec(state.value); | |
| var ntab = (res && res[1]) ? res[1] : items[0].itemId; | |
| if (ntab && (atab != ntab)) { | |
| tab.setActiveTab(ntab); | |
| } | |
| } | |
| }; | |
| if (stateid) { | |
| me.mon(sp, 'statechange', statechange); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.panel.SubConfig', { | |
| extend: 'Ext.tab.Panel', | |
| alias: ['widget.pvePanelSubConfig'], | |
| configPrefix: undefined, | |
| tabPosition: 'left', | |
| tabRotation: 0, | |
| savedTab: undefined, | |
| getHState: function(itemId) { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!itemId) { | |
| if (me.getActiveTab() === undefined) { | |
| me.setActiveTab(0); | |
| } | |
| itemId = me.getActiveTab().itemId; | |
| } | |
| var first = me.items.get(0); | |
| var ntab; | |
| // Note: '' is alias for first tab. | |
| if (itemId === first.itemId) { | |
| ntab = me.configPrefix; | |
| } else { | |
| ntab = me.configPrefix + '-' + itemId; | |
| } | |
| return { value: ntab }; | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| if (!me.phstateid) { | |
| throw "no parent history state specified"; | |
| } | |
| var sp = Ext.state.Manager.getProvider(); | |
| var state = sp.get(me.phstateid); | |
| var hsregex = /^([^\-\s]+)-(\S+)?$/; | |
| if (state && state.value) { | |
| var res = hsregex.exec(state.value); | |
| if (res && res[1] && res[2] && res[1] === me.configPrefix) { | |
| me.activeTab = res[2]; // if we have lazy items, this does nothing | |
| me.savedTab = res[2]; // so we save it here | |
| } | |
| } | |
| Ext.apply(me, { | |
| listeners: { | |
| afterrender: function(tp) { | |
| // if we have lazy items, | |
| // we saved the tabname in savedTab | |
| if (me.activeTab === undefined && me.savedTab) { | |
| me.setActiveTab(me.savedTab); | |
| } else if (me.activeTab === undefined) { | |
| // if we have nothing else, we set 0 as active | |
| me.setActiveTab(0); | |
| } | |
| }, | |
| tabchange: function(tp, newcard, oldcard) { | |
| var state = me.getHState(newcard.itemId); | |
| sp.set(me.phstateid, state); | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| var statechange = function(sp, key, state) { | |
| if ((key === me.phstateid) && state) { | |
| var first = me.items.get(0); | |
| var atab = me.getActiveTab().itemId; | |
| var res = hsregex.exec(state.value); | |
| var ntab = (res && res[1]) ? res[1] : first.itemId; | |
| if (ntab && (atab != ntab)) { | |
| me.setActiveTab(ntab); | |
| } | |
| } | |
| }; | |
| me.mon(sp, 'statechange', statechange); | |
| } | |
| }); | |
| Ext.define('PVE.grid.BackupView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveBackupView'], | |
| 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 filterFn; | |
| if (vmtype === 'openvz') { | |
| filterFn = function(item) { | |
| return item.data.volid.match(':backup/vzdump-openvz-'); | |
| }; | |
| } else if (vmtype === 'lxc') { | |
| filterFn = function(item) { | |
| return item.data.volid.match(':backup/vzdump-lxc-'); | |
| }; | |
| } else if (vmtype === 'qemu') { | |
| filterFn = function(item) { | |
| return item.data.volid.match(':backup/vzdump-qemu-'); | |
| }; | |
| } else { | |
| throw "unsupported VM type '" + vmtype + "'"; | |
| } | |
| me.store = Ext.create('Ext.data.Store', { | |
| model: 'pve-storage-content', | |
| sorters: { | |
| property: 'volid', | |
| order: 'DESC' | |
| }, | |
| filters: { filterFn: filterFn } | |
| }); | |
| var reload = Ext.Function.createBuffered(function() { | |
| if (me.store && me.store.proxy.url) { | |
| me.store.load(); | |
| } | |
| }, 100); | |
| var setStorage = function(storage) { | |
| var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content'; | |
| url += '?content=backup'; | |
| me.store.setProxy({ | |
| type: 'pve', | |
| 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, | |
| listeners: { | |
| buffer: 500, | |
| keyup: function(field) { | |
| me.store.clearFilter(true); | |
| me.store.filter([ | |
| filterFn, | |
| { | |
| property: 'volid', | |
| value: field.getValue(), | |
| anyMatch: true, | |
| caseSensitive: false | |
| } | |
| ]); | |
| } | |
| } | |
| }); | |
| 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() | |
| }); | |
| win.show(); | |
| } | |
| }); | |
| var restore_btn = Ext.create('PVE.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('PVE.button.Button', { | |
| text: gettext('Remove'), | |
| disabled: true, | |
| 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; | |
| }, | |
| enableFn: function(rec) { | |
| return !!rec; | |
| }, | |
| handler: function(b, e, rec){ | |
| var storage = storagesel.getValue(); | |
| if (!storage) { | |
| return; | |
| } | |
| var volid = rec.data.volid; | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + nodename + "/storage/" + storage + "/content/" + volid, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| }, | |
| success: function(response, options) { | |
| reload(); | |
| } | |
| }); | |
| } | |
| }); | |
| var config_btn = Ext.create('PVE.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('Ext.window.Window', { | |
| title: gettext('Configuration'), | |
| width: 600, | |
| height: 400, | |
| layout: 'fit', | |
| modal: true, | |
| items: [{ | |
| itemId: 'configtext', | |
| autoScroll: true, | |
| style: { | |
| 'background-color': 'white', | |
| 'white-space': 'pre', | |
| 'font-family': 'monospace', | |
| padding: '5px' | |
| } | |
| }] | |
| }); | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + nodename + "/vzdump/extractconfig", | |
| method: 'GET', | |
| params: { | |
| volume: rec.data.volid | |
| }, | |
| failure: function(response, opts) { | |
| win.close(); | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| }, | |
| success: function(response,options) { | |
| win.show(); | |
| win.down('#configtext').update(Ext.htmlEncode(response.result.data)); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| stateful: false, | |
| 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: PVE.Utils.format_size, | |
| dataIndex: 'size' | |
| } | |
| ], | |
| listeners: { | |
| activate: reload | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| /* | |
| * Display log entries in a panel with scrollbar | |
| * The log entries are automatically refreshed via a background task, | |
| * with newest entries comming at the bottom | |
| */ | |
| Ext.define('PVE.panel.LogView', { | |
| extend: 'Ext.panel.Panel', | |
| alias: ['widget.pveLogView'], | |
| pageSize: 500, | |
| lineHeight: 16, | |
| viewInfo: undefined, | |
| scrollToEnd: true, | |
| autoScroll: true, | |
| layout: 'auto', | |
| bodyPadding: 5, | |
| getMaxDown: function(scrollToEnd) { | |
| var me = this; | |
| var target = me.getTargetEl(); | |
| var dom = target.dom; | |
| if (scrollToEnd) { | |
| dom.scrollTop = dom.scrollHeight - dom.clientHeight; | |
| } | |
| var maxDown = dom.scrollHeight - dom.clientHeight - | |
| dom.scrollTop; | |
| return maxDown; | |
| }, | |
| updateView: function(start, end, total, text) { | |
| var me = this; | |
| var el = me.dataCmp.el; | |
| if (me.destroyed) { // return if element is not there anymore | |
| return; | |
| } | |
| if (me.viewInfo && me.viewInfo.start === start && | |
| me.viewInfo.end === end && me.viewInfo.total === total && | |
| me.viewInfo.textLength === text.length) { | |
| return; // same content | |
| } | |
| var maxDown = me.getMaxDown(); | |
| var scrollToEnd = (maxDown <= 0) && me.scrollToEnd; | |
| el.setStyle('padding-top', (start*me.lineHeight).toString() + 'px'); | |
| el.update(text); | |
| me.dataCmp.setHeight(total*me.lineHeight); | |
| if (scrollToEnd) { | |
| me.getMaxDown(true); | |
| } | |
| me.viewInfo = { | |
| start: start, | |
| end: end, | |
| total: total, | |
| textLength: text.length | |
| }; | |
| }, | |
| doAttemptLoad: function(start) { | |
| var me = this; | |
| var req_params = { | |
| start: start, | |
| limit: me.pageSize | |
| }; | |
| if (me.log_select_timespan) { | |
| // always show log until the end of the selected day | |
| req_params.until = Ext.Date.format(me.until_date, 'Y-m-d') + ' 23:59:59'; | |
| req_params.since = Ext.Date.format(me.since_date, 'Y-m-d'); | |
| } | |
| PVE.Utils.API2Request({ | |
| url: me.url, | |
| params: req_params, | |
| method: 'GET', | |
| success: function(response) { | |
| PVE.Utils.setErrorMask(me, false); | |
| var list = response.result.data; | |
| var total = response.result.total; | |
| var first = 0, last = 0; | |
| var text = ''; | |
| Ext.Array.each(list, function(item) { | |
| if (!first|| item.n < first) { | |
| first = item.n; | |
| } | |
| if (!last || item.n > last) { | |
| last = item.n; | |
| } | |
| text = text + Ext.htmlEncode(item.t) + "<br>"; | |
| }); | |
| if (first && last && total) { | |
| me.updateView(first -1 , last -1, total, text); | |
| } else { | |
| me.updateView(0, 0, 0, ''); | |
| } | |
| }, | |
| failure: function(response) { | |
| var msg = response.htmlStatus; | |
| PVE.Utils.setErrorMask(me, msg); | |
| } | |
| }); | |
| }, | |
| attemptLoad: function(start) { | |
| var me = this; | |
| if (!me.loadTask) { | |
| me.loadTask = Ext.create('Ext.util.DelayedTask', me.doAttemptLoad, me, []); | |
| } | |
| me.loadTask.delay(200, me.doAttemptLoad, me, [start]); | |
| }, | |
| requestUpdate: function(top, force) { | |
| var me = this; | |
| if (top === undefined) { | |
| var target = me.getTargetEl(); | |
| top = target.dom.scrollTop; | |
| } | |
| var viewStart = parseInt((top / me.lineHeight) - 1, 10); | |
| if (viewStart < 0) { | |
| viewStart = 0; | |
| } | |
| var viewEnd = parseInt(((top + me.getHeight())/ me.lineHeight) + 1, 10); | |
| var info = me.viewInfo; | |
| if (info && !force) { | |
| if (viewStart >= info.start && viewEnd <= info.end) { | |
| return; | |
| } | |
| } | |
| var line = parseInt((top / me.lineHeight) - (me.pageSize / 2) + 10, 10); | |
| if (line < 0) { | |
| line = 0; | |
| } | |
| me.attemptLoad(line); | |
| }, | |
| afterRender: function() { | |
| var me = this; | |
| me.callParent(arguments); | |
| Ext.Function.defer(function() { | |
| var target = me.getTargetEl(); | |
| target.on('scroll', function(e) { | |
| me.requestUpdate(); | |
| }); | |
| me.requestUpdate(0); | |
| }, 20); | |
| }, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.url) { | |
| throw "no url specified"; | |
| } | |
| // show logs from today back to 3 days ago per default | |
| me.until_date = new Date(); | |
| me.since_date = new Date(); | |
| me.since_date.setDate(me.until_date.getDate() - 3); | |
| me.dataCmp = Ext.create('Ext.Component', { | |
| style: 'font:normal 11px tahoma, arial, verdana, sans-serif;' + | |
| 'line-height: ' + me.lineHeight.toString() + 'px; white-space: pre;' | |
| }); | |
| me.task = Ext.TaskManager.start({ | |
| run: function() { | |
| if (!me.isVisible() || !me.scrollToEnd || !me.viewInfo) { | |
| return; | |
| } | |
| var maxDown = me.getMaxDown(); | |
| if (maxDown > 0) { | |
| return; | |
| } | |
| me.requestUpdate(undefined, true); | |
| }, | |
| interval: 1000 | |
| }); | |
| Ext.apply(me, { | |
| items: me.dataCmp, | |
| listeners: { | |
| destroy: function() { | |
| Ext.TaskManager.stop(me.task); | |
| } | |
| } | |
| }); | |
| if (me.log_select_timespan) { | |
| me.tbar = ['->','Since: ', | |
| { | |
| xtype: 'datefield', | |
| maxValue: me.until_date, | |
| value: me.since_date, | |
| name: 'since_date', | |
| format: 'Y-m-d', | |
| listeners: { | |
| select: function(field, date) { | |
| me.since_date_selected = date; | |
| var until_field = field.up().down('field[name=until_date]'); | |
| if (date > until_field.getValue()) { | |
| until_field.setValue(date); | |
| } | |
| } | |
| } | |
| }, | |
| 'Until: ', | |
| { | |
| xtype: 'datefield', | |
| maxValue: me.until_date, | |
| value: me.until_date, | |
| name: 'until_date', | |
| format: 'Y-m-d', | |
| listeners: { | |
| select: function(field, date) { | |
| var since_field = field.up().down('field[name=since_date]'); | |
| if (date < since_field.getValue()) { | |
| since_field.setValue(date); | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'button', | |
| text: 'Update', | |
| handler: function() { | |
| var until_field = me.down('field[name=until_date]'); | |
| var since_field = me.down('field[name=since_date]'); | |
| if (until_field.getValue() < since_field.getValue()) { | |
| Ext.Msg.alert('Error', | |
| 'Since date must be less equal than Until date.'); | |
| until_field.setValue(me.until_date); | |
| since_field.setValue(me.since_date); | |
| } else { | |
| me.until_date = until_field.getValue(); | |
| me.since_date = since_field.getValue(); | |
| me.requestUpdate(); | |
| } | |
| } | |
| } | |
| ]; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.panel.Firewall', { | |
| extend: 'PVE.panel.SubConfig', | |
| alias: 'widget.pveFirewallPanel', | |
| configPrefix: 'firewall', | |
| fwtype: undefined, // 'dc', 'node' or 'vm' | |
| base_url: undefined, | |
| initComponent: function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.base_url) { | |
| throw "no base_url specified"; | |
| } | |
| if (!(me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm')) { | |
| throw "unknown firewall panel type"; | |
| } | |
| var list_refs_url = me.fwtype === 'vm' ? (me.base_url + '/refs') : | |
| '/cluster/firewall/refs'; | |
| var items = [ | |
| { | |
| xtype: 'pveFirewallRules', | |
| title: gettext('Rules'), | |
| allow_iface: true, | |
| base_url: me.base_url + '/rules', | |
| list_refs_url: list_refs_url, | |
| itemId: 'rules' | |
| } | |
| ]; | |
| if (me.fwtype === 'dc') { | |
| items.push({ | |
| xtype: 'pveSecurityGroups', | |
| title: gettext('Security Group'), | |
| itemId: 'sg' | |
| }); | |
| items.push({ | |
| xtype: 'pveFirewallAliases', | |
| base_url: '/cluster/firewall/aliases', | |
| itemId: 'aliases' | |
| }); | |
| items.push({ | |
| xtype: 'pveIPSet', | |
| base_url: '/cluster/firewall/ipset', | |
| list_refs_url: list_refs_url, | |
| itemId: 'ipset' | |
| }); | |
| } | |
| if (me.fwtype === 'vm') { | |
| items.push({ | |
| xtype: 'pveFirewallAliases', | |
| base_url: me.base_url + '/aliases', | |
| itemId: 'aliases' | |
| }); | |
| items.push({ | |
| xtype: 'pveIPSet', | |
| base_url: me.base_url + '/ipset', | |
| list_refs_url: list_refs_url, | |
| itemId: 'ipset' | |
| }); | |
| } | |
| items.push({ | |
| xtype: 'pveFirewallOptions', | |
| title: gettext('Options'), | |
| base_url: me.base_url + '/options', | |
| fwtype: me.fwtype, | |
| itemId: 'options' | |
| }); | |
| if (me.fwtype !== 'dc') { | |
| items.push({ | |
| title: gettext('Log'), | |
| itemId: 'fwlog', | |
| xtype: 'pveLogView', | |
| url: '/api2/extjs' + me.base_url + '/log' | |
| }); | |
| } | |
| Ext.apply(me, { | |
| defaults: { | |
| border: false, | |
| pveSelNode: me.pveSelNode | |
| }, | |
| plugins: [{ | |
| ptype: 'lazyitems', | |
| items: items | |
| }] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| // Ext.create is a function, but | |
| // we defined create a bool in PVE.window.Edit | |
| /*jslint confusion: true*/ | |
| Ext.define('PVE.CephCreatePool', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveCephCreatePool'], | |
| subject: 'Ceph Pool', | |
| create: true, | |
| method: 'POST', | |
| items: [ | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Name'), | |
| name: 'name', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| fieldLabel: gettext('Size'), | |
| name: 'size', | |
| value: 2, | |
| minValue: 1, | |
| maxValue: 3, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| fieldLabel: gettext('Min. Size'), | |
| name: 'min_size', | |
| value: 1, | |
| minValue: 1, | |
| maxValue: 3, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| fieldLabel: gettext('Crush RuleSet'), | |
| name: 'crush_ruleset', | |
| value: 0, | |
| minValue: 0, | |
| maxValue: 32768, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| fieldLabel: 'pg_num', | |
| name: 'pg_num', | |
| value: 64, | |
| minValue: 8, | |
| maxValue: 32768, | |
| allowBlank: false | |
| } | |
| ], | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| Ext.applyIf(me, { | |
| url: "/nodes/" + me.nodename + "/ceph/pools" | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.node.CephPoolList', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveNodeCephPoolList'], | |
| stateful: false, | |
| bufferedRenderer: false, | |
| features: [ { ftype: 'summary'} ], | |
| columns: [ | |
| { | |
| header: gettext('Name'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'pool_name' | |
| }, | |
| { | |
| header: gettext('Size') + '/min', | |
| width: 80, | |
| sortable: false, | |
| renderer: function(v, meta, rec) { | |
| return v + '/' + rec.data.min_size; | |
| }, | |
| dataIndex: 'size' | |
| }, | |
| { | |
| header: 'pg_num', | |
| width: 100, | |
| sortable: false, | |
| dataIndex: 'pg_num' | |
| }, | |
| { | |
| header: 'ruleset', | |
| width: 50, | |
| sortable: false, | |
| dataIndex: 'crush_ruleset' | |
| }, | |
| { | |
| header: gettext('Used'), | |
| columns: [ | |
| { | |
| header: '%', | |
| width: 80, | |
| sortable: true, | |
| align: 'right', | |
| renderer: Ext.util.Format.numberRenderer('0.00'), | |
| dataIndex: 'percent_used', | |
| summaryType: 'sum', | |
| summaryRenderer: Ext.util.Format.numberRenderer('0.00') | |
| }, | |
| { | |
| header: gettext('Total'), | |
| width: 100, | |
| sortable: true, | |
| renderer: PVE.Utils.render_size, | |
| align: 'right', | |
| dataIndex: 'bytes_used', | |
| summaryType: 'sum', | |
| summaryRenderer: PVE.Utils.render_size | |
| } | |
| ] | |
| } | |
| ], | |
| initComponent: function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var sm = Ext.create('Ext.selection.RowModel', {}); | |
| var rstore = Ext.create('PVE.data.UpdateStore', { | |
| interval: 3000, | |
| storeid: 'ceph-pool-list' + nodename, | |
| model: 'ceph-pool-list', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/nodes/" + nodename + "/ceph/pools" | |
| } | |
| }); | |
| var store = Ext.create('PVE.data.DiffStore', { rstore: rstore }); | |
| PVE.Utils.monStoreErrors(me, rstore); | |
| var create_btn = new Ext.Button({ | |
| text: gettext('Create'), | |
| handler: function() { | |
| var win = Ext.create('PVE.CephCreatePool', { | |
| nodename: nodename | |
| }); | |
| win.show(); | |
| } | |
| }); | |
| var remove_btn = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| disabled: true, | |
| confirmMsg: function(rec) { | |
| var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), | |
| "'" + rec.data.pool_name + "'"); | |
| msg += " " + gettext('This will permanently erase all data.'); | |
| return msg; | |
| }, | |
| handler: function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec.data.pool_name) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + nodename + "/ceph/pools/" + | |
| rec.data.pool_name, | |
| method: 'DELETE', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| tbar: [ create_btn, remove_btn ], | |
| listeners: { | |
| activate: rstore.startUpdate, | |
| hide: rstore.stopUpdate, | |
| 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_ruleset', type: 'integer'} | |
| ], | |
| idProperty: 'pool_name' | |
| }); | |
| }); | |
| Ext.define('PVE.CephCreateOsd', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveCephCreateOsd'], | |
| subject: 'Ceph OSD', | |
| showProgress: true, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| me.create = true; | |
| Ext.applyIf(me, { | |
| url: "/nodes/" + me.nodename + "/ceph/osd", | |
| method: 'POST', | |
| items: [ | |
| { | |
| xtype: 'pveCephDiskSelector', | |
| name: 'dev', | |
| nodename: me.nodename, | |
| value: me.dev, | |
| diskType: 'unused', | |
| fieldLabel: gettext('Disk'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'pveCephDiskSelector', | |
| name: 'journal_dev', | |
| nodename: me.nodename, | |
| diskType: 'journal_disks', | |
| fieldLabel: gettext('Journal Disk'), | |
| value: '', | |
| autoSelect: false, | |
| allowBlank: true, | |
| emptyText: 'use OSD disk' | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.CephRemoveOsd', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveCephRemoveOsd'], | |
| isRemove: true, | |
| showProgress: true, | |
| method: 'DELETE', | |
| items: [ | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'cleanup', | |
| checked: true, | |
| labelWidth: 130, | |
| fieldLabel: gettext('Remove Partitions') | |
| } | |
| ], | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| if (me.osdid === undefined || me.osdid < 0) { | |
| throw "no osdid specified"; | |
| } | |
| me.create = true; | |
| me.title = gettext('Remove') + ': ' + 'Ceph OSD osd.' + me.osdid; | |
| Ext.applyIf(me, { | |
| url: "/nodes/" + me.nodename + "/ceph/osd/" + me.osdid | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.node.CephOsdTree', { | |
| extend: 'Ext.tree.Panel', | |
| alias: ['widget.pveNodeCephOsdTree'], | |
| columns: [ | |
| { | |
| xtype: 'treecolumn', | |
| text: 'Name', | |
| dataIndex: 'name', | |
| width: 150 | |
| }, | |
| { | |
| text: 'Type', | |
| dataIndex: 'type', | |
| align: 'right', | |
| width: 60 | |
| }, | |
| { | |
| text: 'Status', | |
| dataIndex: 'status', | |
| align: 'right', | |
| renderer: function(value, metaData, rec) { | |
| if (!value) { | |
| return value; | |
| } | |
| var data = rec.data; | |
| return value + '/' + (data['in'] ? 'in' : 'out'); | |
| }, | |
| width: 80 | |
| }, | |
| { | |
| text: 'weight', | |
| dataIndex: 'crush_weight', | |
| align: 'right', | |
| renderer: function(value, metaData, rec) { | |
| if (rec.data.type !== 'osd') { | |
| return ''; | |
| } | |
| return value; | |
| }, | |
| width: 80 | |
| }, | |
| { | |
| text: 'reweight', | |
| dataIndex: 'reweight', | |
| align: 'right', | |
| renderer: function(value, metaData, rec) { | |
| if (rec.data.type !== 'osd') { | |
| return ''; | |
| } | |
| return value; | |
| }, | |
| width: 90 | |
| }, | |
| { | |
| header: gettext('Used'), | |
| columns: [ | |
| { | |
| text: '%', | |
| dataIndex: 'percent_used', | |
| align: 'right', | |
| renderer: function(value, metaData, rec) { | |
| if (rec.data.type !== 'osd') { | |
| return ''; | |
| } | |
| return Ext.util.Format.number(value, '0.00'); | |
| }, | |
| width: 80 | |
| }, | |
| { | |
| text: gettext('Total'), | |
| dataIndex: 'total_space', | |
| align: 'right', | |
| renderer: function(value, metaData, rec) { | |
| if (rec.data.type !== 'osd') { | |
| return ''; | |
| } | |
| return PVE.Utils.render_size(value); | |
| }, | |
| width: 100 | |
| } | |
| ] | |
| }, | |
| { | |
| header: gettext('Latency (ms)'), | |
| columns: [ | |
| { | |
| text: 'Apply', | |
| dataIndex: 'apply_latency_ms', | |
| align: 'right', | |
| renderer: function(value, metaData, rec) { | |
| if (rec.data.type !== 'osd') { | |
| return ''; | |
| } | |
| return value; | |
| }, | |
| width: 60 | |
| }, | |
| { | |
| text: 'Commit', | |
| dataIndex: 'commit_latency_ms', | |
| align: 'right', | |
| renderer: function(value, metaData, rec) { | |
| if (rec.data.type !== 'osd') { | |
| return ''; | |
| } | |
| return value; | |
| }, | |
| width: 60 | |
| } | |
| ] | |
| } | |
| ], | |
| initComponent: function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var sm = Ext.create('Ext.selection.TreeModel', {}); | |
| var set_button_status; // defined later | |
| var reload = function() { | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + nodename + "/ceph/osd", | |
| waitMsgTarget: me, | |
| method: 'GET', | |
| failure: function(response, opts) { | |
| PVE.Utils.setErrorMask(me, response.htmlStatus); | |
| }, | |
| success: function(response, opts) { | |
| sm.deselectAll(); | |
| me.setRootNode(response.result.data.root); | |
| me.expandAll(); | |
| set_button_status(); | |
| } | |
| }); | |
| }; | |
| var osd_cmd = function(cmd) { | |
| var rec = sm.getSelection()[0]; | |
| if (!(rec && (rec.data.id >= 0) && rec.data.host)) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + rec.data.host + "/ceph/osd/" + | |
| rec.data.id + '/' + cmd, | |
| waitMsgTarget: me, | |
| method: 'POST', | |
| success: reload, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| }; | |
| var service_cmd = function(cmd) { | |
| var rec = sm.getSelection()[0]; | |
| if (!(rec && rec.data.name && rec.data.host)) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + rec.data.host + "/ceph/" + cmd, | |
| params: { service: rec.data.name }, | |
| waitMsgTarget: me, | |
| method: 'POST', | |
| success: function(response, options) { | |
| var upid = response.result.data; | |
| var win = Ext.create('PVE.window.TaskProgress', { upid: upid }); | |
| win.show(); | |
| me.mon(win, 'close', reload, me); | |
| }, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| }; | |
| var start_btn = new Ext.Button({ | |
| text: gettext('Start'), | |
| disabled: true, | |
| handler: function(){ service_cmd('start'); } | |
| }); | |
| var stop_btn = new Ext.Button({ | |
| text: gettext('Stop'), | |
| disabled: true, | |
| handler: function(){ service_cmd('stop'); } | |
| }); | |
| var osd_out_btn = new Ext.Button({ | |
| text: 'Out', | |
| disabled: true, | |
| handler: function(){ osd_cmd('out'); } | |
| }); | |
| var osd_in_btn = new Ext.Button({ | |
| text: 'In', | |
| disabled: true, | |
| handler: function(){ osd_cmd('in'); } | |
| }); | |
| var remove_btn = new Ext.Button({ | |
| text: gettext('Remove'), | |
| disabled: true, | |
| handler: function(){ | |
| var rec = sm.getSelection()[0]; | |
| if (!(rec && (rec.data.id >= 0) && rec.data.host)) { | |
| return; | |
| } | |
| var win = Ext.create('PVE.CephRemoveOsd', { | |
| nodename: rec.data.host, | |
| osdid: rec.data.id | |
| }); | |
| win.show(); | |
| me.mon(win, 'close', reload, me); | |
| } | |
| }); | |
| set_button_status = function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| start_btn.setDisabled(true); | |
| stop_btn.setDisabled(true); | |
| remove_btn.setDisabled(true); | |
| osd_out_btn.setDisabled(true); | |
| osd_in_btn.setDisabled(true); | |
| return; | |
| } | |
| var isOsd = (rec.data.host && (rec.data.type === 'osd') && (rec.data.id >= 0)); | |
| start_btn.setDisabled(!(isOsd && (rec.data.status !== 'up'))); | |
| stop_btn.setDisabled(!(isOsd && (rec.data.status !== 'down'))); | |
| remove_btn.setDisabled(!(isOsd && (rec.data.status === 'down'))); | |
| osd_out_btn.setDisabled(!(isOsd && rec.data['in'])); | |
| osd_in_btn.setDisabled(!(isOsd && !rec.data['in'])); | |
| }; | |
| sm.on('selectionchange', set_button_status); | |
| var reload_btn = new Ext.Button({ | |
| text: gettext('Reload'), | |
| handler: reload | |
| }); | |
| Ext.apply(me, { | |
| tbar: [ reload_btn, start_btn, stop_btn, osd_out_btn, osd_in_btn, remove_btn ], | |
| rootVisible: false, | |
| 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: 'number', name: 'crush_weight' }], | |
| stateful: false, | |
| selModel: sm, | |
| listeners: { | |
| activate: function() { | |
| reload(); | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| reload(); | |
| } | |
| }); | |
| Ext.define('PVE.node.CephDiskList', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveNodeCephDiskList'], | |
| columns: [ | |
| { | |
| header: gettext('Device'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'dev' | |
| }, | |
| { | |
| header: gettext('Usage'), | |
| width: 80, | |
| sortable: false, | |
| renderer: function(v, metaData, rec) { | |
| if (rec && (rec.data.osdid >= 0)) { | |
| return "osd." + rec.data.osdid.toString(); | |
| } | |
| return v || PVE.Utils.noText; | |
| }, | |
| dataIndex: 'used' | |
| }, | |
| { | |
| header: gettext('Size'), | |
| width: 100, | |
| align: 'right', | |
| sortable: false, | |
| renderer: PVE.Utils.format_size, | |
| dataIndex: 'size' | |
| }, | |
| { | |
| header: gettext('Vendor'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'vendor' | |
| }, | |
| { | |
| header: gettext('Model'), | |
| width: 200, | |
| sortable: true, | |
| dataIndex: 'model' | |
| }, | |
| { | |
| header: gettext('Serial'), | |
| flex: 1, | |
| sortable: true, | |
| dataIndex: 'serial' | |
| } | |
| ], | |
| 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 rstore = Ext.create('PVE.data.UpdateStore', { | |
| interval: 3000, | |
| storeid: 'ceph-disk-list' + nodename, | |
| model: 'ceph-disk-list', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/nodes/" + nodename + "/ceph/disks" | |
| }, | |
| sorters: [ | |
| { | |
| property : 'dev', | |
| direction: 'ASC' | |
| } | |
| ] | |
| }); | |
| var store = Ext.create('PVE.data.DiffStore', { rstore: rstore }); | |
| PVE.Utils.monStoreErrors(me, rstore); | |
| var create_btn = new PVE.button.Button({ | |
| text: gettext('Create') + ': OSD', | |
| selModel: sm, | |
| disabled: true, | |
| enableFn: function(rec) { | |
| return !rec.data.used; | |
| }, | |
| handler: function() { | |
| var rec = sm.getSelection()[0]; | |
| var win = Ext.create('PVE.CephCreateOsd', { | |
| nodename: nodename, | |
| dev: rec.data.dev | |
| }); | |
| win.show(); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| tbar: [ create_btn ], | |
| listeners: { | |
| activate: rstore.startUpdate, | |
| hide: rstore.stopUpdate, | |
| destroy: rstore.stopUpdate | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }, function() { | |
| Ext.define('ceph-disk-list', { | |
| extend: 'Ext.data.Model', | |
| fields: [ 'dev', 'used', { name: 'size', type: 'number'}, | |
| {name: 'osdid', type: 'number'}, | |
| 'vendor', 'model', 'serial'], | |
| idProperty: 'dev' | |
| }); | |
| }); | |
| Ext.define('PVE.form.CephDiskSelector', { | |
| extend: 'PVE.form.ComboGrid', | |
| alias: ['widget.pveCephDiskSelector'], | |
| diskType: 'journal_disks', | |
| valueField: 'dev', | |
| displayField: 'dev', | |
| listConfig: { | |
| columns: [ | |
| { | |
| header: gettext('Device'), | |
| width: 80, | |
| sortable: true, | |
| dataIndex: 'dev' | |
| }, | |
| { | |
| header: gettext('Size'), | |
| width: 60, | |
| sortable: false, | |
| renderer: PVE.Utils.format_size, | |
| dataIndex: 'size' | |
| }, | |
| { | |
| header: gettext('Serial'), | |
| flex: 1, | |
| sortable: true, | |
| dataIndex: 'serial' | |
| } | |
| ] | |
| }, | |
| initComponent: function() { | |
| var me = this; | |
| var nodename = me.nodename; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var store = Ext.create('Ext.data.Store', { | |
| filterOnLoad: true, | |
| model: 'ceph-disk-list', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/nodes/" + nodename + "/ceph/disks", | |
| extraParams: { type: me.diskType } | |
| }, | |
| sorters: [ | |
| { | |
| property : 'dev', | |
| direction: 'ASC' | |
| } | |
| ] | |
| }); | |
| Ext.apply(me, { | |
| store: store | |
| }); | |
| me.callParent(); | |
| store.load(); | |
| } | |
| }); | |
| Ext.define('PVE.CephCreateMon', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveCephCreateMon'], | |
| subject: 'Ceph Monitor', | |
| showProgress: true, | |
| setNode: function(nodename) { | |
| var me = this; | |
| me.nodename = nodename; | |
| me.url = "/nodes/" + nodename + "/ceph/mon"; | |
| }, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| me.setNode(me.nodename); | |
| me.create = true; | |
| Ext.applyIf(me, { | |
| method: 'POST', | |
| items: [ | |
| { | |
| xtype: 'pveNodeSelector', | |
| submitValue: false, | |
| fieldLabel: gettext('Host'), | |
| selectCurNode: true, | |
| allowBlank: false, | |
| listeners: { | |
| change: function(f, value) { | |
| me.setNode(value); | |
| } | |
| } | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.node.CephMonList', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveNodeCephMonList'], | |
| 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('PVE.data.UpdateStore', { | |
| interval: 3000, | |
| storeid: 'ceph-mon-list' + nodename, | |
| model: 'ceph-mon-list', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/nodes/" + nodename + "/ceph/mon" | |
| } | |
| }); | |
| var store = Ext.create('PVE.data.DiffStore', { rstore: rstore }); | |
| PVE.Utils.monStoreErrors(me, rstore); | |
| var service_cmd = function(cmd) { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec.data.host) { | |
| Ext.Msg.alert(gettext('Error'), "entry has no host"); | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + rec.data.host + "/ceph/" + cmd, | |
| method: 'POST', | |
| params: { service: "mon." + rec.data.name }, | |
| success: function(response, options) { | |
| var upid = response.result.data; | |
| var win = Ext.create('PVE.window.TaskProgress', { upid: upid }); | |
| win.show(); | |
| }, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| }; | |
| var start_btn = new PVE.button.Button({ | |
| text: gettext('Start'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: function(){ | |
| service_cmd("start"); | |
| } | |
| }); | |
| var stop_btn = new PVE.button.Button({ | |
| text: gettext('Stop'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: function(){ | |
| service_cmd("stop"); | |
| } | |
| }); | |
| var create_btn = new Ext.Button({ | |
| text: gettext('Create'), | |
| handler: function(){ | |
| var win = Ext.create('PVE.CephCreateMon', { | |
| nodename: nodename | |
| }); | |
| win.show(); | |
| } | |
| }); | |
| var remove_btn = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec.data.host) { | |
| Ext.Msg.alert(gettext('Error'), "entry has no host"); | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + rec.data.host + "/ceph/mon/" + | |
| rec.data.name, | |
| method: 'DELETE', | |
| success: function(response, options) { | |
| var upid = response.result.data; | |
| var win = Ext.create('PVE.window.TaskProgress', { upid: upid }); | |
| win.show(); | |
| }, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| tbar: [ start_btn, stop_btn, create_btn, remove_btn ], | |
| columns: [ | |
| { | |
| header: gettext('Name'), | |
| width: 50, | |
| sortable: true, | |
| renderer: function(v) { return "mon." + v; }, | |
| dataIndex: 'name' | |
| }, | |
| { | |
| header: gettext('Host'), | |
| width: 100, | |
| sortable: true, | |
| renderer: function(v) { | |
| return v || 'unknown'; | |
| }, | |
| dataIndex: 'host' | |
| }, | |
| { | |
| header: gettext('Quorum'), | |
| width: 70, | |
| sortable: false, | |
| renderer: PVE.Utils.format_boolean, | |
| dataIndex: 'quorum' | |
| }, | |
| { | |
| header: gettext('Address'), | |
| flex: 1, | |
| sortable: true, | |
| dataIndex: 'addr' | |
| } | |
| ], | |
| listeners: { | |
| activate: rstore.startUpdate, | |
| hide: rstore.stopUpdate, | |
| destroy: rstore.stopUpdate | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }, function() { | |
| Ext.define('ceph-mon-list', { | |
| extend: 'Ext.data.Model', | |
| fields: [ 'addr', 'name', 'rank', 'host', 'quorum' ], | |
| idProperty: 'name' | |
| }); | |
| }); | |
| Ext.define('PVE.node.CephCrushMap', { | |
| extend: 'Ext.panel.Panel', | |
| alias: ['widget.pveNodeCephCrushMap'], | |
| bodyStyle: 'white-space:pre', | |
| bodyPadding: 5, | |
| scrollable: true, | |
| load: function() { | |
| var me = this; | |
| PVE.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; | |
| 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: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveNodeCephStatus'], | |
| cwidth1: 150, | |
| interval: 3000, | |
| initComponent: function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var renderquorum = function(value) { | |
| if (!value || value.length < 0) { | |
| return 'No'; | |
| } | |
| return 'Yes {' + value.join(' ') + '}'; | |
| }; | |
| var rendermonmap = function(d) { | |
| if (!d) { | |
| return ''; | |
| } | |
| var txt = 'e' + d.epoch + ': ' + d.mons.length + " mons at "; | |
| Ext.Array.each(d.mons, function(d) { | |
| txt += d.name + '=' + d.addr + ','; | |
| }); | |
| return txt; | |
| }; | |
| var renderosdmap = function(value) { | |
| if (!value || !value.osdmap) { | |
| return ''; | |
| } | |
| var d = value.osdmap; | |
| var txt = 'e' + d.epoch + ': '; | |
| txt += d.num_osds + ' osds: ' + d.num_up_osds + ' up, ' + | |
| d.num_in_osds + " in"; | |
| return txt; | |
| }; | |
| var renderhealth = function(value) { | |
| if (!value || !value.overall_status) { | |
| return ''; | |
| } | |
| var txt = value.overall_status; | |
| Ext.Array.each(value.summary, function(d) { | |
| txt += " " + d.summary + ';'; | |
| }); | |
| return txt; | |
| }; | |
| var renderpgmap = function(d) { | |
| if (!d) { | |
| return ''; | |
| } | |
| var txt = 'v' + d.version + ': '; | |
| txt += d.num_pgs + " pgs:"; | |
| Ext.Array.each(d.pgs_by_state, function(s) { | |
| txt += " " + s.count + " " + s.state_name; | |
| }); | |
| txt += '; '; | |
| txt += PVE.Utils.format_size(d.data_bytes) + " data, "; | |
| txt += PVE.Utils.format_size(d.bytes_used) + " used, "; | |
| txt += PVE.Utils.format_size(d.bytes_avail) + " avail"; | |
| return txt; | |
| }; | |
| Ext.applyIf(me, { | |
| url: "/api2/json/nodes/" + nodename + "/ceph/status", | |
| rows: { | |
| health: { | |
| header: 'health', | |
| renderer: renderhealth, | |
| required: true | |
| }, | |
| quorum_names: { | |
| header: 'quorum', | |
| renderer: renderquorum, | |
| required: true | |
| }, | |
| fsid: { | |
| header: 'cluster', | |
| required: true | |
| }, | |
| monmap: { | |
| header: 'monmap', | |
| renderer: rendermonmap, | |
| required: true | |
| }, | |
| osdmap: { | |
| header: 'osdmap', | |
| renderer: renderosdmap, | |
| required: true | |
| }, | |
| pgmap: { | |
| header: 'pgmap', | |
| renderer: renderpgmap, | |
| required: true | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| me.on('activate', me.rstore.startUpdate); | |
| me.on('hide', me.rstore.stopUpdate); | |
| me.on('destroy', me.rstore.stopUpdate); | |
| } | |
| }); | |
| Ext.define('PVE.node.CephConfig', { | |
| extend: 'Ext.panel.Panel', | |
| alias: ['widget.pveNodeCephConfig'], | |
| bodyStyle: 'white-space:pre', | |
| bodyPadding: 5, | |
| scrollable: true, | |
| load: function() { | |
| var me = this; | |
| PVE.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; | |
| 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.Ceph', { | |
| extend: 'PVE.panel.SubConfig', | |
| alias: ['widget.pveNodeCeph'], | |
| minTabWidth: 80, | |
| configPrefix: 'ceph', | |
| initComponent: function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| Ext.apply(me, { | |
| defaults: { | |
| border: false, | |
| pveSelNode: me.pveSelNode | |
| }, | |
| plugins: { | |
| ptype: 'lazyitems', | |
| items: [ | |
| { | |
| xtype: 'pveNodeCephStatus', | |
| title: gettext('Status'), | |
| itemId: 'status' | |
| }, | |
| { | |
| xtype: 'pveNodeCephConfig', | |
| title: gettext('Config'), | |
| itemId: 'config' | |
| }, | |
| { | |
| xtype: 'pveNodeCephMonList', | |
| title: gettext('Monitor'), | |
| itemId: 'monlist' | |
| }, | |
| { | |
| xtype: 'pveNodeCephDiskList', | |
| title: gettext('Disks'), | |
| itemId: 'disklist' | |
| }, | |
| { | |
| xtype: 'pveNodeCephOsdTree', | |
| title: 'OSD', | |
| itemId: 'osdtree' | |
| }, | |
| { | |
| xtype: 'pveNodeCephPoolList', | |
| title: gettext('Pools'), | |
| itemId: 'pools' | |
| }, | |
| { | |
| title: 'Crush', | |
| xtype: 'pveNodeCephCrushMap', | |
| itemId: 'crushmap' | |
| }, | |
| { | |
| title: gettext('Log'), | |
| itemId: 'log', | |
| xtype: 'pveLogView', | |
| url: "/api2/extjs/nodes/" + nodename + "/ceph/log" | |
| } | |
| ] | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.node.DNSEdit', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveNodeDNSEdit'], | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| me.items = [ | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Search domain'), | |
| name: 'search', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| fieldLabel: gettext('DNS server') + " 1", | |
| vtype: 'IP64Address', | |
| skipEmptyText: true, | |
| name: 'dns1' | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| fieldLabel: gettext('DNS server') + " 2", | |
| vtype: 'IP64Address', | |
| skipEmptyText: true, | |
| name: 'dns2' | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| fieldLabel: gettext('DNS server') + " 3", | |
| vtype: 'IP64Address', | |
| skipEmptyText: true, | |
| name: 'dns3' | |
| } | |
| ]; | |
| Ext.applyIf(me, { | |
| subject: gettext('DNS'), | |
| url: "/api2/extjs/nodes/" + nodename + "/dns", | |
| fieldDefaults: { | |
| labelWidth: 120 | |
| } | |
| }); | |
| me.callParent(); | |
| me.load(); | |
| } | |
| }); | |
| Ext.define('PVE.node.DNSView', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveNodeDNSView'], | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var run_editor = function() { | |
| var win = Ext.create('PVE.node.DNSEdit', { | |
| pveSelNode: me.pveSelNode | |
| }); | |
| win.show(); | |
| }; | |
| Ext.apply(me, { | |
| url: "/api2/json/nodes/" + nodename + "/dns", | |
| cwidth1: 130, | |
| interval: 1000, | |
| rows: { | |
| search: { header: 'Search domain', required: true }, | |
| dns1: { header: gettext('DNS server') + " 1", required: true }, | |
| dns2: { header: gettext('DNS server') + " 2" }, | |
| dns3: { header: gettext('DNS server') + " 3" } | |
| }, | |
| tbar: [ | |
| { | |
| text: gettext("Edit"), | |
| handler: run_editor | |
| } | |
| ], | |
| listeners: { | |
| itemdblclick: run_editor | |
| } | |
| }); | |
| me.callParent(); | |
| me.on('activate', me.rstore.startUpdate); | |
| me.on('hide', me.rstore.stopUpdate); | |
| me.on('destroy', me.rstore.stopUpdate); | |
| } | |
| }); | |
| Ext.define('PVE.node.TimeView', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveNodeTimeView'], | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var tzoffset = (new Date()).getTimezoneOffset()*60000; | |
| var renderlocaltime = function(value) { | |
| var servertime = new Date((value * 1000) + tzoffset); | |
| return Ext.Date.format(servertime, 'Y-m-d H:i:s'); | |
| }; | |
| var run_editor = function() { | |
| var win = Ext.create('PVE.node.TimeEdit', { | |
| pveSelNode: me.pveSelNode | |
| }); | |
| win.show(); | |
| }; | |
| Ext.apply(me, { | |
| url: "/api2/json/nodes/" + nodename + "/time", | |
| cwidth1: 150, | |
| interval: 1000, | |
| rows: { | |
| timezone: { | |
| header: gettext('Time zone'), | |
| required: true | |
| }, | |
| localtime: { | |
| header: gettext('Server time'), | |
| required: true, | |
| renderer: renderlocaltime | |
| } | |
| }, | |
| tbar: [ | |
| { | |
| text: gettext("Edit"), | |
| handler: run_editor | |
| } | |
| ], | |
| listeners: { | |
| itemdblclick: run_editor | |
| } | |
| }); | |
| me.callParent(); | |
| me.on('activate', me.rstore.startUpdate); | |
| me.on('hide', me.rstore.stopUpdate); | |
| me.on('destroy', me.rstore.stopUpdate); | |
| } | |
| }); | |
| Ext.define('PVE.node.TimeEdit', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveNodeTimeEdit'], | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| Ext.applyIf(me, { | |
| subject: gettext('Time zone'), | |
| url: "/api2/extjs/nodes/" + nodename + "/time", | |
| fieldDefaults: { | |
| labelWidth: 70 | |
| }, | |
| width: 400, | |
| items: { | |
| xtype: 'combo', | |
| fieldLabel: gettext('Time zone'), | |
| name: 'timezone', | |
| queryMode: 'local', | |
| store: Ext.create('PVE.data.TimezoneStore'), | |
| valueField: 'zone', | |
| displayField: 'zone', | |
| triggerAction: 'all', | |
| forceSelection: true, | |
| editable: false, | |
| allowBlank: false | |
| } | |
| }); | |
| me.callParent(); | |
| me.load(); | |
| } | |
| }); | |
| Ext.define('PVE.node.StatusView', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveNodeStatusView'], | |
| disabled: true, | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var socketText = gettext('Socket'); | |
| var socketsText = gettext('Sockets'); | |
| var render_cpuinfo = function(value) { | |
| return value.cpus + " x " + value.model + " (" + | |
| value.sockets.toString() + " " + | |
| (value.sockets > 1 ? socketsText : socketText) + ")"; | |
| }; | |
| var render_loadavg = function(value) { | |
| return value[0] + ", " + value[1] + ", " + value[2]; | |
| }; | |
| var render_cpu = function(value) { | |
| var per = value * 100; | |
| return per.toFixed(2) + "%"; | |
| }; | |
| var render_ksm = function(value) { | |
| return PVE.Utils.format_size(value.shared); | |
| }; | |
| var render_meminfo = function(value) { | |
| var per = (value.used / value.total)*100; | |
| var text = "<div>" + PVE.Utils.totalText + ": " + PVE.Utils.format_size(value.total) + "</div>" + | |
| "<div>" + PVE.Utils.usedText + ": " + PVE.Utils.format_size(value.used) + "</div>"; | |
| return text; | |
| }; | |
| var rows = { | |
| uptime: { header: gettext('Uptime'), required: true, renderer: PVE.Utils.format_duration_long }, | |
| loadavg: { header: gettext('Load average'), required: true, renderer: render_loadavg }, | |
| cpuinfo: { header: gettext('CPUs'), required: true, renderer: render_cpuinfo }, | |
| cpu: { header: gettext('CPU usage'),required: true, renderer: render_cpu }, | |
| wait: { header: gettext('IO delay'), required: true, renderer: render_cpu }, | |
| memory: { header: gettext('RAM usage'), required: true, renderer: render_meminfo }, | |
| swap: { header: gettext('SWAP usage'), required: true, renderer: render_meminfo }, | |
| ksm: { header: gettext('KSM sharing'), required: true, renderer: render_ksm }, | |
| rootfs: { header: gettext('HD space') + ' (root)', required: true, renderer: render_meminfo }, | |
| pveversion: { header: gettext('PVE Manager version'), required: true }, | |
| kversion: { header: gettext('Kernel version'), required: true } | |
| }; | |
| Ext.applyIf(me, { | |
| cwidth1: 150, | |
| //height: 276, | |
| rows: rows | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.node.Summary', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pveNodeSummary', | |
| scrollable: true, | |
| bodyStyle: 'padding:10px', | |
| 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, | |
| style: { | |
| 'background-color': 'white', | |
| 'white-space': 'pre', | |
| 'font-family': 'monospace', | |
| padding: '5px' | |
| } | |
| }); | |
| var win = Ext.create('Ext.window.Window', { | |
| title: gettext('Package versions'), | |
| width: 600, | |
| height: 400, | |
| layout: 'fit', | |
| modal: true, | |
| items: [ view ] | |
| }); | |
| PVE.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 statusview = Ext.create('PVE.node.StatusView', { | |
| title: gettext('Status'), | |
| pveSelNode: me.pveSelNode, | |
| width: 800, | |
| rstore: rstore | |
| }); | |
| var version_btn = new Ext.Button({ | |
| text: gettext('Package versions'), | |
| handler: function(){ | |
| PVE.Utils.checked_command(function() { me.showVersions(); }); | |
| } | |
| }); | |
| var rrdstore = Ext.create('PVE.data.RRDStore', { | |
| rrdurl: "/api2/json/nodes/" + nodename + "/rrddata" | |
| }); | |
| Ext.apply(me, { | |
| tbar: [version_btn, '->', { xtype: 'pveRRDTypeSelector' } ], | |
| plugins: { | |
| ptype: 'lazyitems', | |
| items: [ | |
| { | |
| xtype: 'container', | |
| layout: 'column', | |
| defaults: { | |
| padding: '0 10 10 0' | |
| }, | |
| items: [ | |
| statusview, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('CPU usage'), | |
| fields: ['cpu','iowait'], | |
| fieldTitles: [gettext('CPU usage'), gettext('IO delay')], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Server load'), | |
| fields: ['loadavg'], | |
| fieldTitles: [gettext('Load average')], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Memory usage'), | |
| fields: ['memtotal','memused'], | |
| fieldTitles: [gettext('Total'), gettext('RAM usage')], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Network traffic'), | |
| fields: ['netin','netout'], | |
| store: rrdstore | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| listeners: { | |
| activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, | |
| hide: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); }, | |
| destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.node.ServiceView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveNodeServiceView'], | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var rstore = Ext.create('PVE.data.UpdateStore', { | |
| interval: 1000, | |
| storeid: 'pve-services' + nodename, | |
| model: 'pve-services', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/nodes/" + nodename + "/services" | |
| } | |
| }); | |
| var store = Ext.create('PVE.data.DiffStore', { | |
| rstore: rstore, | |
| sortAfterUpdate: true, | |
| sorters: [ | |
| { | |
| property : 'name', | |
| direction: 'ASC' | |
| } | |
| ] | |
| }); | |
| var service_cmd = function(cmd) { | |
| var sm = me.getSelectionModel(); | |
| var rec = sm.getSelection()[0]; | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + nodename + "/services/" + rec.data.service + "/" + cmd, | |
| method: 'POST', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| me.loading = true; | |
| }, | |
| success: function(response, opts) { | |
| rstore.startUpdate(); | |
| var upid = response.result.data; | |
| var win = Ext.create('PVE.window.TaskProgress', { | |
| upid: upid | |
| }); | |
| win.show(); | |
| } | |
| }); | |
| }; | |
| var start_btn = new Ext.Button({ | |
| text: gettext('Start'), | |
| disabled: true, | |
| handler: function(){ | |
| service_cmd("start"); | |
| } | |
| }); | |
| var stop_btn = new Ext.Button({ | |
| text: gettext('Stop'), | |
| disabled: true, | |
| handler: function(){ | |
| service_cmd("stop"); | |
| } | |
| }); | |
| var restart_btn = new Ext.Button({ | |
| text: gettext('Restart'), | |
| disabled: true, | |
| handler: function(){ | |
| service_cmd("restart"); | |
| } | |
| }); | |
| var set_button_status = function() { | |
| var sm = me.getSelectionModel(); | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| start_btn.disable(); | |
| stop_btn.disable(); | |
| restart_btn.disable(); | |
| return; | |
| } | |
| var service = rec.data.service; | |
| var state = rec.data.state; | |
| if (service == 'pveproxy' || | |
| service == 'pvecluster' || | |
| service == 'pvedaemon') { | |
| if (state == 'running') { | |
| start_btn.disable(); | |
| restart_btn.enable(); | |
| } else { | |
| start_btn.enable(); | |
| restart_btn.disable(); | |
| } | |
| stop_btn.disable(); | |
| } else { | |
| if (state == 'running') { | |
| start_btn.disable(); | |
| restart_btn.enable(); | |
| stop_btn.enable(); | |
| } else { | |
| start_btn.enable(); | |
| restart_btn.disable(); | |
| stop_btn.disable(); | |
| } | |
| } | |
| }; | |
| me.mon(store, 'refresh', set_button_status); | |
| PVE.Utils.monStoreErrors(me, rstore); | |
| Ext.apply(me, { | |
| store: store, | |
| stateful: false, | |
| tbar: [ start_btn, stop_btn, restart_btn ], | |
| columns: [ | |
| { | |
| header: gettext('Name'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'name' | |
| }, | |
| { | |
| header: gettext('Status'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'state' | |
| }, | |
| { | |
| header: gettext('Description'), | |
| renderer: Ext.String.htmlEncode, | |
| dataIndex: 'desc', | |
| flex: 1 | |
| } | |
| ], | |
| listeners: { | |
| selectionchange: set_button_status, | |
| activate: rstore.startUpdate, | |
| hide: rstore.stopUpdate, | |
| destroy: rstore.stopUpdate | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }, function() { | |
| Ext.define('pve-services', { | |
| extend: 'Ext.data.Model', | |
| fields: [ 'service', 'name', 'desc', 'state' ], | |
| idProperty: 'service' | |
| }); | |
| }); | |
| Ext.define('PVE.node.NetworkEdit', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveNodeNetworkEdit'], | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| if (!me.iftype) { | |
| throw "no network device type specified"; | |
| } | |
| me.create = !me.iface; | |
| var iface_vtype; | |
| if (me.iftype === 'bridge') { | |
| iface_vtype = 'BridgeName'; | |
| } else if (me.iftype === 'bond') { | |
| iface_vtype = 'BondName'; | |
| } else if (me.iftype === 'eth' && !me.create) { | |
| iface_vtype = 'InterfaceName'; | |
| } else if (me.iftype === 'vlan' && !me.create) { | |
| iface_vtype = 'InterfaceName'; | |
| } else if (me.iftype === 'OVSBridge') { | |
| iface_vtype = 'BridgeName'; | |
| } else if (me.iftype === 'OVSBond') { | |
| iface_vtype = 'BondName'; | |
| } else if (me.iftype === 'OVSIntPort') { | |
| iface_vtype = 'InterfaceName'; | |
| } else if (me.iftype === 'OVSPort') { | |
| iface_vtype = 'InterfaceName'; | |
| } else { | |
| console.log(me.iftype); | |
| throw "unknown network device type specified"; | |
| } | |
| me.subject = PVE.Utils.render_network_iface_type(me.iftype); | |
| var column2 = []; | |
| if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || | |
| me.iftype === 'OVSBond')) { | |
| column2.push({ | |
| xtype: 'pvecheckbox', | |
| fieldLabel: gettext('Autostart'), | |
| name: 'autostart', | |
| uncheckedValue: 0, | |
| checked: me.create ? true : undefined | |
| }); | |
| } | |
| if (me.iftype === 'bridge') { | |
| column2.push({ | |
| xtype: 'pvecheckbox', | |
| fieldLabel: gettext('VLAN aware'), | |
| name: 'bridge_vlan_aware', | |
| deleteEmpty: !me.create | |
| }); | |
| column2.push({ | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Bridge ports'), | |
| name: 'bridge_ports' | |
| }); | |
| } else if (me.iftype === 'OVSBridge') { | |
| column2.push({ | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Bridge ports'), | |
| name: 'ovs_ports' | |
| }); | |
| column2.push({ | |
| xtype: 'textfield', | |
| fieldLabel: gettext('OVS options'), | |
| name: 'ovs_options' | |
| }); | |
| } else if (me.iftype === 'OVSPort' || me.iftype === 'OVSIntPort') { | |
| column2.push({ | |
| xtype: me.create ? 'PVE.form.BridgeSelector' : 'displayfield', | |
| fieldLabel: PVE.Utils.render_network_iface_type('OVSBridge'), | |
| allowBlank: false, | |
| nodename: nodename, | |
| bridgeType: 'OVSBridge', | |
| name: 'ovs_bridge' | |
| }); | |
| column2.push({ | |
| xtype: 'pveVlanField', | |
| deleteEmpty: !me.create, | |
| name: 'ovs_tag', | |
| value: '' | |
| }); | |
| column2.push({ | |
| xtype: 'textfield', | |
| fieldLabel: gettext('OVS options'), | |
| name: 'ovs_options' | |
| }); | |
| } else if (me.iftype === 'bond') { | |
| column2.push({ | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Slaves'), | |
| name: 'slaves' | |
| }); | |
| var policySelector = Ext.createWidget('bondPolicySelector', { | |
| fieldLabel: gettext('Hash policy'), | |
| name: 'bond_xmit_hash_policy', | |
| deleteEmpty: !me.create, | |
| disabled: true | |
| }); | |
| column2.push({ | |
| xtype: 'bondModeSelector', | |
| fieldLabel: gettext('Mode'), | |
| name: 'bond_mode', | |
| value: me.create ? 'balance-rr' : undefined, | |
| listeners: { | |
| change: function(f, value) { | |
| if (value === 'balance-xor' || | |
| value === '802.3ad') { | |
| policySelector.setDisabled(false); | |
| } else { | |
| policySelector.setDisabled(true); | |
| policySelector.setValue(''); | |
| } | |
| } | |
| }, | |
| allowBlank: false | |
| }); | |
| column2.push(policySelector); | |
| } else if (me.iftype === 'OVSBond') { | |
| column2.push({ | |
| xtype: me.create ? 'PVE.form.BridgeSelector' : 'displayfield', | |
| fieldLabel: PVE.Utils.render_network_iface_type('OVSBridge'), | |
| allowBlank: false, | |
| nodename: nodename, | |
| bridgeType: 'OVSBridge', | |
| name: 'ovs_bridge' | |
| }); | |
| column2.push({ | |
| xtype: 'pveVlanField', | |
| deleteEmpty: !me.create, | |
| name: 'ovs_tag', | |
| value: '' | |
| }); | |
| column2.push({ | |
| xtype: 'textfield', | |
| fieldLabel: gettext('OVS options'), | |
| name: 'ovs_options' | |
| }); | |
| } | |
| var url; | |
| var method; | |
| if (me.create) { | |
| url = "/api2/extjs/nodes/" + nodename + "/network"; | |
| method = 'POST'; | |
| } else { | |
| url = "/api2/extjs/nodes/" + nodename + "/network/" + me.iface; | |
| method = 'PUT'; | |
| } | |
| var column1 = [ | |
| { | |
| xtype: 'hiddenfield', | |
| name: 'type', | |
| value: me.iftype | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| fieldLabel: gettext('Name'), | |
| name: 'iface', | |
| value: me.iface, | |
| vtype: iface_vtype, | |
| allowBlank: false | |
| } | |
| ]; | |
| if (me.iftype === 'OVSBond') { | |
| column1.push( | |
| { | |
| xtype: 'bondModeSelector', | |
| fieldLabel: gettext('Mode'), | |
| name: 'bond_mode', | |
| openvswitch: true, | |
| value: me.create ? 'active-backup' : undefined, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Slaves'), | |
| name: 'ovs_bonds' | |
| } | |
| ); | |
| } else { | |
| column1.push( | |
| { | |
| xtype: 'pvetextfield', | |
| deleteEmpty: !me.create, | |
| fieldLabel: gettext('IP address'), | |
| vtype: 'IPAddress', | |
| name: 'address' | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| deleteEmpty: !me.create, | |
| fieldLabel: gettext('Subnet mask'), | |
| vtype: 'IPAddress', | |
| name: 'netmask', | |
| validator: function(value) { | |
| /*jslint confusion: true */ | |
| if (!me.items) { | |
| return true; | |
| } | |
| var address = me.down('field[name=address]').getValue(); | |
| if (value !== '') { | |
| if (address === '') { | |
| return "Subnet mask requires option 'IP address'"; | |
| } | |
| } else { | |
| if (address !== '') { | |
| return "Option 'IP address' requires a subnet mask"; | |
| } | |
| } | |
| return true; | |
| } | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| deleteEmpty: !me.create, | |
| fieldLabel: gettext('Gateway'), | |
| vtype: 'IPAddress', | |
| name: 'gateway' | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| deleteEmpty: !me.create, | |
| fieldLabel: gettext('IPv6 address'), | |
| vtype: 'IP6Address', | |
| name: 'address6' | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| deleteEmpty: !me.create, | |
| fieldLabel: gettext('Prefix length'), | |
| vtype: 'IP6PrefixLength', | |
| name: 'netmask6', | |
| value: '', | |
| allowBlank: true, | |
| validator: function(value) { | |
| /*jslint confusion: true */ | |
| if (!me.items) { | |
| return true; | |
| } | |
| var address = me.down('field[name=address6]').getValue(); | |
| if (value !== '') { | |
| if (address === '') { | |
| return "IPv6 prefix length requires option 'IPv6 address'"; | |
| } | |
| } else { | |
| if (address !== '') { | |
| return "Option 'IPv6 address' requires an IPv6 prefix length"; | |
| } | |
| } | |
| return true; | |
| } | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| deleteEmpty: !me.create, | |
| fieldLabel: gettext('Gateway'), | |
| vtype: 'IP6Address', | |
| name: 'gateway6' | |
| } | |
| ); | |
| } | |
| Ext.applyIf(me, { | |
| url: url, | |
| method: method, | |
| items: { | |
| xtype: 'inputpanel', | |
| column1: column1, | |
| column2: column2 | |
| } | |
| }); | |
| me.callParent(); | |
| if (me.create) { | |
| me.down('field[name=iface]').setValue(me.iface_default); | |
| } else { | |
| me.load({ | |
| success: function(response, options) { | |
| var data = response.result.data; | |
| if (data.type !== me.iftype) { | |
| var msg = "Got unexpected device type"; | |
| Ext.Msg.alert(gettext('Error'), msg, function() { | |
| me.close(); | |
| }); | |
| return; | |
| } | |
| me.setValues(data); | |
| me.isValid(); // trigger validation | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.node.NetworkView', { | |
| extend: 'Ext.panel.Panel', | |
| alias: ['widget.pveNodeNetworkView'], | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var store = Ext.create('Ext.data.Store', { | |
| model: 'pve-networks', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/nodes/" + nodename + "/network" | |
| }, | |
| sorters: [ | |
| { | |
| property : 'iface', | |
| direction: 'ASC' | |
| } | |
| ] | |
| }); | |
| var reload = function() { | |
| var changeitem = me.down('#changes'); | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + nodename + '/network', | |
| failure: function(response, opts) { | |
| changeitem.update(gettext('Error') + ': ' + response.htmlStatus); | |
| store.loadData({}); | |
| }, | |
| success: function(response, opts) { | |
| var result = Ext.decode(response.responseText); | |
| store.loadData(result.data); | |
| var changes = result.changes; | |
| if (changes === undefined || changes === '') { | |
| changes = gettext("No changes"); | |
| } | |
| changeitem.update("<pre>" + Ext.htmlEncode(changes) + "</pre>"); | |
| } | |
| }); | |
| }; | |
| var run_editor = function() { | |
| var grid = me.down('gridpanel'); | |
| var sm = grid.getSelectionModel(); | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| return; | |
| } | |
| var win = Ext.create('PVE.node.NetworkEdit', { | |
| pveSelNode: me.pveSelNode, | |
| iface: rec.data.iface, | |
| iftype: rec.data.type | |
| }); | |
| win.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var edit_btn = new Ext.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| handler: run_editor | |
| }); | |
| var del_btn = new Ext.Button({ | |
| text: gettext('Remove'), | |
| disabled: true, | |
| handler: function(){ | |
| var grid = me.down('gridpanel'); | |
| var sm = grid.getSelectionModel(); | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| return; | |
| } | |
| var iface = rec.data.iface; | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + nodename + '/network/' + iface, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| var set_button_status = function() { | |
| var grid = me.down('gridpanel'); | |
| var sm = grid.getSelectionModel(); | |
| var rec = sm.getSelection()[0]; | |
| edit_btn.setDisabled(!rec); | |
| del_btn.setDisabled(!rec); | |
| }; | |
| PVE.Utils.monStoreErrors(me, store); | |
| var render_ports = function(value, metaData, record) { | |
| if (value === 'bridge') { | |
| return record.data.bridge_ports; | |
| } else if (value === 'bond') { | |
| return record.data.slaves; | |
| } else if (value === 'OVSBridge') { | |
| return record.data.ovs_ports; | |
| } else if (value === 'OVSBond') { | |
| return record.data.ovs_bonds; | |
| } | |
| }; | |
| var find_next_iface_id = function(prefix) { | |
| var next; | |
| for (next = 0; next <= 9999; next++) { | |
| if (!store.getById(prefix + next.toString())) { | |
| break; | |
| } | |
| } | |
| return prefix + next.toString(); | |
| }; | |
| Ext.apply(me, { | |
| layout: 'border', | |
| tbar: [ | |
| { | |
| text: gettext('Create'), | |
| menu: new Ext.menu.Menu({ | |
| plain: true, | |
| items: [ | |
| { | |
| text: PVE.Utils.render_network_iface_type('bridge'), | |
| handler: function() { | |
| var win = Ext.create('PVE.node.NetworkEdit', { | |
| pveSelNode: me.pveSelNode, | |
| iftype: 'bridge', | |
| iface_default: find_next_iface_id('vmbr') | |
| }); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.render_network_iface_type('bond'), | |
| handler: function() { | |
| var win = Ext.create('PVE.node.NetworkEdit', { | |
| pveSelNode: me.pveSelNode, | |
| iftype: 'bond', | |
| iface_default: find_next_iface_id('bond') | |
| }); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, '-', | |
| { | |
| text: PVE.Utils.render_network_iface_type('OVSBridge'), | |
| handler: function() { | |
| var win = Ext.create('PVE.node.NetworkEdit', { | |
| pveSelNode: me.pveSelNode, | |
| iftype: 'OVSBridge', | |
| iface_default: find_next_iface_id('vmbr') | |
| }); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.render_network_iface_type('OVSBond'), | |
| handler: function() { | |
| var win = Ext.create('PVE.node.NetworkEdit', { | |
| pveSelNode: me.pveSelNode, | |
| iftype: 'OVSBond', | |
| iface_default: find_next_iface_id('bond') | |
| }); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.render_network_iface_type('OVSIntPort'), | |
| handler: function() { | |
| var win = Ext.create('PVE.node.NetworkEdit', { | |
| pveSelNode: me.pveSelNode, | |
| iftype: 'OVSIntPort' | |
| }); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| } | |
| ] | |
| }) | |
| }, ' ', | |
| { | |
| text: gettext('Revert'), | |
| handler: function() { | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + nodename + '/network', | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }, | |
| edit_btn, | |
| del_btn | |
| ], | |
| items: [ | |
| { | |
| xtype: 'gridpanel', | |
| stateful: false, | |
| store: store, | |
| region: 'center', | |
| border: false, | |
| columns: [ | |
| { | |
| header: gettext('Name'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'iface' | |
| }, | |
| { | |
| header: gettext('Type'), | |
| width: 100, | |
| sortable: true, | |
| renderer: PVE.Utils.render_network_iface_type, | |
| dataIndex: 'type' | |
| }, | |
| { | |
| xtype: 'booleancolumn', | |
| header: gettext('Active'), | |
| width: 80, | |
| sortable: true, | |
| dataIndex: 'active', | |
| trueText: 'Yes', | |
| falseText: 'No', | |
| undefinedText: 'No' | |
| }, | |
| { | |
| xtype: 'booleancolumn', | |
| header: gettext('Autostart'), | |
| width: 80, | |
| sortable: true, | |
| dataIndex: 'autostart', | |
| trueText: 'Yes', | |
| falseText: 'No', | |
| undefinedText: 'No' | |
| }, | |
| { | |
| header: gettext('Ports/Slaves'), | |
| dataIndex: 'type', | |
| renderer: render_ports | |
| }, | |
| { | |
| header: gettext('IP address'), | |
| sortable: true, | |
| dataIndex: 'address', | |
| renderer: function(value, metaData, rec) { | |
| if (rec.data.address && rec.data.address6) { | |
| return rec.data.address + "<br>" | |
| + rec.data.address6 + '/' + rec.data.netmask6; | |
| } else if (rec.data.address6) { | |
| return rec.data.address6 + '/' + rec.data.netmask6; | |
| } else { | |
| return rec.data.address; | |
| } | |
| } | |
| }, | |
| { | |
| header: gettext('Subnet mask'), | |
| sortable: true, | |
| dataIndex: 'netmask' | |
| }, | |
| { | |
| header: gettext('Gateway'), | |
| sortable: true, | |
| dataIndex: 'gateway', | |
| renderer: function(value, metaData, rec) { | |
| if (rec.data.gateway && rec.data.gateway6) { | |
| return rec.data.gateway + "<br>" + rec.data.gateway6; | |
| } else if (rec.data.gateway6) { | |
| return rec.data.gateway6; | |
| } else { | |
| return rec.data.gateway; | |
| } | |
| } | |
| } | |
| ], | |
| listeners: { | |
| selectionchange: set_button_status, | |
| itemdblclick: run_editor | |
| } | |
| }, | |
| { | |
| border: false, | |
| region: 'south', | |
| autoScroll: true, | |
| itemId: 'changes', | |
| tbar: [ | |
| gettext('Pending changes') + ' (' + | |
| gettext('Please reboot to activate changes') + ')' | |
| ], | |
| split: true, | |
| bodyPadding: 5, | |
| flex: 0.6, | |
| html: gettext("No changes") | |
| } | |
| ], | |
| listeners: { | |
| activate: reload | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }, function() { | |
| Ext.define('pve-networks', { | |
| extend: 'Ext.data.Model', | |
| fields: [ | |
| 'iface', 'type', 'active', 'autostart', | |
| 'bridge_ports', 'slaves', | |
| 'address', 'netmask', 'gateway', | |
| 'address6', 'netmask6', 'gateway6' | |
| ], | |
| idProperty: 'iface' | |
| }); | |
| }); | |
| Ext.define('PVE.node.Tasks', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveNodeTasks'], | |
| stateful: false, | |
| loadMask: true, | |
| sortableColumns: false, | |
| vmidFilter: 0, | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var store = Ext.create('Ext.data.BufferedStore', { | |
| pageSize: 500, | |
| autoLoad: true, | |
| remoteFilter: true, | |
| model: 'pve-tasks', | |
| proxy: { | |
| type: 'pve', | |
| startParam: 'start', | |
| limitParam: 'limit', | |
| url: "/api2/json/nodes/" + nodename + "/tasks" | |
| } | |
| }); | |
| var userfilter = ''; | |
| var filter_errors = 0; | |
| var updateProxyParams = function() { | |
| var params = { | |
| errors: filter_errors | |
| }; | |
| if (userfilter) { | |
| params.userfilter = userfilter; | |
| } | |
| if (me.vmidFilter) { | |
| params.vmid = me.vmidFilter; | |
| } | |
| store.proxy.extraParams = params; | |
| }; | |
| updateProxyParams(); | |
| var reload_task = Ext.create('Ext.util.DelayedTask',function() { | |
| updateProxyParams(); | |
| store.reload(); | |
| }); | |
| var run_task_viewer = function() { | |
| var sm = me.getSelectionModel(); | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| return; | |
| } | |
| var win = Ext.create('PVE.window.TaskViewer', { | |
| upid: rec.data.upid | |
| }); | |
| win.show(); | |
| }; | |
| var view_btn = new Ext.Button({ | |
| text: gettext('View'), | |
| disabled: true, | |
| handler: run_task_viewer | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| viewConfig: { | |
| trackOver: false, | |
| stripeRows: false, // does not work with getRowClass() | |
| getRowClass: function(record, index) { | |
| var status = record.get('status'); | |
| if (status && status != 'OK') { | |
| return "x-form-invalid-field"; | |
| } | |
| } | |
| }, | |
| tbar: [ | |
| view_btn, '->', gettext('User name') +':', ' ', | |
| { | |
| xtype: 'textfield', | |
| width: 200, | |
| value: userfilter, | |
| enableKeyEvents: true, | |
| listeners: { | |
| keyup: function(field, e) { | |
| userfilter = field.getValue(); | |
| reload_task.delay(500); | |
| } | |
| } | |
| }, ' ', gettext('Only Errors') + ':', ' ', | |
| { | |
| xtype: 'checkbox', | |
| hideLabel: true, | |
| checked: filter_errors, | |
| listeners: { | |
| change: function(field, checked) { | |
| filter_errors = checked ? 1 : 0; | |
| reload_task.delay(10); | |
| } | |
| } | |
| }, ' ' | |
| ], | |
| columns: [ | |
| { | |
| header: gettext("Start Time"), | |
| dataIndex: 'starttime', | |
| width: 100, | |
| renderer: function(value) { | |
| return Ext.Date.format(value, "M d H:i:s"); | |
| } | |
| }, | |
| { | |
| header: gettext("End Time"), | |
| dataIndex: 'endtime', | |
| width: 100, | |
| renderer: function(value, metaData, record) { | |
| 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: PVE.Utils.render_upid | |
| }, | |
| { | |
| header: gettext("Status"), | |
| dataIndex: 'status', | |
| width: 200, | |
| renderer: function(value, metaData, record) { | |
| if (value == 'OK') { | |
| return 'OK'; | |
| } | |
| // metaData.attr = 'style="color:red;"'; | |
| return "ERROR: " + value; | |
| } | |
| } | |
| ], | |
| listeners: { | |
| itemdblclick: run_task_viewer, | |
| selectionchange: function(v, selections) { | |
| view_btn.setDisabled(!(selections && selections[0])); | |
| }, | |
| show: function() { reload_task.delay(10); }, | |
| destroy: function() { reload_task.cancel(); } | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| /*global Blob*/ | |
| Ext.define('PVE.node.SubscriptionKeyEdit', { | |
| extend: 'PVE.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: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveNodeSubscription'], | |
| features: [ {ftype: 'selectable'}], | |
| 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 | |
| }); | |
| PVE.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: PVE.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() { | |
| PVE.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() { | |
| PVE.Utils.checked_command(function (){ me.showReport(); }); | |
| } | |
| } | |
| ], | |
| rows: rows, | |
| listeners: { | |
| activate: reload | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.node.APT', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveNodeAPT'], | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| var store = Ext.create('Ext.data.Store', { | |
| model: 'apt-pkglist', | |
| groupField: 'Origin', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/nodes/" + nodename + "/apt/update" | |
| }, | |
| sorters: [ | |
| { | |
| property : 'Package', | |
| direction: 'ASC' | |
| } | |
| ] | |
| }); | |
| var groupingFeature = Ext.create('Ext.grid.feature.Grouping', { | |
| groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})', | |
| enableGroupingMenu: false | |
| }); | |
| var rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', { | |
| getAdditionalData: function (data, rowIndex, record, orig) { | |
| var headerCt = this.view.headerCt; | |
| var colspan = headerCt.getColumnCount(); | |
| // Usually you would style the my-body-class in CSS file | |
| return { | |
| rowBody: '<div style="padding: 1em">' + Ext.String.htmlEncode(data.Description) + '</div>', | |
| rowBodyColspan: colspan | |
| }; | |
| } | |
| }); | |
| var reload = function() { | |
| store.load(); | |
| }; | |
| me.loadCount = 1; // avoid duplicate load mask | |
| PVE.Utils.monStoreErrors(me, store); | |
| var apt_command = function(cmd){ | |
| PVE.Utils.API2Request({ | |
| url: "/nodes/" + nodename + "/apt/" + cmd, | |
| method: 'POST', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| }, | |
| success: function(response, opts) { | |
| var upid = response.result.data; | |
| var win = Ext.create('PVE.window.TaskViewer', { | |
| upid: upid | |
| }); | |
| win.show(); | |
| me.mon(win, 'close', reload); | |
| } | |
| }); | |
| }; | |
| var sm = Ext.create('Ext.selection.RowModel', {}); | |
| var update_btn = new Ext.Button({ | |
| text: gettext('Refresh'), | |
| handler: function(){ | |
| PVE.Utils.checked_command(function() { apt_command('update'); }); | |
| } | |
| }); | |
| var upgrade_btn = Ext.create('PVE.button.ConsoleButton', { | |
| disabled: !(PVE.UserName && PVE.UserName === 'root@pam'), | |
| text: gettext('Upgrade'), | |
| consoleType: 'upgrade', | |
| nodename: nodename | |
| }); | |
| var show_changelog = function(rec) { | |
| if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) { | |
| return; | |
| } | |
| var view = Ext.createWidget('component', { | |
| autoScroll: true, | |
| style: { | |
| 'background-color': 'white', | |
| 'white-space': 'pre', | |
| 'font-family': 'monospace', | |
| padding: '5px' | |
| } | |
| }); | |
| var win = Ext.create('Ext.window.Window', { | |
| title: gettext('Changelog') + ": " + rec.data.Package, | |
| width: 800, | |
| height: 400, | |
| layout: 'fit', | |
| modal: true, | |
| items: [ view ] | |
| }); | |
| PVE.Utils.API2Request({ | |
| waitMsgTarget: me, | |
| url: "/nodes/" + nodename + "/apt/changelog", | |
| params: { | |
| name: rec.data.Package, | |
| version: rec.data.Version | |
| }, | |
| method: 'GET', | |
| failure: function(response, opts) { | |
| win.close(); | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| }, | |
| success: function(response, opts) { | |
| win.show(); | |
| view.update(Ext.htmlEncode(response.result.data)); | |
| } | |
| }); | |
| }; | |
| var changelog_btn = new PVE.button.Button({ | |
| text: gettext('Changelog'), | |
| selModel: sm, | |
| disabled: true, | |
| enableFn: function(rec) { | |
| if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) { | |
| return false; | |
| } | |
| return true; | |
| }, | |
| handler: function(b, e, rec) { | |
| show_changelog(rec); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| stateful: false, | |
| selModel: sm, | |
| viewConfig: { | |
| stripeRows: false, | |
| emptyText: '<div style="display:table; width:100%; height:100%;"><div style="display:table-cell; vertical-align: middle; text-align:center;"><b>' + gettext('No updates available.') + '</div></div>' | |
| }, | |
| tbar: [ update_btn, upgrade_btn, changelog_btn ], | |
| features: [ groupingFeature, rowBodyFeature ], | |
| columns: [ | |
| { | |
| header: gettext('Package'), | |
| width: 200, | |
| sortable: true, | |
| dataIndex: 'Package' | |
| }, | |
| { | |
| text: gettext('Version'), | |
| columns: [ | |
| { | |
| header: gettext('current'), | |
| width: 100, | |
| sortable: false, | |
| dataIndex: 'OldVersion' | |
| }, | |
| { | |
| header: gettext('new'), | |
| width: 100, | |
| sortable: false, | |
| dataIndex: 'Version' | |
| } | |
| ] | |
| }, | |
| { | |
| header: gettext('Description'), | |
| sortable: false, | |
| dataIndex: 'Title', | |
| flex: 1 | |
| } | |
| ], | |
| listeners: { | |
| activate: reload, | |
| itemdblclick: function(v, rec) { | |
| show_changelog(rec); | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }, function() { | |
| Ext.define('apt-pkglist', { | |
| extend: 'Ext.data.Model', | |
| fields: [ 'Package', 'Title', 'Description', 'Section', 'Arch', | |
| 'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin' ], | |
| idProperty: 'Package' | |
| }); | |
| }); | |
| Ext.define('PVE.node.Config', { | |
| extend: 'PVE.panel.Config', | |
| alias: 'widget.PVE.node.Config', | |
| 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('PVE.data.ObjectStore', { | |
| url: "/api2/json/nodes/" + nodename + "/status", | |
| interval: 1000 | |
| }); | |
| var node_command = function(cmd) { | |
| PVE.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('More'), | |
| iconCls: 'fa fa-fw fa-ellipsis-v', | |
| disabled: !caps.nodes['Sys.PowerMgmt'], | |
| menu: new Ext.menu.Menu({ | |
| items: [ | |
| { | |
| text: gettext('Start all VMs and Containers'), | |
| iconCls: 'fa fa-fw fa-play', | |
| handler: function() { | |
| var msg = gettext('Start all VMs and Containers') + ' (' + gettext('Node') + " '" + nodename + "')"; | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| params: { force: 1 }, | |
| url: '/nodes/' + nodename + '/startall', | |
| method: 'POST', | |
| waitMsgTarget: me, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| } | |
| }); | |
| }); | |
| } | |
| }, | |
| { | |
| text: gettext('Stop all VMs and Containers'), | |
| iconCls: 'fa fa-fw fa-stop fa-red', | |
| handler: function() { | |
| var msg = gettext('Stop all VMs and Containers') + ' (' + gettext('Node') + " '" + nodename + "')"; | |
| Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { | |
| if (btn !== 'yes') { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + nodename + '/stopall', | |
| method: 'POST', | |
| waitMsgTarget: me, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| } | |
| }); | |
| }); | |
| } | |
| }, | |
| { | |
| text: gettext('Migrate all VMs and Containers'), | |
| iconCls: 'fa fa-fw fa-send-o', | |
| handler: function() { | |
| var win = Ext.create('PVE.window.MigrateAll', { | |
| nodename: nodename | |
| }); | |
| win.show(); | |
| } | |
| } | |
| ] | |
| }) | |
| }); | |
| var restartBtn = Ext.create('PVE.button.Button', { | |
| text: gettext('Restart'), | |
| disabled: !caps.nodes['Sys.PowerMgmt'], | |
| confirmMsg: gettext('Node') + " '" + nodename + "' - " + gettext('Restart'), | |
| handler: function() { | |
| node_command('reboot'); | |
| }, | |
| iconCls: 'fa fa-undo' | |
| }); | |
| var shutdownBtn = Ext.create('PVE.button.Button', { | |
| text: gettext('Shutdown'), | |
| disabled: !caps.nodes['Sys.PowerMgmt'], | |
| confirmMsg: gettext('Node') + " '" + nodename + "' - " + gettext('Shutdown'), | |
| 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, | |
| iconCls: 'fa fa-terminal' | |
| }); | |
| 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'), | |
| itemId: 'summary', | |
| xtype: 'pveNodeSummary' | |
| }, | |
| { | |
| title: gettext('Services'), | |
| itemId: 'services', | |
| xtype: 'pveNodeServiceView' | |
| }, | |
| { | |
| title: gettext('Network'), | |
| itemId: 'network', | |
| xtype: 'pveNodeNetworkView' | |
| }, | |
| { | |
| title: gettext('DNS'), | |
| itemId: 'dns', | |
| xtype: 'pveNodeDNSView' | |
| }, | |
| { | |
| title: gettext('Time'), | |
| itemId: 'time', | |
| xtype: 'pveNodeTimeView' | |
| } | |
| ); | |
| } | |
| if (caps.nodes['Sys.Syslog']) { | |
| me.items.push( | |
| { | |
| title: 'Syslog', | |
| itemId: 'syslog', | |
| xtype: 'pveLogView', | |
| url: "/api2/extjs/nodes/" + nodename + "/syslog", | |
| log_select_timespan: 1 | |
| } | |
| ); | |
| } | |
| me.items.push( | |
| { | |
| title: gettext('Task History'), | |
| itemId: 'tasks', | |
| xtype: 'pveNodeTasks' | |
| } | |
| ); | |
| if (caps.nodes['Sys.Console']) { | |
| me.items.push( | |
| { | |
| xtype: 'pveFirewallPanel', | |
| title: gettext('Firewall'), | |
| base_url: '/nodes/' + nodename + '/firewall', | |
| fwtype: 'node', | |
| phstateid: me.hstateid, | |
| itemId: 'firewall' | |
| }, | |
| { | |
| title: gettext('Updates'), | |
| itemId: 'apt', | |
| xtype: 'pveNodeAPT', | |
| nodename: nodename | |
| }, | |
| { | |
| title: gettext('Console'), | |
| itemId: 'console', | |
| xtype: 'pveNoVncConsole', | |
| consoleType: 'shell', | |
| nodename: nodename | |
| }, | |
| { | |
| title: 'Ceph', | |
| itemId: 'ceph', | |
| xtype: 'pveNodeCeph', | |
| phstateid: me.hstateid, | |
| nodename: nodename | |
| } | |
| ); | |
| } | |
| me.items.push( | |
| { | |
| title: gettext('Subscription'), | |
| itemId: 'support', | |
| xtype: 'pveNodeSubscription', | |
| nodename: nodename | |
| } | |
| ); | |
| me.callParent(); | |
| me.mon(me.statusStore, 'load', function(s, records, success) { | |
| var uptimerec = s.data.get('uptime'); | |
| var powermgmt = uptimerec ? uptimerec.data.value : false; | |
| if (!caps.nodes['Sys.PowerMgmt']) { | |
| powermgmt = false; | |
| } | |
| restartBtn.setDisabled(!powermgmt); | |
| shutdownBtn.setDisabled(!powermgmt); | |
| shellBtn.setDisabled(!powermgmt); | |
| }); | |
| me.on('afterrender', function() { | |
| me.statusStore.startUpdate(); | |
| }); | |
| me.on('destroy', function() { | |
| me.statusStore.stopUpdate(); | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.StatusView', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveQemuStatusView'], | |
| disabled: true, | |
| 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 render_cpu = function(value, metaData, record, rowIndex, colIndex, store) { | |
| if (!me.getObjectValue('uptime')) { | |
| return '-'; | |
| } | |
| var maxcpu = me.getObjectValue('cpus', 1); | |
| if (!(Ext.isNumeric(value) && Ext.isNumeric(maxcpu) && (maxcpu >= 1))) { | |
| return '-'; | |
| } | |
| var per = (value * 100); | |
| return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU'); | |
| }; | |
| var render_mem = function(value, metaData, record, rowIndex, colIndex, store) { | |
| var maxmem = me.getObjectValue('maxmem', 0); | |
| var per = (value / maxmem)*100; | |
| var text = "<div>" + PVE.Utils.totalText + ": " + PVE.Utils.format_size(maxmem) + "</div>" + | |
| "<div>" + PVE.Utils.usedText + ": " + PVE.Utils.format_size(value) + "</div>"; | |
| return text; | |
| }; | |
| var rows = {}; | |
| if (template) { | |
| rows = { | |
| name: { header: gettext('Name'), defaultValue: 'no name specified' }, | |
| cpus: { header: gettext('Processors'), required: true}, | |
| maxmem: { header: gettext('Memory'), renderer: PVE.Utils.render_size, required: true}, | |
| maxdisk: { header: gettext('Bootdisk size'), renderer: PVE.Utils.render_size, required: true} | |
| }; | |
| } else { | |
| rows = { | |
| name: { header: gettext('Name'), defaultValue: 'no name specified' }, | |
| qmpstatus: { header: gettext('Status'), defaultValue: 'unknown' }, | |
| cpu: { iconCls: 'fa fa-up', header: gettext('CPU usage'), required: true, renderer: render_cpu }, | |
| cpus: { visible: false }, | |
| mem: { header: gettext('Memory usage'), required: true, renderer: render_mem }, | |
| maxmem: { visible: false }, | |
| maxdisk: { header: gettext('Bootdisk size'), renderer: PVE.Utils.render_size, required: true}, | |
| uptime: { header: gettext('Uptime'), required: true, renderer: PVE.Utils.render_uptime }, | |
| ha: { header: gettext('Managed by HA'), required: true, renderer: PVE.Utils.format_ha } | |
| }; | |
| } | |
| Ext.applyIf(me, { | |
| cwidth1: 150, | |
| rows: rows | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.window.Migrate', { | |
| extend: 'Ext.window.Window', | |
| resizable: false, | |
| migrate: function(target, online) { | |
| var me = this; | |
| PVE.Utils.API2Request({ | |
| params: { target: target, online: online }, | |
| url: '/nodes/' + me.nodename + '/' + me.vmtype + '/' + me.vmid + "/migrate", | |
| 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('PVE.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"; | |
| } | |
| if (!me.vmtype) { | |
| throw "no VM type specified"; | |
| } | |
| var running = false; | |
| var vmrec = PVE.data.ResourceStore.findRecord('vmid', me.vmid, | |
| 0, false, false, true); | |
| if (vmrec && vmrec.data && vmrec.data.running) { | |
| running = true; | |
| } | |
| me.formPanel = Ext.create('Ext.form.Panel', { | |
| bodyPadding: 10, | |
| border: false, | |
| fieldDefaults: { | |
| labelWidth: 100, | |
| anchor: '100%' | |
| }, | |
| items: [ | |
| { | |
| xtype: 'pveNodeSelector', | |
| name: 'target', | |
| fieldLabel: gettext('Target node'), | |
| allowBlank: false, | |
| onlineValidator: true | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'online', | |
| uncheckedValue: 0, | |
| defaultValue: 0, | |
| checked: running, | |
| fieldLabel: gettext('Online') | |
| } | |
| ] | |
| }); | |
| var form = me.formPanel.getForm(); | |
| var submitBtn = Ext.create('Ext.Button', { | |
| text: gettext('Migrate'), | |
| handler: function() { | |
| var values = form.getValues(); | |
| me.migrate(values.target, values.online); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| title: gettext('Migrate') + ((me.vmtype === 'qemu')?' VM ':' CT ') + me.vmid, | |
| width: 350, | |
| modal: true, | |
| layout: 'auto', | |
| border: false, | |
| items: [ me.formPanel ], | |
| buttons: [ submitBtn ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.window.MigrateAll', { | |
| extend: 'Ext.window.Window', | |
| resizable: false, | |
| migrate: function(target, maxworkers) { | |
| var me = this; | |
| PVE.Utils.API2Request({ | |
| params: { target: target, maxworkers: maxworkers}, | |
| url: '/nodes/' + me.nodename + '/' + "/migrateall", | |
| 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('PVE.window.TaskViewer', { | |
| upid: upid | |
| }); | |
| win.show(); | |
| me.close(); | |
| } | |
| }); | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.nodename) { | |
| throw "no node name specified"; | |
| } | |
| me.formPanel = Ext.create('Ext.form.Panel', { | |
| bodyPadding: 10, | |
| border: false, | |
| fieldDefaults: { | |
| labelWidth: 100, | |
| anchor: '100%' | |
| }, | |
| items: [ | |
| { | |
| xtype: 'pveNodeSelector', | |
| name: 'target', | |
| fieldLabel: 'Target node', | |
| allowBlank: false, | |
| onlineValidator: true | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'maxworkers', | |
| minValue: 1, | |
| maxValue: 100, | |
| value: 1, | |
| fieldLabel: 'Parallel jobs', | |
| allowBlank: false | |
| } | |
| ] | |
| }); | |
| var form = me.formPanel.getForm(); | |
| var submitBtn = Ext.create('Ext.Button', { | |
| text: 'Migrate', | |
| handler: function() { | |
| var values = form.getValues(); | |
| me.migrate(values.target, values.maxworkers); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| title: "Migrate All VMs", | |
| width: 350, | |
| modal: true, | |
| layout: 'auto', | |
| border: false, | |
| items: [ me.formPanel ], | |
| buttons: [ submitBtn ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| 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 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)); | |
| refresh(); | |
| PVE.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) { | |
| if (e.getKey() === e.ENTER) { | |
| var cmd = f.getValue(); | |
| f.setValue(''); | |
| executeCmd(cmd); | |
| } else if (e.getKey() === e.PAGE_UP) { | |
| textbox.scrollBy(0, -0.9*textbox.getHeight(), false); | |
| } else if (e.getKey() === e.PAGE_DOWN) { | |
| textbox.scrollBy(0, 0.9*textbox.getHeight(), false); | |
| } | |
| } | |
| } | |
| } | |
| ], | |
| 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: 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"; | |
| } | |
| 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 statusview = Ext.create('PVE.qemu.StatusView', { | |
| title: gettext('Status'), | |
| pveSelNode: me.pveSelNode, | |
| width: template ? 800 : 400, | |
| rstore: rstore | |
| }); | |
| var notesview = Ext.create('PVE.panel.NotesView', { | |
| pveSelNode: me.pveSelNode, | |
| padding: template? '10 0 0 0' : '0 0 0 10', | |
| flex: 1 | |
| }); | |
| if (template) { | |
| Ext.apply(me, { | |
| plugins: { | |
| ptype: 'lazyitems', | |
| items: [ | |
| { | |
| xtype: 'container', | |
| layout: { | |
| type: 'column' | |
| }, | |
| defaults: { | |
| padding: '0 10 10 0' | |
| }, | |
| items: [{ | |
| width: 800, | |
| layout: { | |
| type: 'vbox', | |
| align: 'stretch' | |
| }, | |
| border: false, | |
| items: [ statusview, notesview ] | |
| }] | |
| } | |
| ] | |
| }, | |
| listeners: { | |
| activate: function() { notesview.load(); } | |
| } | |
| }); | |
| } else { | |
| var rrdstore = Ext.create('PVE.data.RRDStore', { | |
| rrdurl: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/rrddata" | |
| }); | |
| Ext.apply(me, { | |
| tbar: [ '->', { xtype: 'pveRRDTypeSelector' } ], | |
| plugins: { | |
| ptype: 'lazyitems', | |
| items: [ | |
| { | |
| xtype: 'container', | |
| layout: { | |
| type: 'column' | |
| }, | |
| defaults: { | |
| padding: '0 10 10 0' | |
| }, | |
| items: [ | |
| { | |
| width: 800, | |
| height: 300, | |
| layout: { | |
| type: 'hbox', | |
| align: 'stretch' | |
| }, | |
| border: false, | |
| items: [ statusview, notesview ] | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('CPU usage'), | |
| pveSelNode: me.pveSelNode, | |
| fields: ['cpu'], | |
| fieldTitles: [gettext('CPU usage')], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Memory usage'), | |
| pveSelNode: me.pveSelNode, | |
| fields: ['maxmem', 'mem'], | |
| fieldTitles: [gettext('Total'), gettext('RAM usage')], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Network traffic'), | |
| pveSelNode: me.pveSelNode, | |
| fields: ['netin','netout'], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Disk IO'), | |
| pveSelNode: me.pveSelNode, | |
| fields: ['diskread','diskwrite'], | |
| store: rrdstore | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| listeners: { | |
| activate: function() {notesview.load(); rrdstore.startUpdate();}, | |
| hide: rrdstore.stopUpdate, | |
| destroy: rrdstore.stopUpdate | |
| } | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.OSTypeInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.OSTypeInputPanel', | |
| onlineHelp: 'chapter-qm.html#_os_settings', | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: 'component', | |
| html: 'Microsoft Windows', | |
| cls:'x-form-check-group-label' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'win8' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'win7' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'w2k8' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'wxp' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'w2k' | |
| } | |
| ]; | |
| me.column2 = [ | |
| { | |
| xtype: 'component', | |
| html: 'Linux/' + gettext('Other OS types'), | |
| cls:'x-form-check-group-label' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'l26' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'l24' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'solaris' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'ostype', | |
| inputValue: 'other' | |
| } | |
| ]; | |
| Ext.Array.each(me.column1, function(def) { | |
| if (def.inputValue) { | |
| def.boxLabel = PVE.Utils.render_kvm_ostype(def.inputValue); | |
| } | |
| }); | |
| Ext.Array.each(me.column2, function(def) { | |
| if (def.inputValue) { | |
| def.boxLabel = PVE.Utils.render_kvm_ostype(def.inputValue); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| useFieldContainer: { | |
| xtype: 'radiogroup', | |
| allowBlank: false | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.OSTypeEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| Ext.apply(me, { | |
| subject: 'OS Type', | |
| items: Ext.create('PVE.qemu.OSTypeInputPanel') | |
| }); | |
| me.callParent(); | |
| me.load({ | |
| success: function(response, options) { | |
| var value = response.result.data.ostype || 'other'; | |
| me.setValues({ ostype: value}); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.ProcessorInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.ProcessorInputPanel', | |
| onlineHelp: 'chapter-qm.html#_cpu', | |
| onGetValues: function(values) { | |
| var me = this; | |
| // build the cpu options: | |
| me.cpu.cputype = values.cputype; | |
| delete values.cputype; | |
| 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; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.cpu = {}; | |
| me.column1 = [ | |
| { | |
| xtype: 'numberfield', | |
| name: 'sockets', | |
| minValue: 1, | |
| maxValue: 4, | |
| value: '1', | |
| fieldLabel: gettext('Sockets'), | |
| allowBlank: false, | |
| listeners: { | |
| change: function(f, value) { | |
| var sockets = me.down('field[name=sockets]').getValue(); | |
| var cores = me.down('field[name=cores]').getValue(); | |
| me.down('field[name=totalcores]').setValue(sockets*cores); | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'cores', | |
| minValue: 1, | |
| maxValue: 128, | |
| value: '1', | |
| fieldLabel: gettext('Cores'), | |
| allowBlank: false, | |
| listeners: { | |
| change: function(f, value) { | |
| var sockets = me.down('field[name=sockets]').getValue(); | |
| var cores = me.down('field[name=cores]').getValue(); | |
| me.down('field[name=totalcores]').setValue(sockets*cores); | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| fieldLabel: gettext('Enable NUMA'), | |
| name: 'numa', | |
| uncheckedValue: 0 | |
| } | |
| ]; | |
| me.column2 = [ | |
| { | |
| xtype: 'CPUModelSelector', | |
| name: 'cputype', | |
| value: '__default__', | |
| fieldLabel: gettext('Type') | |
| }, | |
| { | |
| xtype: 'displayfield', | |
| fieldLabel: gettext('Total cores'), | |
| name: 'totalcores', | |
| value: '1' | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.ProcessorEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel'); | |
| Ext.apply(me, { | |
| subject: gettext('Processors'), | |
| items: ipanel | |
| }); | |
| me.callParent(); | |
| me.load({ | |
| success: function(response, options) { | |
| var data = response.result.data; | |
| var value = data.cpu; | |
| if (value) { | |
| var cpu = PVE.Parser.parseQemuCpu(value); | |
| ipanel.cpu = cpu; | |
| data.cputype = cpu.cputype; | |
| } | |
| me.setValues(data); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.BootOrderPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.pveQemuBootOrderPanel', | |
| vmconfig: {}, // store loaded vm config | |
| bootdisk: undefined, | |
| selection: [], | |
| list: [], | |
| comboboxes: [], | |
| isBootDisk: function(value) { | |
| return (/^(ide|sata|scsi|virtio)\d+$/).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__', PVE.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('pveKVComboBox', { | |
| fieldLabel: gettext('Boot device') + " 1", | |
| labelWidth: 120, | |
| name: 'bd1', | |
| allowBlank: false, | |
| listeners: { | |
| change: me.recomputeSelection | |
| } | |
| }), | |
| Ext.createWidget('pveKVComboBox', { | |
| fieldLabel: gettext('Boot device') + " 2", | |
| labelWidth: 120, | |
| name: 'bd2', | |
| allowBlank: false, | |
| listeners: { | |
| change: me.recomputeSelection | |
| } | |
| }), | |
| Ext.createWidget('pveKVComboBox', { | |
| 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: 'PVE.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: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.MemoryInputPanel', | |
| onlineHelp: 'chapter-qm.html#_memory', | |
| insideWizard: false, | |
| onGetValues: function(values) { | |
| var me = this; | |
| var res; | |
| if (values.memoryType === 'fixed') { | |
| res = { memory: values.memory }; | |
| res['delete'] = "balloon,shares"; | |
| } else { | |
| res = { | |
| memory: values.maxmemory, | |
| balloon: values.balloon | |
| }; | |
| 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; | |
| var items = [ | |
| { | |
| xtype: 'radiofield', | |
| name: 'memoryType', | |
| inputValue: 'fixed', | |
| boxLabel: gettext('Use fixed size memory'), | |
| checked: true, | |
| listeners: { | |
| change: function(f, value) { | |
| if (!me.rendered) { | |
| return; | |
| } | |
| me.down('field[name=memory]').setDisabled(!value); | |
| me.down('field[name=maxmemory]').setDisabled(value); | |
| me.down('field[name=balloon]').setDisabled(value); | |
| me.down('field[name=shares]').setDisabled(value); | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'pveMemoryField', | |
| name: 'memory', | |
| hotplug: me.hotplug, | |
| fieldLabel: gettext('Memory') + ' (MB)', | |
| labelAlign: 'right', | |
| labelWidth: labelWidth | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'memoryType', | |
| inputValue: 'dynamic', | |
| boxLabel: gettext('Automatically allocate memory within this range'), | |
| listeners: { | |
| change: function(f, value) { | |
| if (!me.rendered) { | |
| return; | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'pveMemoryField', | |
| name: 'maxmemory', | |
| hotplug: me.hotplug, | |
| disabled: true, | |
| value: '1024', | |
| fieldLabel: gettext('Maximum memory') + ' (MB)', | |
| labelAlign: 'right', | |
| labelWidth: labelWidth, | |
| listeners: { | |
| change: function(f, value) { | |
| var bf = me.down('field[name=balloon]'); | |
| var balloon = bf.getValue(); | |
| if (balloon > value) { | |
| bf.setValue(value); | |
| } | |
| bf.setMaxValue(value); | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'balloon', | |
| disabled: true, | |
| minValue: 0, | |
| maxValue: 512*1024, | |
| value: '512', | |
| step: 32, | |
| fieldLabel: gettext('Minimum memory') + ' (MB)', | |
| labelAlign: 'right', | |
| labelWidth: labelWidth, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'shares', | |
| disabled: true, | |
| minValue: 0, | |
| maxValue: 50000, | |
| value: '', | |
| step: 10, | |
| fieldLabel: gettext('Shares'), | |
| labelAlign: 'right', | |
| labelWidth: labelWidth, | |
| allowBlank: true, | |
| emptyText: PVE.Utils.defaultText + ' (1000)', | |
| submitEmptyText: false | |
| } | |
| ]; | |
| if (me.insideWizard) { | |
| me.column1 = items; | |
| } else { | |
| me.items = items; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.MemoryEdit', { | |
| extend: 'PVE.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 = { | |
| memory: data.memory, | |
| maxmemory: data.memory, | |
| balloon: data.balloon, | |
| shares: data.shares, | |
| memoryType: data.balloon ? 'dynamic' : 'fixed' | |
| }; | |
| ipanel.setValues(values); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.NetworkInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.NetworkInputPanel', | |
| onlineHelp: 'chapter-qm.html#_network_device', | |
| insideWizard: false, | |
| onGetValues: function(values) { | |
| var me = this; | |
| me.network.model = values.model; | |
| if (values.networkmode === 'none') { | |
| return {}; | |
| } else if (values.networkmode === 'bridge') { | |
| me.network.bridge = values.bridge; | |
| me.network.tag = values.tag; | |
| me.network.firewall = values.firewall; | |
| } else { | |
| me.network.bridge = undefined; | |
| } | |
| 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.bridgesel = Ext.create('PVE.form.BridgeSelector', { | |
| name: 'bridge', | |
| fieldLabel: gettext('Bridge'), | |
| nodename: me.nodename, | |
| labelAlign: 'right', | |
| autoSelect: true, | |
| allowBlank: false | |
| }); | |
| me.column1 = [ | |
| { | |
| xtype: 'radiofield', | |
| name: 'networkmode', | |
| inputValue: 'bridge', | |
| boxLabel: gettext('Bridged mode'), | |
| checked: true, | |
| listeners: { | |
| change: function(f, value) { | |
| if (!me.rendered) { | |
| return; | |
| } | |
| me.down('field[name=bridge]').setDisabled(!value); | |
| me.down('field[name=bridge]').validate(); | |
| me.down('field[name=tag]').setDisabled(!value); | |
| me.down('field[name=firewall]').setDisabled(!value); | |
| } | |
| } | |
| }, | |
| me.bridgesel, | |
| { | |
| xtype: 'pveVlanField', | |
| name: 'tag', | |
| value: '', | |
| labelAlign: 'right' | |
| }, | |
| me.bridgesel, | |
| { | |
| xtype: 'pvecheckbox', | |
| fieldLabel: gettext('Firewall'), | |
| name: 'firewall', | |
| labelAlign: 'right' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| name: 'networkmode', | |
| inputValue: 'nat', | |
| boxLabel: gettext('NAT mode') | |
| } | |
| ]; | |
| if (me.insideWizard) { | |
| me.column1.push({ | |
| xtype: 'radiofield', | |
| name: 'networkmode', | |
| inputValue: 'none', | |
| boxLabel: gettext('No network device') | |
| }); | |
| } | |
| me.column2 = [ | |
| { | |
| xtype: 'PVE.form.NetworkCardSelector', | |
| name: 'model', | |
| fieldLabel: gettext('Model'), | |
| value: 'e1000', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'textfield', | |
| name: 'macaddr', | |
| fieldLabel: gettext('MAC address'), | |
| vtype: 'MacAddress', | |
| allowBlank: true, | |
| emptyText: 'auto' | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'rate', | |
| fieldLabel: gettext('Rate limit') + ' (MB/s)', | |
| minValue: 0, | |
| maxValue: 10*1024, | |
| value: '', | |
| emptyText: 'unlimited', | |
| allowBlank: true | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'queues', | |
| fieldLabel: gettext('Multiqueues'), | |
| minValue: 1, | |
| maxValue: 8, | |
| value: '', | |
| allowBlank: true | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| fieldLabel: gettext('Disconnect'), | |
| name: 'disconnect' | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.NetworkEdit', { | |
| extend: 'PVE.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.create = me.confid ? false : true; | |
| var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', { | |
| confid: me.confid, | |
| nodename: nodename | |
| }); | |
| 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.create) { | |
| 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: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.Smbios1InputPanel', | |
| insideWizard: false, | |
| smbios1: {}, | |
| onGetValues: function(values) { | |
| var me = this; | |
| var params = { | |
| smbios1: PVE.Parser.printQemuSmbios1(values) | |
| }; | |
| return params; | |
| }, | |
| setSmbios1: function(data) { | |
| var me = this; | |
| me.smbios1 = data; | |
| me.setValues(me.smbios1); | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.items = [ | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: 'UUID', | |
| regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/, | |
| name: 'uuid' | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Manufacturer'), | |
| regex: /^\S+$/, | |
| name: 'manufacturer' | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Product'), | |
| regex: /^\S+$/, | |
| name: 'product' | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Version'), | |
| regex: /^\S+$/, | |
| name: 'version' | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Serial'), | |
| regex: /^\S+$/, | |
| name: 'serial' | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('SKU'), | |
| regex: /^\S+$/, | |
| name: 'sku' | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Family'), | |
| regex: /^\S+$/, | |
| name: 'family' | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.Smbios1Edit', { | |
| extend: 'PVE.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); | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| // fixme: howto avoid jslint type confusion? | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.qemu.CDInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.CDInputPanel', | |
| 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.createWidget('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') | |
| }); | |
| if (me.insideWizard) { | |
| me.column1 = items; | |
| } else { | |
| me.items = items; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.CDEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| var nodename = me.pveSelNode.data.node; | |
| if (!nodename) { | |
| throw "no node name specified"; | |
| } | |
| me.create = 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); | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| // fixme: howto avoid jslint type confusion? | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.qemu.HDInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.HDInputPanel', | |
| onlineHelp: 'chapter-qm.html#_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(); | |
| this.lookup('iothread').setDisabled(!value.match(/^(virtio|scsi)/)); | |
| }, | |
| control: { | |
| 'field[name=controller]': { | |
| change: 'onControllerChange', | |
| afterrender: 'onControllerChange' | |
| }, | |
| 'field[name=hdstorage]': { | |
| change: function(f, value) { | |
| if (!value) { // initial store loading fires an unwanted 'change' | |
| return; | |
| } | |
| var me = this.getView(); | |
| var rec = f.store.getById(value); | |
| if (rec.data.type === 'iscsi') { | |
| me.hdfilesel.setStorage(value); | |
| me.hdfilesel.setDisabled(false); | |
| me.formatsel.setValue('raw'); | |
| me.formatsel.setDisabled(true); | |
| me.hdfilesel.setVisible(true); | |
| me.hdsizesel.setDisabled(true); | |
| me.hdsizesel.setVisible(false); | |
| } else if (rec.data.type === 'lvm' || | |
| rec.data.type === 'lvmthin' || | |
| rec.data.type === 'drbd' || | |
| rec.data.type === 'rbd' || | |
| rec.data.type === 'sheepdog' || | |
| rec.data.type === 'zfs' || | |
| rec.data.type === 'zfspool') { | |
| me.hdfilesel.setDisabled(true); | |
| me.hdfilesel.setVisible(false); | |
| me.formatsel.setValue('raw'); | |
| me.formatsel.setDisabled(true); | |
| me.hdsizesel.setDisabled(false); | |
| me.hdsizesel.setVisible(true); | |
| } else { | |
| me.hdfilesel.setDisabled(true); | |
| me.hdfilesel.setVisible(false); | |
| me.formatsel.setValue('qcow2'); | |
| me.formatsel.setDisabled(false); | |
| me.hdsizesel.setDisabled(false); | |
| me.hdsizesel.setVisible(true); | |
| } | |
| } | |
| } | |
| } | |
| }, | |
| onGetValues: function(values) { | |
| var me = this; | |
| 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.create) { | |
| 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.discard) { | |
| me.drive.discard = 'on'; | |
| } else { | |
| delete me.drive.discard; | |
| } | |
| if (values.iothread && confid.match(/^(virtio|scsi)\d+$/)) { | |
| me.drive.iothread = 'on'; | |
| } else { | |
| delete me.drive.iothread; | |
| } | |
| if (values.cache) { | |
| me.drive.cache = values.cache; | |
| } else { | |
| delete me.drive.cache; | |
| } | |
| var params = {}; | |
| 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, true); | |
| } | |
| 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.diskformat = drive.format || 'raw'; | |
| values.cache = drive.cache || '__default__'; | |
| values.discard = (drive.discard === 'on'); | |
| values.iothread = PVE.Parser.parseBoolean(drive.iothread); | |
| me.setValues(values); | |
| }, | |
| setNodename: function(nodename) { | |
| var me = this; | |
| me.hdstoragesel.setNodename(nodename); | |
| me.hdfilesel.setStorage(undefined, nodename); | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.drive = {}; | |
| me.column1 = []; | |
| me.column2 = []; | |
| if (!me.confid || me.unused) { | |
| me.bussel = Ext.createWidget('PVE.form.ControllerSelector', { | |
| vmconfig: me.insideWizard ? {ide2: 'cdrom'} : {} | |
| }); | |
| me.column1.push(me.bussel); | |
| } | |
| if (me.unused) { | |
| me.unusedDisks = Ext.create('PVE.form.KVComboBox', { | |
| name: 'unusedId', | |
| fieldLabel: gettext('Disk image'), | |
| matchFieldWidth: false, | |
| listConfig: { | |
| width: 350 | |
| }, | |
| data: [], | |
| allowBlank: false | |
| }); | |
| me.column1.push(me.unusedDisks); | |
| } else if (me.create) { | |
| me.formatsel = Ext.create('PVE.form.DiskFormatSelector', { | |
| name: 'diskformat', | |
| fieldLabel: gettext('Format'), | |
| value: 'qcow2', | |
| allowBlank: false | |
| }); | |
| me.hdfilesel = Ext.create('PVE.form.FileSelector', { | |
| name: 'hdimage', | |
| nodename: me.nodename, | |
| storageContent: 'images', | |
| fieldLabel: gettext('Disk image'), | |
| disabled: true, | |
| hidden: true, | |
| allowBlank: false | |
| }); | |
| me.hdsizesel = Ext.createWidget('numberfield', { | |
| name: 'disksize', | |
| minValue: 0.001, | |
| maxValue: 128*1024, | |
| decimalPrecision: 3, | |
| value: '32', | |
| fieldLabel: gettext('Disk size') + ' (GB)', | |
| allowBlank: false | |
| }); | |
| me.hdstoragesel = Ext.create('PVE.form.StorageSelector', { | |
| name: 'hdstorage', | |
| nodename: me.nodename, | |
| fieldLabel: gettext('Storage'), | |
| storageContent: 'images', | |
| autoSelect: me.insideWizard, | |
| allowBlank: false | |
| }); | |
| me.column1.push(me.hdstoragesel); | |
| me.column1.push(me.hdfilesel); | |
| me.column1.push(me.hdsizesel); | |
| me.column1.push(me.formatsel); | |
| } 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') | |
| }); | |
| me.column2.push({ | |
| xtype: 'pvecheckbox', | |
| fieldLabel: gettext('No backup'), | |
| name: 'nobackup' | |
| }); | |
| me.column2.push({ | |
| xtype: 'pvecheckbox', | |
| fieldLabel: gettext('Discard'), | |
| name: 'discard' | |
| }); | |
| me.column2.push({ | |
| xtype: 'pvecheckbox', | |
| disabled: me.insideWizard || (me.confid && !me.confid.match(/^(virtio|scsi)/)), | |
| fieldLabel: gettext('IO thread'), | |
| reference: 'iothread', | |
| name: 'iothread' | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.HDEdit', { | |
| extend: 'PVE.window.Edit', | |
| isAdd: true, | |
| 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.create = me.confid ? unused : true; | |
| var ipanel = Ext.create('PVE.qemu.HDInputPanel', { | |
| confid: me.confid, | |
| nodename: nodename, | |
| unused: unused, | |
| create: me.create | |
| }); | |
| var subject; | |
| if (unused) { | |
| me.subject = gettext('Unused Disk'); | |
| } else if (me.create) { | |
| me.subject = gettext('Hard Disk'); | |
| } else { | |
| me.subject = gettext('Hard Disk') + ' (' + me.confid + ')'; | |
| } | |
| me.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(gettext('Error'), 'Unable to parse drive options'); | |
| me.close(); | |
| return; | |
| } | |
| ipanel.setDrive(drive); | |
| me.isValid(); // trigger validation | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| 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' }; | |
| PVE.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') + ' (GB)', | |
| 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 params = { disk: disk, storage: storage }; | |
| if (format) { | |
| params.format = format; | |
| } | |
| if (delete_disk) { | |
| params['delete'] = 1; | |
| } | |
| PVE.Utils.API2Request({ | |
| params: params, | |
| url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/move_disk', | |
| 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('PVE.window.TaskViewer', { upid: upid }); | |
| win.show(); | |
| 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"; | |
| } | |
| var items = [ | |
| { | |
| xtype: 'displayfield', | |
| name: 'disk', | |
| value: me.disk, | |
| fieldLabel: gettext('Disk'), | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| } | |
| ]; | |
| me.hdstoragesel = Ext.create('PVE.form.StorageSelector', { | |
| name: 'hdstorage', | |
| nodename: me.nodename, | |
| fieldLabel: gettext('Target Storage'), | |
| storageContent: 'images', | |
| autoSelect: me.insideWizard, | |
| allowBlank: true, | |
| disabled: false, | |
| hidden: false, | |
| listeners: { | |
| change: function(f, value) { | |
| if (!value) { // initial store loading fires an unwanted 'change | |
| return; | |
| } | |
| var rec = f.store.getById(value); | |
| if (rec.data.type === 'iscsi' || | |
| rec.data.type === 'lvm' || | |
| rec.data.type === 'lvmthin' || | |
| rec.data.type === 'rbd' || | |
| rec.data.type === 'sheepdog' || | |
| rec.data.type === 'zfs' || | |
| rec.data.type === 'zfspool' | |
| ) { | |
| me.formatsel.setValue('raw'); | |
| me.formatsel.setDisabled(true); | |
| } else { | |
| me.formatsel.setDisabled(false); | |
| } | |
| } | |
| } | |
| }); | |
| me.formatsel = Ext.create('PVE.form.DiskFormatSelector', { | |
| name: 'diskformat', | |
| fieldLabel: gettext('Format'), | |
| value: 'raw', | |
| disabled: true, | |
| hidden: false, | |
| allowBlank: false | |
| }); | |
| items.push(me.hdstoragesel); | |
| items.push(me.formatsel); | |
| items.push({ | |
| xtype: 'pvecheckbox', | |
| 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 = gettext("Move disk"); | |
| submitBtn = Ext.create('Ext.Button', { | |
| text: gettext('Move disk'), | |
| 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(); | |
| } | |
| }); | |
| // fixme: howto avoid jslint type confusion? | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.qemu.HDThrottleInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.HDThrottleInputPanel', | |
| insideWizard: false, | |
| unused: false, // ADD usused disk imaged | |
| vmconfig: {}, // used to select usused disks | |
| onGetValues: function(values) { | |
| var me = this; | |
| var confid = me.confid; | |
| 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]; | |
| } | |
| }); | |
| var params = {}; | |
| params[confid] = PVE.Parser.printQemuDrive(me.drive); | |
| return params; | |
| }, | |
| setDrive: function(drive) { | |
| var me = this; | |
| me.drive = drive; | |
| var values = {}; | |
| 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); | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.drive = {}; | |
| me.column1 = []; | |
| me.column2 = []; | |
| var width2 = 140; | |
| me.mbps_rd = Ext.widget('numberfield', { | |
| name: 'mbps_rd', | |
| minValue: 1, | |
| step: 1, | |
| fieldLabel: gettext('Read limit') + ' (MB/s)', | |
| labelWidth: width2, | |
| emptyText: gettext('unlimited') | |
| }); | |
| me.column1.push(me.mbps_rd); | |
| me.mbps_rd_max = Ext.widget('numberfield', { | |
| name: 'mbps_rd_max', | |
| minValue: 1, | |
| step: 1, | |
| fieldLabel: gettext('Read max burst') + ' (MB)', | |
| labelWidth: width2, | |
| emptyText: gettext('default') | |
| }); | |
| me.column2.push(me.mbps_rd_max); | |
| me.mbps_wr = Ext.widget('numberfield', { | |
| name: 'mbps_wr', | |
| minValue: 1, | |
| step: 1, | |
| fieldLabel: gettext('Write limit') + ' (MB/s)', | |
| labelWidth: width2, | |
| emptyText: gettext('unlimited') | |
| }); | |
| me.column1.push(me.mbps_wr); | |
| me.mbps_wr_max = Ext.widget('numberfield', { | |
| name: 'mbps_wr_max', | |
| minValue: 1, | |
| step: 1, | |
| fieldLabel: gettext('Write max burst') + ' (MB)', | |
| labelWidth: width2, | |
| emptyText: gettext('default') | |
| }); | |
| me.column2.push(me.mbps_wr_max); | |
| me.iops_rd = Ext.widget('numberfield', { | |
| name: 'iops_rd', | |
| minValue: 10, | |
| step: 10, | |
| fieldLabel: gettext('Read limit') + ' (ops/s)', | |
| labelWidth: width2, | |
| emptyText: gettext('unlimited') | |
| }); | |
| me.column1.push(me.iops_rd); | |
| me.iops_rd_max = Ext.widget('numberfield', { | |
| name: 'iops_rd_max', | |
| minValue: 10, | |
| step: 10, | |
| fieldLabel: gettext('Read max burst') + ' (ops)', | |
| labelWidth: width2, | |
| emptyText: gettext('default') | |
| }); | |
| me.column2.push(me.iops_rd_max); | |
| me.iops_wr = Ext.widget('numberfield', { | |
| name: 'iops_wr', | |
| minValue: 10, | |
| step: 10, | |
| fieldLabel: gettext('Write limit') + ' (ops/s)', | |
| labelWidth: width2, | |
| emptyText: gettext('unlimited') | |
| }); | |
| me.column1.push(me.iops_wr); | |
| me.iops_wr_max = Ext.widget('numberfield', { | |
| name: 'iops_wr_max', | |
| minValue: 10, | |
| step: 10, | |
| fieldLabel: gettext('Write max burst') + ' (ops)', | |
| labelWidth: width2, | |
| emptyText: gettext('default') | |
| }); | |
| me.column2.push(me.iops_wr_max); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.HDThrottle', { | |
| extend: 'PVE.window.Edit', | |
| isAdd: true, | |
| 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.create = me.confid ? unused : true; | |
| var ipanel = Ext.create('PVE.qemu.HDThrottleInputPanel', { | |
| confid: me.confid, | |
| nodename: nodename | |
| }); | |
| var subject; | |
| if (unused) { | |
| me.subject = gettext('Unused Disk'); | |
| } else { | |
| me.subject = gettext('Hard Disk') + ' (' + me.confid + ')'; | |
| } | |
| me.items = [ ipanel ]; | |
| me.callParent(); | |
| me.load({ | |
| success: function(response, options) { | |
| if (me.confid) { | |
| var value = response.result.data[me.confid]; | |
| var drive = PVE.Parser.parseQemuDrive(me.confid, value); | |
| if (!drive) { | |
| Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options'); | |
| me.close(); | |
| return; | |
| } | |
| ipanel.setDrive(drive); | |
| me.isValid(); // trigger validation | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.CPUOptionsInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.PVE.qemu.CPUOptionsInputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| var delete_array = []; | |
| if (values.vcpus === '') { | |
| delete_array.push('vcpus'); | |
| delete values.vcpus; | |
| } | |
| if (values.cpulimit === '' || values.cpulimit == '0') { | |
| delete_array.push('cpulimit'); | |
| delete values.cpulimit; | |
| } | |
| if (values.cpuunits === '' || values.cpuunits == '1024') { | |
| delete_array.push('cpuunits'); | |
| delete values.cpuunits; | |
| } | |
| if (delete_array.length) { | |
| values['delete'] = delete_array.join(','); | |
| } | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| var items = [ | |
| { | |
| xtype: 'numberfield', | |
| name: 'vcpus', | |
| minValue: 1, | |
| maxValue: me.maxvcpus, | |
| value: '', | |
| fieldLabel: gettext('VCPUs'), | |
| allowBlank: true, | |
| emptyText: me.maxvcpus | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'cpulimit', | |
| minValue: 0, | |
| maxValue: me.maxvcpus, | |
| value: '', | |
| step: 1, | |
| fieldLabel: gettext('CPU limit'), | |
| allowBlank: true, | |
| emptyText: gettext('unlimited') | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'cpuunits', | |
| fieldLabel: gettext('CPU units'), | |
| minValue: 8, | |
| maxValue: 500000, | |
| value: '1024', | |
| allowBlank: true | |
| } | |
| ]; | |
| me.items = items; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.CPUOptions', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| var ipanel = Ext.create('PVE.qemu.CPUOptionsInputPanel', { | |
| maxvcpus: me.maxvcpus | |
| }); | |
| Ext.apply(me, { | |
| subject: gettext('CPU options'), | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| me.load(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.DisplayEdit', { | |
| extend: 'PVE.window.Edit', | |
| vmconfig: undefined, | |
| initComponent : function() { | |
| var me = this; | |
| var displayField; | |
| var validateDisplay = function() { | |
| /*jslint confusion: true */ | |
| var val = displayField.getValue(); | |
| if (me.vmconfig && val.match(/^serial\d+$/)) { | |
| if (me.vmconfig[val] && me.vmconfig[val] === 'socket') { | |
| return true; | |
| } | |
| return "Serial interface '" + val + "' is not correctly configured."; | |
| } | |
| return true; | |
| }; | |
| displayField = Ext.createWidget('DisplaySelector', { | |
| name: 'vga', | |
| value: '__default__', | |
| fieldLabel: gettext('Graphic card'), | |
| validator: validateDisplay | |
| }); | |
| Ext.apply(me, { | |
| subject: gettext('Display'), | |
| width: 350, | |
| items: displayField | |
| }); | |
| me.callParent(); | |
| me.load({ | |
| success: function(response, options) { | |
| var values = response.result.data; | |
| me.vmconfig = values; | |
| me.setValues(values); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.KeyboardEdit', { | |
| extend: 'PVE.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(); | |
| } | |
| }); | |
| // fixme: howto avoid jslint type confusion? | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.qemu.HardwareView', { | |
| extend: 'PVE.grid.PendingObjectGrid', | |
| alias: ['widget.PVE.qemu.HardwareView'], | |
| renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { | |
| var me = this; | |
| var rows = me.rows; | |
| var rowdef = rows[key] || {}; | |
| metaData.tdAttr = "valign=middle"; | |
| if (rowdef.tdCls) { | |
| metaData.tdCls = rowdef.tdCls; | |
| if (rowdef.tdCls == 'pve-itype-icon-storage') { | |
| var value = me.getObjectValue(key, '', true); | |
| if (value.match(/media=cdrom/)) { | |
| metaData.tdCls = 'pve-itype-icon-cdrom'; | |
| return rowdef.cdheader; | |
| } | |
| } | |
| } | |
| 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 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', | |
| renderer: function(value, metaData, record) { | |
| var balloon = me.getObjectValue('balloon'); | |
| if (balloon) { | |
| return PVE.Utils.format_size(balloon*1024*1024) + "/" + | |
| PVE.Utils.format_size(value*1024*1024); | |
| } | |
| return PVE.Utils.format_size(value*1024*1024); | |
| } | |
| }, | |
| 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', | |
| 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 = (sockets*cores) + ' (' + sockets + ' sockets, ' + cores + ' 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; | |
| } | |
| }, | |
| keyboard: { | |
| header: gettext('Keyboard Layout'), | |
| never_delete: true, | |
| editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.KeyboardEdit' : undefined, | |
| tdCls: 'pve-itype-icon-keyboard', | |
| defaultValue: '', | |
| renderer: PVE.Utils.render_kvm_language | |
| }, | |
| vga: { | |
| header: gettext('Display'), | |
| editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined, | |
| never_delete: true, | |
| tdCls: 'pve-itype-icon-display', | |
| defaultValue: '', | |
| renderer: PVE.Utils.render_kvm_vga_driver | |
| }, | |
| cores: { | |
| visible: false | |
| }, | |
| cpu: { | |
| visible: false | |
| }, | |
| numa: { | |
| visible: false | |
| }, | |
| balloon: { | |
| visible: false | |
| }, | |
| hotplug: { | |
| visible: false | |
| }, | |
| vcpus: { | |
| visible: false | |
| }, | |
| cpuunits: { | |
| visible: false | |
| }, | |
| cpulimit: { | |
| visible: false | |
| } | |
| }; | |
| for (i = 0; i < 4; i++) { | |
| confid = "ide" + i; | |
| rows[confid] = { | |
| group: 1, | |
| 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 +')' | |
| }; | |
| } | |
| for (i = 0; i < 6; i++) { | |
| confid = "sata" + i; | |
| rows[confid] = { | |
| group: 1, | |
| 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 +')' | |
| }; | |
| } | |
| for (i = 0; i < 16; i++) { | |
| confid = "scsi" + i; | |
| rows[confid] = { | |
| group: 1, | |
| 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 +')' | |
| }; | |
| } | |
| for (i = 0; i < 16; i++) { | |
| confid = "virtio" + i; | |
| rows[confid] = { | |
| group: 1, | |
| 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 +')' | |
| }; | |
| } | |
| for (i = 0; i < 32; i++) { | |
| confid = "net" + i; | |
| rows[confid] = { | |
| group: 2, | |
| 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 +')' | |
| }; | |
| } | |
| for (i = 0; i < 8; i++) { | |
| rows["unused" + i] = { | |
| group: 3, | |
| tdCls: 'pve-itype-icon-storage', | |
| editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined, | |
| header: gettext('Unused Disk') + ' ' + i | |
| }; | |
| } | |
| 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; | |
| return (g1 !== g2) ? | |
| (g1 > g2 ? 1 : -1) : (v1 > v2 ? 1 : (v1 < v2 ? -1 : 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(/media=cdrom/)) { | |
| editor = 'PVE.qemu.CDEdit'; | |
| } | |
| } | |
| var win; | |
| if (Ext.isString(editor)) { | |
| win = Ext.create(editor, { | |
| pveSelNode: me.pveSelNode, | |
| confid: rec.data.key, | |
| url: '/api2/extjs/' + baseurl | |
| }); | |
| } else { | |
| var config = Ext.apply({ | |
| pveSelNode: me.pveSelNode, | |
| confid: rec.data.key, | |
| url: '/api2/extjs/' + baseurl | |
| }, rowdef.editor); | |
| win = Ext.createWidget(rowdef.editor.xtype, config); | |
| win.load(); | |
| } | |
| win.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var run_diskthrottle = function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| return; | |
| } | |
| var win = Ext.create('PVE.qemu.HDThrottle', { | |
| pveSelNode: me.pveSelNode, | |
| confid: rec.data.key, | |
| url: '/api2/extjs/' + baseurl | |
| }); | |
| 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_cpuoptions = function() { | |
| var sockets = me.getObjectValue('sockets', 1); | |
| var cores = me.getObjectValue('cores', 1); | |
| var win = Ext.create('PVE.qemu.CPUOptions', { | |
| maxvcpus: sockets * cores, | |
| vmid: vmid, | |
| pveSelNode: me.pveSelNode, | |
| url: '/api2/extjs/' + baseurl | |
| }); | |
| 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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: run_editor | |
| }); | |
| var resize_btn = new PVE.button.Button({ | |
| text: gettext('Resize disk'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: run_resize | |
| }); | |
| var move_btn = new PVE.button.Button({ | |
| text: gettext('Move disk'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: run_move | |
| }); | |
| var diskthrottle_btn = new PVE.button.Button({ | |
| text: gettext('Disk Throttle'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: run_diskthrottle | |
| }); | |
| var cpuoptions_btn = new Ext.Button({ | |
| text: gettext('CPU options'), | |
| handler: run_cpuoptions | |
| }); | |
| var remove_btn = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| 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: function(b, e, rec) { | |
| PVE.Utils.API2Request({ | |
| url: '/api2/extjs/' + baseurl, | |
| waitMsgTarget: me, | |
| method: 'PUT', | |
| params: { | |
| 'delete': rec.data.key | |
| }, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| var revert_btn = new PVE.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(','); | |
| PVE.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 set_button_status = function() { | |
| var sm = me.getSelectionModel(); | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| remove_btn.disable(); | |
| edit_btn.disable(); | |
| resize_btn.disable(); | |
| move_btn.disable(); | |
| diskthrottle_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 isDisk = !key.match(/^unused\d+/) && | |
| rowdef.tdCls == 'pve-itype-icon-storage' && | |
| (value && !value.match(/media=cdrom/)); | |
| remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true)); | |
| edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor); | |
| resize_btn.setDisabled(pending || !isDisk); | |
| move_btn.setDisabled(pending || !isDisk); | |
| diskthrottle_btn.setDisabled(pending || !isDisk); | |
| revert_btn.setDisabled(!pending); | |
| }; | |
| Ext.apply(me, { | |
| url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending', | |
| interval: 5000, | |
| selModel: sm, | |
| 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 | |
| }); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| } | |
| ] | |
| }) | |
| }, | |
| remove_btn, | |
| edit_btn, | |
| resize_btn, | |
| move_btn, | |
| diskthrottle_btn, | |
| cpuoptions_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('hide', me.rstore.stopUpdate); | |
| me.on('destroy', me.rstore.stopUpdate); | |
| me.mon(me.rstore, 'refresh', function() { | |
| set_button_status(); | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.StartupInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| 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.qemu.StartupEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| var ipanel = Ext.create('PVE.qemu.StartupInputPanel', {}); | |
| 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); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.ScsiHwEdit', { | |
| extend: 'PVE.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: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| Ext.applyIf(me, { | |
| subject: 'BIOS', | |
| items: { | |
| xtype: 'pveQemuBiosSelector', | |
| name: 'bios', | |
| value: '__default__', | |
| fieldLabel: 'BIOS' | |
| } | |
| }); | |
| me.callParent(); | |
| me.load(); | |
| } | |
| }); | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.qemu.Options', { | |
| extend: 'PVE.grid.PendingObjectGrid', | |
| alias: ['widget.PVE.qemu.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: 'pveWindowEdit', | |
| subject: gettext('Name'), | |
| items: { | |
| xtype: 'textfield', | |
| name: 'name', | |
| vtype: 'DnsName', | |
| value: '', | |
| fieldLabel: gettext('Name'), | |
| allowBlank: true | |
| } | |
| } : undefined | |
| }, | |
| onboot: { | |
| header: gettext('Start at boot'), | |
| defaultValue: '', | |
| renderer: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Start at boot'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| 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'] ? | |
| 'PVE.qemu.StartupEdit' : 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: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.HWType'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Use tablet for pointer'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| 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: 'pveWindowEdit', | |
| 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: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.HWType'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('ACPI support'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| name: 'acpi', | |
| checked: true, | |
| uncheckedValue: 0, | |
| defaultValue: 1, | |
| deleteDefaultValue: true, | |
| fieldLabel: gettext('Enabled') | |
| } | |
| } : undefined | |
| }, | |
| scsihw: { | |
| header: gettext('SCSI Controller Type'), | |
| editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined, | |
| renderer: PVE.Utils.render_scsihw, | |
| defaultValue: '' | |
| }, | |
| bios: { | |
| header: 'BIOS', | |
| editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined, | |
| renderer: PVE.Utils.render_qemu_bios, | |
| defaultValue: '' | |
| }, | |
| kvm: { | |
| header: gettext('KVM hardware virtualization'), | |
| defaultValue: true, | |
| renderer: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.HWType'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('KVM hardware virtualization'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| name: 'kvm', | |
| checked: true, | |
| uncheckedValue: 0, | |
| defaultValue: 1, | |
| deleteDefaultValue: true, | |
| fieldLabel: gettext('Enabled') | |
| } | |
| } : undefined | |
| }, | |
| freeze: { | |
| header: gettext('Freeze CPU at startup'), | |
| defaultValue: false, | |
| renderer: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.PowerMgmt'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Freeze CPU at startup'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| 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: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Use local time for RTC'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| 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: 'pveWindowEdit', | |
| subject: gettext('RTC start date'), | |
| items: { | |
| xtype: 'pvetextfield', | |
| 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.format_boolean, | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Qemu Agent'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| name: 'agent', | |
| uncheckedValue: 0, | |
| defaultValue: 0, | |
| deleteDefaultValue: true, | |
| fieldLabel: gettext('Enabled') | |
| } | |
| } : undefined | |
| }, | |
| protection: { | |
| header: gettext('Protection'), | |
| defaultValue: false, | |
| renderer: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Protection'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| name: 'protection', | |
| uncheckedValue: 0, | |
| defaultValue: 0, | |
| deleteDefaultValue: true, | |
| fieldLabel: gettext('Enabled') | |
| } | |
| } : undefined | |
| } | |
| }; | |
| var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; | |
| var reload = function() { | |
| me.rstore.load(); | |
| }; | |
| var run_editor = function() { | |
| var sm = me.getSelectionModel(); | |
| 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.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var edit_btn = new Ext.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| handler: run_editor | |
| }); | |
| var revert_btn = new PVE.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(','); | |
| PVE.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 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, | |
| listeners: { | |
| itemdblclick: run_editor, | |
| selectionchange: set_button_status | |
| } | |
| }); | |
| me.callParent(); | |
| me.on('activate', me.rstore.startUpdate); | |
| me.on('hide', me.rstore.stopUpdate); | |
| me.on('destroy', me.rstore.stopUpdate); | |
| me.rstore.on('datachanged', function() { | |
| set_button_status(); | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.window.Snapshot', { | |
| extend: 'Ext.window.Window', | |
| resizable: false, | |
| take_snapshot: function(snapname, descr, vmstate) { | |
| var me = this; | |
| var params = { snapname: snapname, vmstate: vmstate ? 1 : 0 }; | |
| if (descr) { | |
| params.description = descr; | |
| } | |
| PVE.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('PVE.window.TaskProgress', { upid: upid }); | |
| win.show(); | |
| me.close(); | |
| } | |
| }); | |
| }, | |
| update_snapshot: function(snapname, descr) { | |
| var me = this; | |
| PVE.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', | |
| fieldLabel: gettext('Timestamp') | |
| }); | |
| } else { | |
| items.push({ | |
| xtype: 'pvecheckbox', | |
| 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'), | |
| 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 | |
| PVE.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(new Date(data.snaptime)); | |
| form.findField('description').setValue(data.description); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.define('PVE.window.Clone', { | |
| extend: 'Ext.window.Window', | |
| resizable: false, | |
| isTemplate: false, | |
| 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) { | |
| params.name = values.name; | |
| } | |
| if (values.target) { | |
| params.target = values.target; | |
| } | |
| if (values.clonemode === 'copy') { | |
| params.full = 1; | |
| if (values.storage) { | |
| params.storage = values.storage; | |
| if (values.diskformat) { | |
| params.format = values.diskformat; | |
| } | |
| } | |
| } | |
| PVE.Utils.API2Request({ | |
| params: params, | |
| url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/clone', | |
| waitMsgTarget: me, | |
| method: 'POST', | |
| failure: function(response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| }, | |
| success: function(response, options) { | |
| me.close(); | |
| } | |
| }); | |
| }, | |
| updateVisibility: function() { | |
| var me = this; | |
| var clonemode = me.kv1.getValue(); | |
| var storage = me.hdstoragesel.getValue(); | |
| var rec = me.hdstoragesel.store.getById(storage); | |
| me.hdstoragesel.setDisabled(clonemode === 'clone'); | |
| if (!rec || clonemode === 'clone') { | |
| me.formatsel.setDisabled(true); | |
| return; | |
| } | |
| if (rec.data.type === 'lvm' || | |
| rec.data.type === 'lvmthin' || | |
| rec.data.type === 'rbd' || | |
| rec.data.type === 'iscsi' || | |
| rec.data.type === 'sheepdog' || | |
| rec.data.type === 'zfs' || | |
| rec.data.type === 'zfspool' | |
| ) { | |
| me.formatsel.setValue('raw'); | |
| me.formatsel.setDisabled(true); | |
| } else { | |
| me.formatsel.setDisabled(false); | |
| } | |
| }, | |
| verifyFeature: function() { | |
| var me = this; | |
| var snapname = me.snapshotSel.getValue(); | |
| var clonemode = me.kv1.getValue(); | |
| var params = { feature: clonemode }; | |
| if (snapname !== 'current') { | |
| params.snapname = snapname; | |
| } | |
| PVE.Utils.API2Request({ | |
| waitMsgTarget: me, | |
| url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/feature', | |
| params: params, | |
| method: 'GET', | |
| failure: function(response, opts) { | |
| me.submitBtn.setDisabled(false); | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| }, | |
| success: function(response, options) { | |
| var res = response.result.data; | |
| me.submitBtn.setDisabled(res.hasFeature !== 1); | |
| me.targetSel.allowedNodes = res.nodes; | |
| me.targetSel.validate(); | |
| } | |
| }); | |
| }, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| 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'; | |
| } | |
| var col1 = []; | |
| var col2 = []; | |
| me.targetSel = Ext.create('PVE.form.NodeSelector', { | |
| name: 'target', | |
| fieldLabel: gettext('Target node'), | |
| selectCurNode: true, | |
| allowBlank: false, | |
| onlineValidator: true | |
| }); | |
| col1.push(me.targetSel); | |
| var modelist = [['copy', gettext('Full Clone')]]; | |
| if (me.isTemplate) { | |
| modelist.push(['clone', gettext('Linked Clone')]); | |
| } | |
| me.kv1 = Ext.create('PVE.form.KVComboBox', { | |
| fieldLabel: gettext('Mode'), | |
| name: 'clonemode', | |
| allowBlank: false, | |
| value: me.isTemplate ? 'clone' : 'copy', | |
| comboItems: modelist | |
| }); | |
| me.mon(me.kv1, 'change', function(t, value) { | |
| me.updateVisibility(); | |
| me.verifyFeature(); | |
| }); | |
| col2.push(me.kv1); | |
| me.snapshotSel = Ext.create('PVE.form.SnapshotSelector', { | |
| name: 'snapname', | |
| fieldLabel: gettext('Snapshot'), | |
| nodename: me.nodename, | |
| vmid: me.vmid, | |
| hidden: me.isTemplate ? true : false, | |
| disabled: false, | |
| allowBlank: false, | |
| value : me.snapname, | |
| listeners: { | |
| change: function(f, value) { | |
| me.verifyFeature(); | |
| } | |
| } | |
| }); | |
| col2.push(me.snapshotSel); | |
| col1.push( | |
| { | |
| xtype: 'pveVMIDSelector', | |
| name: 'newvmid', | |
| value: '', | |
| loadNextFreeVMID: true, | |
| validateExists: false | |
| }, | |
| { | |
| xtype: 'textfield', | |
| name: 'name', | |
| allowBlank: true, | |
| fieldLabel: gettext('Name') | |
| } | |
| ); | |
| me.hdstoragesel = Ext.create('PVE.form.StorageSelector', { | |
| name: 'storage', | |
| nodename: me.nodename, | |
| fieldLabel: gettext('Target Storage'), | |
| storageContent: 'images', | |
| autoSelect: me.insideWizard, | |
| allowBlank: true, | |
| disabled: me.isTemplate ? true : false, // because default mode is clone for templates | |
| hidden: false, | |
| listeners: { | |
| change: function(f, value) { | |
| me.updateVisibility(); | |
| } | |
| } | |
| }); | |
| me.targetSel.on('change', function(f, value) { | |
| me.hdstoragesel.setTargetNode(value); | |
| }); | |
| me.formatsel = Ext.create('PVE.form.DiskFormatSelector', { | |
| name: 'diskformat', | |
| fieldLabel: gettext('Format'), | |
| value: 'raw', | |
| disabled: true, | |
| hidden: false, | |
| allowBlank: false | |
| }); | |
| col2.push({ | |
| xtype: 'pvePoolSelector', | |
| fieldLabel: gettext('Resource Pool'), | |
| name: 'pool', | |
| value: '', | |
| allowBlank: true | |
| }); | |
| col2.push(me.hdstoragesel); | |
| col2.push(me.formatsel); | |
| me.formPanel = Ext.create('Ext.form.Panel', { | |
| bodyPadding: 10, | |
| 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 | |
| } | |
| ] | |
| }); | |
| var form = me.formPanel.getForm(); | |
| var titletext = me.isTemplate ? "Template" : "VM"; | |
| me.title = "Clone " + titletext + " " + me.vmid; | |
| me.submitBtn = Ext.create('Ext.Button', { | |
| text: gettext('Clone'), | |
| disabled: true, | |
| handler: function() { | |
| if (form.isValid()) { | |
| var values = form.getValues(); | |
| me.create_clone(values); | |
| } | |
| } | |
| }); | |
| Ext.apply(me, { | |
| modal: true, | |
| width: 600, | |
| height: 250, | |
| border: false, | |
| layout: 'fit', | |
| buttons: [ me.submitBtn ], | |
| items: [ me.formPanel ] | |
| }); | |
| me.callParent(); | |
| me.verifyFeature(); | |
| } | |
| }); | |
| Ext.define('PVE.qemu.SnapshotTree', { | |
| extend: 'Ext.tree.Panel', | |
| alias: ['widget.pveQemuSnapshotTree'], | |
| load_delay: 3000, | |
| old_digest: 'invalid', | |
| 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; | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot', | |
| method: 'GET', | |
| failure: function(response, opts) { | |
| PVE.Utils.setErrorMask(me, response.htmlStatus); | |
| me.load_task.delay(me.load_delay); | |
| }, | |
| success: function(response, opts) { | |
| PVE.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); | |
| } | |
| }); | |
| PVE.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(); | |
| }); | |
| } | |
| } | |
| }); | |
| }, | |
| 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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| enableFn: valid_snapshot, | |
| handler: run_editor | |
| }); | |
| var rollbackBtn = new PVE.button.Button({ | |
| text: gettext('Rollback'), | |
| disabled: true, | |
| selModel: sm, | |
| enableFn: valid_snapshot_rollback, | |
| confirmMsg: function(rec) { | |
| return PVE.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; | |
| PVE.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('PVE.window.TaskProgress', { upid: upid }); | |
| win.show(); | |
| } | |
| }); | |
| } | |
| }); | |
| var removeBtn = new PVE.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; | |
| PVE.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('PVE.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 PVE.Utils.format_boolean(value); | |
| } | |
| } | |
| }, | |
| { | |
| text: gettext('Date') + "/" + gettext("Status"), | |
| dataIndex: 'snaptime', | |
| resizable: false, | |
| width: 120, | |
| 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, | |
| hide: me.load_task.cancel, | |
| 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', | |
| 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 caps = Ext.state.Manager.get('GuiCap'); | |
| var base_url = '/nodes/' + nodename + "/qemu/" + vmid; | |
| me.statusStore = Ext.create('PVE.data.ObjectStore', { | |
| url: '/api2/json' + base_url + '/status/current', | |
| interval: 1000 | |
| }); | |
| var vm_command = function(cmd, params) { | |
| PVE.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'], | |
| handler: function() { | |
| vm_command('start'); | |
| }, | |
| iconCls: 'fa fa-play' | |
| }); | |
| var migrateBtn = Ext.create('Ext.Button', { | |
| text: gettext('Migrate'), | |
| disabled: !caps.vms['VM.Migrate'], | |
| handler: function() { | |
| var win = Ext.create('PVE.window.Migrate', { | |
| vmtype: 'qemu', | |
| nodename: nodename, | |
| vmid: vmid | |
| }); | |
| win.show(); | |
| }, | |
| iconCls: 'fa fa-send-o' | |
| }); | |
| var resetBtn = Ext.create('PVE.button.Button', { | |
| text: gettext('Reset'), | |
| disabled: !caps.vms['VM.PowerMgmt'], | |
| confirmMsg: PVE.Utils.format_task_description('qmreset', vmid), | |
| handler: function() { | |
| vm_command("reset"); | |
| }, | |
| iconCls: 'fa fa-bolt' | |
| }); | |
| var shutdownBtn = Ext.create('PVE.button.Split', { | |
| text: gettext('Shutdown'), | |
| disabled: !caps.vms['VM.PowerMgmt'], | |
| confirmMsg: PVE.Utils.format_task_description('qmshutdown', vmid), | |
| handler: function() { | |
| vm_command('shutdown'); | |
| }, | |
| menu: { | |
| items: [{ | |
| text: gettext('Stop'), | |
| disabled: !caps.vms['VM.PowerMgmt'], | |
| dangerous: true, | |
| confirmMsg: PVE.Utils.format_task_description('qmstop', vmid), | |
| handler: function() { | |
| vm_command("stop", { timeout: 30 }); | |
| }, | |
| iconCls: 'fa fa-stop' | |
| }] | |
| }, | |
| iconCls: 'fa fa-power-off' | |
| }); | |
| var removeBtn = Ext.create('PVE.button.Button', { | |
| text: gettext('Remove'), | |
| 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 vmname = me.pveSelNode.data.name; | |
| var consoleBtn = Ext.create('PVE.button.ConsoleButton', { | |
| disabled: !caps.vms['VM.Console'], | |
| consoleType: 'kvm', | |
| consoleName: vmname, | |
| nodename: nodename, | |
| vmid: vmid, | |
| iconCls: 'fa fa-terminal' | |
| }); | |
| var descr = vmid + " (" + (vmname ? "'" + vmname + "' " : "'VM " + vmid + "'") + ")"; | |
| Ext.apply(me, { | |
| title: Ext.String.format(gettext("Virtual Machine {0} on node {1}"), descr, "'" + nodename + "'"), | |
| hstateid: 'kvmtab', | |
| tbar: [ resumeBtn, startBtn, shutdownBtn, resetBtn, | |
| removeBtn, migrateBtn, consoleBtn], | |
| defaults: { statusStore: me.statusStore }, | |
| items: [ | |
| { | |
| title: gettext('Summary'), | |
| xtype: 'pveQemuSummary', | |
| itemId: 'summary' | |
| }, | |
| { | |
| title: gettext('Hardware'), | |
| itemId: 'hardware', | |
| xtype: 'PVE.qemu.HardwareView' | |
| }, | |
| { | |
| title: gettext('Options'), | |
| itemId: 'options', | |
| xtype: 'PVE.qemu.Options' | |
| }, | |
| { | |
| title: gettext('Task History'), | |
| itemId: 'tasks', | |
| xtype: 'pveNodeTasks', | |
| vmidFilter: vmid | |
| } | |
| ] | |
| }); | |
| if (caps.vms['VM.Monitor'] && !template) { | |
| me.items.push({ | |
| title: gettext('Monitor'), | |
| itemId: 'monitor', | |
| xtype: 'pveQemuMonitor' | |
| }); | |
| } | |
| if (caps.vms['VM.Backup']) { | |
| me.items.push({ | |
| title: gettext('Backup'), | |
| xtype: 'pveBackupView', | |
| itemId: 'backup' | |
| }); | |
| } | |
| if (caps.vms['VM.Snapshot'] && !template) { | |
| me.items.push({ | |
| title: gettext('Snapshots'), | |
| xtype: 'pveQemuSnapshotTree', | |
| itemId: 'snapshot' | |
| }); | |
| } | |
| if (caps.vms['VM.Console'] && !template) { | |
| me.items.push({ | |
| title: gettext('Console'), | |
| itemId: 'console', | |
| xtype: 'pveNoVncConsole', | |
| vmid: vmid, | |
| consoleType: 'kvm', | |
| nodename: nodename | |
| }); | |
| } | |
| if (caps.vms['VM.Console']) { | |
| me.items.push( | |
| { | |
| xtype: 'pveFirewallPanel', | |
| title: gettext('Firewall'), | |
| base_url: base_url + '/firewall', | |
| fwtype: 'vm', | |
| phstateid: me.hstateid, | |
| itemId: 'firewall' | |
| } | |
| ); | |
| } | |
| if (caps.vms['Permissions.Modify']) { | |
| me.items.push({ | |
| xtype: 'pveACLView', | |
| title: gettext('Permissions'), | |
| itemId: 'permissions', | |
| path: '/vms/' + vmid | |
| }); | |
| } | |
| me.callParent(); | |
| me.mon(me.statusStore, 'load', function(s, records, success) { | |
| var status; | |
| var qmpstatus; | |
| var spice = false; | |
| if (!success) { | |
| me.workspace.checkVmMigration(me.pveSelNode); | |
| 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; | |
| spice = s.data.get('spice') ? true : false; | |
| } | |
| if (qmpstatus === 'prelaunch' || qmpstatus === 'paused') { | |
| startBtn.setVisible(false); | |
| resumeBtn.setVisible(true); | |
| } else { | |
| startBtn.setVisible(true); | |
| resumeBtn.setVisible(false); | |
| } | |
| consoleBtn.setEnableSpice(spice); | |
| startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template); | |
| resetBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running' || template); | |
| shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); | |
| 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(); | |
| }); | |
| } | |
| }); | |
| // fixme: howto avoid jslint type confusion? | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.qemu.CreateWizard', { | |
| extend: 'PVE.window.Wizard', | |
| initComponent: function() { | |
| var me = this; | |
| var summarystore = Ext.create('Ext.data.Store', { | |
| model: 'KeyValue', | |
| sorters: [ | |
| { | |
| property : 'key', | |
| direction: 'ASC' | |
| } | |
| ] | |
| }); | |
| var cdpanel = Ext.create('PVE.qemu.CDInputPanel', { | |
| title: gettext('CD/DVD'), | |
| confid: 'ide2', | |
| fieldDefaults: { | |
| labelWidth: 160 | |
| }, | |
| insideWizard: true | |
| }); | |
| var hdpanel = Ext.create('PVE.qemu.HDInputPanel', { | |
| title: gettext('Hard Disk'), | |
| create: true, | |
| insideWizard: true | |
| }); | |
| var networkpanel = Ext.create('PVE.qemu.NetworkInputPanel', { | |
| title: gettext('Network'), | |
| insideWizard: true | |
| }); | |
| Ext.applyIf(me, { | |
| subject: gettext('Virtual Machine'), | |
| items: [ | |
| { | |
| xtype: 'inputpanel', | |
| title: gettext('General'), | |
| onlineHelp: 'chapter-qm.html#_general_settings', | |
| column1: [ | |
| { | |
| xtype: 'pveNodeSelector', | |
| name: 'nodename', | |
| selectCurNode: true, | |
| fieldLabel: gettext('Node'), | |
| allowBlank: false, | |
| onlineValidator: true, | |
| listeners: { | |
| change: function(f, value) { | |
| networkpanel.setNodename(value); | |
| hdpanel.setNodename(value); | |
| cdpanel.setNodename(value); | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'pveVMIDSelector', | |
| name: 'vmid', | |
| value: '', | |
| loadNextFreeVMID: 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 | |
| } | |
| ], | |
| onGetValues: function(values) { | |
| if (!values.name) { | |
| delete values.name; | |
| } | |
| if (!values.pool) { | |
| delete values.pool; | |
| } | |
| return values; | |
| } | |
| }, | |
| { | |
| title: gettext('OS'), | |
| xtype: 'PVE.qemu.OSTypeInputPanel' | |
| }, | |
| cdpanel, | |
| hdpanel, | |
| { | |
| xtype: 'PVE.qemu.ProcessorInputPanel', | |
| title: gettext('CPU') | |
| }, | |
| { | |
| xtype: 'PVE.qemu.MemoryInputPanel', | |
| insideWizard: true, | |
| title: gettext('Memory') | |
| }, | |
| networkpanel, | |
| { | |
| title: gettext('Confirm'), | |
| layout: 'fit', | |
| items: [ | |
| { | |
| title: gettext('Settings'), | |
| xtype: 'grid', | |
| store: summarystore, | |
| columns: [ | |
| {header: 'Key', width: 150, dataIndex: 'key'}, | |
| {header: 'Value', flex: 1, dataIndex: 'value'} | |
| ] | |
| } | |
| ], | |
| listeners: { | |
| show: function(panel) { | |
| var form = me.down('form').getForm(); | |
| var kv = me.getValues(); | |
| var data = []; | |
| Ext.Object.each(kv, function(key, value) { | |
| if (key === 'delete') { // ignore | |
| return; | |
| } | |
| var html = Ext.htmlEncode(Ext.JSON.encode(value)); | |
| data.push({ key: key, value: value }); | |
| }); | |
| summarystore.suspendEvents(); | |
| summarystore.removeAll(); | |
| summarystore.add(data); | |
| summarystore.sort(); | |
| summarystore.resumeEvents(); | |
| summarystore.fireEvent('datachanged', summarystore); | |
| } | |
| }, | |
| onSubmit: function() { | |
| var kv = me.getValues(); | |
| delete kv['delete']; | |
| var nodename = kv.nodename; | |
| delete kv.nodename; | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + nodename + '/qemu', | |
| waitMsgTarget: me, | |
| method: 'POST', | |
| params: kv, | |
| success: function(response){ | |
| me.close(); | |
| }, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.lxc.StatusView', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| disabled: true, | |
| 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 render_cpu = function(value, metaData, record, rowIndex, colIndex, store) { | |
| if (!me.getObjectValue('uptime')) { | |
| return '-'; | |
| } | |
| var maxcpu = me.getObjectValue('cpus', 1); | |
| if (!(Ext.isNumeric(value) && Ext.isNumeric(maxcpu) && (maxcpu >= 1))) { | |
| return '-'; | |
| } | |
| var cpu = value * 100; | |
| return cpu.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU'); | |
| }; | |
| var render_mem = function(value, metaData, record, rowIndex, colIndex, store) { | |
| var maxmem = me.getObjectValue('maxmem', 0); | |
| var per = (value / maxmem)*100; | |
| var text = "<div>" + PVE.Utils.totalText + ": " + PVE.Utils.format_size(maxmem) + "</div>" + | |
| "<div>" + PVE.Utils.usedText + ": " + PVE.Utils.format_size(value) + "</div>"; | |
| return text; | |
| }; | |
| var render_swap = function(value, metaData, record, rowIndex, colIndex, store) { | |
| var maxswap = me.getObjectValue('maxswap', 0); | |
| var per = (value / maxswap)*100; | |
| var text = "<div>" + PVE.Utils.totalText + ": " + PVE.Utils.format_size(maxswap) + "</div>" + | |
| "<div>" + PVE.Utils.usedText + ": " + PVE.Utils.format_size(value) + "</div>"; | |
| return text; | |
| }; | |
| var render_status = function(value, metaData, record, rowIndex, colIndex, store) { | |
| var failcnt = me.getObjectValue('failcnt', 0); | |
| if (failcnt > 0) { | |
| return value + " (failure count " + failcnt.toString() + ")"; | |
| } | |
| return value; | |
| }; | |
| var rows = {}; | |
| if (template) { | |
| rows = { | |
| name: { header: gettext('Name'), defaultValue: 'no name specified' }, | |
| cpus: { header: gettext('CPU limit'), required: true}, | |
| maxmem: { header: gettext('Memory'), required: true, renderer: PVE.Utils.render_size }, | |
| maxswap: { header: gettext('VSwap'), required: true, renderer: PVE.Utils.render_size }, | |
| maxdisk: { header: gettext('Bootdisk size'), renderer: PVE.Utils.render_size, required: true} | |
| }; | |
| } else { | |
| rows = { | |
| name: { header: gettext('Name'), defaultValue: 'no name specified' }, | |
| status: { header: gettext('Status'), defaultValue: 'unknown', renderer: render_status }, | |
| failcnt: { visible: false }, | |
| cpu: { header: gettext('CPU usage'), required: true, renderer: render_cpu }, | |
| cpus: { visible: false }, | |
| mem: { header: gettext('Memory usage'), required: true, renderer: render_mem }, | |
| maxmem: { visible: false }, | |
| swap: { header: gettext('VSwap usage'), required: true, renderer: render_swap }, | |
| maxswap: { visible: false }, | |
| maxdisk: { header: gettext('Bootdisk size'), renderer: PVE.Utils.render_size, required: true}, | |
| uptime: { header: gettext('Uptime'), required: true, renderer: PVE.Utils.render_uptime }, | |
| ha: { header: gettext('Managed by HA'), required: true, renderer: PVE.Utils.format_ha } | |
| }; | |
| } | |
| Ext.applyIf(me, { | |
| cwidth1: 150, | |
| rows: rows | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.lxc.Summary', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pveLxcSummary', | |
| scrollable: true, | |
| bodyPadding: 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"; | |
| } | |
| 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 statusview = Ext.create('PVE.lxc.StatusView', { | |
| title: gettext('Status'), | |
| pveSelNode: me.pveSelNode, | |
| width: template ? 800 : 400, | |
| rstore: rstore | |
| }); | |
| var notesview = Ext.create('PVE.panel.NotesView', { | |
| pveSelNode: me.pveSelNode, | |
| padding: template ? '10 0 0 0' : '0 0 0 10', | |
| flex: 1 | |
| }); | |
| if (template) { | |
| Ext.apply(me, { | |
| plugins: { | |
| ptype: 'lazyitems', | |
| items: [{ | |
| xtype: 'container', | |
| layout: { | |
| type: 'column' | |
| }, | |
| defaults: { | |
| padding: '0 10 10 0' | |
| }, | |
| items: [{ | |
| width: 800, | |
| layout: { | |
| type: 'vbox', | |
| align: 'stretch' | |
| }, | |
| border: false, | |
| items: [ statusview, notesview ] | |
| }] | |
| }] | |
| }, | |
| listeners: { | |
| activate: function() { notesview.load(); } | |
| } | |
| }); | |
| } else { | |
| var rrdstore = Ext.create('PVE.data.RRDStore', { | |
| rrdurl: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/rrddata" | |
| }); | |
| Ext.apply(me, { | |
| tbar: [ '->' , { xtype: 'pveRRDTypeSelector' } ], | |
| plugins: { | |
| ptype: 'lazyitems', | |
| items: [ | |
| { | |
| xtype: 'container', | |
| layout: { | |
| type: 'column' | |
| }, | |
| defaults: { | |
| padding: '0 10 10 0' | |
| }, | |
| items: [ | |
| { | |
| width: 800, | |
| height: 300, | |
| layout: { | |
| type: 'hbox', | |
| align: 'stretch' | |
| }, | |
| border: false, | |
| items: [ statusview, notesview ] | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('CPU usage'), | |
| pveSelNode: me.pveSelNode, | |
| fields: ['cpu'], | |
| fieldTitles: [gettext('CPU usage')], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Memory usage'), | |
| pveSelNode: me.pveSelNode, | |
| fields: ['maxmem', 'mem'], | |
| fieldTitles: [gettext('Total'), gettext('RAM usage')], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Network traffic'), | |
| pveSelNode: me.pveSelNode, | |
| fields: ['netin','netout'], | |
| store: rrdstore | |
| }, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Disk IO'), | |
| pveSelNode: me.pveSelNode, | |
| fields: ['diskread','diskwrite'], | |
| store: rrdstore | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| listeners: { | |
| activate: function() { notesview.load(); rrdstore.startUpdate(); }, | |
| hide: rrdstore.stopUpdate, | |
| destroy: rrdstore.stopUpdate | |
| } | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.lxc.NetworkInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.pveLxcNetworkInputPanel', | |
| insideWizard: false, | |
| 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.create) { | |
| 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; | |
| if (!me.dataCache) { | |
| throw "no dataCache specified"; | |
| } | |
| var cdata = {}; | |
| if (me.insideWizard) { | |
| me.ifname = 'net0'; | |
| cdata.name = 'eth0'; | |
| } | |
| if (!me.create) { | |
| 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.create && !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') + ' (i.e. 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: 'pvecheckbox', | |
| fieldLabel: gettext('Firewall'), | |
| name: 'firewall', | |
| checked: 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: gettext('IPv4') + ':' | |
| }, | |
| { | |
| 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: gettext('DHCP'), | |
| name: 'ipv4mode', | |
| inputValue: 'dhcp', | |
| checked: dhcp4, | |
| margin: '0 0 0 10' | |
| } | |
| ] | |
| }, | |
| { | |
| xtype: 'textfield', | |
| name: 'ip', | |
| vtype: 'IPCIDRAddress', | |
| value: cdata.ip, | |
| disabled: dhcp4, | |
| fieldLabel: gettext('IPv4/CIDR') | |
| }, | |
| { | |
| xtype: 'textfield', | |
| name: 'gw', | |
| value: cdata.gw, | |
| vtype: 'IPAddress', | |
| disabled: dhcp4, | |
| fieldLabel: gettext('Gateway') + ' (' + gettext('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: gettext('IPv6') + ':' | |
| }, | |
| { | |
| 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: gettext('DHCP'), | |
| name: 'ipv6mode', | |
| inputValue: 'dhcp', | |
| checked: dhcp6, | |
| margin: '0 0 0 10' | |
| }, | |
| { | |
| xtype: 'radiofield', | |
| boxLabel: gettext('SLAAC'), | |
| name: 'ipv6mode', | |
| inputValue: 'auto', | |
| checked: auto6, | |
| margin: '0 0 0 10' | |
| } | |
| ] | |
| }, | |
| { | |
| xtype: 'textfield', | |
| name: 'ip6', | |
| value: cdata.ip6, | |
| vtype: 'IP6CIDRAddress', | |
| disabled: (dhcp6 || auto6), | |
| fieldLabel: gettext('IPv6/CIDR') | |
| }, | |
| { | |
| xtype: 'textfield', | |
| name: 'gw6', | |
| vtype: 'IP6Address', | |
| value: cdata.gw6, | |
| disabled: (dhcp6 || auto6), | |
| fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')' | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.lxc.NetworkEdit', { | |
| extend: 'PVE.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, | |
| create: me.create | |
| }); | |
| 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'], | |
| dataCache: {}, // used to store result of last load | |
| load: function() { | |
| var me = this; | |
| PVE.Utils.setErrorMask(me, true); | |
| PVE.Utils.API2Request({ | |
| url: me.url, | |
| failure: function(response, opts) { | |
| PVE.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus); | |
| }, | |
| success: function(response, opts) { | |
| PVE.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 PVE.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) { | |
| PVE.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 PVE.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, | |
| stateful: false, | |
| 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, | |
| create: true, | |
| dataCache: me.dataCache | |
| }); | |
| win.on('destroy', me.load, me); | |
| win.show(); | |
| } | |
| }, | |
| remove_btn, | |
| edit_btn | |
| ], | |
| columns: [ | |
| { | |
| header: gettext('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: PVE.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: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveLxcRessourceView'], | |
| renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { | |
| var me = this; | |
| var rows = me.rows; | |
| var rowdef = rows[key] || {}; | |
| metaData.tdAttr = "valign=middle"; | |
| if (rowdef.tdCls) { | |
| metaData.tdCls = rowdef.tdCls; | |
| if (rowdef.tdCls == 'pve-itype-icon-storage') { | |
| var value = me.getObjectValue(key, '', true); | |
| } | |
| } | |
| 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 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, | |
| never_delete: true, | |
| defaultValue: 512, | |
| tdCls: 'pve-itype-icon-memory', | |
| renderer: function(value) { | |
| return PVE.Utils.format_size(value*1024*1024); | |
| } | |
| }, | |
| swap: { | |
| header: gettext('Swap'), | |
| editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, | |
| never_delete: true, | |
| defaultValue: 512, | |
| tdCls: 'pve-itype-icon-swap', | |
| renderer: function(value) { | |
| return PVE.Utils.format_size(value*1024*1024); | |
| } | |
| }, | |
| cpulimit: { | |
| header: gettext('CPU limit'), | |
| never_delete: true, | |
| editor: caps.vms['VM.Config.CPU'] ? 'PVE.lxc.CPUEdit' : undefined, | |
| defaultValue: 1, | |
| tdCls: 'pve-itype-icon-processor', | |
| renderer: function(value) { | |
| if (value) { return value; } | |
| return gettext('unlimited'); | |
| } | |
| }, | |
| cpuunits: { | |
| header: gettext('CPU units'), | |
| never_delete: true, | |
| editor: caps.vms['VM.Config.CPU'] ? 'PVE.lxc.CPUEdit' : undefined, | |
| defaultValue: 1024, | |
| tdCls: 'pve-itype-icon-processor' | |
| }, | |
| rootfs: { | |
| header: gettext('Root Disk'), | |
| defaultValue: PVE.Utils.noneText, | |
| editor: mpeditor, | |
| tdCls: 'pve-itype-icon-storage' | |
| } | |
| }; | |
| for (i = 0; i < 10; i++) { | |
| confid = "mp" + i; | |
| rows[confid] = { | |
| group: 1, | |
| tdCls: 'pve-itype-icon-storage', | |
| editor: mpeditor, | |
| header: gettext('Mount Point') + ' (' + confid + ')' | |
| }; | |
| } | |
| for (i = 0; i < 8; i++) { | |
| confid = "unused" + i; | |
| rows[confid] = { | |
| group: 1, | |
| tdCls: 'pve-itype-icon-storage', | |
| editor: mpeditor, | |
| header: gettext('Unused Disk') + ' ' + i | |
| }; | |
| } | |
| var reload = function() { | |
| me.rstore.load(); | |
| }; | |
| var baseurl = 'nodes/' + nodename + '/lxc/' + 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; | |
| var win = Ext.create(editor, { | |
| pveSelNode: me.pveSelNode, | |
| confid: rec.data.key, | |
| url: '/api2/extjs/' + baseurl | |
| }); | |
| win.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var run_resize = function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| return; | |
| } | |
| var win = Ext.create('PVE.window.MPResize', { | |
| disk: rec.data.key, | |
| nodename: nodename, | |
| vmid: vmid | |
| }); | |
| win.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var run_remove = function(b, e, rec) { | |
| PVE.Utils.API2Request({ | |
| url: '/api2/extjs/' + baseurl, | |
| waitMsgTarget: me, | |
| method: 'PUT', | |
| params: { | |
| 'delete': rec.data.key | |
| }, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert('Error', response.htmlStatus); | |
| } | |
| }); | |
| }; | |
| var edit_btn = new PVE.button.Button({ | |
| text: gettext('Edit'), | |
| selModel: sm, | |
| disabled: true, | |
| enableFn: function(rec) { | |
| if (!rec) { | |
| return false; | |
| } | |
| var rowdef = rows[rec.data.key]; | |
| return !!rowdef.editor; | |
| }, | |
| handler: run_editor | |
| }); | |
| var resize_btn = new PVE.button.Button({ | |
| text: gettext('Resize disk'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: run_resize | |
| }); | |
| var remove_btn = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| 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 set_button_status = function() { | |
| var sm = me.getSelectionModel(); | |
| var rec = sm.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 && PVE.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'); | |
| resize_btn.setDisabled(!isDisk); | |
| }; | |
| Ext.apply(me, { | |
| url: '/api2/json/' + baseurl, | |
| selModel: sm, | |
| 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, | |
| pveSelNode: me.pveSelNode | |
| }); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| } | |
| ] | |
| }) | |
| }, | |
| edit_btn, | |
| remove_btn, | |
| resize_btn | |
| ], | |
| rows: rows, | |
| listeners: { | |
| afterrender: reload, | |
| itemdblclick: run_editor, | |
| selectionchange: set_button_status | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.lxc.Options', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveLxcOptions'], | |
| 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: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Start at boot'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| 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'] ? | |
| 'PVE.qemu.StartupEdit' : undefined | |
| }, | |
| ostype: { | |
| header: gettext('OS Type'), | |
| defaultValue: PVE.Utils.unknownText | |
| }, | |
| arch: { | |
| header: gettext('Architecture'), | |
| defaultValue: PVE.Utils.unknownText | |
| }, | |
| console: { | |
| header: gettext('Enable /dev/console'), | |
| defaultValue: 1, | |
| renderer: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Enable /dev/console'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| name: 'console', | |
| uncheckedValue: 0, | |
| defaultValue: 1, | |
| deleteDefaultValue: true, | |
| checked: true, | |
| fieldLabel: gettext('Enable /dev/console') | |
| } | |
| } : undefined | |
| }, | |
| tty: { | |
| header: gettext('TTY count'), | |
| defaultValue: 2, | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('TTY count'), | |
| items: { | |
| xtype: 'numberfield', | |
| name: 'tty', | |
| decimalPrecision: 0, | |
| minValue: 0, | |
| maxValue: 6, | |
| value: 2, | |
| fieldLabel: gettext('TTY count'), | |
| allowEmpty: gettext('Default'), | |
| getSubmitData: function() { | |
| var me = this; | |
| var val = me.getSubmitValue(); | |
| if (val !== null && val !== '' && val !== '2') { | |
| return { tty: val }; | |
| } else { | |
| return { 'delete' : 'tty' }; | |
| } | |
| } | |
| } | |
| } : undefined | |
| }, | |
| cmode: { | |
| header: gettext('Console mode'), | |
| defaultValue: 'tty', | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Console mode'), | |
| items: { | |
| xtype: 'pveKVComboBox', | |
| name: 'cmode', | |
| deleteEmpty: true, | |
| value: '__default__', | |
| comboItems: [ | |
| ['__default__', PVE.Utils.defaultText + " (tty)"], | |
| ['tty', "/dev/tty[X]"], | |
| ['console', "/dev/console"], | |
| ['shell', "shell"] | |
| ], | |
| fieldLabel: gettext('Console mode') | |
| } | |
| } : undefined | |
| }, | |
| protection: { | |
| header: gettext('Protection'), | |
| defaultValue: false, | |
| renderer: PVE.Utils.format_boolean, | |
| editor: caps.vms['VM.Config.Options'] ? { | |
| xtype: 'pveWindowEdit', | |
| subject: gettext('Protection'), | |
| items: { | |
| xtype: 'pvecheckbox', | |
| name: 'protection', | |
| uncheckedValue: 0, | |
| defaultValue: 0, | |
| deleteDefaultValue: true, | |
| fieldLabel: gettext('Enabled') | |
| } | |
| } : undefined | |
| } | |
| }; | |
| 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.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var edit_btn = new PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| enableFn: function(rec) { | |
| var rowdef = rows[rec.data.key]; | |
| return !!rowdef.editor; | |
| }, | |
| handler: run_editor | |
| }); | |
| Ext.apply(me, { | |
| url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/config", | |
| selModel: sm, | |
| tbar: [ edit_btn ], | |
| rows: rows, | |
| listeners: { | |
| itemdblclick: run_editor, | |
| activate: reload | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.lxc.DNSInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.pveLxcDNSInputPanel', | |
| insideWizard: false, | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (!values.searchdomain) { | |
| if (me.insideWizard) { | |
| return {}; | |
| } else { | |
| return { "delete": "searchdomain,nameserver" }; | |
| } | |
| } | |
| var list = []; | |
| Ext.Array.each(['dns1', 'dns2', 'dns3'], function(fn) { | |
| if (values[fn]) { | |
| list.push(values[fn]); | |
| } | |
| delete values[fn]; | |
| }); | |
| if (list.length) { | |
| values.nameserver = list.join(' '); | |
| } else { | |
| if (!me.insideWizard) { | |
| values['delete'] = 'nameserver'; | |
| } | |
| } | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| var items = [ | |
| { | |
| xtype: 'pvetextfield', | |
| name: 'searchdomain', | |
| skipEmptyText: true, | |
| fieldLabel: gettext('DNS domain'), | |
| emptyText: gettext('use host settings'), | |
| allowBlank: true, | |
| listeners: { | |
| change: function(f, value) { | |
| if (!me.rendered) { | |
| return; | |
| } | |
| var field_ids = ['#dns1', '#dns2', '#dns3']; | |
| Ext.Array.each(field_ids, function(fn) { | |
| var field = me.down(fn); | |
| field.setDisabled(!value); | |
| field.clearInvalid(); | |
| }); | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| fieldLabel: gettext('DNS server') + " 1", | |
| vtype: 'IP64Address', | |
| allowBlank: true, | |
| disabled: true, | |
| name: 'dns1', | |
| itemId: 'dns1' | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| fieldLabel: gettext('DNS server') + " 2", | |
| vtype: 'IP64Address', | |
| skipEmptyText: true, | |
| disabled: true, | |
| name: 'dns2', | |
| itemId: 'dns2' | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| fieldLabel: gettext('DNS server') + " 3", | |
| vtype: 'IP64Address', | |
| skipEmptyText: true, | |
| disabled: true, | |
| name: 'dns3', | |
| itemId: 'dns3' | |
| } | |
| ]; | |
| if (me.insideWizard) { | |
| me.column1 = items; | |
| } else { | |
| me.items = items; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.lxc.DNSEdit', { | |
| extend: 'PVE.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.create) { | |
| me.load({ | |
| success: function(response, options) { | |
| var values = response.result.data; | |
| if (values.nameserver) { | |
| values.nameserver.replace(/[,;]/, ' '); | |
| values.nameserver.replace(/^\s+/, ''); | |
| var nslist = values.nameserver.split(/\s+/); | |
| values.dns1 = nslist[0]; | |
| values.dns2 = nslist[1]; | |
| values.dns3 = nslist[2]; | |
| } | |
| ipanel.setValues(values); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| /*jslint confusion: true */ | |
| Ext.define('PVE.lxc.DNS', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveLxcDNS'], | |
| 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: 'pveWindowEdit', | |
| subject: gettext('Hostname'), | |
| items: { | |
| xtype: 'textfield', | |
| name: 'hostname', | |
| vtype: 'DnsName', | |
| value: '', | |
| fieldLabel: gettext('Hostname'), | |
| allowBlank: true, | |
| emptyText: me.pveSelNode.data.name | |
| } | |
| } : undefined | |
| }, | |
| searchdomain: { | |
| header: gettext('DNS domain'), | |
| defaultValue: '', | |
| editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, | |
| renderer: function(value) { | |
| if (me.getObjectValue('nameserver') || me.getObjectValue('searchdomain')) { | |
| return value; | |
| } | |
| return gettext('use host settings'); | |
| } | |
| }, | |
| nameserver: { | |
| header: gettext('DNS server'), | |
| defaultValue: '', | |
| editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, | |
| renderer: function(value) { | |
| if (me.getObjectValue('nameserver') || me.getObjectValue('searchdomain')) { | |
| return value; | |
| } | |
| return 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.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var edit_btn = new PVE.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, | |
| 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', | |
| 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 caps = Ext.state.Manager.get('GuiCap'); | |
| var base_url = '/nodes/' + nodename + '/lxc/' + vmid; | |
| me.statusStore = Ext.create('PVE.data.ObjectStore', { | |
| url: '/api2/json' + base_url + '/status/current', | |
| interval: 1000 | |
| }); | |
| var vm_command = function(cmd, params) { | |
| PVE.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'], | |
| handler: function() { | |
| vm_command('start'); | |
| }, | |
| iconCls: 'fa fa-play' | |
| }); | |
| var umountBtn = Ext.create('Ext.Button', { | |
| text: gettext('Unmount'), | |
| disabled: true, | |
| hidden: true, | |
| handler: function() { | |
| vm_command('umount'); | |
| } | |
| }); | |
| var stopBtn = Ext.create('Ext.menu.Item',{ | |
| text: gettext('Stop'), | |
| disabled: !caps.vms['VM.PowerMgmt'], | |
| confirmMsg: PVE.Utils.format_task_description('vzstop', vmid), | |
| 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'], | |
| confirmMsg: PVE.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'], | |
| handler: function() { | |
| var win = Ext.create('PVE.window.Migrate', { | |
| vmtype: 'lxc', | |
| nodename: nodename, | |
| vmid: vmid | |
| }); | |
| win.show(); | |
| }, | |
| iconCls: 'fa fa-send-o' | |
| }); | |
| var removeBtn = Ext.create('PVE.button.Button', { | |
| text: gettext('Remove'), | |
| disabled: !caps.vms['VM.Allocate'], | |
| handler: function() { | |
| Ext.create('PVE.window.SafeDestroy', { | |
| url: base_url, | |
| item: { type: 'CT', id: vmid } | |
| }).show(); | |
| }, | |
| iconCls: 'fa fa-trash-o' | |
| }); | |
| var vmname = me.pveSelNode.data.name; | |
| var consoleBtn = Ext.create('PVE.button.ConsoleButton', { | |
| disabled: !caps.vms['VM.Console'], | |
| consoleType: 'lxc', | |
| consoleName: vmname, | |
| nodename: nodename, | |
| vmid: vmid, | |
| iconCls: 'fa fa-terminal' | |
| }); | |
| var descr = vmid + " (" + (vmname ? "'" + vmname + "' " : "'CT " + vmid + "'") + ")"; | |
| Ext.apply(me, { | |
| title: Ext.String.format(gettext("Container {0} on node {1}"), descr, "'" + nodename + "'"), | |
| hstateid: 'lxctab', | |
| tbar: [ startBtn, shutdownBtn, umountBtn, removeBtn, | |
| migrateBtn, consoleBtn ], | |
| defaults: { statusStore: me.statusStore }, | |
| items: [ | |
| { | |
| title: gettext('Summary'), | |
| xtype: 'pveLxcSummary', | |
| itemId: 'summary' | |
| }, | |
| { | |
| title: gettext('Resources'), | |
| itemId: 'resources', | |
| layout: 'fit', | |
| plugins: { | |
| ptype: 'lazyitems', | |
| items: [{ | |
| xtype: 'pveLxcRessourceView', | |
| pveSelNode: me.pveSelNode | |
| }] | |
| } | |
| }, | |
| { | |
| title: gettext('Network'), | |
| itemId: 'network', | |
| xtype: 'pveLxcNetworkView' | |
| }, | |
| { | |
| title: gettext('DNS'), | |
| itemId: 'dns', | |
| xtype: 'pveLxcDNS' | |
| }, | |
| { | |
| title: gettext('Options'), | |
| itemId: 'options', | |
| xtype: 'pveLxcOptions' | |
| }, | |
| { | |
| title: gettext('Task History'), | |
| itemId: 'tasks', | |
| xtype: 'pveNodeTasks', | |
| vmidFilter: vmid | |
| } | |
| ] | |
| }); | |
| if (caps.vms['VM.Backup']) { | |
| me.items.push({ | |
| title: gettext('Backup'), | |
| xtype: 'pveBackupView', | |
| itemId: 'backup' | |
| }); | |
| } | |
| if (caps.vms['VM.Console']) { | |
| me.items.push({ | |
| title: gettext('Console'), | |
| itemId: 'console', | |
| xtype: 'pveNoVncConsole', | |
| vmid: vmid, | |
| consoleType: 'lxc', | |
| nodename: nodename | |
| }); | |
| } | |
| if (caps.vms['VM.Snapshot']) { | |
| me.items.push({ | |
| title: gettext('Snapshots'), | |
| xtype: 'pveLxcSnapshotTree', | |
| itemId: 'snapshot' | |
| }); | |
| } | |
| if (caps.vms['VM.Console']) { | |
| me.items.push( | |
| { | |
| xtype: 'pveFirewallPanel', | |
| title: gettext('Firewall'), | |
| base_url: base_url + '/firewall', | |
| fwtype: 'vm', | |
| phstateid: me.hstateid, | |
| itemId: 'firewall' | |
| } | |
| ); | |
| } | |
| if (caps.vms['Permissions.Modify']) { | |
| me.items.push({ | |
| xtype: 'pveACLView', | |
| title: gettext('Permissions'), | |
| itemId: 'permissions', | |
| path: '/vms/' + vmid | |
| }); | |
| } | |
| me.callParent(); | |
| me.mon(me.statusStore, 'load', function(s, records, success) { | |
| var status; | |
| if (!success) { | |
| me.workspace.checkVmMigration(me.pveSelNode); | |
| 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; | |
| } | |
| 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'); | |
| removeBtn.setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); | |
| consoleBtn.setDisabled(template); | |
| if (status === 'mounted') { | |
| umountBtn.setDisabled(false); | |
| umountBtn.setVisible(true); | |
| stopBtn.setDisabled(true); | |
| } else { | |
| umountBtn.setDisabled(true); | |
| umountBtn.setVisible(false); | |
| stopBtn.setDisabled(false); | |
| } | |
| }); | |
| 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', | |
| initComponent: function() { | |
| var me = this; | |
| var summarystore = Ext.create('Ext.data.Store', { | |
| model: 'KeyValue', | |
| sorters: [ | |
| { | |
| property : 'key', | |
| direction: 'ASC' | |
| } | |
| ] | |
| }); | |
| var tmplsel = Ext.create('PVE.form.FileSelector', { | |
| name: 'ostemplate', | |
| storageContent: 'vztmpl', | |
| fieldLabel: gettext('Template'), | |
| allowBlank: false | |
| }); | |
| var tmplstoragesel = Ext.create('PVE.form.StorageSelector', { | |
| name: 'tmplstorage', | |
| fieldLabel: gettext('Storage'), | |
| storageContent: 'vztmpl', | |
| autoSelect: true, | |
| allowBlank: false, | |
| listeners: { | |
| change: function(f, value) { | |
| tmplsel.setStorage(value); | |
| } | |
| } | |
| }); | |
| var rootfspanel = Ext.create('PVE.lxc.MountPointInputPanel', { | |
| title: gettext('Root Disk'), | |
| insideWizard: true, | |
| create: true, | |
| unused: false, | |
| confid: 'rootfs' | |
| }); | |
| var networkpanel = Ext.create('PVE.lxc.NetworkInputPanel', { | |
| title: gettext('Network'), | |
| insideWizard: true, | |
| dataCache: {}, | |
| create: true | |
| }); | |
| Ext.applyIf(me, { | |
| subject: gettext('LXC Container'), | |
| items: [ | |
| { | |
| xtype: 'inputpanel', | |
| title: gettext('General'), | |
| column1: [ | |
| { | |
| xtype: 'pveNodeSelector', | |
| name: 'nodename', | |
| fieldLabel: gettext('Node'), | |
| allowBlank: false, | |
| onlineValidator: true, | |
| listeners: { | |
| change: function(f, value) { | |
| tmplstoragesel.setNodename(value); | |
| tmplsel.setStorage(undefined, value); | |
| networkpanel.setNodename(value); | |
| rootfspanel.setNodename(value); | |
| } | |
| } | |
| }, | |
| { | |
| xtype: 'pveVMIDSelector', | |
| name: 'vmid', | |
| value: '', | |
| loadNextFreeVMID: true, | |
| validateExists: false | |
| }, | |
| { | |
| xtype: 'pvetextfield', | |
| name: 'hostname', | |
| vtype: 'DnsName', | |
| value: '', | |
| fieldLabel: gettext('Hostname'), | |
| skipEmptyText: true, | |
| allowBlank: true | |
| } | |
| ], | |
| 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 (!me.rendered) { | |
| return; | |
| } | |
| me.down('field[name=confirmpw]').validate(); | |
| } | |
| }, | |
| { | |
| xtype: 'textfield', | |
| inputType: 'password', | |
| name: 'confirmpw', | |
| value: '', | |
| fieldLabel: gettext('Confirm password'), | |
| allowBlank: false, | |
| validator: function(value) { | |
| var pw = me.down('field[name=password]').getValue(); | |
| if (pw !== value) { | |
| return "Passwords does not match!"; | |
| } | |
| return true; | |
| } | |
| } | |
| ], | |
| onGetValues: function(values) { | |
| delete values.confirmpw; | |
| if (!values.pool) { | |
| delete values.pool; | |
| } | |
| return values; | |
| } | |
| }, | |
| { | |
| xtype: 'inputpanel', | |
| title: gettext('Template'), | |
| column1: [ tmplstoragesel, tmplsel] | |
| }, | |
| rootfspanel, | |
| { | |
| xtype: 'pveLxcCPUInputPanel', | |
| title: gettext('CPU'), | |
| insideWizard: true | |
| }, | |
| { | |
| xtype: 'pveLxcMemoryInputPanel', | |
| title: gettext('Memory'), | |
| insideWizard: true | |
| }, | |
| networkpanel, | |
| { | |
| xtype: 'pveLxcDNSInputPanel', | |
| title: gettext('DNS'), | |
| insideWizard: true | |
| }, | |
| { | |
| title: gettext('Confirm'), | |
| layout: 'fit', | |
| items: [ | |
| { | |
| title: gettext('Settings'), | |
| xtype: 'grid', | |
| store: summarystore, | |
| columns: [ | |
| {header: 'Key', width: 150, dataIndex: 'key'}, | |
| {header: 'Value', flex: 1, dataIndex: 'value'} | |
| ] | |
| } | |
| ], | |
| listeners: { | |
| show: function(panel) { | |
| var form = me.down('form').getForm(); | |
| var kv = me.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 }); | |
| }); | |
| summarystore.suspendEvents(); | |
| summarystore.removeAll(); | |
| summarystore.add(data); | |
| summarystore.sort(); | |
| summarystore.resumeEvents(); | |
| summarystore.fireEvent('datachanged', summarystore); | |
| } | |
| }, | |
| onSubmit: function() { | |
| var kv = me.getValues(); | |
| delete kv['delete']; | |
| var nodename = kv.nodename; | |
| delete kv.nodename; | |
| delete kv.tmplstorage; | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + nodename + '/lxc', | |
| waitMsgTarget: me, | |
| method: 'POST', | |
| params: kv, | |
| success: function(response, opts){ | |
| var upid = response.result.data; | |
| var win = Ext.create('PVE.window.TaskViewer', { | |
| upid: upid | |
| }); | |
| win.show(); | |
| me.close(); | |
| }, | |
| failure: function(response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.lxc.SnapshotTree', { | |
| extend: 'Ext.tree.Panel', | |
| alias: ['widget.pveLxcSnapshotTree'], | |
| load_delay: 3000, | |
| old_digest: 'invalid', | |
| 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; | |
| PVE.Utils.API2Request({ | |
| url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot', | |
| method: 'GET', | |
| failure: function(response, opts) { | |
| PVE.Utils.setErrorMask(me, response.htmlStatus); | |
| me.load_task.delay(me.load_delay); | |
| }, | |
| success: function(response, opts) { | |
| PVE.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); | |
| } | |
| }); | |
| PVE.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(); | |
| }); | |
| } | |
| } | |
| }); | |
| }, | |
| 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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| enableFn: valid_snapshot, | |
| handler: run_editor | |
| }); | |
| var rollbackBtn = new PVE.button.Button({ | |
| text: gettext('Rollback'), | |
| disabled: true, | |
| selModel: sm, | |
| enableFn: valid_snapshot_rollback, | |
| confirmMsg: function(rec) { | |
| return PVE.Utils.format_task_description('vzrollback', me.vmid) + | |
| " '" + rec.data.name + "'"; | |
| }, | |
| handler: function(btn, event) { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec) { | |
| return; | |
| } | |
| var snapname = rec.data.name; | |
| PVE.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('PVE.window.TaskProgress', { upid: upid }); | |
| win.show(); | |
| } | |
| }); | |
| } | |
| }); | |
| var removeBtn = new PVE.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; | |
| PVE.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('PVE.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 PVE.Utils.format_boolean(value); | |
| // } | |
| // } | |
| // }, | |
| { | |
| text: gettext('Date') + "/" + gettext("Status"), | |
| dataIndex: 'snaptime', | |
| resizable: false, | |
| width: 120, | |
| 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, | |
| hide: me.load_task.cancel, | |
| 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, | |
| take_snapshot: function(snapname, descr, vmstate) { | |
| var me = this; | |
| var params = { snapname: snapname }; | |
| if (descr) { | |
| params.description = descr; | |
| } | |
| PVE.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('PVE.window.TaskProgress', { upid: upid }); | |
| win.show(); | |
| me.close(); | |
| } | |
| }); | |
| }, | |
| update_snapshot: function(snapname, descr) { | |
| var me = this; | |
| PVE.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', | |
| 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'), | |
| 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 | |
| PVE.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(new Date(data.snaptime)); | |
| form.findField('description').setValue(data.description); | |
| } | |
| }); | |
| } | |
| }); | |
| /*jslint confusion: true */ | |
| var labelWidth = 120; | |
| Ext.define('PVE.lxc.MemoryEdit', { | |
| extend: 'PVE.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: 'PVE.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.MountPointEdit', { | |
| extend: 'PVE.window.Edit', | |
| 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.create = me.confid ? unused : true; | |
| var ipanel = Ext.create('PVE.lxc.MountPointInputPanel', { | |
| confid: me.confid, | |
| nodename: nodename, | |
| unused: unused, | |
| create: me.create | |
| }); | |
| var subject; | |
| if (unused) { | |
| subject = gettext('Unused Disk'); | |
| } else if (me.create) { | |
| subject = gettext('Mount Point'); | |
| } else { | |
| subject = gettext('Mount Point') + ' (' + me.confid + ')'; | |
| } | |
| Ext.apply(me, { | |
| subject: subject, | |
| 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 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.lxc.CPUInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.pveLxcCPUInputPanel', | |
| insideWizard: false, | |
| initComponent : function() { | |
| var me = this; | |
| var items = [ | |
| { | |
| xtype: 'numberfield', | |
| name: 'cpulimit', | |
| minValue: 0, | |
| value: '1', | |
| step: 1, | |
| fieldLabel: gettext('CPU limit'), | |
| labelWidth: labelWidth, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'cpuunits', | |
| fieldLabel: gettext('CPU units'), | |
| value: 1024, | |
| minValue: 8, | |
| maxValue: 500000, | |
| labelWidth: labelWidth, | |
| allowBlank: false | |
| } | |
| ]; | |
| if (me.insideWizard) { | |
| me.column1 = items; | |
| } else { | |
| me.items = items; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.lxc.MemoryInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.pveLxcMemoryInputPanel', | |
| insideWizard: false, | |
| initComponent : function() { | |
| var me = this; | |
| var items = [ | |
| { | |
| xtype: 'numberfield', | |
| name: 'memory', | |
| minValue: 32, | |
| maxValue: 512*1024, | |
| value: '512', | |
| step: 32, | |
| fieldLabel: gettext('Memory') + ' (MB)', | |
| labelWidth: labelWidth, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'swap', | |
| minValue: 0, | |
| maxValue: 128*1024, | |
| value: '512', | |
| step: 32, | |
| fieldLabel: gettext('Swap') + ' (MB)', | |
| labelWidth: labelWidth, | |
| allowBlank: false | |
| } | |
| ]; | |
| if (me.insideWizard) { | |
| me.column1 = items; | |
| } else { | |
| me.items = items; | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.lxc.MountPointInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| alias: 'widget.pveLxcMountPointInputPanel', | |
| insideWizard: false, | |
| unused: false, // ADD usused disk imaged | |
| vmconfig: {}, // used to select usused disks | |
| onGetValues: function(values) { | |
| var me = this; | |
| var confid = me.confid || values.mpsel; | |
| if (me.unused) { | |
| me.mpdata.file = me.vmconfig[values.unusedId]; | |
| confid = values.mpsel; | |
| } else if (me.create) { | |
| me.mpdata.file = values.storage + ':' + values.disksize; | |
| } | |
| if (confid !== 'rootfs') { | |
| me.mpdata.mp = values.mp; | |
| } | |
| if (values.ro) { | |
| me.mpdata.ro = 1; | |
| } else { | |
| delete me.mpdata.ro; | |
| } | |
| if (values.quota) { | |
| me.mpdata.quota = 1; | |
| } else { | |
| delete me.mpdata.quota; | |
| } | |
| if (values.acl === 'Default') { | |
| delete me.mpdata.acl; | |
| } else { | |
| me.mpdata.acl = values.acl; | |
| } | |
| if (values.backup) { | |
| me.mpdata.backup = 1; | |
| } else { | |
| delete me.mpdata.backup; | |
| } | |
| var res = {}; | |
| res[confid] = PVE.Parser.printLxcMountPoint(me.mpdata); | |
| return res; | |
| }, | |
| setMountPoint: function(mp) { | |
| var me = this; | |
| me.mpdata = mp; | |
| if (!Ext.isDefined(me.mpdata.acl)) { | |
| me.mpdata.acl = 'Default'; | |
| } | |
| if (mp.type === 'bind') { | |
| me.quota.setDisabled(true); | |
| me.quota.setValue(false); | |
| me.acl.setDisabled(true); | |
| me.backup.setDisabled(true); | |
| me.acl.setValue('Default'); | |
| me.hdstoragesel.setDisabled(true); | |
| } | |
| me.setValues(mp); | |
| }, | |
| setVMConfig: function(vmconfig) { | |
| var me = this; | |
| me.vmconfig = vmconfig; | |
| if (me.mpsel) { | |
| var i; | |
| for (i = 0; i != 8; ++i) { | |
| var name = "mp" + i; | |
| if (!Ext.isDefined(vmconfig[name])) { | |
| me.mpsel.setValue(name); | |
| break; | |
| } | |
| } | |
| } | |
| 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); | |
| } | |
| }, | |
| setNodename: function(nodename) { | |
| var me = this; | |
| me.hdstoragesel.setNodename(nodename); | |
| me.hdfilesel.setStorage(undefined, nodename); | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| var isroot = me.confid === 'rootfs'; | |
| me.mpdata = {}; | |
| me.column1 = []; | |
| if (!me.confid || me.unused) { | |
| var names = []; | |
| var i; | |
| for (i = 0; i != 8; ++i) { | |
| var name = 'mp' + i; | |
| names.push([name, name]); | |
| } | |
| me.mpsel = Ext.create('PVE.form.KVComboBox', { | |
| name: 'mpsel', | |
| fieldLabel: gettext('Mount Point'), | |
| matchFieldWidth: false, | |
| allowBlank: false, | |
| comboItems: names, | |
| validator: function(value) { | |
| if (!me.rendered) { | |
| return; | |
| } | |
| if (Ext.isDefined(me.vmconfig[value])) { | |
| return "Mount point is already in use."; | |
| } | |
| return true; | |
| }, | |
| listeners: { | |
| change: function(field, value) { | |
| field.validate(); | |
| } | |
| } | |
| }); | |
| me.column1.push(me.mpsel); | |
| } | |
| // we always have this around, but only visible when creating a new mp | |
| // since this handles per-filesystem capabilities | |
| me.hdstoragesel = Ext.create('PVE.form.StorageSelector', { | |
| name: 'storage', | |
| nodename: me.nodename, | |
| fieldLabel: gettext('Storage'), | |
| storageContent: 'rootdir', | |
| allowBlank: false, | |
| autoSelect: true, | |
| hidden: me.unused || !me.create, | |
| listeners: { | |
| change: function(f, value) { | |
| if (!value) { // initial store loading fires an unwanted 'change' | |
| return; | |
| } | |
| if (me.mpdata.type === 'bind') { | |
| me.quota.setDisabled(true); | |
| me.quota.setValue(false); | |
| me.acl.setDisabled(true); | |
| me.backup.setDisabled(true); | |
| me.acl.setValue('Default'); | |
| return; | |
| } | |
| var rec = f.store.getById(value); | |
| if (rec.data.type === 'zfs' || | |
| rec.data.type === 'zfspool') { | |
| me.quota.setDisabled(true); | |
| me.quota.setValue(false); | |
| } else { | |
| me.quota.setDisabled(false); | |
| } | |
| if (me.unused || !me.create) { | |
| return; | |
| } | |
| if (rec.data.type === 'iscsi') { | |
| me.hdfilesel.setStorage(value); | |
| me.hdfilesel.setDisabled(false); | |
| me.hdfilesel.setVisible(true); | |
| me.hdsizesel.setDisabled(true); | |
| me.hdsizesel.setVisible(false); | |
| } else if (rec.data.type === 'lvm' || | |
| rec.data.type === 'lvmthin' || | |
| rec.data.type === 'rbd' || | |
| rec.data.type === 'sheepdog' || | |
| rec.data.type === 'zfs' || | |
| rec.data.type === 'zfspool') { | |
| me.hdfilesel.setDisabled(true); | |
| me.hdfilesel.setVisible(false); | |
| me.hdsizesel.setDisabled(false); | |
| me.hdsizesel.setVisible(true); | |
| } else { | |
| me.hdfilesel.setDisabled(true); | |
| me.hdfilesel.setVisible(false); | |
| me.hdsizesel.setDisabled(false); | |
| me.hdsizesel.setVisible(true); | |
| } | |
| } | |
| } | |
| }); | |
| me.column1.push(me.hdstoragesel); | |
| if (me.unused) { | |
| me.unusedDisks = Ext.create('PVE.form.KVComboBox', { | |
| name: 'unusedId', | |
| fieldLabel: gettext('Disk image'), | |
| matchFieldWidth: false, | |
| listConfig: { | |
| width: 350 | |
| }, | |
| data: [], | |
| allowBlank: false, | |
| listeners: { | |
| change: function(f, value) { | |
| // make sure our buttons are enabled/disabled when switching | |
| // between images on different storages: | |
| var disk = me.vmconfig[value]; | |
| var storage = disk.split(':')[0]; | |
| me.hdstoragesel.setValue(storage); | |
| } | |
| } | |
| }); | |
| me.column1.push(me.unusedDisks); | |
| } else if (me.create) { | |
| me.hdfilesel = Ext.create('PVE.form.FileSelector', { | |
| name: 'file', | |
| nodename: me.nodename, | |
| storageContent: 'images', | |
| fieldLabel: gettext('Disk image'), | |
| disabled: true, | |
| hidden: true, | |
| allowBlank: false | |
| }); | |
| me.hdsizesel = Ext.createWidget('numberfield', { | |
| name: 'disksize', | |
| minValue: 0.1, | |
| maxValue: 128*1024, | |
| decimalPrecision: 3, | |
| value: '8', | |
| step: 1, | |
| fieldLabel: gettext('Disk size') + ' (GB)', | |
| allowBlank: false | |
| }); | |
| me.column1.push(me.hdfilesel); | |
| me.column1.push(me.hdsizesel); | |
| } else { | |
| me.column1.push({ | |
| xtype: 'textfield', | |
| disabled: true, | |
| submitValue: false, | |
| fieldLabel: gettext('Disk image'), | |
| name: 'file' | |
| }); | |
| } | |
| me.acl = Ext.createWidget('pveKVComboBox', { | |
| name: 'acl', | |
| fieldLabel: gettext('ACLs'), | |
| comboItems: [['Default', 'Default'], ['1', 'On'], ['0', 'Off']], | |
| value: 'Default', | |
| allowBlank: true | |
| }); | |
| me.quota = Ext.createWidget('pvecheckbox', { | |
| name: 'quota', | |
| defaultValue: 0, | |
| fieldLabel: gettext('Enable quota') | |
| }); | |
| me.column2 = [ | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'ro', | |
| defaultValue: 0, | |
| fieldLabel: gettext('Read-only'), | |
| hidden: me.insideWizard | |
| }, | |
| me.acl, | |
| me.quota | |
| ]; | |
| if (!isroot) { | |
| me.backup = Ext.createWidget('pvecheckbox',{ | |
| xtype: 'pvecheckbox', | |
| name: 'backup', | |
| fieldLabel: gettext('Backup') | |
| }); | |
| if (me.mpdata.type !== 'bind') { | |
| me.column2.push(me.backup); | |
| } | |
| me.column2.push({ | |
| xtype: 'textfield', | |
| name: 'mp', | |
| value: '', | |
| emptyText: gettext('/some/path'), | |
| allowBlank: false, | |
| hidden: isroot, | |
| fieldLabel: gettext('Path') | |
| }); | |
| } | |
| 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' }; | |
| PVE.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('PVE.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') + ' (GB)', | |
| 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; | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.pool.StatusView', { | |
| extend: 'PVE.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('hide', rstore.stopUpdate); | |
| me.on('destroy', rstore.stopUpdate); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.pool.Config', { | |
| extend: 'PVE.panel.Config', | |
| alias: 'widget.pvePoolConfig', | |
| 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'), | |
| xtype: 'pvePoolSummary', | |
| itemId: 'summary' | |
| }, | |
| { | |
| title: gettext('Members'), | |
| xtype: 'pvePoolMembers', | |
| pool: pool, | |
| itemId: 'members' | |
| }, | |
| { | |
| xtype: 'pveACLView', | |
| title: gettext('Permissions'), | |
| itemId: 'permissions', | |
| path: '/pool/' + pool | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.grid.TemplateSelector', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: 'widget.pveTemplateSelector', | |
| stateful: false, | |
| 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: 'pve', | |
| 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(); | |
| }; | |
| PVE.Utils.monStoreErrors(me, store); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| 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: 600, | |
| height: 400, | |
| 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('PVE.button.Button', { | |
| text: gettext('Download'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: function(button, event, rec) { | |
| PVE.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; | |
| var win = Ext.create('PVE.window.TaskViewer', { | |
| upid: upid | |
| }); | |
| win.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 += " (" + PVE.Utils.format_size(bytes) + ')'; | |
| } | |
| pbar.updateProgress(per, text); | |
| }; | |
| var abortBtn = Ext.create('Ext.Button', { | |
| text: gettext('Abort'), | |
| disabled: true, | |
| handler: function() { | |
| me.close(); | |
| } | |
| }); | |
| var submitBtn = Ext.create('Ext.Button', { | |
| text: gettext('Upload'), | |
| disabled: true, | |
| handler: function(button) { | |
| var fd; | |
| try { | |
| fd = new FormData(); | |
| } catch (err) { | |
| doStandardSubmit(); | |
| return; | |
| } | |
| button.setDisabled(true); | |
| abortBtn.setDisabled(false); | |
| var field = form.findField('content'); | |
| fd.append("content", field.getValue()); | |
| field.setDisabled(true); | |
| field = form.findField('filename'); | |
| var file = field.fileInputEl.dom; | |
| fd.append("filename", file.files[0]); | |
| field.setDisabled(true); | |
| pbar.setVisible(true); | |
| updateProgress(0); | |
| xhr = new XMLHttpRequest(); | |
| xhr.addEventListener("load", function(e) { | |
| if (xhr.status == 200) { | |
| me.close(); | |
| } else { | |
| var msg = gettext('Error') + " " + xhr.status.toString() + ": " + Ext.htmlEncode(xhr.statusText); | |
| var result = Ext.decode(xhr.responseText); | |
| result.message = msg; | |
| var htmlStatus = PVE.Utils.extractRequestError(result, true); | |
| Ext.Msg.alert(gettext('Error'), htmlStatus, function(btn) { | |
| me.close(); | |
| }); | |
| } | |
| }, false); | |
| xhr.addEventListener("error", function(e) { | |
| var msg = "Error " + e.target.status.toString() + " occurred while receiving the document."; | |
| Ext.Msg.alert(gettext('Error'), msg, function(btn) { | |
| me.close(); | |
| }); | |
| }); | |
| xhr.upload.addEventListener("progress", function(evt) { | |
| if (evt.lengthComputable) { | |
| var percentComplete = evt.loaded / evt.total; | |
| updateProgress(percentComplete, evt.loaded); | |
| } | |
| }, false); | |
| xhr.open("POST", "/api2/json" + baseurl, true); | |
| xhr.send(fd); | |
| } | |
| }); | |
| form.on('validitychange', function(f, valid) { | |
| submitBtn.setDisabled(!valid); | |
| }); | |
| Ext.apply(me, { | |
| title: gettext('Upload'), | |
| items: me.formPanel, | |
| buttons: [ abortBtn, submitBtn ], | |
| listeners: { | |
| close: function() { | |
| if (xhr) { | |
| xhr.abort(); | |
| } | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.ContentView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: 'widget.pveStorageContentView', | |
| stateful: false, | |
| viewConfig: { | |
| trackOver: false, | |
| loadMask: false | |
| }, | |
| 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: 'pve', | |
| url: '/api2/json' + baseurl | |
| }, | |
| sorters: { | |
| property: 'volid', | |
| order: 'DESC' | |
| } | |
| }); | |
| var sm = Ext.create('Ext.selection.RowModel', {}); | |
| var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{ | |
| groupHeaderTpl: '{[ PVE.Utils.format_content_types(values.name) ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})' | |
| }); | |
| var reload = function() { | |
| store.load(); | |
| me.statusStore.load(); | |
| }; | |
| PVE.Utils.monStoreErrors(me, store); | |
| var templateButton = Ext.create('PVE.button.Button',{ | |
| itemId: 'tmpl-btn', | |
| text: gettext('Templates'), | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.TemplateDownload', { | |
| nodename: nodename, | |
| storage: storage | |
| }); | |
| win.show(); | |
| win.on('destroy', reload); | |
| } | |
| }); | |
| var uploadButton = Ext.create('PVE.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); | |
| } | |
| }); | |
| me.statusStore = Ext.create('PVE.data.ObjectStore', { | |
| url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status' | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| features: [ groupingFeature ], | |
| tbar: [ | |
| { | |
| xtype: 'pveButton', | |
| 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); | |
| } | |
| }, | |
| { | |
| xtype: 'pveButton', | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| disabled: true, | |
| confirmMsg: function(rec) { | |
| return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), | |
| "'" + rec.data.volid + "'"); | |
| }, | |
| enableFn: function(rec) { | |
| return rec && rec.data.content !== 'images'; | |
| }, | |
| handler: function(b, e, rec) { | |
| PVE.Utils.API2Request({ | |
| url: baseurl + '/' + rec.data.volid, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }, | |
| templateButton, | |
| uploadButton, | |
| '->', | |
| 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('Size'), | |
| width: 100, | |
| renderer: PVE.Utils.format_size, | |
| dataIndex: 'size' | |
| } | |
| ], | |
| listeners: { | |
| activate: reload | |
| } | |
| }); | |
| me.callParent(); | |
| // disable the buttons/restrict the upload window | |
| // if templates or uploads are not allowed | |
| me.mon(me.statusStore, 'load', function(s,records,succes) { | |
| var availcontent = []; | |
| Ext.Array.each(records, function(item){ | |
| if (item.id === 'content') { | |
| availcontent = item.data.value.split(','); | |
| } | |
| }); | |
| var templ = false; | |
| var upload = false; | |
| var cts = []; | |
| Ext.Array.each(availcontent, function(content) { | |
| if (content === 'vztmpl') { | |
| templ = true; | |
| cts.push('vztmpl'); | |
| } else if (content === 'iso') { | |
| upload = true; | |
| cts.push('iso'); | |
| } | |
| }); | |
| if (templ !== upload) { | |
| uploadButton.contents = cts; | |
| } | |
| templateButton.setDisabled(!templ); | |
| uploadButton.setDisabled(!upload && !templ); | |
| }); | |
| } | |
| }, function() { | |
| Ext.define('pve-storage-content', { | |
| extend: 'Ext.data.Model', | |
| fields: [ | |
| 'volid', 'content', 'format', 'size', 'used', 'vmid', | |
| 'channel', 'id', 'lun', | |
| { | |
| name: 'text', | |
| convert: function(value, record) { | |
| // check for volid, because if you click on a grouping header, | |
| // it calls convert (but with an empty volid) | |
| if (value || record.data.volid === null) { | |
| return value; | |
| } | |
| return PVE.Utils.render_storage_content(value, {}, record); | |
| } | |
| } | |
| ], | |
| idProperty: 'volid' | |
| }); | |
| }); | |
| Ext.define('PVE.storage.StatusView', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: 'widget.pveStorageStatusView', | |
| disabled: true, | |
| title: gettext('Status'), | |
| cwidth1: 150, | |
| interval: 30000, | |
| rows : { | |
| disable: { | |
| header: gettext('Enabled'), | |
| required: true, | |
| renderer: PVE.Utils.format_neg_boolean | |
| }, | |
| active: { | |
| header: gettext('Active'), | |
| required: true, | |
| renderer: PVE.Utils.format_boolean | |
| }, | |
| content: { | |
| header: gettext('Content'), | |
| required: true, | |
| renderer: PVE.Utils.format_content_types | |
| }, | |
| type: { | |
| header: gettext('Type'), | |
| required: true, | |
| renderer: PVE.Utils.format_storage_type | |
| }, | |
| shared: { | |
| header: gettext('Shared'), | |
| required: true, | |
| renderer: PVE.Utils.format_boolean | |
| }, | |
| total: { | |
| header: gettext('Size'), | |
| required: true, | |
| renderer: PVE.Utils.render_size | |
| }, | |
| used: { | |
| header: gettext('Used'), | |
| required: true, | |
| renderer: PVE.Utils.render_size | |
| }, | |
| avail: { | |
| header: gettext('Avail'), | |
| required: true, | |
| renderer: PVE.Utils.render_size | |
| } | |
| }, | |
| 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"; | |
| } | |
| Ext.apply(me, { | |
| url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status" | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.Summary', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pveStorageSummary', | |
| scrollable: true, | |
| bodyPadding: 10, | |
| defaults: { | |
| style: {'padding-top':'10px'}, | |
| width: 800 | |
| }, | |
| tbar: [ | |
| '->', | |
| { | |
| xtype: 'pveRRDTypeSelector' | |
| } | |
| ], | |
| 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 statusview = Ext.create('PVE.storage.StatusView', { | |
| pveSelNode: me.pveSelNode, | |
| style: {'padding-top':'0px'} | |
| }); | |
| var rstore = statusview.rstore; | |
| var rrdstore = Ext.create('PVE.data.RRDStore', { | |
| rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata" | |
| }); | |
| Ext.apply(me, { | |
| items: [ | |
| statusview, | |
| { | |
| xtype: 'pveRRDChart', | |
| title: gettext('Usage'), | |
| fields: ['total','used'], | |
| fieldTitles: ['Total Size', 'Used Size'], | |
| store: rrdstore | |
| } | |
| ], | |
| listeners: { | |
| activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, | |
| hide: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); }, | |
| destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.Browser', { | |
| extend: 'PVE.panel.Config', | |
| alias: 'widget.PVE.storage.Browser', | |
| 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', | |
| 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']) { | |
| me.items.push({ | |
| xtype: 'pveStorageContentView', | |
| title: gettext('Content'), | |
| itemId: 'content' | |
| }); | |
| } | |
| if (caps.storage['Permissions.Modify']) { | |
| me.items.push({ | |
| xtype: 'pveACLView', | |
| title: gettext('Permissions'), | |
| itemId: 'permissions', | |
| path: '/storage/' + storeid | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.DirInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'dir'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? '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: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'shared', | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Shared') | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| fieldLabel: gettext('Max Backups'), | |
| name: 'maxfiles', | |
| minValue: 0, | |
| maxValue: 365, | |
| value: me.create ? '1' : undefined, | |
| allowBlank: false | |
| } | |
| ]; | |
| if (me.create || me.storageId !== 'local') { | |
| me.column2.unshift({ | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.DirEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.DirInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: PVE.Utils.format_storage_type('dir'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| 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.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: 'pve', | |
| url: '/api2/json/nodes/' + me.nodename + '/scan/nfs' | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.NFSInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'nfs'; | |
| // hack: for now we always create nvf v3 | |
| // fixme: make this configurable | |
| values.options = 'vers=3'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'server', | |
| value: '', | |
| fieldLabel: gettext('Server'), | |
| allowBlank: false, | |
| listeners: { | |
| change: function(f, value) { | |
| if (me.create) { | |
| var exportField = me.down('field[name=export]'); | |
| exportField.setServer(value); | |
| exportField.setValue(''); | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| xtype: me.create ? '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: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| fieldLabel: gettext('Max Backups'), | |
| name: 'maxfiles', | |
| minValue: 0, | |
| maxValue: 365, | |
| value: me.create ? '1' : undefined, | |
| allowBlank: false | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.NFSEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.NFSInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: 'NFS', | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| 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.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: 'pve', | |
| url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs' | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.GlusterFsInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'glusterfs'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'server', | |
| value: '', | |
| fieldLabel: gettext('Server'), | |
| allowBlank: false, | |
| listeners: { | |
| change: function(f, value) { | |
| if (me.create) { | |
| var volumeField = me.down('field[name=volume]'); | |
| volumeField.setServer(value); | |
| volumeField.setValue(''); | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| xtype: me.create ? 'pvetextfield' : 'displayfield', | |
| name: 'server2', | |
| value: '', | |
| fieldLabel: gettext('Second Server'), | |
| allowBlank: true | |
| }, | |
| { | |
| xtype: me.create ? 'pveGlusterFsScan' : 'displayfield', | |
| name: 'volume', | |
| value: '', | |
| fieldLabel: 'Volume name', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'pveContentTypeSelector', | |
| cts: ['images', 'iso', 'backup', 'vztmpl'], | |
| name: 'content', | |
| value: 'images', | |
| multiSelect: true, | |
| fieldLabel: gettext('Content'), | |
| allowBlank: false | |
| } | |
| ]; | |
| me.column2 = [ | |
| { | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| fieldLabel: gettext('Max Backups'), | |
| name: 'maxfiles', | |
| minValue: 0, | |
| maxValue: 365, | |
| value: me.create ? '1' : undefined, | |
| allowBlank: false | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.GlusterFsEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.GlusterFsInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: PVE.Utils.format_storage_type('glusterfs'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| 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.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: 'pve', | |
| url: '/api2/json/nodes/' + me.nodename + '/scan/iscsi' | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.IScsiInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'iscsi'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.content = values.luns ? 'images' : 'none'; | |
| delete values.luns; | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'portal', | |
| value: '', | |
| fieldLabel: 'Portal', | |
| allowBlank: false, | |
| listeners: { | |
| change: function(f, value) { | |
| if (me.create) { | |
| var exportField = me.down('field[name=target]'); | |
| exportField.setPortal(value); | |
| exportField.setValue(''); | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| readOnly: !me.create, | |
| xtype: me.create ? 'pveIScsiScan' : 'displayfield', | |
| name: 'target', | |
| value: '', | |
| fieldLabel: 'Target', | |
| allowBlank: false | |
| } | |
| ]; | |
| me.column2 = [ | |
| { | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| }, | |
| { | |
| xtype: 'checkbox', | |
| name: 'luns', | |
| checked: true, | |
| fieldLabel: gettext('Use LUNs directly') | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.IScsiEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.IScsiInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: PVE.Utils.format_storage_type('iscsi'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| me.load({ | |
| success: function(response, options) { | |
| var values = response.result.data; | |
| var ctypes = values.content || ''; | |
| if (values.storage === 'local') { | |
| values.content = ctypes.split(','); | |
| } | |
| if (values.nodes) { | |
| values.nodes = values.nodes.split(','); | |
| } | |
| values.enable = values.disable ? 0 : 1; | |
| values.luns = (values.content === 'images') ? true : false; | |
| ipanel.setValues(values); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| 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: 'pve', | |
| url: '/api2/json/nodes/' + me.nodename + '/scan/lvm' | |
| } | |
| }); | |
| 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: 'pve', | |
| url: '/api2/json/storage/' | |
| } | |
| }); | |
| store.loadData([{ storage: '' }], true); | |
| Ext.apply(me, { | |
| store: store | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.LVMInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'lvm'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| submitValue: !!me.create, | |
| allowBlank: false | |
| } | |
| ]; | |
| var vgnameField = Ext.createWidget(me.create ? 'textfield' : 'displayfield', { | |
| name: 'vgname', | |
| hidden: !!me.create, | |
| disabled: !!me.create, | |
| value: '', | |
| fieldLabel: gettext('Volume group'), | |
| allowBlank: false | |
| }); | |
| if (me.create) { | |
| 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: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'shared', | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Shared') | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.LVMEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.LVMInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: PVE.Utils.format_storage_type('lvm'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| 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.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: 'pve', | |
| url: '/api2/json/nodes/' + me.nodename + '/scan/lvmthin' | |
| } | |
| }); | |
| 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: 'pve', | |
| 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.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'lvmthin'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| submitValue: !!me.create, | |
| allowBlank: false | |
| } | |
| ]; | |
| var vgnameField = Ext.createWidget(me.create ? 'textfield' : 'displayfield', { | |
| name: 'vgname', | |
| hidden: !!me.create, | |
| disabled: !!me.create, | |
| value: '', | |
| fieldLabel: gettext('Volume group'), | |
| allowBlank: false | |
| }); | |
| var thinpoolField = Ext.createWidget(me.create ? 'textfield' : 'displayfield', { | |
| name: 'thinpool', | |
| hidden: !!me.create, | |
| disabled: !!me.create, | |
| value: '', | |
| fieldLabel: gettext('Thin Pool'), | |
| allowBlank: false | |
| }); | |
| if (me.create) { | |
| 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.create) { | |
| 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 = [ | |
| { | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.LvmThinEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.LvmThinInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: PVE.Utils.format_storage_type('lvmthin'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| 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.storage.RBDInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'rbd'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'pool', | |
| value: 'rbd', | |
| fieldLabel: gettext('Pool'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'monhost', | |
| value: '', | |
| fieldLabel: gettext('Monitor Host'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'username', | |
| value: 'admin', | |
| fieldLabel: gettext('User name'), | |
| allowBlank: true | |
| } | |
| ]; | |
| // here value is an array, | |
| // while before it was a string | |
| /*jslint confusion: true*/ | |
| me.column2 = [ | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| }, | |
| { | |
| xtype: 'pveContentTypeSelector', | |
| cts: ['images', 'rootdir'], | |
| fieldLabel: gettext('Content'), | |
| name: 'content', | |
| value: ['images'], | |
| multiSelect: true, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'krbd', | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('KRBD') | |
| } | |
| ]; | |
| /*jslint confusion: false*/ | |
| if (me.create || me.storageId !== 'local') { | |
| me.column2.unshift({ | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.RBDEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.RBDInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: PVE.Utils.format_storage_type('rbd'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| 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.storage.SheepdogInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'sheepdog'; | |
| values.content = 'images'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'portal', | |
| value: '127.0.0.1:7000', | |
| fieldLabel: gettext('Gateway'), | |
| allowBlank: false | |
| } | |
| ]; | |
| me.column2 = [ | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| } | |
| ]; | |
| if (me.create || me.storageId !== 'local') { | |
| me.column2.unshift({ | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.SheepdogEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.SheepdogInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: PVE.Utils.format_storage_type('sheepdog'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| me.load({ | |
| success: function(response, options) { | |
| var values = response.result.data; | |
| if (values.nodes) { | |
| values.nodes = values.nodes.split(','); | |
| } | |
| values.enable = values.disable ? 0 : 1; | |
| ipanel.setValues(values); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.storage.ZFSInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'zfs'; | |
| values.content = 'images'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'portal', | |
| value: '', | |
| fieldLabel: gettext('Portal'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'pool', | |
| value: '', | |
| fieldLabel: gettext('Pool'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'blocksize', | |
| value: '4k', | |
| fieldLabel: gettext('Block Size'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'target', | |
| value: '', | |
| fieldLabel: gettext('Target'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'comstar_tg', | |
| value: '', | |
| fieldLabel: gettext('Target group'), | |
| allowBlank: true | |
| } | |
| ]; | |
| me.column2 = [ | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| }, | |
| { | |
| xtype: me.create ? 'pveiScsiProviderSelector' : 'displayfield', | |
| name: 'iscsiprovider', | |
| value: 'comstar', | |
| fieldLabel: gettext('iSCSI Provider'), | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'sparse', | |
| checked: false, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Thin provision') | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'nowritecache', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Write cache') | |
| }, | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'comstar_hg', | |
| value: '', | |
| fieldLabel: gettext('Host group'), | |
| allowBlank: true | |
| } | |
| ]; | |
| if (me.create || me.storageId !== 'local') { | |
| me.column2.unshift({ | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.ZFSEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.ZFSInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: 'ZFS Storage', | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| me.load({ | |
| success: function(response, options) { | |
| var values = response.result.data; | |
| if (values.nodes) { | |
| values.nodes = values.nodes.split(','); | |
| } | |
| values.enable = values.disable ? 0 : 1; | |
| ipanel.setValues(values); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| 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: 'pve', | |
| url: '/api2/json/nodes/' + me.nodename + '/scan/zfs' | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.ZFSPoolInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'zfspool'; | |
| } else { | |
| delete values.storage; | |
| } | |
| values.disable = values.enable ? 0 : 1; | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'storage', | |
| value: me.storageId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| } | |
| ]; | |
| if (me.create) { | |
| 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: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Enable') | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'sparse', | |
| checked: false, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Thin provision') | |
| } | |
| ]; | |
| if (me.create || me.storageId !== 'local') { | |
| me.column2.unshift({ | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| emptyText: gettext('All') + ' (' + | |
| gettext('No restrictions') +')', | |
| multiSelect: true, | |
| autoSelect: false | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.storage.ZFSPoolEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.storageId; | |
| if (me.create) { | |
| me.url = '/api2/extjs/storage'; | |
| me.method = 'POST'; | |
| } else { | |
| me.url = '/api2/extjs/storage/' + me.storageId; | |
| me.method = 'PUT'; | |
| } | |
| var ipanel = Ext.create('PVE.storage.ZFSPoolInputPanel', { | |
| create: me.create, | |
| storageId: me.storageId | |
| }); | |
| Ext.apply(me, { | |
| subject: PVE.Utils.format_storage_type('zfspool'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| 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.ha.StatusView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveHAStatusView'], | |
| sortPriority: { | |
| quorum: 1, | |
| master: 2, | |
| lrm: 3, | |
| service: 4 | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.rstore = Ext.create('PVE.data.ObjectStore', { | |
| interval: me.interval, | |
| model: 'pve-ha-status', | |
| storeid: 'pve-store-' + (++Ext.idSeed), | |
| groupField: 'type', | |
| proxy: { | |
| type: 'pve', | |
| url: '/api2/json/cluster/ha/status/current' | |
| } | |
| }); | |
| PVE.Utils.monStoreErrors(me, me.rstore); | |
| var store = Ext.create('PVE.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; | |
| } | |
| }] | |
| }); | |
| 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('hide', me.rstore.stopUpdate); | |
| me.on('destroy', me.rstore.stopUpdate); | |
| } | |
| }, function() { | |
| Ext.define('pve-ha-status', { | |
| extend: 'Ext.data.Model', | |
| fields: [ | |
| 'id', 'type', 'node', 'status', 'sid' | |
| ], | |
| idProperty: 'id' | |
| }); | |
| }); | |
| Ext.define('PVE.ha.GroupSelector', { | |
| extend: 'PVE.form.ComboGrid', | |
| alias: ['widget.pveHAGroupSelector'], | |
| autoSelect: false, | |
| valueField: 'group', | |
| displayField: 'group', | |
| listConfig: { | |
| columns: [ | |
| { | |
| header: gettext('Group'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'group' | |
| }, | |
| { | |
| header: gettext('Nodes'), | |
| width: 100, | |
| sortable: false, | |
| dataIndex: 'nodes' | |
| }, | |
| { | |
| header: gettext('Comment'), | |
| flex: 1, | |
| dataIndex: 'comment', | |
| renderer: Ext.String.htmlEncode | |
| } | |
| ] | |
| }, | |
| store: { | |
| model: 'pve-ha-groups', | |
| sorters: { | |
| property: 'group', | |
| 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', 'restricted', 'digest', 'nofailback', | |
| 'nodes', 'comment' | |
| ], | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/cluster/ha/groups" | |
| }, | |
| idProperty: 'group' | |
| }); | |
| }); | |
| Ext.define('PVE.ha.VMResourceInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| vmid: undefined, | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.sid = values.vmid; | |
| } | |
| if (values.group === '') { | |
| if (!me.create) { | |
| values['delete'] = values['delete'] ? ',group' : 'group'; | |
| } | |
| delete values.group; | |
| } | |
| delete values.vmid; | |
| if (values.enable) { | |
| values.state = 'enabled'; | |
| } else { | |
| values.state = 'disabled'; | |
| } | |
| delete values.enable; | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.vmid ? 'displayfield' : 'pveVMIDSelector', | |
| name: 'vmid', | |
| fieldLabel: 'VM ID', | |
| value: me.vmid, | |
| loadNextFreeVMID: false, | |
| validateExists: true | |
| } | |
| ]; | |
| me.column2 = [ | |
| { | |
| xtype: 'pveHAGroupSelector', | |
| name: 'group', | |
| value: '', | |
| fieldLabel: gettext('Group') | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'enable', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('enable') | |
| } | |
| ]; | |
| me.columnB = [ | |
| { | |
| xtype: 'textfield', | |
| name: 'comment', | |
| fieldLabel: gettext('Comment') | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.ha.VMResourceEdit', { | |
| extend: 'PVE.window.Edit', | |
| vmid: undefined, | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.vmid; | |
| if (me.create) { | |
| 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', { | |
| create: me.create, | |
| vmid: me.vmid | |
| }); | |
| Ext.apply(me, { | |
| subject: gettext('VM Resource'), | |
| isAdd: true, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| me.load({ | |
| success: function(response, options) { | |
| var values = response.result.data; | |
| values.enable = true; | |
| if (values.state === 'disabled') { | |
| values.enable = false; | |
| } | |
| 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'], | |
| initComponent : function() { | |
| var me = this; | |
| var caps = Ext.state.Manager.get('GuiCap'); | |
| var store = new Ext.data.Store({ | |
| model: 'pve-ha-resources', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/cluster/ha/resources" | |
| }, | |
| sorters: { | |
| property: 'sid', | |
| order: 'DESC' | |
| } | |
| }); | |
| var reload = function() { | |
| store.load(); | |
| }; | |
| var render_error = function(dataIndex, value, metaData, record) { | |
| var errors = record.data.errors; | |
| if (errors) { | |
| var msg = errors[dataIndex]; | |
| if (msg) { | |
| metaData.tdCls = 'x-form-invalid-field'; | |
| var html = '<p>' + Ext.htmlEncode(msg) + '</p>'; | |
| metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + | |
| html.replace(/\"/g,'"') + '"'; | |
| } | |
| } | |
| return value; | |
| }; | |
| var sm = Ext.create('Ext.selection.RowModel', {}); | |
| var run_editor = function() { | |
| var rec = sm.getSelection()[0]; | |
| var sid = rec.data.sid; | |
| var regex = /^(\S+):(\S+)$/; | |
| var res = regex.exec(sid); | |
| if (res[1] !== 'vm' && res[1] !== 'ct') { | |
| return; | |
| } | |
| var vmid = res[2]; | |
| var win = Ext.create('PVE.ha.VMResourceEdit',{ | |
| vmid: vmid | |
| }); | |
| win.on('destroy', reload); | |
| win.show(); | |
| }; | |
| var remove_btn = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: function(btn, event, rec) { | |
| var sid = rec.data.sid; | |
| PVE.Utils.API2Request({ | |
| url: '/cluster/ha/resources/' + sid, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| var edit_btn = new PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| 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, | |
| renderer: function(v) { | |
| return v || 'enabled'; | |
| }, | |
| dataIndex: 'state' | |
| }, | |
| { | |
| 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: { | |
| activate: reload, | |
| 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' | |
| ], | |
| idProperty: 'sid' | |
| }); | |
| }); | |
| Ext.define('PVE.ha.GroupInputPanel', { | |
| extend: 'PVE.panel.InputPanel', | |
| groupId: undefined, | |
| onGetValues: function(values) { | |
| var me = this; | |
| if (me.create) { | |
| values.type = 'group'; | |
| } | |
| return values; | |
| }, | |
| initComponent : function() { | |
| var me = this; | |
| me.column1 = [ | |
| { | |
| xtype: me.create ? 'textfield' : 'displayfield', | |
| name: 'group', | |
| value: me.groupId || '', | |
| fieldLabel: 'ID', | |
| vtype: 'StorageId', | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'pveNodeSelector', | |
| name: 'nodes', | |
| fieldLabel: gettext('Nodes'), | |
| allowBlank: false, | |
| multiSelect: true, | |
| autoSelect: false | |
| } | |
| ]; | |
| me.column2 = [ | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'restricted', | |
| uncheckedValue: 0, | |
| fieldLabel: 'restricted' | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| name: 'nofailback', | |
| uncheckedValue: 0, | |
| fieldLabel: 'nofailback' | |
| } | |
| ]; | |
| me.columnB = [ | |
| { | |
| xtype: 'textfield', | |
| name: 'comment', | |
| fieldLabel: gettext('Comment') | |
| } | |
| ]; | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.ha.GroupEdit', { | |
| extend: 'PVE.window.Edit', | |
| groupId: undefined, | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.groupId; | |
| if (me.create) { | |
| 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', { | |
| create: me.create, | |
| groupId: me.groupId | |
| }); | |
| Ext.apply(me, { | |
| subject: gettext('HA Group'), | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| me.load({ | |
| success: function(response, options) { | |
| var values = response.result.data; | |
| if (values.nodes) { | |
| values.nodes = values.nodes.split(','); | |
| } | |
| ipanel.setValues(values); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.ha.GroupsView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveHAGroupsView'], | |
| 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 = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: function(btn, event, rec) { | |
| var group = rec.data.group; | |
| PVE.Utils.API2Request({ | |
| url: '/cluster/ha/groups/' + group, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| var edit_btn = new PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| 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: PVE.Utils.format_boolean, | |
| dataIndex: 'restricted' | |
| }, | |
| { | |
| header: 'nofailback', | |
| width: 100, | |
| sortable: true, | |
| renderer: PVE.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'], | |
| 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.panel.HA', { | |
| extend: 'PVE.panel.SubConfig', | |
| alias: 'widget.pveHAPanel', | |
| configPrefix: 'ha', | |
| initComponent: function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| var items = [ | |
| { | |
| title: gettext('Status'), | |
| xtype: 'pveHAStatusView', | |
| itemId: 'status' | |
| }, | |
| { | |
| title: gettext('Resources'), | |
| xtype: 'pveHAResourcesView', | |
| itemId: 'resources' | |
| }, | |
| { | |
| title: gettext('Groups'), | |
| xtype: 'pveHAGroupsView', | |
| itemId: 'groups' | |
| }, | |
| { | |
| title: gettext('Fencing'), | |
| xtype: 'pveFencingView', | |
| itemId: 'fencing' | |
| } | |
| ]; | |
| Ext.apply(me, { | |
| defaults: { | |
| border: false, | |
| pveSelNode: me.pveSelNode | |
| }, | |
| plugins: [{ | |
| ptype: 'lazyitems', | |
| items: items | |
| }] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.NodeView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveDcNodeView'], | |
| initComponent : function() { | |
| var me = this; | |
| var rstore = Ext.create('PVE.data.UpdateStore', { | |
| interval: 3000, | |
| storeid: 'pve-dc-nodes', | |
| model: 'pve-dc-nodes', | |
| proxy: { | |
| type: 'pve', | |
| url: "/api2/json/cluster/status" | |
| }, | |
| filters: { | |
| property: 'type', | |
| value : 'node' | |
| } | |
| }); | |
| var store = Ext.create('PVE.data.DiffStore', { rstore: rstore }); | |
| var noClusterText = gettext("Standalone node - no cluster defined"); | |
| var status = Ext.create('Ext.Component', { | |
| padding: 2, | |
| html: ' ', | |
| dock: 'bottom' | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| stateful: false, | |
| bbar: [ status ], | |
| columns: [ | |
| { | |
| header: gettext('Name'), | |
| width: 200, | |
| sortable: true, | |
| dataIndex: 'name' | |
| }, | |
| { | |
| header: 'ID', | |
| width: 50, | |
| sortable: true, | |
| dataIndex: 'nodeid' | |
| }, | |
| { | |
| header: gettext('Online'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'online', | |
| renderer: PVE.Utils.format_boolean | |
| }, | |
| { | |
| header: gettext('Support'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'level', | |
| renderer: PVE.Utils.render_support_level | |
| }, | |
| { | |
| header: gettext('Server Address'), | |
| flex: 1, | |
| sortable: true, | |
| dataIndex: 'ip' | |
| } | |
| ], | |
| listeners: { | |
| show: rstore.startUpdate, | |
| hide: rstore.stopUpdate, | |
| destroy: rstore.stopUpdate | |
| } | |
| }); | |
| me.callParent(); | |
| me.mon(rstore, 'load', function(s, records, success) { | |
| if (!success) { | |
| return; | |
| } | |
| var cluster_rec = rstore.getById('cluster'); | |
| if (!cluster_rec) { | |
| status.update(noClusterText); | |
| return; | |
| } | |
| var cluster_data = cluster_rec.getData(); | |
| if (!cluster_data) { | |
| status.update(noClusterText); | |
| return; | |
| } | |
| var text = gettext("Cluster") + ": " + cluster_data.name + ", " + | |
| gettext("Quorate") + ": " + PVE.Utils.format_boolean(cluster_data.quorate); | |
| status.update(text); | |
| }); | |
| } | |
| }, function() { | |
| Ext.define('pve-dc-nodes', { | |
| extend: 'Ext.data.Model', | |
| fields: [ 'id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'], | |
| idProperty: 'id' | |
| }); | |
| }); | |
| Ext.define('PVE.dc.Summary', { | |
| extend: 'Ext.panel.Panel', | |
| alias: ['widget.pveDcSummary'], | |
| initComponent: function() { | |
| var me = this; | |
| var nodegrid = Ext.create('PVE.dc.NodeView', { | |
| title: gettext('Nodes'), | |
| border: false, | |
| region: 'center', | |
| flex: 3 | |
| }); | |
| Ext.apply(me, { | |
| layout: 'border', | |
| items: [ nodegrid ], | |
| listeners: { | |
| activate: function() { | |
| nodegrid.fireEvent('show', nodegrid); | |
| }, | |
| hide: function() { | |
| nodegrid.fireEvent('hide', nodegrid); | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.HttpProxyEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| Ext.applyIf(me, { | |
| subject: gettext('HTTP proxy'), | |
| items: { | |
| xtype: 'pvetextfield', | |
| name: 'http_proxy', | |
| vtype: 'HttpProxy', | |
| emptyText: gettext('Do not use any proxy'), | |
| deleteEmpty: true, | |
| value: '', | |
| fieldLabel: gettext('HTTP proxy') | |
| } | |
| }); | |
| me.callParent(); | |
| me.load(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.KeyboardEdit', { | |
| extend: 'PVE.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.dc.ConsoleViewerEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| var data = []; | |
| Ext.Array.each(['__default__','vv', 'html5'], function(value) { | |
| data.push([value, PVE.Utils.render_console_viewer(value)]); | |
| }); | |
| Ext.applyIf(me, { | |
| subject: gettext('Console Viewer'), | |
| items: { | |
| xtype: 'pveKVComboBox', | |
| name: 'console', | |
| value: '__default__', | |
| fieldLabel: gettext('Console Viewer'), | |
| comboItems: data | |
| } | |
| }); | |
| me.callParent(); | |
| me.load(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.EmailFromEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| Ext.applyIf(me, { | |
| subject: gettext('Email from address'), | |
| items: { | |
| xtype: 'pvetextfield', | |
| name: 'email_from', | |
| vtype: 'pveMail', | |
| emptyText: 'root@$hostname', | |
| deleteEmpty: true, | |
| value: '', | |
| fieldLabel: gettext('Email from address') | |
| } | |
| }); | |
| me.callParent(); | |
| me.load(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.OptionView', { | |
| extend: 'PVE.grid.ObjectGrid', | |
| alias: ['widget.pveDcOptionView'], | |
| noProxyText: gettext('Do not use any proxy'), | |
| initComponent : function() { | |
| var me = this; | |
| var reload = function() { | |
| me.rstore.load(); | |
| }; | |
| var rows = { | |
| keyboard: { | |
| header: gettext('Keyboard Layout'), | |
| editor: 'PVE.dc.KeyboardEdit', | |
| renderer: PVE.Utils.render_kvm_language, | |
| required: true | |
| }, | |
| http_proxy: { | |
| header: gettext('HTTP proxy'), | |
| editor: 'PVE.dc.HttpProxyEdit', | |
| required: true, | |
| renderer: function(value) { | |
| if (!value) { | |
| return me.noProxyText; | |
| } | |
| return value; | |
| } | |
| }, | |
| console: { | |
| header: gettext('Console Viewer'), | |
| editor: 'PVE.dc.ConsoleViewerEdit', | |
| required: true, | |
| renderer: PVE.Utils.render_console_viewer | |
| }, | |
| email_from: { | |
| header: gettext('Email from address'), | |
| editor: 'PVE.dc.EmailFromEdit', | |
| required: true, | |
| renderer: function(value) { | |
| if (!value) { | |
| return 'root@$hostname'; | |
| } | |
| return value; | |
| } | |
| } | |
| }; | |
| 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 = Ext.create(rowdef.editor, { | |
| url: "/api2/extjs/cluster/options", | |
| confid: rec.data.key | |
| }); | |
| win.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var edit_btn = new PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| Ext.apply(me, { | |
| url: "/api2/json/cluster/options", | |
| interval: 1000, | |
| selModel: sm, | |
| tbar: [ edit_btn ], | |
| rows: rows, | |
| listeners: { | |
| itemdblclick: run_editor, | |
| activate: reload | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.StorageView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveStorageView'], | |
| initComponent : function() { | |
| var me = this; | |
| var store = new Ext.data.Store({ | |
| model: 'pve-storage', | |
| proxy: { | |
| type: 'pve', | |
| 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; | |
| var editor; | |
| if (type === 'dir') { | |
| editor = 'PVE.storage.DirEdit'; | |
| } else if (type === 'nfs') { | |
| editor = 'PVE.storage.NFSEdit'; | |
| } else if (type === 'glusterfs') { | |
| editor = 'PVE.storage.GlusterFsEdit'; | |
| } else if (type === 'lvm') { | |
| editor = 'PVE.storage.LVMEdit'; | |
| } else if (type === 'lvmthin') { | |
| editor = 'PVE.storage.LvmThinEdit'; | |
| } else if (type === 'iscsi') { | |
| editor = 'PVE.storage.IScsiEdit'; | |
| } else if (type === 'rbd') { | |
| editor = 'PVE.storage.RBDEdit'; | |
| } else if (type === 'sheepdog') { | |
| editor = 'PVE.storage.SheepdogEdit'; | |
| } else if (type === 'zfs') { | |
| editor = 'PVE.storage.ZFSEdit'; | |
| } else if (type === 'zfspool') { | |
| editor = 'PVE.storage.ZFSPoolEdit'; | |
| } else { | |
| return; | |
| } | |
| var win = Ext.create(editor, { | |
| storageId: rec.data.storage | |
| }); | |
| win.show(); | |
| win.on('destroy', reload); | |
| }; | |
| var edit_btn = new PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| var remove_btn = new PVE.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.storage + "'"); | |
| }, | |
| handler: function(btn, event, rec) { | |
| PVE.Utils.API2Request({ | |
| url: '/storage/' + rec.data.storage, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| viewConfig: { | |
| trackOver: false | |
| }, | |
| tbar: [ | |
| { | |
| text: gettext('Add'), | |
| menu: new Ext.menu.Menu({ | |
| items: [ | |
| { | |
| text: PVE.Utils.format_storage_type('dir'), | |
| iconCls: 'fa fa-fw fa-folder', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.DirEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.format_storage_type('lvm'), | |
| iconCls: 'fa fa-fw fa-folder', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.LVMEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.format_storage_type('lvmthin'), | |
| iconCls: 'fa fa-fw fa-folder', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.LvmThinEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.format_storage_type('nfs'), | |
| iconCls: 'fa fa-fw fa-building', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.NFSEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.format_storage_type('iscsi'), | |
| iconCls: 'fa fa-fw fa-building', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.IScsiEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.format_storage_type('glusterfs'), | |
| iconCls: 'fa fa-fw fa-building', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.GlusterFsEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.format_storage_type('rbd'), | |
| iconCls: 'fa fa-fw fa-building', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.RBDEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.format_storage_type('zfs'), | |
| iconCls: 'fa fa-fw fa-building', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.ZFSEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| { | |
| text: PVE.Utils.format_storage_type('zfspool'), | |
| iconCls: 'fa fa-fw fa-folder', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.ZFSPoolEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| } | |
| /* the following type are conidered unstable | |
| * so we do not enable that on the GUI for now | |
| { | |
| text: PVE.Utils.format_storage_type('sheepdog'), | |
| iconCls: 'fa fa-fw fa-building', | |
| handler: function() { | |
| var win = Ext.create('PVE.storage.SheepdogEdit', {}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| } | |
| */ | |
| ] | |
| }) | |
| }, | |
| remove_btn, | |
| edit_btn | |
| ], | |
| columns: [ | |
| { | |
| header: 'ID', | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'storage' | |
| }, | |
| { | |
| header: gettext('Type'), | |
| width: 60, | |
| sortable: true, | |
| dataIndex: 'type', | |
| renderer: PVE.Utils.format_storage_type | |
| }, | |
| { | |
| header: gettext('Content'), | |
| width: 150, | |
| sortable: true, | |
| dataIndex: 'content', | |
| renderer: PVE.Utils.format_content_types | |
| }, | |
| { | |
| header: gettext('Path') + '/' + gettext('Target'), | |
| flex: 1, | |
| sortable: true, | |
| dataIndex: 'path', | |
| renderer: function(value, metaData, record) { | |
| if (record.data.target) { | |
| return record.data.target; | |
| } | |
| return value; | |
| } | |
| }, | |
| { | |
| header: gettext('Shared'), | |
| width: 80, | |
| sortable: true, | |
| dataIndex: 'shared', | |
| renderer: PVE.Utils.format_boolean | |
| }, | |
| { | |
| header: gettext('Enabled'), | |
| width: 80, | |
| sortable: true, | |
| dataIndex: 'disable', | |
| renderer: PVE.Utils.format_neg_boolean | |
| } | |
| ], | |
| 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' | |
| }); | |
| }); | |
| Ext.define('PVE.dc.UserEdit', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveDcUserEdit'], | |
| isAdd: true, | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.userid; | |
| var url; | |
| var method; | |
| var realm; | |
| if (me.create) { | |
| url = '/api2/extjs/access/users'; | |
| method = 'POST'; | |
| } else { | |
| url = '/api2/extjs/access/users/' + me.userid; | |
| method = 'PUT'; | |
| } | |
| var verifypw; | |
| var pwfield; | |
| var validate_pw = function() { | |
| if (verifypw.getValue() !== pwfield.getValue()) { | |
| return gettext("Passwords does 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.create ? 'textfield' : 'displayfield', | |
| name: 'userid', | |
| fieldLabel: gettext('User name'), | |
| value: me.userid, | |
| allowBlank: false, | |
| submitValue: me.create ? 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: 'pvecheckbox', | |
| 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: 'pveMail' | |
| } | |
| ]; | |
| var columnB = [ | |
| { | |
| xtype: 'textfield', | |
| name: 'comment', | |
| fieldLabel: gettext('Comment') | |
| }, | |
| { | |
| xtype: 'textfield', | |
| name: 'keys', | |
| fieldLabel: gettext('Key IDs') | |
| } | |
| ]; | |
| if (me.create) { | |
| 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('PVE.panel.InputPanel', { | |
| column1: column1, | |
| column2: column2, | |
| columnB: columnB, | |
| 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.create) { | |
| 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); | |
| } | |
| }); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.window.PasswordEdit', { | |
| extend: 'PVE.window.Edit', | |
| initComponent : function() { | |
| var me = this; | |
| if (!me.userid) { | |
| throw "no userid specified"; | |
| } | |
| var verifypw; | |
| var pwfield; | |
| var validate_pw = function() { | |
| if (verifypw.getValue() !== pwfield.getValue()) { | |
| return gettext("Passwords does not match"); | |
| } | |
| return true; | |
| }; | |
| verifypw = Ext.createWidget('textfield', { | |
| inputType: 'password', | |
| fieldLabel: gettext('Confirm password'), | |
| name: 'verifypassword', | |
| submitValue: false, | |
| validator: validate_pw | |
| }); | |
| pwfield = Ext.createWidget('textfield', { | |
| inputType: 'password', | |
| fieldLabel: gettext('Password'), | |
| minLength: 5, | |
| name: 'password', | |
| validator: validate_pw | |
| }); | |
| Ext.apply(me, { | |
| subject: gettext('Password'), | |
| url: '/api2/extjs/access/password', | |
| items: [ | |
| pwfield, verifypw, | |
| { | |
| xtype: 'hiddenfield', | |
| name: 'userid', | |
| value: me.userid | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.UserView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveUserView'], | |
| 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 = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| disabled: true, | |
| selModel: sm, | |
| enableFn: function(rec) { | |
| if (!caps.access['User.Modify']) { | |
| return false; | |
| } | |
| return rec.data.userid !== 'root@pam'; | |
| }, | |
| confirmMsg: function (rec) { | |
| return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), | |
| "'" + rec.data.userid + "'"); | |
| }, | |
| handler: function(btn, event, rec) { | |
| var userid = rec.data.userid; | |
| PVE.Utils.API2Request({ | |
| url: '/access/users/' + userid, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| 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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| enableFn: function(rec) { | |
| return !!caps.access['User.Modify']; | |
| }, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| var pwchange_btn = new PVE.button.Button({ | |
| text: gettext('Password'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: function(btn, event, rec) { | |
| var win = Ext.create('PVE.window.PasswordEdit',{ | |
| userid: rec.data.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 | |
| ]; | |
| var render_full_name = function(firstname, metaData, record) { | |
| var first = firstname || ''; | |
| var last = record.data.lastname || ''; | |
| return first + " " + last; | |
| }; | |
| 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, | |
| stateful: false, | |
| 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: PVE.Utils.format_boolean, | |
| dataIndex: 'enable' | |
| }, | |
| { | |
| header: gettext('Expire'), | |
| width: 80, | |
| sortable: true, | |
| renderer: PVE.Utils.format_expire, | |
| dataIndex: 'expire' | |
| }, | |
| { | |
| header: gettext('Name'), | |
| width: 150, | |
| sortable: true, | |
| renderer: render_full_name, | |
| dataIndex: 'firstname' | |
| }, | |
| { | |
| 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'], | |
| 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 = new PVE.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.poolid + "'"); | |
| }, | |
| handler: function(btn, event, rec) { | |
| PVE.Utils.API2Request({ | |
| url: '/pools/' + rec.data.poolid, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| 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 PVE.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 | |
| ]; | |
| PVE.Utils.monStoreErrors(me, store); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| 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: 'PVE.window.Edit', | |
| alias: ['widget.pveDcPoolEdit'], | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.poolid; | |
| var url; | |
| var method; | |
| if (me.create) { | |
| 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.create ? 'pvetextfield' : 'displayfield', | |
| fieldLabel: gettext('Name'), | |
| name: 'poolid', | |
| value: me.poolid, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Comment'), | |
| name: 'comment', | |
| allowBlank: true | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| me.load(); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.dc.GroupView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveGroupView'], | |
| 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 = new PVE.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.groupid + "'"); | |
| }, | |
| handler: function(btn, event, rec) { | |
| PVE.Utils.API2Request({ | |
| url: '/access/groups/' + rec.data.groupid, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| 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 PVE.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 | |
| ]; | |
| PVE.Utils.monStoreErrors(me, store); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| 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: 'PVE.window.Edit', | |
| alias: ['widget.pveDcGroupEdit'], | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.groupid; | |
| var url; | |
| var method; | |
| if (me.create) { | |
| 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.create ? 'pvetextfield' : 'displayfield', | |
| fieldLabel: gettext('Name'), | |
| name: 'groupid', | |
| value: me.groupid, | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Comment'), | |
| name: 'comment', | |
| allowBlank: true | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| if (!me.create) { | |
| me.load(); | |
| } | |
| } | |
| }); | |
| Ext.define('PVE.dc.RoleView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveRoleView'], | |
| 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, ' '); | |
| }; | |
| PVE.Utils.monStoreErrors(me, store); | |
| Ext.apply(me, { | |
| store: store, | |
| stateful: false, | |
| viewConfig: { | |
| trackOver: false | |
| }, | |
| columns: [ | |
| { | |
| 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(); | |
| } | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.ACLAdd', { | |
| extend: 'PVE.window.Edit', | |
| alias: ['widget.pveACLAdd'], | |
| url: '/access/acl', | |
| method: 'PUT', | |
| isAdd: true, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| me.create = true; | |
| var items = [ | |
| { | |
| xtype: me.path ? 'hiddenfield' : 'textfield', | |
| 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: 'pvecheckbox', | |
| name: 'propagate', | |
| checked: true, | |
| uncheckedValue: 0, | |
| fieldLabel: gettext('Propagate') | |
| }); | |
| } | |
| var ipanel = Ext.create('PVE.panel.InputPanel', { | |
| items: items | |
| }); | |
| Ext.apply(me, { | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.ACLView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveACLView'], | |
| // use fixed path | |
| path: undefined, | |
| initComponent : function() { | |
| var me = this; | |
| var store = Ext.create('Ext.data.Store',{ | |
| model: 'pve-acl', | |
| proxy: { | |
| type: 'pve', | |
| 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 PVE.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'; | |
| } | |
| PVE.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); | |
| } | |
| }); | |
| } | |
| }); | |
| PVE.Utils.monStoreErrors(me, store); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| 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'], | |
| 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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| var remove_btn = new PVE.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.realm + "'"); | |
| }, | |
| enableFn: function(rec) { | |
| return !(rec.data.type === 'pve' || rec.data.type === 'pam'); | |
| }, | |
| handler: function(btn, event, rec) { | |
| var realm = rec.data.realm; | |
| PVE.Utils.API2Request({ | |
| url: '/access/domains/' + realm, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| 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, | |
| stateful: false, | |
| 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: 'PVE.window.Edit', | |
| alias: ['widget.pveDcAuthEdit'], | |
| isAdd: true, | |
| initComponent : function() { | |
| var me = this; | |
| me.create = !me.realm; | |
| var url; | |
| var method; | |
| var serverlist; | |
| if (me.create) { | |
| url = '/api2/extjs/access/domains'; | |
| method = 'POST'; | |
| } else { | |
| url = '/api2/extjs/access/domains/' + me.realm; | |
| method = 'PUT'; | |
| } | |
| var column1 = [ | |
| { | |
| xtype: me.create ? '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.create) { | |
| throw 'unknown auth type'; | |
| } | |
| me.subject = 'Proxmox VE authentication server'; | |
| } else if (me.authType === 'pam') { | |
| if (me.create) { | |
| throw 'unknown auth type'; | |
| } | |
| me.subject = 'linux PAM'; | |
| } else { | |
| throw 'unknown auth type '; | |
| } | |
| column1.push({ | |
| xtype: 'pvecheckbox', | |
| 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: 'pvetextfield', | |
| fieldLabel: gettext('Fallback Server'), | |
| deleteEmpty: !me.create, | |
| name: 'server2' | |
| }, | |
| { | |
| xtype: 'numberfield', | |
| name: 'port', | |
| fieldLabel: gettext('Port'), | |
| minValue: 1, | |
| maxValue: 65535, | |
| emptyText: gettext('Default'), | |
| submitEmptyText: false | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| fieldLabel: 'SSL', | |
| name: 'secure', | |
| uncheckedValue: 0 | |
| } | |
| ); | |
| } | |
| // Two Factor Auth settings | |
| column2.push({ | |
| xtype: 'pveKVComboBox', | |
| name: 'tfa', | |
| deleteEmpty: !me.create, | |
| value: '', | |
| fieldLabel: gettext('TFA'), | |
| comboItems: [ ['__default__', PVE.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: 'numberfield', | |
| name: 'oath_step', | |
| value: '', | |
| minValue: 10, | |
| step: 1, | |
| allowDecimals: false, | |
| allowBlank: true, | |
| emptyText: PVE.Utils.defaultText + ' (30)', | |
| submitEmptyText: false, | |
| hidden: true, | |
| fieldLabel: 'OATH time step' | |
| }); | |
| column2.push({ | |
| xtype: 'numberfield', | |
| name: 'oath_digits', | |
| value: '', | |
| minValue: 6, | |
| maxValue: 8, | |
| step: 1, | |
| allowDecimals: false, | |
| allowBlank: true, | |
| emptyText: PVE.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('PVE.panel.InputPanel', { | |
| column1: column1, | |
| column2: column2, | |
| columnB: [{ | |
| xtype: 'textfield', | |
| name: 'comment', | |
| fieldLabel: gettext('Comment') | |
| }], | |
| onGetValues: function(values) { | |
| if (!values.port) { | |
| if (!me.create) { | |
| PVE.Utils.assemble_field_data(values, { 'delete': 'port' }); | |
| } | |
| delete values.port; | |
| } | |
| if (me.create) { | |
| 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.create) { | |
| 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: 'PVE.window.Edit', | |
| alias: ['widget.pveDcBackupEdit'], | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| me.create = !me.jobid; | |
| var url; | |
| var method; | |
| if (me.create) { | |
| 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' | |
| }); | |
| var selModeField = Ext.create('PVE.form.KVComboBox', { | |
| xtype: 'pveKVComboBox', | |
| comboItems: [ | |
| ['include', gettext('Include selected VMs')], | |
| ['all', gettext('All')], | |
| ['exclude', gettext('Exclude selected VMs')] | |
| ], | |
| fieldLabel: gettext('Selection mode'), | |
| name: 'selMode', | |
| value: '' | |
| }); | |
| var insideUpdate = false; | |
| var sm = Ext.create('Ext.selection.CheckboxModel', { | |
| mode: 'SIMPLE', | |
| listeners: { | |
| selectionchange: function(model, selected) { | |
| if (!insideUpdate) { // avoid endless loop | |
| var sel = []; | |
| Ext.Array.each(selected, function(record) { | |
| sel.push(record.data.vmid); | |
| }); | |
| insideUpdate = true; | |
| vmidField.setValue(sel); | |
| insideUpdate = false; | |
| } | |
| } | |
| } | |
| }); | |
| 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 PVE.Utils.runningText; | |
| } else { | |
| return PVE.Utils.stoppedText; | |
| } | |
| } | |
| }, | |
| { | |
| header: gettext('Name'), | |
| dataIndex: 'name', | |
| flex: 1 | |
| }, | |
| { | |
| header: gettext('Type'), | |
| dataIndex: 'type' | |
| } | |
| ] | |
| }); | |
| var nodesel = Ext.create('PVE.form.NodeSelector', { | |
| name: 'node', | |
| fieldLabel: gettext('Node'), | |
| allowBlank: true, | |
| editable: true, | |
| autoSelect: false, | |
| emptyText: '-- ' + gettext('All') + ' --', | |
| listeners: { | |
| change: function(f, value) { | |
| storagesel.setNodename(value || 'localhost'); | |
| var mode = selModeField.getValue(); | |
| store.clearFilter(); | |
| store.filterBy(function(rec) { | |
| return (!value || rec.get('node') === value); | |
| }); | |
| if (mode === 'all') { | |
| sm.selectAll(true); | |
| } | |
| } | |
| } | |
| }); | |
| var column1 = [ | |
| nodesel, | |
| storagesel, | |
| { | |
| xtype: 'pveDayOfWeekSelector', | |
| name: 'dow', | |
| fieldLabel: gettext('Day of week'), | |
| multiSelect: true, | |
| value: ['sat'], | |
| allowBlank: false | |
| }, | |
| { | |
| xtype: 'timefield', | |
| fieldLabel: gettext('Start Time'), | |
| name: 'starttime', | |
| format: 'H:i', | |
| value: '00:00', | |
| allowBlank: false | |
| }, | |
| selModeField | |
| ]; | |
| var column2 = [ | |
| { | |
| xtype: 'textfield', | |
| fieldLabel: gettext('Send email to'), | |
| name: 'mailto' | |
| }, | |
| { | |
| xtype: 'pveEmailNotificationSelector', | |
| fieldLabel: gettext('Email notification'), | |
| name: 'mailnotification', | |
| deleteEmpty: me.create ? false : true, | |
| value: me.create ? 'always' : '' | |
| }, | |
| { | |
| xtype: 'pveCompressionSelector', | |
| fieldLabel: gettext('Compression'), | |
| name: 'compress', | |
| deleteEmpty: me.create ? false : true, | |
| value: 'lzo' | |
| }, | |
| { | |
| xtype: 'pveBackupModeSelector', | |
| fieldLabel: gettext('Mode'), | |
| value: 'snapshot', | |
| name: 'mode' | |
| }, | |
| { | |
| xtype: 'pvecheckbox', | |
| fieldLabel: gettext('Enable'), | |
| name: 'enabled', | |
| uncheckedValue: 0, | |
| defaultValue: 1, | |
| checked: true | |
| }, | |
| vmidField | |
| ]; | |
| var ipanel = Ext.create('PVE.panel.InputPanel', { | |
| column1: column1, | |
| column2: column2, | |
| onGetValues: function(values) { | |
| if (!values.node) { | |
| if (!me.create) { | |
| PVE.Utils.assemble_field_data(values, { 'delete': 'node' }); | |
| } | |
| delete values.node; | |
| } | |
| var selMode = values.selMode; | |
| delete values.selMode; | |
| if (selMode === 'all') { | |
| values.all = 1; | |
| values.exclude = ''; | |
| delete values.vmid; | |
| } else if (selMode === 'exclude') { | |
| values.all = 1; | |
| values.exclude = values.vmid; | |
| delete values.vmid; | |
| } | |
| return values; | |
| } | |
| }); | |
| var update_vmid_selection = function(list, mode) { | |
| if (insideUpdate) { | |
| return; // should not happen - just to be sure | |
| } | |
| insideUpdate = true; | |
| if (mode !== 'all') { | |
| sm.deselectAll(true); | |
| if (list) { | |
| Ext.Array.each(list.split(','), function(vmid) { | |
| var rec = store.findRecord('vmid', vmid); | |
| if (rec) { | |
| sm.select(rec, true); | |
| } | |
| }); | |
| } | |
| } | |
| insideUpdate = false; | |
| }; | |
| vmidField.on('change', function(f, value) { | |
| var mode = selModeField.getValue(); | |
| update_vmid_selection(value, mode); | |
| }); | |
| selModeField.on('change', function(f, value, oldValue) { | |
| if (value === 'all') { | |
| sm.selectAll(true); | |
| vmgrid.setDisabled(true); | |
| } else { | |
| vmgrid.setDisabled(false); | |
| } | |
| if (oldValue === 'all') { | |
| sm.deselectAll(true); | |
| vmidField.setValue(''); | |
| } | |
| var list = vmidField.getValue(); | |
| update_vmid_selection(list, value); | |
| }); | |
| var reload = function() { | |
| store.load({ | |
| params: { type: 'vm' }, | |
| callback: function() { | |
| var node = nodesel.getValue(); | |
| store.clearFilter(); | |
| store.filterBy(function(rec) { | |
| return (!node || node.length === 0 || rec.get('node') === node); | |
| }); | |
| var list = vmidField.getValue(); | |
| var mode = selModeField.getValue(); | |
| if (mode === 'all') { | |
| sm.selectAll(true); | |
| } else { | |
| update_vmid_selection(list, mode); | |
| } | |
| } | |
| }); | |
| }; | |
| Ext.applyIf(me, { | |
| subject: gettext("Backup Job"), | |
| url: url, | |
| method: method, | |
| items: [ ipanel, vmgrid ] | |
| }); | |
| me.callParent(); | |
| if (me.create) { | |
| selModeField.setValue('include'); | |
| } else { | |
| me.load({ | |
| success: function(response, options) { | |
| var data = response.result.data; | |
| data.dow = data.dow.split(','); | |
| if (data.all || data.exclude) { | |
| if (data.exclude) { | |
| data.vmid = data.exclude; | |
| data.selMode = 'exclude'; | |
| } else { | |
| data.vmid = ''; | |
| data.selMode = 'all'; | |
| } | |
| } else { | |
| data.selMode = 'include'; | |
| } | |
| me.setValues(data); | |
| } | |
| }); | |
| } | |
| reload(); | |
| } | |
| }); | |
| Ext.define('PVE.dc.BackupView', { | |
| extend: 'Ext.grid.GridPanel', | |
| alias: ['widget.pveDcBackupView'], | |
| 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: 'pve', | |
| 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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| var remove_btn = new PVE.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) { | |
| PVE.Utils.API2Request({ | |
| url: '/cluster/backup/' + rec.data.id, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| callback: function() { | |
| reload(); | |
| }, | |
| failure: function (response, opts) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| } | |
| }); | |
| } | |
| }); | |
| PVE.Utils.monStoreErrors(me, store); | |
| Ext.apply(me, { | |
| store: store, | |
| selModel: sm, | |
| stateful: false, | |
| viewConfig: { | |
| trackOver: false | |
| }, | |
| tbar: [ | |
| { | |
| text: gettext('Add'), | |
| handler: function() { | |
| var win = Ext.create('PVE.dc.BackupEdit',{}); | |
| win.on('destroy', reload); | |
| win.show(); | |
| } | |
| }, | |
| remove_btn, | |
| edit_btn | |
| ], | |
| columns: [ | |
| { | |
| header: gettext('Enabled'), | |
| width: 80, | |
| dataIndex: 'enabled', | |
| xtype: 'checkcolumn', | |
| sortable: true, | |
| disabled: true, | |
| disabledCls: 'x-item-enabled', | |
| stopSelection: false | |
| }, | |
| { | |
| header: gettext('Node'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'node', | |
| renderer: function(value) { | |
| if (value) { | |
| return value; | |
| } | |
| return me.allText; | |
| } | |
| }, | |
| { | |
| header: gettext('Day of week'), | |
| width: 200, | |
| sortable: false, | |
| dataIndex: 'dow', | |
| renderer: function(val) { | |
| var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; | |
| var selected = []; | |
| var cur = -1; | |
| val.split(',').forEach(function(day){ | |
| cur++; | |
| var dow = (dows.indexOf(day)+6)%7; | |
| if (cur === dow) { | |
| if (selected.length === 0 || selected[selected.length-1] === 0) { | |
| selected.push(1); | |
| } else { | |
| selected[selected.length-1]++; | |
| } | |
| } else { | |
| while (cur < dow) { | |
| cur++; | |
| selected.push(0); | |
| } | |
| selected.push(1); | |
| } | |
| }); | |
| cur = -1; | |
| var days = []; | |
| selected.forEach(function(item) { | |
| cur++; | |
| if (item > 2) { | |
| days.push(Ext.Date.dayNames[(cur+1)] + '-' + Ext.Date.dayNames[(cur+item)%7]); | |
| cur += item-1; | |
| } else if (item == 2) { | |
| days.push(Ext.Date.dayNames[cur+1]); | |
| days.push(Ext.Date.dayNames[(cur+2)%7]); | |
| cur++; | |
| } else if (item == 1) { | |
| days.push(Ext.Date.dayNames[(cur+1)%7]); | |
| } | |
| }); | |
| return days.join(', '); | |
| } | |
| }, | |
| { | |
| header: gettext('Start Time'), | |
| width: 60, | |
| sortable: true, | |
| dataIndex: 'starttime' | |
| }, | |
| { | |
| header: gettext('Storage'), | |
| width: 100, | |
| sortable: true, | |
| dataIndex: 'storage' | |
| }, | |
| { | |
| header: gettext('Selection'), | |
| flex: 1, | |
| sortable: false, | |
| dataIndex: 'vmid', | |
| renderer: function(value, metaData, record) { | |
| /*jslint confusion: true */ | |
| if (record.data.all) { | |
| if (record.data.exclude) { | |
| return Ext.String.format(me.allExceptText, record.data.exclude); | |
| } | |
| return me.allText; | |
| } | |
| if (record.data.vmid) { | |
| return record.data.vmid; | |
| } | |
| return "-"; | |
| } | |
| } | |
| ], | |
| listeners: { | |
| activate: reload, | |
| itemdblclick: run_editor | |
| } | |
| }); | |
| me.callParent(); | |
| } | |
| }, function() { | |
| Ext.define('pve-cluster-backup', { | |
| extend: 'Ext.data.Model', | |
| fields: [ | |
| 'id', 'starttime', 'dow', | |
| 'storage', 'node', 'vmid', 'exclude', | |
| 'mailto', | |
| { name: 'enabled', type: 'boolean' }, | |
| { name: 'all', type: 'boolean' }, | |
| { name: 'snapshot', type: 'boolean' }, | |
| { name: 'stop', type: 'boolean' }, | |
| { name: 'suspend', type: 'boolean' }, | |
| { name: 'compress', type: 'boolean' } | |
| ] | |
| }); | |
| }); | |
| Ext.define('PVE.dc.Support', { | |
| extend: 'Ext.panel.Panel', | |
| alias: 'widget.pveDcSupport', | |
| pveGuidePath: '/pve-docs/index.html', | |
| invalidHtml: '<h1>No valid subscription</h1>' + PVE.Utils.noSubKeyHtml, | |
| communityHtml: 'Please use the public community <a target="_blank" href="http://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="http://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() { | |
| PVE.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.SecurityGroupEdit', { | |
| extend: 'PVE.window.Edit', | |
| base_url: "/cluster/firewall/groups", | |
| allow_iface: false, | |
| initComponent : function() { | |
| /*jslint confusion: true */ | |
| var me = this; | |
| me.create = (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.create) { | |
| 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('PVE.panel.InputPanel', { | |
| create: me.create, | |
| items: items | |
| }); | |
| Ext.apply(me, { | |
| subject: subject, | |
| items: [ ipanel ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| Ext.define('PVE.SecurityGroupList', { | |
| extend: 'Ext.grid.Panel', | |
| alias: 'widget.pveSecurityGroupList', | |
| 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({ | |
| fields: [ 'group', 'comment', 'digest' ], | |
| proxy: { | |
| type: 'pve', | |
| url: '/api2/json' + me.base_url | |
| }, | |
| idProperty: 'group', | |
| 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 PVE.button.Button({ | |
| text: gettext('Edit'), | |
| disabled: true, | |
| selModel: sm, | |
| handler: run_editor | |
| }); | |
| me.addBtn = new PVE.button.Button({ | |
| text: gettext('Create'), | |
| handler: function() { | |
| sm.deselectAll(); | |
| var win = Ext.create('PVE.SecurityGroupEdit', {}); | |
| win.show(); | |
| win.on('destroy', reload); | |
| } | |
| }); | |
| me.removeBtn = new PVE.button.Button({ | |
| text: gettext('Remove'), | |
| selModel: sm, | |
| disabled: true, | |
| handler: function() { | |
| var rec = sm.getSelection()[0]; | |
| if (!rec || !me.base_url) { | |
| return; | |
| } | |
| PVE.Utils.API2Request({ | |
| url: me.base_url + '/' + rec.data.group, | |
| method: 'DELETE', | |
| waitMsgTarget: me, | |
| failure: function(response, options) { | |
| Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
| }, | |
| callback: reload | |
| }); | |
| } | |
| }); | |
| Ext.apply(me, { | |
| store: store, | |
| tbar: [ '<b>' + gettext('Group') + ':</b>', me.addBtn, me.removeBtn, me.editBtn ], | |
| selModel: sm, | |
| columns: [ | |
| { header: gettext('Group'), dataIndex: 'group', width: 100 }, | |
| { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } | |
| ], | |
| listeners: { | |
| itemdblclick: run_editor, | |
| select: function(sm, rec) { | |
| 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>', | |
| flex: 0.75, | |
| border: false | |
| }); | |
| var sglist = Ext.createWidget('pveSecurityGroupList', { | |
| region: 'west', | |
| rule_panel: rule_panel, | |
| flex: 0.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', | |
| 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', | |
| itemId: 'summary' | |
| }); | |
| me.items.push({ | |
| xtype: 'pveDcOptionView', | |
| title: gettext('Options'), | |
| itemId: 'options' | |
| }); | |
| } | |
| if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) { | |
| me.items.push({ | |
| xtype: 'pveStorageView', | |
| title: gettext('Storage'), | |
| itemId: 'storage' | |
| }); | |
| } | |
| if (caps.dc['Sys.Audit']) { | |
| me.items.push({ | |
| xtype: 'pveDcBackupView', | |
| title: gettext('Backup'), | |
| itemId: 'backup' | |
| }); | |
| } | |
| me.items.push({ | |
| xtype: 'pveUserView', | |
| title: gettext('Users'), | |
| itemId: 'users' | |
| }); | |
| if (caps.dc['Sys.Audit']) { | |
| me.items.push({ | |
| xtype: 'pveGroupView', | |
| title: gettext('Groups'), | |
| itemId: 'groups' | |
| }); | |
| me.items.push({ | |
| xtype: 'pvePoolView', | |
| title: gettext('Pools'), | |
| itemId: 'pools' | |
| }); | |
| me.items.push({ | |
| xtype: 'pveACLView', | |
| title: gettext('Permissions'), | |
| itemId: 'permissions' | |
| }); | |
| me.items.push({ | |
| xtype: 'pveRoleView', | |
| title: gettext('Roles'), | |
| itemId: 'roles' | |
| }); | |
| me.items.push({ | |
| xtype: 'pveAuthView', | |
| title: gettext('Authentication'), | |
| itemId: 'domains' | |
| }); | |
| me.items.push({ | |
| xtype: 'pveHAPanel', | |
| title: 'HA', | |
| phstateid: me.hstateid, | |
| itemId: 'ha' | |
| }); | |
| me.items.push({ | |
| xtype: 'pveFirewallPanel', | |
| title: gettext('Firewall'), | |
| base_url: '/cluster/firewall', | |
| fwtype: 'dc', | |
| phstateid: me.hstateid, | |
| itemId: 'firewall' | |
| }); | |
| me.items.push({ | |
| xtype: 'pveDcSupport', | |
| title: gettext('Support'), | |
| itemId: 'support' | |
| }); | |
| } | |
| me.callParent(); | |
| } | |
| }); | |
| /* | |
| * Workspace base class | |
| * | |
| * popup login window when auth fails (call onLogin handler) | |
| * update (re-login) ticket every 15 minutes | |
| * | |
| */ | |
| Ext.define('PVE.Workspace', { | |
| extend: 'Ext.container.Viewport', | |
| title: 'Proxmox Virtual Environment', | |
| loginData: null, // Data from last login call | |
| onLogin: function(loginData) {}, | |
| // private | |
| updateLoginData: function(loginData) { | |
| var me = this; | |
| me.loginData = loginData; | |
| PVE.CSRFPreventionToken = loginData.CSRFPreventionToken; | |
| PVE.UserName = loginData.username; | |
| if (loginData.cap) { | |
| Ext.state.Manager.set('GuiCap', loginData.cap); | |
| } | |
| // creates a session cookie (expire = null) | |
| // that way the cookie gets deleted after browser window close | |
| Ext.util.Cookies.set('PVEAuthCookie', loginData.ticket, null, '/', null, true); | |
| me.onLogin(loginData); | |
| }, | |
| // private | |
| showLogin: function() { | |
| var me = this; | |
| PVE.Utils.authClear(); | |
| PVE.UserName = null; | |
| me.loginData = null; | |
| if (!me.login) { | |
| me.login = Ext.create('PVE.window.LoginWindow', { | |
| handler: function(data) { | |
| me.login = null; | |
| me.updateLoginData(data); | |
| PVE.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) { // auth failure | |
| me.showLogin(); | |
| } | |
| }); | |
| me.callParent(); | |
| if (!PVE.Utils.authOK()) { | |
| me.showLogin(); | |
| } else { | |
| if (me.loginData) { | |
| me.onLogin(me.loginData); | |
| } | |
| } | |
| Ext.TaskManager.start({ | |
| run: function() { | |
| var ticket = PVE.Utils.authOK(); | |
| if (!ticket || !PVE.UserName) { | |
| return; | |
| } | |
| Ext.Ajax.request({ | |
| params: { | |
| username: PVE.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.ConsoleWorkspace', { | |
| extend: 'PVE.Workspace', | |
| alias: ['widget.pveConsoleWorkspace'], | |
| title: gettext('Console'), | |
| initComponent : function() { | |
| // novnc is a string in param | |
| // but a boolean in content | |
| /*jslint confusion: true*/ | |
| var me = this; | |
| var param = Ext.Object.fromQueryString(window.location.search); | |
| var consoleType = me.consoleType || param.console; | |
| param.novnc = (param.novnc === '1') ? true : false; | |
| var content; | |
| if (consoleType === 'kvm') { | |
| me.title = "VM " + param.vmid; | |
| if (param.vmname) { | |
| me.title += " ('" + param.vmname + "')"; | |
| } | |
| content = { | |
| xtype: 'pveKVMConsole', | |
| novnc: param.novnc, | |
| vmid: param.vmid, | |
| nodename: param.node, | |
| vmname: param.vmname, | |
| toplevel: true | |
| }; | |
| } else if (consoleType === 'lxc') { | |
| me.title = "CT " + param.vmid; | |
| if (param.vmname) { | |
| me.title += " ('" + param.vmname + "')"; | |
| } | |
| content = { | |
| xtype: 'pveLxcConsole', | |
| novnc: param.novnc, | |
| vmid: param.vmid, | |
| nodename: param.node, | |
| vmname: param.vmname, | |
| toplevel: true | |
| }; | |
| } else if (consoleType === 'shell') { | |
| me.title = "node '" + param.node + "'"; | |
| content = { | |
| xtype: 'pveShell', | |
| novnc: param.novnc, | |
| nodename: param.node, | |
| toplevel: true | |
| }; | |
| } else if (consoleType === 'upgrade') { | |
| me.title = Ext.String.format(gettext('System upgrade on node {0}'), "'" + param.node + "'"); | |
| content = { | |
| xtype: 'pveShell', | |
| novnc: param.novnc, | |
| nodename: param.node, | |
| ugradeSystem: true, | |
| toplevel: true | |
| }; | |
| } else { | |
| content = { | |
| border: false, | |
| bodyPadding: 10, | |
| html: gettext('Error') + ': No such console type' | |
| }; | |
| } | |
| Ext.apply(me, { | |
| layout: { type: 'fit' }, | |
| border: false, | |
| items: [ content ] | |
| }); | |
| me.callParent(); | |
| } | |
| }); | |
| 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) { | |
| PVE.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 { | |
| // TODO: display something useful | |
| // Note:: error mask has wrong zindex, so we do not | |
| // use that - see bug 114 | |
| // PVE.Utils.setErrorMask(cont, 'nothing selected'); | |
| //} | |
| }, | |
| selectById: function(nodeid) { | |
| var me = this; | |
| var tree = me.down('pveResourceTree'); | |
| tree.selectById(nodeid); | |
| }, | |
| checkVmMigration: function(record) { | |
| var me = this; | |
| var tree = me.down('pveResourceTree'); | |
| tree.checkVmMigration(record); | |
| }, | |
| onLogin: function(loginData) { | |
| var me = this; | |
| me.updateUserInfo(); | |
| if (loginData) { | |
| PVE.data.ResourceStore.startUpdate(); | |
| PVE.Utils.API2Request({ | |
| url: '/version', | |
| method: 'GET', | |
| success: function(response) { | |
| PVE.VersionInfo = response.result.data; | |
| me.updateVersionInfo(); | |
| } | |
| }); | |
| } | |
| }, | |
| updateUserInfo: function() { | |
| var me = this; | |
| var ui = me.query('#userinfo')[0]; | |
| if (PVE.UserName) { | |
| var msg = Ext.String.format(gettext("You are logged in as {0}"), "'" + PVE.UserName + "'"); | |
| ui.update('<div class="x-unselectable" style="white-space:nowrap;">' + msg + '</div>'); | |
| } else { | |
| ui.update(''); | |
| } | |
| ui.updateLayout(); | |
| }, | |
| updateVersionInfo: function() { | |
| var me = this; | |
| var ui = me.query('#versioninfo')[0]; | |
| if (PVE.VersionInfo) { | |
| var version = PVE.VersionInfo.version + '-' + PVE.VersionInfo.release + '/' + | |
| PVE.VersionInfo.repoid; | |
| ui.update('Proxmox Virtual Environment ' + version); | |
| } else { | |
| ui.update('Proxmox 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) { | |
| var comp; | |
| var tlckup = { | |
| root: 'PVE.dc.Config', | |
| node: 'PVE.node.Config', | |
| qemu: 'PVE.qemu.Config', | |
| lxc: 'PVE.lxc.Config', | |
| storage: 'PVE.storage.Browser', | |
| pool: 'pvePoolConfig' | |
| }; | |
| if (selected.length > 0) { | |
| var n = selected[0]; | |
| comp = { | |
| xtype: tlckup[n.data.type || 'root'] || | |
| 'pvePanelConfig', | |
| layout: { type: 'fit' }, | |
| 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="http://www.proxmox.com">' + | |
| '<img height=30 width=209 src="/pve2/images/proxmox_logo.png"/></a>' | |
| }, | |
| { | |
| minWidth: 200, | |
| flex: 1, | |
| id: 'versioninfo', | |
| html: 'Proxmox Virtual Environment' | |
| }, | |
| { | |
| pack: 'end', | |
| margin: '0 10 0 0', | |
| id: 'userinfo', | |
| stateful: false | |
| }, | |
| createVM, | |
| createCT, | |
| { | |
| pack: 'end', | |
| margin: '0 5 0 0', | |
| xtype: 'button', | |
| baseCls: 'x-btn', | |
| iconCls: 'fa fa-sign-out', | |
| text: gettext("Logout"), | |
| handler: function() { | |
| PVE.data.ResourceStore.stopUpdate(); | |
| me.showLogin(); | |
| me.setContent(); | |
| var rt = me.down('pveResourceTree'); | |
| rt.clearTree(); | |
| } | |
| } | |
| ] | |
| }, | |
| { | |
| 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) { | |
| console.log('test'); | |
| var viewHeight = me.getSize().height; | |
| if (height > (viewHeight - 150)) { | |
| panel.setHeight(viewHeight - 150); | |
| } | |
| } | |
| } | |
| } | |
| ] | |
| }); | |
| me.callParent(); | |
| me.updateUserInfo(); | |
| } | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment