diff --git a/confluent_client/bin/nodeconsole b/confluent_client/bin/nodeconsole index f05d5783..82b95583 100755 --- a/confluent_client/bin/nodeconsole +++ b/confluent_client/bin/nodeconsole @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import base64 import optparse import os import subprocess @@ -46,6 +47,8 @@ argparser.add_option('-l', '--log', action='store_true', default=False, argparser.add_option('-T', '--Timestamp', action='store_true', default=False, help= 'Dump log in stdout with timestamps') +argparser.add_option('-s', '--screenshot', action='store_true', default=False, + help='Attempt to grab screenshot and render using kitty image protocol') argparser.add_option('-w','--windowed', action='store_true', default=False, help='Open terminal windows for each node. The ' 'environment variable NODECONSOLE_WINDOWED_COMMAND ' @@ -69,6 +72,16 @@ argparser.add_option('-w','--windowed', action='store_true', default=False, (options, args) = argparser.parse_args() +def kitty_draw(data): + while data: + chunk, data = data[:4096], data[4096:] + m = 1 if data else 0 + sys.stdout.write('\x1b_Ga=T,f=100,m={};'.format(m)) + sys.stdout.write(chunk.decode('utf8')) + sys.stdout.write('\x1b\\') + sys.stdout.flush() + sys.stdout.write('\n') + pass_through_args = [] killcon = False try: @@ -106,6 +119,16 @@ if options.Timestamp: logreader.dump_to_console(logname) sys.exit(0) +if options.screenshot: + sess = client.Command() + for res in sess.read('/noderange/{}/console/ikvm_screenshot'.format(args[0])): + for node in res.get('databynode', {}): + imgdata = res['databynode'][node]['image']['imgdata'] + sys.stdout.write('{}: '.format(node)) + kitty_draw(imgdata.encode()) + sys.stdout.write('\n') + sys.exit(0) + def kill(noderange): sess = client.Command() envstring=os.environ.get('NODECONSOLE_WINDOWED_COMMAND') diff --git a/confluent_server/confluent/core.py b/confluent_server/confluent/core.py index 61a03d05..ef1c33ee 100644 --- a/confluent_server/confluent/core.py +++ b/confluent_server/confluent/core.py @@ -442,6 +442,10 @@ def _init_core(): 'pluginattrs': ['hardwaremanagement.method'], 'default': 'ipmi', }), + 'ikvm_screenshot': PluginRoute({ + 'pluginattrs': ['hardwaremanagement.method'], + 'default': 'ipmi', + }), }, 'description': PluginRoute({ 'pluginattrs': ['hardwaremanagement.method'], diff --git a/confluent_server/confluent/messages.py b/confluent_server/confluent/messages.py index f74f027a..62830400 100644 --- a/confluent_server/confluent/messages.py +++ b/confluent_server/confluent/messages.py @@ -18,6 +18,7 @@ # This module implements client/server messages emitted from plugins. # Things are defined here to 'encourage' developers to coordinate information # format. This is also how different data formats are supported +import base64 import confluent.exceptions as exc import confluent.config.configmanager as cfm import confluent.config.conf as cfgfile @@ -1882,6 +1883,12 @@ class GraphicalConsole(ConfluentMessage): else: self.kvpairs = {name: {'Launcher': kv}} +class ScreenShot(ConfluentMessage): + readonly = True + def __init__(self, imgdata, node, imgformat=None): + self.kvpairs = {node: {'image': {'imgformat': imgformat, 'imgdata': base64.b64encode(imgdata)}}} + + class CryptedAttributes(Attributes): defaulttype = 'password' diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index 16878d95..ed4236c9 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -28,6 +28,7 @@ import eventlet.greenpool as greenpool import eventlet.queue as queue import eventlet.support.greendns from fnmatch import fnmatch +import io import os import pwd import pyghmi.constants as pygconstants @@ -51,6 +52,14 @@ except NameError: pci_cache = {} +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() + def get_dns_txt(qstring): return eventlet.support.greendns.resolver.query( qstring, 'TXT')[0].strings[0].replace('i=', '') @@ -607,6 +616,8 @@ class IpmiHandler(object): self.handle_description() elif self.element == ['console', 'ikvm_methods']: self.handle_ikvm_methods() + elif self.element == ['console', 'ikvm_screenshot']: + self.handle_ikvm_screenshot() elif self.element == ['console', 'ikvm']: self.handle_ikvm() else: @@ -1641,6 +1652,14 @@ class IpmiHandler(object): dsc = {'ikvm_methods': dsc} self.output.put(msg.KeyValueData(dsc, self.node)) + def handle_ikvm_screenshot(self): + # good background for the webui, and kitty + imgdata = RetainedIO() + imgformat = self.ipmicmd.get_screenshot(imgdata) + imgdata = imgdata.getvalue() + if imgdata: + self.output.put(msg.ScreenShot(imgdata, self.node, imgformat=imgformat)) + def handle_ikvm(self): methods = self.ipmicmd.get_ikvm_methods() if 'openbmc' in methods: diff --git a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py index 2a2d26f5..0f2cd5ae 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/redfish.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/redfish.py @@ -27,6 +27,7 @@ import eventlet.greenpool as greenpool import eventlet.queue as queue import eventlet.support.greendns from fnmatch import fnmatch +import io import os import pwd import pyghmi.constants as pygconstants @@ -42,6 +43,14 @@ if not hasattr(ssl, 'SSLEOFError'): pci_cache = {} +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() + def get_dns_txt(qstring): return eventlet.support.greendns.resolver.query( qstring, 'TXT')[0].strings[0].replace('i=', '') @@ -464,6 +473,8 @@ class IpmiHandler(object): self.handle_description() elif self.element == ['console', 'ikvm_methods']: self.handle_ikvm_methods() + elif self.element == ['console', 'ikvm_screenshot']: + self.handle_ikvm_screenshot() elif self.element == ['console', 'ikvm']: self.handle_ikvm() else: @@ -1498,6 +1509,14 @@ class IpmiHandler(object): dsc = {'ikvm_methods': dsc} self.output.put(msg.KeyValueData(dsc, self.node)) + def handle_ikvm_screenshot(self): + # good background for the webui, and kitty + imgdata = RetainedIO() + imgformat = self.ipmicmd.get_screenshot(imgdata) + imgdata = imgdata.getvalue() + if imgdata: + self.output.put(msg.ScreenShot(imgdata, self.node, imgformat=imgformat)) + def handle_ikvm(self): methods = self.ipmicmd.get_ikvm_methods() if 'openbmc' in methods: