summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Tomic <atomic@tesora.com>2015-06-22 15:01:42 -0400
committerPeter Stachowski <peter@tesora.com>2016-01-27 14:41:15 -0500
commitfb33d87fef21052126b1ae10efb2f2e4dca9ccf9 (patch)
treefbf1eb6ec57d1ba64c940495bf8ddb250a21fef3
parentfd3d348adaeb7f012c625923cbc6a2944f535739 (diff)
downloadpython-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.txt1
-rw-r--r--troveclient/exceptions.py5
-rw-r--r--troveclient/v1/instances.py166
-rw-r--r--troveclient/v1/shell.py152
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 '