Repository URL to install this package:
|
Version:
4.0.115 ▾
|
import subprocess
import os
import re
import time
from subprocess import call
from subprocess import check_call
from subprocess import check_output
import shutil
from os import remove, close
from contextlib import contextmanager
from queue import Queue
from queue import Queue, Empty
from threading import Thread
from tempfile import mkstemp
from workloadmgr import exception
from workloadmgr.openstack.common import log as logging
from workloadmgr.openstack.common.gettextutils import _
from workloadmgr import autolog
LOG = logging.getLogger(__name__)
Logger = autolog.Logger(LOG)
###
# This file assumes that vmdk has MBR partition table. It iterates thru
# all the partitions in the partition table and for each partition, it
# discovers the file system and copies only the blocks that were allocated
# by the file system.
# Here is the algorithm to detect the blocks that were allocated by the
# file system without reading the entire disk.
# 1. The algorithm works for extx class of file systems and assumes
# intimate knowledge of the file system. We will leverage debugfs
# (a slightly modified version) to explore the internal datastructures
# of the file system.
# 2. First it copies the superblock of the file system but copying
# part['start'], 400 blocks of extent from remote vmdk to local disk
# 3. Using debugfs, it explores the block size and number of block groups
# of the file system
# 4. It then copies inode blocks and bitmap blocks from each block group
# 5. Using debugfs it opens the file system, identifies only allocated
# blocks from the bitmap and copies those blocks from the remote
# disk
##
"""
def _(*args):
return str(args[0])
class log():
def info(self, *arg):
print(arg[0])
def exception(self, *arg):
print(arg[0])
def error(self, *arg):
print(arg[0])
def debug(self, *arg):
print(arg[0])
def critical(self, *arg):
print(arg[0])
LOG = log()
"""
##
# create_empty_vmdk() function as the name suggests creates an empty vmdk
# file at path specified by filepath. the size of the vmdk file is
# specified by capacity
##
def create_empty_vmdk(filepath, capacity):
vix_disk_lib_env = os.environ.copy()
vix_disk_lib_env['LD_LIBRARY_PATH'] = '/usr/lib/vmware-vix-disklib/lib64'
# Create empty vmdk file
try:
cmdline = "trilio-vix-disk-cli -create "
cmdline += "-cap " + str(capacity / (1024 * 1024))
cmdline += " " + filepath
check_output(
cmdline.split(" "),
stderr=subprocess.STDOUT,
env=vix_disk_lib_env)
except subprocess.CalledProcessError as ex:
LOG.critical(_("cmd: %s resulted in error: %s") % (cmdline, ex.output))
LOG.exception(ex)
raise
def enqueue_output(out, queue):
for line in iter(out.readline, b''):
queue.put(line)
out.close()
##
# mount_local_vmdk() mounts the vmdk at the path specified. It returns the
# mounted path
##
@contextmanager
def mount_local_vmdk(diskslist, mntlist, diskonly=False):
vix_disk_lib_env = os.environ.copy()
vix_disk_lib_env['LD_LIBRARY_PATH'] = '/usr/lib/vmware-vix-disklib/lib64'
try:
vmdkfiles = []
with open(diskslist, 'r') as f:
for line in f.read().split():
vmdkfiles.append(line.rstrip().strip())
processes = []
mountpoints = {}
for vmdkfile in vmdkfiles:
try:
fileh, listfile = mkstemp()
close(fileh)
with open(listfile, 'w') as f:
f.write(vmdkfile)
cmdspec = [
"trilio-vix-disk-cli",
"-mount",
"-mountpointsfile",
mntlist,
]
if diskonly:
cmdspec += ['-diskonly']
cmdspec += [listfile]
cmd = " ".join(cmdspec)
LOG.info(_(cmd))
process = subprocess.Popen(cmdspec,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=-1,
env=vix_disk_lib_env,
close_fds=True,
shell=False)
queue = Queue()
read_thread = Thread(
target=enqueue_output, args=(
process.stdout, queue))
read_thread.daemon = True # thread dies with the program
read_thread.start()
mountpath = None
while process.poll() is None:
try:
try:
output = queue.get(timeout=5)
LOG.info(_(output))
except Empty:
continue
except Exception as ex:
LOG.exception(ex)
if output.startswith(
"Pausing the process until it is resumed"):
break
except Exception as ex:
LOG.exception(ex)
if not process.poll() is None:
_returncode = process.returncode # pylint: disable=E1101
if _returncode:
LOG.debug(_('Result was %s') % _returncode)
raise exception.ProcessExecutionError(
exit_code=_returncode,
stderr=process.stderr.read(),
cmd=cmd)
with open(mntlist, 'r') as f:
for line in f:
line = line.strip("\n")
mountpoints[line.split(":")[0]] = line.split(":")[
1].split(";")
LOG.info(_(mountpoints))
process.stdin.close()
processes.append(process)
except Exception as ex:
LOG.exception(ex)
raise
finally:
if os.path.isfile(listfile):
os.remove(listfile)
yield mountpoints
except Exception as ex:
LOG.exception(ex)
raise
finally:
umount_local_vmdk(processes)
##
# unmounts a vmdk by sending signal 18 to the process that as suspended during mount process
##
def umount_local_vmdk(processes):
for process in processes:
process.send_signal(18)
process.wait()
_returncode = process.returncode # pylint: disable=E1101
if _returncode != 0:
LOG.debug(_('Result was %s') % _returncode)
raise exception.ProcessExecutionError(
exit_code=_returncode,
stderr=process.stderr.read())
def execute_cmd(cmdspec, env=None):
process = subprocess.Popen(cmdspec,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=-1,
env=env,
close_fds=True,
shell=False)
# TODO: throw exception on error?
output, error = process.communicate()
if process.returncode:
raise exception.ProcessExecutionError(
cmd=" ".join(cmdspec),
description=error,
exit_code=process.returncode)
return output, error
def execute_debugfs(cmdspec):
# Read the superblock and read the blocksize
debugfs_env = {}
debugfs_env['DEBUGFS_PAGER'] = '__none__'
return execute_cmd(cmdspec, debugfs_env)
def populate_extents(hostip, username, password, vmspec, remotepath,
mountpath, extentsfile):
# copy the boot record to a file
vix_disk_lib_env = os.environ.copy()
vix_disk_lib_env['LD_LIBRARY_PATH'] = '/usr/lib/vmware-vix-disklib/lib64'
try:
cmdspec = ["trilio-vix-disk-cli", "-downloadextents",
remotepath, "-extentfile", extentsfile,
"-host", hostip,
"-user", username,
"-password", password,
"-vm", vmspec,
mountpath]
check_output(cmdspec, stderr=subprocess.STDOUT, env=vix_disk_lib_env)
except subprocess.CalledProcessError as ex:
LOG.critical(_("cmd: %s resulted in error: %s") % (cmdspec, ex.output))
LOG.exception(ex)
raise
##
# Copy an extent specified by start/count from remote disk to
# local disk.
##
def populate_extent(hostip, username, password, vmspec, remotepath,
mountpath, start, count):
# copy the boot record to a file
vix_disk_lib_env = os.environ.copy()
vix_disk_lib_env['LD_LIBRARY_PATH'] = '/usr/lib/vmware-vix-disklib/lib64'
try:
cmdline = ["trilio-vix-disk-cli",
"-download", remotepath,
"-host", hostip,
"-user", username,
"-password", password,
"-vm", vmspec,
"-start", str(start),
"-count", str(count),
mountpath, ]
check_output(cmdline, stderr=subprocess.STDOUT, env=vix_disk_lib_env)
except subprocess.CalledProcessError as ex:
LOG.critical(_("cmd: %s resulted in error: %s") % (cmdline, ex.output))
LOG.exception(ex)
raise
def populate_bootrecord(hostip, username, password, vmspec, remotepath,
mountpath, diskCapacity):
diskCapacity = int(diskCapacity) - 400 * 512
populate_extent(hostip, username, password, vmspec, remotepath,
mountpath, 0, 400)
# GPT disks has second boot record at the end of the disk
populate_extent(hostip, username, password, vmspec, remotepath,
mountpath, diskCapacity / 512, 400)
return
##
# getfdisk_output():
# if the underlying disk is fdisk, read the partition table from the mounted disk
#
##
def getfdisk_output(mountpath=None):
partitions = []
cmdspec = ["sudo", "fdisk", "-l", ]
if mountpath:
cmdspec.append(str(mountpath))
LOG.info(_(" ".join(cmdspec)))
#stdout_value = check_output(cmdspec, stderr=subprocess.STDOUT)
process = subprocess.Popen(cmdspec,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=-1,
close_fds=True,
shell=False)
stdout_value, stderr_value = process.communicate()
parse = False
for line in stdout_value.split("\n"):
if parse:
partition = {}
fields = line.split()
if (len(fields) == 0):
continue
index = 0
partition["Device Name"] = fields[index]
index += 1
if fields[index] == "*":
partition["Boot Device"] = str(True)
index += 1
else:
partition["Boot Device"] = str(False)
partition["start"] = fields[index]
index += 1
partition["end"] = fields[index]
index += 1
partition["blocks"] = fields[index].strip("+")
index += 1
partition["id"] = fields[index].lower()
index += 1
partition["system"] = " ".join(fields[index:])
index += 1
if len(re.findall(r'\d+', partition['start'])) and\
len(re.findall(r'\d+', partition['end'])) and\
len(re.findall(r'\d+', partition['blocks'])):
partitions.append(partition)
if "Device" in line and "Boot" in line:
parse = True
return partitions
##
# getgptdisk_output():
# if the underlying disk is gptdisk, read the partition table from
# the mounted disk
#
# Fdisk shows gpt partition as follows:
# stack@openstack01:~/gptsupport$ fdisk -l 1t.img
#
# WARNING: GPT (GUID Partition Table) detected on '1t.img'! The util fdisk doesn't support GPT. Use GNU Parted.
#
#
# Disk 1t.img: 1099.5 GB, 1099511627776 bytes
# 256 heads, 63 sectors/track, 133152 cylinders, total 2147483648 sectors
# Units = sectors of 1 * 512 = 512 bytes
# Sector size (logical/physical): 512 bytes / 512 bytes
# I/O size (minimum/optimal): 512 bytes / 512 bytes
# Disk identifier: 0x00000000
#
# Device Boot Start End Blocks Id System
# 1t.img1 1 2147483647 1073741823+ ee GPT
#
#
# A typical partition to support would be:
#
# stack@openstack01:~/gptsupport$ sgdisk -p 1t.img
# Disk 1t.img: 2147483648 sectors, 1024.0 GiB
# Logical sector size: 512 bytes
# Disk identifier (GUID): 006BE6CD-B6F1-4DBE-9CD5-8207D66A7BEE
# Partition table holds up to 128 entries
# First usable sector is 34, last usable sector is 2147483614
# Partitions will be aligned on 2048-sector boundaries
# Total free space is 900013290 sectors (429.2 GiB)
#
# Number Start (sector) End (sector) Size Code Name
# 1 2048 147483614 70.3 GiB 8300 First
# 2 147484672 157483614 4.8 GiB 8300
# 3 157485056 247483614 42.9 GiB 8300 Second
# 4 247484416 347483614 47.7 GiB 8300 Third
# 5 347484160 447483614 47.7 GiB 8300 Forth
# 6 447483904 547483614 47.7 GiB 8300 Fifth
# 7 547483648 647483614 47.7 GiB 8300 Sixth
# 8 647485440 747483614 47.7 GiB 8300 Seventh
# 9 747485184 847483614 47.7 GiB 8300 Eighth
# 10 847484928 947483614 47.7 GiB 8300
# 11 947484672 1047483614 47.7 GiB 8300 Ninth
# 12 1047484416 1147483614 47.7 GiB 8300 Tenth
# 13 1147484160 1247483614 47.7 GiB 8300 Eleventh
#
##
def getgptdisk_output(mountpath=None):
partitions = []
cmdspec = ["sudo", "sgdisk", "-p", ]
if mountpath:
cmdspec.append(str(mountpath))
LOG.info(_(" ".join(cmdspec)))
process = subprocess.Popen(cmdspec,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
bufsize=-1,
close_fds=True,
shell=False)
stdout_value, stderr_value = process.communicate()
parse = False
for line in stdout_value.split("\n"):
if parse:
partition = {}
fields = line.split()
if (len(fields) == 0):
continue
index = 0
partition["Number"] = fields[index]
index += 1
partition["start"] = fields[index]
index += 1
partition["end"] = fields[index]
index += 1
partition["blocks"] = str((int(partition['end']) -
int(partition['start']) + 1) / 2)
index += 2
partition["id"] = fields[index].lower()
index += 1
partition["system"] = " ".join(fields[index:])
index += 1
partitions.append(partition)
if "Number" in line and "Start" in line and "End" in line and \
"Size" in line and "Code" in line and "Name" in line:
parse = True
return partitions
def get_blockgroups(mountpath):
try:
# Read the superblock and read the blocksize
blocksize = 4096
blockgroups = ""
cmdspec = ["/opt/stack/workloadmgr/debugfs/debugfs",
"-R", "stats -h", mountpath]
superblock, error = execute_debugfs(cmdspec)
# get the block size
for line in superblock.split("\n"):
LOG.info(_(line))
if "Block size:" in line:
blocksize = int(line.split(":")[1].strip())
cmdspec = ["/opt/stack/workloadmgr/debugfs/debugfs",
"-R", "stats", mountpath]
blockgroups, error = execute_debugfs(cmdspec)
except Exception as ex:
LOG.exception(_(ex))
LOG.error(_("Cannot read block groups from %s") % mountpath)
return blocksize, blockgroups
def get_usedblockslist_from_part(mountpath, usedblockfile, part, blocksize):
totalblockscopied = 0
try:
cmdspec = ["/opt/stack/workloadmgr/debugfs/debugfs",
"-R", "stats -d", mountpath]
usedblocks, error = execute_debugfs(cmdspec)
except BaseException:
# we did not recognize a valid partition on the disk. Copy
# entire partition
LOG.info(_("No valid ext fs found on partitin starting at:" +
str(part['start'])))
startblk = 0
length = (int(part['blocks']) * 1024) / blocksize
usedblocks = "startblk " + str(startblk) + " length " + str(length)
partoff = int(part['start']) * 512
with open(usedblockfile, 'a') as f:
for line in usedblocks.split("\n"):
if "startblk" in line or not "length" not in line:
continue
extoff = int(re.findall(r'\d+', line)[0])
length = int(re.findall(r'\d+', line)[1])
totalblockscopied += length
extoffsec = partoff + extoff * blocksize
lengthsec = length * blocksize
f.write(str(extoffsec) + "," + str(lengthsec) + "\n")
return totalblockscopied
def get_usedblockslist_from_lv(mountpath, usedblockfiles, lv, pvinfo,
blocksize):
totalblockscopied = {}
try:
cmdspec = ["/opt/stack/workloadmgr/debugfs/debugfs",
"-R", "stats -d", "/dev/" + lv['LVM2_VG_NAME'] +
"/" + lv['LVM2_LV_NAME']]
usedblocks, error = execute_debugfs(cmdspec)
except BaseException:
# we did not recognize a valid file system on the lv.
# we probably need to bail out and backup the entire
# partition using cbt
LOG.info(_("No valid ext fs found on partitin starting on: /dev/" +
lv['LVM2_VG_NAME'] + "/" + lv['LVM2_LV_NAME']))
startblk = 0
length = int(lv['LVM2_LV_SIZE']) / blocksize
usedblocks = "startblk " + str(startblk) + " length " + str(length)
filehandles = {}
for key, value in usedblockfiles.items():
filehandles[key] = open(value, "a")
totalblockscopied[key] = 0
lvoffset = 0
for line in usedblocks.split("\n"):
if "startblk" in line or not "length" not in line:
continue
extoff = int(re.findall(r'\d+', line)[0])
length = int(re.findall(r'\d+', line)[1])
extoffsec = getlogicaladdrtopvaddr(
lv, pvinfo, extoff * blocksize, length * blocksize)
for extoff in extoffsec:
eoff = int(extoff['offset']) + int(extoff['pv']['PV_DISK_OFFSET'])
filehandles[extoff['pv']['filename']].write(
str(eoff) + "," + str(extoff['length']) + "\n")
totalblockscopied[extoff['pv']['filename']
] += int(extoff['length']) / blocksize
for key, value in usedblockfiles.items():
filehandles[key].close()
return totalblockscopied
##
# copy_free_bitmap_from_part():
# Creates an empty bitmap vmdk file with the name specified by localvmdkpath
##
def copy_free_bitmap_from_part(hostip, username, password, vmspec, filename,
mountpath, startsector,
blocksize, blockgroups):
# Convert the start offset from 512 size into blocksize
try:
if int(startsector) * 512 % blocksize:
LOG.info(_("The partition start %s is not aligned to \
file system block size") % startsector)
partoff = int(startsector) * 512
# copy bitmap blocks here
index = 0
fileh, bitmapfile = mkstemp()
close(fileh)
with open(bitmapfile, 'w') as f:
for line in blockgroups.split("\n"):
if "block bitmap at" in line and "inode bitmap" in line:
bitmapblock = int(
re.findall(
r'\d+',
line.split(":")[1])[0])
inodeblock = int(re.findall(r'\d+', line.split(":")[1])[1])
index += 1
if index % 50 == 0:
LOG.info(_("copying bitmapblock: " + str(bitmapblock)))
LOG.info(_("copying inodeblock: " + str(bitmapblock)))
bitmapsec = partoff + bitmapblock * blocksize
inodesec = partoff + inodeblock * blocksize
f.write(str(bitmapsec) + "," + str(blocksize) + "\n")
f.write(str(inodesec) + "," + str(blocksize) + "\n")
populate_extents(hostip, username, password, vmspec,
filename, mountpath, bitmapfile)
finally:
if os.path.isfile(bitmapfile):
os.remove(bitmapfile)
##
# copy_free_bitmap_from_lv():
# Creates an empty bitmap vmdk file with the name specified by localvmdkpath
##
def copy_free_bitmap_from_lv(hostip, username, password, vmspec, devmap,
lvinfo, pvlist, blocksize, blockgroups):
# copy bitmap blocks here
index = 0
bitmapfileh = {}
bitmapfiles = {}
for dmap in devmap:
filename = dmap['dev']['backing']['fileName']
fileh, bitmapfiles[filename] = mkstemp()
close(fileh)
bitmapfileh[filename] = open(bitmapfiles[filename], "w")
try:
for line in blockgroups.split("\n"):
if "block bitmap at" in line and "inode bitmap" in line:
bitmapblock = int(re.findall(r'\d+', line.split(":")[1])[0])
inodeblock = int(re.findall(r'\d+', line.split(":")[1])[1])
index += 1
if index % 50 == 0:
LOG.info(_("copying bitmapblock: " + str(bitmapblock)))
LOG.info(_("copying inodeblock: " + str(bitmapblock)))
bitmapsec = getlogicaladdrtopvaddr(
lvinfo, pvlist, bitmapblock * blocksize, blocksize)
inodesec = getlogicaladdrtopvaddr(
lvinfo, pvlist, inodeblock * blocksize, blocksize)
for bsec in bitmapsec:
boff = int(bsec['offset']) + \
int(bsec['pv']['PV_DISK_OFFSET'])
bitmapfileh[bsec['pv']['filename']].write(
str(boff) + "," + str(blocksize) + "\n")
for isec in inodesec:
ioff = int(isec['offset']) + \
int(isec['pv']['PV_DISK_OFFSET'])
bitmapfileh[isec['pv']['filename']].write(
str(ioff) + "," + str(blocksize) + "\n")
for dmap in devmap:
filename = dmap['dev']['backing']['fileName']
bitmapfileh[filename].close()
for dmap in devmap:
filename = bitmapfiles[dmap['dev']['backing']['fileName']]
populate_extents(hostip, username, password, vmspec,
dmap['dev']['backing']['fileName'],
dmap['localvmdkpath'], filename)
except Exception as ex:
LOG.exception(_(ex))
finally:
try:
for dmap in devmap:
filename = dmap['dev']['backing']['fileName']
if os.path.isfile(bitmapfiles[filename]):
os.remove(bitmapfiles[filename])
except BaseException:
pass
##
# copy_used_blocks():
# Copies used blocks from remote disk to local disk
##
def copy_used_blocks(hostip, username, password, vmspec, dev,
mountpath, usedblocksfile):
populate_extents(
hostip,
username,
password,
vmspec,
dev['backing']['fileName'],
mountpath,
usedblocksfile)
def read_partition_table(mountpath=None):
partitions = getfdisk_output(mountpath)
if len(partitions) == 1 and partitions[0]['id'] == 'ee':
# We found a gpt partition
partitions = getgptdisk_output(mountpath)
return partitions
def get_partition_table_from_vmdk(hostip, username, password, vmspec,
remotepath, localvmdkpath, extentsfile):
fileh, listfile = mkstemp()
close(fileh)
with open(listfile, 'w') as f:
f.write(localvmdkpath)
fileh, mntlist = mkstemp()
close(fileh)
with mount_local_vmdk(listfile, mntlist, diskonly=True) as mountpaths:
for key, value in mountpaths.items():
partitions = read_partition_table(value[0].split(";")[0].strip())
# If there is an extended partition, make sure the extended partition
# logical partition table is populated
for part in partitions:
if part['id'] == '5' or part['id'] == 'f':
extended_part = part
populate_extent(hostip, username, password, vmspec,
remotepath, localvmdkpath,
str(extended_part['start']), 2048)
with open(extentsfile, "a") as f:
f.write(str(int(extended_part['start']) * 512) + "," +
str(2048 * 512) + "\n")
with mount_local_vmdk(listfile, mntlist, diskonly=True) as mountpaths:
for key, value in mountpaths.items():
partitions = read_partition_table(
value[0].split(";")[0].strip())
break
oldpartitiontable = []
while len(oldpartitiontable) < len(partitions):
oldpartitiontable = partitions
for part in oldpartitiontable:
if int(part['start']) > int(extended_part['start']) and \
int(part['end']) + 2048 < int(extended_part['end']):
populate_extent(hostip, username, password, vmspec,
remotepath, localvmdkpath,
str(int(part['end']) + 1), 2048)
with open(extentsfile, "a") as f:
f.write(str(int(part['end'] + 1) * 512) + "," +
str(2048 * 512) + "\n")
with mount_local_vmdk(listfile, mntlist, diskonly=True) as mountpaths:
for key, value in mountpaths.items():
partitions = read_partition_table(
value[0].split(";")[0].strip())
break
break
os.remove(listfile)
os.remove(mntlist)
return partitions
def _getpvinfo(mountpath, startoffset='0', length=None):
subprocess.check_output(["pvscan"], stderr=subprocess.STDOUT)
subprocess.check_output(["pvdisplay", mountpath], stderr=subprocess.STDOUT)
LOG.info(_(mountpath + ":" + str(startoffset) + " is part of LVM"))
cmd = ["pvs", "--noheadings", "--nameprefixes", ]
pvstr = subprocess.check_output(cmd + [mountpath],
stderr=subprocess.STDOUT)
pvinfo = {}
for line in pvstr.strip().split("\n"):
for attr in line.strip().split(" "):
if 'LVM2_PV_NAME' in attr.split("=")[0] or\
'LVM2_VG_NAME' in attr.split("=")[0]:
pvinfo[attr.split("=")[0]] = attr.split(
"=")[1].strip("\'").strip().strip("B")
cmd = ["pvs", "--noheadings", "--units", "b", "-o",
"pv_all", "--nameprefixes", ]
pvstr = subprocess.check_output(cmd + [mountpath],
stderr=subprocess.STDOUT)
for line in pvstr.strip().split("\n"):
for attr in line.strip().split(" "):
if attr.split("=")[0].startswith('LVM2'):
pvinfo[attr.split("=")[0]] = attr.split("=")[
1].strip("\'").strip("B")
cmd = ["pvs", "--noheadings", "--units", "b", "-o",
"pvseg_all", "--nameprefixes", ]
pvstr = subprocess.check_output(cmd + [mountpath],
stderr=subprocess.STDOUT)
pvinfo['PV_DISK_OFFSET'] = startoffset
pvinfo['PV_SEGMENTS'] = []
for seg in pvstr.strip().split("\n"):
segs = {}
for attr in seg.strip().split(" "):
if attr.split("=")[0].startswith('LVM2'):
segs[attr.split("=")[0]] = attr.split("=")[
1].strip("\'").strip("B")
if len(segs):
pvinfo['PV_SEGMENTS'].append(segs)
return pvinfo
def mountdevice(mountpath, startoffset='0', length=None):
options = []
if length:
options = ["-o", startoffset, "--sizelimit", length]
freedev = subprocess.check_output(["losetup", "-f"],
stderr=subprocess.STDOUT)
freedev = freedev.strip("\n")
subprocess.check_output(["losetup", freedev, mountpath, ] + options,
stderr=subprocess.STDOUT)
return freedev
def mountpv(mountpath, startoffset='0', length=None):
try:
devpath = mountdevice(mountpath, startoffset, length)
pvinfo = _getpvinfo(freedev, startoffset, length)
return freedev, pvinfo
except Exception as ex:
LOG.exception(ex)
LOG.info(_(mountpath + ":" + startoffset + " does not have lvm pv"))
dismountpv(freedev)
raise
def dismountpv(devpath):
subprocess.check_output(["losetup", "-d", devpath],
stderr=subprocess.STDOUT)
def getvgs(pvinfos=None):
subprocess.check_output(["vgscan"], stderr=subprocess.STDOUT)
# Activate volume groups on the pv
subprocess.check_output(["vgchange", "-ay"], stderr=subprocess.STDOUT)
vgcmd = ["vgs", "--noheadings", "--nameprefixes", ]
vgoutput = subprocess.check_output(vgcmd, stderr=subprocess.STDOUT)
vglist = []
for vg in vgoutput.strip().split("\n"):
vg = vg.strip()
vginfo = {}
for attr in vg.strip().split(" "):
if attr.split("=")[0].startswith('LVM2'):
vginfo[attr.split("=")[0]] =\
attr.split("=")[1].strip("\'").strip("B")
if len(
vginfo) and 'LVM2_VG_NAME' in vginfo and vginfo['LVM2_VG_NAME'] != "tvault-appliance-vg":
vglist.append(vginfo)
return vglist
def deactivatevgs(vgname):
if vgname != "tvault-appliance-vg":
vgcmd = ["vgchange", "-an", vgname]
subprocess.check_output(vgcmd, stderr=subprocess.STDOUT)
def getloop_part_start_size(loopdev):
loopdev = loopdev.strip().rstrip()
if re.search("loop[0-9]+p[0-9]+", loopdev):
with open('/sys/block/' + re.search("loop[0-9]+", loopdev).group(0) + '/' + re.search("loop[0-9]+p[0-9]+", loopdev).group(0) + '/size', 'r') as f:
size = int(f.read()) * 512
with open('/sys/block/' + re.search("loop[0-9]+", loopdev).group(0) + '/' + re.search("loop[0-9]+p[0-9]+", loopdev).group(0) + '/start', 'r') as f:
start = int(f.read()) * 512
else:
start = 0
with open('/sys/block/' + re.search("loop[0-9]+", loopdev).group(0) + '/size', 'r') as f:
size = int(f.read()) * 512
return start, size
def getpvs(vgs):
subprocess.check_output(["pvscan"], stderr=subprocess.STDOUT)
pvlist = []
incompletevgs = set()
# get the list of volumes
for vg in vgs:
vgname = vg['LVM2_VG_NAME']
vgdisplay = subprocess.check_output(["vgdisplay", "-v", vgname],
stderr=subprocess.STDOUT)
for line in vgdisplay.split("\n"):
if "PV Name" in line:
pvpath = line.strip().rstrip().split(" ")[-1].strip().rstrip()
if re.search("loop[0-9]+", pvpath):
start, size = getloop_part_start_size(pvpath)
pvinfo = _getpvinfo(pvpath, start, size)
pvlist.append(pvinfo)
else:
incompletevgs.add(vg['LVM2_VG_NAME'])
# clean up the PVs that were part of incomplete vgs
purgedpvlist = []
for pv in pvlist:
if not pv['LVM2_VG_NAME'] in incompletevgs:
purgedpvlist.append(pv)
return purgedpvlist
def getlvs(vgs):
subprocess.check_output(["lvscan"], stderr=subprocess.STDOUT)
lvlist = []
# get the list of volumes
for vg in vgs:
vgname = vg['LVM2_VG_NAME']
lvs = subprocess.check_output(["lvs", "--noheadings", "--units", "b",
"--nameprefixes", vgname],
stderr=subprocess.STDOUT)
lvnames = []
for line in lvs.strip().split("\n"):
lvinfo = {}
for attr in line.strip().split(" "):
if attr.split("=")[0].startswith('LVM2'):
lvinfo[attr.split("=")[0]] =\
attr.split("=")[1].strip("\'").strip("B")
if not len(lvinfo):
continue
lvinfo['LVM2_LV_PATH'] = "/dev/" + \
vgname + "/" + lvinfo['LVM2_LV_NAME']
cmd = ["lvs", "--segments", "--noheadings", "--units", "b", "-o",
"seg_all", "--nameprefixes",
"/dev/" + vgname + "/" + lvinfo['LVM2_LV_NAME']]
lvsegs = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
lvinfo['LVM_LE_SEGMENTS'] = []
for seg in lvsegs.strip().split("\n"):
seginfo = {}
for attr in seg.strip().split(" "):
if attr.split("=")[0].startswith('LVM2'):
seginfo[attr.split("=")[0]] =\
attr.split("=")[1].strip("\'").strip("B")
if len(seginfo):
lvinfo['LVM_LE_SEGMENTS'].append(seginfo)
if len(lvinfo):
lvnames.append(lvinfo)
lvlist += lvnames
return lvlist
def getlogicaladdrtopvaddr(lvinfo, pvlist, startoffset, length):
# first find the LVM segment that this belongs to
iosegs = []
for lvseg in lvinfo['LVM_LE_SEGMENTS']:
if length and int(lvseg['LVM2_SEG_START']) <= startoffset and int(
lvseg['LVM2_SEG_START']) + int(lvseg['LVM2_SEG_SIZE']) > startoffset:
seglength = min(length, int(lvseg['LVM2_SEG_START']) +
int(lvseg['LVM2_SEG_SIZE']) - startoffset)
iosegs.append({'lvseg': lvseg, 'startoffset': startoffset -
int(lvseg['LVM2_SEG_START']), 'length': seglength})
startoffset += seglength
length -= seglength
# find pvs segments
pvsegs = []
for ioseg in iosegs:
pvname = ioseg['lvseg']['LVM2_SEG_PE_RANGES'].split(":")[0]
startpe = int(
ioseg['lvseg']['LVM2_SEG_PE_RANGES'].split(":")[1].split('-')[0])
endpe = int(
ioseg['lvseg']['LVM2_SEG_PE_RANGES'].split(":")[1].split('-')[1])
pesize = int(ioseg['lvseg']['LVM2_SEG_SIZE']) / (endpe - startpe + 1)
offset = ioseg['startoffset']
lenseg = ioseg['length']
for pv in pvlist:
if pv['LVM2_PV_NAME'] == pvname:
pvoffset = int(pv['LVM2_PE_START']) + startpe * pesize + offset
pvlength = lenseg
pvsegs.append({'pv': pv, 'offset': pvoffset,
'length': pvlength})
return pvsegs
def getpartaddrtopvaddr(part, pvlist, startoffset, length):
return [{'pv': None, 'offset': startoffset + part['start'] * 512,
'length': length}]
def copylvextsuperblock(hostip, username, password, vmspec, devmap,
lvinfo, pvinfo):
pvsegs = getlogicaladdrtopvaddr(lvinfo, pvinfo, 0, 400 * 512)
for pvseg in pvsegs:
populate_extent(hostip, username, password, vmspec,
pvseg['pv']['filename'], pvseg['pv']['localvmdkpath'],
str((pvseg['offset'] +
int(pvseg['pv']['PV_DISK_OFFSET'])) / 512),
str(pvseg['length'] / 512))
def performlvthickcopy(hostip, username, password, vmspec, devmap,
lvsrc, srcpvlist, extentsfile):
try:
totalblocks = {}
copylvextsuperblock(hostip, username, password, vmspec,
devmap, lvsrc, srcpvlist)
for pvs, vgs, lvs, mountinfo in mountlvmvgs(hostip, username, password,
vmspec, devmap):
if len(vgs) == 0:
LOG.info(
_("This VM does not contain any volume groups. Defaulting to cbt"))
raise Exception(
"This VM does not contain any volume groups. Defaulting to cbt")
if len(lvs) == 0:
LOG.info(
_("This VM does not contain any logical volumes. Defaulting to cbt"))
raise Exception(
"This VM does not contain any logical volumes. Defaulting to cbt")
blocksize, blockgroups = get_blockgroups(lvsrc['LVM2_LV_PATH'])
copy_free_bitmap_from_lv(hostip, username, password, vmspec, devmap,
lvsrc, srcpvlist, blocksize, blockgroups)
for pvs, vgs, lvs, mountinfo in mountlvmvgs(hostip, username, password,
vmspec, devmap):
totalblocks = get_usedblockslist_from_lv(
lvsrc['LVM2_LV_PATH'], extentsfile, lvsrc, srcpvlist, blocksize)
except Exception as ex:
LOG.exception(ex)
LOG.error(_("Cannot open lv: %s") % (lvsrc['LVM2_LV_PATH']))
return totalblocks
def mountlvmvgs(hostip, username, password, vmspec, devmap):
fileh, listfile = mkstemp()
close(fileh)
with open(listfile, 'w') as f:
for dmap in devmap:
f.write(dmap['localvmdkpath'] + "\n")
fileh, mntlist = mkstemp()
close(fileh)
mountinfo = {}
vgs = []
pvs = []
lvs = []
try:
with mount_local_vmdk(listfile, mntlist, diskonly=True) as mountpaths:
try:
for key, value in mountpaths.items():
mountpath = value[0].split(";")[0].strip()
devpath = mountdevice(mountpath)
# Add partition mappings here
try:
cmd = ["partx", "-d", devpath]
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except BaseException:
pass
try:
cmd = ["partx", "-a", devpath]
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except BaseException:
pass
for dmap in devmap:
if dmap['localvmdkpath'] == key:
mountinfo[key] = {
'mountpath': mountpath,
'devpath': devpath,
'localvmdkpath': key,
'filename': dmap['dev']['backing']['fileName']}
# explore VGs and volumes on the disk
vgs = getvgs()
if len(vgs):
lvs = getlvs(vgs)
if len(lvs):
pvs = getpvs(vgs)
for index, pv in enumerate(pvs):
for key, mount in mountinfo.items():
if mount['devpath'] in pv['LVM2_PV_NAME']:
pvs[index]['filename'] = mount['filename']
pvs[index]['localvmdkpath'] = mount['localvmdkpath']
# purge vms based on pvlist
purgedvgs = []
# if pv list does not have any reference to vg, purge the vg
for vg in vgs:
for pv in pvs:
if vg['LVM2_VG_NAME'] == pv['LVM2_VG_NAME']:
purgedvgs.append(vg)
break
purgedlvs = []
for lv in lvs:
found = False
for vg in purgedvgs:
if lv['LVM2_VG_NAME'] == vg['LVM2_VG_NAME']:
found = True
break
if found:
purgedlvs.append(lv)
yield pvs, purgedvgs, purgedlvs, mountinfo
except Exception as ex:
LOG.exception(ex)
finally:
for vg in vgs:
deactivatevgs(vg['LVM2_VG_NAME'])
time.sleep(2)
for key, mount in mountinfo.items():
dismountpv(mount['devpath'])
time.sleep(2)
for vg in vgs:
try:
deactivatevgs(vg['LVM2_VG_NAME'])
except BaseException:
pass
finally:
if os.path.isfile(listfile):
os.remove(listfile)
if os.path.isfile(mntlist):
os.remove(mntlist)
def lvmextents_in_partition(hostip, username, password, vmspec,
devmap, extentsfiles):
totalblocks = 0
pvdevices = []
devtopartmap = {}
# for each LV, check if ext file system on the LV
for pvs, vgs, lvs, mountinfo in mountlvmvgs(hostip, username, password,
vmspec, devmap):
pass
# TODO: we need to take care of the situation when vg is partially present
totalblocks = {}
for key, value in extentsfiles.items():
totalblocks[key] = 0
for lv in lvs:
lvtotalblocks = performlvthickcopy(hostip, username,
password, vmspec, devmap,
lv, pvs, extentsfiles)
for key, value in lvtotalblocks.items():
totalblocks[key] += lvtotalblocks[key]
return totalblocks
def process_partitions(hostip, username, password, vmspec, devmap,
logicalobjects, extentsfiles):
totalblocks = {}
process = None
# If partition has ext2 or its variant of file system, read the
# blocksize and all the block groups of the file system
partblockgroups = {}
for key, value in extentsfiles.items():
totalblocks[key] = 0
for partinfo in logicalobjects['regularpartitions']:
try:
localvmdkpath = None
partition = partinfo['partition']
if partition['id'] == 'ee' or partition['id'] == '5' \
or partition['id'] == 'f':
continue
filename = partinfo['filename']
for dmap in devmap:
if partinfo['filename'] == dmap['dev']['backing']['fileName']:
localvmdkpath = dmap['localvmdkpath']
break
if localvmdkpath is None:
raise Exception("Something went wrong. Could not find local \
vmdk that corresponds to remotepath")
# Check for regular partitions
fileh, listfile = mkstemp()
close(fileh)
with open(listfile, 'w') as f:
f.write(localvmdkpath)
fileh, mntlist = mkstemp()
close(fileh)
with mount_local_vmdk(listfile, mntlist, diskonly=True) as mountpaths:
for key, value in mountpaths.items():
mountpath = value[0].split(";")[0].strip()
break
try:
freedev = subprocess.check_output(["losetup", "-f"],
stderr=subprocess.STDOUT)
freedev = freedev.strip("\n")
startoffset = str(int(partition['start']) * 512)
length = partition['blocks'] + "KiB"
options = ["-o", startoffset, "--sizelimit", length]
subprocess.check_output(["losetup", freedev, mountpath, ] + options,
stderr=subprocess.STDOUT)
# display list of partitions that has ext file system
partblockgroups = get_blockgroups(freedev)
finally:
dismountpv(freedev)
# copy bitmap blocks and inode blocks to empty vmdk
blocksize = partblockgroups[0]
blockgroups = partblockgroups[1]
copy_free_bitmap_from_part(
hostip,
username,
password,
vmspec,
filename,
localvmdkpath,
partition['start'],
blocksize,
blockgroups)
# Get the list of used blocks for each file system
with mount_local_vmdk(listfile, mntlist, diskonly=True) as mountpaths:
for key, value in mountpaths.items():
mountpath = value[0].split(";")[0].strip()
break
##
# TODO: The used blocks can be pretty big. Make sure
# we are handling large lists correctly.
try:
freedev = None
freedev = subprocess.check_output(["losetup", "-f"],
stderr=subprocess.STDOUT)
freedev = freedev.strip("\n")
subprocess.check_output(["losetup",
freedev,
mountpath,
"-o",
str(int(
partition['start']) * 512),
"--sizelimit",
partition['blocks'] + "KiB"],
stderr=subprocess.STDOUT)
blocksize = partblockgroups[0]
totalblocks[filename] += get_usedblockslist_from_part(
freedev, extentsfiles[filename],
partition, blocksize)
finally:
if freedev:
dismountpv(freedev)
except Exception as ex:
LOG.exception(ex)
LOG.info(_(partinfo['filename'] + ":" + str(partition) +
"partition does not have ext fs. Ignoring now"))
finally:
if os.path.isfile(listfile):
os.remove(listfile)
if os.path.isfile(mntlist):
os.remove(mntlist)
return totalblocks
def discover_lvs_and_partitions(hostip, username, password, vmspec, devmap,
partitions):
totalblocks = 0
lvmresources = {}
# create separate list of disks and partitions used by LVM
# and non lvm
#
lvmdisks = []
lvmpartitions = []
regularpartitions = []
rawdisks = []
try:
for pvs, vgs, lvs, mountinfo in mountlvmvgs(hostip, username, password,
vmspec, devmap):
for pv in pvs:
for key, mount in mountinfo.items():
if mount['devpath'] in pv['LVM2_PV_NAME']:
for dmap in devmap:
if dmap['localvmdkpath'] == key:
lvmresources[pv["LVM2_PV_NAME"]] = \
{'filename': dmap['dev']['backing']['fileName'],
'startoffset': pv['PV_DISK_OFFSET']}
claimed = set()
# Identify lvm disks and partitions
for pv, resinfo in lvmresources.items():
if re.search("loop[0-9]+p[0-9]+", pv):
lvmpartitions.append(resinfo)
else:
lvmdisks.append(resinfo)
claimed.add(resinfo['filename'] + ':' +
str(resinfo['startoffset']))
# identify raw disks here
for filename, parttable in partitions.items():
if filename + ":0" in claimed:
continue
if len(parttable) == 0:
rawdisks.append({'filename': filename, 'startoffset': 0})
else:
for part in parttable:
if filename + ':' + \
str(int(part['start']) * 512) in claimed:
continue
regularpartitions.append({'filename': filename,
'partition': part})
return {'lvmdisks': lvmdisks, 'lvmpartitions': lvmpartitions,
'regularpartitions': regularpartitions, 'rawdisks': rawdisks}
except Exception as ex:
LOG.exception(ex)
##
# Description:
# Start with regular partitioned disk now.
# Extend it to lvm based snapshots later. This may involve reading the lvm metadata and reconstructing
# the volume. We can only support simple volumes that spans part of the disk for now. We may extend
# this feature to more complex compositions later
#
# Arguments:
# hostip - vcetner ip address
# username - admin user name for vcenter
# password - password for the user
# vmspec - moref of the vm that we are backing up
# devmap - [{dev: dev, localvmdkpath: localvmdkpath}]
#
# Return Value:
# extentsinfo = [{extentsfile: extentsfile, partitions:partitions,
# totalblocks: totalblocks}]
##
def _thickcopyextents(hostip, username, password, vmspec, devmap):
try:
# Read the partition table from each device
partitions = {}
extentsinfo = {}
extentsfiles = {}
totalblocks = {}
# for each LV, check if ext file system on the LV
for dmap in devmap:
fileh, extentsfiles[dmap['dev']['backing']['fileName']] = mkstemp()
totalblocks[dmap['dev']['backing']['fileName']] = 0
close(fileh)
for dmap in devmap:
filename = dmap['dev']['backing']['fileName']
capacity = dmap['dev']['capacityInBytes']
populate_bootrecord(hostip, username, password, vmspec,
filename, dmap['localvmdkpath'], capacity)
with open(extentsfiles[filename], "a") as f:
f.write(str(0) + "," + str(400 * 512) + "\n")
totalblocks[filename] += 400 * 512 / 4096
partitions[filename] = get_partition_table_from_vmdk(
hostip,
username,
password,
vmspec,
filename,
dmap['localvmdkpath'],
extentsfiles[filename])
# if no partitions found, see if this is raw LVM PV
if len(partitions[filename]) > 0:
# First copy super blocks of each partition
#
for part in partitions[filename]:
if part['id'] != 'ee' and part['id'] != '5' \
and part['id'] != 'f':
populate_extent(hostip, username, password, vmspec,
filename, dmap['localvmdkpath'],
str(part['start']), 400)
with open(extentsfiles[filename], "a") as f:
f.write(str(int(part['start']) * 512) + "," +
str(400 * 512) + "\n")
totalblocks[filename] += 400 * 512 / 4096
# mount all devices here
# do vg scan
# sort the partitions/disks into lvms and partitions
# sort lvs and partitions into ext fs and not ext fs
logicalobjects = discover_lvs_and_partitions(
hostip, username, password, vmspec, devmap, partitions)
# identify rest of partitions that were not part of LVM configuration
lvmtotalblocks = lvmextents_in_partition(hostip, username, password,
vmspec, devmap, extentsfiles)
for key, value in lvmtotalblocks.items():
totalblocks[key] += lvmtotalblocks[key]
parttotalblocks = process_partitions(hostip, username, password,
vmspec, devmap, logicalobjects,
extentsfiles)
for key, value in parttotalblocks.items():
totalblocks[key] += parttotalblocks[key]
return {'extentsfiles': extentsfiles, 'totalblocks': totalblocks,
'partitions': partitions}
except Exception as ex:
LOG.exception(ex)
for key, filename in extentsfiles.items():
if os.path.isfile(filename):
os.remove(filename)
raise
def thickcopyextents(hostip, username, password, vmspec, devmap):
try:
return _thickcopyextents(hostip, username, password,
vmspec, devmap)
except Exception as ex:
LOG.exception(_(ex))
return None
"""
def thickcopy(hostip, username, password, vmspec, dev, localvmdkpath):
extentsfile, partitions,\
totalblocks, listfile, mntlist = thickcopyextents(hostip,
username, password,
vmspec, dev, localvmdkpath)
if not extentsfile:
LOG.info(_( "Cannot use thickcopy to upload snapshot"))
return
# Copy used blocks of the file system from remote disk to local disk
LOG.info(_( "Copying " + str(totalblocks) +\
" used blocks from remote to local disk"))
copy_used_blocks(hostip, username, password,
vmspec, dev, localvmdkpath, extentsfile)
LOG.info(_( "Copied.. "))
for part in partitions:
#verify the file system can be mounted
process, mountpaths = mount_local_vmdk(listfile,
mntlist, diskonly=True)
try:
for key, value in mountpaths.iteritems():
mountpath = value[0].split(";")[0].strip()
##
# This will tend to be pretty big
try:
freedev = subprocess.check_output(["losetup", "-f"],
stderr=subprocess.STDOUT)
freedev = freedev.strip("\n")
subprocess.check_output(["losetup", freedev, mountpath, "-o",
str(int(part['start'])*512), "--sizelimit", part['blocks'] + "KiB"],
stderr=subprocess.STDOUT)
cmdspec = ["/opt/stack/workloadmgr/debugfs/debugfs",
"-R", "ls", freedev]
ls, error = execute_debugfs(cmdspec)
LOG.info(_(ls))
#input("Enter something: ")
except:
LOG.info(_("Cannot display root directory"))
finally:
subprocess.check_output(["losetup", "-d", freedev],
stderr=subprocess.STDOUT)
finally:
umount_local_vmdk(process)
return True
vmdkfile = "/tmp/disks/vmdk1"
"""
"""
if os.path.isfile(vmdkfile):
os.remove(vmdkfile)
# Create an empty VMDK file where the snapshot image go
#dev = {'capacityInBytes': 68719476736, 'backing' : {'fileName' : '[Datastore2] centos7/centos7.vmdk'}}
create_empty_vmdk(vmdkfile, dev['capacityInBytes'])
#thickcopy("192.168.1.130", "root", "vmware", 'moref=vm-6023', dev, vmdkfile)
"""
"""
# R1Soft VM
if os.path.isfile(vmdkfile):
os.remove(vmdkfile)
print("Downloading first disk")
dev = {'capacityInBytes': 21474836480, 'backing' : {'fileName' : '[Datastore4] r1soft/r1soft-000001.vmdk'}}
create_empty_vmdk(vmdkfile, dev['capacityInBytes'])
thickcopy("192.168.1.130", "root", "vmware", 'moref=vm-6020', dev, vmdkfile)
print("Downloading second disk")
if os.path.isfile(vmdkfile):
os.remove(vmdkfile)
# Create an empty VMDK file where the snapshot image go
dev = {'capacityInBytes': 68719476736, 'backing' : {'fileName' : '[Datastore4] r1soft/r1soft_1-000001.vmdk'}}
create_empty_vmdk(vmdkfile, dev['capacityInBytes'])
thickcopy("192.168.1.130", "root", "vmware", 'moref=vm-6020', dev, vmdkfile)
print("Downloading third disk")
if os.path.isfile(vmdkfile):
os.remove(vmdkfile)
dev = {'capacityInBytes': 34359738368, 'backing' : {'fileName' : '[datastore1 (2)] r1soft/r1soft-000002.vmdk'}}
create_empty_vmdk(vmdkfile, dev['capacityInBytes'])
thickcopy("192.168.1.130", "root", "vmware", 'moref=vm-6020', dev, vmdkfile)
"""