#!/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 . # # ----------------------------------------------------------------------- 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)