#!/usr/bin/env python
# vsphere-vm-info --- retrieve information about registered vm by name

# Author: Noah Friedman <friedman@splode.com>
# Created: 2017-11-02
# Public domain

# $Id: vsphere-vm-info,v 1.46 2019/08/23 22:59:40 friedman Exp $

# Commentary:
# Code:

from   __future__ import print_function
from   pyVmomi    import vim

import vspherelib     as vsl
import sys

#import httplib
#httplib.HTTPConnection.debuglevel = 1

hw_compat = {
    '14' : [ 'ESXi 6.7', 'Workstation Pro 14.x', 'Fusion 10.x', 'Player 14.x' ],
    '13' : [ 'ESXi 6.5' ],
    '11' : [ 'ESXi 6.0', 'Workstation 11.x', 'Fusion 7.x', 'Player 7.x' ],
    '10' : [ 'ESXi 5.5', 'Workstation 10.x', 'Fusion 6.x', 'Player 6.x' ],
     '9' : [ 'ESXi 5.1', 'Workstation 9.x',  'Fusion 5.x', 'Player 5.x' ],
     '8' : [ 'ESXi 5.0', 'Workstation 8.x',  'Fusion 4.x', 'Player 4.x' ],
     '7' : [ 'ESXi/ESX 4.x', 'Server 2.x', 'Workstation 6.5.x/7.x', 'Fusion 2.x/3.x', 'Player 3.x' ],
     '4' : [ 'ESX 3.x', 'Server 1.x', 'Lab Manager 2.x', 'ACE 2.x' ],
}

proplist = [
    'config.annotation',
    'config.bootOptions.bootDelay',
    'config.cpuHotAddEnabled',
    'config.cpuHotRemoveEnabled',
    'config.files.vmPathName',
    'config.guestFullName',
    'config.hardware.device',
    'config.hardware.memoryMB',
    'config.hardware.numCPU',
    'config.hardware.numCoresPerSocket',
    'config.memoryHotAddEnabled',
    'config.nestedHVEnabled',
    'config.template',
    'config.uuid',
    'config.version',

    'guest.hostName',
    'guest.net',

    'layoutEx.disk',
    'layoutEx.file',

    'name',
    'parent',

    #'resourcePool.name',         # separate MO, can't fetch from vm proplist
    'resourcePool',

    'summary.config.numEthernetCards',
    'summary.config.numVirtualDisks',
    'summary.runtime.bootTime',
    #'summary.runtime.host.name', # separate MO, can't fetch from vm proplist
    'summary.runtime.host',

    'summary.runtime.powerState',
    'summary.runtime.question',
    'summary.storage', ]

proplist_verbose = [
    'config.extraConfig',

    'config.instanceUuid',
    'config.locationId',
    'config.changeVersion',
    'config.tools.afterPowerOn',
    'config.tools.afterResume',
    'config.tools.beforeGuestReboot',
    'config.tools.beforeGuestShutdown',
    'config.tools.beforeGuestStandby',
    'config.tools.syncTimeWithHost',
    'config.tools.toolsUpgradePolicy',
    'config.flags.disableAcceleration',
    'config.flags.diskUuidEnabled',
    'config.flags.enableLogging',
    'config.flags.faultToleranceType',
    'config.flags.htSharing',
    'config.flags.monitorType',
    'config.flags.runWithDebugInfo',
    'config.flags.snapshotDisabled',
    'config.flags.snapshotLocked',
    'config.flags.snapshotPowerOffBehavior',
    'config.flags.virtualExecUsage',
    'config.flags.virtualMmuUsage',
    'config.guestId',
    'config.defaultPowerOps.defaultPowerOffType',
    'config.defaultPowerOps.defaultResetType',
    'config.defaultPowerOps.defaultSuspendType',
    'config.defaultPowerOps.powerOffType',
    'config.defaultPowerOps.resetType',
    'config.defaultPowerOps.standbyAction',
    'config.defaultPowerOps.suspendType',

    'guest.toolsStatus',
    'guest.toolsRunningStatus',
    'guest.toolsVersionStatus',
    'guest.toolsVersionStatus2',
    'guest.toolsVersion',
    'guest.guestState',
    'guest.screen.width',
    'guest.screen.height',
    'guest.ipStack',
]

