]> git.apps.os.sepia.ceph.com Git - teuthology.git/commitdiff
add Scanner, UnitTestScanner, ValgrindScanner
authorVallari Agrawal <val.agl002@gmail.com>
Sat, 1 Oct 2022 11:16:52 +0000 (16:46 +0530)
committerVallari Agrawal <val.agl002@gmail.com>
Mon, 27 Nov 2023 16:25:41 +0000 (21:55 +0530)
1. add 'run_unit_test' to Remote
2. create util/scanner.py
3. new exception: UnitTestError
4. add `lxml` dependency in setup.cfg

Signed-off-by: Vallari Agrawal <val.agl002@gmail.com>
requirements.txt
setup.cfg
teuthology/exceptions.py
teuthology/orchestra/remote.py
teuthology/orchestra/test/xml_files/test_scan_nose.xml [new file with mode: 0644]
teuthology/util/scanner.py [new file with mode: 0644]

index f56344bd85cd848ef1c6546e7ce9e8783ead04e2..56af1c00e859f13d1adc5c854e10eb416cfa5a94 100644 (file)
@@ -124,6 +124,8 @@ keystoneauth1==4.3.1
     #   python-novaclient
 lupa==1.14.1
     # via teuthology (pyproject.toml)
+lxml==4.9.3
+    # via teuthology (pyproject.toml)
 markupsafe==2.0.1
     # via jinja2
 mock==4.0.3
index 6b9d2c2c127849dad9e9b42b84d37a5d32f37b61..9d1c0134e5fae0f3cfe211baadb4a44a5a38c00b 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,6 +41,7 @@ install_requires =
     httplib2
     humanfriendly
     lupa
+    lxml
     ndg-httpsclient
     netaddr
     paramiko
index cbe8b5941fe198669a84419d90f6b8b43779b626..da38343545119a5a08ffddc9ad47dc1a399eac7e 100644 (file)
@@ -212,3 +212,27 @@ class NoRemoteError(Exception):
 
     def __str__(self):
         return self.message
+
+
+class UnitTestError(Exception):
+    """
+    Exception thrown on unit test failure
+    """
+    def __init__(self, exitstatus=None, node=None, label=None, message=None):
+        self.exitstatus = exitstatus
+        self.node = node
+        self.label = label
+        self.message = message
+
+    def __str__(self):
+        prefix = "Unit test failed"
+        if self.label:
+            prefix += " ({label})".format(label=self.label)
+        if self.node:
+            prefix += " on {node}".format(node=self.node)
+        if self.exitstatus:
+            prefix += " with status {status}".format(status=self.exitstatus)
+        return "{prefix}: '{message}'".format(
+            prefix=prefix,
+            message=self.message,
+        )
index 0392acf87e6f1c095f0e3940c017e411fd99a5d6..ce77a519cf36189c16a0e15c151b8e3c19467a24 100644 (file)
@@ -11,7 +11,8 @@ from teuthology.orchestra import console
 from teuthology.orchestra.opsys import OS
 import teuthology.provision
 from teuthology import misc
-from teuthology.exceptions import CommandFailedError
+from teuthology.exceptions import CommandFailedError, UnitTestError
+from teuthology.util.scanner import UnitTestScanner
 from teuthology.misc import host_shortname
 import errno
 import re
@@ -523,6 +524,20 @@ class Remote(RemoteShell):
         r.remote = self
         return r
 
