2018-06-04 01:09:42 -04:00
#!/usr/bin/env python3
2018-06-06 01:47:53 -04:00
# pvcf.py - Supplemental functions for the PVC CLI client
# Part of the Parallel Virtual Cluster (PVC) system
#
# Copyright (C) 2018 Joshua M. Boniface <joshua@boniface.me>
#
# This program 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.
#
# This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
#
###############################################################################
2018-06-07 00:12:55 -04:00
import os , sys , libvirt , uuid , kazoo . client , lxml . objectify , click , ansiiprint
2018-06-05 01:39:59 -04:00
2018-06-06 12:28:15 -04:00
#
# Validate a UUID
#
def validateUUID ( dom_uuid ) :
try :
2018-06-06 13:03:02 -04:00
uuid . UUID ( dom_uuid )
2018-06-06 12:28:15 -04:00
return True
2018-06-06 13:03:02 -04:00
except :
2018-06-06 12:28:15 -04:00
return False
2018-06-05 01:39:59 -04:00
#
# Connect and disconnect from Zookeeper
#
def startZKConnection ( zk_host ) :
zk = kazoo . client . KazooClient ( hosts = zk_host )
zk . start ( )
return zk
def stopZKConnection ( zk ) :
zk . stop ( )
zk . close ( )
return 0
#
# XML information parsing functions
#
2018-06-05 18:45:54 -04:00
2018-06-11 01:23:53 -04:00
# Get the main details for a VM object from XML
def getDomainMainDetails ( parsed_xml ) :
2018-06-05 18:45:54 -04:00
# Get the information we want from it
2018-06-11 01:23:53 -04:00
duuid = str ( parsed_xml . uuid )
dname = str ( parsed_xml . name )
dmemory = str ( parsed_xml . memory )
dmemory_unit = str ( parsed_xml . memory . attrib [ ' unit ' ] )
2018-06-11 12:26:51 -04:00
if dmemory_unit == ' KiB ' :
dmemory = str ( int ( dmemory ) * 1024 )
elif dmemory_unit == ' GiB ' :
dmemory = str ( int ( dmemory ) / 1024 )
2018-06-11 01:23:53 -04:00
dvcpu = str ( parsed_xml . vcpu )
2018-06-05 18:45:54 -04:00
try :
dvcputopo = ' {} / {} / {} ' . format ( parsed_xml . cpu . topology . attrib [ ' sockets ' ] , parsed_xml . cpu . topology . attrib [ ' cores ' ] , parsed_xml . cpu . topology . attrib [ ' threads ' ] )
except :
dvcputopo = ' N/A '
2018-06-11 01:23:53 -04:00
2018-06-11 12:26:51 -04:00
return duuid , dname , dmemory , dvcpu , dvcputopo
2018-06-11 01:23:53 -04:00
# Get long-format details
def getDomainExtraDetails ( parsed_xml ) :
2018-06-05 01:39:59 -04:00
dtype = parsed_xml . os . type
darch = parsed_xml . os . type . attrib [ ' arch ' ]
dmachine = parsed_xml . os . type . attrib [ ' machine ' ]
2018-06-11 01:23:53 -04:00
dconsole = parsed_xml . devices . console . attrib [ ' type ' ]
demulator = parsed_xml . devices . emulator
return dtype , darch , dmachine , dconsole , demulator
# Get CPU features
def getDomainCPUFeatures ( parsed_xml ) :
2018-06-05 01:39:59 -04:00
dfeatures = [ ]
for feature in parsed_xml . features . getchildren ( ) :
dfeatures . append ( feature . tag )
2018-06-11 01:23:53 -04:00
return dfeatures
# Get disk devices
def getDomainDisks ( parsed_xml ) :
2018-06-05 01:39:59 -04:00
ddisks = [ ]
2018-06-05 12:27:41 -04:00
for device in parsed_xml . devices . getchildren ( ) :
if device . tag == ' disk ' :
disk_attrib = device . source . attrib
2018-06-05 18:45:54 -04:00
disk_target = device . target . attrib
2018-06-05 12:27:41 -04:00
disk_type = device . attrib [ ' type ' ]
if disk_type == ' network ' :
2018-06-05 18:45:54 -04:00
disk_obj = { ' type ' : disk_attrib . get ( ' protocol ' ) , ' name ' : disk_attrib . get ( ' name ' ) , ' dev ' : disk_target . get ( ' dev ' ) , ' bus ' : disk_target . get ( ' bus ' ) }
2018-06-05 12:27:41 -04:00
elif disk_type == ' file ' :
2018-06-05 18:45:54 -04:00
disk_obj = { ' type ' : ' file ' , ' name ' : disk_attrib . get ( ' file ' ) , ' dev ' : disk_target . get ( ' dev ' ) , ' bus ' : disk_target . get ( ' bus ' ) }
2018-06-05 12:27:41 -04:00
else :
disk_obj = { }
ddisks . append ( disk_obj )
2018-06-11 01:23:53 -04:00
return ddisks
# Get network devices
def getDomainNetworks ( parsed_xml ) :
dnets = [ ]
for device in parsed_xml . devices . getchildren ( ) :
2018-06-05 12:27:41 -04:00
if device . tag == ' interface ' :
net_type = device . attrib [ ' type ' ]
net_mac = device . mac . attrib [ ' address ' ]
net_bridge = device . source . attrib [ net_type ]
net_model = device . model . attrib [ ' type ' ]
net_obj = { ' type ' : net_type , ' mac ' : net_mac , ' source ' : net_bridge , ' model ' : net_model }
dnets . append ( net_obj )
2018-06-11 01:23:53 -04:00
return dnets
# Get controller devices
def getDomainControllers ( parsed_xml ) :
dcontrollers = [ ]
for device in parsed_xml . devices . getchildren ( ) :
2018-06-05 12:27:41 -04:00
if device . tag == ' controller ' :
controller_type = device . attrib [ ' type ' ]
try :
controller_model = device . attrib [ ' model ' ]
except KeyError :
controller_model = ' none '
controller_obj = { ' type ' : controller_type , ' model ' : controller_model }
dcontrollers . append ( controller_obj )
2018-06-05 01:39:59 -04:00
2018-06-11 01:23:53 -04:00
return dcontrollers
# Parse an XML object
def getDomainXML ( zk , dom_uuid ) :
try :
xml = zk . get ( ' /domains/ %s /xml ' % dom_uuid ) [ 0 ] . decode ( ' ascii ' )
except :
return None
# Parse XML using lxml.objectify
parsed_xml = lxml . objectify . fromstring ( xml )
return parsed_xml
# Root function
def getInformationFromXML ( zk , uuid , long_output ) :
# Obtain the contents of the XML from Zookeeper
try :
dstate = zk . get ( ' /domains/ {} /state ' . format ( uuid ) ) [ 0 ] . decode ( ' ascii ' )
dhypervisor = zk . get ( ' /domains/ {} /hypervisor ' . format ( uuid ) ) [ 0 ] . decode ( ' ascii ' )
dlasthypervisor = zk . get ( ' /domains/ {} /lasthypervisor ' . format ( uuid ) ) [ 0 ] . decode ( ' ascii ' )
except :
return None
if dlasthypervisor == ' ' :
dlasthypervisor = ' N/A '
parsed_xml = getDomainXML ( zk , uuid )
2018-06-11 12:26:51 -04:00
duuid , dname , dmemory , dvcpu , dvcputopo = getDomainMainDetails ( parsed_xml )
2018-06-11 01:23:53 -04:00
if long_output == True :
dtype , darch , dmachine , dconsole , demulator = getDomainExtraDetails ( parsed_xml )
dfeatures = getDomainCPUFeatures ( parsed_xml )
ddisks = getDomainDisks ( parsed_xml )
2018-06-12 11:58:47 -04:00
dnets = getDomainNetworks ( parsed_xml )
2018-06-11 01:23:53 -04:00
dcontrollers = getDomainControllers ( parsed_xml )
2018-06-05 18:45:54 -04:00
# Format a nice output; do this line-by-line then concat the elements at the end
ainformation = [ ]
2018-06-07 00:12:55 -04:00
ainformation . append ( ' {} Virtual machine information: {} ' . format ( ansiiprint . bold ( ) , ansiiprint . end ( ) ) )
2018-06-05 18:45:54 -04:00
ainformation . append ( ' ' )
# Basic information
2018-06-07 00:12:55 -04:00
ainformation . append ( ' {} UUID: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , duuid ) )
ainformation . append ( ' {} Name: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dname ) )
2018-06-11 12:26:51 -04:00
ainformation . append ( ' {} Memory [MiB]: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dmemory ) )
2018-06-07 00:12:55 -04:00
ainformation . append ( ' {} vCPUs: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dvcpu ) )
ainformation . append ( ' {} Topology [S/C/T]: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dvcputopo ) )
2018-06-05 18:45:54 -04:00
if long_output == True :
# Virtualization information
ainformation . append ( ' ' )
2018-06-07 00:12:55 -04:00
ainformation . append ( ' {} Emulator: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , demulator ) )
ainformation . append ( ' {} Type: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dtype ) )
ainformation . append ( ' {} Arch: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , darch ) )
ainformation . append ( ' {} Machine: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dmachine ) )
ainformation . append ( ' {} Features: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , ' ' . join ( dfeatures ) ) )
2018-06-05 18:45:54 -04:00
# PVC cluster information
ainformation . append ( ' ' )
2018-06-07 00:12:55 -04:00
dstate_colour = {
' start ' : ansiiprint . green ( ) ,
' stop ' : ansiiprint . red ( ) ,
' shutdown ' : ansiiprint . yellow ( ) ,
' migrate ' : ansiiprint . blue ( ) ,
' unmigrate ' : ansiiprint . blue ( )
}
ainformation . append ( ' {} State: {} {} {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dstate_colour [ dstate ] , dstate , ansiiprint . end ( ) ) )
ainformation . append ( ' {} Active Hypervisor: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dhypervisor ) )
ainformation . append ( ' {} Last Hypervisor: {} {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , dlasthypervisor ) )
2018-06-05 18:45:54 -04:00
if long_output == True :
# Disk list
ainformation . append ( ' ' )
2018-06-07 00:21:15 -04:00
name_length = 0
2018-06-05 18:45:54 -04:00
for disk in ddisks :
2018-06-07 00:21:15 -04:00
_name_length = len ( disk [ ' name ' ] ) + 1
if _name_length > name_length :
name_length = _name_length
ainformation . append ( ' {0} Disks: {1} {2} ID Type { 3: < {width} } Dev Bus {4} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , ansiiprint . bold ( ) , ' Name ' , ansiiprint . end ( ) , width = name_length ) )
for disk in ddisks :
ainformation . append ( ' {0: <3} {1: <5} { 2: < {width} } {3: <4} {4: <5} ' . format ( ddisks . index ( disk ) , disk [ ' type ' ] , disk [ ' name ' ] , disk [ ' dev ' ] , disk [ ' bus ' ] , width = name_length ) )
2018-06-05 18:45:54 -04:00
# Network list
ainformation . append ( ' ' )
2018-06-07 00:21:53 -04:00
ainformation . append ( ' {} Interfaces: {} {} ID Type Source Model MAC {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , ansiiprint . bold ( ) , ansiiprint . end ( ) ) )
2018-06-05 18:45:54 -04:00
for net in dnets :
2018-06-07 00:21:53 -04:00
ainformation . append ( ' {0: <3} {1: <8} {2: <10} {3: <8} {4} ' . format ( dnets . index ( net ) , net [ ' type ' ] , net [ ' source ' ] , net [ ' model ' ] , net [ ' mac ' ] ) )
2018-06-05 18:45:54 -04:00
# Controller list
ainformation . append ( ' ' )
2018-06-07 00:21:15 -04:00
ainformation . append ( ' {} Controllers: {} {} ID Type Model {} ' . format ( ansiiprint . purple ( ) , ansiiprint . end ( ) , ansiiprint . bold ( ) , ansiiprint . end ( ) ) )
2018-06-05 18:45:54 -04:00
for controller in dcontrollers :
2018-06-07 00:21:15 -04:00
ainformation . append ( ' {0: <3} {1: <14} {2: <8} ' . format ( dcontrollers . index ( controller ) , controller [ ' type ' ] , controller [ ' model ' ] ) )
2018-06-05 18:45:54 -04:00
# Join it all together
information = ' \n ' . join ( ainformation )
return information
2018-06-05 01:39:59 -04:00
#
# Cluster search functions
#
def getClusterDomainList ( zk ) :
# Get a list of UUIDs by listing the children of /domains
uuid_list = zk . get_children ( ' /domains ' )
name_list = [ ]
# For each UUID, get the corresponding name from the data
for uuid in uuid_list :
name_list . append ( zk . get ( ' /domains/ %s ' % uuid ) [ 0 ] . decode ( ' ascii ' ) )
return uuid_list , name_list
def searchClusterByUUID ( zk , uuid ) :
try :
# Get the lists
uuid_list , name_list = getClusterDomainList ( zk )
# We're looking for UUID, so find that element ID
index = uuid_list . index ( uuid )
# Get the name_list element at that index
name = name_list [ index ]
except ValueError :
# We didn't find anything
2018-06-06 12:28:15 -04:00
return None
2018-06-05 01:39:59 -04:00
return name
def searchClusterByName ( zk , name ) :
try :
# Get the lists
uuid_list , name_list = getClusterDomainList ( zk )
# We're looking for name, so find that element ID
index = name_list . index ( name )
# Get the uuid_list element at that index
uuid = uuid_list [ index ]
except ValueError :
# We didn't find anything
2018-06-06 12:28:15 -04:00
return None
2018-06-05 01:39:59 -04:00
return uuid
#
# Allow mutually exclusive options in Click
#
class MutuallyExclusiveOption ( click . Option ) :
def __init__ ( self , * args , * * kwargs ) :
2018-06-05 20:03:40 -04:00
meargs = kwargs . pop ( ' mutually_exclusive ' , [ ] )
_me_arg = [ ]
_me_func = [ ]
for arg in meargs :
_me_arg . append ( arg [ ' argument ' ] )
_me_func . append ( arg [ ' function ' ] )
self . me_arg = set ( _me_arg )
self . me_func = set ( _me_func )
2018-06-05 01:39:59 -04:00
help = kwargs . get ( ' help ' , ' ' )
2018-06-05 20:03:40 -04:00
if self . me_func :
ex_str = ' , ' . join ( self . me_arg )
2018-06-05 01:39:59 -04:00
kwargs [ ' help ' ] = help + (
2018-06-05 20:03:40 -04:00
' Mutually exclusive with ` ' + ex_str + ' `. '
2018-06-05 01:39:59 -04:00
)
2018-06-05 20:03:40 -04:00
2018-06-05 01:39:59 -04:00
super ( MutuallyExclusiveOption , self ) . __init__ ( * args , * * kwargs )
def handle_parse_result ( self , ctx , opts , args ) :
2018-06-05 20:03:40 -04:00
if self . me_func . intersection ( opts ) and self . name in opts :
2018-06-05 01:39:59 -04:00
raise click . UsageError (
" Illegal usage: ` {} ` is mutually exclusive with "
" arguments ` {} `. " . format (
2018-06-05 20:03:40 -04:00
self . opts [ - 1 ] ,
' , ' . join ( self . me_arg )
2018-06-05 01:39:59 -04:00
)
)
return super ( MutuallyExclusiveOption , self ) . handle_parse_result (
ctx ,
opts ,
args
)