moId_types = [ vim.HostSystem, vim.ResourcePool ]
moId_map   = {}
folder_map = None


def get_args():
    p = vsl.ArgumentParser( loadrc=True )
    p.add( '-a', '--all',     action='store_true', help='Retrieve all known VMs' )
    p.add( '-v', '--verbose', action='count',      help='Display extended info' )
    p.add( '-R', '--refresh', action='store_true', help='Refresh storage info' )
    p.add( 'vm', nargs='*',                        help='VM names, if not all' )

    args = p.parse()
    if not args.all and not args.vm:
        vsl.printerr( 'Specify VM names or -a (--all)' )
        sys.exit( 1 )
    return args


def storage_size( storage ):
    comb = storage.committed + storage.uncommitted
    return vsl.scale_size( comb )

def plural( text, n ):
    if abs( int( n ) ) != 1:
        text += 's'
    return text


stdindent  =  -17
indentL    = stdindent - 3
indentS    = " " * abs( indentL )
farindentL = stdindent - 10
farindentS = " " * abs( farindentL )

def p( *parm ):
    if parm[ 1 ] is None:
        return
    w = stdindent
    if isinstance( parm[ 0 ], (int, long) ):
        w = parm[ 0 ]
        parm = parm[ 1: ]
    end = '\n'
    if isinstance( parm[ -1 ], (str, unicode) ) and parm[ -1 ] == "":
        end  = ''
        parm = parm[ : -1 ]

    if len( parm ) > 1 and isinstance( parm[ 1 ], list):
        val = str.join( '\n' + farindentS + indentS + '   ', map( str, parm[ 1 ] ))
    else:
        val = str.join( ' ', map( str, parm[ 1: ] ) )
    s = '%*s : %s' % (w, parm[ 0 ], val )
    print( s, end=end )

def pi( *parm ):
    print( indentS, end="" )
    p( *parm )

def pf( label, dic, attrs, extra='' ):
    p( label, extra )
    for n, attr in enumerate( attrs ):
        try:
            if n == 0 and not extra:
                fn = p
            else:
                fn = pi

            if type( attr ) is tuple:
                fn( farindentL, attr[0], dic[ attr[1] ] )
            else:
                fn( farindentL, attr, dic[ attr ] )

        except KeyError:
            pass