+    def run_unit_test(self, xml_path_regex, output_yaml, **kwargs):
+        try:
+            r = self.run(**kwargs)
+        except CommandFailedError as exc:
+            if xml_path_regex:
+                error_msg = UnitTestScanner(remote=self).scan_and_write(xml_path_regex, output_yaml)
+                if error_msg:
+                    raise UnitTestError(
+                        exitstatus=exc.exitstatus, node=exc.node, 
+                        label=exc.label, message=error_msg
+                    )
+            raise exc
+        return r
+
     def _sftp_put_file(self, local_path, remote_path):
         """
         Use the paramiko.SFTPClient to put a file. Returns the remote filename.
@@ -543,12 +558,14 @@ class Remote(RemoteShell):
         sftp.get(remote_path, local_path)
         return local_path
 
-    def _sftp_open_file(self, remote_path):
+    def _sftp_open_file(self, remote_path, mode=None):
         """
         Use the paramiko.SFTPClient to open a file. Returns a
         paramiko.SFTPFile object.
         """
         sftp = self.ssh.open_sftp()
+        if mode:
+            return sftp.open(remote_path, mode)
         return sftp.open(remote_path)
 
     def _sftp_get_size(self, remote_path):
diff --git a/teuthology/orchestra/test/xml_files/test_scan_nose.xml b/teuthology/orchestra/test/xml_files/test_scan_nose.xml
new file mode 100644 (file)
index 0000000..d0fab48
--- /dev/null
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<testsuite name="nosetests" tests="644" errors="0" failures="1" skip="79">
+<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_origin_response" time="3.205"></testcase>
+<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_origin_wildcard" time="3.081"></testcase>
+<testcase classname="s3tests_boto3.functional.test_s3" name="test_cors_header_option" time="3.119"></testcase>
+<testcase classname="s3tests_boto3.functional.test_s3" name="test_set_bucket_tagging" time="0.059"><failure type="builtins.AssertionError" message="'NoSuchTagSetError' != 'NoSuchTagSet'&#10;-------------------- &gt;&gt; begin captured logging &lt;&lt; --------------------&#10;botocore.hooks: DEBUG: Event choose-service-name: calling handler &lt;function handle_service_name_alias at 0x7fb6def62d30&gt;&#10;botocore.hooks: DEBUG: Event creating-client-class.s3: calling handler &lt;function add_generate_presigned_post at 0x7fb6defdad30&gt;&#10;botocore.hooks: DEBUG: Event creating-client-class.s3: calling handler &lt;function lazy_call.&lt;locals&gt;._handler at 0x7fb6debf5d30&gt;&#10;botocore.hooks: DEBUG: Event creating-client-class.s3: calling handler &lt;function add_generate_presigned_url at 0x7fb6defdaaf0&gt;&#10;botocore.endpoint: DEBUG: Setting s3 timeout as (60, 60)&#10;botocore.client: DEBUG: Registering retry handlers for service: s3&#10;botocore.hooks: DEBUG: Event before-parameter-build.s3.CreateBucket: calling handler &lt;function validate_bucket_name at 0x7fb6def01430&gt;&#10;botocore.hooks: DEBUG: Event before-parameter-build.s3.CreateBucket: calling handler &lt;bound method S3RegionRedirector.redirect_from_cache of &lt;botocore.utils.S3RegionRedirector object at 0x7fb6de659d30&gt;&gt;&#10;botocore.hooks: DEBUG: Event before-parameter-build.s3.CreateBucket: calling handler &lt;bound method S3ArnParamHandler.handle_arn of &lt;botocore.utils.S3ArnParamHandler object at 0x7fb6de659580&gt;&gt;&#10;botocore.hooks: DEBUG: Event before-parameter-build.s3.CreateBucket: calling handler &lt;function generate_idempotent_uuid at 0x7fb6def01280&gt;&#10;botocore.hooks: DEBUG: Event before-call.s3.CreateBucket: calling handler &lt;function add_expect_header at 0x7fb6def01790&gt;&#10;botocore.hooks: DEBUG: Event before-call.s3.CreateBucket: calling handler &lt;bound method S3RegionRedirector.set_request_url of &lt;botocore.utils.S3RegionRedirector object at 0x7fb6de659d30&gt;&gt;&#10;botocore.hooks: DEBUG: Event before-call.s3.CreateBucket: calling handler &lt;function add_recursion_detection_header at 0x7fb6deef7ee0&gt;&#10;botocore.hooks: DEBUG: Event before-call.s3.CreateBucket: calling handler &lt;function inject_api_version_header_if_needed at 0x7fb6def02af0&gt;&#10;botocore.endpoint: DEBUG: Making request for OperationModel(name=CreateBucket) with params: {'url_path': '/test-client.0-2txq2dyjghs0vdf-335', 'query_string': {}, 'method': 'PUT', 'headers': {'User-Agent': 'Boto3/1.24.82 Python/3.8.10 Linux/5.4.0-126-generic Botocore/1.27.82'}, 'body': b'', 'url': 'http://smithi196.front.sepia.ceph.com:80/test-client.0-2txq2dyjghs0vdf-335', 'context': {'client_region': 'us-east-1', 'client_config': &lt;botocore.config.Config object at 0x7fb6de1298b0&gt;, 'has_streaming_input': False, 'auth_type': None, 'signing': {'bucket': 'test-client.0-2txq2dyjghs0vdf-335'}}}&#10;botocore.hooks: DEBUG: Event request-created.s3.CreateBucket: calling handler &lt;bound method RequestSigner.handler of &lt;botocore.signers.RequestSigner object at 0x7fb6de129e20&gt;&gt;&#10;botocore.hooks: DEBUG: Event choose-signer.s3.CreateBucket: calling handler &lt;bound method S3EndpointSetter.set_signer of &lt;botocore.utils.S3EndpointSetter object at 0x7fb6de6598b0&gt;&gt;&#10;botocore.hooks: DEBUG: Event choose-signer.s3.CreateBucket: calling handler &lt;function set_operation_specific_signer at 0x7fb6def01160&gt;&#10;botocore.hooks: DEBUG: Event before-sign.s3.CreateBucket: calling handler &lt;bound method S3EndpointSetter.set_endpoint of &lt;botocore.utils.S3EndpointSetter object at 0x7fb6de6598b0&gt;&gt;&#10;botocore.utils: DEBUG: Using S3 path style addressing.&#10;botocore.auth: DEBUG: Calculating signature using v4 auth.&#10;botocore.auth: DEBUG: CanonicalRequest:&#10;PUT&#10;/test-client.0-2txq2dyjghs0vdf-335&#10;&#10;host:smithi196.front.sepia.ceph.com&#10;x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&#10;x-amz-date:20220929T065029Z&#10;&#10;host;x-amz-content-sha256;x-amz-date&#10;e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&#10;botocore.auth: DEBUG: StringToSign:&#10;AWS4-HMAC-SHA256&#10;20220929T065029Z&#10;20220929/us-east-1/s3/aws4_request&#10;ddfd952c0ac842cff08711f6b1425bec213bd1f69ae5ae6f37afb7a2f66e7fcb&#10;botocore.auth: DEBUG: Signature:&#10;8b7f685e9b8a9a807437088da293390ac21ed9a10acf51903a8da2281bdc9c45&#10;botocore.hooks: DEBUG: Event request-created.s3.CreateBucket: calling handler &lt;function add_retry_headers at 0x7fb6def031f0&gt;&#10;botocore.endpoint: DEBUG: Sending http request: &lt;AWSPreparedRequest stream_output=False, method=PUT, url=http://smithi196.front.sepia.ceph.com:80/test-client.0-2txq2dyjghs0vdf-335, headers={'User-Agent': b'Boto3/1.24.82 Python/3.8.10 Linux/5.4.0-126-generic Botocore/1.27.82', 'X-Amz-Date': b'20220929T065029Z', 'X-Amz-Content-SHA256': b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'Authorization': b'AWS4-HMAC-SHA256 Credential=EBIAVLDBAMGWPWFMDQHA/20220929/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=8b7f685e9b8a9a807437088da293390ac21ed9a10acf51903a8da2281bdc9c45', 'amz-sdk-invocation-id': b'1795d992-7f27-43c5-ae48-06ef5ea88c6c', 'amz-sdk-request': b'attempt=1', 'Content-Length': '0'}&gt;&#10;urllib3.connectionpool: DEBUG: Starting new HTTP connection (1): smithi196.front.sepia.ceph.com:80&#10;urllib3.connectionpool: DEBUG: http://smithi196.front.sepia.ceph.com:80 &quot;PUT /test-client.0-2txq2dyjghs0vdf-335 HTTP/1.1&quot; 200 0&#10;botocore.parsers: DEBUG: Response headers: {'x-amz-request-id': 'tx00000e29af2294ab8b56c-0063354035-1157-default', 'Content-Length': '0', 'Date': 'Thu, 29 Sep 2022 06:50:29 GMT', 'Connection': 'Keep-Alive'}&#10;botocore.parsers: DEBUG: Response body:&#10;b''&#10;botocore.hooks: DEBUG: Event needs-retry.s3.CreateBucket: calling handler &lt;botocore.retryhandler.RetryHandler object at 0x7fb6de6597c0&gt;&#10;botocore.retryhandler: DEBUG: No retry needed.&#10;botocore.hooks: DEBUG: Event needs-retry.s3.CreateBucket: calling handler &lt;bound method S3RegionRedirector.redirect_from_error of &lt;botocore.utils.S3RegionRedirector object at 0x7fb6de659d30&gt;&gt;&#10;botocore.hooks: DEBUG: Event choose-service-name: calling handler &lt;function handle_service_name_alias at 0x7fb6def62d30&gt;&#10;botocore.hooks: DEBUG: Event creating-client-class.s3: calling handler &lt;function add_generate_presigned_post at 0x7fb6defdad30&gt;&#10;botocore.hooks: DEBUG: Event creating-client-class.s3: calling handler &lt;function lazy_call.&lt;locals&gt;._handler at 0x7fb6debf5d30&gt;&#10;botocore.hooks: DEBUG: Event creating-client-class.s3: calling handler &lt;function add_generate_presigned_url at 0x7fb6defdaaf0&gt;&#10;botocore.endpoint: DEBUG: Setting s3 timeout as (60, 60)&#10;botocore.client: DEBUG: Registering retry handlers for service: s3&#10;botocore.hooks: DEBUG: Event before-parameter-build.s3.GetBucketTagging: calling handler &lt;function validate_bucket_name at 0x7fb6def01430&gt;&#10;botocore.hooks: DEBUG: Event before-parameter-build.s3.GetBucketTagging: calling handler &lt;bound method S3RegionRedirector.redirect_from_cache of &lt;botocore.utils.S3RegionRedirector object at 0x7fb6dec1eaf0&gt;&gt;&#10;botocore.hooks: DEBUG: Event before-parameter-build.s3.GetBucketTagging: calling handler &lt;bound method S3ArnParamHandler.handle_arn of &lt;botocore.utils.S3ArnParamHandler object at 0x7fb6dec1ec10&gt;&gt;&#10;botocore.hooks: DEBUG: Event before-parameter-build.s3.GetBucketTagging: calling handler &lt;function generate_idempotent_uuid at 0x7fb6def01280&gt;&#10;botocore.hooks: DEBUG: Event before-call.s3.GetBucketTagging: calling handler &lt;function add_expect_header at 0x7fb6def01790&gt;&#10;botocore.hooks: DEBUG: Event before-call.s3.GetBucketTagging: calling handler &lt;bound method S3RegionRedirector.set_request_url of &lt;botocore.utils.S3RegionRedirector object at 0x7fb6dec1eaf0&gt;&gt;&#10;botocore.hooks: DEBUG: Event before-call.s3.GetBucketTagging: calling handler &lt;function add_recursion_detection_header at 0x7fb6deef7ee0&gt;&#10;botocore.hooks: DEBUG: Event before-call.s3.GetBucketTagging: calling handler &lt;function inject_api_version_header_if_needed at 0x7fb6def02af0&gt;&#10;botocore.endpoint: DEBUG: Making request for OperationModel(name=GetBucketTagging) with params: {'url_path': '/test-client.0-2txq2dyjghs0vdf-335?tagging', 'query_string': {}, 'method': 'GET', 'headers': {'User-Agent': 'Boto3/1.24.82 Python/3.8.10 Linux/5.4.0-126-generic Botocore/1.27.82'}, 'body': b'', 'url': 'http://smithi196.front.sepia.ceph.com:80/test-client.0-2txq2dyjghs0vdf-335?tagging', 'context': {'client_region': 'us-east-1', 'client_config': &lt;botocore.config.Config object at 0x7fb6de2c5220&gt;, 'has_streaming_input': False, 'auth_type': None, 'signing': {'bucket': 'test-client.0-2txq2dyjghs0vdf-335'}}}&#10;botocore.hooks: DEBUG: Event request-created.s3.GetBucketTagging: calling handler &lt;bound method RequestSigner.handler of &lt;botocore.signers.RequestSigner object at 0x7fb6de2c5dc0&gt;&gt;&#10;botocore.hooks: DEBUG: Event choose-signer.s3.GetBucketTagging: calling handler &lt;bound method S3EndpointSetter.set_signer of &lt;botocore.utils.S3EndpointSetter object at 0x7fb6dec1e760&gt;&gt;&#10;botocore.hooks: DEBUG: Event choose-signer.s3.GetBucketTagging: calling handler &lt;function set_operation_specific_signer at 0x7fb6def01160&gt;&#10;botocore.hooks: DEBUG: Event before-sign.s3.GetBucketTagging: calling handler &lt;bound method S3EndpointSetter.set_endpoint of &lt;botocore.utils.S3EndpointSetter object at 0x7fb6dec1e760&gt;&gt;&#10;botocore.utils: DEBUG: Using S3 path style addressing.&#10;botocore.auth: DEBUG: Calculating signature using v4 auth.&#10;botocore.auth: DEBUG: CanonicalRequest:&#10;GET&#10;/test-client.0-2txq2dyjghs0vdf-335&#10;tagging=&#10;host:smithi196.front.sepia.ceph.com&#10;x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&#10;x-amz-date:20220929T065029Z&#10;&#10;host;x-amz-content-sha256;x-amz-date&#10;e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855&#10;botocore.auth: DEBUG: StringToSign:&#10;AWS4-HMAC-SHA256&#10;20220929T065029Z&#10;20220929/us-east-1/s3/aws4_request&#10;8a096d01796a8a6afca50c1bc3bc5c9098917c26a6dba7e752412ce31041c575&#10;botocore.auth: DEBUG: Signature:&#10;a58a94727b0c0d6d43e8783c91499ce9a9758260aa09a286524c0eb1bc4883d1&#10;botocore.hooks: DEBUG: Event request-created.s3.GetBucketTagging: calling handler &lt;function add_retry_headers at 0x7fb6def031f0&gt;&#10;botocore.endpoint: DEBUG: Sending http request: &lt;AWSPreparedRequest stream_output=False, method=GET, url=http://smithi196.front.sepia.ceph.com:80/test-client.0-2txq2dyjghs0vdf-335?tagging, headers={'User-Agent': b'Boto3/1.24.82 Python/3.8.10 Linux/5.4.0-126-generic Botocore/1.27.82', 'X-Amz-Date': b'20220929T065029Z', 'X-Amz-Content-SHA256': b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'Authorization': b'AWS4-HMAC-SHA256 Credential=EBIAVLDBAMGWPWFMDQHA/20220929/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a58a94727b0c0d6d43e8783c91499ce9a9758260aa09a286524c0eb1bc4883d1', 'amz-sdk-invocation-id': b'c4afeb5a-6a87-4e29-83ef-b96195038217', 'amz-sdk-request': b'attempt=1'}&gt;&#10;urllib3.connectionpool: DEBUG: Starting new HTTP connection (1): smithi196.front.sepia.ceph.com:80&#10;urllib3.connectionpool: DEBUG: http://smithi196.front.sepia.ceph.com:80 &quot;GET /test-client.0-2txq2dyjghs0vdf-335?tagging HTTP/1.1&quot; 404 248&#10;botocore.parsers: DEBUG: Response headers: {'Content-Length': '248', 'x-amz-request-id': 'tx00000ebc589e4bcad8d86-0063354035-1157-default', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/xml', 'Date': 'Thu, 29 Sep 2022 06:50:29 GMT', 'Connection': 'Keep-Alive'}&#10;botocore.parsers: DEBUG: Response body:&#10;b'&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;Error&gt;&lt;Code&gt;NoSuchTagSetError&lt;/Code&gt;&lt;BucketName&gt;test-client.0-2txq2dyjghs0vdf-335&lt;/BucketName&gt;&lt;RequestId&gt;tx00000ebc589e4bcad8d86-0063354035-1157-default&lt;/RequestId&gt;&lt;HostId&gt;1157-default-default&lt;/HostId&gt;&lt;/Error&gt;'&#10;botocore.hooks: DEBUG: Event needs-retry.s3.GetBucketTagging: calling handler &lt;botocore.retryhandler.RetryHandler object at 0x7fb6dec1ebe0&gt;&#10;botocore.retryhandler: DEBUG: No retry needed.&#10;botocore.hooks: DEBUG: Event needs-retry.s3.GetBucketTagging: calling handler &lt;bound method S3RegionRedirector.redirect_from_error of &lt;botocore.utils.S3RegionRedirector object at 0x7fb6dec1eaf0&gt;&gt;&#10;--------------------- &gt;&gt; end captured logging &lt;&lt; ---------------------"><![CDATA[  File "/usr/lib/python3.8/unittest/case.py", line 60, in testPartExecutor
+    yield
+  File "/usr/lib/python3.8/unittest/case.py", line 676, in run
+    self._callTestMethod(testMethod)
+  File "/usr/lib/python3.8/unittest/case.py", line 633, in _callTestMethod
+    method()
+  File "/home/ubuntu/cephtest/s3-tests-client.0/virtualenv/lib/python3.8/site-packages/nose/case.py", line 198, in runTest
+    self.test(*self.arg)
+  File "/home/ubuntu/cephtest/s3-tests-client.0/s3tests_boto3/functional/test_s3.py", line 7692, in test_set_bucket_tagging
+    eq(error_code, 'NoSuchTagSet')
+  File "/home/ubuntu/cephtest/s3-tests-client.0/virtualenv/lib/python3.8/site-packages/nose/tools/trivial.py", line 29, in eq_
+    raise AssertionError(msg or "%r != %r" % (a, b))
+'NoSuchTagSetError' != 'NoSuchTagSet'
+-------------------- >> begin captured logging << --------------------
+botocore.hooks: DEBUG: Event choose-service-name: calling handler <function handle_service_name_alias at 0x7fb6def62d30>
+PUT
+/test-client.0-2txq2dyjghs0vdf-335
+
+host:smithi196.front.sepia.ceph.com
+x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+x-amz-date:20220929T065029Z
+
+host;x-amz-content-sha256;x-amz-date
+e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+botocore.auth: DEBUG: StringToSign:
+AWS4-HMAC-SHA256
+20220929T065029Z
+20220929/us-east-1/s3/aws4_request
+ddfd952c0ac842cff08711f6b1425bec213bd1f69ae5ae6f37afb7a2f66e7fcb
+botocore.auth: DEBUG: Signature:
+8b7f685e9b8a9a807437088da293390ac21ed9a10acf51903a8da2281bdc9c45
+botocore.hooks: DEBUG: Event request-created.s3.CreateBucket: calling handler <function add_retry_headers at 0x7fb6def031f0>
+botocore.endpoint: DEBUG: Sending http request: <AWSPreparedRequest stream_output=False, method=PUT, url=http://smithi196.front.sepia.ceph.com:80/test-client.0-2txq2dyjghs0vdf-335, headers={'User-Agent': b'Boto3/1.24.82 Python/3.8.10 Linux/5.4.0-126-generic Botocore/1.27.82', 'X-Amz-Date': b'20220929T065029Z', 'X-Amz-Content-SHA256': b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'Authorization': b'AWS4-HMAC-SHA256 Credential=EBIAVLDBAMGWPWFMDQHA/20220929/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=8b7f685e9b8a9a807437088da293390ac21ed9a10acf51903a8da2281bdc9c45', 'amz-sdk-invocation-id': b'1795d992-7f27-43c5-ae48-06ef5ea88c6c', 'amz-sdk-request': b'attempt=1', 'Content-Length': '0'}>
+urllib3.connectionpool: DEBUG: Starting new HTTP connection (1): smithi196.front.sepia.ceph.com:80
+urllib3.connectionpool: DEBUG: http://smithi196.front.sepia.ceph.com:80 "PUT /test-client.0-2txq2dyjghs0vdf-335 HTTP/1.1" 200 0
+botocore.parsers: DEBUG: Response headers: {'x-amz-request-id': 'tx00000e29af2294ab8b56c-0063354035-1157-default', 'Content-Length': '0', 'Date': 'Thu, 29 Sep 2022 06:50:29 GMT', 'Connection': 'Keep-Alive'}
+botocore.parsers: DEBUG: Response body:
+b''
+botocore.hooks: DEBUG: Event needs-retry.s3.CreateBucket: calling handler <botocore.retryhandler.RetryHandler object at 0x7fb6de6597c0>
+botocore.retryhandler: DEBUG: No retry needed.
+GET
+/test-client.0-2txq2dyjghs0vdf-335
+tagging=
+host:smithi196.front.sepia.ceph.com
+x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+x-amz-date:20220929T065029Z
+
+host;x-amz-content-sha256;x-amz-date
+e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
+botocore.auth: DEBUG: StringToSign:
+AWS4-HMAC-SHA256
+20220929T065029Z
+20220929/us-east-1/s3/aws4_request
+8a096d01796a8a6afca50c1bc3bc5c9098917c26a6dba7e752412ce31041c575
+botocore.auth: DEBUG: Signature:
+a58a94727b0c0d6d43e8783c91499ce9a9758260aa09a286524c0eb1bc4883d1
+botocore.hooks: DEBUG: Event request-created.s3.GetBucketTagging: calling handler <function add_retry_headers at 0x7fb6def031f0>
+botocore.endpoint: DEBUG: Sending http request: <AWSPreparedRequest stream_output=False, method=GET, url=http://smithi196.front.sepia.ceph.com:80/test-client.0-2txq2dyjghs0vdf-335?tagging, headers={'User-Agent': b'Boto3/1.24.82 Python/3.8.10 Linux/5.4.0-126-generic Botocore/1.27.82', 'X-Amz-Date': b'20220929T065029Z', 'X-Amz-Content-SHA256': b'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'Authorization': b'AWS4-HMAC-SHA256 Credential=EBIAVLDBAMGWPWFMDQHA/20220929/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a58a94727b0c0d6d43e8783c91499ce9a9758260aa09a286524c0eb1bc4883d1', 'amz-sdk-invocation-id': b'c4afeb5a-6a87-4e29-83ef-b96195038217', 'amz-sdk-request': b'attempt=1'}>
+urllib3.connectionpool: DEBUG: Starting new HTTP connection (1): smithi196.front.sepia.ceph.com:80
+urllib3.connectionpool: DEBUG: http://smithi196.front.sepia.ceph.com:80 "GET /test-client.0-2txq2dyjghs0vdf-335?tagging HTTP/1.1" 404 248
+botocore.parsers: DEBUG: Response headers: {'Content-Length': '248', 'x-amz-request-id': 'tx00000ebc589e4bcad8d86-0063354035-1157-default', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/xml', 'Date': 'Thu, 29 Sep 2022 06:50:29 GMT', 'Connection': 'Keep-Alive'}
+botocore.parsers: DEBUG: Response body:
+b'<?xml version="1.0" encoding="UTF-8"?><Error><Code>NoSuchTagSetError</Code><BucketName>test-client.0-2txq2dyjghs0vdf-335</BucketName><RequestId>tx00000ebc589e4bcad8d86-0063354035-1157-default</RequestId><HostId>1157-default-default</HostId></Error>'
+botocore.hooks: DEBUG: Event needs-retry.s3.GetBucketTagging: calling handler <botocore.retryhandler.RetryHandler object at 0x7fb6dec1ebe0>
+botocore.retryhandler: DEBUG: No retry needed.
+botocore.hooks: DEBUG: Event needs-retry.s3.GetBucketTagging: calling handler <bound method S3RegionRedirector.redirect_from_error of <botocore.utils.S3RegionRedirector object at 0x7fb6dec1eaf0>>
+--------------------- >> end captured logging << ---------------------]]></failure></testcase>
+</testsuite>
\ No newline at end of file
diff --git a/teuthology/util/scanner.py b/teuthology/util/scanner.py
new file mode 100644 (file)
index 0000000..1e140d6
--- /dev/null
@@ -0,0 +1,149 @@
+import logging
+import yaml
+from typing import Optional, Tuple
+from collections import defaultdict
+from lxml import etree
+
+log = logging.getLogger(__name__)
+
+
+class Scanner():
+    def __init__(self, remote=None) -> None:
+        self.summary_data = []
+        self.remote = remote
+
+    def _parse(self, file_content) -> Tuple[str, dict]:
+        """
+        This parses file_content and returns:
+        :returns: a message string 
+        :returns: data dictionary with additional info
+
+        Just an abstract method in Scanner class, 
+        to be defined in inherited classes. 
+        """
+        raise NotImplementedError
+    
+    def scan_file(self, path: str) -> Optional[str]:
+        if not path:
+            return None
+        try:
+            file = self.remote._sftp_open_file(path, 'r')
+            file_content = file.read()
+            txt, data = self._parse(file_content)
+            if data:
+                data["file_path"] = path
+                self.summary_data += [data]
+            file.close()
+            return txt
+        except Exception as exc:
+            log.error(str(exc))
+
+    def scan_all_files(self, path_regex: str) -> [str]:
+        """
+        Scans all files matching path_regex
+        and collect additional data in self.summary_data 
+
+        :param path_regex: Regex string to find all the files which have to be scanned. 
+                           Example: /path/to/dir/*.xml
+        """
+        (_, stdout, _) = self.remote.ssh.exec_command(f'ls -d {path_regex}', timeout=200)
+        
+        files = stdout.read().decode().split('\n')
+        
+        extracted_txts = []
+        for fpath in files:
+            txt = self.scan_file(fpath)
+            if txt:
+                extracted_txts += [txt]
+        return extracted_txts
+    
+    def write_summary(self, yaml_path: str) -> None:
+        """
+        Create yaml file locally 
+        with self.summary_data.
+        """
+        if self.summary_data and yaml_path:
+            with open(yaml_path, 'a') as f:
+                yaml.safe_dump(self.summary_data, f, default_flow_style=False)
+        else:
+            log.info("summary_data or yaml_file is empty!")
+
+
+class UnitTestScanner(Scanner):
+    def __init__(self, remote=None) -> None:
+        super().__init__(remote)
+
+    def _parse(self, file_content: str) -> Tuple[Optional[str], Optional[dict]]:
+        xml_tree = etree.fromstring(file_content)
+
+        failed_testcases = xml_tree.xpath('.//failure/.. | .//error/..')
+        if len(failed_testcases) == 0:
+            return None, None
+
+        exception_txt = ""
+        error_data = defaultdict(list)
+        for testcase in failed_testcases:
+            testcase_name = testcase.get("name", "test-name")
+            testcase_suitename = testcase.get("classname", "suite-name")
+            for child in testcase:
+                if child.tag in ['failure', 'error']:
+                    fault_kind = child.tag
+                    reason = child.get('message', 'No message found in xml output, check logs.')
+                    short_reason = reason[:200]
+                    error_data[testcase_suitename] += [{
+                            "kind": fault_kind, 
+                            "testcase": testcase_name,
+                            "message": reason,
+                        }]
+                    if not exception_txt:
+                        exception_txt = f'{fault_kind.upper()}: Test `{testcase_name}` of `{testcase_suitename}`. Reason: {short_reason}.'
+        
+        return exception_txt, { "failed_testsuites": dict(error_data), "num_of_failures": len(failed_testcases) }
+    
+    def scan_and_write(self, path_regex: str, summary_path: str) -> Optional[str]:
+        """
+        Scan all files matching 'path_regex'
+        and write summary in 'summary_path'.
+        """
+        try:
+            errors = self.scan_all_files(path_regex)
+            self.write_summary(summary_path)
+            if errors:
+                return errors[0]
+        except Exception as scanner_exc:
+            log.error(str(scanner_exc))
+
+
+class ValgrindScanner(Scanner):
+    def __init__(self, remote=None) -> None:
+        super().__init__(remote)
+
+    def _parse(self, file_content: str) -> Tuple[Optional[str], Optional[dict]]:
+        xml_tree = etree.fromstring(file_content)
+        if not xml_tree:
+            return None, None
+        
+        error_tree = xml_tree.find('error')
+        if error_tree is None:
+            return None, None
+        
+        error_data = {
+            "kind": error_tree.findtext("kind"),
+            "traceback": [],
+        }
+        for frame in error_tree.xpath("stack/frame"):
+            if len(error_data["traceback"]) >= 5:
+                break
+            curr_frame = {
+                "file": f"{frame.findtext('dir', '')}/{frame.findtext('file', '')}",
+                "line": frame.findtext("line", ''),
+                "function": frame.findtext("fn", ''),
+            }
+            error_data["traceback"].append(curr_frame)
+
+        traceback_functions = "\n".join(
+                frame.get("function", "N/A") 
+                for frame in error_data["traceback"][:3]
+            )
+        exception_text = f"valgrind error: {error_data['kind']}\n{traceback_functions}"
+        return exception_text, error_data