Adding some disk image utils
Copy, clone, rebase images



git-svn-id: https://svn.apache.org/repos/asf/incubator/tashi/trunk@1455862 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/tashi/client/tashi-client.py b/src/tashi/client/tashi-client.py
index 640342e..1cc0526 100755
--- a/src/tashi/client/tashi-client.py
+++ b/src/tashi/client/tashi-client.py
@@ -261,6 +261,8 @@
 'getSlots': (getSlots, None),
 'getImages': (None, ['id', 'imageName', 'imageSize']), 
 'copyImage': (None, None), 
+'cloneImage': (None, None), 
+'rebaseImage': (None, None), 
 'createVm': (None, ['id', 'hostId', 'name', 'user', 'state', 'disk', 'memory', 'cores']),
 'createMany': (createMany, ['id', 'hostId', 'name', 'user', 'state', 'disk', 'memory', 'cores']),
 'shutdownMany': (shutdownMany, None),
@@ -286,6 +288,8 @@
 'getSlots': [('cores', int, lambda: 1, False), ('memory', int, lambda: 128, False)],
 'getImages': [],
 'copyImage': [('src', str, lambda: requiredArg('src'),True), ('dst', str, lambda: requiredArg('dst'), True)],
+'cloneImage': [('src', str, lambda: requiredArg('src'),True), ('dst', str, lambda: requiredArg('dst'), True)],
+'rebaseImage': [('src', str, lambda: requiredArg('src'),True), ('dst', str, lambda: requiredArg('dst'), True)],
 'getHosts': [],
 'getUsers': [],
 'getNetworks': [],
@@ -311,6 +315,8 @@
 'vmmSpecificCall': '[instance, arg]',
 'getSlots' : '[cores, memory]',
 'copyImage' : '[src, dst]',
+'cloneImage' : '[src, dst]',
+'rebaseImage' : '[src, dst]',
 }
 
 # Descriptions
@@ -336,6 +342,8 @@
 'vmmSpecificCall': 'Direct access to VM manager specific functionality',
 'getImages' : 'Gets a list of available VM images',
 'copyImage' : 'Copies a VM image',
+'cloneImage' : 'Creates a clone based on an existing image',
+'rebaseImage' : 'Merge changes from a cloned image to another image',
 }
 
 # Example use strings
@@ -360,6 +368,8 @@
 'getVmLayout': [''],
 'getImages': [''],
 'copyImage': ['--src src.qcow2 --dst dst.qcow2'],
+'cloneImage': ['--src src.qcow2 --dst dst-clone.qcow2'],
+'rebaseImage': ['--src src-clone.qcow2 --dst dst-newimage.qcow2'],
 'vmmSpecificCall': ['--instance 12345 --arg startVnc', '--instance vmname --arg stopVnc'],
 }
 
diff --git a/src/tashi/clustermanager/clustermanagerservice.py b/src/tashi/clustermanager/clustermanagerservice.py
index e5b14b7..73fcf52 100644
--- a/src/tashi/clustermanager/clustermanagerservice.py
+++ b/src/tashi/clustermanager/clustermanagerservice.py
@@ -13,7 +13,7 @@
 # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
-# under the License.    
+# under the License.	
 
 import logging
 import threading
@@ -21,6 +21,7 @@
 
 from tashi.rpycservices.rpyctypes import Errors, InstanceState, Instance, HostState, TashiException
 from tashi import boolean, ConnectionManager, vmStates, hostStates, version, scrubString
+from tashi.dfs.diskimage import QemuImage
 
 class ClusterManagerService(object):
 	"""RPC service for the ClusterManager"""
@@ -66,13 +67,15 @@
 
 		threading.Thread(name="monitorCluster", target=self.__monitorCluster).start()
 
+		self.qemuImage = QemuImage(self.config)
+
 	def __initAccounting(self):
 		self.accountBuffer = []
 		self.accountLines = 0
 		self.accountingClient = None
 		try:
 			if (self.accountingHost is not None) and \
-				    (self.accountingPort is not None):
+					(self.accountingPort is not None):
 				self.accountingClient = ConnectionManager(self.username, self.password, self.accountingPort)[self.accountingHost]
 		except:
 			self.log.exception("Could not init accounting")
@@ -696,6 +699,26 @@
 		except Exception, e:
 			self.log.exception('DFS image copy failed: %s (%s->%s)' % (e, imageSrc, imageDst))
 
+    # extern
+	def cloneImage(self, src, dst):
+		imageSrc = self.dfs.getLocalHandle("images/" + src)
+		imageDst = self.dfs.getLocalHandle("images/" + dst)
+		self.log.info('DFS image clone: %s->%s' % (imageSrc, imageDst))
+		try:
+			self.qemuImage.cloneImage(imageSrc, imageDst)
+		except Exception, e:
+			self.log.info('DFS image clone error: %s' % (e))
+
+    # extern
+	def rebaseImage(self, src, dst):
+		imageSrc = self.dfs.getLocalHandle("images/" + src)
+		imageDst = self.dfs.getLocalHandle("images/" + dst)
+		self.log.info('DFS image rebase: %s->%s' % (imageSrc, imageDst))
+		try:
+			self.qemuImage.rebaseImage(imageSrc, imageDst)
+		except Exception, e:
+			self.log.info('DFS image rebase error: %s' % (e))
+
 	# extern
 	def vmmSpecificCall(self, instanceId, arg):
 		instance = self.data.getInstance(instanceId)
diff --git a/src/tashi/dfs/diskimage.py b/src/tashi/dfs/diskimage.py
new file mode 100644
index 0000000..929156b
--- /dev/null
+++ b/src/tashi/dfs/diskimage.py
@@ -0,0 +1,46 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+import logging
+import subprocess
+
+from tashi.dfs.diskimageinterface import DiskImageInterface
+
+class QemuImage(DiskImageInterface):
+    '''  This class implements the image creation for qemu/kvm '''
+
+    def __init__(self, config):
+        DiskImageInterface.__init__(self, config)
+        self.log = logging.getLogger(__name__)
+        self.prefix = self.config.get("Vfs", "prefix")
+        
+
+    def cloneImage(self, srcImage, dstImage):
+        self.log.info("Cloning image %s -> %s" % (srcImage, dstImage))
+        cmd = "/usr/bin/qemu-img create -b %s -f qcow2 %s" % (srcImage, dstImage)
+        p = subprocess.Popen(cmd.split(), executable=cmd.split()[0], stdout=subprocess.PIPE) 
+        self.log.info("Finished cloning image %s" % (p.stdout.readlines()))
+
+    def rebaseImage(self, srcImage, dstImage):
+        self.log.info("Rebasing image %s -> %s" % (srcImage, dstImage))
+        cmd = "/usr/bin/qemu-img convert %s -O qcow2 %s" % (srcImage, dstImage)
+        p = subprocess.Popen(cmd.split(), executable=cmd.split()[0], stdout=subprocess.PIPE) 
+        self.log.info("Finished rebasing image %s" % (p.stdout.readlines()))
+
+    def getImageInfo(self, srcImage):
+        cmd = "/usr/bin/qemu-img info %s" % srcImage
+        subprocess.Popen(cmd.split(), executable=cmd.split()[0], stdout=subprocess.PIPE) 
diff --git a/src/tashi/dfs/diskimageinterface.py b/src/tashi/dfs/diskimageinterface.py
new file mode 100644
index 0000000..2c4bce2
--- /dev/null
+++ b/src/tashi/dfs/diskimageinterface.py
@@ -0,0 +1,31 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+# 
+#   http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.    
+
+class DiskImageInterface():
+	def __init__(self, config):
+		if self.__class__ is DiskImageInterface:
+			raise NotImplementedError
+		self.config = config
+	
+	def cloneImage(self, srcImage, dstImage):
+		raise NotImplementedError
+	
+	def rebaseImage(self, srcImage, dstImage):
+		raise NotImplementedError
+	
+	def getImageInfo(self, srcImage, dstImage):
+		raise NotImplementedError
diff --git a/src/tashi/rpycservices/rpycservices.py b/src/tashi/rpycservices/rpycservices.py
index 0f760fc..5f47de4 100644
--- a/src/tashi/rpycservices/rpycservices.py
+++ b/src/tashi/rpycservices/rpycservices.py
@@ -19,7 +19,7 @@
 from tashi.rpycservices.rpyctypes import Instance, Host, User
 import cPickle
 
-clusterManagerRPCs = ['createVm', 'shutdownVm', 'destroyVm', 'suspendVm', 'resumeVm', 'migrateVm', 'pauseVm', 'unpauseVm', 'getHosts', 'getNetworks', 'getUsers', 'getInstances', 'vmmSpecificCall', 'registerNodeManager', 'vmUpdate', 'activateVm', 'registerHost', 'unregisterHost', 'getImages', 'copyImage', 'setHostState', 'setHostNotes', 'addReservation', 'delReservation', 'getReservation']
+clusterManagerRPCs = ['createVm', 'shutdownVm', 'destroyVm', 'suspendVm', 'resumeVm', 'migrateVm', 'pauseVm', 'unpauseVm', 'getHosts', 'getNetworks', 'getUsers', 'getInstances', 'vmmSpecificCall', 'registerNodeManager', 'vmUpdate', 'activateVm', 'registerHost', 'unregisterHost', 'getImages', 'copyImage', 'cloneImage', 'rebaseImage', 'setHostState', 'setHostNotes', 'addReservation', 'delReservation', 'getReservation']
 nodeManagerRPCs = ['instantiateVm', 'shutdownVm', 'destroyVm', 'suspendVm', 'resumeVm', 'prepReceiveVm', 'prepSourceVm', 'migrateVm', 'receiveVm', 'pauseVm', 'unpauseVm', 'getVmInfo', 'listVms', 'vmmSpecificCall', 'getHostInfo', 'liveCheck']
 accountingRPCs = ['record']