class DisplayVM( object ):
    def __init__( self, vsi, vmprops, verbose=False, sep="" ):
        self.vsi = vsi
        self.vmprops = vmprops
        self.vm = vsl.flat_to_nested_dict( vmprops, objtype=vsl.pseudoPropAttr )
        try:
            self.vm.config.extraConfig = vsl.flat_to_nested_dict(
                vsl.attr_to_dict( self.vm.config.extraConfig ),
                objtype=vsl.pseudoPropAttr )
        except AttributeError:
            pass
        self.verbose = verbose
        self.sep = sep
        self.display()

    def display( self ):
        vm = self.vm

        self.display_name()
        self.display_location()
        self.display_resource_pool()
        if self.verbose:
            p( 'Guest family', vm.config.guestId )
        p( 'Guest type', vm.config.guestFullName )
        self.display_hw_ver()
        self.display_cpu()
        self.display_mem()
        p( 'NICs',    vm.summary.config.numEthernetCards )
        p( 'Disks',   vm.summary.config.numVirtualDisks )
        p( 'Storage', storage_size( vm.summary.storage ))
        try:
            p( 'Nested HV', vm.config.nestedHVEnabled )
        except AttributeError:
            pass
        self.display_host()
        self.display_power_state()
        self.display_networks()
        self.display_disks()

        if self.verbose:
            self.display_dns()
            self.display_tools()
            self.display_power_ops()
            self.display_flags()
            self.display_guest()
            if self.verbose > 1:
                self.display_extraconfig_props()
            else:
                self.display_vnc()
                self.display_guestinfo_props()
            self.display_annotation()

        self.display_question()
        print( self.sep )

    def display_name( self ):
        if self.vm.config.template:
            p( 'VM Name', self.vm.name, '    [TEMPLATE]' )
        else:
            p( 'VM Name', self.vm.name )
            try:
                hn = self.vm.guest.hostName
                if hn and hn != self.vm.name:
                    p( 'VM Guest Name', hn )
            except AttributeError:
                pass

    def display_location( self ):
        vm      = self.vm
        vm_conf = self.vm.config

        p( 'VM moId', vm.obj._moId )
        p( 'VM UUID', vm_conf.uuid)
        if self.verbose:
            p( 'Instance Id',    vm_conf.instanceUuid )
            try:
                p( 'Location Id', vm_conf.locationId)
            except AttributeError:
                pass
            p( 'Config modtime', vm_conf.changeVersion )
        p( 'VMX', vm_conf.files.vmPathName )

        try:
            folder = folder_map[ vm.parent ]
            p( 'Folder', folder )
        except (AttributeError, KeyError):
            pass

    def display_resource_pool( self ):
        try:
            resourcePool = self.vm.resourcePool
            p( 'Resource Pool', moId_map[ resourcePool._moId ] )
        except KeyError:
            p( 'Resource Pool', resourcePool._moId )
        except AttributeError:
            pass

    def display_hw_ver( self ):
        hw_ver = self.vm.config.version[ 4: ]  # skip 'vmx-'
        if hw_ver[ 0 ] == '0':
            hw_ver = hw_ver[ 1: ]
        try:
            hw_desc = '\t({})'.format( hw_compat[ hw_ver ][0] )
        except KeyError:
            hw_desc = ""
        hw_ver += hw_desc
        p( 'HW version',  hw_ver )

    def display_cpu( self ):
        vm_conf = self.vm.config
        hw_conf = self.vm.config.hardware
        numCPU  = hw_conf.numCPU

        coreargs = [plural( 'core', numCPU )]
        if getattr( hw_conf, 'numCoresPerSocket', None ):
            nsockets = numCPU / hw_conf.numCoresPerSocket
            coreargs.append( plural('%d socket' % nsockets, nsockets ))
        if getattr( vm_conf, 'cpuHotAddEnabled', False ):
            coreargs.append( 'hot add enabled' )
        if getattr( vm_conf, 'cpuHotRemoveEnabled', False ):
            coreargs.append( 'hot remove enabled' )

        p( 'CPU', numCPU, str.join( ', ', coreargs ) )

    def display_mem( self ):
        memoryMB = self.vm.config.hardware.memoryMB
        memargs = [ vsl.scale_size( memoryMB * 2**20 ) ]
        if getattr( self.vm.config, 'memoryHotAddEnabled', False ):
            memargs.append( 'hot add enabled' )
        p( 'Memory', str.join( ', ', memargs ))

    def display_host( self ):
        host = self.vm.summary.runtime.host
        try:
            p( 'Hypervisor', moId_map[ host._moId ] )
        except KeyError:
            p( 'Hypervisor', host._moId )

    def display_power_state( self ):
        rt_conf = self.vm.summary.runtime
        p( 'Boot delay',  self.vm.config.bootOptions.bootDelay, "ms" )
        p( 'State',       rt_conf.powerState )
        try:
            p( 'Boot time', rt_conf.bootTime )
        except AttributeError:
            pass

    def display_tools( self ):
        pf( 'Tools ', self.vm.config.tools,
            ( ('Power On' , 'afterPowerOn'),
              ('Resume'   , 'afterResume'),
              ('Standby'  , 'beforeGuestStandby'),
              ('Shutdown' , 'beforeGuestShutdown'),
              ('Reboot'   , 'beforeGuestReboot'),
              ('Upgrade'  , 'toolsUpgradePolicy'),
              ('syncTime' , 'syncTimeWithHost'), ))

    def display_guest( self ):
        pf( 'Guest Tools', self.vm.guest,
            ( ('Tools Status',    'toolsStatus'),
              ('Running Status',  'toolsRunningStatus'),
              ('Version Status',  'toolsVersionStatus'),
              ('Version Status2', 'toolsVersionStatus2'),
              ('Version',         'toolsVersion'),
              ('State',           'guestState'),
            ))
        try:
            pf( 'Screen', self.vm.guest.screen,
                 ( ('Width',    'width'),
                   ('Height',  'height'),
                 ))
        except AttributeError:
            pass

    def display_flags( self ):
        pf( 'Flags', self.vm.config.flags,
            sorted( ( 'disableAcceleration',
                      'enableLogging',
                      'runWithDebugInfo',
                      'monitorType',
                      'htSharing',
                      'snapshotDisabled',
                      'snapshotLocked',
                      'diskUuidEnabled',
                      'virtualMmuUsage',
                      'virtualExecUsage',
                      'snapshotPowerOffBehavior',
                      'faultToleranceType', )))

    def display_power_ops( self ):
        pf( 'Power Ops', self.vm.config.defaultPowerOps,
            sorted( ( 'powerOffType',
                      'suspendType',
                      'resetType',
                      'defaultPowerOffType',
                      'defaultSuspendType',
                      'defaultResetType',
                      'standbyAction', )))

    def display_vnc( self ):
        try:
            vnc = self.vm.config.extraConfig.RemoteDisplay.vnc
            if getattr( vnc, 'key', False ):
                vnc.key = '(set)'
            else:
                vnc.key = 'None'
            pf( 'VNC', vnc,
                ( 'enabled',
                  'port',
                  'key', ))
        except AttributeError:
            return

    def display_extraconfig_props( self ):
        prop = vsl.attr_to_dict( self.vmprops[ 'config.extraConfig' ] )
        for key in prop:
            val = prop[ key ]
            if val == '':
                val = ' '
            elif val.find( '\n' ) > 0:
                val = [ s.strip( '\t\r' ) for s in val.split( '\n' ) ]
                val = str.join( '\\n', val )
            prop[ key ] = val
        pf( 'ExtraConfig', prop, sorted( prop ))

    def display_guestinfo_props( self ):
        ec = self.vmprops[ 'config.extraConfig' ]
        prop = dict( map( lambda elt: (elt.key[ 10: ], elt.value.strip( ' \t\r' ) ),
                          filter( lambda elt: elt.key.find( 'guestinfo.' ) == 0, ec ) ))
        if prop:
            for key in prop:
                val = prop[ key ]
                if val.find( '\n' ) > 0:
                    val = [ s.strip() for s in val.split( '\n' ) ]
                    val = str.join( '\\n', val )
                prop[ key ] = '"{}"'.format( val )
            pf( 'GuestInfo', prop, sorted( prop ))

    def display_dns( self ):
        dns_list = self.vsi.vmguest_dns_config( self.vm )
        for dns in dns_list:
            if not dns[ 'domain' ]:
                dns[ 'domain' ] = ' '
            pf( 'DNS', dns,
                 ( 'domain',
                   'server',
                   'search', ))

    def display_networks( self ):
        eth = vim.vm.device.VirtualEthernetCard
        sw_map = { eth.NetworkBackingInfo                : 'vSwitch',
                   eth.DistributedVirtualPortBackingInfo : 'dvportgroup', }

        if self.verbose:
            routes = self.vsi.vmguest_ip_routes( self.vm )
        else:
            routes = None

        for nic in self.vsi.vmguest_nic_info( self.vm ):
            try:
                backing = sw_map[ type( nic[ 'backing' ])]
            except KeyError:
                backing = 'unknown switch type'

            pf( nic[ 'label' ], nic,
                ( 'macAddress', ),
                extra='{} on "{}" ({})'.format(
                    nic[ 'type' ], nic[ 'netlabel' ], backing) )


            if self.verbose:
                obj = nic[ 'obj' ]
                con = obj.connectable
                pi( farindentL, 'wakeOnLanEnabled',  obj.wakeOnLanEnabled )
                pi( farindentL, 'startConnected',    con.startConnected )
                pi( farindentL, 'connected',         con.connected )
                pi( farindentL, 'allowGuestControl', con.allowGuestControl )

            cidr = nic.get( 'ip' )
            if cidr:
                pi( farindentL, 'IP Addresses', cidr )

            if routes:
                nicroutes = routes.pop( 0 )
                if not nicroutes:
                    continue

                formatted = []
                #width = max( len( elt.get( 'network', '' )) for elt in nicroutes )
                width = 0
                for elt in nicroutes:
                    gw = elt.get( 'gateway', None )
                    if gw:
                        s = '{1:<{0}} via {2}'.format( width, elt[ 'network' ], gw )
                    else:
                        s = elt[ 'network' ]
                    formatted.append( s )
                pi( farindentL, 'Routes', formatted )


    def display_disks( self ):
        for disk in self.vsi.vmguest_disk_info( self.vm ):
            disk[ 'capacity' ]  = vsl.scale_size( disk[ 'capacity' ] );
            disk[ 'allocated' ] = vsl.scale_size( disk[ 'allocated' ] );

            vfrcache = 'vflash read cache'
            try:
                desc = '{}, blk={}'.format(
                    vsl.scale_size( disk[ 'vflash_reserve' ] ),
                    vsl.scale_size( disk[ 'vflash_blksz'   ] ) )
                disk[ vfrcache ] = desc
            except KeyError:
                pass

            pf( disk[ 'label' ], disk,
                ( 'capacity',
                  'allocated',
                  'device',
                  'backing',
                  'deviceName',
                  'diskMode',
                  vfrcache, ),
                extra=disk[ 'fileName' ] )


    def _display_text( self, label, text, *options ):
        text = vsl.fold_text( text, maxlen=70, indent=abs( indentL ) )
        if text.find ("\n") >= 0:
            text = "\n" + indentS + text
        if text != "":
            p( label, text )
        if options:
            print()
            for choice in options:
                print( indentS, choice )

    def display_annotation( self ):
        try:
            self._display_text( 'Annotation', self.vm.config.annotation )
        except:
            pass

    def display_question( self ):
        try:
            question = self.vm.summary.runtime.question
            choices = [ '\t[{}] {}'.format( choice.key, choice.summary )
                        for choice in question.choice.choiceInfo ]
            try:
                choices[ question.choice.defaultIndex ] += ' (default)'
            except (KeyError, AttributeError):
                pass
            self._display_text( 'Question', question.text, *choices )
        except AttributeError:
            pass


