From 9058a478af9258c2718b8a8ca29e9f705c30d14e Mon Sep 17 00:00:00 2001 From: Dominik Csapak Date: Tue, 13 Jun 2017 14:56:04 +0200 Subject: [PATCH] add replication grid this patch adds a replication grid, which shows the status/configuration for the defined replication jobs, and allow them to be added/edited/removed in case the replication grid gets shown in the datacenter, we omit the status fields, because we only have the configuration there when opening the grid, we check if there are at least two nodes, because currently we only allow replication in a cluster environment Signed-off-by: Dominik Csapak --- www/css/ext6-pve.css | 5 + www/manager6/Makefile | 1 + www/manager6/grid/Replication.js | 486 +++++++++++++++++++++++++++++++ 3 files changed, 492 insertions(+) create mode 100644 www/manager6/grid/Replication.js diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css index 6680477b..67bc865d 100644 --- a/www/css/ext6-pve.css +++ b/www/css/ext6-pve.css @@ -532,3 +532,8 @@ table.osds td:first-of-type { .pve-invalid-row { background-color: #f3d6d7; } + +.pve-static-mask div.x-mask-msg-text { + padding: 10px; + background-image: none; +} diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 7eb6b211..6c3efe0d 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -198,6 +198,7 @@ JSSRC= \ ha/Groups.js \ ha/Fencing.js \ dc/Summary.js \ + grid/Replication.js \ dc/Health.js \ dc/Guests.js \ dc/OptionView.js \ diff --git a/www/manager6/grid/Replication.js b/www/manager6/grid/Replication.js new file mode 100644 index 00000000..99c7ccc5 --- /dev/null +++ b/www/manager6/grid/Replication.js @@ -0,0 +1,486 @@ +Ext.define('PVE.window.ReplicaEdit', { + extend: 'PVE.window.Edit', + xtype: 'pveReplicaEdit', + + subject: gettext('Replication Job'), + + + url: '/cluster/replication', + method: 'POST', + + initComponent: function() { + var me = this; + + var vmid = me.pveSelNode.data.vmid; + var nodename = me.pveSelNode.data.node; + + var items = []; + + items.push({ + xtype: (me.isCreate && !vmid)?'pveGuestIDSelector':'displayfield', + name: 'guest', + fieldLabel: 'CT/VM ID', + value: vmid || '' + }); + + items.push( + { + xtype: me.isCreate ? 'pveNodeSelector':'displayfield', + name: 'target', + disallowedNodes: [nodename], + allowBlank: false, + onlineValidator: true, + fieldLabel: gettext("Target") + }, + { + xtype: 'pveCalendarEvent', + fieldLabel: gettext('Schedule'), + emptyText: '*/15', + name: 'schedule' + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Rate (MB/s)'), + step: 1, + minValue: 1, + emptyText: gettext('unlimited'), + name: 'rate' + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment' + } + ); + + me.items = [ + { + xtype: 'inputpanel', + itemId: 'ipanel', + + onGetValues: function(values) { + var me = this.up('window'); + + PVE.Utils.delete_if_default(values, 'rate', '', me.isCreate); + PVE.Utils.delete_if_default(values, 'schedule', '*/15', me.isCreate); + PVE.Utils.delete_if_default(values, 'comment', '', me.isCreate); + + if (me.isCreate) { + values.type = 'local'; + var id = -1; + if (me.highestids[values.guest] !== undefined) { + id = me.highestids[values.guest]; + } + id++; + values.id = values.guest + '-' + id.toString(); + delete values.guest; + } + return values; + }, + items: items + } + ]; + + me.callParent(); + + if (me.isCreate) { + me.load({ + success: function(response) { + var jobs = response.result.data; + var highestids = {}; + Ext.Array.forEach(jobs, function(job) { + var match = /^([0-9]+)\-([0-9]+)$/.exec(job.id); + if (match) { + var vmid = parseInt(match[1],10); + var id = parseInt(match[2],10); + if (highestids[vmid] < id || + highestids[vmid] === undefined) { + highestids[vmid] = id; + } + } + }); + + me.highestids = highestids; + } + }); + + } else { + me.load({ + success: function(response, options) { + me.setValues(response.result.data); + me.digest = response.result.data.digest; + } + }); + } + } +}); + +Ext.define('PVE.grid.ReplicaView', { + extend: 'Ext.grid.Panel', + xtype: 'pveReplicaView', + + // not here yet: + //onlineHelp: 'todo', + + stateful: true, + stateId: 'grid-pve-replication-status', + + controller: { + xclass: 'Ext.app.ViewController', + + addJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + var win = Ext.create('PVE.window.ReplicaEdit', { + isCreate: true, + method: 'POST', + pveSelNode: me.pveSelNode + }); + win.on('destroy', function() { controller.reload(); }); + win.show(); + }, + + editJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + var data = rec.data; + var win = Ext.create('PVE.window.ReplicaEdit', { + url: '/cluster/replication/' + data.id, + method: 'PUT', + pveSelNode: me.pveSelNode + }); + win.on('destroy', function() { controller.reload(); }); + win.show(); + }, + + removeJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + PVE.Utils.API2Request({ + url: '/api2/extjs/cluster/replication/' + rec.data.id, + waitMsgTarget: me, + method: 'DELETE', + callback: function() { controller.reload(); }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }, + + showLog: function(button, event, rec) { + var me = this.getView(); + var controller = this; + var logView = Ext.create('PVE.panel.LogView', { + border: false, + url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/log" + }); + var win = Ext.create('Ext.window.Window', { + items: [ logView ], + layout: 'fit', + width: 800, + height: 400, + modal: true, + title: gettext("Replication Log") + }); + var task = { + run: function() { + logView.requestUpdate(); + }, + interval: 1000 + }; + Ext.TaskManager.start(task); + win.on('destroy', function() { + Ext.TaskManager.stop(task); + controller.reload(); + }); + win.show(); + }, + + reload: function() { + var me = this.getView(); + me.rstore.load(); + }, + + dblClick: function(grid, record, item) { + var me = this; + me.editJob(undefined, undefined, record); + }, + + // check for cluster + // currently replication is for cluster only, so we disable the whole + // component + checkPrerequisites: function() { + var me = this.getView(); + if (PVE.data.ResourceStore.getNodes().length < 2) { + me.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']); + } + }, + + control: { + '#': { + itemdblclick: 'dblClick', + afterlayout: 'checkPrerequisites' + } + } + }, + + tbar: [ + { + text: gettext('Add'), + itemId: 'addButton', + handler: 'addJob' + }, + { + xtype: 'pveButton', + text: gettext('Edit'), + itemId: 'editButton', + handler: 'editJob', + disabled: true + }, + { + xtype: 'pveButton', + text: gettext('Remove'), + itemId: 'removeButton', + handler: 'removeJob', + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove replication job {0}'), + "'" + rec.id + "'" + '
' + + gettext('(Note: Removal of replication job has to be replicated)')); + return msg; + }, + disabled: true + }, + { + xtype: 'pveButton', + text: gettext('Log'), + itemId: 'logButton', + handler: 'showLog', + disabled: true + } + ], + + initComponent: function() { + var me = this; + var mode = ''; + var url = '/cluster/replication'; + + me.nodename = me.pveSelNode.data.node; + me.vmid = me.pveSelNode.data.vmid; + + me.columns = [ + { + text: gettext('ID'), + dataIndex: 'id', + width: 60, + hidden: true + }, + { + text: gettext('Guest'), + dataIndex: 'guest', + width: 75 + }, + { + text: gettext('Job'), + dataIndex: 'jobnum', + width: 65 + }, + { + text: gettext('Target'), + dataIndex: 'target' + } + ]; + + if (!me.nodename) { + mode = 'dc'; + me.stateId = 'grid-pve-replication-dc'; + } else if (!me.vmid) { + mode = 'node'; + url = '/nodes/' + me.nodename + '/replication'; + } else { + mode = 'vm'; + url = '/nodes/' + me.nodename + '/replication' + '?guest=' + me.vmid; + } + + if (mode !== 'dc') { + me.columns.push( + { + text: gettext('Status'), + dataIndex: 'state', + width: 60, + renderer: function(value, metadata, record) { + + if (record.data.pid) { + metadata.tdCls = 'x-grid-row-loading'; + return ''; + } + + var states = []; + + if (record.data.remove_job) { + states.push(''); + } + + if (record.data.error) { + states.push(''); + } + + if (states.length > 0) { + return states.join(','); + } + + return ''; + } + }, + { + text: gettext('Status Text'), + dataIndex: 'error', + minWidth: 100, + flex: 1, + renderer: function(value, metadata, record) { + var states = []; + + if (record.data.remove_job) { + states.push(gettext("Removal Scheduled")); + } + + if (record.data.error) { + states.push(record.data.error); + } + + if (states.length > 0) { + return states.join(', '); + } + + return '-'; + } + }, + { + text: gettext('Last Sync'), + dataIndex: 'last_sync', + renderer: function(value, metadata, record) { + if (!value) { + return '-'; + } + + if (record.data.pid) { + return gettext('syncing'); + } + + return PVE.Utils.render_timestamp(value); + } + }, + { + text: gettext('Duration'), + dataIndex: 'duration', + renderer: PVE.Utils.render_duration + }, + { + text: gettext('Next Sync'), + dataIndex: 'next_sync', + renderer: function(value) { + if (!value) { + return '-'; + } + + var now = new Date(); + var next = new Date(value*1000); + + if (next < now) { + return gettext('now'); + } + + return PVE.Utils.render_timestamp(value); + } + } + ); + } + + me.columns.push( + { + text: gettext('Schedule'), + dataIndex: 'schedule' + }, + { + text: gettext('Rate'), + dataIndex: 'rate', + renderer: function(value) { + if (!value) { + return gettext('unlimited'); + } + + return value.toString() + ' MB/s'; + }, + hidden: true + }, + { + text: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.htmlEncode + } + ); + + me.rstore = Ext.create('PVE.data.UpdateStore', { + storeid: 'pve-replica-' + me.nodename + me.vmid, + model: (mode === 'dc')? 'pve-replication' : 'pve-replication-state', + interval: 3000, + proxy: { + type: 'pve', + url: "/api2/json" + url + } + }); + + me.store = Ext.create('PVE.data.DiffStore', { + rstore: me.rstore, + sorters: [ + { + property: 'guest' + }, + { + property: 'jobnum' + } + ] + }); + + me.callParent(); + + // we cannot access the log in the datacenter, because + // we do not know where/if the jobs runs + if (mode === 'dc') { + me.down('#logButton').setHidden(true); + } + + // if we set the warning mask, we do not want to load + // or set the mask on store errors + if (PVE.data.ResourceStore.getNodes().length < 2) { + return; + } + + PVE.Utils.monStoreErrors(me, me.rstore); + + me.on('destroy', me.rstore.stopUpdate); + me.rstore.startUpdate(); + } +}, function() { + + Ext.define('pve-replication', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'target', 'comment', 'rate', 'type', + { name: 'guest', type: 'integer' }, + { name: 'jobnum', type: 'integer' }, + { name: 'schedule', defaultValue: '*/15' } + ] + }); + + Ext.define('pve-replication-state', { + extend: 'pve-replication', + fields: [ + 'last_sync', 'next_sync', 'error', 'duration', 'state', + 'fail_count', 'remove_job', 'pid' + ] + }); + +});