diff --git a/www/manager/lxc/CmdMenu.js b/www/manager/lxc/CmdMenu.js new file mode 100644 index 00000000..f793b2e6 --- /dev/null +++ b/www/manager/lxc/CmdMenu.js @@ -0,0 +1,112 @@ +Ext.define('PVE.lxc.CmdMenu', { + 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 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); + } + }); + }; + + me.title = "CT " + vmid; + + me.items = [ + { + text: gettext('Start'), + icon: '/pve2/images/start.png', + handler: function() { + vm_command('start'); + } + }, + { + text: gettext('Migrate'), + icon: '/pve2/images/forward.png', + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'lxc', + nodename: nodename, + vmid: vmid + }); + win.show(); + } + }, + { + text: gettext('Suspend'), + icon: '/pve2/images/forward.png', + handler: function() { + var msg = Ext.String.format(gettext("Do you really want to suspend CT {0}?"), vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command('suspend'); + }); + } + }, + { + text: gettext('Resume'), + icon: '/pve2/images/forward.png', + handler: function() { + vm_command('resume'); + } + }, + { + text: gettext('Shutdown'), + icon: '/pve2/images/stop.png', + handler: function() { + var msg = Ext.String.format(gettext("Do you really want to shutdown CT {0}?"), vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command('shutdown'); + }); + } + }, + { + text: gettext('Stop'), + icon: '/pve2/images/gtk-stop.png', + handler: function() { + var msg = Ext.String.format(gettext("Do you really want to stop CT {0}?"), vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command("stop"); + }); + } + }, + { + text: gettext('Console'), + icon: '/pve2/images/display.png', + handler: function() { + PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname); + } + } + ]; + + me.callParent(); + } +}); diff --git a/www/manager/lxc/Config.js b/www/manager/lxc/Config.js new file mode 100644 index 00000000..374820a3 --- /dev/null +++ b/www/manager/lxc/Config.js @@ -0,0 +1,202 @@ +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 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'); + } + }); + + var umountBtn = Ext.create('Ext.Button', { + text: gettext('Unmount'), + disabled: true, + hidden: true, + handler: function() { + vm_command('umount'); + } + }); + + var stopBtn = Ext.create('PVE.button.Button', { + text: gettext('Stop'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Ext.String.format(gettext("Do you really want to stop VM {0}?"), vmid), + handler: function() { + vm_command("stop"); + } + }); + + var shutdownBtn = Ext.create('PVE.button.Button', { + text: gettext('Shutdown'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Ext.String.format(gettext("Do you really want to shutdown VM {0}?"), vmid), + handler: function() { + vm_command('shutdown'); + } + }); + + 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(); + } + }); + + var removeBtn = Ext.create('PVE.button.Button', { + text: gettext('Remove'), + disabled: !caps.vms['VM.Allocate'], + dangerous: true, + confirmMsg: Ext.String.format(gettext('Are you sure you want to remove VM {0}? This will permanently erase all VM data.'), vmid), + handler: function() { + PVE.Utils.API2Request({ + url: base_url, + method: 'DELETE', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }); + + 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 + }); + + 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, stopBtn, removeBtn, + migrateBtn, consoleBtn ], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveLxcSummary', + itemId: 'summary' + }, + { + 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([ + { + 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.statusStore.on('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'; + } + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running'); + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + stopBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'stopped'); + removeBtn.setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + + if (status === 'mounted') { + umountBtn.setDisabled(false); + umountBtn.setVisible(true); + stopBtn.setVisible(false); + } else { + umountBtn.setDisabled(true); + umountBtn.setVisible(false); + stopBtn.setVisible(true); + } + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); diff --git a/www/manager/lxc/CreateWizard.js b/www/manager/lxc/CreateWizard.js new file mode 100644 index 00000000..c3bf5198 --- /dev/null +++ b/www/manager/lxc/CreateWizard.js @@ -0,0 +1,244 @@ +/*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 storagesel = Ext.create('PVE.form.StorageSelector', { + name: 'storage', + fieldLabel: gettext('Storage'), + storageContent: 'rootdir', + autoSelect: true, + allowBlank: false + }); + + 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 bridgesel = Ext.create('PVE.form.BridgeSelector', { + name: 'bridge', + fieldLabel: gettext('Bridge'), + labelAlign: 'right', + autoSelect: true, + allowBlank: false + }); + + Ext.applyIf(me, { + subject: gettext('LXC Container'), + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + column1: [ + { + xtype: 'PVE.form.NodeSelector', + name: 'nodename', + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true, + listeners: { + change: function(f, value) { + tmplstoragesel.setNodename(value); + tmplsel.setStorage(undefined, value); + bridgesel.setNodename(value); + storagesel.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 + }, + storagesel, + { + 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] + }, +// { +// xtype: 'pveLxcResourceInputPanel', +// title: gettext('Resources') +// }, + { + xtype: 'inputpanel', + title: gettext('Network'), + column1: [ + bridgesel, + { + xtype: 'pvecheckbox', + fieldLabel: gettext('Firewall'), + name: 'firewall', + checked: false, + disabled: true + } + ], + onGetValues: function(values) { + var netif = PVE.Parser.printLxcNetwork({ + link: values.bridge, + firewall: values.firewall + }); + return { net0: netif }; + } + }, + { + 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(); + } +}); + + + diff --git a/www/manager/lxc/StatusView.js b/www/manager/lxc/StatusView.js new file mode 100644 index 00000000..20da0ece --- /dev/null +++ b/www/manager/lxc/StatusView.js @@ -0,0 +1,79 @@ +Ext.define('PVE.lxc.StatusView', { + extend: 'PVE.grid.ObjectGrid', + + 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 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 = "
" + PVE.Utils.totalText + ": " + PVE.Utils.format_size(maxmem) + "
" + + "
" + PVE.Utils.usedText + ": " + PVE.Utils.format_size(value) + "
"; + 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 = "
" + PVE.Utils.totalText + ": " + PVE.Utils.format_size(maxswap) + "
" + + "
" + PVE.Utils.usedText + ": " + PVE.Utils.format_size(value) + "
"; + 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 = { + 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 }, + uptime: { header: gettext('Uptime'), required: true, renderer: PVE.Utils.render_uptime }, + ha: { header: gettext('Managed by HA'), required: true, renderer: PVE.Utils.format_boolean } + }; + + Ext.applyIf(me, { + cwidth1: 150, + height: 200, + rows: rows + }); + + me.callParent(); + } +}); diff --git a/www/manager/lxc/Summary.js b/www/manager/lxc/Summary.js new file mode 100644 index 00000000..9ad19b56 --- /dev/null +++ b/www/manager/lxc/Summary.js @@ -0,0 +1,102 @@ +Ext.define('PVE.lxc.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveLxcSummary', + + 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 rstore = me.statusStore; + + var statusview = Ext.create('PVE.lxc.StatusView', { + title: gettext('Status'), + pveSelNode: me.pveSelNode, + width: 400, + rstore: rstore + }); + + var rrdurl = "/api2/png/nodes/" + nodename + "/lxc/" + vmid + "/rrd"; + + var notesview = Ext.create('PVE.panel.NotesView', { + pveSelNode: me.pveSelNode, + flex: 1 + }); + + Ext.apply(me, { + tbar: [ + '->', + { + xtype: 'pveRRDTypeSelector' + } + ], + autoScroll: true, + bodyStyle: 'padding:10px', + defaults: { + style: 'padding-top:10px', + width: 800 + }, + items: [ + { + style: 'padding-top:0px', + layout: { + type: 'hbox', + align: 'stretchmax' + }, + border: false, + items: [ statusview, notesview ] + }, + { + xtype: 'pveRRDView', + title: gettext('CPU usage'), + pveSelNode: me.pveSelNode, + datasource: 'cpu', + rrdurl: rrdurl + }, + { + xtype: 'pveRRDView', + title: gettext('Memory usage'), + pveSelNode: me.pveSelNode, + datasource: 'mem,maxmem', + rrdurl: rrdurl + }, + { + xtype: 'pveRRDView', + title: gettext('Network traffic'), + pveSelNode: me.pveSelNode, + datasource: 'netin,netout', + rrdurl: rrdurl + }, + { + xtype: 'pveRRDView', + title: gettext('Disk IO'), + pveSelNode: me.pveSelNode, + datasource: 'diskread,diskwrite', + rrdurl: rrdurl + } + ] + }); + + me.on('show', function() { + notesview.load(); + }); + + me.callParent(); + } +});