Merge "make verification job great again"

This commit is contained in:
Jenkins 2017-06-15 19:09:16 +00:00 committed by Gerrit Code Review
commit 947b4b23d3

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
import argparse import argparse
import collections
import gzip import gzip
import json import json
import logging import logging
@ -22,126 +23,462 @@ import subprocess
import sys import sys
import uuid import uuid
from rally.cli import envutils from rally import api
from rally.plugins.openstack import credential
from rally.ui import utils from rally.ui import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
LOG.setLevel(logging.DEBUG) LOG.setLevel(logging.DEBUG)
BASE_DIR = "rally-verify"
MODES = {"full": "--pattern set=full", "light": "--pattern set=smoke"}
DEPLOYMENT_NAME = "devstack"
VERIFIER_TYPE = "tempest"
VERIFIER_SOURCE = "https://git.openstack.org/openstack/tempest"
VERIFIER_EXT_REPO = "https://git.openstack.org/openstack/keystone"
VERIFIER_EXT_NAME = "keystone_tests"
SKIP_TESTS = (
"tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON."
"test_get_flavor[id-1f12046b-753d-40d2-abb6-d8eb8b30cb2f,smoke]: "
"This test was skipped intentionally")
XFAIL_TESTS = (
"tempest.api.compute.servers.test_server_actions.ServerActionsTestJSON."
"test_get_vnc_console[id-c6bc11bf-592e-4015-9319-1c98dc64daf5]: "
"This test fails because 'novnc' console type is unavailable")
TEST_NAME_RE = re.compile(r"^[a-zA-Z_.0-9]+(\[[a-zA-Z-,=0-9]*\])?$")
# NOTE(andreykurilin): this variable is used to generate output file names # NOTE(andreykurilin): this variable is used to generate output file names
# with prefix ${CALL_COUNT}_ . # with prefix ${CALL_COUNT}_ .
_call_count = 0 _call_count = 0
# NOTE(andreykurilin): if some command fails, script should end with
# error status
_return_status = 0
def call_rally(cmd, print_output=False, output_type=None): class Status(object):
"""Execute a Rally command and write result in files.""" PASS = "success"
global _return_status ERROR = "error"
global _call_count SKIPPED = "skip"
_call_count += 1 FAILURE = "fail"
data = {"cmd": "rally --rally-debug %s" % cmd}
stdout_file = "{base_dir}/{prefix}_{cmd}.txt.gz"
cmd = cmd.replace("/", "_")
data.update({"stdout_file": stdout_file.format(base_dir=BASE_DIR,
prefix=_call_count,
cmd=cmd.replace(" ", "_"))})
if output_type:
data["output_file"] = data["stdout_file"].replace(
".txt.", ".%s." % output_type)
data["cmd"] += " --to %s" % data["output_file"]
data["cmd"] += " --type %s" % output_type
try:
LOG.info("Try to execute `%s`." % data["cmd"])
stdout = subprocess.check_output(data["cmd"].split(),
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.error("Command `%s` failed." % data["cmd"])
stdout = e.output
data["status"] = "fail"
_return_status = 1
else:
data["status"] = "success"
if output_type:
# let's gzip results
with open(data["output_file"]) as f:
output = f.read()
with gzip.open(data["output_file"], "wb") as f:
f.write(output)
stdout = "$ %s\n%s" % (data["cmd"], stdout)
with gzip.open(data["stdout_file"], "wb") as f:
f.write(stdout)
if print_output:
print(stdout)
return data
def start_verification(args): class Step(object):
"""Start a verification, show results and generate reports.""" COMMAND = None
results = call_rally("verify start %s" % args) DEPENDS_ON = None
results["uuid"] = envutils.get_global(envutils.ENV_VERIFICATION) CALL_ARGS = {}
results["show"] = call_rally("verify show")
results["show_detailed"] = call_rally("verify show --detailed") BASE_DIR = "rally-verify"
for output_type in ("json", "html", "junit-xml"): HTML_TEMPLATE = ("<span class=\"%(status)s\">[%(status)s]</span>\n"
results[output_type.replace("-", "_")] = call_rally( "<a href=\"%(output_file)s\">%(doc)s</a>\n"
"verify report", output_type=output_type) "<code>$ %(cmd)s</code>")
# NOTE(andreykurilin): we need to clean verification uuid from global
# environment to be able to load it next time(for another verification). def __init__(self, args, rapi):
envutils.clear_global(envutils.ENV_VERIFICATION) self.args = args
return results self.rapi = rapi
self.result = {"status": Status.PASS,
"doc": self.__doc__}
@property
def name(self):
return " ".join(re.findall("[A-Z][^A-Z]*",
self.__class__.__name__)).lower()
def check(self, results):
"""Check weather this step should be executed or skipped."""
if self.DEPENDS_ON is not None:
if results[self.DEPENDS_ON].result["status"] in (
Status.PASS, Status.FAILURE):
return True
else:
self.result["status"] = Status.SKIPPED
msg = ("Step '%s' is skipped, since depends on step '%s' is "
"skipped or finished with an error." %
(self.name, results[self.DEPENDS_ON].name))
stdout_file = self._generate_path(
"%s.txt.gz" % self.__class__.__name__)
self.result["output_file"] = self._write_file(
stdout_file, msg, compress=True)
return False
return True
def setUp(self):
"""Obtain variables required for execution"""
pass
def run(self):
"""Execute step. The default action - execute the command"""
self.setUp()
cmd = "rally --rally-debug verify %s" % (self.COMMAND % self.CALL_ARGS)
self.result["cmd"] = cmd
self.result["status"], self.result["output"] = self.call_rally(cmd)
stdout_file = self._generate_path("%s.txt.gz" % cmd)
self.result["output_file"] = self._write_file(
stdout_file, self.result["output"], compress=True)
@classmethod
def _generate_path(cls, root):
global _call_count
_call_count += 1
root = root.replace("<", "").replace(">", "").replace("/", "_")
parts = ["%s" % _call_count]
for path in root.split(" "):
if path.startswith(cls.BASE_DIR):
path = path[len(cls.BASE_DIR) + 1:]
parts.append(path)
return os.path.join(cls.BASE_DIR, "_".join(parts))
@classmethod
def _write_file(cls, path, data, compress=False):
"""Create a file and write some data to it."""
if compress:
with gzip.open(path, "wb") as f:
f.write(data)
else:
with open(path, "wb") as f:
f.write(data)
return path
@staticmethod
def call_rally(command):
"""Execute a Rally verify command."""
try:
LOG.info("Try to execute `%s`." % command)
stdout = subprocess.check_output(command.split(),
stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
LOG.error("Command `%s` failed." % command)
return Status.ERROR, e.output
else:
return Status.PASS, stdout
def to_html(self):
return self.HTML_TEMPLATE % self.result
def write_file(filename, data): class SetUpStep(Step):
"""Create a file and write some data to it.""" """Validate deployment, create required resources and directories."""
path = os.path.join(BASE_DIR, filename)
with open(path, "wb") as f: DEPLOYMENT_NAME = "devstack"
f.write(data)
return path def run(self):
if not os.path.exists("%s/extra" % self.BASE_DIR):
os.makedirs("%s/extra" % self.BASE_DIR)
# ensure that deployment exit
deployment = self.rapi.deployment._get(self.DEPLOYMENT_NAME)
# check it
result = self.rapi.deployment.check(
deployment=self.DEPLOYMENT_NAME)["openstack"]
if "admin_error" in result[0] or "user_error" in result[0]:
self.result["status"] = Status.ERROR
return
try:
subprocess.check_call(["rally", "deployment", "use",
"--deployment", self.DEPLOYMENT_NAME],
stdout=sys.stdout)
except subprocess.CalledProcessError:
self.result["status"] = Status.ERROR
return
credentials = deployment.get_credentials_for("openstack")["admin"]
clients = credentials.clients()
if self.args.ctx_create_resources:
# If the 'ctx-create-resources' arg is provided, delete images and
# flavors, and also create a shared network to make Tempest context
# create needed resources.
LOG.info("The 'ctx-create-resources' arg is provided. Deleting "
"images and flavors, and also creating a shared network "
"to make Tempest context create needed resources.")
LOG.info("Deleting images.")
for image in clients.glance().images.list():
clients.glance().images.delete(image.id)
LOG.info("Deleting flavors.")
for flavor in clients.nova().flavors.list():
clients.nova().flavors.delete(flavor.id)
LOG.info("Creating a shared network.")
net_body = {
"network": {
"name": "shared-net-%s" % str(uuid.uuid4()),
"tenant_id": clients.keystone.auth_ref.project_id,
"shared": True
}
}
clients.neutron().create_network(net_body)
else:
# Otherwise, just in case create only flavors with the following
# properties: RAM = 64MB and 128MB, VCPUs = 1, disk = 0GB to make
# Tempest context discover them.
LOG.info("The 'ctx-create-resources' arg is not provided. "
"Creating flavors to make Tempest context discover them.")
for flv_ram in [64, 128]:
params = {
"name": "flavor-%s" % str(uuid.uuid4()),
"ram": flv_ram,
"vcpus": 1,
"disk": 0
}
LOG.info("Creating flavor '%s' with the following properties: "
"RAM = %dMB, VCPUs = 1, disk = 0GB" %
(params["name"], flv_ram))
clients.nova().flavors.create(**params)
def to_html(self):
return ""
def generate_trends_reports(uuid_1, uuid_2): class ListPlugins(Step):
"""Generate trends reports.""" """List plugins for verifiers management."""
results = {}
for output_type in ("json", "html", "junit-xml"): COMMAND = "list-plugins"
results[output_type.replace("-", "_")] = call_rally( DEPENDS_ON = SetUpStep
"verify report --uuid %s %s" % (uuid_1, uuid_2),
output_type=output_type)
return results
def render_page(**render_vars): class CreateVerifier(Step):
template = utils.get_template("ci/index_verify.html") """Create a Tempest verifier."""
with open(os.path.join(BASE_DIR, "extra/index.html"), "w") as f:
f.write(template.render(**render_vars)) COMMAND = ("create-verifier --type %(type)s --name %(name)s "
"--source %(source)s")
DEPENDS_ON = ListPlugins
CALL_ARGS = {"type": "tempest",
"name": "my-verifier",
"source": "https://git.openstack.org/openstack/tempest"}
class ShowVerifier(Step):
"""Show information about the created verifier."""
COMMAND = "show-verifier"
DEPENDS_ON = CreateVerifier
class ListVerifiers(Step):
"""List all installed verifiers."""
COMMAND = "list-verifiers"
DEPENDS_ON = CreateVerifier
class UpdateVerifier(Step):
"""Switch the verifier to the penultimate version."""
COMMAND = "update-verifier --version %(version)s --update-venv"
DEPENDS_ON = CreateVerifier
def setUp(self):
"""Obtain penultimate verifier commit for downgrading to it"""
verifier_id = self.rapi.verifier.list()[0]["uuid"]
verifications_dir = os.path.join(
os.path.expanduser("~"),
".rally/verification/verifier-%s/repo" % verifier_id)
# Get the penultimate verifier commit ID
p_commit_id = subprocess.check_output(
["git", "log", "-n", "1", "--pretty=format:%H"],
cwd=verifications_dir).strip()
self.CALL_ARGS = {"version": p_commit_id}
class ConfigureVerifier(Step):
"""Generate and show the verifier config file."""
COMMAND = "configure-verifier --show"
DEPENDS_ON = CreateVerifier
class ExtendVerifier(Step):
"""Extend verifier with keystone integration tests."""
COMMAND = "add-verifier-ext --source %(source)s"
DEPENDS_ON = CreateVerifier
CALL_ARGS = {"source": "https://git.openstack.org/openstack/keystone"}
class ListVerifierExtensions(Step):
"""List all extensions of verifier."""
COMMAND = "list-verifier-exts"
DEPENDS_ON = ExtendVerifier
class ListVerifierTests(Step):
"""List all tests of specific verifier."""
COMMAND = "list-verifier-tests"
DEPENDS_ON = CreateVerifier
class RunVerification(Step):
"""Run a verification."""
DEPENDS_ON = ConfigureVerifier
COMMAND = ("start --pattern set=%(set)s --skip-list %(skip_tests)s "
"--xfail-list %(xfail_tests)s --tag %(tag)s %(set)s-set "
"--detailed")
SKIP_TESTS = {
"tempest.api.compute.flavors.test_flavors.FlavorsV2TestJSON."
"test_get_flavor[id-1f12046b-753d-40d2-abb6-d8eb8b30cb2f,smoke]":
"This test was skipped intentionally"}
XFAIL_TESTS = {
"tempest.api.compute.servers.test_server_actions."
"ServerActionsTestJSON.test_get_vnc_console"
"[id-c6bc11bf-592e-4015-9319-1c98dc64daf5]":
"This test fails because 'novnc' console type is unavailable"}
def setUp(self):
self.CALL_ARGS["tag"] = "tag-1 tag-2"
self.CALL_ARGS["set"] = "full" if self.args.mode == "full" else "smoke"
# Start a verification, show results and generate reports
skip_tests = json.dumps(self.SKIP_TESTS)
xfail_tests = json.dumps(self.XFAIL_TESTS)
self.CALL_ARGS["skip_tests"] = self._write_file(
self._generate_path("skip-list.json"), skip_tests)
self.CALL_ARGS["xfail_tests"] = self._write_file(
self._generate_path("xfail-list.json"), xfail_tests)
def run(self):
super(RunVerification, self).run()
if "Success: 0" in self.result["output"]:
self.result["status"] = Status.FAILURE
class ReRunVerification(RunVerification):
"""Re-Run previous verification."""
COMMAND = "rerun --tag one-more-attempt"
def run(self):
super(RunVerification, self).run()
if "Success: 0" in self.result["output"]:
self.result["status"] = Status.FAILURE
class ShowVerification(Step):
"""Show results of verification."""
COMMAND = "show"
DEPENDS_ON = RunVerification
class ShowSecondVerification(ShowVerification):
"""Show results of verification."""
DEPENDS_ON = ReRunVerification
class ShowDetailedVerification(Step):
"""Show detailed results of verification."""
COMMAND = "show --detailed"
DEPENDS_ON = RunVerification
class ShowDetailedSecondVerification(ShowDetailedVerification):
"""Show detailed results of verification."""
DEPENDS_ON = ReRunVerification
class ReportVerificationMixin(Step):
"""Mixin for obtaining reports of verifications."""
COMMAND = "report --uuid %(uuids)s --type %(type)s --to %(out)s"
HTML_TEMPLATE = ("<span class=\"%(status)s\">[%(status)s]</span>\n"
"<a href=\"%(out)s\">%(doc)s</a> "
"[<a href=\"%(output_file)s\">Output from CLI</a>]\n"
"<code>$ %(cmd)s</code>")
def setUp(self):
self.CALL_ARGS["out"] = "<path>"
self.CALL_ARGS["uuids"] = "<uuid-1> <uuid-2>"
cmd = self.COMMAND % self.CALL_ARGS
report = "%s.%s" % (cmd.replace("/", "_").replace(" ", "_"),
self.CALL_ARGS["type"])
print(report)
self.CALL_ARGS["out"] = self._generate_path(report)
self.CALL_ARGS["uuids"] = " ".join(
[v["uuid"] for v in self.rapi.verification.list()])
print(self.COMMAND % self.CALL_ARGS)
def run(self):
super(ReportVerificationMixin, self).run()
creport = "%s.gz" % self.CALL_ARGS["out"]
with open(self.CALL_ARGS["out"], "rb") as f_in:
with gzip.open(creport, "wb") as f_out:
f_out.writelines(f_in)
self.result["out"] = creport
class HtmlVerificationReport(ReportVerificationMixin):
"""Generate HTML report for verification(s)."""
CALL_ARGS = {"type": "html"}
DEPENDS_ON = RunVerification
class JsonVerificationReport(ReportVerificationMixin):
"""Generate JSON report for verification(s)."""
CALL_ARGS = {"type": "json"}
DEPENDS_ON = RunVerification
class JunitVerificationReport(ReportVerificationMixin):
"""Generate JUNIT report for verification(s)."""
CALL_ARGS = {"type": "junit-xml"}
DEPENDS_ON = RunVerification
class ListVerifications(Step):
"""List all verifications."""
COMMAND = "list"
DEPENDS_ON = CreateVerifier
class DeleteVerifierExtension(Step):
"""Delete keystone extension."""
COMMAND = "delete-verifier-ext --name %(name)s"
CALL_ARGS = {"name": "keystone_tests"}
DEPENDS_ON = ExtendVerifier
class DeleteVerifier(Step):
"""Delete Tempest verifier and all verifications."""
COMMAND = "delete-verifier --id %(id)s --force"
CALL_ARGS = {"id": CreateVerifier.CALL_ARGS["name"]}
DEPENDS_ON = CreateVerifier
def run(args):
steps = [SetUpStep,
ListPlugins,
CreateVerifier,
ShowVerifier,
ListVerifiers,
UpdateVerifier,
ConfigureVerifier,
ExtendVerifier,
ListVerifierExtensions,
ListVerifierTests,
RunVerification,
ShowVerification,
ShowDetailedVerification,
HtmlVerificationReport,
JsonVerificationReport,
JunitVerificationReport,
ListVerifications,
DeleteVerifierExtension,
DeleteVerifier]
if args.compare:
# need to launch one more verification
place_to_insert = steps.index(ShowDetailedVerification) + 1
# insert steps in reverse order to be able to use the same index
steps.insert(place_to_insert, ShowDetailedSecondVerification)
steps.insert(place_to_insert, ShowSecondVerification)
steps.insert(place_to_insert, ReRunVerification)
results = collections.OrderedDict()
rapi = api.API()
for step_cls in steps:
step = step_cls(args, rapi=rapi)
if step.check(results):
step.run()
results[step_cls] = step
return results.values()
def main(): def main():
@ -150,7 +487,7 @@ def main():
help="Mode of job. The 'full' mode corresponds to the " help="Mode of job. The 'full' mode corresponds to the "
"full set of verifier tests. The 'light' mode " "full set of verifier tests. The 'light' mode "
"corresponds to the smoke set of verifier tests.", "corresponds to the smoke set of verifier tests.",
choices=MODES.keys()) choices=["light", "full"])
parser.add_argument("--compare", action="store_true", parser.add_argument("--compare", action="store_true",
help="Start the second verification to generate a " help="Start the second verification to generate a "
"trends report for two verifications.") "trends report for two verifications.")
@ -162,140 +499,17 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if not os.path.exists("%s/extra" % BASE_DIR): steps = run(args)
os.makedirs("%s/extra" % BASE_DIR) results = [step.to_html() for step in steps]
# Choose and check the deployment template = utils.get_template("ci/index_verify.html")
call_rally("deployment use --deployment %s" % DEPLOYMENT_NAME) with open(os.path.join(Step.BASE_DIR, "extra/index.html"), "w") as f:
call_rally("deployment check") f.write(template.render(steps=results))
config = json.loads( if len([None for step in steps if step.result["status"] in (
subprocess.check_output(["rally", "deployment", "config"])) Status.PASS, Status.FAILURE)]) == len(steps):
config = config["creds"]["openstack"] return 0
config.update(config.pop("admin")) return 1
clients = credential.OpenStackCredential(**config).clients()
if args.ctx_create_resources:
# If the 'ctx-create-resources' arg is provided, delete images and
# flavors, and also create a shared network to make Tempest context
# create needed resources.
LOG.info("The 'ctx-create-resources' arg is provided. Deleting "
"images and flavors, and also creating a shared network "
"to make Tempest context create needed resources.")
LOG.info("Deleting images.")
for image in clients.glance().images.list():
clients.glance().images.delete(image.id)
LOG.info("Deleting flavors.")
for flavor in clients.nova().flavors.list():
clients.nova().flavors.delete(flavor.id)
LOG.info("Creating a shared network.")
net_body = {
"network": {
"name": "shared-net-%s" % str(uuid.uuid4()),
"tenant_id": clients.keystone.auth_ref.project_id,
"shared": True
}
}
clients.neutron().create_network(net_body)
else:
# Otherwise, just in case create only flavors with the following
# properties: RAM = 64MB and 128MB, VCPUs = 1, disk = 0GB to make
# Tempest context discover them.
LOG.info("The 'ctx-create-resources' arg is not provided. "
"Creating flavors to make Tempest context discover them.")
for flv_ram in [64, 128]:
params = {
"name": "flavor-%s" % str(uuid.uuid4()),
"ram": flv_ram,
"vcpus": 1,
"disk": 0
}
LOG.info(
"Creating flavor '%s' with the following properties: RAM "
"= %dMB, VCPUs = 1, disk = 0GB" % (params["name"], flv_ram))
clients.nova().flavors.create(**params)
render_vars = dict(verifications=[])
# List plugins for verifiers management
render_vars["list_plugins"] = call_rally("verify list-plugins")
# Create a verifier
render_vars["create_verifier"] = call_rally(
"verify create-verifier --type %s --name my-verifier --source %s"
% (VERIFIER_TYPE, VERIFIER_SOURCE))
# Show the verifier
render_vars["show_verifier"] = call_rally("verify show-verifier")
# List verifiers
render_vars["list_verifiers"] = call_rally("verify list-verifiers")
# Get verifier ID
verifier_id = envutils.get_global(envutils.ENV_VERIFIER)
# Get the penultimate verifier commit ID
repo_dir = os.path.join(
os.path.expanduser("~"),
".rally/verification/verifier-%s/repo" % verifier_id)
p_commit_id = subprocess.check_output(
["git", "log", "-n", "1", "--pretty=format:%H"], cwd=repo_dir).strip()
# Switch the verifier to the penultimate version
render_vars["update_verifier"] = call_rally(
"verify update-verifier --version %s --update-venv" % p_commit_id)
# Generate and show the verifier config file
render_vars["configure_verifier"] = call_rally(
"verify configure-verifier --show")
# Add a verifier extension
render_vars["add_verifier_ext"] = call_rally(
"verify add-verifier-ext --source %s" % VERIFIER_EXT_REPO)
# List verifier extensions
render_vars["list_verifier_exts"] = call_rally("verify list-verifier-exts")
# List verifier tests
render_vars["list_verifier_tests"] = call_rally(
"verify list-verifier-tests %s" % MODES[args.mode])
# Start a verification, show results and generate reports
skip_list_path = write_file("skip-list.yaml", SKIP_TESTS)
xfail_list_path = write_file("xfail-list.yaml", XFAIL_TESTS)
run_args = ("%s --skip-list %s --xfail-list %s --tag first-run %s-set "
"--detailed" % (MODES[args.mode], skip_list_path,
xfail_list_path, args.mode))
render_vars["verifications"].append(start_verification(run_args))
if args.compare:
# Start another verification, show results and generate reports
with gzip.open(render_vars["list_verifier_tests"]["stdout_file"]) as f:
tests = [t for t in f.read().split("\n") if TEST_NAME_RE.match(t)]
load_list_path = write_file("load-list.txt", "\n".join(tests))
run_args = "--load-list %s --tag second-run %s-set --detailed" % (
load_list_path, args.mode)
render_vars["verifications"].append(start_verification(run_args))
# Generate trends reports for two verifications
render_vars["compare"] = generate_trends_reports(
render_vars["verifications"][-2]["uuid"],
render_vars["verifications"][-1]["uuid"])
# List verifications
render_vars["list"] = call_rally("verify list")
# Delete the verifier extension
render_vars["delete_verifier_ext"] = call_rally(
"verify delete-verifier-ext --name %s" % VERIFIER_EXT_NAME)
# Delete the verifier and all verifications
render_vars["delete_verifier"] = call_rally(
"verify delete-verifier --id %s --force" % verifier_id)
render_page(**render_vars)
return _return_status
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())