diff options
| author | Alex Tomic <atomic@tesora.com> | 2015-06-22 15:01:42 -0400 |
|---|---|---|
| committer | Peter Stachowski <peter@tesora.com> | 2016-01-27 14:41:15 -0500 |
| commit | fb33d87fef21052126b1ae10efb2f2e4dca9ccf9 (patch) | |
| tree | fbf1eb6ec57d1ba64c940495bf8ddb250a21fef3 | |
| parent | fd3d348adaeb7f012c625923cbc6a2944f535739 (diff) | |
| download | python-troveclient-fb33d87fef21052126b1ae10efb2f2e4dca9ccf9.tar.gz | |
Client Changes for Guest Log File Retrieval
Implements log file retrieval from the guest agent. The contents
of the log file are pushed up to a swift container as a series of
objects that represent a subset of the lines in the log.
The following trove CLI commands are implemented:
trove log-list <instance> : lists log files available on guest
trove log-enable <instance> <log> : enables writing to log file
trove log-disable <instance> <log>: disables writing to log file
trove log-publish <instance> <log>: publishes updates to swift container
trove log-discard <instance> <log>: discards published logs
trove log-tail <instance> <log> : displays last lines of log
trove log-save <instance> <log> : saves the entire log to a file
Change-Id: Ic15c455747b9f1966d83d7034c9b748ca5e2cce9
Co-Authored-By: Morgan Jones <morgan@tesora.com>
Co-Authored-By: Alex Tomic <atomic@tesora.com>
Co-Authored-By: Peter Stachowski <peter@tesora.com>
| -rw-r--r-- | requirements.txt | 1 | ||||
| -rw-r--r-- | troveclient/exceptions.py | 5 | ||||
| -rw-r--r-- | troveclient/v1/instances.py | 166 | ||||
| -rw-r--r-- | troveclient/v1/shell.py | 152 |
4 files changed, 324 insertions, 0 deletions
diff --git a/requirements.txt b/requirements.txt index cb0d4f6..b4051f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ oslo.utils>=3.4.0 # Apache-2.0 python-keystoneclient!=1.8.0,!=2.1.0,>=1.6.0 # Apache-2.0 Babel>=1.3 # BSD six>=1.9.0 # MIT +python-swiftclient>=2.2.0 # Apache-2.0 diff --git a/troveclient/exceptions.py b/troveclient/exceptions.py index 0f205a2..536a8d4 100644 --- a/troveclient/exceptions.py +++ b/troveclient/exceptions.py @@ -34,3 +34,8 @@ class NoTokenLookupException(Exception): class ResponseFormatError(Exception): """Could not parse the response format.""" pass + + +class GuestLogNotFoundError(Exception): + """The specified guest log does not exist.""" + pass diff --git a/troveclient/v1/instances.py b/troveclient/v1/instances.py index eb1bb7c..2c3a970 100644 --- a/troveclient/v1/instances.py +++ b/troveclient/v1/instances.py @@ -15,13 +15,29 @@ # License for the specific language governing permissions and limitations # under the License. +import os + from troveclient import base from troveclient import common +from troveclient import exceptions + +from swiftclient import client REBOOT_SOFT = 'SOFT' REBOOT_HARD = 'HARD' +def swift_client(): + auth_url = os.getenv("OS_AUTH_URL") + user = os.getenv("OS_USERNAME") + key = os.getenv("OS_PASSWORD") + tenant = os.getenv("OS_TENANT_NAME") + os_options = {'region_name': os.getenv("OS_REGION_NAME")} + + return client.Connection(auth_url, user, key, tenant_name=tenant, + auth_version="2.0", os_options=os_options) + + class Instance(base.Resource): """An Instance is an opaque instance used to store Database instances.""" def __repr__(self): @@ -43,10 +59,19 @@ class Instance(base.Resource): self.manager.edit(self.id, detach_replica_source=True) +class DatastoreLog(base.Resource): + """A DatastoreLog is a log on the database guest instance.""" + + def __repr__(self): + return "<DatastoreLog: %s>" % self.name + + class Instances(base.ManagerWithFind): """Manage :class:`Instance` resources.""" resource_class = Instance + log_cache = {} + def create(self, name, flavor_id, volume=None, databases=None, users=None, restorePoint=None, availability_zone=None, datastore=None, datastore_version=None, nics=None, configuration=None, @@ -205,6 +230,146 @@ class Instances(base.ManagerWithFind): body = {'eject_replica_source': {}} self._action(instance, body) + def log_list(self, instance): + """Get a list of all guest logs. + + :param instance: The :class:`Instance` (or its ID) of the database + instance to get the log for. + :rtype: list of :class:`DatastoreLog`. + """ + url = '/instances/%s/log' % base.getid(instance) + resp, body = self.api.client.get(url) + common.check_for_exceptions(resp, body, url) + return [DatastoreLog(self, log, loaded=True) for log in body['logs']] + + def log_show(self, instance, log_name): + return self._log_action(instance, log_name) + + def log_enable(self, instance, log_name): + return self._log_action(instance, log_name, enable=True) + + def log_disable(self, instance, log_name, discard=None): + return self._log_action(instance, log_name, + disable=True, discard=discard) + + def log_publish(self, instance, log_name, disable=None, discard=None): + return self._log_action(instance, log_name, disable=disable, + publish=True, discard=discard) + + def log_discard(self, instance, log_name): + return self._log_action(instance, log_name, discard=True) + + def _log_action(self, instance, log_name, enable=None, disable=None, + publish=None, discard=None): + """Perform action on guest log. + + :param instance: The :class:`Instance` (or its ID) of the database + instance to get the log for. + :param log_name: The name of <log> to publish + :param enable: Turn on <log> + :param disable: Turn off <log> + :param publish: Publish log to associated container + :param discard: Delete the associated container + :rtype: List of :class:`DatastoreLog`. + """ + body = {"name": log_name} + if enable: + body.update({'enable': int(enable)}) + if disable: + body.update({'disable': int(disable)}) + if publish: + body.update({'publish': int(publish)}) + if discard: + body.update({'discard': int(discard)}) + url = "/instances/%s/log" % base.getid(instance) + resp, body = self.api.client.post(url, body=body) + common.check_for_exceptions(resp, body, url) + return DatastoreLog(self, body['log'], loaded=True) + + def _get_container_info(self, instance, log_name, publish): + try: + log_info = self._log_action(instance, log_name, publish=publish) + container = log_info.container + prefix = log_info.prefix + metadata_file = log_info.metafile + return container, prefix, metadata_file + except client.ClientException as ex: + if ex.http_status == 404: + raise exceptions.GuestLogNotFoundError() + raise + + def log_generator(self, instance, log_name, publish=None, lines=50, + swift=None): + """Return generator to yield the last <lines> lines of guest log. + + :param instance: The :class:`Instance` (or its ID) of the database + instance to get the log for. + :param log_name: The name of <log> to publish + :param publish: Publish updates before displaying log + :param lines: Display last <lines> lines of log (0 for all lines) + :param swift: Connection to swift + :rtype: generator function to yield log as chunks. + """ + + if not swift: + swift = swift_client() + + def _log_generator(instance, log_name, publish, lines, swift): + try: + container, prefix, metadata_file = self._get_container_info( + instance, log_name, publish) + head, body = swift.get_container(container, prefix=prefix) + log_obj_to_display = [] + if lines: + total_lines = lines + partial_results = False + parts = sorted(body, key=lambda obj: obj['last_modified'], + reverse=True) + for part in parts: + obj_hdrs = swift.head_object(container, part['name']) + obj_lines = int(obj_hdrs['x-object-meta-lines']) + log_obj_to_display.insert(0, part) + if obj_lines >= lines: + partial_results = True + break + lines -= obj_lines + if not partial_results: + lines = total_lines + part = log_obj_to_display.pop(0) + hdrs, log_obj = swift.get_object(container, part['name']) + log_by_lines = log_obj.splitlines() + yield "\n".join(log_by_lines[-1 * lines:]) + "\n" + else: + log_obj_to_display = sorted( + body, key=lambda obj: obj['last_modified']) + for log_part in log_obj_to_display: + headers, log_obj = swift.get_object(container, + log_part['name']) + yield log_obj + except client.ClientException as ex: + if ex.http_status == 404: + raise exceptions.GuestLogNotFoundError() + raise + + return lambda: _log_generator(instance, log_name, publish, + lines, swift) + + def log_save(self, instance, log_name, publish=None, filename=None): + """Saves a guest log to a file. + + :param instance: The :class:`Instance` (or its ID) of the database + instance to get the log for. + :param log_name: The name of <log> to publish + :param publish: Publish updates before displaying log + :rtype: Filename to which log was saved + """ + written_file = filename or (instance.name + '-' + log_name + ".log") + log_gen = self.log_generator(instance, log_name, publish, 0) + with open(written_file, 'w') as f: + for log_obj in log_gen(): + f.write(log_obj) + return written_file + class InstanceStatus(object): @@ -218,3 +383,4 @@ class InstanceStatus(object): RESTART_REQUIRED = "RESTART_REQUIRED" PROMOTING = "PROMOTING" EJECTING = "EJECTING" + LOGGING = "LOGGING" diff --git a/troveclient/v1/shell.py b/troveclient/v1/shell.py index 8d7b38f..fd4e50a 100644 --- a/troveclient/v1/shell.py +++ b/troveclient/v1/shell.py @@ -23,6 +23,7 @@ INSTANCE_ERROR = ("Instance argument(s) must be of the form --instance " "<opt=value[,opt=value]> - see help for details.") NIC_ERROR = ("Invalid NIC argument: %s. Must specify either net-id or port-id " "but not both. Please refer to help.") +NO_LOG_FOUND_ERROR = "ERROR: No published '%s' log was found for %s." try: import simplejson as json @@ -1394,6 +1395,157 @@ def do_metadata_delete(cs, args): cs.metadata.delete(args.instance_id, args.key) +@utils.arg('instance', metavar='<instance>', + help='Id or Name of the instance.') +@utils.service_type('database') +def do_log_list(cs, args): + """Lists the log files available for instance.""" + instance = _find_instance(cs, args.instance) + log_list = cs.instances.log_list(instance) + utils.print_list(log_list, + ['name', 'type', 'status', 'published', 'pending', + 'container', 'prefix']) + + +@utils.arg('instance', metavar='<instance>', + help='Id or Name of the instance.') +@utils.arg('log_name', metavar='<log_name>', help='Name of log to show.') +@utils.service_type('database') +def do_log_show(cs, args): + """Instructs Trove guest to show details of log.""" + try: + instance = _find_instance(cs, args.instance) + log_info = cs.instances.log_show(instance, args.log_name) + _print_object(log_info) + except exceptions.GuestLogNotFoundError: + print(NO_LOG_FOUND_ERROR % (args.log_name, instance)) + except Exception as ex: + error_msg = ex.message.split('\n') + print(error_msg[0]) + + +@utils.arg('instance', metavar='<instance>', + help='Id or Name of the instance.') +@utils.arg('log_name', metavar='<log_name>', help='Name of log to publish.') +@utils.service_type('database') +def do_log_enable(cs, args): + """Instructs Trove guest to start collecting log details.""" + try: + instance = _find_instance(cs, args.instance) + log_info = cs.instances.log_enable(instance, args.log_name) + _print_object(log_info) + except exceptions.GuestLogNotFoundError: + print(NO_LOG_FOUND_ERROR % (args.log_name, instance)) + except Exception as ex: + error_msg = ex.message.split('\n') + print(error_msg[0]) + + +@utils.arg('instance', metavar='<instance>', + help='Id or Name of the instance.') +@utils.arg('log_name', metavar='<log_name>', help='Name of log to publish.') +@utils.arg('--discard', action='store_true', default=False, + help='Discard published contents of specified log.') +@utils.service_type('database') +def do_log_disable(cs, args): + """Instructs Trove guest to stop collecting log details.""" + try: + instance = _find_instance(cs, args.instance) + log_info = cs.instances.log_disable(instance, args.log_name, + discard=args.discard) + _print_object(log_info) + except exceptions.GuestLogNotFoundError: + print(NO_LOG_FOUND_ERROR % (args.log_name, instance)) + except Exception as ex: + error_msg = ex.message.split('\n') + print(error_msg[0]) + + +@utils.arg('instance', metavar='<instance>', + help='Id or Name of the instance.') +@utils.arg('log_name', metavar='<log_name>', help='Name of log to publish.') +@utils.arg('--disable', action='store_true', default=False, + help='Stop collection of specified log.') +@utils.arg('--discard', action='store_true', default=False, + help='Discard published contents of specified log.') +@utils.service_type('database') +def do_log_publish(cs, args): + """Instructs Trove guest to publish latest log entries on instance.""" + try: + instance = _find_instance(cs, args.instance) + log_info = cs.instances.log_publish( + instance, args.log_name, disable=args.disable, + discard=args.discard) + _print_object(log_info) + except exceptions.GuestLogNotFoundError: + print(NO_LOG_FOUND_ERROR % (args.log_name, instance)) + except Exception as ex: + error_msg = ex.message.split('\n') + print(error_msg[0]) + + +@utils.arg('instance', metavar='<instance>', + help='Id or Name of the instance.') +@utils.arg('log_name', metavar='<log_name>', help='Name of log to publish.') +@utils.service_type('database') +def do_log_discard(cs, args): + """Instructs Trove guest to discard the container of the published log.""" + try: + instance = _find_instance(cs, args.instance) + log_info = cs.instances.log_discard(instance, args.log_name) + _print_object(log_info) + except exceptions.GuestLogNotFoundError: + print(NO_LOG_FOUND_ERROR % (args.log_name, instance)) + except Exception as ex: + error_msg = ex.message.split('\n') + print(error_msg[0]) + + +@utils.arg('instance', metavar='<instance>', + help='Id or Name of the instance.') +@utils.arg('log_name', metavar='<log_name>', help='Name of log to publish.') +@utils.arg('--publish', action='store_true', default=False, + help='Publish latest entries from guest before display.') +@utils.arg('--lines', metavar='<lines>', default=50, type=int, + help='Publish latest entries from guest before display.') +@utils.service_type('database') +def do_log_tail(cs, args): + """Display log entries for instance.""" + try: + instance = _find_instance(cs, args.instance) + log_gen = cs.instances.log_generator(instance, args.log_name, + args.publish, args.lines) + for log_part in log_gen(): + print(log_part, end="") + except exceptions.GuestLogNotFoundError: + print(NO_LOG_FOUND_ERROR % (args.log_name, instance)) + except Exception as ex: + error_msg = ex.message.split('\n') + print(error_msg[0]) + + +@utils.arg('instance', metavar='<instance>', + help='Id or Name of the instance.') +@utils.arg('log_name', metavar='<log_name>', help='Name of log to publish.') +@utils.arg('--publish', action='store_true', default=False, + help='Publish latest entries from guest before display.') +@utils.arg('--file', metavar='<file>', default=None, + help='Path of file to save log to for instance.') +@utils.service_type('database') +def do_log_save(cs, args): + """Save log file for instance.""" + try: + instance = _find_instance(cs, args.instance) + filename = cs.instances.log_save(instance, args.log_name, + args.publish, args.file) + print('Log "%s" written to %s' % (args.log_name, filename)) + except exceptions.GuestLogNotFoundError: + print(NO_LOG_FOUND_ERROR % (args.log_name, instance)) + except Exception as ex: + error_msg = ex.message.split('\n') + print(error_msg[0]) + + # @utils.arg('datastore_version', # metavar='<datastore_version>', # help='Datastore version name or UUID assigned to the ' |
