-
Notifications
You must be signed in to change notification settings - Fork 0
/
vmreport.py
489 lines (448 loc) · 20.3 KB
/
vmreport.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
#!/usr/bin/python3
#######################################################################
# vmreport.py - VMware ESXi Guest Reporting Tool
#######################################################################
# This tool either displays information about all Linux VMs (in all Data
# Centers, or in one Data Center), or generates a return code
# for a query regarding one specific VM
#
# REQUIRES:
# 0) Python v3
# 1) Additional Python Modules: PyVmoni, Pyvim
# 2) Credentials for a valid vSphere/vCenter User ID that has at
# least Read-Only access to the Linux VM objects in the VMware
# environment
#
# NOTES:
# 0) IMPORTANT! This tool was designed for an environment with two
# data centers, where the ESXi inventory was
# separately-maintained and FT was used for selected VMs;
# also, the credentials used for programmatic access were
# limited (in vSphere) to only having access to the Linux
# VMs (so I didn't have to worry about separating the wheat
# from the chaff in terms of the OS on the VMs) and further
# the logic for the "-c" parameter makes assumptions that
# were peculiar to host naming convention in that environment;
# ALL of these design decisions are likely to clash with your
# environment, so review and modify the code before attempting
# to use it!
# 1) The tool connects to an ESXi/Vcenter Server using the VMware
# APIs, then retrieves (and potentially displays) the information
# for individual Guests
# 2) This tool exits with the following return codes:
# 0 - If invoked WITHOUT the "-c" parameter, then the tool
# completed normally; if invoked WITH the "-c" parameter,
# then no VM was found with a matching name
# 1 - The tool was invoked with "-c" and a VM was found
# matching the name provided
# 252-254 - An invalid host name was provided with the "-c"
# parameter
# 255 - Invalid command-line parameter combination
# 3) Based in part on listallvms.py by sm
#
# KNOWN BUGS:
# 0) There is no error-handling for comm failures when attempting
# to contact the VMware environment
# 1) Hopefully I didn't munge anything when I sanitized this
# code for publication; I don't have access to a vSphere
# environment against which to test it now
#
# TO DO:
# 0) Explore handling comm issues that occur with VMware APIs
# 1) Re-implement using vSphere REST interface
#######################################################################
TOOL_VERSION_ = '100'
#######################################################################
# Change Log (Reverse Chronological Order)
# Who When______ What__________________________________________________
# dxb 2024-01-14 I really need to use "pylint" more
# dxb 2023-12-09 Sanitized and published to GitHub
# sm 2020-01-14 Make the credentials into dictionary
# sm 2020-01-14 Fix the display of multiple VMs when Guest
# is FT-enabled
# dxb 2019-12-20 Initial creation
#######################################################################
# Module Imports #
##################
# System-specific functions/parameters
import sys
# OS-specific functions
import os
from os import system, name
# Command-line argument parser
import argparse
# Exit handlers
import atexit
# Low-level network functions
import socket
# TLS/SSL wrapper for socket objects
import ssl
# VMware-provided ESXi APIs
from pyVmomi import vmodl
from pyVmomi import vim
from pyVim import connect
from pyVim.connect import SmartConnect, Disconnect
# Declare a Class (instead of a dictionary or variable names)
# of ANSI codes for screen control and Colors for text output
# Reference example --> ANSI_.BOLD_TEXT
class ANSI_:
"""
Define ANSI screen and color control variables that can be
referenced in print statements to highlight text
"""
INVERT_TEXT = '\033[7m'
EOL = '\033[0K'
UNDERLINE_TEXT = '\033[4m'
STRIKETHRU = '\033[09m'
SCREEN_HOME = '\033[0;0H'
GREEN_BLACK = '\033[32;40m'
YELLOW_BLACK = '\033[33;40m'
RED_BLACK = '\033[31;40m'
BLUE_BLACK = '\033[34;40m'
WHITE_BLACK = '\033[37;40m'
CYAN_RED = '\033[36;41m'
MAGENTA_BLACK = '\033[35;40m'
PINK_BLACK = '\033[95m'
BOLD_TEXT = '\033[1m'
BLINK_ON = '\033[5m'
ALL_OFF = '\033[0m'
# Create a Dictionary containing IP addresses of vSpheres,
# indexed by Data Center
VSPHERES_ = dict()
VSPHERES_['DC1'] = '10.2.4.30'
VSPHERES_['DC2'] = '10.2.4.60'
# Create a Dictionary containing User Credentials for vSphere, indexed
# by username and password
CREDENTIALS_ = dict()
CREDENTIALS_['USER'] = 'service_id'
CREDENTIALS_['PASSWORD'] = 'password'
# Define how tool was invoked
OUR_TOOL_ = os.path.realpath(__file__)
#######################################################################
# Function: vsphere_connect_func #
# Local Variables: SSL_OBJECT_ = An SSL socket object #
# Global Variables: VSPHERE_TARGET_ = Hostname/IP of the target #
# vSphere #
#######################################################################
def vsphere_connect_func(**kwargs):
"""
Create a connection to a vSphere host, including an Exit Handler
to close the connection at exit
Arguments: **kwargs = Double pointer to keyword arguments
referenced by socket methods
Returns: vsphere_connect_func.ESX, an object referencing the
connection to the vSphere host
"""
# Create an SSL socket object
# PROTOCOL_SSLv23 specifies both SSL and TLS support and is the
# most-interoperable option
SSL_OBJECT_ = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
# Instruct methods to not require an SSL cert; if one is supplied
# by the remote host, then no validity check is made
SSL_OBJECT_.verify_mode = ssl.CERT_NONE
# Connect to the vSphere
ESX_CONN_ = connect.SmartConnect(host=VSPHERE_TARGET_, \
user=CREDENTIALS_['USER'], pwd=CREDENTIALS_['PASSWORD'], \
sslContext=SSL_OBJECT_)
# Put the vSphere connector object into the kwargs for this function
vsphere_connect_func.ESX = ESX_CONN_
# Register an exit handler that will disconnect from the vSphere
# when this tool exits
atexit.register(connect.Disconnect, ESX_CONN_)
#######################################################################
# Function: print_vm_info_func #
# Local Variables: SPHERE_CONTENT_ #
# ROOT_DATA_ These are #
# VIEW_TYPE_ all variables #
# IS_RECURSIVE_ to hold and #
# VM_DATA_ manipulate the #
# VM_LIST_ data extracted #
# LINE_COUNT_ from the VMware #
# FT_SKIP_COUNT_ environment #
# THIS_VM_ #
# THIS_VM_DATA_ THIS_VM_NAME_ #
# THIS_VM_RAM_ THIS_VM_CPU_ #
# THIS_VM_STATE_ THIS_VM_TOOLS_ #
# THIS_VM_FT_ #
# Global Variables: vsphere_connect_func.ESX, the vSphere object #
#######################################################################
def print_vm_info_func(SYSTEM_NAME_):
"""
Gather and display information retrieved by vSphere
Arguments: SYSTEM_NAME_ - A string; if not blank, defining a
specific host name which will cause this function to
return "1" if the VM exists, "0" otherwise
Returns: Two integer values
If SYSTEM_NAME_ is blank, then the values are, in order,
LINE_COUNT_ and FT_SKIP_COUNT_
If SYSTEM_NAME_ is not blank, then the first value
returned is "1" if a VM with that name exists, "0"
otherwise; and the second value is always 0
"""
# Retrieve information from the ESX
SPHERE_CONTENT_ = vsphere_connect_func.ESX.RetrieveContent()
# Store all VMs that are available in the rootFolder of ESXi cluster
ROOT_DATA_ = SPHERE_CONTENT_.rootFolder
# Restrict View to only VMs
VIEW_TYPE_ = [vim.VirtualMachine]
# When looking in rootFolder, recurse
IS_RECURSIVE_ = True
# Get the data
VM_DATA_ = SPHERE_CONTENT_.viewManager.CreateContainerView(ROOT_DATA_, VIEW_TYPE_, IS_RECURSIVE_)
# Extract a list of VMs
VM_LIST_ = VM_DATA_.view
# Initalize a counter so I can put in a separator line
LINE_COUNT_ = 0
# Initalize a counter to track VMs I skip because they are FT images
FT_SKIP_COUNT_ = 0
# Cycle through the list of VMs
for THIS_VM_ in VM_LIST_:
# Get the block of info for this VM
THIS_VM_DATA_ = THIS_VM_.summary
# Get the name of the VM as it appears in the vCenter interface
THIS_VM_NAME_ = THIS_VM_DATA_.config.name
if ARGS_.d:
print('\tTHIS_VM_NAME_ is '+THIS_VM_NAME_)
# If passed a host name to check against, then do so here; if the
# host doesn't match, then skip the rest of the loop
if SYSTEM_NAME_ != '':
if SYSTEM_NAME_ != THIS_VM_NAME_:
continue
else:
# Match found - exit function here
return(1, 0)
# This next if/else block is used to exclude FT images on the
# other Data Center (that is, it prevents this tool from
# displaying the FT image of a DC1-based VM when processing
# the DC2 VMs)
if (THIS_VM_NAME_[0:3] == 'dc1' or THIS_VM_NAME_[0:3] == 'dc2') and THIS_VM_DATA_.runtime.powerState == 'poweredOn':
pass
else:
FT_SKIP_COUNT_ += 1
if ARGS_.d:
print('\t\tSkipping '+THIS_VM_NAME_+'FT_SKIP_COUNT_ is '+
str(FT_SKIP_COUNT_))
continue
# Get the RAM in MB
THIS_VM_RAM_ = THIS_VM_DATA_.config.memorySizeMB
# I want to display it in GB, so change units and round it
THIS_VM_RAM_ = int(THIS_VM_RAM_ / 1024)
# Get the number of Virtual CPUs
THIS_VM_CPU_ = THIS_VM_DATA_.config.numCpu
# Determine if this is an FT
if (THIS_VM_DATA_.runtime.faultToleranceState == 'running'):
THIS_VM_FT_ = ANSI_.BOLD_TEXT+'YES'+ANSI_.ALL_OFF
else:
THIS_VM_FT_ = ' NO'
# Determine if it is powered on; if running, also get VMTools status
if THIS_VM_DATA_.runtime.powerState == 'poweredOn':
THIS_VM_STATE_ = " On"
if THIS_VM_DATA_.guest.toolsStatus == 'toolsOk':
THIS_VM_TOOLS_ = 'YES'
else:
if THIS_VM_FT_ == ' NO':
THIS_VM_TOOLS_ = 'NO'
else:
continue
else:
THIS_VM_STATE_ = ANSI_.BOLD_TEXT+ANSI_.RED_BLACK+'Off'+ANSI_.ALL_OFF
# Since the VM is not running, I can't get a status of VMTools
THIS_VM_TOOLS_ = '---'
# Display the information
print('\t\t'+THIS_VM_NAME_+'\t '+THIS_VM_STATE_+'\t\t'+' '+
THIS_VM_TOOLS_+'\t\t\t'+' '+str(THIS_VM_RAM_)+'\t\t\t'+
' '+str(THIS_VM_CPU_)+'\t\t'+' '+THIS_VM_FT_)
# If this is the 5th record, print a separator line
if LINE_COUNT_ != 0 and (LINE_COUNT_ % 5 == 0):
print('\t\t' + 105* '-')
# Increment counter
LINE_COUNT_ += 1
# What I return depends on how tool was invoked
if (SYSTEM_NAME_ != ''):
# I got done and did not match the host name
LINE_COUNT_ = 0
FT_SKIP_COUNT_ = 0
# Return (in order) the count of VMs I displayed, and those skipped
return(LINE_COUNT_, FT_SKIP_COUNT_)
#################
# Program Start #
#################
DESC_TEXT_ = ('\n'+ANSI_.BOLD_TEXT+OUR_TOOL_+' - '+ANSI_.GREEN_BLACK+
'Virtual Machine Information Reporting Tool'+
ANSI_.BLUE_BLACK+' v'+TOOL_VERSION_+ANSI_.ALL_OFF)
HELP_TEXT_ = (DESC_TEXT_+'\n\n\t'+ ANSI_.BOLD_TEXT+'Usage:'+ANSI_.ALL_OFF+
' %(prog)s [ [ '+ANSI_.BOLD_TEXT+'-c'+ANSI_.BLUE_BLACK+
' <HOSTNAME>'+ANSI_.ALL_OFF+' | '+ANSI_.BOLD_TEXT+'-e'+
ANSI_.ALL_OFF+' | '+ANSI_.BOLD_TEXT+'-w'+ANSI_.ALL_OFF+
' ] | '+ANSI_.BOLD_TEXT+'-h'+ANSI_.ALL_OFF+' ]')
EPILOG_TEXT_ = ('\tThe '+ANSI_.BOLD_TEXT+'-c'+ANSI_.ALL_OFF+', '+
ANSI_.BOLD_TEXT+'-e'+ANSI_.ALL_OFF+' and '+
ANSI_.BOLD_TEXT+'-w'+ANSI_.ALL_OFF+
' command-line flags conflict with each other\n \n')
# Create an argument parser object
# usage=argparse.SUPPRESS - Prevents the normal "usage" header from
# appearing; I build my in "description"
# formatter_class=argparse.RawTextHelpFormatter - Allows me to
# control help screen formatting
COMMAND_LINE_ = argparse.ArgumentParser(usage=argparse.SUPPRESS,
description=HELP_TEXT_, epilog=EPILOG_TEXT_,
formatter_class=argparse.RawTextHelpFormatter,
add_help=True)
COMMAND_LINE_.add_argument('-c', action='store', default='',
metavar=ANSI_.BOLD_TEXT+'<HOSTNAME>'+ANSI_.ALL_OFF+
'\t\tLook up a specific host (use the "m" name, for example '+
ANSI_.BOLD_TEXT+'adc1snm0dbw00'+ANSI_.ALL_OFF+')',
help='\tConflicts with '+ANSI_.BOLD_TEXT+ANSI_.YELLOW_BLACK+
'-e'+ANSI_.ALL_OFF+' and '+ANSI_.BOLD_TEXT+ANSI_.YELLOW_BLACK+
'-w'+ANSI_.ALL_OFF+' command-line parameters'+
'\n\tExits with '+ANSI_.BOLD_TEXT+'1'+ANSI_.ALL_OFF+
' if the host exists, '+ANSI_.BOLD_TEXT+'0'+
ANSI_.ALL_OFF+' otherwise')
COMMAND_LINE_.add_argument('-d', action='store_true',
help="Enable debugging messages to "+ANSI_.BOLD_TEXT+
"stdout"+ANSI_.ALL_OFF)
COMMAND_LINE_.add_argument('-e', action='store_true',
help='Limit output to '+ANSI_.BOLD_TEXT+ANSI_.YELLOW_BLACK+
'DC1-based'+ANSI_.ALL_OFF+' VMs (conflicts with '+
ANSI_.BOLD_TEXT+'-c'+ANSI_.ALL_OFF+' and '+
ANSI_.BOLD_TEXT+'-w'+ANSI_.ALL_OFF+')')
COMMAND_LINE_.add_argument('-w', action='store_true',
help='Limit output to '+ANSI_.BOLD_TEXT+ANSI_.YELLOW_BLACK+
'DC2-based'+ANSI_.ALL_OFF+' VMs (conflicts with '+
ANSI_.BOLD_TEXT+'-c'+ANSI_.ALL_OFF+' and '+
ANSI_.BOLD_TEXT+'-e'+ANSI_.ALL_OFF+')')
# Parse the command-line based on the added arguments
ARGS_ = COMMAND_LINE_.parse_args()
if ARGS_.d:
print('ARGS_.c is ' + ARGS_.c)
print('ARGS_.d is ' + str(ARGS_.d))
print('ARGS_.e is ' + str(ARGS_.e))
print('ARGS_.w is ' + str(ARGS_.w))
# Validate command-line options
# -e and -w conflict
if ARGS_.e and ARGS_.w:
print(DESC_TEXT_+'\n\n\t'+ANSI_.BOLD_TEXT+ANSI_.MAGENTA_BLACK+
'FATAL ERROR: '+ANSI_.BLUE_BLACK+'-e'+ANSI_.RED_BLACK+
' conflicts with '+ANSI_.BLUE_BLACK+'-w'+ANSI_.ALL_OFF+'\n')
sys.exit(255)
# -c conflicts with -e and -w
if (ARGS_.e or ARGS_.w) and ARGS_.c != '':
print(DESC_TEXT_+'\n\n\t'+ANSI_.BOLD_TEXT+ANSI_.MAGENTA_BLACK+
'FATAL ERROR: '+ANSI_.BLUE_BLACK+'-c'+ANSI_.RED_BLACK+
' conflicts with '+ANSI_.BLUE_BLACK+'-e'+ANSI_.RED_BLACK+
' and '+ANSI_.BLUE_BLACK+'-w'+ANSI_.ALL_OFF+'\n')
sys.exit(255)
# Was -c specified?
if ARGS_.c != '':
# Yes, I need to validate the hostname
#####################################################################
# IMPORTANT NOTE #
# This logic was designed around a naming convention specific to #
# the environment I was in when I wrote this tool. It's almost a #
# guarantee that your environment uses a vastly different naming #
# convention for Linux hosts. Please adapt this code to work for #
# your naming convention! #
#####################################################################
if ARGS_.d:
print('ARGS_.c is ' + ARGS_.c)
print('ARGS_.c[0:1] is ' + ARGS_.c[0:1])
print('ARGS_.c[0:2] is ' + ARGS_.c[0:2])
print('ARGS_.c[4:6] is ' + ARGS_.c[4:6])
print('ARGS_.c[6:8] is ' + ARGS_.c[6:8])
# Must be 13 characters, no more or less
if len(ARGS_.c) != 13:
print(DESC_TEXT_+'\n\n\t'+ANSI_.BOLD_TEXT+ANSI_.MAGENTA_BLACK+
'FATAL ERROR A: '+ANSI_.ALL_OFF+ANSI_.BOLD_TEXT+ARGS_.c+
ANSI_.RED_BLACK+' is not a valid hostname (length)'+
ANSI_.ALL_OFF + '\n')
sys.exit(254)
elif (ARGS_.c[0:2] != 'dc'):
# First two must be 'dc'
print(DESC_TEXT_+'\n\n\t'+ANSI_.BOLD_TEXT+ANSI_.MAGENTA_BLACK+
'FATAL ERROR B: '+ANSI_.ALL_OFF+ANSI_.BOLD_TEXT+ARGS_.c+
ANSI_.RED_BLACK+' is not a valid hostname ('+ARGS_.c[0:2]+
')'+ANSI_.ALL_OFF+'\n')
sys.exit(253)
elif (ARGS_.c[4:6] != 'xx'):
# The 5th and 6th characters must be 'xx'
print(DESC_TEXT_+'\n\n\t'+ANSI_.BOLD_TEXT+ANSI_.MAGENTA_BLACK+
'FATAL ERROR C: '+ANSI_.ALL_OFF+ANSI_.BOLD_TEXT+ARGS_.c+
ANSI_.RED_BLACK+' is not a valid hostname ('+ARGS_.c[4:6]+
')'+ANSI_.ALL_OFF+'\n')
sys.exit(252)
elif (ARGS_.c[6:8] != '00'):
# The 7th and 8th characters must be '00'
print(DESC_TEXT_+'\n\n\t'+ANSI_.BOLD_TEXT+ANSI_.MAGENTA_BLACK+
'FATAL ERROR D: '+ANSI_.ALL_OFF+ANSI_.BOLD_TEXT+ARGS_.c+
ANSI_.RED_BLACK+' is not a valid hostname ('+ARGS_.c[6:8]+')'+
ANSI_.ALL_OFF+'\n')
sys.exit(251)
else:
# Determine Data Center based on 3rd character
if (ARGS_.c[2:1] == '1'):
VSPHERE_LIST_ = [VSPHERES_['DC1']]
else:
VSPHERE_LIST_ = [VSPHERES_['DC2']]
else:
#####################################################################
# IMPORTANT NOTE #
# This logic was designed around an environment with only two #
# data centers (or two ESXi infrastructures); if your environment #
# is different, then this logic won't work for you unless you #
# change it to match your environment #
#####################################################################
# -c was not used; check to see if -e or -w was specified
if (ARGS_.e or ARGS_.w):
# Yes - I already know that BOTH -e and -c were
# not specified, so I only have to test for one
if ARGS_.e:
VSPHERE_LIST_ = [VSPHERES_['DC1']]
else:
VSPHERE_LIST_ = [VSPHERES_['DC2']]
else:
# No, so I'm getting the full listing (both DCs)
VSPHERE_LIST_ = [VSPHERES_['DC1'], VSPHERES_['DC2']]
# VSPHERE_LIST_ is now populated with the list of vSphere
# servers I'll be contacting
# If not invoked with -c, ID this tool
if (ARGS_.c == ''):
print(DESC_TEXT_)
# Debugging ouput
if ARGS_.d:
print('VSPHERE_LIST_ is ' + str(VSPHERE_LIST_))
# Cycle through list of vSphere servers
for VSPHERE_TARGET_ in VSPHERE_LIST_:
if ARGS_.d:
print('VSPHERE_TARGET_ is ' + VSPHERE_TARGET_)
# Connect to the vSphere
vsphere_connect_func()
# What I do with the connection depends on how tool was invoked
if ARGS_.c != '':
# Look for a specific host
(WAS_FOUND_, ALWAYS_ZERO_) = print_vm_info_func(ARGS_.c)
if ARGS_.d:
print('WAS_FOUND_ is ' + str(WAS_FOUND_))
# Exit with an RC or "1" if I found the system,
# or "0" otherwise
sys.exit(WAS_FOUND_)
else:
# Print header
print('\n\t\t'+ANSI_.BOLD_TEXT+ANSI_.GREEN_BLACK+
'___VM_Name___\t__State__\t__Tools__\t\t__RAM(GB)__\t\t__CPU__\t\t__FT?__'
+ANSI_.ALL_OFF)
# Print the VM data
(THIS_DC_COUNT_, THIS_DC_SKIP_) = print_vm_info_func(ARGS_.c)
# Display count of VMs listed, and those skipped
print('\n\t\t'+ANSI_.BOLD_TEXT+str(THIS_DC_COUNT_)+
' VMs in this DC'+ANSI_.ALL_OFF+' ('+str(THIS_DC_SKIP_)+
' skipped)\n')
# End of if ARGS_.d
# End of if ARGS_.c != ''
# End of for VSPHERE_TARGET_ in VSPHERE_LIST_
if ARGS_.d:
print('\nEXITING\n')
# If I get here, I was NOT invoked with -c, and so I always exit
# with an RC of "0"
sys.exit(0)
# End of vmreport.py
####################