def init_folder_map( vsi ):
    timer = vsl.Timer('init folder map')
    p2f = vsi.path_to_subfolder_map( 'vm' )
    global folder_map
    folder_map = vsl.inverted_dict ( p2f )
    timer.report()

def init_moId_map( vsi ):
    timer = vsl.Timer('init moId map')
    global moId_map
    moId_map = dict( map( lambda o: ( str( o[ 'obj' ]._moId ), o[ 'name' ] ),
                          vsi.get_obj_props( moId_types, [ 'name' ] )))
    timer.report()


def display_vmlist( vsi, vmlist, verbose ):
    sep = '-' * 78
    timer = vsl.Timer('print')
    n = len( vmlist )
    for vm in vmlist:
        n -= 1
        if not n:
            sep = ""
        DisplayVM( vsi, vm, verbose, sep=sep )
    timer.report()

def main():
    args = get_args()
    vsi  = vsl.vmomiConnect( args )

    init_folder_map( vsi )
    init_moId_map( vsi )

    if args.verbose:
        proplist.extend( proplist_verbose )

    if args.vm:
        vmlist = vsi.search_by_name( args.vm )
        if not vmlist:
            return
        if args.refresh:
            for vm in vmlist:
                vm.RefreshStorageInfo()
        vmlist = vsi.get_obj_props( [vim.VirtualMachine], proplist, vmlist,
                                    ignoreInvalidProps=True )
    else:
        vmlist = vsi.get_obj_props( [vim.VirtualMachine], proplist,
                                    ignoreInvalidProps=True )
        vmlist.sort( key=lambda elt: elt[ 'name' ] )

    display_vmlist( vsi, vmlist, args.verbose )


##########

if __name__ == '__main__':
    main()

# eof
