#!/usr/bin/env python3 # 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. # Version @VERSION@ # # A plugin for executing script needed by vmops cloud import os, sys, time import XenAPIPlugin if os.path.exists("/opt/xensource/sm"): sys.path.extend(["/opt/xensource/sm/", "/usr/local/sbin/", "/sbin/"]) if os.path.exists("/usr/lib/xcp/sm"): sys.path.extend(["/usr/lib/xcp/sm/", "/usr/local/sbin/", "/sbin/"]) import SR, VDI, SRCommand, util, lvutil from util import CommandException import vhdutil import shutil import lvhdutil import errno import subprocess import xs_errors import cleanup import stat import random import cloudstack_pluginlib as lib import logging lib.setup_logging("/var/log/cloud/cloud.log") VHDUTIL = "vhd-util" VHD_PREFIX = 'VHD-' CLOUD_DIR = '/var/run/cloud_mount' def echo(fn): def wrapped(*v, **k): name = fn.__name__ logging.debug("#### CLOUD enter %s ####" % name ) res = fn(*v, **k) logging.debug("#### CLOUD exit %s ####" % name ) return res return wrapped @echo def create_secondary_storage_folder(session, args): local_mount_path = None logging.debug("create_secondary_storage_folder, args: " + str(args)) try: try: # Mount the remote resource folder locally remote_mount_path = args["remoteMountPath"] local_mount_path = os.path.join(CLOUD_DIR, util.gen_uuid()) nfsVersion = args["nfsVersion"] mount(remote_mount_path, local_mount_path, nfsVersion) # Create the new folder new_folder = local_mount_path + "/" + args["newFolder"] if not os.path.isdir(new_folder): current_umask = os.umask(0) os.makedirs(new_folder) os.umask(current_umask) except OSError as e: errMsg = "create_secondary_storage_folder failed: errno: " + str(e.errno) + ", strerr: " + e.strerror logging.debug(errMsg) raise xs_errors.XenError(errMsg) except: errMsg = "create_secondary_storage_folder failed." logging.debug(errMsg) raise xs_errors.XenError(errMsg) finally: if local_mount_path != None: # Unmount the local folder umount(local_mount_path) # Remove the local folder os.system("rmdir " + local_mount_path) return "1" @echo def delete_secondary_storage_folder(session, args): local_mount_path = None logging.debug("delete_secondary_storage_folder, args: " + str(args)) try: try: # Mount the remote resource folder locally remote_mount_path = args["remoteMountPath"] local_mount_path = os.path.join(CLOUD_DIR, util.gen_uuid()) nfsVersion = args["nfsVersion"] mount(remote_mount_path, local_mount_path, nfsVersion) # Delete the specified folder folder = local_mount_path + "/" + args["folder"] if os.path.isdir(folder): os.system("rm -f " + folder + "/*") os.system("rmdir " + folder) except OSError as e: errMsg = "delete_secondary_storage_folder failed: errno: " + str(e.errno) + ", strerr: " + e.strerror logging.debug(errMsg) raise xs_errors.XenError(errMsg) except: errMsg = "delete_secondary_storage_folder failed." logging.debug(errMsg) raise xs_errors.XenError(errMsg) finally: if local_mount_path != None: # Unmount the local folder umount(local_mount_path) # Remove the local folder os.system("rmdir " + local_mount_path) return "1" @echo def post_create_private_template(session, args): local_mount_path = None try: try: # get local template folder templatePath = args["templatePath"] local_mount_path = os.path.join(CLOUD_DIR, util.gen_uuid()) nfsVersion = args["nfsVersion"] mount(templatePath, local_mount_path, nfsVersion) # Retrieve args filename = args["templateFilename"] name = args["templateName"] description = args["templateDescription"] checksum = args["checksum"] file_size = args["size"] virtual_size = args["virtualSize"] template_id = args["templateId"] # Create the template.properties file template_properties_install_path = local_mount_path + "/template.properties" f = open(template_properties_install_path, "w") f.write("filename=" + filename + "\n") f.write("vhd=true\n") f.write("id=" + template_id + "\n") f.write("vhd.filename=" + filename + "\n") f.write("public=false\n") f.write("uniquename=" + name + "\n") f.write("vhd.virtualsize=" + virtual_size + "\n") f.write("virtualsize=" + virtual_size + "\n") f.write("checksum=" + checksum + "\n") f.write("hvm=true\n") f.write("description=" + description + "\n") f.write("vhd.size=" + str(file_size) + "\n") f.write("size=" + str(file_size) + "\n") f.close() logging.debug("Created template.properties file") # Set permissions permissions = stat.S_IREAD | stat.S_IWRITE | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH os.chmod(template_properties_install_path, permissions) logging.debug("Set permissions on template and template.properties") except: errMsg = "post_create_private_template failed." logging.debug(errMsg) raise xs_errors.XenError(errMsg) finally: if local_mount_path != None: # Unmount the local folder umount(local_mount_path) # Remove the local folder os.system("rmdir " + local_mount_path) return "1" def isfile(path, isISCSI): errMsg = '' exists = True if isISCSI: exists = checkVolumeAvailability(path) else: exists = os.path.isfile(path) if not exists: errMsg = "File " + path + " does not exist." logging.debug(errMsg) raise xs_errors.XenError(errMsg) return errMsg def copyfile(fromFile, toFile, isISCSI): logging.debug("Starting to copy " + fromFile + " to " + toFile) errMsg = '' if isISCSI: bs = "4M" else: bs = "128k" try: cmd = ['dd', 'if=' + fromFile, 'iflag=direct', 'of=' + toFile, 'oflag=direct', 'bs=' + bs] txt = util.pread2(cmd) except: try: os.system("rm -f " + toFile) except: txt = '' txt = '' errMsg = "Error while copying " + fromFile + " to " + toFile + " in secondary storage" logging.debug(errMsg) raise xs_errors.XenError(errMsg) logging.debug("Successfully copied " + fromFile + " to " + toFile) return errMsg def chdir(path): try: os.chdir(path) except OSError as e: errMsg = "Unable to chdir to " + path + " because of OSError with errno: " + str(e.errno) + " and strerr: " + e.strerror logging.debug(errMsg) raise xs_errors.XenError(errMsg) logging.debug("Chdired to " + path) return def scanParent(path): # Do a scan for the parent for ISCSI volumes # Note that the parent need not be visible on the XenServer parentUUID = '' try: lvName = os.path.basename(path) dirname = os.path.dirname(path) vgName = os.path.basename(dirname) vhdInfo = vhdutil.getVHDInfoLVM(lvName, lvhdutil.extractUuid, vgName) parentUUID = vhdInfo.parentUuid except: errMsg = "Could not get vhd parent of " + path logging.debug(errMsg) raise xs_errors.XenError(errMsg) return parentUUID def getParent(path, isISCSI): parentUUID = '' try : if isISCSI: parentUUID = vhdutil.getParent(path, lvhdutil.extractUuid) else: parentUUID = vhdutil.getParent(path, cleanup.FileVDI.extractUuid) except: errMsg = "Could not get vhd parent of " + path logging.debug(errMsg) raise xs_errors.XenError(errMsg) return parentUUID def getParentOfSnapshot(snapshotUuid, primarySRPath, isISCSI): snapshotVHD = getVHD(snapshotUuid, isISCSI) snapshotPath = os.path.join(primarySRPath, snapshotVHD) baseCopyUuid = '' if isISCSI: checkVolumeAvailability(snapshotPath) baseCopyUuid = scanParent(snapshotPath) else: baseCopyUuid = getParent(snapshotPath, isISCSI) logging.debug("Base copy of snapshotUuid: " + snapshotUuid + " is " + baseCopyUuid) return baseCopyUuid def setParent(parent, child): try: cmd = [VHDUTIL, "modify", "-p", parent, "-n", child] txt = util.pread2(cmd) except: errMsg = "Unexpected error while trying to set parent of " + child + " to " + parent logging.debug(errMsg) raise xs_errors.XenError(errMsg) logging.debug("Successfully set parent of " + child + " to " + parent) return def rename(originalVHD, newVHD): try: os.rename(originalVHD, newVHD) except OSError as e: errMsg = "OSError while renaming " + origiinalVHD + " to " + newVHD + "with errno: " + str(e.errno) + " and strerr: " + e.strerror logging.debug(errMsg) raise xs_errors.XenError(errMsg) return def makedirs(path): if not os.path.isdir(path): try: os.makedirs(path) except OSError as e: umount(path) if os.path.isdir(path): return errMsg = "OSError while creating " + path + " with errno: " + str(e.errno) + " and strerr: " + e.strerror logging.debug(errMsg) raise xs_errors.XenError(errMsg) return def mount(remoteDir, localDir, nfsVersion=None): makedirs(localDir) options = "soft,tcp,timeo=133,retrans=1" if nfsVersion: options += ",vers=" + nfsVersion try: cmd = ['mount', '-o', options, remoteDir, localDir] txt = util.pread2(cmd) except: txt = '' errMsg = "Unexpected error while trying to mount " + remoteDir + " to " + localDir logging.debug(errMsg) raise xs_errors.XenError(errMsg) logging.debug("Successfully mounted " + remoteDir + " to " + localDir) return def umount(localDir): try: cmd = ['umount', localDir] util.pread2(cmd) except CommandException: errMsg = "CommandException raised while trying to umount " + localDir logging.debug(errMsg) raise xs_errors.XenError(errMsg) logging.debug("Successfully unmounted " + localDir) return def mountSnapshotsDir(secondaryStorageMountPath, localMountPointPath, path): # The aim is to mount secondaryStorageMountPath on # And create / dir on it, if it doesn't exist already. # Assuming that secondaryStorageMountPath exists remotely # Just mount secondaryStorageMountPath//SecondaryStorageHost/ everytime # Never unmount. # path is like "snapshots/account/volumeId", we mount secondary_storage:/snapshots relativeDir = path.split("/")[0] restDir = "/".join(path.split("/")[1:]) snapshotsDir = os.path.join(secondaryStorageMountPath, relativeDir) makedirs(localMountPointPath) # if something is not mounted already on localMountPointPath, # mount secondaryStorageMountPath on localMountPath if os.path.ismount(localMountPointPath): # There is more than one secondary storage per zone. # And we are mounting each sec storage under a zone-specific directory # So two secondary storage snapshot dirs will never get mounted on the same point on the same XenServer. logging.debug("The remote snapshots directory has already been mounted on " + localMountPointPath) else: mount(snapshotsDir, localMountPointPath) # Create accountId/instanceId dir on localMountPointPath, if it doesn't exist backupsDir = os.path.join(localMountPointPath, restDir) makedirs(backupsDir) return backupsDir def unmountAll(path): try: for dir in os.listdir(path): if dir.isdigit(): logging.debug("Unmounting Sub-Directory: " + dir) localMountPointPath = os.path.join(path, dir) umount(localMountPointPath) except: logging.debug("Ignoring the error while trying to unmount the snapshots dir") @echo def unmountSnapshotsDir(session, args): dcId = args['dcId'] localMountPointPath = os.path.join(CLOUD_DIR, dcId) localMountPointPath = os.path.join(localMountPointPath, "snapshots") unmountAll(localMountPointPath) try: umount(localMountPointPath) except: logging.debug("Ignoring the error while trying to unmount the snapshots dir.") return "1" def getPrimarySRPath(primaryStorageSRUuid, isISCSI): if isISCSI: primarySRDir = lvhdutil.VG_PREFIX + primaryStorageSRUuid return os.path.join(lvhdutil.VG_LOCATION, primarySRDir) else: return os.path.join(SR.MOUNT_BASE, primaryStorageSRUuid) def getBackupVHD(UUID): return UUID + '.' + SR.DEFAULT_TAP def getVHD(UUID, isISCSI): if isISCSI: return VHD_PREFIX + UUID else: return UUID + '.' + SR.DEFAULT_TAP def getIsTrueString(stringValue): booleanValue = False if (stringValue and stringValue == 'true'): booleanValue = True return booleanValue def makeUnavailable(uuid, primarySRPath, isISCSI): if not isISCSI: return VHD = getVHD(uuid, isISCSI) path = os.path.join(primarySRPath, VHD) manageAvailability(path, '-an') return def manageAvailability(path, value): if path.__contains__("/var/run/sr-mount"): return logging.debug("Setting availability of " + path + " to " + value) try: cmd = ['/usr/sbin/lvchange', value, path] util.pread2(cmd) except: #CommandException, (rc, cmdListStr, stderr): #errMsg = "CommandException thrown while executing: " + cmdListStr + " with return code: " + str(rc) + " and stderr: " + stderr errMsg = "Unexpected exception thrown by lvchange" logging.debug(errMsg) if value == "-ay": # Raise an error only if we are trying to make it available. # Just warn if we are trying to make it unavailable after the # snapshot operation is done. raise xs_errors.XenError(errMsg) return def checkVolumeAvailability(path): try: if not isVolumeAvailable(path): # The VHD file is not available on XenSever. The volume is probably # inactive or detached. # Do lvchange -ay to make it available on XenServer manageAvailability(path, '-ay') except: errMsg = "Could not determine status of ISCSI path: " + path logging.debug(errMsg) raise xs_errors.XenError(errMsg) success = False i = 0 while i < 6: i = i + 1 # Check if the vhd is actually visible by checking for the link # set isISCSI to true success = isVolumeAvailable(path) if success: logging.debug("Made vhd: " + path + " available and confirmed that it is visible") break # Sleep for 10 seconds before checking again. time.sleep(10) # If not visible within 1 min fail if not success: logging.debug("Could not make vhd: " + path + " available despite waiting for 1 minute. Does it exist?") return success def isVolumeAvailable(path): # Check if iscsi volume is available on this XenServer. status = "0" try: p = subprocess.Popen(["/bin/bash", "-c", "if [ -L " + path + " ]; then echo 1; else echo 0;fi"], stdout=subprocess.PIPE) status = p.communicate()[0].strip("\n") except: errMsg = "Could not determine status of ISCSI path: " + path logging.debug(errMsg) raise xs_errors.XenError(errMsg) return (status == "1") def getVhdParent(session, args): logging.debug("getParent with " + str(args)) primaryStorageSRUuid = args['primaryStorageSRUuid'] snapshotUuid = args['snapshotUuid'] isISCSI = getIsTrueString(args['isISCSI']) primarySRPath = getPrimarySRPath(primaryStorageSRUuid, isISCSI) logging.debug("primarySRPath: " + primarySRPath) baseCopyUuid = getParentOfSnapshot(snapshotUuid, primarySRPath, isISCSI) return baseCopyUuid def getSnapshotSize(session, args): primaryStorageSRUuid = args['primaryStorageSRUuid'] snapshotUuid = args['snapshotUuid'] isISCSI = getIsTrueString(args['isISCSI']) primarySRPath = getPrimarySRPath(primaryStorageSRUuid, isISCSI) logging.debug("primarySRPath: " + primarySRPath) snapshotVHD = getVHD(snapshotUuid, isISCSI) snapshotPath = os.path.join(primarySRPath, snapshotVHD) physicalSize = vhdutil.getSizePhys(snapshotPath) return str(physicalSize) def backupSnapshot(session, args): logging.debug("Called backupSnapshot with " + str(args)) primaryStorageSRUuid = args['primaryStorageSRUuid'] secondaryStorageMountPath = args['secondaryStorageMountPath'] snapshotUuid = args['snapshotUuid'] prevBackupUuid = args['prevBackupUuid'] backupUuid = args['backupUuid'] isISCSI = getIsTrueString(args['isISCSI']) path = args['path'] localMountPoint = args['localMountPoint'] primarySRPath = getPrimarySRPath(primaryStorageSRUuid, isISCSI) logging.debug("primarySRPath: " + primarySRPath) baseCopyUuid = getParentOfSnapshot(snapshotUuid, primarySRPath, isISCSI) baseCopyVHD = getVHD(baseCopyUuid, isISCSI) baseCopyPath = os.path.join(primarySRPath, baseCopyVHD) logging.debug("Base copy path: " + baseCopyPath) # Mount secondary storage mount path on XenServer along the path # /var/run/sr-mount//snapshots/ and create / dir # on it. backupsDir = mountSnapshotsDir(secondaryStorageMountPath, localMountPoint, path) logging.debug("Backups dir " + backupsDir) prevBackupUuid = prevBackupUuid.split("/")[-1] # Check existence of snapshot on primary storage isfile(baseCopyPath, isISCSI) physicalSize = vhdutil.getSizePhys(baseCopyPath) if prevBackupUuid: # Check existence of prevBackupFile prevBackupVHD = getBackupVHD(prevBackupUuid) prevBackupFile = os.path.join(backupsDir, prevBackupVHD) isfile(prevBackupFile, False) # copy baseCopyPath to backupsDir with new uuid backupVHD = getBackupVHD(backupUuid) backupFile = os.path.join(backupsDir, backupVHD) logging.debug("Back up " + baseCopyUuid + " to Secondary Storage as " + backupUuid) copyfile(baseCopyPath, backupFile, isISCSI) vhdutil.setHidden(backupFile, False) # Because the primary storage is always scanned, the parent of this base copy is always the first base copy. # We don't want that, we want a chain of VHDs each of which is a delta from the previous. # So set the parent of the current baseCopyVHD to prevBackupVHD if prevBackupUuid: # If there was a previous snapshot setParent(prevBackupFile, backupFile) txt = "1#" + backupUuid + "#" + str(physicalSize) return txt @echo def deleteSnapshotBackup(session, args): logging.debug("Calling deleteSnapshotBackup with " + str(args)) secondaryStorageMountPath = args['secondaryStorageMountPath'] backupUUID = args['backupUUID'] path = args['path'] localMountPoint = args['localMountPoint'] backupsDir = mountSnapshotsDir(secondaryStorageMountPath, localMountPoint, path) # chdir to the backupsDir for convenience chdir(backupsDir) backupVHD = getBackupVHD(backupUUID) logging.debug("checking existence of " + backupVHD) # The backupVHD is on secondary which is NFS and not ISCSI. if not os.path.isfile(backupVHD): logging.debug("backupVHD " + backupVHD + "does not exist. Not trying to delete it") return "1" logging.debug("backupVHD " + backupVHD + " exists.") # Just delete the backupVHD try: os.remove(backupVHD) except OSError as e: errMsg = "OSError while removing " + backupVHD + " with errno: " + str(e.errno) + " and strerr: " + e.strerror logging.debug(errMsg) raise xs_errors.XenError(errMsg) return "1" @echo def revert_memory_snapshot(session, args): logging.debug("Calling revert_memory_snapshot with " + str(args)) vmName = args['vmName'] snapshotUUID = args['snapshotUUID'] oldVmUuid = args['oldVmUuid'] snapshotMemory = args['snapshotMemory'] hostUUID = args['hostUUID'] try: cmd = '''xe vbd-list vm-uuid=%s | grep 'vdi-uuid' | grep -v 'not in database' | sed -e 's/vdi-uuid ( RO)://g' ''' % oldVmUuid vdiUuids = os.popen(cmd).read().split() cmd2 = '''xe vm-param-get param-name=power-state uuid=''' + oldVmUuid if os.popen(cmd2).read().split()[0] != 'halted': os.system("xe vm-shutdown force=true vm=" + vmName) os.system("xe vm-destroy uuid=" + oldVmUuid) os.system("xe snapshot-revert snapshot-uuid=" + snapshotUUID) if snapshotMemory == 'true': os.system("xe vm-resume vm=" + vmName + " on=" + hostUUID) for vdiUuid in vdiUuids: os.system("xe vdi-destroy uuid=" + vdiUuid) except OSError as e: errMsg = "OSError while reverting vm " + vmName + " to snapshot " + snapshotUUID + " with errno: " + str(e.errno) + " and strerr: " + e.strerror logging.debug(errMsg) raise xs_errors.XenError(errMsg) return "0" if __name__ == "__main__": XenAPIPlugin.dispatch({"getVhdParent":getVhdParent, "create_secondary_storage_folder":create_secondary_storage_folder, "delete_secondary_storage_folder":delete_secondary_storage_folder, "post_create_private_template":post_create_private_template, "backupSnapshot": backupSnapshot, "deleteSnapshotBackup": deleteSnapshotBackup, "unmountSnapshotsDir": unmountSnapshotsDir, "revert_memory_snapshot":revert_memory_snapshot, "getSnapshotSize":getSnapshotSize})