Implement 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 now supported: 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 Log declarations and scenario tests were added for MySQL and PostgreSQL. Co-Authored-By: Morgan Jones <morgan@tesora.com> Co-Authored-By: Alex Tomic <atomic@tesora.com> Co-Authored-By: Peter Stachowski <peter@tesora.com> Implements: blueprint datastore-log-operations Change-Id: I16c3bba4a3183d05af2971be6ba56110105797a6
This commit is contained in:
committed by
Peter Stachowski
parent
895eecc6c2
commit
2bf92b906d
@@ -172,6 +172,20 @@ function init_trove {
|
||||
# Initialize the trove database
|
||||
$TROVE_MANAGE db_sync
|
||||
|
||||
# Add an admin user to the 'tempest' alt_demo tenant.
|
||||
# This is needed to test the guest_log functionality.
|
||||
# The first part mimics the tempest setup, so make sure we have that.
|
||||
ALT_USERNAME=${ALT_USERNAME:-alt_demo}
|
||||
ALT_TENANT_NAME=${ALT_TENANT_NAME:-alt_demo}
|
||||
get_or_create_project ${ALT_TENANT_NAME} default
|
||||
get_or_create_user ${ALT_USERNAME} "$ADMIN_PASSWORD" "default" "alt_demo@example.com"
|
||||
get_or_add_user_project_role Member ${ALT_USERNAME} ${ALT_TENANT_NAME}
|
||||
|
||||
# The second part adds an admin user to the tenant.
|
||||
ADMIN_ALT_USERNAME=${ADMIN_ALT_USERNAME:-admin_${ALT_USERNAME}}
|
||||
get_or_create_user ${ADMIN_ALT_USERNAME} "$ADMIN_PASSWORD" "default" "admin_alt_demo@example.com"
|
||||
get_or_add_user_project_role admin ${ADMIN_ALT_USERNAME} ${ALT_TENANT_NAME}
|
||||
|
||||
# If no guest image is specified, skip remaining setup
|
||||
[ -z "$TROVE_GUEST_IMAGE_URL" ] && return 0
|
||||
|
||||
|
||||
@@ -43,3 +43,4 @@ oslo.messaging!=2.8.0,!=3.1.0,>2.6.1 # Apache-2.0
|
||||
osprofiler>=0.4.0 # Apache-2.0
|
||||
oslo.log>=1.14.0 # Apache-2.0
|
||||
oslo.db>=4.1.0 # Apache-2.0
|
||||
enum34;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD
|
||||
|
||||
@@ -6,7 +6,6 @@ hacking<0.11,>=0.10.0
|
||||
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
|
||||
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
|
||||
reno>=0.1.1 # Apache2
|
||||
|
||||
coverage>=3.6 # Apache-2.0
|
||||
nose # LGPL
|
||||
nosexcover # BSD
|
||||
@@ -16,7 +15,6 @@ WebTest>=2.0 # MIT
|
||||
wsgi-intercept>=0.6.1 # MIT License
|
||||
proboscis>=1.2.5.3 # Apache-2.0
|
||||
http://tarballs.openstack.org/python-troveclient/python-troveclient-master.tar.gz#egg=python-troveclient
|
||||
|
||||
mock>=1.2 # BSD
|
||||
mox3>=0.7.0 # Apache-2.0
|
||||
testtools>=1.4.0 # MIT
|
||||
|
||||
@@ -106,6 +106,14 @@ class API(wsgi.Router):
|
||||
controller=instance_resource,
|
||||
action="configuration",
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect("/{tenant_id}/instances/{id}/log",
|
||||
controller=instance_resource,
|
||||
action="guest_log_list",
|
||||
conditions={'method': ['GET']})
|
||||
mapper.connect("/{tenant_id}/instances/{id}/log",
|
||||
controller=instance_resource,
|
||||
action="guest_log_action",
|
||||
conditions={'method': ['POST']})
|
||||
|
||||
def _cluster_router(self, mapper):
|
||||
cluster_resource = ClusterController().create_resource()
|
||||
|
||||
@@ -514,6 +514,21 @@ backup = {
|
||||
}
|
||||
}
|
||||
|
||||
guest_log = {
|
||||
"action": {
|
||||
"name": "guest_log:action",
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": non_empty_string,
|
||||
"enable": boolean_string,
|
||||
"disable": boolean_string,
|
||||
"publish": boolean_string,
|
||||
"discard": boolean_string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configuration = {
|
||||
"create": {
|
||||
"name": "configuration:create",
|
||||
|
||||
@@ -396,6 +396,13 @@ common_opts = [
|
||||
cfg.IntOpt('timeout_wait_for_service', default=120,
|
||||
help='Maximum time (in seconds) to wait for a service to '
|
||||
'become alive.'),
|
||||
cfg.StrOpt('guest_log_container_name',
|
||||
default='database_logs',
|
||||
help='Name of container that stores guest log components.'),
|
||||
cfg.IntOpt('guest_log_limit', default=1000000,
|
||||
help='Maximum size of a chunk saved in guest log container.'),
|
||||
cfg.IntOpt('guest_log_expiry', default=2592000,
|
||||
help='Expiry (in seconds) of objects in guest log container.'),
|
||||
]
|
||||
|
||||
# Profiling specific option groups
|
||||
@@ -539,6 +546,11 @@ mysql_opts = [
|
||||
help='Databases to exclude when listing databases.',
|
||||
deprecated_name='ignore_dbs',
|
||||
deprecated_group='DEFAULT'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='general,slow_query',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
cfg.IntOpt('guest_log_long_query_time', default=1000,
|
||||
help='The time in milliseconds that a statement must take in '
|
||||
'in order to be logged in the slow_query log.'),
|
||||
]
|
||||
|
||||
# Percona
|
||||
@@ -612,6 +624,11 @@ percona_opts = [
|
||||
help='Databases to exclude when listing databases.',
|
||||
deprecated_name='ignore_dbs',
|
||||
deprecated_group='DEFAULT'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='general,slow_query',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
cfg.IntOpt('guest_log_long_query_time', default=1000,
|
||||
help='The time in milliseconds that a statement must take in '
|
||||
'in order to be logged in the slow_query log.'),
|
||||
]
|
||||
|
||||
# Percona XtraDB Cluster
|
||||
@@ -689,6 +706,11 @@ pxc_opts = [
|
||||
cfg.StrOpt('root_controller',
|
||||
default='trove.extensions.common.service.DefaultRootController',
|
||||
help='Root controller implementation for pxc.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='general,slow_query',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
cfg.IntOpt('guest_log_long_query_time', default=1000,
|
||||
help='The time in milliseconds that a statement must take in '
|
||||
'in order to be logged in the slow_query log.'),
|
||||
]
|
||||
|
||||
# Redis
|
||||
@@ -758,6 +780,8 @@ redis_opts = [
|
||||
cfg.StrOpt('root_controller',
|
||||
default='trove.extensions.common.service.DefaultRootController',
|
||||
help='Root controller implementation for redis.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
]
|
||||
|
||||
# Cassandra
|
||||
@@ -803,6 +827,8 @@ cassandra_opts = [
|
||||
cfg.StrOpt('root_controller',
|
||||
default='trove.extensions.common.service.DefaultRootController',
|
||||
help='Root controller implementation for cassandra.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
]
|
||||
|
||||
# Couchbase
|
||||
@@ -859,6 +885,8 @@ couchbase_opts = [
|
||||
cfg.StrOpt('root_controller',
|
||||
default='trove.extensions.common.service.DefaultRootController',
|
||||
help='Root controller implementation for couchbase.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
]
|
||||
|
||||
# MongoDB
|
||||
@@ -940,6 +968,8 @@ mongodb_opts = [
|
||||
cfg.StrOpt('root_controller',
|
||||
default='trove.extensions.common.service.DefaultRootController',
|
||||
help='Root controller implementation for mongodb.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
]
|
||||
|
||||
# PostgreSQL
|
||||
@@ -987,6 +1017,13 @@ postgresql_opts = [
|
||||
cfg.StrOpt('root_controller',
|
||||
default='trove.extensions.common.service.DefaultRootController',
|
||||
help='Root controller implementation for postgresql.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='general',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
cfg.IntOpt('guest_log_long_query_time', default=0,
|
||||
help="The time in milliseconds that a statement must take in "
|
||||
"in order to be logged in the 'general' log. A value of "
|
||||
"'0' logs all statements, while '-1' turns off "
|
||||
"statement logging."),
|
||||
]
|
||||
|
||||
# Apache CouchDB
|
||||
@@ -1030,6 +1067,8 @@ couchdb_opts = [
|
||||
cfg.StrOpt('root_controller',
|
||||
default='trove.extensions.common.service.DefaultRootController',
|
||||
help='Root controller implementation for couchdb.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
]
|
||||
|
||||
# Vertica
|
||||
@@ -1090,6 +1129,8 @@ vertica_opts = [
|
||||
default='trove.extensions.vertica.service.'
|
||||
'VerticaRootController',
|
||||
help='Root controller implementation for Vertica.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
]
|
||||
|
||||
# DB2
|
||||
@@ -1134,6 +1175,8 @@ db2_opts = [
|
||||
cfg.StrOpt('root_controller',
|
||||
default='trove.extensions.common.service.DefaultRootController',
|
||||
help='Root controller implementation for db2.'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
]
|
||||
|
||||
# MariaDB
|
||||
@@ -1203,6 +1246,11 @@ mariadb_opts = [
|
||||
help='Databases to exclude when listing databases.',
|
||||
deprecated_name='ignore_dbs',
|
||||
deprecated_group='DEFAULT'),
|
||||
cfg.StrOpt('guest_log_exposed_logs', default='general,slow_query',
|
||||
help='List of Guest Logs to expose for publishing.'),
|
||||
cfg.IntOpt('guest_log_long_query_time', default=1000,
|
||||
help='The time in milliseconds that a statement must take in '
|
||||
'in order to be logged in the slow_query log.'),
|
||||
]
|
||||
|
||||
# RPC version groups
|
||||
|
||||
@@ -230,6 +230,11 @@ class UnprocessableEntity(TroveError):
|
||||
message = _("Unable to process the contained request.")
|
||||
|
||||
|
||||
class UnauthorizedRequest(TroveError):
|
||||
|
||||
message = _("Unauthorized request.")
|
||||
|
||||
|
||||
class CannotResizeToSameSize(TroveError):
|
||||
|
||||
message = _("No change was requested in the size of the instance.")
|
||||
|
||||
@@ -93,12 +93,15 @@ class ServiceStatuses(object):
|
||||
BUILDING = ServiceStatus(0x09, 'building', 'BUILD')
|
||||
PROMOTING = ServiceStatus(0x10, 'promoting replica', 'PROMOTE')
|
||||
EJECTING = ServiceStatus(0x11, 'ejecting replica source', 'EJECT')
|
||||
LOGGING = ServiceStatus(0x12, 'transferring guest logs', 'LOGGING')
|
||||
UNKNOWN = ServiceStatus(0x16, 'unknown', 'ERROR')
|
||||
NEW = ServiceStatus(0x17, 'new', 'NEW')
|
||||
DELETED = ServiceStatus(0x05, 'deleted', 'DELETED')
|
||||
FAILED_TIMEOUT_GUESTAGENT = ServiceStatus(0x18, 'guestagent error',
|
||||
'ERROR')
|
||||
INSTANCE_READY = ServiceStatus(0x19, 'instance ready', 'BUILD')
|
||||
RESTART_REQUIRED = ServiceStatus(0x20, 'restart required',
|
||||
'RESTART_REQUIRED')
|
||||
|
||||
# Dissuade further additions at run-time.
|
||||
ServiceStatus.__init__ = None
|
||||
|
||||
@@ -19,7 +19,6 @@ import csv
|
||||
import json
|
||||
import re
|
||||
import six
|
||||
import StringIO
|
||||
import yaml
|
||||
|
||||
from ConfigParser import SafeConfigParser
|
||||
@@ -198,7 +197,7 @@ class IniCodec(StreamCodec):
|
||||
|
||||
def serialize(self, dict_data):
|
||||
parser = self._init_config_parser(dict_data)
|
||||
output = StringIO.StringIO()
|
||||
output = six.StringIO()
|
||||
parser.write(output)
|
||||
|
||||
return output.getvalue()
|
||||
@@ -212,8 +211,8 @@ class IniCodec(StreamCodec):
|
||||
for s in parser.sections()}
|
||||
|
||||
def _pre_parse(self, stream):
|
||||
buf = StringIO.StringIO()
|
||||
for line in StringIO.StringIO(stream):
|
||||
buf = six.StringIO()
|
||||
for line in six.StringIO(stream):
|
||||
# Ignore commented lines.
|
||||
if not line.startswith(self._comment_markers):
|
||||
# Strip leading and trailing whitespaces from each line.
|
||||
@@ -285,7 +284,7 @@ class PropertiesCodec(StreamCodec):
|
||||
self._unpack_singletons = unpack_singletons
|
||||
|
||||
def serialize(self, dict_data):
|
||||
output = StringIO.StringIO()
|
||||
output = six.StringIO()
|
||||
writer = csv.writer(output, delimiter=self._delimiter,
|
||||
quoting=self.QUOTING_MODE,
|
||||
strict=self.STRICT_MODE,
|
||||
@@ -297,7 +296,7 @@ class PropertiesCodec(StreamCodec):
|
||||
return output.getvalue()
|
||||
|
||||
def deserialize(self, stream):
|
||||
reader = csv.reader(StringIO.StringIO(stream),
|
||||
reader = csv.reader(six.StringIO(stream),
|
||||
delimiter=self._delimiter,
|
||||
quoting=self.QUOTING_MODE,
|
||||
strict=self.STRICT_MODE,
|
||||
@@ -373,4 +372,4 @@ class JsonCodec(StreamCodec):
|
||||
return json.dumps(dict_data)
|
||||
|
||||
def deserialize(self, stream):
|
||||
return json.load(StringIO.StringIO(stream))
|
||||
return json.load(six.StringIO(stream))
|
||||
|
||||
@@ -200,7 +200,7 @@ def build_polling_task(retriever, condition=lambda value: value,
|
||||
raise exception.PollTimeOut
|
||||
|
||||
return loopingcall.FixedIntervalLoopingCall(
|
||||
f=poll_and_check).start(sleep_time, True)
|
||||
f=poll_and_check).start(sleep_time, initial_delay=False)
|
||||
|
||||
|
||||
def poll_until(retriever, condition=lambda value: value,
|
||||
|
||||
@@ -427,3 +427,17 @@ class API(object):
|
||||
LOG.debug("Demoting instance %s to non-master.", self.id)
|
||||
self._call("demote_replication_master", AGENT_HIGH_TIMEOUT,
|
||||
self.version_cap)
|
||||
|
||||
def guest_log_list(self):
|
||||
LOG.debug("Retrieving guest log list for %s.", self.id)
|
||||
result = self._call("guest_log_list", AGENT_HIGH_TIMEOUT,
|
||||
self.version_cap)
|
||||
LOG.debug("guest_log_list 1 returns %s", result)
|
||||
return result
|
||||
|
||||
def guest_log_action(self, log_name, enable, disable, publish, discard):
|
||||
LOG.debug("Processing guest log '%s' for %s.", log_name, self.id)
|
||||
return self._call("guest_log_action", AGENT_HIGH_TIMEOUT,
|
||||
self.version_cap, log_name=log_name,
|
||||
enable=enable, disable=disable,
|
||||
publish=publish, discard=discard)
|
||||
|
||||
@@ -75,16 +75,21 @@ def exists(path, is_directory=False, as_root=False):
|
||||
:param as_root: Execute as root.
|
||||
:type as_root: boolean
|
||||
"""
|
||||
if as_root:
|
||||
|
||||
found = (not is_directory and os.path.isfile(path) or
|
||||
(is_directory and os.path.isdir(path)))
|
||||
|
||||
# Only check as root if we can't see it as the regular user, since
|
||||
# this is more expensive
|
||||
if not found and as_root:
|
||||
test_flag = '-d' if is_directory else '-f'
|
||||
cmd = 'test %s %s && echo 1 || echo 0' % (test_flag, path)
|
||||
stdout, _ = utils.execute_with_timeout(
|
||||
cmd, shell=True, check_exit_code=False,
|
||||
run_as_root=True, root_helper='sudo')
|
||||
return bool(int(stdout))
|
||||
found = bool(int(stdout))
|
||||
|
||||
return (not is_directory and os.path.isfile(path) or
|
||||
(is_directory and os.path.isdir(path)))
|
||||
return found
|
||||
|
||||
|
||||
def _read_file_as_root(path, codec):
|
||||
@@ -182,9 +187,13 @@ class FileMode(object):
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def SET_FULL(cls):
|
||||
def SET_ALL_RWX(cls):
|
||||
return cls(reset=[stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO]) # =0777
|
||||
|
||||
@classmethod
|
||||
def SET_FULL(cls):
|
||||
return cls.SET_ALL_RWX()
|
||||
|
||||
@classmethod
|
||||
def SET_GRP_RW_OTH_R(cls):
|
||||
return cls(reset=[stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH]) # =0064
|
||||
@@ -198,13 +207,37 @@ class FileMode(object):
|
||||
return cls(reset=[stat.S_IRUSR | stat.S_IWUSR]) # =0600
|
||||
|
||||
@classmethod
|
||||
def ADD_READ_ALL(cls):
|
||||
def ADD_ALL_R(cls):
|
||||
return cls(add=[stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH]) # +0444
|
||||
|
||||
@classmethod
|
||||
def ADD_READ_ALL(cls):
|
||||
return cls.ADD_ALL_R()
|
||||
|
||||
@classmethod
|
||||
def ADD_USR_RW_GRP_RW(cls):
|
||||
return cls(add=[stat.S_IRUSR | stat.S_IWUSR |
|
||||
stat.S_IRGRP | stat.S_IWGRP]) # +0660
|
||||
|
||||
@classmethod
|
||||
def ADD_USR_RW_GRP_RW_OTH_R(cls):
|
||||
return cls(add=[stat.S_IRUSR | stat.S_IWUSR |
|
||||
stat.S_IRGRP | stat.S_IWGRP |
|
||||
stat.S_IROTH]) # +0664
|
||||
|
||||
@classmethod
|
||||
def ADD_GRP_RW(cls):
|
||||
return cls(add=[stat.S_IRGRP | stat.S_IWGRP]) # +0060
|
||||
|
||||
@classmethod
|
||||
def ADD_GRP_RX(cls):
|
||||
return cls(add=[stat.S_IRGRP | stat.S_IXGRP]) # +0050
|
||||
|
||||
@classmethod
|
||||
def ADD_GRP_RX_OTH_RX(cls):
|
||||
return cls(add=[stat.S_IRGRP | stat.S_IXGRP |
|
||||
stat.S_IROTH | stat.S_IXOTH]) # +0055
|
||||
|
||||
def __init__(self, reset=None, add=None, remove=None):
|
||||
self._reset = list(reset) if reset is not None else []
|
||||
self._add = list(add) if add is not None else []
|
||||
|
||||
@@ -439,6 +439,8 @@ class SetServerVariable(object):
|
||||
return "SET GLOBAL %s=%s" % (self.key, 0)
|
||||
elif self.value is None:
|
||||
return "SET GLOBAL %s" % (self.key)
|
||||
elif isinstance(self.value, str):
|
||||
return "SET GLOBAL %s='%s'" % (self.key, self.value)
|
||||
else:
|
||||
return "SET GLOBAL %s=%s" % (self.key, self.value)
|
||||
|
||||
|
||||
@@ -24,13 +24,16 @@ from .service.install import PgSqlInstall
|
||||
from .service.root import PgSqlRoot
|
||||
from .service.status import PgSqlAppStatus
|
||||
import pgutil
|
||||
from trove.common import cfg
|
||||
from trove.common import utils
|
||||
from trove.guestagent import backup
|
||||
from trove.guestagent.datastore import manager
|
||||
from trove.guestagent import guest_log
|
||||
from trove.guestagent import volume
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class Manager(
|
||||
@@ -54,6 +57,40 @@ class Manager(
|
||||
def configuration_manager(self):
|
||||
return self._configuration_manager
|
||||
|
||||
@property
|
||||
def datastore_log_defs(self):
|
||||
owner = 'postgres'
|
||||
datastore_dir = '/var/log/postgresql/'
|
||||
long_query_time = CONF.get(self.manager).get(
|
||||
'guest_log_long_query_time')
|
||||
general_log_file = self.build_log_file_name(
|
||||
self.GUEST_LOG_DEFS_GENERAL_LABEL, owner,
|
||||
datastore_dir=datastore_dir)
|
||||
general_log_dir, general_log_filename = os.path.split(general_log_file)
|
||||
return {
|
||||
self.GUEST_LOG_DEFS_GENERAL_LABEL: {
|
||||
self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.USER,
|
||||
self.GUEST_LOG_USER_LABEL: owner,
|
||||
self.GUEST_LOG_FILE_LABEL: general_log_file,
|
||||
self.GUEST_LOG_ENABLE_LABEL: {
|
||||
'logging_collector': 'on',
|
||||
'log_destination': self._quote_str('stderr'),
|
||||
'log_directory': self._quote_str(general_log_dir),
|
||||
'log_filename': self._quote_str(general_log_filename),
|
||||
'log_statement': self._quote_str('all'),
|
||||
'debug_print_plan': 'on',
|
||||
'log_min_duration_statement': long_query_time,
|
||||
},
|
||||
self.GUEST_LOG_DISABLE_LABEL: {
|
||||
'logging_collector': 'off',
|
||||
},
|
||||
self.GUEST_LOG_RESTART_LABEL: True,
|
||||
},
|
||||
}
|
||||
|
||||
def _quote_str(self, value):
|
||||
return "'%s'" % value
|
||||
|
||||
def do_prepare(self, context, packages, databases, memory_mb, users,
|
||||
device_path, mount_point, backup_info, config_contents,
|
||||
root_password, overrides, cluster_config, snapshot):
|
||||
|
||||
@@ -21,6 +21,8 @@ from trove.common import cfg
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.datastore.experimental.postgresql.service.status import (
|
||||
PgSqlAppStatus)
|
||||
from trove.guestagent import guest_log
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
@@ -55,6 +57,7 @@ class PgSqlProcess(object):
|
||||
def restart(self, context):
|
||||
PgSqlAppStatus.get().restart_db_service(
|
||||
self.SERVICE_CANDIDATES, CONF.state_change_wait_time)
|
||||
self.set_guest_log_status(guest_log.LogStatus.Restart_Completed)
|
||||
|
||||
def start_db(self, context, enable_on_boot=True, update_db=False):
|
||||
PgSqlAppStatus.get().start_db_service(
|
||||
|
||||
@@ -16,13 +16,19 @@
|
||||
|
||||
import abc
|
||||
|
||||
from oslo_config import cfg as oslo_cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import periodic_task
|
||||
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common.i18n import _
|
||||
from trove.common import instance
|
||||
from trove.guestagent.common import guestagent_utils
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.common.operating_system import FileMode
|
||||
from trove.guestagent import dbaas
|
||||
from trove.guestagent import guest_log
|
||||
from trove.guestagent.strategies import replication as repl_strategy
|
||||
from trove.guestagent import volume
|
||||
|
||||
@@ -36,8 +42,23 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
functionality should be pulled back here from the existing managers.
|
||||
"""
|
||||
|
||||
def __init__(self, manager_name):
|
||||
GUEST_LOG_TYPE_LABEL = 'type'
|
||||
GUEST_LOG_USER_LABEL = 'user'
|
||||
GUEST_LOG_FILE_LABEL = 'file'
|
||||
GUEST_LOG_SECTION_LABEL = 'section'
|
||||
GUEST_LOG_ENABLE_LABEL = 'enable'
|
||||
GUEST_LOG_DISABLE_LABEL = 'disable'
|
||||
GUEST_LOG_RESTART_LABEL = 'restart'
|
||||
|
||||
GUEST_LOG_BASE_DIR = '/var/log/trove'
|
||||
GUEST_LOG_DATASTORE_DIRNAME = 'datastore'
|
||||
|
||||
GUEST_LOG_DEFS_GUEST_LABEL = 'guest'
|
||||
GUEST_LOG_DEFS_GENERAL_LABEL = 'general'
|
||||
GUEST_LOG_DEFS_ERROR_LABEL = 'error'
|
||||
GUEST_LOG_DEFS_SLOW_QUERY_LABEL = 'slow_query'
|
||||
|
||||
def __init__(self, manager_name):
|
||||
super(Manager, self).__init__(CONF)
|
||||
|
||||
# Manager properties
|
||||
@@ -45,6 +66,12 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
self.__manager = None
|
||||
self.__prepare_error = False
|
||||
|
||||
# Guest log
|
||||
self._guest_log_context = None
|
||||
self._guest_log_loaded_context = None
|
||||
self._guest_log_cache = None
|
||||
self._guest_log_defs = None
|
||||
|
||||
@property
|
||||
def manager_name(self):
|
||||
"""This returns the passed-in name of the manager."""
|
||||
@@ -104,6 +131,104 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
"""
|
||||
return None
|
||||
|
||||
@property
|
||||
def datastore_log_defs(self):
|
||||
"""Any datastore-specific log files should be overridden in this dict
|
||||
by the corresponding Manager class.
|
||||
|
||||
Format of a dict entry:
|
||||
|
||||
'name_of_log': {self.GUEST_LOG_TYPE_LABEL:
|
||||
Specified by the Enum in guest_log.LogType,
|
||||
self.GUEST_LOG_USER_LABEL:
|
||||
User that owns the file,
|
||||
self.GUEST_LOG_FILE_LABEL:
|
||||
Path on filesystem where the log resides,
|
||||
self.GUEST_LOG_SECTION_LABEL:
|
||||
Section where to put config (if ini style)
|
||||
self.GUEST_LOG_ENABLE_LABEL: {
|
||||
Dict of config_group settings to enable log},
|
||||
self.GUEST_LOG_DISABLE_LABEL: {
|
||||
Dict of config_group settings to disable log},
|
||||
|
||||
See guestagent_log_defs for an example.
|
||||
"""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def guestagent_log_defs(self):
|
||||
"""These are log files that should be available on every Trove
|
||||
instance. By definition, these should be of type LogType.SYS
|
||||
"""
|
||||
log_dir = CONF.get('log_dir', '/var/log/trove/')
|
||||
log_file = CONF.get('log_file', 'trove-guestagent.log')
|
||||
guestagent_log = guestagent_utils.build_file_path(log_dir, log_file)
|
||||
return {
|
||||
self.GUEST_LOG_DEFS_GUEST_LABEL: {
|
||||
self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.SYS,
|
||||
self.GUEST_LOG_USER_LABEL: None,
|
||||
self.GUEST_LOG_FILE_LABEL: guestagent_log,
|
||||
},
|
||||
}
|
||||
|
||||
@property
|
||||
def guest_log_defs(self):
|
||||
"""Return all the guest log defs."""
|
||||
if not self._guest_log_defs:
|
||||
self._guest_log_defs = dict(self.datastore_log_defs)
|
||||
self._guest_log_defs.update(self.guestagent_log_defs)
|
||||
return self._guest_log_defs
|
||||
|
||||
@property
|
||||
def guest_log_context(self):
|
||||
return self._guest_log_context
|
||||
|
||||
@guest_log_context.setter
|
||||
def guest_log_context(self, context):
|
||||
self._guest_log_context = context
|
||||
|
||||
@property
|
||||
def guest_log_cache(self):
|
||||
"""Make sure the guest_log_cache is loaded and return it."""
|
||||
self._refresh_guest_log_cache()
|
||||
return self._guest_log_cache
|
||||
|
||||
def _refresh_guest_log_cache(self):
|
||||
if self._guest_log_cache:
|
||||
# Replace the context if it's changed
|
||||
if self._guest_log_loaded_context != self.guest_log_context:
|
||||
for log_name in self._guest_log_cache.keys():
|
||||
self._guest_log_cache[log_name].context = (
|
||||
self.guest_log_context)
|
||||
else:
|
||||
# Load the initial cache
|
||||
self._guest_log_cache = {}
|
||||
if self.guest_log_context:
|
||||
gl_defs = self.guest_log_defs
|
||||
try:
|
||||
exposed_logs = CONF.get(self.manager).get(
|
||||
'guest_log_exposed_logs')
|
||||
except oslo_cfg.NoSuchOptError:
|
||||
exposed_logs = ''
|
||||
LOG.debug("Available log defs: %s" % ",".join(gl_defs.keys()))
|
||||
exposed_logs = exposed_logs.lower().replace(',', ' ').split()
|
||||
LOG.debug("Exposing log defs: %s" % ",".join(exposed_logs))
|
||||
expose_all = 'all' in exposed_logs
|
||||
for log_name in gl_defs.keys():
|
||||
gl_def = gl_defs[log_name]
|
||||
exposed = expose_all or log_name in exposed_logs
|
||||
LOG.debug("Building guest log '%s' from def: %s "
|
||||
"(exposed: %s)" %
|
||||
(log_name, gl_def, exposed))
|
||||
self._guest_log_cache[log_name] = guest_log.GuestLog(
|
||||
self.guest_log_context, log_name,
|
||||
gl_def[self.GUEST_LOG_TYPE_LABEL],
|
||||
gl_def[self.GUEST_LOG_USER_LABEL],
|
||||
gl_def[self.GUEST_LOG_FILE_LABEL],
|
||||
exposed)
|
||||
|
||||
self._guest_log_loaded_context = self.guest_log_context
|
||||
|
||||
################
|
||||
# Status related
|
||||
################
|
||||
@@ -259,6 +384,175 @@ class Manager(periodic_task.PeriodicTasks):
|
||||
LOG.debug("Cluster creation complete, starting status checks.")
|
||||
self.status.end_install()
|
||||
|
||||
#############
|
||||
# Log related
|
||||
#############
|
||||
def guest_log_list(self, context):
|
||||
LOG.info(_("Getting list of guest logs."))
|
||||
self.guest_log_context = context
|
||||
gl_cache = self.guest_log_cache
|
||||
result = filter(None, [gl_cache[log_name].show()
|
||||
if gl_cache[log_name].exposed else None
|
||||
for log_name in gl_cache.keys()])
|
||||
LOG.info(_("Returning list of logs: %s") % result)
|
||||
return result
|
||||
|
||||
def guest_log_action(self, context, log_name, enable, disable,
|
||||
publish, discard):
|
||||
if enable and disable:
|
||||
raise exception.BadRequest("Cannot enable and disable log '%s'." %
|
||||
log_name)
|
||||
# Enable if we are publishing, unless told to disable
|
||||
if publish and not disable:
|
||||
enable = True
|
||||
LOG.info(_("Processing guest log '%(log)s' "
|
||||
"(enable=%(en)s, disable=%(dis)s, "
|
||||
"publish=%(pub)s, discard=%(disc)s).") %
|
||||
{'log': log_name, 'en': enable, 'dis': disable,
|
||||
'pub': publish, 'disc': discard})
|
||||
self.guest_log_context = context
|
||||
gl_cache = self.guest_log_cache
|
||||
if log_name in gl_cache:
|
||||
if ((gl_cache[log_name].type == guest_log.LogType.SYS) and
|
||||
not publish):
|
||||
if enable or disable:
|
||||
if enable:
|
||||
action_text = "enable"
|
||||
else:
|
||||
action_text = "disable"
|
||||
raise exception.BadRequest("Cannot %s a SYSTEM log ('%s')."
|
||||
% (action_text, log_name))
|
||||
if gl_cache[log_name].type == guest_log.LogType.USER:
|
||||
requires_change = (
|
||||
(gl_cache[log_name].enabled and disable) or
|
||||
(not gl_cache[log_name].enabled and enable))
|
||||
if requires_change:
|
||||
restart_required = self.guest_log_enable(
|
||||
context, log_name, disable)
|
||||
if restart_required:
|
||||
self.set_guest_log_status(
|
||||
guest_log.LogStatus.Restart_Required, log_name)
|
||||
gl_cache[log_name].enabled = enable
|
||||
log_details = gl_cache[log_name].show()
|
||||
if discard:
|
||||
log_details = gl_cache[log_name].discard_log()
|
||||
if publish:
|
||||
log_details = gl_cache[log_name].publish_log()
|
||||
LOG.info(_("Details for log '%(log)s': %(det)s") %
|
||||
{'log': log_name, 'det': log_details})
|
||||
return log_details
|
||||
|
||||
raise exception.NotFound("Log '%s' is not defined." % log_name)
|
||||
|
||||
def guest_log_enable(self, context, log_name, disable):
|
||||
"""This method can be overridden by datastore implementations to
|
||||
facilitate enabling and disabling USER type logs. If the logs
|
||||
can be enabled with simple configuration group changes, however,
|
||||
the code here will probably suffice.
|
||||
Must return whether the datastore needs to be restarted in order for
|
||||
the logging to begin.
|
||||
"""
|
||||
restart_required = False
|
||||
verb = ("Disabling" if disable else "Enabling")
|
||||
if self.configuration_manager:
|
||||
LOG.debug("%s log '%s'" % (verb, log_name))
|
||||
gl_def = self.guest_log_defs[log_name]
|
||||
enable_cfg_label = "%s_%s_log" % (self.GUEST_LOG_ENABLE_LABEL,
|
||||
log_name)
|
||||
disable_cfg_label = "%s_%s_log" % (self.GUEST_LOG_DISABLE_LABEL,
|
||||
log_name)
|
||||
restart_required = gl_def.get(self.GUEST_LOG_RESTART_LABEL,
|
||||
restart_required)
|
||||
if disable:
|
||||
self._apply_log_overrides(
|
||||
context, enable_cfg_label, disable_cfg_label,
|
||||
gl_def.get(self.GUEST_LOG_DISABLE_LABEL),
|
||||
gl_def.get(self.GUEST_LOG_SECTION_LABEL),
|
||||
restart_required)
|
||||
else:
|
||||
self._apply_log_overrides(
|
||||
context, disable_cfg_label, enable_cfg_label,
|
||||
gl_def.get(self.GUEST_LOG_ENABLE_LABEL),
|
||||
gl_def.get(self.GUEST_LOG_SECTION_LABEL),
|
||||
restart_required)
|
||||
else:
|
||||
msg = (_("%(verb)s log '%(log)s' not supported - "
|
||||
"no configuration manager defined!") %
|
||||
{'verb': verb, 'log': log_name})
|
||||
LOG.error(msg)
|
||||
raise exception.GuestError(msg)
|
||||
|
||||
return restart_required
|
||||
|
||||
def _apply_log_overrides(self, context, remove_label,
|
||||
apply_label, cfg_values, section_label,
|
||||
restart_required):
|
||||
self.configuration_manager.remove_system_override(
|
||||
change_id=remove_label)
|
||||
if cfg_values:
|
||||
config_man_values = cfg_values
|
||||
if section_label:
|
||||
config_man_values = {section_label: cfg_values}
|
||||
self.configuration_manager.apply_system_override(
|
||||
config_man_values, change_id=apply_label)
|
||||
if restart_required:
|
||||
self.status.set_status(instance.ServiceStatuses.RESTART_REQUIRED)
|
||||
else:
|
||||
self.apply_overrides(context, cfg_values)
|
||||
|
||||
def set_guest_log_status(self, status, log_name=None):
|
||||
"""Sets the status of log_name to 'status' - if log_name is not
|
||||
provided, sets the status on all logs.
|
||||
"""
|
||||
gl_cache = self.guest_log_cache
|
||||
names = [log_name]
|
||||
if not log_name or log_name not in gl_cache:
|
||||
names = gl_cache.keys()
|
||||
for name in names:
|
||||
# If we're already in restart mode and we're asked to set the
|
||||
# status to restart, assume enable/disable has been flipped
|
||||
# without a restart and set the status to restart done
|
||||
if (gl_cache[name].status == guest_log.LogStatus.Restart_Required
|
||||
and status == guest_log.LogStatus.Restart_Required):
|
||||
gl_cache[name].status = guest_log.LogStatus.Restart_Completed
|
||||
else:
|
||||
gl_cache[name].status = status
|
||||
|
||||
def build_log_file_name(self, log_name, owner, datastore_dir=None):
|
||||
"""Build a log file name based on the log_name and make sure the
|
||||
directories exist and are accessible by owner.
|
||||
"""
|
||||
if datastore_dir is None:
|
||||
base_dir = self.GUEST_LOG_BASE_DIR
|
||||
if not operating_system.exists(base_dir, is_directory=True):
|
||||
operating_system.create_directory(
|
||||
base_dir, user=owner, group=owner, force=True,
|
||||
as_root=True)
|
||||
datastore_dir = guestagent_utils.build_file_path(
|
||||
base_dir, self.GUEST_LOG_DATASTORE_DIRNAME)
|
||||
|
||||
if not operating_system.exists(datastore_dir, is_directory=True):
|
||||
operating_system.create_directory(
|
||||
datastore_dir, user=owner, group=owner, force=True,
|
||||
as_root=True)
|
||||
log_file_name = guestagent_utils.build_file_path(
|
||||
datastore_dir, '%s-%s.log' % (self.manager, log_name))
|
||||
|
||||
return self.validate_log_file(log_file_name, owner)
|
||||
|
||||
def validate_log_file(self, log_file, owner):
|
||||
"""Make sure the log file exists and is accessible by owner.
|
||||
"""
|
||||
if not operating_system.exists(log_file, as_root=True):
|
||||
operating_system.write_file(log_file, '', as_root=True)
|
||||
|
||||
operating_system.chown(log_file, user=owner, group=owner,
|
||||
as_root=True)
|
||||
operating_system.chmod(log_file, FileMode.ADD_USR_RW_GRP_RW_OTH_R,
|
||||
as_root=True)
|
||||
LOG.debug("Set log file '%s' as readable" % log_file)
|
||||
return log_file
|
||||
|
||||
###############
|
||||
# Not Supported
|
||||
###############
|
||||
|
||||
@@ -20,6 +20,8 @@ import os
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from trove.common import cfg
|
||||
from trove.common import configurations
|
||||
from trove.common import exception
|
||||
from trove.common.i18n import _
|
||||
from trove.common import instance as rd_instance
|
||||
@@ -27,10 +29,12 @@ from trove.guestagent import backup
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.datastore import manager
|
||||
from trove.guestagent.datastore.mysql_common import service
|
||||
from trove.guestagent import guest_log
|
||||
from trove.guestagent import volume
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class MySqlManager(manager.Manager):
|
||||
@@ -66,6 +70,56 @@ class MySqlManager(manager.Manager):
|
||||
return self.mysql_app(
|
||||
self.mysql_app_status.get()).configuration_manager
|
||||
|
||||
@property
|
||||
def datastore_log_defs(self):
|
||||
owner = 'mysql'
|
||||
datastore_dir = self.mysql_app.get_data_dir()
|
||||
server_section = configurations.MySQLConfParser.SERVER_CONF_SECTION
|
||||
long_query_time = CONF.get(self.manager).get(
|
||||
'guest_log_long_query_time') / 1000
|
||||
general_log_file = self.build_log_file_name(
|
||||
self.GUEST_LOG_DEFS_GENERAL_LABEL, owner,
|
||||
datastore_dir=datastore_dir)
|
||||
error_log_file = self.validate_log_file('/var/log/mysqld.log', owner)
|
||||
slow_query_log_file = self.build_log_file_name(
|
||||
self.GUEST_LOG_DEFS_SLOW_QUERY_LABEL, owner,
|
||||
datastore_dir=datastore_dir)
|
||||
return {
|
||||
self.GUEST_LOG_DEFS_GENERAL_LABEL: {
|
||||
self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.USER,
|
||||
self.GUEST_LOG_USER_LABEL: owner,
|
||||
self.GUEST_LOG_FILE_LABEL: general_log_file,
|
||||
self.GUEST_LOG_SECTION_LABEL: server_section,
|
||||
self.GUEST_LOG_ENABLE_LABEL: {
|
||||
'general_log': 'on',
|
||||
'general_log_file': general_log_file,
|
||||
'log_output': 'file',
|
||||
},
|
||||
self.GUEST_LOG_DISABLE_LABEL: {
|
||||
'general_log': 'off',
|
||||
},
|
||||
},
|
||||
self.GUEST_LOG_DEFS_SLOW_QUERY_LABEL: {
|
||||
self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.USER,
|
||||
self.GUEST_LOG_USER_LABEL: owner,
|
||||
self.GUEST_LOG_FILE_LABEL: slow_query_log_file,
|
||||
self.GUEST_LOG_SECTION_LABEL: server_section,
|
||||
self.GUEST_LOG_ENABLE_LABEL: {
|
||||
'slow_query_log': 'on',
|
||||
'slow_query_log_file': slow_query_log_file,
|
||||
'long_query_time': long_query_time,
|
||||
},
|
||||
self.GUEST_LOG_DISABLE_LABEL: {
|
||||
'slow_query_log': 'off',
|
||||
},
|
||||
},
|
||||
self.GUEST_LOG_DEFS_ERROR_LABEL: {
|
||||
self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.SYS,
|
||||
self.GUEST_LOG_USER_LABEL: owner,
|
||||
self.GUEST_LOG_FILE_LABEL: error_log_file,
|
||||
},
|
||||
}
|
||||
|
||||
def change_passwords(self, context, users):
|
||||
return self.mysql_admin().change_passwords(users)
|
||||
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
# Copyright 2015 Tesora Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from datetime import datetime
|
||||
import enum
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from oslo_log import log as logging
|
||||
from swiftclient.client import ClientException
|
||||
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common.i18n import _
|
||||
from trove.common.remote import create_swift_client
|
||||
from trove.common import stream_codecs
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.common.operating_system import FileMode
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class LogType(enum.Enum):
|
||||
"""Represent the type of the log object."""
|
||||
|
||||
# System logs. These are always enabled.
|
||||
SYS = 1
|
||||
|
||||
# User logs. These can be enabled or disabled.
|
||||
USER = 2
|
||||
|
||||
|
||||
class LogStatus(enum.Enum):
|
||||
"""Represent the status of the log object."""
|
||||
|
||||
# The log is disabled and potentially no data is being written to
|
||||
# the corresponding log file
|
||||
Disabled = 1
|
||||
|
||||
# Logging is on, but no determination has been made about data availability
|
||||
Enabled = 2
|
||||
|
||||
# Logging is on, but no log data is available to publish
|
||||
Unavailable = 3
|
||||
|
||||
# Logging is on and data is available to be published
|
||||
Ready = 4
|
||||
|
||||
# Logging is on and all data has been published
|
||||
Published = 5
|
||||
|
||||
# Logging is on and some data has been published
|
||||
Partial = 6
|
||||
|
||||
# Log file has been rotated, so next publish will discard log first
|
||||
Rotated = 7
|
||||
|
||||
# Waiting for a datastore restart to begin logging
|
||||
Restart_Required = 8
|
||||
|
||||
# Now that restart has completed, regular status can be reported again
|
||||
# This is an internal status
|
||||
Restart_Completed = 9
|
||||
|
||||
|
||||
class GuestLog(object):
|
||||
|
||||
MF_FILE_SUFFIX = '_metafile'
|
||||
MF_LABEL_LOG_NAME = 'log_name'
|
||||
MF_LABEL_LOG_TYPE = 'log_type'
|
||||
MF_LABEL_LOG_FILE = 'log_file'
|
||||
MF_LABEL_LOG_SIZE = 'log_size'
|
||||
MF_LABEL_LOG_HEADER = 'log_header_digest'
|
||||
|
||||
def __init__(self, log_context, log_name, log_type, log_user, log_file,
|
||||
log_exposed):
|
||||
self._context = log_context
|
||||
self._name = log_name
|
||||
self._type = log_type
|
||||
self._user = log_user
|
||||
self._file = log_file
|
||||
self._exposed = log_exposed
|
||||
self._size = None
|
||||
self._published_size = None
|
||||
self._header_digest = 'abc'
|
||||
self._published_header_digest = None
|
||||
self._status = None
|
||||
self._cached_context = None
|
||||
self._cached_swift_client = None
|
||||
self._enabled = log_type == LogType.SYS
|
||||
self._file_readable = False
|
||||
self._container_name = None
|
||||
self._codec = stream_codecs.JsonCodec()
|
||||
|
||||
self._set_status(self._type == LogType.USER,
|
||||
LogStatus.Disabled, LogStatus.Enabled)
|
||||
|
||||
# The directory should already exist - make sure we have access to it
|
||||
log_dir = os.path.dirname(self._file)
|
||||
operating_system.chmod(
|
||||
log_dir, FileMode.ADD_GRP_RX_OTH_RX, as_root=True)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self._context
|
||||
|
||||
@context.setter
|
||||
def context(self, context):
|
||||
self._context = context
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@property
|
||||
def swift_client(self):
|
||||
if not self._cached_swift_client or (
|
||||
self._cached_context != self.context):
|
||||
self._cached_swift_client = create_swift_client(self.context)
|
||||
self._cached_context = self.context
|
||||
return self._cached_swift_client
|
||||
|
||||
@property
|
||||
def exposed(self):
|
||||
return self._exposed or self.context.is_admin
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, enabled):
|
||||
self._enabled = enabled
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, status):
|
||||
# Keep the status in Restart_Required until we're set
|
||||
# to Restart_Completed
|
||||
if (self.status != LogStatus.Restart_Required or
|
||||
(self.status == LogStatus.Restart_Required and
|
||||
status == LogStatus.Restart_Completed)):
|
||||
self._status = status
|
||||
LOG.debug("Log status for '%s' set to %s" % (self._name, status))
|
||||
else:
|
||||
LOG.debug("Log status for '%s' *not* set to %s (currently %s)" %
|
||||
(self._name, status, self.status))
|
||||
|
||||
def get_container_name(self, force=False):
|
||||
if not self._container_name or force:
|
||||
container_name = CONF.guest_log_container_name
|
||||
try:
|
||||
self.swift_client.get_container(container_name, prefix='dummy')
|
||||
except ClientException as ex:
|
||||
if ex.http_status == 404:
|
||||
LOG.debug("Container '%s' not found; creating now" %
|
||||
container_name)
|
||||
self.swift_client.put_container(
|
||||
container_name, headers=self._get_headers())
|
||||
else:
|
||||
LOG.exception(_("Could not retrieve container '%s'") %
|
||||
container_name)
|
||||
raise
|
||||
self._container_name = container_name
|
||||
return self._container_name
|
||||
|
||||
def _set_status(self, use_first, first_status, second_status):
|
||||
if use_first:
|
||||
self.status = first_status
|
||||
else:
|
||||
self.status = second_status
|
||||
|
||||
def show(self):
|
||||
if self.exposed:
|
||||
self._refresh_details()
|
||||
container_name = 'None'
|
||||
prefix = 'None'
|
||||
if self._published_size:
|
||||
container_name = self.get_container_name()
|
||||
prefix = self._object_prefix()
|
||||
pending = self._size - self._published_size
|
||||
if self.status == LogStatus.Rotated:
|
||||
pending = self._size
|
||||
return {
|
||||
'name': self._name,
|
||||
'type': self._type.name,
|
||||
'status': self.status.name.replace('_', ' '),
|
||||
'published': self._published_size,
|
||||
'pending': pending,
|
||||
'container': container_name,
|
||||
'prefix': prefix,
|
||||
'metafile': self._metafile_name()
|
||||
}
|
||||
else:
|
||||
raise exception.UnauthorizedRequest(_(
|
||||
"Not authorized to show log '%s'.") % self._name)
|
||||
|
||||
def _refresh_details(self):
|
||||
|
||||
if self._published_size is None:
|
||||
# Initializing, so get all the values
|
||||
try:
|
||||
meta_details = self._get_meta_details()
|
||||
self._published_size = int(
|
||||
meta_details[self.MF_LABEL_LOG_SIZE])
|
||||
self._published_header_digest = (
|
||||
meta_details[self.MF_LABEL_LOG_HEADER])
|
||||
except ClientException as ex:
|
||||
if ex.http_status == 404:
|
||||
LOG.debug("No published metadata found for log '%s'" %
|
||||
self._name)
|
||||
self._published_size = 0
|
||||
else:
|
||||
LOG.exception(_("Could not get meta details for log '%s'")
|
||||
% self._name)
|
||||
raise
|
||||
|
||||
self._update_details()
|
||||
LOG.debug("Log size for '%s' set to %d (published %d)" % (
|
||||
self._name, self._size, self._published_size))
|
||||
|
||||
def _update_details(self):
|
||||
# Make sure we can read the file
|
||||
if not self._file_readable or not os.access(self._file, os.R_OK):
|
||||
if not os.access(self._file, os.R_OK):
|
||||
if operating_system.exists(self._file, as_root=True):
|
||||
operating_system.chmod(
|
||||
self._file, FileMode.ADD_ALL_R, as_root=True)
|
||||
self._file_readable = True
|
||||
|
||||
if os.path.isfile(self._file):
|
||||
logstat = os.stat(self._file)
|
||||
self._size = logstat.st_size
|
||||
self._update_log_header_digest(self._file)
|
||||
|
||||
if self._log_rotated():
|
||||
self.status = LogStatus.Rotated
|
||||
# See if we have stuff to publish
|
||||
elif logstat.st_size > self._published_size:
|
||||
self._set_status(self._published_size,
|
||||
LogStatus.Partial, LogStatus.Ready)
|
||||
# We've published everything so far
|
||||
elif logstat.st_size == self._published_size:
|
||||
self._set_status(self._published_size,
|
||||
LogStatus.Published, LogStatus.Enabled)
|
||||
# We've already handled this case (log rotated) so what gives?
|
||||
else:
|
||||
raise ("Bug in _log_rotated ?")
|
||||
else:
|
||||
self._published_size = 0
|
||||
self._size = 0
|
||||
|
||||
if not self._size or not self.enabled:
|
||||
user_status = LogStatus.Disabled
|
||||
if self.enabled:
|
||||
user_status = LogStatus.Enabled
|
||||
self._set_status(self._type == LogType.USER,
|
||||
user_status, LogStatus.Unavailable)
|
||||
|
||||
def _log_rotated(self):
|
||||
"""If the file is smaller than the last reported size
|
||||
or the first line hash is different, we can probably assume
|
||||
the file changed under our nose.
|
||||
"""
|
||||
if (self._published_size > 0 and
|
||||
(self._size < self._published_size or
|
||||
self._published_header_digest != self._header_digest)):
|
||||
return True
|
||||
|
||||
def _update_log_header_digest(self, log_file):
|
||||
with open(log_file, 'r') as log:
|
||||
self._header_digest = hashlib.md5(log.readline()).hexdigest()
|
||||
|
||||
def _get_headers(self):
|
||||
return {'X-Delete-After': CONF.guest_log_expiry}
|
||||
|
||||
def publish_log(self):
|
||||
if self.exposed:
|
||||
if self._log_rotated():
|
||||
LOG.debug("Log file rotation detected for '%s' - "
|
||||
"discarding old log" % self._name)
|
||||
self._delete_log_components()
|
||||
if os.path.isfile(self._file):
|
||||
self._publish_to_container(self._file)
|
||||
else:
|
||||
raise RuntimeError(_(
|
||||
"Cannot publish log file '%s' as it does not exist.") %
|
||||
self._file)
|
||||
return self.show()
|
||||
else:
|
||||
raise exception.UnauthorizedRequest(_(
|
||||
"Not authorized to publish log '%s'.") % self._name)
|
||||
|
||||
def discard_log(self):
|
||||
if self.exposed:
|
||||
self._delete_log_components()
|
||||
return self.show()
|
||||
else:
|
||||
raise exception.UnauthorizedRequest(_(
|
||||
"Not authorized to discard log '%s'.") % self._name)
|
||||
|
||||
def _delete_log_components(self):
|
||||
container_name = self.get_container_name(force=True)
|
||||
prefix = self._object_prefix()
|
||||
swift_files = [swift_file['name']
|
||||
for swift_file in self.swift_client.get_container(
|
||||
container_name, prefix=prefix)[1]]
|
||||
swift_files.append(self._metafile_name())
|
||||
for swift_file in swift_files:
|
||||
self.swift_client.delete_object(container_name, swift_file)
|
||||
self._set_status(self._type == LogType.USER,
|
||||
LogStatus.Disabled, LogStatus.Enabled)
|
||||
self._published_size = 0
|
||||
|
||||
def _publish_to_container(self, log_filename):
|
||||
log_component, log_lines = '', 0
|
||||
chunk_size = CONF.guest_log_limit
|
||||
container_name = self.get_container_name(force=True)
|
||||
|
||||
def _read_chunk(f):
|
||||
while True:
|
||||
current_chunk = f.read(chunk_size)
|
||||
if not current_chunk:
|
||||
break
|
||||
yield current_chunk
|
||||
|
||||
def _write_log_component():
|
||||
object_headers.update({'x-object-meta-lines': log_lines})
|
||||
component_name = '%s%s' % (self._object_prefix(),
|
||||
self._object_name())
|
||||
self.swift_client.put_object(container_name,
|
||||
component_name, log_component,
|
||||
headers=object_headers)
|
||||
self._published_size = (
|
||||
self._published_size + len(log_component))
|
||||
self._published_header_digest = self._header_digest
|
||||
|
||||
self._refresh_details()
|
||||
self._put_meta_details()
|
||||
object_headers = self._get_headers()
|
||||
with open(log_filename, 'r') as log:
|
||||
LOG.debug("seeking to %s", self._published_size)
|
||||
log.seek(self._published_size)
|
||||
for chunk in _read_chunk(log):
|
||||
for log_line in chunk.splitlines():
|
||||
if len(log_component) + len(log_line) > chunk_size:
|
||||
_write_log_component()
|
||||
log_component, log_lines = '', 0
|
||||
log_component = log_component + log_line + '\n'
|
||||
log_lines += 1
|
||||
if log_lines > 0:
|
||||
_write_log_component()
|
||||
self._put_meta_details()
|
||||
|
||||
def _put_meta_details(self):
|
||||
metafile_name = self._metafile_name()
|
||||
metafile_details = {
|
||||
self.MF_LABEL_LOG_NAME: self._name,
|
||||
self.MF_LABEL_LOG_TYPE: self._type.name,
|
||||
self.MF_LABEL_LOG_FILE: self._file,
|
||||
self.MF_LABEL_LOG_SIZE: self._published_size,
|
||||
self.MF_LABEL_LOG_HEADER: self._header_digest,
|
||||
}
|
||||
container_name = self.get_container_name()
|
||||
self.swift_client.put_object(container_name, metafile_name,
|
||||
self._codec.serialize(metafile_details),
|
||||
headers=self._get_headers())
|
||||
LOG.debug("_put_meta_details has published log size as %s",
|
||||
self._published_size)
|
||||
|
||||
def _metafile_name(self):
|
||||
return self._object_prefix().rstrip('/') + '_metafile'
|
||||
|
||||
def _object_prefix(self):
|
||||
return '%(instance_id)s/%(datastore)s-%(log)s/' % {
|
||||
'instance_id': CONF.guest_id,
|
||||
'datastore': CONF.datastore_manager,
|
||||
'log': self._name}
|
||||
|
||||
def _object_name(self):
|
||||
return 'log-%s' % str(datetime.utcnow()).replace(' ', 'T')
|
||||
|
||||
def _get_meta_details(self):
|
||||
LOG.debug("Getting meta details for '%s'" % self._name)
|
||||
metafile_name = self._metafile_name()
|
||||
container_name = self.get_container_name()
|
||||
headers, metafile_details = self.swift_client.get_object(
|
||||
container_name, metafile_name)
|
||||
LOG.debug("Found meta details for '%s'" % self._name)
|
||||
return self._codec.deserialize(metafile_details)
|
||||
@@ -297,6 +297,8 @@ class SimpleInstance(object):
|
||||
return InstanceStatus.PROMOTE
|
||||
if InstanceTasks.EJECTING.action == action:
|
||||
return InstanceStatus.EJECT
|
||||
if InstanceTasks.LOGGING.action == action:
|
||||
return InstanceStatus.LOGGING
|
||||
|
||||
# Check for server status.
|
||||
if self.db_info.server_status in ["BUILD", "ERROR", "REBOOT",
|
||||
|
||||
@@ -25,6 +25,7 @@ from trove.common import exception
|
||||
from trove.common.i18n import _
|
||||
from trove.common.i18n import _LI
|
||||
from trove.common import pagination
|
||||
from trove.common.remote import create_guest_client
|
||||
from trove.common import utils
|
||||
from trove.common import wsgi
|
||||
from trove.datastore import models as datastore_models
|
||||
@@ -331,3 +332,33 @@ class InstanceController(wsgi.Controller):
|
||||
{'instance_id': id, 'config': config})
|
||||
return wsgi.Result(views.DefaultConfigurationView(
|
||||
config).data(), 200)
|
||||
|
||||
def guest_log_list(self, req, tenant_id, id):
|
||||
"""Return all information about all logs for an instance."""
|
||||
LOG.debug("Listing logs for tenant %s" % tenant_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
instance = models.Instance.load(context, id)
|
||||
if not instance:
|
||||
raise exception.NotFound(uuid=id)
|
||||
client = create_guest_client(context, id)
|
||||
guest_log_list = client.guest_log_list()
|
||||
return wsgi.Result({'logs': guest_log_list}, 200)
|
||||
|
||||
def guest_log_action(self, req, body, tenant_id, id):
|
||||
"""Processes a guest log."""
|
||||
LOG.info(_("Processing log for tenant %s"), tenant_id)
|
||||
context = req.environ[wsgi.CONTEXT_KEY]
|
||||
instance = models.Instance.load(context, id)
|
||||
if not instance:
|
||||
raise exception.NotFound(uuid=id)
|
||||
log_name = body['name']
|
||||
enable = body.get('enable', None)
|
||||
disable = body.get('disable', None)
|
||||
publish = body.get('publish', None)
|
||||
discard = body.get('discard', None)
|
||||
if enable and disable:
|
||||
raise exception.BadRequest(_("Cannot enable and disable log."))
|
||||
client = create_guest_client(context, id)
|
||||
guest_log = client.guest_log_action(log_name, enable, disable,
|
||||
publish, discard)
|
||||
return wsgi.Result({'log': guest_log}, 200)
|
||||
|
||||
@@ -79,6 +79,7 @@ class InstanceTasks(object):
|
||||
'Promoting the instance to replica source.')
|
||||
EJECTING = InstanceTask(0x09, 'EJECTING',
|
||||
'Ejecting the replica source.')
|
||||
LOGGING = InstanceTask(0x0a, 'LOGGING', 'Transferring guest logs.')
|
||||
|
||||
BUILDING_ERROR_DNS = InstanceTask(0x50, 'BUILDING', 'Build error: DNS.',
|
||||
is_error=True)
|
||||
|
||||
@@ -165,3 +165,30 @@ class DefaultConfigurationView(object):
|
||||
for key, val in self.config:
|
||||
config_dict[key] = val
|
||||
return {"instance": {"configuration": config_dict}}
|
||||
|
||||
|
||||
class GuestLogView(object):
|
||||
|
||||
def __init__(self, guest_log):
|
||||
self.guest_log = guest_log
|
||||
|
||||
def data(self):
|
||||
return {
|
||||
'name': self.guest_log.name,
|
||||
'type': self.guest_log.type,
|
||||
'status': self.guest_log.status,
|
||||
'published': self.guest_log.published,
|
||||
'pending': self.guest_log.pending,
|
||||
'container': self.guest_log.container,
|
||||
'prefix': self.guest_log.prefix,
|
||||
'metafile': self.guest_log.metafile,
|
||||
}
|
||||
|
||||
|
||||
class GuestLogsView(object):
|
||||
|
||||
def __init__(self, guest_logs):
|
||||
self.guest_logs = guest_logs
|
||||
|
||||
def data(self):
|
||||
return [GuestLogView(l).data() for l in self.guest_logs]
|
||||
|
||||
@@ -1263,6 +1263,27 @@ class BuiltInstanceTasks(BuiltInstance, NotifyMixin, ConfigurationMixin):
|
||||
finally:
|
||||
self.reset_task_status()
|
||||
|
||||
def guest_log_list(self):
|
||||
LOG.info(_("Retrieving guest log list for instance %s.") % self.id)
|
||||
try:
|
||||
return self.guest.guest_log_list()
|
||||
except GuestError:
|
||||
LOG.error(_("Failed to retrieve guest log list for instance "
|
||||
"%s.") % self.id)
|
||||
finally:
|
||||
self.reset_task_status()
|
||||
|
||||
def guest_log_action(self, log_name, enable, disable, publish, discard):
|
||||
LOG.info(_("Processing guest log for instance %s.") % self.id)
|
||||
try:
|
||||
return self.guest.guest_log_action(log_name, enable, disable,
|
||||
publish, discard)
|
||||
except GuestError:
|
||||
LOG.error(_("Failed to process guest log for instance %s.")
|
||||
% self.id)
|
||||
finally:
|
||||
self.reset_task_status()
|
||||
|
||||
def refresh_compute_server_info(self):
|
||||
"""Refreshes the compute server field."""
|
||||
server = self.nova_client.servers.get(self.server.id)
|
||||
|
||||
@@ -70,6 +70,7 @@ class TestConfig(object):
|
||||
'dbaas_url': "http://localhost:8775/v1.0/dbaas",
|
||||
'version_url': "http://localhost:8775/",
|
||||
'nova_url': "http://localhost:8774/v2",
|
||||
'swift_url': "http://localhost:8080/v1/AUTH_",
|
||||
'dbaas_datastore': "mysql",
|
||||
'dbaas_datastore_id': "a00000a0-00a0-0a00-00a0-000a000000aa",
|
||||
'dbaas_datastore_name_no_versions': "Test_Datastore_1",
|
||||
|
||||
+34
-19
@@ -35,6 +35,7 @@ from trove.tests.api import versions
|
||||
from trove.tests.scenario.groups import backup_group
|
||||
from trove.tests.scenario.groups import cluster_actions_group
|
||||
from trove.tests.scenario.groups import database_actions_group
|
||||
from trove.tests.scenario.groups import guest_log_group
|
||||
from trove.tests.scenario.groups import instance_actions_group
|
||||
from trove.tests.scenario.groups import instance_create_group
|
||||
from trove.tests.scenario.groups import instance_delete_group
|
||||
@@ -126,6 +127,12 @@ base_groups = [
|
||||
GROUP_SETUP
|
||||
]
|
||||
|
||||
# cluster groups
|
||||
cluster_actions_groups = list(base_groups)
|
||||
cluster_actions_groups.extend([cluster_actions_group.GROUP,
|
||||
negative_cluster_actions_group.GROUP])
|
||||
|
||||
# instance groups
|
||||
instance_create_groups = list(base_groups)
|
||||
instance_create_groups.extend([instance_create_group.GROUP,
|
||||
instance_delete_group.GROUP])
|
||||
@@ -139,9 +146,8 @@ user_actions_groups.extend([user_actions_group.GROUP])
|
||||
database_actions_groups = list(instance_create_groups)
|
||||
database_actions_groups.extend([database_actions_group.GROUP])
|
||||
|
||||
cluster_actions_groups = list(base_groups)
|
||||
cluster_actions_groups.extend([cluster_actions_group.GROUP,
|
||||
negative_cluster_actions_group.GROUP])
|
||||
guest_log_groups = list(instance_create_groups)
|
||||
guest_log_groups.extend([guest_log_group.GROUP])
|
||||
|
||||
instance_actions_groups = list(instance_create_groups)
|
||||
instance_actions_groups.extend([instance_actions_group.GROUP])
|
||||
@@ -149,31 +155,40 @@ instance_actions_groups.extend([instance_actions_group.GROUP])
|
||||
replication_groups = list(instance_create_groups)
|
||||
replication_groups.extend([replication_group.GROUP])
|
||||
|
||||
# groups common to all datastores
|
||||
common_groups = list(instance_actions_groups)
|
||||
common_groups.extend([guest_log_groups])
|
||||
|
||||
# Module based groups
|
||||
register(["backup"], backup_groups)
|
||||
register(["cluster"], cluster_actions_groups)
|
||||
register(["database"], database_actions_groups)
|
||||
register(["instance_actions"], instance_actions_groups)
|
||||
register(["guest_log"], guest_log_groups)
|
||||
register(["instance", "instance_actions"], instance_actions_groups)
|
||||
register(["instance_create"], instance_create_groups)
|
||||
register(["user"], user_actions_groups)
|
||||
register(["replication"], replication_groups)
|
||||
|
||||
# Datastore based groups - these should contain all functionality
|
||||
# currently supported by the datastore
|
||||
register(["cassandra_supported"], backup_groups, instance_actions_groups)
|
||||
register(["couchbase_supported"], instance_actions_groups)
|
||||
register(["couchdb_supported"], instance_actions_groups)
|
||||
register(["postgresql_supported"], backup_groups, database_actions_groups,
|
||||
instance_actions_groups, user_actions_groups)
|
||||
register(["mongodb_supported"], backup_groups, cluster_actions_groups,
|
||||
database_actions_groups, instance_actions_groups, user_actions_groups)
|
||||
register(["cassandra_supported"], common_groups,
|
||||
backup_groups)
|
||||
register(["couchbase_supported"], common_groups)
|
||||
register(["couchdb_supported"], common_groups)
|
||||
register(["postgresql_supported"], common_groups,
|
||||
backup_groups, database_actions_groups, user_actions_groups)
|
||||
register(["mongodb_supported"], common_groups,
|
||||
backup_groups, cluster_actions_groups,
|
||||
database_actions_groups, user_actions_groups)
|
||||
register(["mysql_supported", "mariadb_supported", "percona_supported"],
|
||||
backup_groups, database_actions_groups, instance_actions_groups,
|
||||
common_groups,
|
||||
backup_groups, database_actions_groups,
|
||||
replication_groups, user_actions_groups)
|
||||
register(["redis_supported"], backup_groups, instance_actions_groups,
|
||||
replication_groups)
|
||||
register(["vertica_supported"], cluster_actions_groups,
|
||||
instance_actions_groups)
|
||||
register(["pxc_supported"], instance_actions_groups, cluster_actions_groups)
|
||||
register(["db2_supported"], database_actions_groups,
|
||||
instance_actions_groups, user_actions_groups)
|
||||
register(["redis_supported"], common_groups,
|
||||
backup_groups, replication_groups)
|
||||
register(["vertica_supported"], common_groups,
|
||||
cluster_actions_groups)
|
||||
register(["pxc_supported"], common_groups,
|
||||
cluster_actions_groups)
|
||||
register(["db2_supported"], common_groups,
|
||||
database_actions_groups, user_actions_groups)
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
# Copyright 2015 Tesora Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from proboscis import test
|
||||
|
||||
from trove.tests.scenario.groups import instance_create_group
|
||||
from trove.tests.scenario.groups.test_group import TestGroup
|
||||
|
||||
|
||||
GROUP = "scenario.guest_log_group"
|
||||
|
||||
|
||||
@test(depends_on_groups=[instance_create_group.GROUP], groups=[GROUP])
|
||||
class GuestLogGroup(TestGroup):
|
||||
"""Test Guest Log functionality."""
|
||||
|
||||
def __init__(self):
|
||||
super(GuestLogGroup, self).__init__(
|
||||
'guest_log_runners', 'GuestLogRunner')
|
||||
|
||||
@test
|
||||
def test_log_list(self):
|
||||
"""Test that log-list works."""
|
||||
self.test_runner.run_test_log_list()
|
||||
|
||||
@test
|
||||
def test_admin_log_list(self):
|
||||
"""Test that log-list works for admin user."""
|
||||
self.test_runner.run_test_admin_log_list()
|
||||
|
||||
@test
|
||||
def test_log_show(self):
|
||||
"""Test that log-show works on USER log."""
|
||||
self.test_runner.run_test_log_show()
|
||||
|
||||
@test
|
||||
def test_log_enable_sys(self):
|
||||
"""Ensure log-enable on SYS log fails."""
|
||||
self.test_runner.run_test_log_enable_sys()
|
||||
|
||||
@test
|
||||
def test_log_disable_sys(self):
|
||||
"""Ensure log-disable on SYS log fails."""
|
||||
self.test_runner.run_test_log_disable_sys()
|
||||
|
||||
@test
|
||||
def test_log_show_unauth_user(self):
|
||||
"""Ensure log-show by unauth client on USER log fails."""
|
||||
self.test_runner.run_test_log_show_unauth_user()
|
||||
|
||||
@test
|
||||
def test_log_list_unauth_user(self):
|
||||
"""Ensure log-list by unauth client on USER log fails."""
|
||||
self.test_runner.run_test_log_list_unauth_user()
|
||||
|
||||
@test
|
||||
def test_log_generator_unauth_user(self):
|
||||
"""Ensure log-generator by unauth client on USER log fails."""
|
||||
self.test_runner.run_test_log_generator_unauth_user()
|
||||
|
||||
@test
|
||||
def test_log_generator_publish_unauth_user(self):
|
||||
"""Ensure log-generator by unauth client with publish fails."""
|
||||
self.test_runner.run_test_log_generator_publish_unauth_user()
|
||||
|
||||
@test
|
||||
def test_log_show_unexposed_user(self):
|
||||
"""Ensure log-show on unexposed log fails for auth client."""
|
||||
self.test_runner.run_test_log_show_unexposed_user()
|
||||
|
||||
@test
|
||||
def test_log_enable_unexposed_user(self):
|
||||
"""Ensure log-enable on unexposed log fails for auth client."""
|
||||
self.test_runner.run_test_log_enable_unexposed_user()
|
||||
|
||||
@test
|
||||
def test_log_disable_unexposed_user(self):
|
||||
"""Ensure log-disable on unexposed log fails for auth client."""
|
||||
self.test_runner.run_test_log_disable_unexposed_user()
|
||||
|
||||
@test
|
||||
def test_log_publish_unexposed_user(self):
|
||||
"""Ensure log-publish on unexposed log fails for auth client."""
|
||||
self.test_runner.run_test_log_publish_unexposed_user()
|
||||
|
||||
@test
|
||||
def test_log_discard_unexposed_user(self):
|
||||
"""Ensure log-discard on unexposed log fails for auth client."""
|
||||
self.test_runner.run_test_log_discard_unexposed_user()
|
||||
|
||||
@test(runs_after=[test_log_show])
|
||||
def test_log_enable_user(self):
|
||||
"""Test log-enable on USER log."""
|
||||
self.test_runner.run_test_log_enable_user()
|
||||
|
||||
@test(runs_after=[test_log_enable_user])
|
||||
def test_log_enable_flip_user(self):
|
||||
"""Test that flipping restart-required log-enable works."""
|
||||
self.test_runner.run_test_log_enable_flip_user()
|
||||
|
||||
@test(runs_after=[test_log_enable_flip_user])
|
||||
def test_restart_datastore(self):
|
||||
"""Test restart datastore if required."""
|
||||
self.test_runner.run_test_restart_datastore()
|
||||
|
||||
@test(runs_after=[test_restart_datastore])
|
||||
def test_wait_for_restart(self):
|
||||
"""Wait for restart to complete."""
|
||||
self.test_runner.run_test_wait_for_restart()
|
||||
|
||||
@test(runs_after=[test_wait_for_restart])
|
||||
def test_log_publish_user(self):
|
||||
"""Test log-publish on USER log."""
|
||||
self.test_runner.run_test_log_publish_user()
|
||||
|
||||
@test(runs_after=[test_log_publish_user])
|
||||
def test_add_data(self):
|
||||
"""Add data for second log-publish on USER log."""
|
||||
self.test_runner.run_test_add_data()
|
||||
|
||||
@test(runs_after=[test_add_data])
|
||||
def test_verify_data(self):
|
||||
"""Verify data for second log-publish on USER log."""
|
||||
self.test_runner.run_test_verify_data()
|
||||
|
||||
@test(runs_after=[test_verify_data])
|
||||
def test_log_publish_again_user(self):
|
||||
"""Test log-publish again on USER log."""
|
||||
self.test_runner.run_test_log_publish_again_user()
|
||||
|
||||
@test(runs_after=[test_log_publish_again_user])
|
||||
def test_log_generator_user(self):
|
||||
"""Test log-generator on USER log."""
|
||||
self.test_runner.run_test_log_generator_user()
|
||||
|
||||
@test(runs_after=[test_log_generator_user])
|
||||
def test_log_generator_publish_user(self):
|
||||
"""Test log-generator with publish on USER log."""
|
||||
self.test_runner.run_test_log_generator_publish_user()
|
||||
|
||||
@test(runs_after=[test_log_generator_publish_user])
|
||||
def test_log_generator_swift_client_user(self):
|
||||
"""Test log-generator on USER log with passed-in Swift client."""
|
||||
self.test_runner.run_test_log_generator_swift_client_user()
|
||||
|
||||
@test(runs_after=[test_log_generator_swift_client_user])
|
||||
def test_add_data_again(self):
|
||||
"""Add more data for log-generator row-by-row test on USER log."""
|
||||
self.test_runner.run_test_add_data_again()
|
||||
|
||||
@test(runs_after=[test_add_data_again])
|
||||
def test_verify_data_again(self):
|
||||
"""Verify data for log-generator row-by-row test on USER log."""
|
||||
self.test_runner.run_test_verify_data_again()
|
||||
|
||||
@test(runs_after=[test_verify_data_again])
|
||||
def test_log_generator_user_by_row(self):
|
||||
"""Test log-generator on USER log row-by-row."""
|
||||
self.test_runner.run_test_log_generator_user_by_row()
|
||||
|
||||
@test(runs_after=[test_log_generator_user_by_row])
|
||||
def test_log_save_user(self):
|
||||
"""Test log-save on USER log."""
|
||||
self.test_runner.run_test_log_save_user()
|
||||
|
||||
@test(runs_after=[test_log_save_user])
|
||||
def test_log_save_publish_user(self):
|
||||
"""Test log-save on USER log with publish."""
|
||||
self.test_runner.run_test_log_save_publish_user()
|
||||
|
||||
@test(runs_after=[test_log_save_publish_user])
|
||||
def test_log_discard_user(self):
|
||||
"""Test log-discard on USER log."""
|
||||
self.test_runner.run_test_log_discard_user()
|
||||
|
||||
@test(runs_after=[test_log_discard_user])
|
||||
def test_log_disable_user(self):
|
||||
"""Test log-disable on USER log."""
|
||||
self.test_runner.run_test_log_disable_user()
|
||||
|
||||
@test(runs_after=[test_log_disable_user])
|
||||
def test_restart_datastore_again(self):
|
||||
"""Test restart datastore again if required."""
|
||||
self.test_runner.run_test_restart_datastore()
|
||||
|
||||
@test(runs_after=[test_restart_datastore_again])
|
||||
def test_wait_for_restart_again(self):
|
||||
"""Wait for restart to complete again."""
|
||||
self.test_runner.run_test_wait_for_restart()
|
||||
|
||||
@test
|
||||
def test_log_show_sys(self):
|
||||
"""Test that log-show works for SYS log."""
|
||||
self.test_runner.run_test_log_show_sys()
|
||||
|
||||
@test(runs_after=[test_log_show_sys])
|
||||
def test_log_publish_sys(self):
|
||||
"""Test log-publish on SYS log."""
|
||||
self.test_runner.run_test_log_publish_sys()
|
||||
|
||||
@test(runs_after=[test_log_publish_sys])
|
||||
def test_log_publish_again_sys(self):
|
||||
"""Test log-publish again on SYS log."""
|
||||
self.test_runner.run_test_log_publish_again_sys()
|
||||
|
||||
@test(depends_on=[test_log_publish_again_sys])
|
||||
def test_log_generator_sys(self):
|
||||
"""Test log-generator on SYS log."""
|
||||
self.test_runner.run_test_log_generator_sys()
|
||||
|
||||
@test(runs_after=[test_log_generator_sys])
|
||||
def test_log_generator_publish_sys(self):
|
||||
"""Test log-generator with publish on SYS log."""
|
||||
self.test_runner.run_test_log_generator_publish_sys()
|
||||
|
||||
@test(depends_on=[test_log_generator_publish_sys])
|
||||
def test_log_generator_swift_client_sys(self):
|
||||
"""Test log-generator on SYS log with passed-in Swift client."""
|
||||
self.test_runner.run_test_log_generator_swift_client_sys()
|
||||
|
||||
@test(depends_on=[test_log_generator_publish_sys],
|
||||
runs_after=[test_log_generator_swift_client_sys])
|
||||
def test_log_save_sys(self):
|
||||
"""Test log-save on SYS log."""
|
||||
self.test_runner.run_test_log_save_sys()
|
||||
|
||||
@test(runs_after=[test_log_save_sys])
|
||||
def test_log_save_publish_sys(self):
|
||||
"""Test log-save on SYS log with publish."""
|
||||
self.test_runner.run_test_log_save_publish_sys()
|
||||
|
||||
@test(runs_after=[test_log_save_publish_sys])
|
||||
def test_log_discard_sys(self):
|
||||
"""Test log-discard on SYS log."""
|
||||
self.test_runner.run_test_log_discard_sys()
|
||||
@@ -46,3 +46,9 @@ class MysqlHelper(SqlHelper):
|
||||
|
||||
def get_invalid_groups(self):
|
||||
return [{'key_buffer_size': 4}, {"join_buffer_size": 'string_value'}]
|
||||
|
||||
def get_exposed_user_log_names(self):
|
||||
return ['general', 'slow_query']
|
||||
|
||||
def get_unexposed_sys_log_names(self):
|
||||
return ['guest', 'error']
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from proboscis import SkipTest
|
||||
|
||||
from trove.tests.scenario.helpers.sql_helper import SqlHelper
|
||||
|
||||
|
||||
@@ -23,7 +25,7 @@ class PostgresqlHelper(SqlHelper):
|
||||
'postgresql')
|
||||
|
||||
def get_helper_credentials(self):
|
||||
return {'name': 'lite', 'password': 'litepass', 'database': 'firstdb'}
|
||||
return {'name': 'lite', 'password': 'litepass', 'database': 'lite'}
|
||||
|
||||
def get_valid_database_definitions(self):
|
||||
return [{'name': 'db1'}, {'name': 'db2'}, {'name': 'db3'}]
|
||||
@@ -35,6 +37,15 @@ class PostgresqlHelper(SqlHelper):
|
||||
{'name': 'user3', 'password': 'password1',
|
||||
'databases': [{'name': 'db1'}, {'name': 'db2'}]}]
|
||||
|
||||
def add_actual_data(self, *args, **kwargs):
|
||||
raise SkipTest("Adding data to PostgreSQL is broken")
|
||||
|
||||
def verify_actual_data(self, *args, **kwargs):
|
||||
raise SkipTest("Verifying data in PostgreSQL is broken")
|
||||
|
||||
def remove_actual_data(self, *args, **kwargs):
|
||||
raise SkipTest("Removing data from PostgreSQL is broken")
|
||||
|
||||
def get_dynamic_group(self):
|
||||
return {'max_worker_processes': 11}
|
||||
|
||||
@@ -45,3 +56,9 @@ class PostgresqlHelper(SqlHelper):
|
||||
return [{'timezone': 997},
|
||||
{"max_worker_processes": 'string_value'},
|
||||
{"standard_conforming_strings": 'string_value'}]
|
||||
|
||||
def get_exposed_user_log_names(self):
|
||||
return ['general']
|
||||
|
||||
def log_enable_requires_restart(self):
|
||||
return True
|
||||
|
||||
@@ -29,16 +29,20 @@ class DataType(Enum):
|
||||
_fn_data dictionary defined in TestHelper.
|
||||
"""
|
||||
|
||||
# micro amount of data, useful for testing datastore logging, etc.
|
||||
micro = 1
|
||||
# another micro dataset (also for datastore logging)
|
||||
micro2 = 2
|
||||
# very tiny amount of data, useful for testing replication
|
||||
# propagation, etc.
|
||||
tiny = 1
|
||||
tiny = 3
|
||||
# another tiny dataset (also for replication propagation)
|
||||
tiny2 = 2
|
||||
tiny2 = 4
|
||||
# small amount of data (this can be added to each instance
|
||||
# after creation, for example).
|
||||
small = 3
|
||||
small = 5
|
||||
# large data, enough to make creating a backup take 20s or more.
|
||||
large = 4
|
||||
large = 6
|
||||
|
||||
|
||||
class TestHelper(object):
|
||||
@@ -98,14 +102,20 @@ class TestHelper(object):
|
||||
self.DATA_START = 'start'
|
||||
self.DATA_SIZE = 'size'
|
||||
self._fn_data = {
|
||||
DataType.micro.name: {
|
||||
self.DATA_START: 100,
|
||||
self.DATA_SIZE: 10},
|
||||
DataType.micro2.name: {
|
||||
self.DATA_START: 200,
|
||||
self.DATA_SIZE: 10},
|
||||
DataType.tiny.name: {
|
||||
self.DATA_START: 1,
|
||||
self.DATA_START: 1000,
|
||||
self.DATA_SIZE: 100},
|
||||
DataType.tiny2.name: {
|
||||
self.DATA_START: 500,
|
||||
self.DATA_START: 2000,
|
||||
self.DATA_SIZE: 100},
|
||||
DataType.small.name: {
|
||||
self.DATA_START: 1000,
|
||||
self.DATA_START: 10000,
|
||||
self.DATA_SIZE: 1000},
|
||||
DataType.large.name: {
|
||||
self.DATA_START: 100000,
|
||||
@@ -328,3 +338,69 @@ class TestHelper(object):
|
||||
"""Return a list of configuration groups with invalid values.
|
||||
"""
|
||||
return []
|
||||
|
||||
###################
|
||||
# Guest Log related
|
||||
###################
|
||||
def get_exposed_log_list(self):
|
||||
"""Return the list of exposed logs for the datastore. This
|
||||
method shouldn't need to be overridden.
|
||||
"""
|
||||
logs = []
|
||||
try:
|
||||
logs.extend(self.get_exposed_user_log_names())
|
||||
except SkipTest:
|
||||
pass
|
||||
try:
|
||||
logs.extend(self.get_exposed_sys_log_names())
|
||||
except SkipTest:
|
||||
pass
|
||||
|
||||
return logs
|
||||
|
||||
def get_full_log_list(self):
|
||||
"""Return the full list of all logs for the datastore. This
|
||||
method shouldn't need to be overridden.
|
||||
"""
|
||||
logs = self.get_exposed_log_list()
|
||||
try:
|
||||
logs.extend(self.get_unexposed_user_log_names())
|
||||
except SkipTest:
|
||||
pass
|
||||
try:
|
||||
logs.extend(self.get_unexposed_sys_log_names())
|
||||
except SkipTest:
|
||||
pass
|
||||
|
||||
return logs
|
||||
|
||||
# Override these guest log methods if needed
|
||||
def get_exposed_user_log_names(self):
|
||||
"""Return the names of the user logs that are visible to all users.
|
||||
The first log name will be used for tests.
|
||||
"""
|
||||
raise SkipTest("No exposed user log names defined.")
|
||||
|
||||
def get_unexposed_user_log_names(self):
|
||||
"""Return the names of the user logs that not visible to all users.
|
||||
The first log name will be used for tests.
|
||||
"""
|
||||
raise SkipTest("No unexposed user log names defined.")
|
||||
|
||||
def get_exposed_sys_log_names(self):
|
||||
"""Return the names of SYS logs that are visible to all users.
|
||||
The first log name will be used for tests.
|
||||
"""
|
||||
raise SkipTest("No exposed sys log names defined.")
|
||||
|
||||
def get_unexposed_sys_log_names(self):
|
||||
"""Return the names of the sys logs that not visible to all users.
|
||||
The first log name will be used for tests.
|
||||
"""
|
||||
return ['guest']
|
||||
|
||||
def log_enable_requires_restart(self):
|
||||
"""Returns whether enabling or disabling a USER log requires a
|
||||
restart of the datastore.
|
||||
"""
|
||||
return False
|
||||
|
||||
@@ -19,11 +19,8 @@ from troveclient.compat import exceptions
|
||||
|
||||
from trove.common.utils import generate_uuid
|
||||
from trove.common.utils import poll_until
|
||||
from trove.tests.config import CONFIG
|
||||
from trove.tests.scenario.helpers.test_helper import DataType
|
||||
from trove.tests.scenario.runners.test_runners import TestRunner
|
||||
from trove.tests.util import create_dbaas_client
|
||||
from trove.tests.util.users import Requirements
|
||||
|
||||
|
||||
class BackupRunner(TestRunner):
|
||||
@@ -47,7 +44,6 @@ class BackupRunner(TestRunner):
|
||||
self.incremental_backup_info = None
|
||||
self.restore_instance_id = 0
|
||||
self.restore_host = None
|
||||
self.other_client = None
|
||||
|
||||
def run_backup_create_instance_invalid(
|
||||
self, expected_exception=exceptions.BadRequest,
|
||||
@@ -235,21 +231,13 @@ class BackupRunner(TestRunner):
|
||||
def run_backup_get_unauthorized_user(
|
||||
self, expected_exception=exceptions.NotFound,
|
||||
expected_http_code=404):
|
||||
self._create_other_client()
|
||||
self.assert_raises(
|
||||
expected_exception, None,
|
||||
self.other_client.backups.get, self.backup_info.id)
|
||||
self.unauth_client.backups.get, self.backup_info.id)
|
||||
# we're using a different client, so we'll check the return code
|
||||
# on it explicitly, instead of depending on 'assert_raises'
|
||||
self.assert_client_code(expected_http_code=expected_http_code,
|
||||
client=self.other_client)
|
||||
|
||||
def _create_other_client(self):
|
||||
if not self.other_client:
|
||||
requirements = Requirements(is_admin=False)
|
||||
other_user = CONFIG.users.find_user(
|
||||
requirements, black_list=[self.instance_info.user.auth_user])
|
||||
self.other_client = create_dbaas_client(other_user)
|
||||
client=self.unauth_client)
|
||||
|
||||
def run_restore_from_backup(self):
|
||||
self.assert_restore_from_backup(self.backup_info.id)
|
||||
@@ -312,14 +300,13 @@ class BackupRunner(TestRunner):
|
||||
def run_delete_backup_unauthorized_user(
|
||||
self, expected_exception=exceptions.NotFound,
|
||||
expected_http_code=404):
|
||||
self._create_other_client()
|
||||
self.assert_raises(
|
||||
expected_exception, None,
|
||||
self.other_client.backups.delete, self.backup_info.id)
|
||||
self.unauth_client.backups.delete, self.backup_info.id)
|
||||
# we're using a different client, so we'll check the return code
|
||||
# on it explicitly, instead of depending on 'assert_raises'
|
||||
self.assert_client_code(expected_http_code=expected_http_code,
|
||||
client=self.other_client)
|
||||
client=self.unauth_client)
|
||||
|
||||
def run_delete_backup(self, expected_http_code=202):
|
||||
self.assert_delete_backup(self.backup_info.id, expected_http_code)
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
# Copyright 2015 Tesora Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from swiftclient.client import ClientException
|
||||
import tempfile
|
||||
|
||||
from troveclient.compat import exceptions
|
||||
|
||||
from trove.common import cfg
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent import guest_log
|
||||
from trove.tests.config import CONFIG
|
||||
from trove.tests.scenario.helpers.test_helper import DataType
|
||||
from trove.tests.scenario.runners.test_runners import TestRunner
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class GuestLogRunner(TestRunner):
|
||||
|
||||
def __init__(self):
|
||||
super(GuestLogRunner, self).__init__()
|
||||
|
||||
self.container = CONF.guest_log_container_name
|
||||
self.prefix_pattern = '%(instance_id)s/%(datastore)s-%(log)s/'
|
||||
self._last_log_published = {}
|
||||
self._last_log_contents = {}
|
||||
|
||||
def _get_last_log_published(self, log_name):
|
||||
return self._last_log_published.get(log_name, None)
|
||||
|
||||
def _set_last_log_published(self, log_name, published):
|
||||
self._last_log_published[log_name] = published
|
||||
|
||||
def _get_last_log_contents(self, log_name):
|
||||
return self._last_log_contents.get(log_name, [])
|
||||
|
||||
def _set_last_log_contents(self, log_name, published):
|
||||
self._last_log_contents[log_name] = published
|
||||
|
||||
def _get_exposed_user_log_names(self):
|
||||
"""Returns the full list of exposed user logs."""
|
||||
return self.test_helper.get_exposed_user_log_names()
|
||||
|
||||
def _get_exposed_user_log_name(self):
|
||||
"""Return the first exposed user log name."""
|
||||
return self.test_helper.get_exposed_user_log_names()[0]
|
||||
|
||||
def _get_unexposed_sys_log_name(self):
|
||||
"""Return the first unexposed sys log name."""
|
||||
return self.test_helper.get_unexposed_sys_log_names()[0]
|
||||
|
||||
def run_test_log_list(self):
|
||||
self.assert_log_list(self.auth_client,
|
||||
self.test_helper.get_exposed_log_list())
|
||||
|
||||
def assert_log_list(self, client, expected_list):
|
||||
log_list = list(client.instances.log_list(self.instance_info.id))
|
||||
log_names = list(ll.name for ll in log_list)
|
||||
self.assert_list_elements_equal(expected_list, log_names)
|
||||
|
||||
def run_test_admin_log_list(self):
|
||||
self.assert_log_list(self.admin_client,
|
||||
self.test_helper.get_full_log_list())
|
||||
|
||||
def run_test_log_show(self):
|
||||
log_pending = self._set_zero_or_none()
|
||||
self.assert_log_show(self.auth_client,
|
||||
self._get_exposed_user_log_name(),
|
||||
expected_published=0,
|
||||
expected_pending=log_pending)
|
||||
|
||||
def _set_zero_or_none(self):
|
||||
"""This attempts to handle the case where an existing instance
|
||||
is used. Values that would normally be '0' are not, and must
|
||||
be ignored.
|
||||
"""
|
||||
value = 0
|
||||
if self.is_using_existing_instance:
|
||||
value = None
|
||||
return value
|
||||
|
||||
def assert_log_show(self, client, log_name,
|
||||
expected_http_code=200,
|
||||
expected_type=guest_log.LogType.USER.name,
|
||||
expected_status=guest_log.LogStatus.Disabled.name,
|
||||
expected_published=None, expected_pending=None):
|
||||
self.report.log("Executing log_show for log '%s'" % log_name)
|
||||
log_details = client.instances.log_show(
|
||||
self.instance_info.id, log_name)
|
||||
self.assert_client_code(expected_http_code)
|
||||
self.assert_log_details(
|
||||
log_details, log_name,
|
||||
expected_type=expected_type,
|
||||
expected_status=expected_status,
|
||||
expected_published=expected_published,
|
||||
expected_pending=expected_pending)
|
||||
|
||||
def assert_log_details(self, log_details, expected_log_name,
|
||||
expected_type=guest_log.LogType.USER.name,
|
||||
expected_status=guest_log.LogStatus.Disabled.name,
|
||||
expected_published=None, expected_pending=None):
|
||||
"""Check that the action generates the proper response data.
|
||||
For log_published and log_pending, setting the value to 'None'
|
||||
will skip that check (useful when using an existing instance,
|
||||
as there may be pending things in user logs right from the get-go)
|
||||
and setting it to a value other than '0' will verify that the actual
|
||||
value is '>=value' (since it's impossible to know what the actual
|
||||
value will be at any given time). '0' will still match exclusively.
|
||||
"""
|
||||
self.report.log("Validating log details for log '%s'" %
|
||||
expected_log_name)
|
||||
self._set_last_log_published(expected_log_name, log_details.published)
|
||||
self.assert_equal(expected_log_name, log_details.name,
|
||||
"Wrong log name for '%s' log" % expected_log_name)
|
||||
self.assert_equal(expected_type, log_details.type,
|
||||
"Wrong log type for '%s' log" % expected_log_name)
|
||||
current_status = log_details.status.replace(' ', '_')
|
||||
self.assert_equal(expected_status, current_status,
|
||||
"Wrong log status for '%s' log" % expected_log_name)
|
||||
if expected_published is None:
|
||||
pass
|
||||
elif expected_published == 0:
|
||||
self.assert_equal(0, log_details.published,
|
||||
"Wrong log published for '%s' log" %
|
||||
expected_log_name)
|
||||
else:
|
||||
self.assert_true(log_details.published >= expected_published,
|
||||
"Missing log published for '%s' log: "
|
||||
"expected %d, got %d" %
|
||||
(expected_log_name, expected_published,
|
||||
log_details.published))
|
||||
if expected_pending is None:
|
||||
pass
|
||||
elif expected_pending == 0:
|
||||
self.assert_equal(0, log_details.pending,
|
||||
"Wrong log pending for '%s' log" %
|
||||
expected_log_name)
|
||||
else:
|
||||
self.assert_true(log_details.pending >= expected_pending,
|
||||
"Missing log pending for '%s' log: "
|
||||
"expected %d, got %d" %
|
||||
(expected_log_name, expected_pending,
|
||||
log_details.pending))
|
||||
container = self.container
|
||||
prefix = self.prefix_pattern % {
|
||||
'instance_id': self.instance_info.id,
|
||||
'datastore': CONFIG.dbaas_datastore,
|
||||
'log': expected_log_name}
|
||||
metafile = prefix.rstrip('/') + '_metafile'
|
||||
if expected_published == 0:
|
||||
self.assert_storage_gone(container, prefix, metafile)
|
||||
container = 'None'
|
||||
prefix = 'None'
|
||||
else:
|
||||
self.assert_storage_exists(container, prefix, metafile)
|
||||
self.assert_equal(container, log_details.container,
|
||||
"Wrong log container for '%s' log" %
|
||||
expected_log_name)
|
||||
self.assert_equal(prefix, log_details.prefix,
|
||||
"Wrong log prefix for '%s' log" % expected_log_name)
|
||||
self.assert_equal(metafile, log_details.metafile,
|
||||
"Wrong log metafile for '%s' log" %
|
||||
expected_log_name)
|
||||
|
||||
def assert_log_enable(self, client, log_name,
|
||||
expected_http_code=200,
|
||||
expected_type=guest_log.LogType.USER.name,
|
||||
expected_status=guest_log.LogStatus.Disabled.name,
|
||||
expected_published=None, expected_pending=None):
|
||||
self.report.log("Executing log_enable for log '%s'" % log_name)
|
||||
log_details = client.instances.log_enable(
|
||||
self.instance_info.id, log_name)
|
||||
self.assert_client_code(expected_http_code)
|
||||
self.assert_log_details(
|
||||
log_details, log_name,
|
||||
expected_type=expected_type,
|
||||
expected_status=expected_status,
|
||||
expected_published=expected_published,
|
||||
expected_pending=expected_pending)
|
||||
|
||||
def assert_log_disable(self, client, log_name, discard=None,
|
||||
expected_http_code=200,
|
||||
expected_type=guest_log.LogType.USER.name,
|
||||
expected_status=guest_log.LogStatus.Disabled.name,
|
||||
expected_published=None, expected_pending=None):
|
||||
self.report.log("Executing log_disable for log '%s' (discard: %s)" %
|
||||
(log_name, discard))
|
||||
log_details = client.instances.log_disable(
|
||||
self.instance_info.id, log_name, discard=discard)
|
||||
self.assert_client_code(expected_http_code)
|
||||
self.assert_log_details(
|
||||
log_details, log_name,
|
||||
expected_type=expected_type,
|
||||
expected_status=expected_status,
|
||||
expected_published=expected_published,
|
||||
expected_pending=expected_pending)
|
||||
|
||||
def assert_log_publish(self, client, log_name, disable=None, discard=None,
|
||||
expected_http_code=200,
|
||||
expected_type=guest_log.LogType.USER.name,
|
||||
expected_status=guest_log.LogStatus.Disabled.name,
|
||||
expected_published=None, expected_pending=None):
|
||||
self.report.log("Executing log_publish for log '%s' (disable: %s "
|
||||
"discard: %s)" %
|
||||
(log_name, disable, discard))
|
||||
log_details = client.instances.log_publish(
|
||||
self.instance_info.id, log_name, disable=disable, discard=discard)
|
||||
self.assert_client_code(expected_http_code)
|
||||
self.assert_log_details(
|
||||
log_details, log_name,
|
||||
expected_type=expected_type,
|
||||
expected_status=expected_status,
|
||||
expected_published=expected_published,
|
||||
expected_pending=expected_pending)
|
||||
|
||||
def assert_log_discard(self, client, log_name,
|
||||
expected_http_code=200,
|
||||
expected_type=guest_log.LogType.USER.name,
|
||||
expected_status=guest_log.LogStatus.Disabled.name,
|
||||
expected_published=None, expected_pending=None):
|
||||
self.report.log("Executing log_discard for log '%s'" % log_name)
|
||||
log_details = client.instances.log_discard(
|
||||
self.instance_info.id, log_name)
|
||||
self.assert_client_code(expected_http_code)
|
||||
self.assert_log_details(
|
||||
log_details, log_name,
|
||||
expected_type=expected_type,
|
||||
expected_status=expected_status,
|
||||
expected_published=expected_published,
|
||||
expected_pending=expected_pending)
|
||||
|
||||
def assert_storage_gone(self, container, prefix, metafile):
|
||||
try:
|
||||
headers, container_files = self.swift_client.get_container(
|
||||
container, prefix=prefix)
|
||||
self.assert_equal(0, len(container_files),
|
||||
"Found files in %s/%s: %s" %
|
||||
(container, prefix, container_files))
|
||||
except ClientException as ex:
|
||||
if ex.http_status == 404:
|
||||
self.report.log("Container '%s' does not exist" %
|
||||
container)
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
self.swift_client.get_object(container, metafile)
|
||||
self.fail("Found metafile after discard: %s" % metafile)
|
||||
except ClientException as ex:
|
||||
if ex.http_status == 404:
|
||||
self.report.log("Metafile '%s' gone as expected" %
|
||||
metafile)
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def assert_storage_exists(self, container, prefix, metafile):
|
||||
try:
|
||||
headers, container_files = self.swift_client.get_container(
|
||||
container, prefix=prefix)
|
||||
self.assert_true(len(container_files) > 0,
|
||||
"No files found in %s/%s" %
|
||||
(container, prefix))
|
||||
except ClientException as ex:
|
||||
if ex.http_status == 404:
|
||||
self.fail("Container '%s' does not exist" % container)
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
self.swift_client.get_object(container, metafile)
|
||||
except ClientException as ex:
|
||||
if ex.http_status == 404:
|
||||
self.fail("Missing metafile: %s" % metafile)
|
||||
else:
|
||||
raise
|
||||
|
||||
def run_test_log_enable_sys(self,
|
||||
expected_exception=exceptions.BadRequest,
|
||||
expected_http_code=400):
|
||||
self.assert_log_enable_fails(
|
||||
self.admin_client,
|
||||
expected_exception, expected_http_code,
|
||||
self._get_unexposed_sys_log_name())
|
||||
|
||||
def assert_log_enable_fails(self, client,
|
||||
expected_exception, expected_http_code,
|
||||
log_name):
|
||||
self.assert_raises(expected_exception, None,
|
||||
client.instances.log_enable,
|
||||
self.instance_info.id, log_name)
|
||||
# we may not be using the main client, so check explicitly here
|
||||
self.assert_client_code(expected_http_code, client)
|
||||
|
||||
def run_test_log_disable_sys(self,
|
||||
expected_exception=exceptions.BadRequest,
|
||||
expected_http_code=400):
|
||||
self.assert_log_disable_fails(
|
||||
self.admin_client,
|
||||
expected_exception, expected_http_code,
|
||||
self._get_unexposed_sys_log_name())
|
||||
|
||||
def assert_log_disable_fails(self, client,
|
||||
expected_exception, expected_http_code,
|
||||
log_name, discard=None):
|
||||
self.assert_raises(expected_exception, None,
|
||||
client.instances.log_disable,
|
||||
self.instance_info.id, log_name,
|
||||
discard=discard)
|
||||
# we may not be using the main client, so check explicitly here
|
||||
self.assert_client_code(expected_http_code, client)
|
||||
|
||||
def run_test_log_show_unauth_user(self,
|
||||
expected_exception=exceptions.NotFound,
|
||||
expected_http_code=404):
|
||||
self.assert_log_show_fails(
|
||||
self.unauth_client,
|
||||
expected_exception, expected_http_code,
|
||||
self._get_exposed_user_log_name())
|
||||
|
||||
def assert_log_show_fails(self, client,
|
||||
expected_exception, expected_http_code,
|
||||
log_name):
|
||||
self.assert_raises(expected_exception, None,
|
||||
client.instances.log_show,
|
||||
self.instance_info.id, log_name)
|
||||
# we may not be using the main client, so check explicitly here
|
||||
self.assert_client_code(expected_http_code, client)
|
||||
|
||||
def run_test_log_list_unauth_user(self,
|
||||
expected_exception=exceptions.NotFound,
|
||||
expected_http_code=404):
|
||||
self.assert_raises(expected_exception, None,
|
||||
self.unauth_client.instances.log_list,
|
||||
self.instance_info.id)
|
||||
# we're not using the main client, so check explicitly here
|
||||
self.assert_client_code(expected_http_code, self.unauth_client)
|
||||
|
||||
def run_test_log_generator_unauth_user(self):
|
||||
self.assert_log_generator_unauth_user(
|
||||
self.unauth_client, self._get_exposed_user_log_name())
|
||||
|
||||
def assert_log_generator_unauth_user(self, client, log_name, publish=None):
|
||||
try:
|
||||
client.instances.log_generator(
|
||||
self.instance_info.id, log_name, publish=publish)
|
||||
raise("Client allowed unauthorized access to log_generator")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def run_test_log_generator_publish_unauth_user(self):
|
||||
self.assert_log_generator_unauth_user(
|
||||
self.unauth_client, self._get_exposed_user_log_name(),
|
||||
publish=True)
|
||||
|
||||
def run_test_log_show_unexposed_user(
|
||||
self, expected_exception=exceptions.BadRequest,
|
||||
expected_http_code=400):
|
||||
self.assert_log_show_fails(
|
||||
self.auth_client,
|
||||
expected_exception, expected_http_code,
|
||||
self._get_unexposed_sys_log_name())
|
||||
|
||||
def run_test_log_enable_unexposed_user(
|
||||
self, expected_exception=exceptions.BadRequest,
|
||||
expected_http_code=400):
|
||||
self.assert_log_enable_fails(
|
||||
self.auth_client,
|
||||
expected_exception, expected_http_code,
|
||||
self._get_unexposed_sys_log_name())
|
||||
|
||||
def run_test_log_disable_unexposed_user(
|
||||
self, expected_exception=exceptions.BadRequest,
|
||||
expected_http_code=400):
|
||||
self.assert_log_disable_fails(
|
||||
self.auth_client,
|
||||
expected_exception, expected_http_code,
|
||||
self._get_unexposed_sys_log_name())
|
||||
|
||||
def run_test_log_publish_unexposed_user(
|
||||
self, expected_exception=exceptions.BadRequest,
|
||||
expected_http_code=400):
|
||||
self.assert_log_publish_fails(
|
||||
self.auth_client,
|
||||
expected_exception, expected_http_code,
|
||||
self._get_unexposed_sys_log_name())
|
||||
|
||||
def assert_log_publish_fails(self, client,
|
||||
expected_exception, expected_http_code,
|
||||
log_name,
|
||||
disable=None, discard=None):
|
||||
self.assert_raises(expected_exception, None,
|
||||
client.instances.log_publish,
|
||||
self.instance_info.id, log_name,
|
||||
disable=disable, discard=discard)
|
||||
# we may not be using the main client, so check explicitly here
|
||||
self.assert_client_code(expected_http_code, client)
|
||||
|
||||
def run_test_log_discard_unexposed_user(
|
||||
self, expected_exception=exceptions.BadRequest,
|
||||
expected_http_code=400):
|
||||
self.assert_log_discard_fails(
|
||||
self.auth_client,
|
||||
expected_exception, expected_http_code,
|
||||
self._get_unexposed_sys_log_name())
|
||||
|
||||
def assert_log_discard_fails(self, client,
|
||||
expected_exception, expected_http_code,
|
||||
log_name):
|
||||
self.assert_raises(expected_exception, None,
|
||||
client.instances.log_discard,
|
||||
self.instance_info.id, log_name)
|
||||
# we may not be using the main client, so check explicitly here
|
||||
self.assert_client_code(expected_http_code, client)
|
||||
|
||||
def run_test_log_enable_user(self):
|
||||
expected_status = guest_log.LogStatus.Ready.name
|
||||
expected_pending = 1
|
||||
if self.test_helper.log_enable_requires_restart():
|
||||
expected_status = guest_log.LogStatus.Restart_Required.name
|
||||
# if using an existing instance, there may already be something
|
||||
expected_pending = self._set_zero_or_none()
|
||||
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_enable(
|
||||
self.auth_client,
|
||||
log_name,
|
||||
expected_status=expected_status,
|
||||
expected_published=0, expected_pending=expected_pending)
|
||||
|
||||
def run_test_log_enable_flip_user(self):
|
||||
# for restart required datastores, test that flipping them
|
||||
# back to disabled returns the status to 'Disabled'
|
||||
# from 'Restart_Required'
|
||||
if self.test_helper.log_enable_requires_restart():
|
||||
# if using an existing instance, there may already be something
|
||||
expected_pending = self._set_zero_or_none()
|
||||
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_disable(
|
||||
self.auth_client,
|
||||
log_name,
|
||||
expected_status=guest_log.LogStatus.Disabled.name,
|
||||
expected_published=0, expected_pending=expected_pending)
|
||||
self.assert_log_enable(
|
||||
self.auth_client,
|
||||
log_name,
|
||||
expected_status=guest_log.LogStatus.Restart_Required.name,
|
||||
expected_published=0, expected_pending=expected_pending)
|
||||
|
||||
def run_test_restart_datastore(self, expected_http_code=202):
|
||||
if self.test_helper.log_enable_requires_restart():
|
||||
instance_id = self.instance_info.id
|
||||
# we need to wait until the heartbeat flips the instance
|
||||
# back into 'ACTIVE' before we issue the restart command
|
||||
expected_states = ['RESTART_REQUIRED', 'ACTIVE']
|
||||
self.assert_instance_action(instance_id, expected_states, None)
|
||||
self.auth_client.instances.restart(instance_id)
|
||||
self.assert_client_code(expected_http_code)
|
||||
|
||||
def run_test_wait_for_restart(self, expected_states=['REBOOT', 'ACTIVE']):
|
||||
if self.test_helper.log_enable_requires_restart():
|
||||
self.assert_instance_action(self.instance_info.id,
|
||||
expected_states, None)
|
||||
|
||||
def run_test_log_publish_user(self):
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_publish(
|
||||
self.auth_client,
|
||||
log_name,
|
||||
expected_status=guest_log.LogStatus.Published.name,
|
||||
expected_published=1, expected_pending=0)
|
||||
|
||||
def run_test_add_data(self):
|
||||
self.test_helper.add_data(DataType.micro, self.get_instance_host())
|
||||
|
||||
def run_test_verify_data(self):
|
||||
self.test_helper.verify_data(DataType.micro, self.get_instance_host())
|
||||
|
||||
def run_test_log_publish_again_user(self):
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_publish(
|
||||
self.admin_client,
|
||||
log_name,
|
||||
expected_status=guest_log.LogStatus.Published.name,
|
||||
expected_published=self._get_last_log_published(log_name),
|
||||
expected_pending=0)
|
||||
|
||||
def run_test_log_generator_user(self):
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_generator(
|
||||
self.auth_client,
|
||||
log_name,
|
||||
lines=2, expected_lines=2)
|
||||
|
||||
def assert_log_generator(self, client, log_name, publish=False,
|
||||
lines=4, expected_lines=None,
|
||||
swift_client=None):
|
||||
self.report.log("Executing log_generator for log '%s' (publish: %s)" %
|
||||
(log_name, publish))
|
||||
log_gen = client.instances.log_generator(
|
||||
self.instance_info.id, log_name,
|
||||
publish=publish, lines=lines, swift=swift_client)
|
||||
log_contents = "".join([chunk for chunk in log_gen()])
|
||||
self.report.log("Returned %d lines for log '%s': %s" % (
|
||||
len(log_contents.splitlines()), log_name, log_contents))
|
||||
self._set_last_log_contents(log_name, log_contents)
|
||||
if expected_lines:
|
||||
self.assert_equal(expected_lines,
|
||||
len(log_contents.splitlines()),
|
||||
"Wrong line count for '%s' log" % log_name)
|
||||
else:
|
||||
self.assert_true(len(log_contents.splitlines()) <= lines,
|
||||
"More than %d lines found for '%s' log" %
|
||||
(lines, log_name))
|
||||
|
||||
def run_test_log_generator_publish_user(self):
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_generator(
|
||||
self.auth_client,
|
||||
log_name, publish=True,
|
||||
lines=3, expected_lines=3)
|
||||
|
||||
def run_test_log_generator_swift_client_user(self):
|
||||
swift_client = self.swift_client
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_generator(
|
||||
self.auth_client,
|
||||
log_name, publish=True,
|
||||
lines=3, expected_lines=3,
|
||||
swift_client=swift_client)
|
||||
|
||||
def run_test_add_data_again(self):
|
||||
# Add some more data so we have at least 3 log data files
|
||||
self.test_helper.add_data(DataType.micro2, self.get_instance_host())
|
||||
|
||||
def run_test_verify_data_again(self):
|
||||
self.test_helper.verify_data(DataType.micro2, self.get_instance_host())
|
||||
|
||||
def run_test_log_generator_user_by_row(self):
|
||||
log_name = self._get_exposed_user_log_name()
|
||||
self.assert_log_publish(
|
||||
self.auth_client,
|
||||
log_name,
|
||||
expected_status=guest_log.LogStatus.Published.name,
|
||||
expected_published=self._get_last_log_published(log_name),
|
||||
expected_pending=0)
|
||||
# Now get the full contents of the log
|
||||
self.assert_log_generator(self.auth_client, log_name, lines=100000)
|
||||
log_lines = len(self._get_last_log_contents(log_name).splitlines())
|
||||
# Make sure we get the right number of log lines back each time
|
||||
for lines in range(1, log_lines):
|
||||
self.assert_log_generator(
|
||||
self.auth_client,
|
||||
log_name, lines=lines, expected_lines=lines)
|
||||
|
||||
def run_test_log_save_user(self):
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_test_log_save(self.auth_client, log_name)
|
||||
|
||||
def run_test_log_save_publish_user(self):
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_test_log_save(self.auth_client, log_name, publish=True)
|
||||
|
||||
def assert_test_log_save(self, client, log_name, publish=False):
|
||||
# generate the file
|
||||
self.report.log("Executing log_save for log '%s' (publish: %s)" %
|
||||
(log_name, publish))
|
||||
with tempfile.NamedTemporaryFile() as temp_file:
|
||||
client.instances.log_save(self.instance_info.id,
|
||||
log_name=log_name, publish=publish,
|
||||
filename=temp_file.name)
|
||||
file_contents = operating_system.read_file(temp_file.name)
|
||||
# now grab the contents ourselves
|
||||
self.assert_log_generator(client, log_name, lines=100000)
|
||||
# and compare them
|
||||
self.assert_equal(self._get_last_log_contents(log_name),
|
||||
file_contents)
|
||||
|
||||
def run_test_log_discard_user(self):
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_discard(
|
||||
self.auth_client,
|
||||
log_name,
|
||||
expected_status=guest_log.LogStatus.Ready.name,
|
||||
expected_published=0, expected_pending=1)
|
||||
|
||||
def run_test_log_disable_user(self):
|
||||
expected_status = guest_log.LogStatus.Disabled.name
|
||||
if self.test_helper.log_enable_requires_restart():
|
||||
expected_status = guest_log.LogStatus.Restart_Required.name
|
||||
for log_name in self._get_exposed_user_log_names():
|
||||
self.assert_log_disable(
|
||||
self.auth_client,
|
||||
log_name,
|
||||
expected_status=expected_status,
|
||||
expected_published=0, expected_pending=1)
|
||||
|
||||
def run_test_log_show_sys(self):
|
||||
self.assert_log_show(
|
||||
self.admin_client,
|
||||
self._get_unexposed_sys_log_name(),
|
||||
expected_type=guest_log.LogType.SYS.name,
|
||||
expected_status=guest_log.LogStatus.Ready.name,
|
||||
expected_published=0, expected_pending=1)
|
||||
|
||||
def run_test_log_publish_sys(self):
|
||||
log_name = self._get_unexposed_sys_log_name()
|
||||
self.assert_log_publish(
|
||||
self.admin_client,
|
||||
log_name,
|
||||
expected_type=guest_log.LogType.SYS.name,
|
||||
expected_status=guest_log.LogStatus.Partial.name,
|
||||
expected_published=1, expected_pending=1)
|
||||
|
||||
def run_test_log_publish_again_sys(self):
|
||||
log_name = self._get_unexposed_sys_log_name()
|
||||
self.assert_log_publish(
|
||||
self.admin_client,
|
||||
log_name,
|
||||
expected_type=guest_log.LogType.SYS.name,
|
||||
expected_status=guest_log.LogStatus.Partial.name,
|
||||
expected_published=self._get_last_log_published(log_name) + 1,
|
||||
expected_pending=1)
|
||||
|
||||
def run_test_log_generator_sys(self):
|
||||
self.assert_log_generator(
|
||||
self.admin_client,
|
||||
self._get_unexposed_sys_log_name(),
|
||||
lines=4, expected_lines=4)
|
||||
|
||||
def run_test_log_generator_publish_sys(self):
|
||||
self.assert_log_generator(
|
||||
self.admin_client,
|
||||
self._get_unexposed_sys_log_name(), publish=True,
|
||||
lines=4, expected_lines=4)
|
||||
|
||||
def run_test_log_generator_swift_client_sys(self):
|
||||
self.assert_log_generator(
|
||||
self.admin_client,
|
||||
self._get_unexposed_sys_log_name(), publish=True,
|
||||
lines=4, expected_lines=4,
|
||||
swift_client=self.swift_client)
|
||||
|
||||
def run_test_log_save_sys(self):
|
||||
self.assert_test_log_save(
|
||||
self.admin_client,
|
||||
self._get_unexposed_sys_log_name())
|
||||
|
||||
def run_test_log_save_publish_sys(self):
|
||||
self.assert_test_log_save(
|
||||
self.admin_client,
|
||||
self._get_unexposed_sys_log_name(),
|
||||
publish=True)
|
||||
|
||||
def run_test_log_discard_sys(self):
|
||||
self.assert_log_discard(
|
||||
self.admin_client,
|
||||
self._get_unexposed_sys_log_name(),
|
||||
expected_type=guest_log.LogType.SYS.name,
|
||||
expected_status=guest_log.LogStatus.Ready.name,
|
||||
expected_published=0, expected_pending=1)
|
||||
@@ -36,8 +36,6 @@ class InstanceCreateRunner(TestRunner):
|
||||
|
||||
def run_empty_instance_create(
|
||||
self, expected_states=['BUILD', 'ACTIVE'], expected_http_code=200):
|
||||
# TODO(pmalik): Instance create should return 202 Accepted (cast)
|
||||
# rather than 200 OK (call).
|
||||
name = self.instance_info.name
|
||||
flavor = self._get_instance_flavor()
|
||||
trove_volume_size = CONFIG.get('trove_volume_size', 1)
|
||||
@@ -79,9 +77,7 @@ class InstanceCreateRunner(TestRunner):
|
||||
self, with_dbs=True, with_users=True, configuration_id=None,
|
||||
expected_states=['BUILD', 'ACTIVE'], expected_http_code=200,
|
||||
create_helper_user=True):
|
||||
# TODO(pmalik): Instance create should return 202 Accepted (cast)
|
||||
# rather than 200 OK (call).
|
||||
name = self.instance_info.name
|
||||
name = self.instance_info.name + '_init'
|
||||
flavor = self._get_instance_flavor()
|
||||
trove_volume_size = CONFIG.get('trove_volume_size', 1)
|
||||
self.init_inst_dbs = (self.test_helper.get_valid_database_definitions()
|
||||
@@ -91,6 +87,9 @@ class InstanceCreateRunner(TestRunner):
|
||||
if configuration_id:
|
||||
self.init_config_group_id = configuration_id
|
||||
|
||||
if self.is_using_existing_instance:
|
||||
raise SkipTest("Using existing instance.")
|
||||
|
||||
if (self.init_inst_dbs or self.init_inst_users or
|
||||
self.init_config_group_id):
|
||||
info = self.assert_instance_create(
|
||||
@@ -231,6 +230,8 @@ class InstanceCreateRunner(TestRunner):
|
||||
instances = [self.instance_info.id]
|
||||
if self.init_inst_id:
|
||||
instances.append(self.init_inst_id)
|
||||
if self.is_using_existing_instance:
|
||||
expected_states = ['ACTIVE']
|
||||
self.assert_all_instance_states(instances, expected_states)
|
||||
|
||||
def run_add_initialized_instance_data(self):
|
||||
|
||||
@@ -16,14 +16,15 @@
|
||||
import os
|
||||
import time as timer
|
||||
|
||||
from oslo_config.cfg import NoSuchOptError
|
||||
from proboscis import asserts
|
||||
import swiftclient
|
||||
from troveclient.compat import exceptions
|
||||
|
||||
from oslo_config.cfg import NoSuchOptError
|
||||
from trove.common import cfg
|
||||
from trove.common import exception
|
||||
from trove.common import utils
|
||||
from trove.common.utils import poll_until, build_polling_task
|
||||
from trove.common import exception
|
||||
from trove.tests.api.instances import instance_info
|
||||
from trove.tests.config import CONFIG
|
||||
from trove.tests.util import create_dbaas_client
|
||||
@@ -78,7 +79,9 @@ class TestRunner(object):
|
||||
instance_info.volume = None
|
||||
|
||||
self.auth_client = create_dbaas_client(self.instance_info.user)
|
||||
self.unauth_client = None
|
||||
self._unauth_client = None
|
||||
self._admin_client = None
|
||||
self._swift_client = None
|
||||
self._test_helper = None
|
||||
|
||||
@classmethod
|
||||
@@ -148,12 +151,13 @@ class TestRunner(object):
|
||||
def test_helper(self, test_helper):
|
||||
self._test_helper = test_helper
|
||||
|
||||
def get_unauth_client(self):
|
||||
if not self.unauth_client:
|
||||
self.unauth_client = self._create_unauthorized_client()
|
||||
return self.unauth_client
|
||||
@property
|
||||
def unauth_client(self):
|
||||
if not self._unauth_client:
|
||||
self._unauth_client = self._create_unauthorized_client()
|
||||
return self._unauth_client
|
||||
|
||||
def _create_unauthorized_client(self, force=False):
|
||||
def _create_unauthorized_client(self):
|
||||
"""Create a client from a different 'unauthorized' user
|
||||
to facilitate negative testing.
|
||||
"""
|
||||
@@ -162,6 +166,37 @@ class TestRunner(object):
|
||||
requirements, black_list=[self.instance_info.user.auth_user])
|
||||
return create_dbaas_client(other_user)
|
||||
|
||||
@property
|
||||
def admin_client(self):
|
||||
if not self._admin_client:
|
||||
self._admin_client = self._create_admin_client()
|
||||
return self._admin_client
|
||||
|
||||
def _create_admin_client(self):
|
||||
"""Create a client from an admin user."""
|
||||
requirements = Requirements(is_admin=True, services=["swift"])
|
||||
admin_user = CONFIG.users.find_user(requirements)
|
||||
return create_dbaas_client(admin_user)
|
||||
|
||||
@property
|
||||
def swift_client(self):
|
||||
if not self._swift_client:
|
||||
self._swift_client = self._create_swift_client()
|
||||
return self._swift_client
|
||||
|
||||
def _create_swift_client(self):
|
||||
"""Create a swift client from the admin user details."""
|
||||
requirements = Requirements(is_admin=True, services=["swift"])
|
||||
user = CONFIG.users.find_user(requirements)
|
||||
os_options = {'region_name': os.getenv("OS_REGION_NAME")}
|
||||
return swiftclient.client.Connection(
|
||||
authurl=CONFIG.nova_client['auth_url'],
|
||||
user=user.auth_user,
|
||||
key=user.auth_key,
|
||||
tenant_name=user.tenant,
|
||||
auth_version='2.0',
|
||||
os_options=os_options)
|
||||
|
||||
def assert_raises(self, expected_exception, expected_http_code,
|
||||
client_cmd, *cmd_args, **cmd_kwargs):
|
||||
asserts.assert_raises(expected_exception, client_cmd,
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
# Copyright 2015 Tesora Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import getpass
|
||||
import os
|
||||
|
||||
from mock import DEFAULT
|
||||
from mock import MagicMock
|
||||
from mock import patch
|
||||
from proboscis.asserts import assert_equal
|
||||
from proboscis.asserts import assert_true
|
||||
|
||||
from trove.common.context import TroveContext
|
||||
from trove.common import exception
|
||||
from trove.guestagent.common import operating_system
|
||||
from trove.guestagent.datastore import manager
|
||||
from trove.guestagent import guest_log
|
||||
from trove.tests.unittests import trove_testtools
|
||||
|
||||
|
||||
class MockManager(manager.Manager):
|
||||
def __init__(self):
|
||||
super(MockManager, self).__init__('mysql')
|
||||
self._app = MagicMock()
|
||||
self._status = MagicMock()
|
||||
self._configuration_manager = MagicMock()
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
return self._app
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@property
|
||||
def configuration_manager(self):
|
||||
return self._configuration_manager
|
||||
|
||||
|
||||
class ManagerTest(trove_testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(ManagerTest, self).setUp()
|
||||
|
||||
self.chmod_patch = patch.object(operating_system, 'chmod')
|
||||
self.chmod_mock = self.chmod_patch.start()
|
||||
self.addCleanup(self.chmod_patch.stop)
|
||||
|
||||
self.manager = MockManager()
|
||||
self.context = TroveContext()
|
||||
|
||||
self.log_name_sys = 'guest'
|
||||
self.log_name_user = 'general'
|
||||
self.prefix = 'log_prefix'
|
||||
self.container = 'log_container'
|
||||
self.size = 1024
|
||||
self.published = 128
|
||||
self.guest_log_user = guest_log.GuestLog(
|
||||
self.context, self.log_name_user, guest_log.LogType.USER, None,
|
||||
'/tmp/gen.log', True)
|
||||
self.guest_log_sys = guest_log.GuestLog(
|
||||
self.context, self.log_name_sys, guest_log.LogType.SYS, None,
|
||||
'/tmp/guest.log', True)
|
||||
for gl in [self.guest_log_user, self.guest_log_sys]:
|
||||
gl._container_name = self.container
|
||||
gl._refresh_details = MagicMock()
|
||||
gl._log_rotated = MagicMock(return_value=False)
|
||||
gl._publish_to_container = MagicMock()
|
||||
gl._delete_log_components = MagicMock()
|
||||
gl._object_prefix = MagicMock(return_value=self.prefix)
|
||||
gl._size = self.size
|
||||
gl._published_size = self.published
|
||||
self.manager._guest_log_cache = {
|
||||
self.log_name_user: self.guest_log_user,
|
||||
self.log_name_sys: self.guest_log_sys}
|
||||
self.expected_details_user = {
|
||||
'status': 'Disabled',
|
||||
'prefix': self.prefix,
|
||||
'container': self.container,
|
||||
'name': self.log_name_user,
|
||||
'published': self.published,
|
||||
'metafile': self.prefix + '_metafile',
|
||||
'type': 'USER',
|
||||
'pending': self.size - self.published}
|
||||
self.expected_details_sys = dict(self.expected_details_user)
|
||||
self.expected_details_sys['type'] = 'SYS'
|
||||
self.expected_details_sys['status'] = 'Enabled'
|
||||
self.expected_details_sys['name'] = self.log_name_sys
|
||||
|
||||
def tearDown(self):
|
||||
super(ManagerTest, self).tearDown()
|
||||
|
||||
def test_update_status(self):
|
||||
self.manager.update_status(self.context)
|
||||
self.manager.status.update.assert_any_call()
|
||||
|
||||
def test_guest_log_list(self):
|
||||
log_list = self.manager.guest_log_list(self.context)
|
||||
expected = [self.expected_details_sys, self.expected_details_user]
|
||||
assert_equal(self._flatten_list_of_dicts(expected),
|
||||
self._flatten_list_of_dicts(log_list),
|
||||
"Wrong list: %s (Expected: %s)" % (
|
||||
self._flatten_list_of_dicts(log_list),
|
||||
self._flatten_list_of_dicts(expected)))
|
||||
|
||||
def _flatten_list_of_dicts(self, lod):
|
||||
value = sorted("".join("%s%s" % (k, d[k]) for k in sorted(d.keys()))
|
||||
for d in lod)
|
||||
return "".join(sorted(value))
|
||||
|
||||
def test_guest_log_action_enable_disable(self):
|
||||
self.assertRaisesRegexp(exception.BadRequest,
|
||||
"Cannot enable and disable",
|
||||
self.manager.guest_log_action,
|
||||
self.context,
|
||||
self.log_name_sys,
|
||||
True, True, False, False)
|
||||
|
||||
def test_guest_log_action_enable_sys(self):
|
||||
self.assertRaisesRegexp(exception.BadRequest,
|
||||
"Cannot enable a SYSTEM log",
|
||||
self.manager.guest_log_action,
|
||||
self.context,
|
||||
self.log_name_sys,
|
||||
True, False, False, False)
|
||||
|
||||
def test_guest_log_action_disable_sys(self):
|
||||
self.assertRaisesRegexp(exception.BadRequest,
|
||||
"Cannot disable a SYSTEM log",
|
||||
self.manager.guest_log_action,
|
||||
self.context,
|
||||
self.log_name_sys,
|
||||
False, True, False, False)
|
||||
|
||||
def test_guest_log_action_publish_sys(self):
|
||||
with patch.object(os.path, 'isfile', return_value=True):
|
||||
log_details = self.manager.guest_log_action(self.context,
|
||||
self.log_name_sys,
|
||||
False, False,
|
||||
True, False)
|
||||
assert_equal(log_details, self.expected_details_sys,
|
||||
"Wrong details: %s (expected %s)" %
|
||||
(log_details, self.expected_details_sys))
|
||||
assert_equal(
|
||||
1, self.guest_log_sys._publish_to_container.call_count)
|
||||
|
||||
def test_guest_log_action_discard_sys(self):
|
||||
log_details = self.manager.guest_log_action(self.context,
|
||||
self.log_name_sys,
|
||||
False, False,
|
||||
False, True)
|
||||
assert_equal(log_details, self.expected_details_sys,
|
||||
"Wrong details: %s (expected %s)" %
|
||||
(log_details, self.expected_details_sys))
|
||||
assert_equal(
|
||||
1, self.guest_log_sys._delete_log_components.call_count)
|
||||
|
||||
def test_guest_log_action_enable_user(self):
|
||||
with patch.object(manager.Manager, 'guest_log_enable',
|
||||
return_value=False) as mock_enable:
|
||||
log_details = self.manager.guest_log_action(self.context,
|
||||
self.log_name_user,
|
||||
True, False,
|
||||
False, False)
|
||||
assert_equal(log_details, self.expected_details_user,
|
||||
"Wrong details: %s (expected %s)" %
|
||||
(log_details, self.expected_details_user))
|
||||
assert_equal(1, mock_enable.call_count)
|
||||
|
||||
def test_guest_log_action_disable_user(self):
|
||||
with patch.object(manager.Manager, 'guest_log_enable',
|
||||
return_value=False) as mock_enable:
|
||||
self.guest_log_user._enabled = True
|
||||
log_details = self.manager.guest_log_action(self.context,
|
||||
self.log_name_user,
|
||||
False, True,
|
||||
False, False)
|
||||
assert_equal(log_details, self.expected_details_user,
|
||||
"Wrong details: %s (expected %s)" %
|
||||
(log_details, self.expected_details_user))
|
||||
assert_equal(1, mock_enable.call_count)
|
||||
|
||||
def test_guest_log_action_publish_user(self):
|
||||
with patch.object(manager.Manager, 'guest_log_enable',
|
||||
return_value=False) as mock_enable:
|
||||
with patch.object(os.path, 'isfile', return_value=True):
|
||||
log_details = self.manager.guest_log_action(self.context,
|
||||
self.log_name_user,
|
||||
False, False,
|
||||
True, False)
|
||||
assert_equal(log_details, self.expected_details_user,
|
||||
"Wrong details: %s (expected %s)" %
|
||||
(log_details, self.expected_details_user))
|
||||
assert_equal(1, mock_enable.call_count)
|
||||
|
||||
def test_guest_log_action_discard_user(self):
|
||||
log_details = self.manager.guest_log_action(self.context,
|
||||
self.log_name_user,
|
||||
False, False,
|
||||
False, True)
|
||||
assert_equal(log_details, self.expected_details_user,
|
||||
"Wrong details: %s (expected %s)" %
|
||||
(log_details, self.expected_details_user))
|
||||
assert_equal(1, self.guest_log_user._delete_log_components.call_count)
|
||||
|
||||
def test_set_guest_log_status_disabled(self):
|
||||
data = [
|
||||
{'orig': guest_log.LogStatus.Enabled,
|
||||
'new': guest_log.LogStatus.Disabled,
|
||||
'expect': guest_log.LogStatus.Disabled},
|
||||
{'orig': guest_log.LogStatus.Restart_Required,
|
||||
'new': guest_log.LogStatus.Enabled,
|
||||
'expect': guest_log.LogStatus.Restart_Required},
|
||||
{'orig': guest_log.LogStatus.Restart_Required,
|
||||
'new': guest_log.LogStatus.Restart_Completed,
|
||||
'expect': guest_log.LogStatus.Restart_Completed},
|
||||
{'orig': guest_log.LogStatus.Published,
|
||||
'new': guest_log.LogStatus.Partial,
|
||||
'expect': guest_log.LogStatus.Partial},
|
||||
]
|
||||
for datum in data:
|
||||
self.assert_guest_log_status(datum['orig'],
|
||||
datum['new'],
|
||||
datum['expect'])
|
||||
|
||||
def assert_guest_log_status(self, original_status, new_status,
|
||||
expected_final_status):
|
||||
gl_cache = self.manager.guest_log_cache
|
||||
gl_cache[self.log_name_sys]._status = original_status
|
||||
self.manager.set_guest_log_status(new_status, self.log_name_sys)
|
||||
assert_equal(gl_cache[self.log_name_sys].status, expected_final_status,
|
||||
"Unexpected status for '%s': %s' (Expected %s)" %
|
||||
(self.log_name_sys, gl_cache[self.log_name_sys].status,
|
||||
expected_final_status))
|
||||
|
||||
def test_build_log_file_name(self):
|
||||
current_owner = getpass.getuser()
|
||||
with patch.multiple(operating_system,
|
||||
exists=MagicMock(return_value=False),
|
||||
write_file=DEFAULT,
|
||||
create_directory=DEFAULT,
|
||||
chown=DEFAULT,
|
||||
chmod=DEFAULT) as os_mocks:
|
||||
log_file = self.manager.build_log_file_name(self.log_name_sys,
|
||||
current_owner)
|
||||
expected_filename = '%s/%s/%s-%s.log' % (
|
||||
self.manager.GUEST_LOG_BASE_DIR,
|
||||
self.manager.GUEST_LOG_DATASTORE_DIRNAME,
|
||||
self.manager.manager, self.log_name_sys)
|
||||
expected_call_counts = {'exists': 1,
|
||||
'write_file': 1,
|
||||
'create_directory': 2,
|
||||
'chown': 1,
|
||||
'chmod': 1}
|
||||
self.assert_build_log_file_name(expected_filename, log_file,
|
||||
os_mocks, expected_call_counts)
|
||||
|
||||
def assert_build_log_file_name(self, expected_filename, filename,
|
||||
mocks, call_counts):
|
||||
assert_equal(expected_filename, filename,
|
||||
"Unexpected filename: %s (expected %s)" %
|
||||
(filename, expected_filename))
|
||||
for key in mocks.keys():
|
||||
assert_true(
|
||||
mocks[key].call_count == call_counts[key],
|
||||
"%s called %d time(s)" % (key, mocks[key].call_count))
|
||||
|
||||
def test_build_log_file_name_with_dir(self):
|
||||
current_owner = getpass.getuser()
|
||||
log_dir = '/tmp'
|
||||
with patch.multiple(operating_system,
|
||||
exists=MagicMock(return_value=False),
|
||||
write_file=DEFAULT,
|
||||
create_directory=DEFAULT,
|
||||
chown=DEFAULT,
|
||||
chmod=DEFAULT) as os_mocks:
|
||||
log_file = self.manager.build_log_file_name(self.log_name_sys,
|
||||
current_owner,
|
||||
datastore_dir=log_dir)
|
||||
expected_filename = '%s/%s-%s.log' % (
|
||||
log_dir,
|
||||
self.manager.manager, self.log_name_sys)
|
||||
expected_call_counts = {'exists': 1,
|
||||
'write_file': 1,
|
||||
'create_directory': 1,
|
||||
'chown': 1,
|
||||
'chmod': 1}
|
||||
self.assert_build_log_file_name(expected_filename, log_file,
|
||||
os_mocks, expected_call_counts)
|
||||
|
||||
def test_validate_log_file(self):
|
||||
file_name = '/tmp/non-existent-file'
|
||||
current_owner = getpass.getuser()
|
||||
with patch.multiple(operating_system,
|
||||
exists=MagicMock(return_value=False),
|
||||
write_file=DEFAULT,
|
||||
chown=DEFAULT,
|
||||
chmod=DEFAULT) as os_mocks:
|
||||
log_file = self.manager.validate_log_file(file_name, current_owner)
|
||||
assert_equal(file_name, log_file, "Unexpected filename")
|
||||
for key in os_mocks.keys():
|
||||
assert_true(os_mocks[key].call_count == 1,
|
||||
"%s not called" % key)
|
||||
@@ -808,7 +808,7 @@ class TestOperatingSystem(trove_testtools.TestCase):
|
||||
as_root=True)
|
||||
|
||||
def _assert_execute_call(self, exec_args, exec_kwargs,
|
||||
fun, return_value, *args, **kwargs):
|
||||
func, return_value, *args, **kwargs):
|
||||
"""
|
||||
Execute a function with given arguments.
|
||||
Assert a return value and appropriate sequence of calls to the
|
||||
@@ -826,8 +826,8 @@ class TestOperatingSystem(trove_testtools.TestCase):
|
||||
'utils.execute_with_timeout'.
|
||||
:type exec_kwargs: list-of-dicts
|
||||
|
||||
:param fun: Tested function call.
|
||||
:type fun: callable
|
||||
:param func: Tested function call.
|
||||
:type func: callable
|
||||
|
||||
:param return_value: Expected return value or exception
|
||||
from the tested call if any.
|
||||
@@ -844,9 +844,9 @@ class TestOperatingSystem(trove_testtools.TestCase):
|
||||
return_value=('0', '')) as exec_call:
|
||||
if isinstance(return_value, ExpectedException):
|
||||
with return_value:
|
||||
fun(*args, **kwargs)
|
||||
func(*args, **kwargs)
|
||||
else:
|
||||
actual_value = fun(*args, **kwargs)
|
||||
actual_value = func(*args, **kwargs)
|
||||
if return_value is not None:
|
||||
self.assertEqual(return_value, actual_value,
|
||||
"Return value mismatch.")
|
||||
|
||||
Reference in New Issue
Block a user