From a9aeb2e8364c602abdc073559898d4eeb06f9e0e Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Wed, 25 Dec 2019 19:52:15 -0500 Subject: [PATCH] Implement log viewing/following with API --- client-cli/cli_lib/vm.py | 189 ++++++++++++++++++++++++++++----------- client-cli/pvc.py | 24 +++-- 2 files changed, 148 insertions(+), 65 deletions(-) diff --git a/client-cli/cli_lib/vm.py b/client-cli/cli_lib/vm.py index 7e0d85b5..765a6d1e 100644 --- a/client-cli/cli_lib/vm.py +++ b/client-cli/cli_lib/vm.py @@ -20,26 +20,64 @@ # ############################################################################### -import difflib -import colorama +import time +import subprocess import click +import requests from collections import deque -import client_lib.ansiprint as ansiprint -import client_lib.zkhandler as zkhandler -import client_lib.common as common +import cli_lib.ansiprint as ansiprint +import cli_lib.ceph as ceph -import client_lib.ceph as ceph +def get_request_uri(config, endpoint): + """ + Return the fully-formed URI for {endpoint} + """ + uri = '{}://{}{}{}'.format( + config['api_scheme'], + config['api_host'], + config['api_prefix'], + endpoint + ) + return uri -def get_console_log(zk_conn, domain, lines=1000): - # Validate that VM exists in cluster - dom_uuid = getDomainUUID(zk_conn, domain) - if not dom_uuid: - return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) +# +# Primary functions +# +def view_console_log(config, vm, lines=100): + """ + Return console log lines from the API and display them in a pager - # Get the data from ZK - console_log = zkhandler.readdata(zk_conn, '/domains/{}/consolelog'.format(dom_uuid)) + API endpoint: GET /vm/{vm}/console + API arguments: lines={lines} + API schema: {"name":"{vmname}","data":"{console_log}"} + """ + request_uri = get_request_uri(config, '/vm/{vm}/console'.format(vm=vm)) + if config['debug']: + print( + 'API endpoint: GET {}'.format(request_uri) + ) + + # Get the data from the API + response = requests.get( + request_uri, + params={'lines': lines} + ) + + if config['debug']: + print( + 'Response code: {}'.format( + response.status_code + ) + ) + print( + 'Response headers: {}'.format( + response.headers + ) + ) + + console_log = response.json()['data'] # Shrink the log buffer to length lines shrunk_log = console_log.split('\n')[-lines:] @@ -50,18 +88,44 @@ def get_console_log(zk_conn, domain, lines=1000): pager = subprocess.Popen(['less', '-R'], stdin=subprocess.PIPE) pager.communicate(input=loglines.encode('utf8')) except FileNotFoundError: - return False, 'ERROR: The "less" pager is required to view console logs.' + click.echo("Error: `less` pager not found, dumping log ({} lines) to stdout".format(lines)) + return True, loglines return True, '' -def follow_console_log(zk_conn, domain, lines=10): - # Validate that VM exists in cluster - dom_uuid = getDomainUUID(zk_conn, domain) - if not dom_uuid: - return False, 'ERROR: Could not find VM "{}" in the cluster!'.format(domain) +def follow_console_log(config, vm, lines=10): + """ + Return and follow console log lines from the API - # Get the initial data from ZK - console_log = zkhandler.readdata(zk_conn, '/domains/{}/consolelog'.format(dom_uuid)) + API endpoint: GET /vm/{vm}/console + API arguments: lines={lines} + API schema: {"name":"{vmname}","data":"{console_log}"} + """ + request_uri = get_request_uri(config, '/vm/{vm}/console'.format(vm=vm)) + if config['debug']: + print( + 'API endpoint: GET {}'.format(request_uri) + ) + + # Get the (initial) data from the API + response = requests.get( + request_uri, + params={'lines': lines} + ) + + if config['debug']: + print( + 'Response code: {}'.format( + response.status_code + ) + ) + print( + 'Response headers: {}'.format( + response.headers + ) + ) + + console_log = response.json()['data'] # Shrink the log buffer to length lines shrunk_log = console_log.split('\n')[-lines:] @@ -70,39 +134,60 @@ def follow_console_log(zk_conn, domain, lines=10): # Print the initial data and begin following print(loglines, end='') - try: - while True: - # Grab the next line set - new_console_log = zkhandler.readdata(zk_conn, '/domains/{}/consolelog'.format(dom_uuid)) - # Split the new and old log strings into constitutent lines - old_console_loglines = console_log.split('\n') - new_console_loglines = new_console_log.split('\n') - # Set the console log to the new log value for the next iteration - console_log = new_console_log - # Remove the lines from the old log until we hit the first line of the new log; this - # ensures that the old log is a string that we can remove from the new log entirely - for index, line in enumerate(old_console_loglines, start=0): - if line == new_console_loglines[0]: - del old_console_loglines[0:index] - break - # Rejoin the log lines into strings - old_console_log = '\n'.join(old_console_loglines) - new_console_log = '\n'.join(new_console_loglines) - # Remove the old lines from the new log - diff_console_log = new_console_log.replace(old_console_log, "") - # If there's a difference, print it out - if diff_console_log: - print(diff_console_log, end='') - # Wait a second - time.sleep(1) - except kazoo.exceptions.NoNodeError: - return False, 'ERROR: VM has gone away.' - except: - return False, 'ERROR: Lost connection to Zookeeper node.' + while True: + # Grab the next line set + # Get the (initial) data from the API + response = requests.get( + '{}://{}{}{}'.format( + config['api_scheme'], + config['api_host'], + config['api_prefix'], + '/vm/{}/console'.format(vm) + ), + params={'lines': lines} + ) + + if config['debug']: + print( + 'Response code: {}'.format( + response.status_code + ) + ) + print( + 'Response headers: {}'.format( + response.headers + ) + ) + + new_console_log = response.json()['data'] + # Split the new and old log strings into constitutent lines + old_console_loglines = console_log.split('\n') + new_console_loglines = new_console_log.split('\n') + # Set the console log to the new log value for the next iteration + console_log = new_console_log + # Remove the lines from the old log until we hit the first line of the new log; this + # ensures that the old log is a string that we can remove from the new log entirely + for index, line in enumerate(old_console_loglines, start=0): + if line == new_console_loglines[0]: + del old_console_loglines[0:index] + break + # Rejoin the log lines into strings + old_console_log = '\n'.join(old_console_loglines) + new_console_log = '\n'.join(new_console_loglines) + # Remove the old lines from the new log + diff_console_log = new_console_log.replace(old_console_log, "") + # If there's a difference, print it out + if diff_console_log: + print(diff_console_log, end='') + # Wait a second + time.sleep(1) return True, '' -def format_info(zk_conn, domain_information, long_output): +# +# Output display functions +# +def format_info(config, domain_information, long_output): # Format a nice output; do this line-by-line then concat the elements at the end ainformation = [] ainformation.append('{}Virtual machine information:{}'.format(ansiprint.bold(), ansiprint.end())) @@ -211,7 +296,7 @@ def format_info(zk_conn, domain_information, long_output): click.echo('') -def format_list(zk_conn, vm_list, raw): +def format_list(config, vm_list, raw): # Function to strip the "br" off of nets and return a nicer list def getNiceNetID(domain_information): # Network list diff --git a/client-cli/pvc.py b/client-cli/pvc.py index 5e059f15..7422ee6e 100755 --- a/client-cli/pvc.py +++ b/client-cli/pvc.py @@ -32,7 +32,6 @@ import yaml import requests import cli_lib.ansiprint as ansiprint -import cli_lib.common as pvc_common import cli_lib.cluster as pvc_cluster import cli_lib.node as pvc_node import cli_lib.vm as pvc_vm @@ -42,11 +41,15 @@ import cli_lib.ceph as pvc_ceph myhostname = socket.gethostname().split('.')[0] zk_host = '' +config = dict() +config['debug'] = True +config['api_scheme'] = 'http' +config['api_host'] = 'localhost:7370' +config['api_prefix'] = '/api/v1' + CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=120) -def cleanup(retcode, retmsg, zk_conn=None): - if zk_conn: - pvc_common.stopZKConnection(zk_conn) +def cleanup(retcode, retmsg): if retcode == True: if retmsg != '': click.echo(retmsg) @@ -647,7 +650,7 @@ def vm_info(domain, long_output): 'domain' ) @click.option( - '-l', '--lines', 'lines', default=1000, show_default=True, + '-l', '--lines', 'lines', default=100, show_default=True, help='Display this many log lines from the end of the log buffer.' ) @click.option( @@ -659,16 +662,11 @@ def vm_log(domain, lines, follow): Show console logs of virtual machine DOMAIN on its current node in the 'less' pager or continuously. DOMAIN may be a UUID or name. Note that migrating a VM to a different node will cause the log buffer to be overwritten by entries from the new node. """ - # Open a Zookeeper connection - zk_conn = pvc_common.startZKConnection(zk_host) if follow: - # Handle the "new" default of the follow - if lines == 1000: - lines = 10 - retcode, retmsg = pvc_vm.follow_console_log(zk_conn, domain, lines) + retcode, retmsg = pvc_vm.follow_console_log(config, domain, lines) else: - retcode, retmsg = pvc_vm.get_console_log(zk_conn, domain, lines) - cleanup(retcode, retmsg, zk_conn) + retcode, retmsg = pvc_vm.view_console_log(config, domain, lines) + cleanup(retcode, retmsg) ############################################################################### # pvc vm list