k3nny
737f9bea38
All checks were successful
lint / docker (push) Successful in 9m14s
fix code passing ruff linter pre-commit ruff pre-commit ruff format
271 lines
11 KiB
Python
Executable File
271 lines
11 KiB
Python
Executable File
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
# -----------------------------------------------------------------------
|
|
# This file is part of TISBackup
|
|
#
|
|
# TISBackup is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# TISBackup is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with TISBackup. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# -----------------------------------------------------------------------
|
|
|
|
import atexit
|
|
import getpass
|
|
from datetime import date, datetime, timedelta
|
|
|
|
import pyVmomi
|
|
import requests
|
|
from pyVim.connect import Disconnect, SmartConnect
|
|
from pyVmomi import vim, vmodl
|
|
|
|
# Disable HTTPS verification warnings.
|
|
from requests.packages import urllib3
|
|
|
|
from .common import *
|
|
|
|
urllib3.disable_warnings()
|
|
import os
|
|
import re
|
|
import tarfile
|
|
import time
|
|
import xml.etree.ElementTree as ET
|
|
from stat import *
|
|
|
|
|
|
class backup_vmdk(backup_generic):
|
|
type = "esx-vmdk"
|
|
|
|
required_params = backup_generic.required_params + ["esxhost", "password_file", "server_name"]
|
|
optional_params = backup_generic.optional_params + ["esx_port", "prefix_clone", "create_ovafile", "halt_vm"]
|
|
|
|
esx_port = 443
|
|
prefix_clone = "clone-"
|
|
create_ovafile = "no"
|
|
halt_vm = "no"
|
|
|
|
def make_compatible_cookie(self, client_cookie):
|
|
cookie_name = client_cookie.split("=", 1)[0]
|
|
cookie_value = client_cookie.split("=", 1)[1].split(";", 1)[0]
|
|
cookie_path = client_cookie.split("=", 1)[1].split(";", 1)[1].split(";", 1)[0].lstrip()
|
|
cookie_text = " " + cookie_value + "; $" + cookie_path
|
|
# Make a cookie
|
|
cookie = dict()
|
|
cookie[cookie_name] = cookie_text
|
|
return cookie
|
|
|
|
def download_file(self, url, local_filename, cookie, headers):
|
|
r = requests.get(url, stream=True, headers=headers, cookies=cookie, verify=False)
|
|
with open(local_filename, "wb") as f:
|
|
for chunk in r.iter_content(chunk_size=1024 * 1024 * 64):
|
|
if chunk:
|
|
f.write(chunk)
|
|
f.flush()
|
|
return local_filename
|
|
|
|
def export_vmdks(self, vm):
|
|
HttpNfcLease = vm.ExportVm()
|
|
try:
|
|
infos = HttpNfcLease.info
|
|
device_urls = infos.deviceUrl
|
|
vmdks = []
|
|
for device_url in device_urls:
|
|
deviceId = device_url.key
|
|
deviceUrlStr = device_url.url
|
|
diskFileName = vm.name.replace(self.prefix_clone, "") + "-" + device_url.targetId
|
|
diskUrlStr = deviceUrlStr.replace("*", self.esxhost)
|
|
# diskLocalPath = './' + diskFileName
|
|
|
|
cookie = self.make_compatible_cookie(si._stub.cookie)
|
|
headers = {"Content-Type": "application/octet-stream"}
|
|
self.logger.debug("[%s] exporting disk: %s" % (self.server_name, diskFileName))
|
|
|
|
self.download_file(diskUrlStr, diskFileName, cookie, headers)
|
|
vmdks.append({"filename": diskFileName, "id": deviceId})
|
|
finally:
|
|
HttpNfcLease.Complete()
|
|
return vmdks
|
|
|
|
def create_ovf(self, vm, vmdks):
|
|
ovfDescParams = vim.OvfManager.CreateDescriptorParams()
|
|
ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams)
|
|
root = ET.fromstring(ovf.ovfDescriptor)
|
|
new_id = list(root[0][1].attrib.values())[0][1:3]
|
|
ovfFiles = []
|
|
for vmdk in vmdks:
|
|
old_id = vmdk["id"][1:3]
|
|
id = vmdk["id"].replace(old_id, new_id)
|
|
ovfFiles.append(vim.OvfManager.OvfFile(size=os.path.getsize(vmdk["filename"]), path=vmdk["filename"], deviceId=id))
|
|
|
|
ovfDescParams = vim.OvfManager.CreateDescriptorParams()
|
|
ovfDescParams.ovfFiles = ovfFiles
|
|
|
|
ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams)
|
|
ovf_filename = vm.name + ".ovf"
|
|
self.logger.debug("[%s] creating ovf file: %s" % (self.server_name, ovf_filename))
|
|
|
|
with open(ovf_filename, "w") as f:
|
|
f.write(ovf.ovfDescriptor)
|
|
return ovf_filename
|
|
|
|
def create_ova(self, vm, vmdks, ovf_filename):
|
|
ova_filename = vm.name + ".ova"
|
|
vmdks.insert(0, {"filename": ovf_filename, "id": "false"})
|
|
self.logger.debug("[%s] creating ova file: %s" % (self.server_name, ova_filename))
|
|
with tarfile.open(ova_filename, "w") as tar:
|
|
for vmdk in vmdks:
|
|
tar.add(vmdk["filename"])
|
|
os.unlink(vmdk["filename"])
|
|
return ova_filename
|
|
|
|
def clone_vm(self, vm):
|
|
task = self.wait_task(
|
|
vm.CreateSnapshot_Task(
|
|
name="backup", description="Automatic backup " + datetime.now().strftime("%Y-%m-%d %H:%M:%s"), memory=False, quiesce=True
|
|
)
|
|
)
|
|
snapshot = task.info.result
|
|
prefix_vmclone = self.prefix_clone
|
|
clone_name = prefix_vmclone + vm.name
|
|
datastore = "[%s]" % vm.datastore[0].name
|
|
|
|
vmx_file = vim.vm.FileInfo(logDirectory=None, snapshotDirectory=None, suspendDirectory=None, vmPathName=datastore)
|
|
|
|
config = vim.vm.ConfigSpec(
|
|
name=clone_name, memoryMB=vm.summary.config.memorySizeMB, numCPUs=vm.summary.config.numCpu, files=vmx_file
|
|
)
|
|
|
|
hosts = datacenter.hostFolder.childEntity
|
|
resource_pool = hosts[0].resourcePool
|
|
|
|
self.wait_task(vmFolder.CreateVM_Task(config=config, pool=resource_pool))
|
|
|
|
new_vm = [x for x in vmFolder.childEntity if x.name == clone_name][0]
|
|
|
|
controller = vim.vm.device.VirtualDeviceSpec()
|
|
controller.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
|
|
controller.device = vim.vm.device.VirtualLsiLogicController(busNumber=0, sharedBus="noSharing")
|
|
controller.device.key = 0
|
|
i = 0
|
|
|
|
vm_devices = []
|
|
clone_folder = "%s/" % "/".join(new_vm.summary.config.vmPathName.split("/")[:-1])
|
|
for device in vm.config.hardware.device:
|
|
if device.__class__.__name__ == "vim.vm.device.VirtualDisk":
|
|
cur_vers = int(re.findall(r"\d{3,6}", device.backing.fileName)[0])
|
|
|
|
if cur_vers == 1:
|
|
source = device.backing.fileName.replace("-000001", "")
|
|
else:
|
|
source = device.backing.fileName.replace("%d." % cur_vers, "%d." % (cur_vers - 1))
|
|
|
|
dest = clone_folder + source.split("/")[-1]
|
|
disk_spec = vim.VirtualDiskManager.VirtualDiskSpec(diskType="sparseMonolithic", adapterType="ide")
|
|
self.wait_task(si.content.virtualDiskManager.CopyVirtualDisk_Task(sourceName=source, destName=dest, destSpec=disk_spec))
|
|
# self.wait_task(si.content.virtualDiskManager.ShrinkVirtualDisk_Task(dest))
|
|
|
|
diskfileBacking = vim.vm.device.VirtualDisk.FlatVer2BackingInfo()
|
|
diskfileBacking.fileName = dest
|
|
diskfileBacking.diskMode = "persistent"
|
|
vdisk_spec = vim.vm.device.VirtualDeviceSpec()
|
|
vdisk_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
|
|
vdisk_spec.device = vim.vm.device.VirtualDisk(capacityInKB=10000, controllerKey=controller.device.key)
|
|
vdisk_spec.device.key = 0
|
|
vdisk_spec.device.backing = diskfileBacking
|
|
vdisk_spec.device.unitNumber = i
|
|
vm_devices.append(vdisk_spec)
|
|
i += 1
|
|
|
|
vm_devices.append(controller)
|
|
|
|
config.deviceChange = vm_devices
|
|
self.wait_task(new_vm.ReconfigVM_Task(config))
|
|
self.wait_task(snapshot.RemoveSnapshot_Task(removeChildren=True))
|
|
return new_vm
|
|
|
|
def wait_task(self, task):
|
|
while task.info.state in ["queued", "running"]:
|
|
time.sleep(2)
|
|
self.logger.debug("[%s] %s", self.server_name, task.info.descriptionId)
|
|
return task
|
|
|
|
def do_backup(self, stats):
|
|
try:
|
|
dest_dir = os.path.join(self.backup_dir, "%s" % self.backup_start_date)
|
|
if not os.path.isdir(dest_dir):
|
|
if not self.dry_run:
|
|
os.makedirs(dest_dir)
|
|
else:
|
|
print('mkdir "%s"' % dest_dir)
|
|
else:
|
|
raise Exception("backup destination directory already exists : %s" % dest_dir)
|
|
os.chdir(dest_dir)
|
|
user_esx, password_esx, null = open(self.password_file).read().split("\n")
|
|
|
|
global si
|
|
si = SmartConnect(host=self.esxhost, user=user_esx, pwd=password_esx, port=self.esx_port)
|
|
|
|
if not si:
|
|
raise Exception("Could not connect to the specified host using specified " "username and password")
|
|
|
|
atexit.register(Disconnect, si)
|
|
|
|
content = si.RetrieveContent()
|
|
for child in content.rootFolder.childEntity:
|
|
if hasattr(child, "vmFolder"):
|
|
global vmFolder, datacenter
|
|
datacenter = child
|
|
vmFolder = datacenter.vmFolder
|
|
vmList = vmFolder.childEntity
|
|
for vm in vmList:
|
|
if vm.name == self.server_name:
|
|
vm_is_off = vm.summary.runtime.powerState == "poweredOff"
|
|
if str2bool(self.halt_vm):
|
|
vm.ShutdownGuest()
|
|
vm_is_off = True
|
|
|
|
if vm_is_off:
|
|
vmdks = self.export_vmdks(vm)
|
|
ovf_filename = self.create_ovf(vm, vmdks)
|
|
else:
|
|
new_vm = self.clone_vm(vm)
|
|
vmdks = self.export_vmdks(new_vm)
|
|
ovf_filename = self.create_ovf(vm, vmdks)
|
|
self.wait_task(new_vm.Destroy_Task())
|
|
|
|
if str2bool(self.create_ovafile):
|
|
ova_filename = self.create_ova(vm, vmdks, ovf_filename) # noqa : F841
|
|
|
|
if str2bool(self.halt_vm):
|
|
vm.PowerOnVM()
|
|
|
|
if os.path.exists(dest_dir):
|
|
for file in os.listdir(dest_dir):
|
|
stats["written_bytes"] += os.stat(file)[ST_SIZE]
|
|
stats["total_files_count"] += 1
|
|
stats["written_files_count"] += 1
|
|
stats["total_bytes"] = stats["written_bytes"]
|
|
else:
|
|
stats["written_bytes"] = 0
|
|
|
|
stats["backup_location"] = dest_dir
|
|
|
|
stats["log"] = "XVA backup from %s OK, %d bytes written" % (self.server_name, stats["written_bytes"])
|
|
stats["status"] = "OK"
|
|
|
|
except BaseException as e:
|
|
stats["status"] = "ERROR"
|
|
stats["log"] = str(e)
|
|
raise
|
|
|
|
|
|
register_driver(backup_vmdk)
|