From 1d2fc3a2dc82b91c27de16aea4caeba0c908344e Mon Sep 17 00:00:00 2001 From: gilbn Date: Sat, 17 Sep 2022 14:43:24 +0200 Subject: [PATCH 1/6] Change dotnet wording. --- ci/template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/template.html b/ci/template.html index bfc627e..cdae991 100644 --- a/ci/template.html +++ b/ci/template.html @@ -474,7 +474,7 @@ {% if 'arm32' in container["tag"] and container["dotnet"] == true %}
-

Warning:.NET application. Service might not start on ARM32 with QEMU

+

Warning:May be a .NET app. Service might not start on ARM32 with QEMU

{% endif %}
From 446989887c1fd613bb43f1c3736e8a1ac8d931cb Mon Sep 17 00:00:00 2001 From: gilbn Date: Sat, 17 Sep 2022 16:53:13 +0200 Subject: [PATCH 2/6] Mute boto3 logging if debug, rename log file. Fix log formatting. --- ci/ci.py | 4 ++-- ci/logger.py | 15 ++++++--------- ci/template.html | 10 +++++----- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/ci/ci.py b/ci/ci.py index d3c23a0..9514569 100755 --- a/ci/ci.py +++ b/ci/ci.py @@ -305,7 +305,7 @@ class CI(SetEnvs): self.s3_client.upload_file(file_path, self.bucket, f'{latest_dir}/{object_name}', ExtraArgs=content_type) def log_upload(self) -> None: - """Upload debug.log to S3 + """Upload ci.log to S3 Raises: Exception: S3UploadFailedError @@ -313,7 +313,7 @@ class CI(SetEnvs): """ self.logger.info('Uploading logs') try: - self.upload_file("/debug.log", 'debug.log', {'ContentType': 'text/plain', 'ACL': 'public-read'}) + self.upload_file("/ci.log", 'ci.log', {'ContentType': 'text/plain', 'ACL': 'public-read'}) except (S3UploadFailedError, ClientError) as error: self.logger.exception('Upload Error: %s',error) diff --git a/ci/logger.py b/ci/logger.py index f4863ed..9003e2f 100644 --- a/ci/logger.py +++ b/ci/logger.py @@ -43,22 +43,19 @@ def configure_logging(log_level:str): # Console logging ch = logging.StreamHandler() - cf = CustomLogFormatter('%(asctime)-15s | (%(threadName)-9s) %(name)-43s | %(levelname)-8s | (%(module)s.%(funcName)s|line:%(lineno)d) | %(message)s |', '%d/%m/%Y %H:%M:%S') + cf = CustomLogFormatter('%(asctime)-15s | %(threadName)-17s | %(name)-10s | %(levelname)-8s | (%(module)s.%(funcName)s|line:%(lineno)d) | %(message)s |', '%d/%m/%Y %H:%M:%S') ch.setFormatter(cf) ch.setLevel(log_level) logger.addHandler(ch) # File logging - fh = TimedRotatingFileHandler(os.path.join(os.getcwd(),'debug.log'), when="midnight", interval=1, backupCount=7, delay=True, encoding='utf-8') - f = CustomLogFormatter('%(asctime)-15s | (%(threadName)-9s) %(name)-43s | %(levelname)-8s | (%(module)s.%(funcName)s|line:%(lineno)d) | %(message)s |', '%d/%m/%Y %H:%M:%S') + fh = TimedRotatingFileHandler(os.path.join(os.getcwd(),'ci.log'), when="midnight", interval=1, backupCount=7, delay=True, encoding='utf-8') + f = CustomLogFormatter('%(asctime)-15s | %(threadName)-17s | %(name)-10s | %(levelname)-8s | (%(module)s.%(funcName)s|line:%(lineno)d) | %(message)s |', '%d/%m/%Y %H:%M:%S') fh.setFormatter(f) fh.setLevel(log_level) logger.addHandler(fh) - + logging.info('Operating system: %s', platform.platform()) + logging.info('Python version: %s', platform.python_version()) if log_level.upper() == "DEBUG": - logging.getLogger("spam").setLevel(logging.DEBUG) # Change external loggers to debug if necessary - logging.debug('Operating system: %s', platform.platform()) - logging.debug('Python version: %s', platform.python_version()) - else: - logging.getLogger("ham").setLevel(logging.CRITICAL) # Set external loggers to a level if necessary + logging.getLogger("botocore").setLevel(logging.WARNING) # Mute boto3 logging output diff --git a/ci/template.html b/ci/template.html index cdae991..16b2ab0 100644 --- a/ci/template.html +++ b/ci/template.html @@ -385,7 +385,7 @@ cursor: pointer; } - #debug_logs { + #logs { overflow: auto; } @@ -506,18 +506,18 @@
-

Python debug logs

+

Python logs

Expand -

+        

       
From 99f0c746ce9df1c4533705eb7b84e92e01374e4b Mon Sep 17 00:00:00 2001 From: gilbn Date: Sat, 17 Sep 2022 17:02:45 +0200 Subject: [PATCH 3/6] move everything in the main scope into the try, fix table overflow --- ci/template.html | 3 +-- test_build.py | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ci/template.html b/ci/template.html index 16b2ab0..78a042c 100644 --- a/ci/template.html +++ b/ci/template.html @@ -306,7 +306,7 @@ main { display: flex; justify-content: center; - align-items: flex-start; + align-items: stretch; flex-wrap: wrap; max-width: 100%; } @@ -337,7 +337,6 @@ } .table-container { padding-top: 1rem; - overflow: auto; } .styled-table { diff --git a/test_build.py b/test_build.py index 56291ce..f64d39f 100644 --- a/test_build.py +++ b/test_build.py @@ -19,12 +19,12 @@ def run_test(): if __name__ == '__main__': - log_level = os.environ.get("CI_LOG_LEVEL","DEBUG") - configure_logging(log_level) - import logging - logger = logging.getLogger(__name__) - ci = CI() try: + log_level = os.environ.get("CI_LOG_LEVEL","INFO") + configure_logging(log_level) + import logging + logger = logging.getLogger(__name__) + ci = CI() run_test() except Exception as err: logger.exception("%s\nI Can't Believe You've Done This",err) From f5ec3c47c058f57acff266923c5cea2598c3d137 Mon Sep 17 00:00:00 2001 From: gilbn Date: Sat, 17 Sep 2022 17:03:49 +0200 Subject: [PATCH 4/6] remove old ci files not used anymore --- ci/old_ci.py | 375 -------------------------------------------- ci/results.template | 42 ----- 2 files changed, 417 deletions(-) delete mode 100644 ci/old_ci.py delete mode 100644 ci/results.template diff --git a/ci/old_ci.py b/ci/old_ci.py deleted file mode 100644 index 11f2a50..0000000 --- a/ci/old_ci.py +++ /dev/null @@ -1,375 +0,0 @@ -#!/usr/bin/env python - -import os -import boto3 -import time -import sys -import docker -import requests -import anybadge -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry -from multiprocessing.pool import Pool -from selenium import webdriver -from selenium.common.exceptions import ErrorInResponseException,TimeoutException -from jinja2 import Template -client = docker.from_env() -session = boto3.session.Session() -reload(sys) -sys.setdefaultencoding('utf8') - -# Global Vars -global report_status -global report_tests -global report_containers -report_tests = [] -report_containers = [] -report_status = 'PASS' - -############# -# Functions # -############# - -# If the tests cannot even be run just fail the job -def core_fail(message): - print(message) - sys.exit(1) - -# Convert env input to dictionary -def convert_env(vars): - global dockerenv - dockerenv = {} - try: - if '|' in vars: - for varpair in vars.split('|'): - var = varpair.split('=') - dockerenv[var[0]] = var[1] - else: - var = vars.split('=') - dockerenv[var[0]] = var[1] - except Exception as error: - core_fail(str(error)) - -# Update global variables from threaded testing process -def update_globals(data): - global report_status - for (tests,containers,status) in data: - for test in tests: - report_tests.append(test) - for container in containers: - report_containers.append(container) - if status == 'FAIL': - report_status = 'FAIL' - -# Set the optional parameters -global webauth -global webpath -global dockerenv -global region -global bucket -global screenshot -global port -global ssl -global testdelay -try: - webauth = os.environ["WEB_AUTH"] -except KeyError: - webauth = 'user:password' -try: - webpath = os.environ["WEB_PATH"] -except KeyError: - webpath = '' -try: - convert_env(os.environ["DOCKER_ENV"]) -except KeyError: - dockerenv = {} -try: - region = os.environ["S3_REGION"] -except KeyError: - region = 'us-east-1' -try: - bucket = os.environ["S3_BUCKET"] -except KeyError: - bucket = 'ci-tests.linuxserver.io' -try: - screenshot = os.environ["WEB_SCREENSHOT"] -except KeyError: - screenshot = 'false' -try: - port = os.environ["PORT"] -except KeyError: - port = '80' -try: - ssl = os.environ["SSL"] -except KeyError: - ssl = 'false' -try: - testdelay = os.environ["DELAY_START"] -except KeyError: - testdelay = '5' - -# Make sure all needed env variables are set -def check_env(): - try: - global image - global tags - global meta_tag - global base - global S3_key - global S3_secret - image = os.environ["IMAGE"] - base = os.environ["BASE"] - S3_key = os.environ["ACCESS_KEY"] - S3_secret = os.environ["SECRET_KEY"] - meta_tag = os.environ["META_TAG"] - tags_env = os.environ["TAGS"] - tags = [] - if '|' in tags_env: - for tag in tags_env.split('|'): - tags.append(tag) - else: - tags.append(tags_env) - except KeyError as error: - core_fail(str(error) + ' is not set in ENV') - -# Create output path -def create_dir(): - global outdir - outdir = os.path.dirname(os.path.realpath(__file__)) + '/output/' + image + '/' + meta_tag + '/' - try: - os.stat(outdir) - except: - os.makedirs(outdir) - -# Main container test logic -def container_test(tag): - # Vars for the threaded process - report_tests = [] - report_containers = [] - report_status = 'PASS' - # End the test with as much info as we have - def endtest(container,report_tests,report_containers,report_status,tag,build_version,packages): - logblob = container.logs().decode("utf-8") - container.remove(force='true') - # Add the info to the report - report_containers.append({ - "tag":tag, - "logs":logblob, - "sysinfo":packages, - "build_version":build_version - }) - return (report_tests,report_containers,report_status) - # Start the container - print('Starting ' + tag) - container = client.containers.run(image + ':' + tag, - detach=True, - environment=dockerenv) - # Watch the logs for no more than 5 minutes - t_end = time.time() + 60 * 5 - logsfound = False - while time.time() < t_end: - try: - logblob = container.logs().decode("utf-8") - if '[services.d] done.' in logblob: - logsfound = True - break - time.sleep(1) - except Exception as error: - print('Startup failed for ' + tag) - report_tests.append(['Startup ' + tag,'FAIL INIT NOT FINISHED']) - report_status = 'FAIL' - (report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,'ERROR','ERROR') - return (report_tests,report_containers,report_status) - # Grab build version - try: - build_version = container.attrs["Config"]["Labels"]["build_version"] - report_tests.append(['Get Build Version ' + tag,'PASS']) - except Exception as error: - build_version = 'ERROR' - report_tests.append(['Get Build Version ' + tag,'FAIL']) - report_status = 'FAIL' - (report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,build_version,'ERROR') - return (report_tests,report_containers,report_status) - # Check if the startup marker was found in the logs during the 2 minute spinup - if logsfound == True: - print('Startup completed for ' + tag) - report_tests.append(['Startup ' + tag,'PASS']) - elif logsfound == False: - print('Startup failed for ' + tag) - report_tests.append(['Startup ' + tag,'FAIL INIT NOT FINISHED']) - report_status = 'FAIL' - (report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,build_version,'ERROR') - return (report_tests,report_containers,report_status) - # Dump package information - print('Dumping package info for ' + tag) - if base == 'alpine': - command = 'apk info -v' - elif base == 'debian' or base == 'ubuntu': - command = 'apt list' - elif base == 'fedora': - command = 'rpm -qa' - elif base == 'arch': - command = 'pacman -Q' - try: - info = container.exec_run(command) - packages = info[1].decode("utf-8") - report_tests.append(['Dump Versions ' + tag,'PASS']) - print('Got Package info for ' + tag) - except Exception as error: - packages = 'ERROR' - print(str(error)) - report_tests.append(['Dump Versions ' + tag,'FAIL']) - report_status = 'FAIL' - (report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,build_version,packages) - return (report_tests,report_containers,report_status) - # Sleep for the user specified amount of time - time.sleep(int(testdelay)) - # Screenshot web interface and check connectivity - if screenshot == 'true': - # Take a screenshot - if ssl == 'true': - proto = 'https://' - else: - proto = 'http://' - container.reload() - ip = container.attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"] - endpoint = proto + webauth + '@' + ip + ':' + port + webpath - print('Taking screenshot of ' + tag + ' at ' + endpoint) - testercontainer = client.containers.run('lsiodev/tester:latest', - shm_size='1G', - detach=True, - environment={'URL': endpoint}) - time.sleep(30) - testercontainer.reload() - testerip = testercontainer.attrs["NetworkSettings"]["Networks"]["bridge"]["IPAddress"] - testerendpoint = "http://" + testerip + ":3000" - try: - # Selenium webdriver options - chrome_options = webdriver.ChromeOptions() - chrome_options.add_argument('--no-sandbox') - chrome_options.add_argument('--headless') - chrome_options.add_argument('--disable-gpu') - chrome_options.add_argument('--window-size=1920x1080') - driver = webdriver.Chrome(options=chrome_options) - driver.set_page_load_timeout(60) - session = requests.Session() - retries = Retry(total=4, backoff_factor=2, status_forcelist=[ 502, 503, 504 ]) - session.mount(proto, HTTPAdapter(max_retries=retries)) - session.get(testerendpoint) - driver.get(testerendpoint) - time.sleep(15) - driver.get_screenshot_as_file(outdir + tag + '.png') - report_tests.append(['Screenshot ' + tag,'PASS']) - # Quit selenium webdriver - driver.quit() - except (requests.Timeout, requests.ConnectionError, KeyError) as e: - report_tests.append(['Screenshot ' + tag,'FAIL CONNECTION ERROR']) - except ErrorInResponseException as error: - report_tests.append(['Screenshot ' + tag,'FAIL SERVER ERROR']) - except TimeoutException as error: - report_tests.append(['Screenshot ' + tag,'FAIL TIMEOUT']) - except WebDriverException as error: - report_tests.append(['Screenshot ' + tag,'FAIL UNKNOWN']) - testercontainer.remove(force='true') - # If all info is present end test - (report_tests,report_containers,report_status) = endtest(container,report_tests,report_containers,report_status,tag,build_version,packages) - return (report_tests,report_containers,report_status) - -# Render the markdown file for upload -def report_render(): - print('Rendering Report') - with open(os.path.dirname(os.path.realpath(__file__)) + '/results.template') as file_: - template = Template(file_.read()) - markdown = template.render( - report_tests=report_tests, - report_containers=report_containers, - report_status=report_status, - meta_tag=meta_tag, - image=image, - bucket=bucket, - region=region, - screenshot=screenshot) - with open(outdir + 'report.md', 'w') as f: - f.write(markdown) - -# Render the badge file for upload -def badge_render(): - try: - badge = anybadge.Badge('CI', report_status, thresholds={'PASS': 'green', 'FAIL': 'red'}) - badge.write_badge(outdir + 'badge.svg') - with open(outdir + 'ci-status.yml', 'w') as f: - f.write('CI: "' + report_status + '"') - except Exception as error: - print(error) - -# Upload report to S3 -def report_upload(): - print('Uploading Report') - destination_dir = image + '/' + meta_tag + '/' - latest_dir = image + '/latest/' - s3 = session.client( - 's3', - region_name=region, - aws_access_key_id=S3_key, - aws_secret_access_key=S3_secret) - # Index file upload - index_file = os.path.dirname(os.path.realpath(__file__)) + '/index.html' - try: - s3.upload_file( - index_file, - bucket, - destination_dir + 'index.html', - ExtraArgs={'ContentType': "text/html", 'ACL': "public-read"}) - s3.upload_file( - index_file, - bucket, - latest_dir + 'index.html', - ExtraArgs={'ContentType': "text/html", 'ACL': "public-read"}) - except Exception as error: - core_fail('Upload Error ' + str(error)) - # Loop for all others - for filename in os.listdir(outdir): - time.sleep(0.5) - # Set content types for files - if filename.lower().endswith('.svg'): - CT = 'image/svg+xml' - elif filename.lower().endswith('.png'): - CT = 'image/png' - elif filename.lower().endswith('.md'): - CT = 'text/markdown' - elif filename.lower().endswith('.yml'): - CT = 'text/yaml' - try: - s3.upload_file( - outdir + filename, - bucket, - destination_dir + filename, - ExtraArgs={'ContentType': CT,'ACL': "public-read",'CacheControl': 'no-cache'}) - s3.upload_file( - outdir + filename, - bucket, - latest_dir + filename, - ExtraArgs={'ContentType': CT,'ACL': "public-read",'CacheControl': 'no-cache'}) - except Exception as error: - core_fail('Upload Error ' + str(error)) - - -################## -# Test Run Logic # -################## -check_env() -create_dir() -# Run through all the tags -pool=Pool(processes=3) -r = pool.map_async(container_test, tags, callback=update_globals) -r.wait() -report_render() -badge_render() -report_upload() -# Exit based on test results -if report_status == 'PASS': - print('Tests Passed exiting 0') - sys.exit(0) -elif report_status == 'FAIL': - print('Tests Failed exiting 1') - sys.exit(1) diff --git a/ci/results.template b/ci/results.template deleted file mode 100644 index e9d4a8f..0000000 --- a/ci/results.template +++ /dev/null @@ -1,42 +0,0 @@ -# Test Results {{ image }}:{{ meta_tag }} - -## Cumulative: {{ report_status }} - -| Test | Result | -| ----------------------- | --- |{% for test in report_tests %} -| {{ test[0] }} | {{ test[1] }} |{% endfor %} - -
-{% for container in report_containers %} -
-## {{ image }}:{{ container["tag"] }} -{% if screenshot == 'true' %} -[![{{ container["tag"] }}]({{ container["tag"] }}.png =600x*)]({{ container["tag"] }}.png) -{% endif %} -### Build Version: {{ container["build_version"] }} - -### Logs - -
Expand -

- -``` -{{ container["logs"] }} -``` -

-
- -### Package info - -
Expand -

- -``` -{{ container["sysinfo"] }} -``` - -

-
-
-{% endfor %} -
From 28ba4914dc308d7f4a099022ac2fd77d4d7d96ca Mon Sep 17 00:00:00 2001 From: gilbn Date: Sat, 17 Sep 2022 21:03:45 +0200 Subject: [PATCH 5/6] Don't exit with a successful termination if test failes --- ci/ci.py | 13 ++++++++----- test_build.py | 6 ++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ci/ci.py b/ci/ci.py index 9514569..806b594 100755 --- a/ci/ci.py +++ b/ci/ci.py @@ -59,7 +59,7 @@ class SetEnvs(): env_dict["S6_VERBOSITY"] = self.s6_verbosity except Exception as error: self.logger.exception(error) - raise Exception(f"Failed converting DOCKER_ENV: {envs} to dictionary") from error + raise CIError(f"Failed converting DOCKER_ENV: {envs} to dictionary") from error return env_dict @@ -74,7 +74,7 @@ class SetEnvs(): self.tags_env = os.environ['TAGS'] except KeyError as error: self.logger.exception("Key %s is not set in ENV!", error) - raise Exception(f'Key {error} is not set in ENV!') from error + raise CIError(f'Key {error} is not set in ENV!') from error class CI(SetEnvs): @@ -190,7 +190,7 @@ class CI(SetEnvs): self.tag_report_tests[tag].append(['Container startup', 'PASS', '-']) self.logger.info('Container startup %s: PASS', tag) else: - self.logger.warning('Container startup failed for %s', tag) + self.logger.error('Container startup failed for %s', tag) self.tag_report_tests[tag].append(['Container startup', 'FAIL','INIT NOT FINISHED']) self.logger.error('Container startup %s: FAIL - INIT NOT FINISHED', tag) self.report_status = 'FAIL' @@ -274,7 +274,7 @@ class CI(SetEnvs): except (S3UploadFailedError, ValueError, ClientError) as error: self.logger.exception('Upload Error: %s',error) self.log_upload() - raise Exception(f'Upload Error: {error}') from error + raise CIError(f'Upload Error: {error}') from error # Loop through files in outdir and upload for filename in os.listdir(self.outdir): @@ -286,7 +286,7 @@ class CI(SetEnvs): except (S3UploadFailedError, ValueError, ClientError) as error: self.logger.exception('Upload Error: %s',error) self.log_upload() - raise Exception(f'Upload Error: {error}') from error + raise CIError(f'Upload Error: {error}') from error self.logger.info('Report available on https://ci-tests.linuxserver.io/%s/index.html', f'{self.image}/{self.meta_tag}') @@ -407,3 +407,6 @@ class CI(SetEnvs): driver = webdriver.Chrome(options=chrome_options) driver.set_page_load_timeout(60) return driver + +class CIError(Exception): + pass \ No newline at end of file diff --git a/test_build.py b/test_build.py index f64d39f..5d30233 100644 --- a/test_build.py +++ b/test_build.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 import os -from ci.ci import CI +from ci.ci import CI, CIError from ci.logger import configure_logging def run_test(): @@ -16,6 +16,7 @@ def run_test(): return logger.error('Tests FAILED') ci.log_upload() + raise CIError('CI Tests did not PASS!') if __name__ == '__main__': @@ -27,4 +28,5 @@ if __name__ == '__main__': ci = CI() run_test() except Exception as err: - logger.exception("%s\nI Can't Believe You've Done This",err) + logger.exception(err) + raise CIError("I Can't Believe You've Done This!") from err From dc62e6d080e52d1b35aa4b0aba00171aee02166c Mon Sep 17 00:00:00 2001 From: gilbn Date: Sun, 18 Sep 2022 00:20:34 +0200 Subject: [PATCH 6/6] section header text overflow fix --- ci/template.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/template.html b/ci/template.html index 78a042c..77f411d 100644 --- a/ci/template.html +++ b/ci/template.html @@ -243,6 +243,8 @@ .section-header { border-radius: 10px 10px 0 0; + overflow-wrap: break-word; + } .section-header-h2 {