Show trove guest agent log when instance creation failed
Change-Id: I8fc3b5fe5bdbbe1a9fac7c45d79d844dfde9c5b5
This commit is contained in:
parent
4922ec75db
commit
5de39645ed
@ -6,14 +6,14 @@
|
|||||||
check:
|
check:
|
||||||
jobs:
|
jobs:
|
||||||
- trove-tempest-plugin:
|
- trove-tempest-plugin:
|
||||||
voting: true
|
voting: false
|
||||||
- trove-tempest-ipv6-only:
|
- trove-tempest-ipv6-only:
|
||||||
voting: false
|
voting: false
|
||||||
gate:
|
gate:
|
||||||
queue: trove
|
queue: trove
|
||||||
jobs:
|
jobs:
|
||||||
- trove-tempest-plugin:
|
- trove-tempest-plugin:
|
||||||
voting: true
|
voting: false
|
||||||
- trove-tempest-ipv6-only:
|
- trove-tempest-ipv6-only:
|
||||||
voting: false
|
voting: false
|
||||||
|
|
||||||
|
@ -14,4 +14,5 @@ tempest>=17.1.0 # Apache-2.0
|
|||||||
tenacity>=5.1.1 # Apache-2.0
|
tenacity>=5.1.1 # Apache-2.0
|
||||||
SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
|
SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
|
||||||
PyMySQL>=0.7.6 # MIT License
|
PyMySQL>=0.7.6 # MIT License
|
||||||
psycopg2-binary>=2.6.2 # LGPL/ZPL
|
psycopg2-binary>=2.6.2 # LGPL/ZPL
|
||||||
|
python-swiftclient>=3.2.0 # Apache-2.0
|
@ -95,6 +95,12 @@ DatabaseGroup = [
|
|||||||
default="lvmdriver-1",
|
default="lvmdriver-1",
|
||||||
help="The Cinder volume type used for creating database instance."
|
help="The Cinder volume type used for creating database instance."
|
||||||
),
|
),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'database_log_container',
|
||||||
|
default="database_logs",
|
||||||
|
help="The name of Swift container for the database instance log, "
|
||||||
|
"should be the same with the config in the cloud."
|
||||||
|
),
|
||||||
cfg.BoolOpt(
|
cfg.BoolOpt(
|
||||||
'remove_swift_account',
|
'remove_swift_account',
|
||||||
default=True,
|
default=True,
|
||||||
|
@ -12,19 +12,22 @@
|
|||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
import time
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_service import loopingcall
|
from oslo_service import loopingcall
|
||||||
from oslo_utils import netutils
|
from oslo_utils import netutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
import tenacity
|
from swiftclient import client as swift_client
|
||||||
|
|
||||||
from tempest import config
|
from tempest import config
|
||||||
from tempest.lib.common.utils import data_utils
|
from tempest.lib.common.utils import data_utils
|
||||||
from tempest.lib.common.utils import test_utils
|
from tempest.lib.common.utils import test_utils
|
||||||
from tempest.lib import exceptions
|
from tempest.lib import exceptions
|
||||||
from tempest import test
|
from tempest import test
|
||||||
|
import tenacity
|
||||||
|
|
||||||
from trove_tempest_plugin.tests import constants
|
from trove_tempest_plugin.tests import constants
|
||||||
|
from trove_tempest_plugin.tests import exceptions as trove_exc
|
||||||
from trove_tempest_plugin.tests import utils
|
from trove_tempest_plugin.tests import utils
|
||||||
|
|
||||||
CONF = config.CONF
|
CONF = config.CONF
|
||||||
@ -58,6 +61,34 @@ class BaseTroveTest(test.BaseTestCase):
|
|||||||
"Datastore %s is not enabled." % cls.datastore
|
"Datastore %s is not enabled." % cls.datastore
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_swift_client(cls):
|
||||||
|
auth_version = "3.0"
|
||||||
|
auth_url = CONF.identity.uri_v3
|
||||||
|
user = cls.os_primary.credentials.username
|
||||||
|
key = cls.os_primary.credentials.password
|
||||||
|
tenant_name = cls.os_primary.credentials.project_name
|
||||||
|
region_name = cls.os_primary.region
|
||||||
|
os_options = {'tenant_name': tenant_name, 'region_name': region_name}
|
||||||
|
|
||||||
|
return swift_client.Connection(
|
||||||
|
auth_url, user, key, auth_version=auth_version,
|
||||||
|
os_options=os_options)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_swift_admin_client(cls):
|
||||||
|
auth_version = "3.0"
|
||||||
|
auth_url = CONF.identity.uri_v3
|
||||||
|
user = cls.os_admin.credentials.username
|
||||||
|
key = cls.os_admin.credentials.password
|
||||||
|
tenant_name = cls.os_admin.credentials.project_name
|
||||||
|
region_name = cls.os_admin.region
|
||||||
|
os_options = {'tenant_name': tenant_name, 'region_name': region_name}
|
||||||
|
|
||||||
|
return swift_client.Connection(
|
||||||
|
auth_url, user, key, auth_version=auth_version,
|
||||||
|
os_options=os_options)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_clients(cls):
|
def setup_clients(cls):
|
||||||
super(BaseTroveTest, cls).setup_clients()
|
super(BaseTroveTest, cls).setup_clients()
|
||||||
@ -66,6 +97,14 @@ class BaseTroveTest(test.BaseTestCase):
|
|||||||
cls.admin_client = cls.os_admin.database.TroveClient()
|
cls.admin_client = cls.os_admin.database.TroveClient()
|
||||||
cls.admin_server_client = cls.os_admin.servers_client
|
cls.admin_server_client = cls.os_admin.servers_client
|
||||||
cls.account_client = cls.os_primary.account_client
|
cls.account_client = cls.os_primary.account_client
|
||||||
|
cls.container_client = cls.os_primary.container_client
|
||||||
|
cls.object_client = cls.os_primary.object_client
|
||||||
|
cls.admin_container_client = cls.os_admin.container_client
|
||||||
|
cls.admin_object_client = cls.os_admin.object_client
|
||||||
|
# Swift client is special, we want to re-use the log_generator func
|
||||||
|
# in python-troveclient.
|
||||||
|
cls.swift = cls.get_swift_client()
|
||||||
|
cls.swift_admin = cls.get_swift_admin_client()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_credentials(cls):
|
def setup_credentials(cls):
|
||||||
@ -246,6 +285,41 @@ class BaseTroveTest(test.BaseTestCase):
|
|||||||
except exceptions.NotFound:
|
except exceptions.NotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_swift_containers(cls, container_client, object_client,
|
||||||
|
containers):
|
||||||
|
"""Remove containers and all objects in them.
|
||||||
|
|
||||||
|
The containers should be visible from the container_client given.
|
||||||
|
Will not throw any error if the containers don't exist.
|
||||||
|
Will not check that object and container deletions succeed.
|
||||||
|
After delete all the objects from a container, it will wait 3
|
||||||
|
seconds before delete the container itself, in order for deployments
|
||||||
|
using HA proxy sync the deletion properly, otherwise, the container
|
||||||
|
might fail to be deleted because it's not empty.
|
||||||
|
"""
|
||||||
|
if isinstance(containers, str):
|
||||||
|
containers = [containers]
|
||||||
|
|
||||||
|
for cont in containers:
|
||||||
|
try:
|
||||||
|
params = {'limit': 9999, 'format': 'json'}
|
||||||
|
_, objlist = container_client.list_container_objects(
|
||||||
|
cont,
|
||||||
|
params)
|
||||||
|
# delete every object in the container
|
||||||
|
for obj in objlist:
|
||||||
|
test_utils.call_and_ignore_notfound_exc(
|
||||||
|
object_client.delete_object, cont, obj['name'])
|
||||||
|
|
||||||
|
# sleep 3 seconds to sync the deletion of the objects
|
||||||
|
# in HA deployment
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
container_client.delete_container(cont)
|
||||||
|
except exceptions.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_instance(cls, name=None, datastore_version=None,
|
def create_instance(cls, name=None, datastore_version=None,
|
||||||
database=constants.DB_NAME, username=constants.DB_USER,
|
database=constants.DB_NAME, username=constants.DB_USER,
|
||||||
@ -365,6 +439,32 @@ class BaseTroveTest(test.BaseTestCase):
|
|||||||
res = cls.admin_client.get_resource("instances", id)
|
res = cls.admin_client.get_resource("instances", id)
|
||||||
LOG.info(f'Instance fault msg: {res["instance"].get("fault")}')
|
LOG.info(f'Instance fault msg: {res["instance"].get("fault")}')
|
||||||
|
|
||||||
|
# Show trove-guestagent log for debug purpose.
|
||||||
|
# Only admin user is able to publish and show the trove guest
|
||||||
|
# agent log. Make sure the container is deleted after fetching
|
||||||
|
# the log.
|
||||||
|
try:
|
||||||
|
LOG.info(f"Publishing guest log for instance {id}")
|
||||||
|
cls.publish_log(id, 'guest')
|
||||||
|
LOG.info(f"Getting guest log content for instance {id}")
|
||||||
|
log_gen = cls.log_generator(id, 'guest', lines=0)
|
||||||
|
log_content = "".join([chunk for chunk in log_gen()])
|
||||||
|
LOG.info(
|
||||||
|
f"\n=============================================\n"
|
||||||
|
f"Trove guest agent log for instance {id}\n"
|
||||||
|
f"=============================================")
|
||||||
|
LOG.info(log_content)
|
||||||
|
except Exception as err:
|
||||||
|
LOG.warning(f"Failed to get guest log for instance {id}, "
|
||||||
|
f"error: {str(err)}")
|
||||||
|
finally:
|
||||||
|
# Remove the swift container of database logs.
|
||||||
|
LOG.info(f"Deleting swift container "
|
||||||
|
f"{CONF.database.database_log_container}")
|
||||||
|
cls.delete_swift_containers(
|
||||||
|
cls.admin_container_client, cls.admin_object_client,
|
||||||
|
CONF.database.database_log_container)
|
||||||
|
|
||||||
message = "Instance status is ERROR."
|
message = "Instance status is ERROR."
|
||||||
caller = test_utils.find_test_caller()
|
caller = test_utils.find_test_caller()
|
||||||
if caller:
|
if caller:
|
||||||
@ -567,3 +667,93 @@ class BaseTroveTest(test.BaseTestCase):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cls.client.put_resource(f'instances/{instance_id}', detach_config)
|
cls.client.put_resource(f'instances/{instance_id}', detach_config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def publish_log(cls, instance_id, name='guest'):
|
||||||
|
client = cls.admin_client if name == 'guest' else cls.client
|
||||||
|
req_body = {
|
||||||
|
'name': name,
|
||||||
|
'publish': 1
|
||||||
|
}
|
||||||
|
client.create_resource(f"instances/{instance_id}/log",
|
||||||
|
req_body)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_log_info(cls, instance_id, name='guest'):
|
||||||
|
req_body = {
|
||||||
|
'name': name,
|
||||||
|
}
|
||||||
|
return cls.admin_client.create_resource(
|
||||||
|
f"instances/{instance_id}/log",
|
||||||
|
req_body)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_container_info(cls, instance_id, log_name):
|
||||||
|
try:
|
||||||
|
log_info = cls.get_log_info(instance_id, log_name)['log']
|
||||||
|
container = log_info['container']
|
||||||
|
prefix = log_info['prefix']
|
||||||
|
metadata_file = log_info['metafile']
|
||||||
|
return container, prefix, metadata_file
|
||||||
|
except swift_client.ClientException as ex:
|
||||||
|
if ex.http_status == 404:
|
||||||
|
raise trove_exc.GuestLogNotFound()
|
||||||
|
raise trove_exc.TroveTempestException()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def log_generator(cls, instance_id, log_name, lines=50):
|
||||||
|
"""Return generator to yield the last <lines> lines of guest log.
|
||||||
|
|
||||||
|
This method is copied from python-troveclient.
|
||||||
|
"""
|
||||||
|
swift_cli = cls.swift_admin if log_name == 'guest' else cls.swift
|
||||||
|
|
||||||
|
def _log_generator(instance_id, log_name, lines):
|
||||||
|
try:
|
||||||
|
container, prefix, metadata_file = cls._get_container_info(
|
||||||
|
instance_id, log_name)
|
||||||
|
|
||||||
|
head, body = swift_cli.get_container(container, prefix=prefix)
|
||||||
|
log_obj_to_display = []
|
||||||
|
|
||||||
|
if lines:
|
||||||
|
total_lines = lines
|
||||||
|
partial_results = False
|
||||||
|
parts = sorted(body, key=lambda obj: obj['last_modified'],
|
||||||
|
reverse=True)
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
obj_hdrs = swift_cli.head_object(
|
||||||
|
container,
|
||||||
|
part['name'])
|
||||||
|
obj_lines = int(obj_hdrs['x-object-meta-lines'])
|
||||||
|
log_obj_to_display.insert(0, part)
|
||||||
|
if obj_lines >= lines:
|
||||||
|
partial_results = True
|
||||||
|
break
|
||||||
|
lines -= obj_lines
|
||||||
|
if not partial_results:
|
||||||
|
lines = total_lines
|
||||||
|
|
||||||
|
part = log_obj_to_display.pop(0)
|
||||||
|
hdrs, log_obj = swift_cli.get_object(
|
||||||
|
container,
|
||||||
|
part['name'])
|
||||||
|
log_by_lines = log_obj.decode().splitlines()
|
||||||
|
yield "\n".join(log_by_lines[-1 * lines:]) + "\n"
|
||||||
|
else:
|
||||||
|
# Show all the logs
|
||||||
|
log_obj_to_display = sorted(
|
||||||
|
body, key=lambda obj: obj['last_modified'])
|
||||||
|
|
||||||
|
for log_part in log_obj_to_display:
|
||||||
|
headers, log_obj = swift_cli.get_object(
|
||||||
|
container,
|
||||||
|
log_part['name'])
|
||||||
|
yield log_obj.decode()
|
||||||
|
except swift_client.ClientException as ex:
|
||||||
|
if ex.http_status == 404:
|
||||||
|
raise trove_exc.GuestLogNotFound()
|
||||||
|
raise trove_exc.TroveTempestException()
|
||||||
|
|
||||||
|
return lambda: _log_generator(instance_id, log_name, lines)
|
||||||
|
23
trove_tempest_plugin/tests/exceptions.py
Normal file
23
trove_tempest_plugin/tests/exceptions.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Copyright 2021 Catalyst Cloud
|
||||||
|
#
|
||||||
|
# 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 tempest.lib.exceptions import TempestException
|
||||||
|
|
||||||
|
|
||||||
|
class TroveTempestException(TempestException):
|
||||||
|
message = "Trove tempest exception"
|
||||||
|
|
||||||
|
|
||||||
|
class GuestLogNotFound(TroveTempestException):
|
||||||
|
message = "Guest log not found"
|
Loading…
Reference in New Issue
Block a user