[pve-devel] [PATCH manager 5/7] add replication grid

Dominik Csapak d.csapak at proxmox.com
Tue Jun 13 14:56:04 CEST 2017


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 <d.csapak at proxmox.com>
---
 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  + "'" + '<br>' +
+					    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('<i class="fa fa-ban warning" title="'
+					+ gettext("Removal Scheduled") + '"></i>');
+			}
+
+			if (record.data.error) {
+			    states.push('<i class="fa fa-times critical" title="'
+					+ gettext("Error") + '"></i>');
+			}
+
+			if (states.length > 0) {
+			    return states.join(',');
+			}
+
+			return '<i class="fa fa-check good"></i>';
+		    }
+		},
+		{
+		    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'
+	]
+    });
+
+});
-- 
2.11.0





More information about the pve-devel mailing list