[pve-devel] [PATCH] Add Snapshot to LXC

Wolfgang Link w.link at proxmox.com
Fri Jul 17 09:26:42 CEST 2015


Signed-off-by: Wolfgang Link <w.link at proxmox.com>
---
 www/manager/Makefile            |   2 +
 www/manager/Utils.js            |   3 +
 www/manager/lxc/Config.js       |   9 +-
 www/manager/lxc/Snapshot.js     | 198 ++++++++++++++++++++++++++
 www/manager/lxc/SnapshotTree.js | 304 ++++++++++++++++++++++++++++++++++++++++
 5 files changed, 515 insertions(+), 1 deletion(-)
 create mode 100644 www/manager/lxc/Snapshot.js
 create mode 100644 www/manager/lxc/SnapshotTree.js

diff --git a/www/manager/Makefile b/www/manager/Makefile
index 05eee89..d330c82 100644
--- a/www/manager/Makefile
+++ b/www/manager/Makefile
@@ -147,6 +147,8 @@ JSSRC= 				                 	\
 	lxc/DNS.js					\
 	lxc/Config.js					\
 	lxc/CreateWizard.js				\
+	lxc/SnapshotTree.js				\
+	lxc/Snapshot.js				\
 	pool/StatusView.js				\
 	pool/Summary.js					\
 	pool/Config.js					\
diff --git a/www/manager/Utils.js b/www/manager/Utils.js
index 7632822..351f157 100644
--- a/www/manager/Utils.js
+++ b/www/manager/Utils.js
@@ -542,6 +542,9 @@ Ext.define('PVE.Utils', { statics: {
 	vzshutdown: ['CT', gettext('Shutdown') ],
 	vzsuspend: [ 'CT', gettext('Suspend') ],
 	vzresume: [ 'CT', gettext('Resume') ],
+	vzsnapshot: [ 'CT', gettext('Snapshot') ],
+	vzrollback: [ 'CT', gettext('Rollback') ],
+	vzdelsnapshot: [ 'CT', gettext('Delete Snapshot') ],
 	hamigrate: [ 'HA', gettext('Migrate') ],
 	hastart: [ 'HA', gettext('Start') ],
 	hastop: [ 'HA', gettext('Stop') ],
diff --git a/www/manager/lxc/Config.js b/www/manager/lxc/Config.js
index 578b4ea..e42a3c5 100644
--- a/www/manager/lxc/Config.js
+++ b/www/manager/lxc/Config.js
@@ -172,7 +172,14 @@ Ext.define('PVE.lxc.Config', {
 		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([
diff --git a/www/manager/lxc/Snapshot.js b/www/manager/lxc/Snapshot.js
new file mode 100644
index 0000000..1743b21
--- /dev/null
+++ b/www/manager/lxc/Snapshot.js
@@ -0,0 +1,198 @@
+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: 'StorageId',
+		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: 400
+	    });
+	}
+
+	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('datachanged', summarystore);
+
+		form.findField('snaptime').setValue(new Date(data.snaptime));
+		form.findField('description').setValue(data.description);
+	    }
+	});
+    }
+});
diff --git a/www/manager/lxc/SnapshotTree.js b/www/manager/lxc/SnapshotTree.js
new file mode 100644
index 0000000..f13e64f
--- /dev/null
+++ b/www/manager/lxc/SnapshotTree.js
@@ -0,0 +1,304 @@
+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 = 'x-tree-node-computer-running';
+			} else {
+			    item.iconCls = 'x-tree-node-computer';
+			}
+		    } else {
+			item.iconCls = 'x-tree-node-snapshot';
+		    }
+		    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;
+			} 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) {
+		   Ext.getCmp('snapshotBtn').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) {
+		var msg = Ext.String.format(gettext('Are you sure you want to rollback to snapshot {0}'),
+					    "'" + rec.data.name + "'");
+		return msg;
+	    },
+	    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', {
+	    id: '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 value;
+			}
+		    }
+		}
+	    ],
+	    columnLines: true, // will work in 4.1?
+	    listeners: {
+		show: me.reload,
+		hide: me.load_task.cancel,
+		destroy: me.load_task.cancel,
+		// disable collapse
+		beforeitemcollapse: function() { return false; },
+		itemdblclick: run_editor
+	    }
+	});
+
+	me.callParent();
+
+	me.store.sorters.add(new Ext.util.Sorter({
+	    sorterFn: me.sorterFn
+	}));
+    }
+});
-- 
2.1.4





More information about the pve-devel mailing list