2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-01-11 18:42:29 +00:00

Basic VCenter plugin

This provides nodeinventory (mac and -s most interestingly),
nodepower, nodesetboot (and by extension, nodedeploy -n),
and nodeconsole (console.method=vcenter).
This commit is contained in:
Jarrod Johnson
2025-04-25 13:19:38 -04:00
parent 0bdcaecf82
commit 85249ae71b
2 changed files with 316 additions and 1 deletions

View File

@@ -369,7 +369,7 @@ node = {
'the managed node. If not specified, then console '
'is disabled. "ipmi" should be specified for most '
'systems if console is desired.'),
'validvalues': ('ssh', 'ipmi', 'openbmc', 'tsmsol'),
'validvalues': ('ssh', 'ipmi', 'openbmc', 'tsmsol', 'vcenter'),
},
# 'virtualization.host': {
# 'description': ('Hypervisor where this node does/should reside'),

View File

@@ -0,0 +1,315 @@
import codecs
import confluent.util as util
import confluent.messages as msg
import eventlet
import json
import struct
webclient = eventlet.import_patched('pyghmi.util.webclient')
import eventlet.green.socket as socket
import eventlet
import confluent.interface.console as conapi
def fixuuid(baduuid):
# VMWare changes the endian stuff in BIOS
uuidprefix = (baduuid[:8], baduuid[9:13], baduuid[14:18])
a = codecs.encode(struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]),
'hex')
a = util.stringify(a)
uuid = (a[:8], a[8:12], a[12:16], baduuid[19:23], baduuid[24:])
return '-'.join(uuid).lower()
class VmConsole(conapi.Console):
def __init__(self, host, port, tls):
if tls:
raise Exception('TODO') # need to have a framework for storing host certificate
self.host = host
self.port = port
self.socket = None
def connect(self, callback):
self.connected = True
self.socket = socket.create_connection((self.host, self.port))
self.datacallback = callback
self.recvr = eventlet.spawn(self.recvdata)
def write(self, data):
self.socket.sendall(data)
def close(self):
if self.socket:
self.socket.close()
def recvdata(self):
while self.connected:
try:
pendingdata = self.socket.recv(1024)
except Exception as e:
print(repr(e))
pendingdata = ''
if pendingdata == '':
self.datacallback(conapi.ConsoleEvent.Disconnect)
return
self.datacallback(pendingdata)
class VmwApiClient:
def __init__(self, vcsa, user, password, configmanager):
self.cachedurls = {}
self.user = user
self.password = password
if configmanager:
cv = util.TLSCertVerifier(
configmanager, vcsa, 'pubkeys.tls_hardwaremanager'
).verify_cert
else:
cv = lambda x: True
try:
self.user = self.user.decode()
self.password = self.password.decode()
except Exception:
pass
self.wc = webclient.SecureHTTPConnection(vcsa, port=443, verifycallback=cv)
self.login()
self.vmlist = {}
self.vmbyid = {}
def login(self):
self.wc.set_basic_credentials(self.user, self.password)
self.wc.request('POST', '/api/session')
rsp = self.wc.getresponse()
body = rsp.read().decode().replace('"', '')
del self.wc.stdheaders['Authorization']
self.wc.set_header('vmware-api-session-id', body)
def list_vms(self):
rsp = self.wc.grab_json_response('/api/vcenter/vm')
self.vmlist = {}
for vm in rsp:
name = vm['name']
vmid = vm['vm']
self.vmlist[name] = vmid
self.vmbyid[vmid] = name
return self.vmlist
def index_vm(self, vm):
if vm in self.vmlist:
return self.vmlist[vm]
if vm in self.vmbyid:
return vm
self.list_vms()
if vm not in self.vmlist:
if vm in self.vmbyid:
return vm
raise Exception("VM not found")
return self.vmlist[vm]
def get_vm(self, vm):
vm = self.index_vm(vm)
rsp = self.wc.grab_json_response(f'/api/vcenter/vm/{vm}')
return rsp
def get_vm_inventory(self, vm):
rawinv = self.get_vm(vm)
hwver = rawinv['hardware']['version']
uuid = fixuuid(rawinv['identity']['bios_uuid'])
serial = rawinv['identity']['instance_uuid']
invitems = []
sysinfo = {'name': 'System',
'present': True,
'information': {
'Product name': 'VMWare Virtual machine',
'UUID': uuid,
'Manufacturer': 'VMWare',
'Model': hwver,
'Serial Number': serial
}}
inventory = [sysinfo]
for nic in rawinv['nics']:
nicinfo = rawinv['nics'][nic]
label = nicinfo['label']
mac = nicinfo['mac_address']
inventory.append({
'present': True,
'name': label,
'information': {
'Type': 'Ethernet',
'Model': nicinfo['type'],
'MAC Address 1': mac}
})
yield msg.KeyValueData({'inventory': inventory}, vm)
def get_vm_host(self, vm):
# unfortunately, the REST api doesn't manifest this as a simple attribute,
vm = self.index_vm(vm)
rsp = self.wc.grab_json_response(f'/api/vcenter/host')
for hostinfo in rsp:
host = hostinfo['host']
rsp = self.wc.grab_json_response(f'/api/vcenter/vm?hosts={host}')
for guest in rsp:
if guest['vm'] == vm:
return hostinfo
def get_vm_serial(self, vm):
vm = self.index_vm(vm)
rsp = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/hardware/serial')
if rsp[1] == 200 and len(rsp[0]) > 0:
portid = rsp[0][0]['port']
rsp = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/hardware/serial/{portid}')
if rsp[1] == 200:
if rsp[0]['backing']['type'] != 'NETWORK_SERVER':
return
netloc = rsp[0]['backing']['network_location']
portnum = netloc.split(':')[-1]
tlsenabled = False
if netloc.startswith('telnets'):
tlsenabled = True
hostinfo = self.get_vm_host(vm)
hostname = hostinfo['name']
rsp[0]
return {
'server': hostname,
'port': portnum,
'tls': tlsenabled,
}
def get_vm_bootdev(self, vm):
vm = self.index_vm(vm)
rsp = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/hardware/boot')
if rsp[0]['enter_setup_mode']:
return 'setup'
rsp = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/hardware/boot/device')
if rsp[0][0]['type'] == 'ETHERNET':
return 'network'
return 'default'
def get_vm_power(self, vm):
vm = self.index_vm(vm)
rsp = self.wc.grab_json_response(f'/api/vcenter/vm/{vm}/power')
if rsp['state'] == 'POWERED_ON':
return 'on'
if rsp['state'] == 'POWERED_OFF':
return 'off'
if rsp['state'] == 'SUSPENDED':
return 'suspended'
raise Exception("Unknown response {}".format(repr(rsp)))
def set_vm_power(self, vm, state):
vm = self.index_vm(vm)
if state == 'boot':
current = self.get_vm_power(vm)
if current == 'on':
state = 'reset'
else:
state = 'start'
elif state == 'on':
state = 'start'
elif state == 'off':
state = 'stop'
rsp = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/power?action={state}', method='POST')
def set_vm_bootdev(self, vm, bootdev):
vm = self.index_vm(vm)
self.wc.set_header('Content-Type', 'application/json')
try:
bootdevs = []
entersetup = False
if bootdev == 'setup':
entersetup = True
elif bootdev == 'default':
# In theory, we should be able to send an empty device list.
# However, vmware api counter to documentation seems to just ignore
# such a request. So instead we just go "disk first"
# and rely upon fast fail/retry to take us to a normal place
currdisks, rcode = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/hardware/disk')
currdisks = [x['disk'] for x in currdisks]
bootdevs.append({'type': 'DISK', 'disks': currdisks})
elif bootdev in ('net', 'network'):
currnics, rcode = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/hardware/ethernet')
for nic in currnics:
bootdevs.append({'type': 'ETHERNET', 'nic': nic['nic']})
payload = {'devices': bootdevs}
rsp = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/hardware/boot/device',
payload,
method='PUT')
rsp = self.wc.grab_json_response_with_status(f'/api/vcenter/vm/{vm}/hardware/boot',
{'enter_setup_mode': entersetup},
method='PATCH')
finally:
del self.wc.stdheaders['Content-Type']
def prep_vcsa_clients(nodes, configmanager):
cfginfo = configmanager.get_node_attributes(nodes, ['hardwaremanagement.manager', 'secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'], decrypt=True)
clientsbyvcsa = {}
clientsbynode = {}
for node in nodes:
cfg = cfginfo[node]
currvcsa = cfg['hardwaremanagement.manager']['value']
if currvcsa not in clientsbyvcsa:
user = cfg.get('secret.hardwaremanagementuser', {}).get('value', None)
passwd = cfg.get('secret.hardwaremanagementpassword', {}).get('value', None)
clientsbyvcsa[currvcsa] = VmwApiClient(currvcsa, user, passwd, configmanager)
clientsbynode[node] = clientsbyvcsa[currvcsa]
return clientsbynode
def retrieve(nodes, element, configmanager, inputdata):
clientsbynode = prep_vcsa_clients(nodes, configmanager)
for node in nodes:
currclient = clientsbynode[node]
if element == ['power', 'state']:
yield msg.PowerState(node, currclient.get_vm_power(node))
elif element == ['boot', 'nextdevice']:
yield msg.BootDevice(node, currclient.get_vm_bootdev(node))
elif element[:2] == ['inventory', 'hardware'] and len(element) == 4:
for rsp in currclient.get_vm_inventory(node):
yield rsp
def update(nodes, element, configmanager, inputdata):
clientsbynode = prep_vcsa_clients(nodes, configmanager)
for node in nodes:
currclient = clientsbynode[node]
if element == ['power', 'state']:
currclient.set_vm_power(node, inputdata.powerstate(node))
yield msg.PowerState(node, currclient.get_vm_power(node))
elif element == ['boot', 'nextdevice']:
currclient.set_vm_bootdev(node, inputdata.bootdevice(node))
yield msg.BootDevice(node, currclient.get_vm_bootdev(node))
# assume this is only console for now
def create(nodes, element, configmanager, inputdata):
clientsbynode = prep_vcsa_clients(nodes, configmanager)
for node in nodes:
serialdata = clientsbynode[node].get_vm_serial(node)
return VmConsole(serialdata['server'], serialdata['port'], serialdata['tls'])
if __name__ == '__main__':
import sys
import os
from pprint import pprint
myuser = os.environ['VMWUSER']
mypass = os.environ['VMWPASS']
vc = VmwApiClient(sys.argv[1], myuser, mypass, None)
vm = sys.argv[2]
if sys.argv[3] == 'setboot':
vc.set_vm_bootdev(vm, sys.argv[4])
vc.get_vm_bootdev(vm)
elif sys.argv[3] == 'power':
vc.set_vm_power(vm, sys.argv[4])
elif sys.argv[3] == 'getinfo':
vc.get_vm(vm)
print("Bootdev: " + vc.get_vm_bootdev(vm))
print("Power: " + vc.get_vm_power(vm))
print("Serial: " + repr(vc.get_vm_serial(vm)))