diff --git a/confluent_server/confluent/config/attributes.py b/confluent_server/confluent/config/attributes.py index bd08b39c..248063f2 100644 --- a/confluent_server/confluent/config/attributes.py +++ b/confluent_server/confluent/config/attributes.py @@ -637,6 +637,9 @@ node = { 'description': ('Fingerprint of the TLS certificate recognized as' 'belonging to the hardware manager of the server'), }, + 'pubkeys.tls': { + 'description': ('Fingerprint of the TLS certificate for service running on host.'), + }, 'pubkeys.ssh': { 'description': ('Fingerprint of the SSH key of the OS running on the ' 'system.'), diff --git a/confluent_server/confluent/plugins/hardwaremanagement/proxmox.py b/confluent_server/confluent/plugins/hardwaremanagement/proxmox.py new file mode 100644 index 00000000..9953bc29 --- /dev/null +++ b/confluent_server/confluent/plugins/hardwaremanagement/proxmox.py @@ -0,0 +1,266 @@ + +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 +import io +import urllib.parse as urlparse + +class RetainedIO(io.BytesIO): + # Need to retain buffer after close + def __init__(self): + self.resultbuffer = None + def close(self): + self.resultbuffer = self.getbuffer() + super().close() + +class PmxConsole(conapi.Console): + pass + # this more closely resembles OpenBMC.., websocket based and all + +class PmxApiClient: + def __init__(self, server, user, password, configmanager): + self.user = user + self.password = password + if configmanager: + cv = util.TLSCertVerifier( + configmanager, server, 'pubkeys.tls' + ).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(server, port=8006, verifycallback=cv) + self.vmmap = {} + self.login() + self.vmlist = {} + self.vmbyid = {} + + def login(self): + loginform = { + 'username': self.user, + 'password': self.password, + } + loginbody = urlparse.urlencode(loginform) + rsp = self.wc.grab_json_response_with_status('/api2/json/access/ticket', loginbody) + self.wc.cookies['PVEAuthCookie'] = rsp[0]['data']['ticket'] + self.wc.set_header('CSRFPreventionToken', rsp[0]['data']['CSRFPreventionToken']) + + + def get_screenshot(self, vm, outfile): + raise Exception("Not implemented") + + def map_vms(self): + rsp = self.wc.grab_json_response('/api2/json/cluster/resources') + for datum in rsp.get('data', []): + if datum['type'] == 'qemu': + self.vmmap[datum['name']] = (datum['node'], datum['id']) + return self.vmmap + + + def get_vm(self, vm): + if vm not in self.vmmap: + self.map_vms() + return self.vmmap[vm] + + + def get_vm_inventory(self, vm): + host, guest = self.get_vm(vm) + cfg = self.wc.grab_json_response(f'/api2/json/nodes/{host}/{guest}/pending') + myuuid = None + sysinfo = {'name': 'System', 'present': True, 'information': { + 'Product name': 'Proxmox qemu virtual machine', + 'Manufacturer': 'qemu' + }} + invitems = [sysinfo] + for datum in cfg['data']: + if datum['key'] == 'smbios1': + smbios = datum['value'] + for smbio in smbios.split(','): + if '=' in smbio: + k, v = smbio.split('=') + if k == 'uuid': + sysinfo['information']['UUID'] = v + elif datum['key'].startswith('net'): + label = 'Network adapter {}'.format(datum['key']) + niccfg = datum['value'] + cfgparts = niccfg.split(',') + nicmodel, mac = cfgparts[0].split('=') + invitems.append({ + 'present': True, + 'name': label, + 'information': { + 'Type': 'Ethernet', + 'Model': nicmodel, + 'MAC Address 1': mac, + } + }) + yield msg.KeyValueData({'inventory': invitems}, vm) + + + def get_vm_serial(self, vm): + # This would be termproxy + # Example url + #wss:///api2/json/nodes/{host}/{guest}/vncwebsocket?port=5900&vncticket=URLENCODEDTICKET + raise Exception('TODO') + + def get_vm_bootdev(self, vm): + host, guest = self.get_vm(vm) + cfg = self.wc.grab_json_response(f'/api2/json/nodes/{host}/{guest}/pending') + for datum in cfg['data']: + if datum['key'] == 'boot': + bootseq = datum.get('pending', datum['value']) + for kv in bootseq.split(','): + k, v = kv.split('=') + if k == 'order': + bootdev = v.split(';')[0] + if bootdev.startswith('net'): + return 'network' + return 'default' + + + def get_vm_power(self, vm): + host, guest = self.get_vm(vm) + rsp = self.wc.grab_json_response(f'/api2/json/nodes/{host}/{guest}/status/current') + rsp = rsp['data'] + currstatus = rsp["qmpstatus"] # stopped, "running" + if currstatus == 'running': + return 'on' + elif currstatus == 'stopped': + return off + raise Exception("Unknnown response to status query") + + def set_vm_power(self, vm, state): + host, guest = self.get_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'/api2/json/nodes/{host}/{guest}/status/{state}', method='POST') + print(repr(rsp)) + + def set_vm_bootdev(self, vm, bootdev): + host, guest = self.get_vm(vm) + if bootdev not in ('net', 'network', 'default'): + raise Exception('Requested boot device not supported') + cfg = self.wc.grab_json_response(f'/api2/json/nodes/{host}/{guest}/pending') + nonnetdevs = [] + netdevs = [] + for datum in cfg['data']: + if datum['key'] == 'boot': + bootseq = datum.get('pending', datum['value']) + for item in bootseq.split(','): + if item.startswith('order='): + bootdevs = item.replace('order=', '').split(';') + for cbootdev in bootdevs: + if cbootdev.startswith('net'): + netdevs.append(cbootdev) + else: + nonnetdevs.append(cbootdev) + if bootdev in ('net', 'network'): + newbootdevs = netdevs + nonnetdevs + else: + newbootdevs = nonnetdevs + netdevs + neworder = 'order=' + ';'.join(newbootdevs) + self.wc.set_header('Content-Type', 'application/json') + try: + self.wc.grab_json_response_with_status(f'/api2/json/nodes/{host}/{guest}/config', {'boot': neworder}, method='PUT') + finally: + del self.wc.stdheaders['Content-Type'] + + +def prep_proxmox_clients(nodes, configmanager): + cfginfo = configmanager.get_node_attributes(nodes, ['hardwaremanagement.manager', 'secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'], decrypt=True) + clientsbypmx = {} + clientsbynode = {} + for node in nodes: + cfg = cfginfo[node] + currpmx = cfg['hardwaremanagement.manager']['value'] + if currpmx not in clientsbypmx: + user = cfg.get('secret.hardwaremanagementuser', {}).get('value', None) + passwd = cfg.get('secret.hardwaremanagementpassword', {}).get('value', None) + clientsbypmx[currpmx] = PmxApiClient(currpmx, user, passwd, configmanager) + clientsbynode[node] = clientsbypmx[currpmx] + return clientsbynode + +def retrieve(nodes, element, configmanager, inputdata): + clientsbynode = prep_proxmox_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 + elif element == ['console', 'ikvm_methods']: + dsc = {'ikvm_methods': ['screenshot']} + yield msg.KeyValueData(dsc, node) + elif element == ['console', 'ikvm_screenshot']: + # good background for the webui, and kitty + imgdata = RetainedIO() + imgformat = currclient.get_screenshot(node, imgdata) + imgdata = imgdata.getvalue() + if imgdata: + yield msg.ScreenShot(imgdata, node, imgformat=imgformat) + + + + + + +def update(nodes, element, configmanager, inputdata): + clientsbynode = prep_proxmox_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['PMXUSER'] + mypass = os.environ['PMXPASS'] + vc = PmxApiClient(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': + print(repr(list(vc.get_vm_inventory(vm)))) + print("Bootdev: " + vc.get_vm_bootdev(vm)) + print("Power: " + vc.get_vm_power(vm)) + #print("Serial: " + repr(vc.get_vm_serial(vm)))