Last active
October 30, 2022 12:36
-
-
Save gitisz/63fd094cfc0eb53eb36f172a6efb6049 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from ast import Try | |
import socket | |
import sys | |
import logging | |
import atexit | |
import argparse | |
import getpass | |
from time import sleep | |
import vsanapiutils | |
import requests | |
import paramiko | |
from colorama import Fore, Style | |
from pyVmomi import vim | |
import vsanmgmtObjects | |
from pyVim.connect import SmartConnect, Disconnect | |
from ssl import _create_unverified_context | |
from vmware.vapi.vsphere.client import create_vsphere_client | |
from com.vmware.vcenter_client import VM | |
from com.vmware.vcenter.vm_client import Power | |
CATEGORY_NAME = "ShutdownTier" | |
CATEGORY_ID = "urn:vmomi:InventoryServiceCategory:02368c0a-3e88-4147-b743-069a466ad592:GLOBAL" | |
VCENTER_NAME = "VCENTER" | |
ESX_ROOT_USER = "root" | |
LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" | |
logging.root.handlers = [] | |
logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG , filename='vcenter-api-shutdown.log') | |
streamHandler = logging.StreamHandler() | |
streamHandler.setLevel(logging.INFO) | |
formatter = logging.Formatter(LOG_FORMAT) | |
streamHandler.setFormatter(formatter) | |
logger = logging.getLogger("") | |
logger.addHandler(streamHandler) | |
logger.info("Application started.") | |
def GetArgs(): | |
""" | |
Supports the command-line arguments listed below. | |
""" | |
parser = argparse.ArgumentParser( | |
description='Process args for vSAN SDK sample application') | |
parser.add_argument('-s', | |
'--host', | |
required=True, | |
action='store', | |
help='Remote host to connect to') | |
parser.add_argument('-o', | |
'--port', | |
type=int, | |
default=443, | |
action='store', | |
help='Port to connect on') | |
parser.add_argument('-u', | |
'--vcenter-username', | |
required=True, | |
action='store', | |
help='User name to use when connecting to vCenter Host') | |
parser.add_argument('-p', | |
'--password', | |
required=False, | |
action='store', | |
help='Password to use when connecting to vCenter host') | |
parser.add_argument('--cluster', | |
dest='clusterName', | |
metavar="CLUSTER", | |
default='VSAN-Cluster') | |
args = parser.parse_args() | |
return args | |
def get_status(status): | |
message = "%s%s%s" | |
if status == "green": | |
message = message % (Fore.GREEN, status, Style.RESET_ALL) | |
if status == "info": | |
message = message % (Fore.LIGHTYELLOW_EX, status, Style.RESET_ALL) | |
if status == "skipped": | |
message = message % (Fore.LIGHTYELLOW_EX, status, Style.RESET_ALL) | |
if status == "red": | |
message = message % (Fore.RED, status, Style.RESET_ALL) | |
return message | |
def get_unverified_session(): | |
session = requests.session() | |
session.verify = False | |
requests.packages.urllib3.disable_warnings() | |
return session | |
def getComputeInstance(entityName, serviceInstance): | |
content = serviceInstance.RetrieveContent() | |
searchIndex = content.searchIndex | |
datacenters = content.rootFolder.childEntity | |
for datacenter in datacenters: | |
instance = searchIndex.FindChild(datacenter.hostFolder, entityName) | |
if instance is not None: | |
return instance | |
return None | |
def getClusterInstance(cluster_name, serviceInstance): | |
content = serviceInstance.RetrieveContent() | |
searchIndex = content.searchIndex | |
datacenters = content.rootFolder.childEntity | |
for datacenter in datacenters: | |
cluster = searchIndex.FindChild(datacenter.hostFolder, cluster_name) | |
if cluster is not None: | |
return cluster | |
return None | |
def getVmInstance(vm_name, serviceInstance): | |
content = serviceInstance.RetrieveContent() | |
container = content.rootFolder | |
recursive = True | |
viewType = [vim.VirtualMachine] | |
containerView = content.viewManager.CreateContainerView(container, viewType, recursive) | |
vms = containerView.view | |
for vm in vms: | |
vm = vm.config.name == vm_name | |
if vm is not None: | |
return vm | |
return None | |
def evaluateVSanClusterHealth(service_instance, context, host, cluster_name): | |
logger.info("Evaluating vSAN cluster health prior to shutdown...") | |
apiVersion = vsanapiutils.GetLatestVmodlVersion(host) | |
vcMos = vsanapiutils.GetVsanVcMos(service_instance._stub, | |
context=context, | |
version=apiVersion) | |
vhs = vcMos['vsan-cluster-health-system'] | |
cluster = getClusterInstance(cluster_name, service_instance) | |
fetchFromCache = True | |
healthSummary = vhs.QueryClusterHealthSummary( | |
cluster=cluster, | |
includeObjUuids=True, | |
fetchFromCache=fetchFromCache) | |
clusterStatus = healthSummary.clusterStatus | |
logger.info("Cluster %s Status: %s" % | |
(cluster_name, get_status(clusterStatus.status))) | |
for hostStatus in clusterStatus.trackedHostsStatus: | |
logger.info("Host %s Status: %s" % | |
(hostStatus.hostname, get_status(hostStatus.status))) | |
vsanClusterHealthGroups = healthSummary.groups | |
healthy = True | |
for group in vsanClusterHealthGroups: | |
logger.info("%s - %s" % (group.groupName.upper(), get_status(group.groupHealth))) | |
if group.groupHealth not in ["green", "info", "skipped"]: | |
healthy = False | |
for test in group.groupTests: | |
logger.info( | |
"%s - %s - %s" % | |
(group.groupName.upper(), test.testName, get_status(test.testHealth))) | |
if test.testHealth not in ["green", "info", "skipped"]: | |
healthy = False | |
if not healthy: | |
logger.warning("Exiting automated shutdown: vSAN cluster is unhealthy") | |
sys.exit() | |
def waitForGuestShutdownsToComplete(vsphere_client, vm_tasks): | |
for vm_task in vm_tasks: | |
status = vsphere_client.vcenter.vm.Power.get(vm_task["vm_id"]) | |
if status.state == Power.State.POWERED_OFF: | |
vm_task["power_state"] = Power.State.POWERED_OFF | |
powered_on_vms = list(filter(lambda vm: vm["power_state"] == Power.State.POWERED_ON, vm_tasks)) | |
if len(powered_on_vms) > 0: | |
logger.info("Waiting on %s VMs to properly shutdown." % (len(powered_on_vms))) | |
sleep(2) | |
waitForGuestShutdownsToComplete(vsphere_client, vm_tasks) | |
def shutdownAllVmsInCluster(vsphere_client, tiers): | |
logger.info("Shutting down VMs in cluster based on SHUTDOWN_TIER.") | |
tag_ids = vsphere_client.tagging.Tag.list_tags_for_category(CATEGORY_ID) | |
tags = [] | |
vm_tasks = [] | |
for tag_id in tag_ids: | |
tags.append(vsphere_client.tagging.Tag.get(tag_id)) | |
for tag in sorted(tags, reverse=False, key=lambda t: t.name): | |
objects = vsphere_client.tagging.TagAssociation.list_attached_objects(tag.id) | |
for object in objects: | |
vm = vsphere_client.vcenter.VM.get(object.id) | |
if tag.name in tiers: | |
status = vsphere_client.vcenter.vm.Power.get(object.id) | |
if status.state == Power.State.POWERED_ON: | |
logger.info("Shutting down VM %s in Tier %s" % (vm.name, tag.name)) | |
vsphere_client.vcenter.vm.guest.Power.shutdown(object.id) | |
vm_tasks.append({ "vm_id": object.id, "power_state": Power.State.POWERED_ON}) | |
else: | |
logger.info("VM %s is already in the %s state." % (vm.name, Power.State.POWERED_OFF)) | |
waitForGuestShutdownsToComplete(vsphere_client, vm_tasks) | |
vm_tasks = [] | |
def disableVcenterHighAvailabilityMode(service_instance, cluster_name): | |
logger.info("Disabling vSphere HA Mode.") | |
cluster = getClusterInstance(cluster_name, service_instance) | |
clusterConfigSpec = vim.cluster.ConfigSpec() | |
clusterConfigSpec.dasConfig = cluster.configuration.dasConfig | |
clusterConfigSpec.dasConfig.enabled = False | |
tasks = [] | |
task = cluster.ReconfigureCluster_Task(clusterConfigSpec, True) | |
tasks.append(task) | |
vsanapiutils.WaitForTasks(tasks, service_instance) | |
def waitForVclsVmsToRetreat(vsphere_client): | |
vms = vsphere_client.vcenter.VM.list() | |
powered_on_vcls_vms = list(filter(lambda vm: vm.name.startswith ("vCLS"), vms)) | |
if len(powered_on_vcls_vms) > 0: | |
logger.info("Waiting on %s vCLS VMs to retreat." % (len(powered_on_vcls_vms))) | |
sleep(2) | |
waitForVclsVmsToRetreat(vsphere_client) | |
def enableVclsRetreatMode(vsphere_client, service_instance, cluster_name): | |
logger.info("Enabling vCLS Retreat Mode.") | |
cluster = getClusterInstance(cluster_name, service_instance) | |
key = "config.vcls.clusters.{}.enabled".format(cluster._moId) | |
value = "false" | |
option_value = vim.option.OptionValue | |
vim_values = [option_value(key=key, value=str(value))] | |
opt_mgr = service_instance.RetrieveContent().setting | |
opt_mgr.UpdateValues(vim_values) | |
waitForVclsVmsToRetreat(vsphere_client) | |
def ensureVsanResyncingCompleted(service_instance, context, host, cluster_name): | |
logger.info("Ensuring vSAN Resyncing Objects has completed.") | |
apiVersion = vsanapiutils.GetLatestVmodlVersion(host) | |
vcMos = vsanapiutils.GetVsanVcMos( | |
service_instance._stub, context=context, version=apiVersion) | |
vhs = vcMos['vsan-cluster-object-system'] | |
cluster = getClusterInstance(cluster_name, service_instance) | |
object_filter = vim.cluster.VsanSyncingObjectFilter() | |
object_filter.resyncStatus = 'active' | |
object_filter.resyncType = None | |
object_filter.offset = 0 | |
object_filter.numberOfObjects = 100 | |
resyincing_objects = vhs.QuerySyncingVsanObjectsSummary(cluster, object_filter) | |
if resyincing_objects.totalObjectsToSync > 0: | |
logger.info("Exiting automated shutdown: vSAN cluster is resyncing objects") | |
sys.exit() | |
def waitForVcenterVmGuestShutdownsToComplete(host, username, password): | |
ssh_client = paramiko.SSHClient() | |
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
ssh_client.connect(host.name, username=username, password=password) | |
stdin, stdout, stderr = ssh_client.exec_command('esxcli vm process list') | |
response = stdout.read().decode() | |
ssh_client.close() | |
logger.info(response) | |
if "Display Name: %s" % (VCENTER_NAME) in response: | |
logger.info("Waiting for vCenter VM on host %s to shutdown." % (host.name)) | |
sleep(10) | |
waitForVcenterVmGuestShutdownsToComplete(host, username, password) | |
def returnVCenterHost(vsphere_client): | |
logger.info("Shutting down vCenter VM in cluster.") | |
hosts = vsphere_client.vcenter.Host.list() | |
for host in hosts: | |
vms = vsphere_client.vcenter.VM.list(VM.FilterSpec(hosts={host.host})) | |
for vm in vms: | |
if vm.name == VCENTER_NAME: | |
status = vsphere_client.vcenter.vm.Power.get(vm.vm) | |
if status == Power.Info(state=Power.State.POWERED_ON): | |
logger.info("%s is running on: %s" % (VCENTER_NAME, host.name)) | |
return host | |
def returnAllPweredOnHosts(vsphere_client): | |
logger.info("Getting a list of all powered on hosts in cluster.") | |
all_hosts = [] | |
hosts = vsphere_client.vcenter.Host.list() | |
for host in hosts: | |
if host.power_state == "POWERED_ON": | |
all_hosts.append(host) | |
return all_hosts | |
def shutdownVCenterVmInCluster(vsphere_client, vcenter_host): | |
logger.info("Shutting down vCenter VM in cluster.") | |
vms = vsphere_client.vcenter.VM.list(VM.FilterSpec(hosts={vcenter_host.host})) | |
for vm in vms: | |
if vm.name == VCENTER_NAME: | |
status = vsphere_client.vcenter.vm.Power.get(vm.vm) | |
if status == Power.Info(state=Power.State.POWERED_ON): | |
logger.info("Shutting down %s on host: %s" % (VCENTER_NAME, vcenter_host.name)) | |
vsphere_client.vcenter.vm.guest.Power.shutdown(vm.vm) | |
break | |
def waitForServiceToBeEnabled(cluster_name, service_instance): | |
cluster = getClusterInstance(cluster_name, service_instance) | |
for host in cluster.host: | |
serviceInfo = host.configManager.serviceSystem.serviceInfo | |
for service in serviceInfo.service: | |
if service.label == "SSH" and service.running == "false": | |
logger.info("Waiting for SSH to be started on host%s." % (host)) | |
sleep(2) | |
waitForServiceToBeEnabled(cluster_name, service_instance) | |
def startSshServiceOnHosts(service_instance, cluster_name, start=True): | |
cluster = getClusterInstance(cluster_name, service_instance) | |
for host in cluster.host: | |
serviceInfo = host.configManager.serviceSystem.serviceInfo | |
for service in serviceInfo.service: | |
if service.label == "SSH": | |
if start: | |
host.configManager.serviceSystem.StartService(service.key) | |
waitForServiceToBeEnabled(cluster_name, service_instance) | |
else: | |
host.configManager.serviceSystem.StopService(service.key) | |
def disableClusterMemberUpdates(all_powered_on_hosts, username, password): | |
logger.info("Disabling cluster member updates.") | |
for host in all_powered_on_hosts: | |
logger.info("Disabling cluster member updates on host %s." % (host.name)) | |
ssh_client = paramiko.SSHClient() | |
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
ssh_client.connect(host.name, username=username, password=password) | |
stdin, stdout, stderr = ssh_client.exec_command('esxcfg-advcfg -s 1 /VSAN/IgnoreClusterMemberListUpdates') | |
response = stdout.read().decode() | |
ssh_client.close() | |
logger.info(response) | |
def prepareClusterForReboot(all_powered_on_hosts, username, password): | |
logger.info("Preparing cluster for reboot.") | |
# Run reboot prepare script only on one host | |
for host in all_powered_on_hosts: | |
logger.info("Running reboot preparation script on host %s." % (host.name)) | |
ssh_client = paramiko.SSHClient() | |
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
ssh_client.connect(host.name, username=username, password=password) | |
stdin, stdout, stderr = ssh_client.exec_command('python /usr/lib/vmware/vsan/bin/reboot_helper.py prepare') | |
response = stdout.read().decode() | |
ssh_client.close() | |
logger.info(response) | |
break | |
def waitForHostToEnterMaintenanceMode(host, username, password): | |
logger.info("Waiting for host %s to enter maintenance mode." % (host.name)) | |
ssh_client = paramiko.SSHClient() | |
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
ssh_client.connect(host.name, username=username, password=password) | |
stdin, stdout, stderr = ssh_client.exec_command('vim-cmd /hostsvc/hostsummary | grep inMaintenanceMode') | |
response = stdout.read().decode() | |
ssh_client.close() | |
if "inMaintenanceMode = true" not in response: | |
sleep(2) | |
waitForHostToEnterMaintenanceMode(host, username, password) | |
def placeHostsInMaintenanceMode(all_powered_on_hosts, username, password): | |
logger.info("Placing hosts into maintenance mode.") | |
for host in all_powered_on_hosts: | |
logger.info("Placing host %s into maintenance mode." % (host.name)) | |
ssh_client = paramiko.SSHClient() | |
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
ssh_client.connect(host.name, username=username, password=password) | |
stdin, stdout, stderr = ssh_client.exec_command('esxcli system maintenanceMode set -e true -m noAction') | |
response = stdout.read().decode() | |
ssh_client.close() | |
logger.info(response) | |
waitForHostToEnterMaintenanceMode(host, username, password) | |
def waitForHostToShutDown(host): | |
HOST_ONLINE = True | |
while HOST_ONLINE: | |
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
try: | |
s.connect((host.name, 22)) | |
logger.info("Waiting for host %s to shutdown." % (host.name)) | |
except socket.error as e: | |
HOST_ONLINE = False | |
finally: | |
s.close() | |
sleep(2) | |
def performRollingHostShutdown(all_powered_on_hosts, username, password): | |
logger.info("Performing rolling host shutdown.") | |
for host in all_powered_on_hosts: | |
waitForHostToEnterMaintenanceMode(host, username, password) | |
ssh_client = paramiko.SSHClient() | |
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
ssh_client.connect(host.name, username=username, password=password) | |
stdin, stdout, stderr = ssh_client.exec_command('esxcli system shutdown poweroff --reason AUTOMATED_SHUTDOWN_ON_UPS_BACKUP') | |
ssh_client.close() | |
waitForHostToShutDown(host) | |
def main(): | |
""" | |
This application automates shutdown of vSphere, vCenter, vSAN, VMs and ESXi hosts. | |
https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vsan-monitoring.doc/GUID-31B4F958-30A9-4BEC-819E-32A18A685688.html | |
""" | |
args = GetArgs() | |
tiers = ["0", "1", "2", "3"] | |
password = args.password | |
if not password: | |
password = getpass.getpass(prompt="Password: ") | |
context = _create_unverified_context() | |
vsphere_client = create_vsphere_client(server=args.host, | |
username=args.vcenter_username, | |
password=password, | |
session=get_unverified_session()) | |
service_instance = SmartConnect(host=args.host, | |
user=args.vcenter_username, | |
pwd=password, | |
port=int(args.port), | |
sslContext=context) | |
atexit.register(Disconnect, service_instance) | |
about_info = service_instance.content.about | |
vcenter_host = None | |
all_powered_on_hosts = [] | |
if about_info.apiType == 'VirtualCenter': | |
evaluateVSanClusterHealth(service_instance, context, args.host, args.clusterName) | |
shutdownAllVmsInCluster(vsphere_client, tiers) | |
disableVcenterHighAvailabilityMode(service_instance, args.clusterName) | |
enableVclsRetreatMode(vsphere_client, service_instance, args.clusterName) | |
ensureVsanResyncingCompleted(service_instance, context, args.host, args.clusterName) | |
startSshServiceOnHosts(service_instance, args.clusterName) | |
vcenter_host = returnVCenterHost(vsphere_client) | |
all_powered_on_hosts = returnAllPweredOnHosts(vsphere_client) | |
shutdownVCenterVmInCluster(vsphere_client, vcenter_host) | |
# At this point, access to vCenter is now unavailable. The remaining functions will use SSH. | |
waitForVcenterVmGuestShutdownsToComplete(vcenter_host, ESX_ROOT_USER, password) | |
disableClusterMemberUpdates(all_powered_on_hosts, ESX_ROOT_USER, password) | |
prepareClusterForReboot(all_powered_on_hosts, ESX_ROOT_USER, password) | |
placeHostsInMaintenanceMode(all_powered_on_hosts, ESX_ROOT_USER, password) | |
performRollingHostShutdown(all_powered_on_hosts, ESX_ROOT_USER, password) | |
else: | |
logger.info("Server provided is not a vCenter server.") | |
logger.info("All vCenter shutdown tasks are complete.") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This code shutdowns vCenter and ESX hosts running vSAN and VCHA. It essentially follows this guide. It has the added benefit of shuttung down VMs in tiers, which is handy so some VMs can shutdown ahead of others.
Requirements:
Note: also needs
vsanapiutils.py
andvsanmgmtObjects.py
which you can download from VMWare.To run:
python vcenter-api-shutdown.py -s <vcenter-ip> -u <vcenter-username> -p <vcenter-password> --cluster <vcenter-cluster>
Dockerfile Example:
Assumptions:
ShutdownTier
Example Output: