Tim Andersson has proposed merging ~andersson123/autopkgtest-cloud:lint_autopkgtest-web into autopkgtest-cloud:master.
Commit message: Lint only autopkgtest-web directory for easier testing Requested reviews: Canonical's Ubuntu QA (canonical-ubuntu-qa) For more details, see: https://code.launchpad.net/~andersson123/autopkgtest-cloud/+git/autopkgtest-cloud/+merge/444162 Lint only autopkgtest-web directory for easier testing -- Your team Canonical's Ubuntu QA is requested to review the proposed merge of ~andersson123/autopkgtest-cloud:lint_autopkgtest-web into autopkgtest-cloud:master.
diff --git a/.launchpad.yaml b/.launchpad.yaml index 6dca924..4f6669e 100755 --- a/.launchpad.yaml +++ b/.launchpad.yaml @@ -6,4 +6,8 @@ jobs: series: focal architectures: amd64 packages: [pylint, python3, shellcheck, yamllint] +<<<<<<< .launchpad.yaml run: ./ci/lint_test +======= + run: ./ci/lint_test -v +>>>>>>> .launchpad.yaml diff --git a/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/create-test-instances b/charms/focal/autopkgtest-cloud-worker/autopkgtest-cloud/tools/create-test-instances old mode 100755 new mode 100644 diff --git a/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py b/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py index fc87317..a6efd32 100644 --- a/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py +++ b/charms/focal/autopkgtest-web/reactive/autopkgtest_web.py @@ -1,3 +1,14 @@ +""" +Web app for autopkgtest-cloud +""" +#pylint: disable=import-error, possibly-unused-variable, consider-using-f-string, invalid-name, bad-option-value +from textwrap import dedent +import glob +import os +import shutil +import subprocess + +from charmhelpers.core.hookenv import charm_dir, config from charms.layer import status from charms.reactive import ( when, @@ -6,16 +17,8 @@ from charms.reactive import ( when_not, set_flag, clear_flag, - hook, ) -from charmhelpers.core.hookenv import charm_dir, config -from textwrap import dedent - -import glob -import os -import shutil -import subprocess AUTOPKGTEST_CLOUD_CONF = os.path.expanduser("~ubuntu/autopkgtest-cloud.conf") GITHUB_SECRETS_PATH = os.path.expanduser("~ubuntu/github-secrets.json") @@ -29,6 +32,7 @@ SWIFT_WEB_CREDENTIALS_PATH = os.path.expanduser( @when_not("autopkgtest-web.autopkgtest_web_symlinked") def symlink_autopkgtest_cloud(): + """Creates a symbolic link to webcontrol dir in autopkgtest-cloud repository""" try: autopkgtest_cloud = os.path.join(charm_dir(), "webcontrol") os.symlink(autopkgtest_cloud, os.path.expanduser("~ubuntu/webcontrol")) @@ -40,6 +44,7 @@ def symlink_autopkgtest_cloud(): @when("amqp.connected") @when_not("amqp.available") def setup_rabbitmq(rabbitmq): + """Setup access to rabbitmq queueing server""" rabbitmq.request_access("webcontrol", "/") status.waiting("Waiting on RabbitMQ to configure vhost") @@ -50,14 +55,15 @@ def setup_rabbitmq(rabbitmq): "config.set.hostname", ) def write_autopkgtest_cloud_conf(rabbitmq): + """Sets up config for local and remote databases""" swiftinternal = config().get("storage-url-internal") hostname = config().get("hostname") rabbituser = rabbitmq.username() rabbitpassword = rabbitmq.password() rabbithost = rabbitmq.private_address() clear_flag("autopkgtest-web.config-written") - with open(f"{AUTOPKGTEST_CLOUD_CONF}.new", "w") as f: - f.write( + with open(f"{AUTOPKGTEST_CLOUD_CONF}.new", "w", encoding='utf-8') as config_file: + config_file.write( dedent( """\ [web] @@ -68,7 +74,7 @@ def write_autopkgtest_cloud_conf(rabbitmq): [amqp] uri=amqp://{rabbituser}:{rabbitpassword}@{rabbithost}""".format( - **locals() + **locals() ) ) ) @@ -81,6 +87,7 @@ def write_autopkgtest_cloud_conf(rabbitmq): "autopkgtest-web.config-written", ) def set_up_systemd_units(): + """Sets up systemd units for autopkgtest-web services""" any_changed = False for unit in glob.glob(os.path.join(charm_dir(), "units", "*")): base = os.path.basename(unit) @@ -111,6 +118,7 @@ def set_up_systemd_units(): ) @when_not("autopkgtest-web.website-initially-configured") def initially_configure_website(website): + """Sets initial variables for autopkgtest-web website""" set_up_web_config(website) @@ -127,8 +135,9 @@ def initially_configure_website(website): "autopkgtest-web.website-initially-configured" ) def set_up_web_config(apache): + """Sets up proxies and filepaths for website""" webcontrol_dir = os.path.join(charm_dir(), "webcontrol") - sn = config().get("hostname") + ser_name = config().get("hostname") https_proxy = config().get("https-proxy") no_proxy = config().get("no-proxy") @@ -154,10 +163,10 @@ def set_up_web_config(apache): pass if https_proxy: - with open(https_proxy_env, "w") as f: - f.write("https_proxy={}".format(https_proxy)) - with open(https_proxy_apache, "w") as f: - f.write("SetEnv https_proxy {}".format(https_proxy)) + with open(https_proxy_env, "w", encoding='utf-8') as https_proxy_file: + https_proxy_file.write("https_proxy={}".format(https_proxy)) + with open(https_proxy_apache, "w", encoding='utf-8') as https_proxy_apache_file: + https_proxy_apache_file.write("SetEnv https_proxy {}".format(https_proxy)) no_proxy_env = os.path.join(environment_d, "no_proxy.conf") no_proxy_apache = os.path.join(conf_enabled, "no_proxy.conf") @@ -172,12 +181,12 @@ def set_up_web_config(apache): pass if no_proxy: - with open(no_proxy_env, "w") as f: + with open(no_proxy_env, "w", encoding='utf-8') as f: f.write("no_proxy={}".format(no_proxy)) - with open(no_proxy_apache, "w") as f: + with open(no_proxy_apache, "w", encoding='utf-8') as f: f.write("SetEnv no_proxy {}".format(no_proxy)) - server_name = "ServerName {}".format(sn) if sn else "" + server_name = "ServerName {}".format(ser_name) if ser_name else "" apache.send_site_config( dedent( """\ @@ -205,7 +214,7 @@ def set_up_web_config(apache): {server_name} </VirtualHost> """.format( - **locals() + **locals() ) ) ) @@ -216,9 +225,10 @@ def set_up_web_config(apache): @when_all("config.changed.github-secrets", "config.set.github-secrets") def write_github_secrets(): + """Writes github secrets to file""" github_secrets = config().get("github-secrets") - with open(GITHUB_SECRETS_PATH, "w") as f: + with open(GITHUB_SECRETS_PATH, "w", encoding='utf-8') as f: f.write(github_secrets) try: @@ -232,6 +242,7 @@ def write_github_secrets(): @when_not("config.set.github-secrets") def clear_github_secrets(): + """Removes symlink to github secrets file""" try: os.unlink(GITHUB_SECRETS_PATH) except FileNotFoundError: @@ -246,9 +257,10 @@ def clear_github_secrets(): @when_all("config.changed.swift-web-credentials", "config.set.swift-web-credentials") def write_swift_web_credentials(): + """Writes swift credentials to file""" swift_credentials = config().get("swift-web-credentials") - with open(SWIFT_WEB_CREDENTIALS_PATH, "w") as f: + with open(SWIFT_WEB_CREDENTIALS_PATH, "w", encoding='utf-8') as f: f.write(swift_credentials) try: @@ -262,6 +274,7 @@ def write_swift_web_credentials(): @when_not("config.set.swift-web-credentials") def clear_swift_web_credentials(): + """Removes symlink to swift web creds file""" try: os.unlink(SWIFT_WEB_CREDENTIALS_PATH) except FileNotFoundError: @@ -278,9 +291,10 @@ def clear_swift_web_credentials(): "config.set.github-status-credentials", ) def write_github_status_credentials(): + """Writes github status creds to file and symlinks them""" github_status_credentials = config().get("github-status-credentials") - with open(GITHUB_STATUS_CREDENTIALS_PATH, "w") as f: + with open(GITHUB_STATUS_CREDENTIALS_PATH, "w", encoding='utf-8') as f: f.write(github_status_credentials) try: @@ -294,6 +308,7 @@ def write_github_status_credentials(): @when_not("config.set.github-status-credentials") def clear_github_status_credentials(): + """Removes symlink to github status credentials""" try: os.unlink(GITHUB_STATUS_CREDENTIALS_PATH) except FileNotFoundError: @@ -309,6 +324,7 @@ def clear_github_status_credentials(): @when_not("autopkgtest-web.bootstrap-symlinked") def symlink_bootstrap(): + """Symlinks to bootstrap file""" try: os.symlink( os.path.join( @@ -323,7 +339,8 @@ def symlink_bootstrap(): @when_not("autopkgtest-web.runtime-dir-created") def make_runtime_tmpfiles(): - with open("/etc/tmpfiles.d/autopkgtest-web-runtime.conf", "w") as r: + """Makes all of the necessary tmp files for autopkgtest-web""" + with open("/etc/tmpfiles.d/autopkgtest-web-runtime.conf", "w", encoding='utf-8') as r: r.write("D %t/autopkgtest_webcontrol 0755 www-data www-data\n") subprocess.check_call(["systemd-tmpfiles", "--create"]) set_flag("autopkgtest-web.runtime-dir-created") @@ -331,6 +348,7 @@ def make_runtime_tmpfiles(): @when_not("autopkgtest-web.running-json-symlinked") def symlink_running(): + """Symlinks to json files""" try: os.symlink( os.path.join( @@ -345,6 +363,7 @@ def symlink_running(): @when_not("autopkgtest-web.public-db-symlinked") def symlink_public_db(): + """Creates symlink to public database on host""" try: publicdir = os.path.expanduser("~ubuntu/public/") os.makedirs(publicdir) @@ -361,12 +380,14 @@ def symlink_public_db(): @when("leadership.is_leader") @when_not("autopkgtest-cloud.leadership_flag_written") def write_leadership_flag(): - with open("/run/autopkgtest-web-is-leader", "w") as _: + """Sets juju leadership status""" + with open("/run/autopkgtest-web-is-leader", "w", encoding='utf-8') as _: set_flag("autopkgtest-cloud.leadership_flag_written") @when_not("leadership.is_leader") @when("autopkgtest-cloud.leadership_flag_written") def clear_leadership_flag(): + """Clears juju leadership""" os.unlink("/run/autopkgtest-web-is-leader") clear_flag("autopkgtest-cloud.leadership_flag_written") diff --git a/charms/focal/autopkgtest-web/webcontrol/amqp-status-collector b/charms/focal/autopkgtest-web/webcontrol/amqp-status-collector index 66a9ed4..514bac4 100755 --- a/charms/focal/autopkgtest-web/webcontrol/amqp-status-collector +++ b/charms/focal/autopkgtest-web/webcontrol/amqp-status-collector @@ -1,6 +1,9 @@ #!/usr/bin/python3 -# Pick up running tests, their status and logtail from the "teststatus" fanout -# queue, and regularly write it into /run/running.json +#pylint: disable=invalid-name, import-error, too-many-function-args +''' +Pick up running tests, their status and logtail from the "teststatus" fanout +queue, and regularly write it into /run/running.json +''' import os import json @@ -17,11 +20,11 @@ running_name = os.path.join(os.path.sep, 'run', 'amqp-status-collector', 'running.json') -running_name_new = "{}.new".format(running_name) +running_name_new = f"{running_name}.new" # package -> runhash -> release -> arch -> (params, duration, logtail) running_tests = {} -last_update = 0 +LAST_UPDATE = 0 def amqp_connect(): @@ -33,19 +36,17 @@ def amqp_connect(): parts = urllib.parse.urlsplit(amqp_uri, allow_fragments=False) amqp_con = amqp.Connection(parts.hostname, userid=parts.username, password=parts.password) - logging.info('Connected to AMQP server at %s@%s' % (parts.username, parts.hostname)) + logging.info('Connected to AMQP server at %s@%s', parts.username, parts.hostname) return amqp_con -def update_output(amqp_channel, force_update=False): +def update_output(force_update=False): '''Update report''' - global last_update - # update at most every 10 s now = time.time() - if not force_update and now - last_update < 10: + if not force_update and now - LAST_UPDATE < 10: return with open(running_name_new, 'w', encoding='utf-8') as f: @@ -64,12 +65,13 @@ def process_message(msg): runhash = '' params = info.get('params', {}) for p in sorted(params): - runhash += '%s_%s;' % (p, params[p]) + runhash += f'{p}_{params[p]};' if info['running']: running_tests.setdefault(info['package'], {}).setdefault( runhash, {}).setdefault( - info['release'], {})[info['architecture']] = (params, info.get('duration', 0), info['logtail']) + info['release'], {})[info['architecture']] = \ + (params, info.get('duration', 0), info['logtail']) else: try: del running_tests[info['package']][runhash][info['release']][info['architecture']] @@ -93,15 +95,15 @@ def process_message(msg): logging.basicConfig(level=('DEBUG' in os.environ and logging.DEBUG or logging.INFO)) -amqp_con = amqp_connect() -status_ch = amqp_con.channel() +amqp_connection = amqp_connect() +status_ch = amqp_connection.channel() status_ch.access_request('/data', active=True, read=True, write=False) status_ch.exchange_declare(exchange_name, 'fanout', durable=False, auto_delete=True) -queue_name = 'running-listener-%s' % socket.getfqdn() +queue_name = f'running-listener-{socket.getfqdn}' status_ch.queue_declare(queue_name, durable=False, auto_delete=True) status_ch.queue_bind(queue_name, exchange_name, queue_name) -logging.info('Listening to requests on %s' % queue_name) +logging.info('Listening to requests on %s', queue_name) status_ch.basic_consume('', callback=process_message, no_ack=True) while status_ch.callbacks: status_ch.wait() diff --git a/charms/focal/autopkgtest-web/webcontrol/cache-amqp b/charms/focal/autopkgtest-web/webcontrol/cache-amqp index c32ea40..4804aba 100755 --- a/charms/focal/autopkgtest-web/webcontrol/cache-amqp +++ b/charms/focal/autopkgtest-web/webcontrol/cache-amqp @@ -1,4 +1,7 @@ #!/usr/bin/python3 +#pylint: disable=invalid-name, import-error, consider-using-f-string, too-many-locals, bad-option-value + +"""Handles the amqp cache""" import argparse import configparser @@ -18,6 +21,7 @@ AMQP_CONTEXTS = ["ubuntu", "huge", "ppa", "upstream"] class AutopkgtestQueueContents: + """Class to handle and store the contents of the autopkgtest queue""" def __init__(self, amqp_uri, database): assert amqp_uri is not None assert database is not None @@ -42,7 +46,7 @@ class AutopkgtestQueueContents: self.amqp_channel.queue_declare( queue_name, durable=True, passive=True ) - logger.info(f"Semaphore queue '{queue_name}' exists") + logger.info("Semaphore queue %s exists", queue_name) except AMQPChannelException as e: (code, _, _, _) = e.args if code != 404: @@ -52,7 +56,7 @@ class AutopkgtestQueueContents: self.amqp_channel = self.amqp_con.channel() # queue does not exist, create it logger.info( - f"Semaphore queue {queue_name} does not exist, initialising..." + "Semaphore queue %s does not exist, initialising...", queue_name ) self.amqp_channel.queue_declare( queue_name, durable=True @@ -62,9 +66,10 @@ class AutopkgtestQueueContents: routing_key=queue_name, ) else: # not the leader - logging.error( - "We are not the leader, and there is no semaphore queue yet, we can't do anything - exiting." - ) + logging.error("%s%s", + "We are not the leader, and there is no semaphore ", + "queue yet, we can't do anything - exiting." + ) sys.exit(0) @property @@ -82,7 +87,7 @@ class AutopkgtestQueueContents: releases.append(row[0]) for r in releases: for row in db_con.execute( - "SELECT DISTINCT arch from test WHERE release=?", (r,) + "SELECT DISTINCT arch from test WHERE release=?", (r,) ): release_arches.setdefault(r, []).append(row[0]) return release_arches @@ -122,7 +127,6 @@ class AutopkgtestQueueContents: res.append(r) except (ValueError, IndexError): logging.error('Received invalid request format "%s"', r) - return return res def get_queue_contents(self): @@ -142,7 +146,8 @@ class AutopkgtestQueueContents: f"semaphore-{context}-{release}-{arch}" ) logging.info( - f"Trying to lock semaphore queue {semaphore_queue_name}..." + "Trying to lock semaphore queue %s...", + semaphore_queue_name ) r = None while r is None: @@ -177,7 +182,8 @@ class AutopkgtestQueueContents: } all_arches.add(arch) logging.debug( - f"Releasing semaphore lock {semaphore_queue_name}" + "Releasing semaphore lock %s", + semaphore_queue_name ) channel.basic_reject(r.delivery_tag, True) @@ -233,22 +239,22 @@ if __name__ == "__main__": logger.setLevel(logging.INFO) try: - amqp_uri = cp["amqp"]["uri"] + amqpuri = cp["amqp"]["uri"] except KeyError: print("No AMQP URI found", file=sys.stderr) sys.exit(1) try: - database = cp["web"]["database_ro"] + db = cp["web"]["db_ro"] except KeyError: - print("No database found", file=sys.stderr) + print("No db found", file=sys.stderr) sys.exit(1) - aq = AutopkgtestQueueContents(amqp_uri, database) + aq = AutopkgtestQueueContents(amqpuri, db) queue_contents = aq.get_queue_contents() with tempfile.NamedTemporaryFile( - mode="w", dir=os.path.dirname(args.output), delete=False + mode="w", dir=os.path.dirname(args.output), delete=False ) as tf: try: json.dump(queue_contents, tf, indent=2) diff --git a/charms/focal/autopkgtest-web/webcontrol/download-all-results b/charms/focal/autopkgtest-web/webcontrol/download-all-results index ab0a1e3..aebff04 100755 --- a/charms/focal/autopkgtest-web/webcontrol/download-all-results +++ b/charms/focal/autopkgtest-web/webcontrol/download-all-results @@ -1,14 +1,17 @@ #!/usr/bin/python3 +#pylint: disable=invalid-name, consider-using-with, too-many-locals, too-many-branches, consider-using-f-string, no-member, bad-option-value -# Authors: Iain Lane, Martin Pitt +''' +Authors: Iain Lane, Martin Pitt -# This script uses the OpenStack Swift API to list all of the contents of our -# containers, diffs that against the local database, and then downloads any -# missing results. +This script uses the OpenStack Swift API to list all of the contents of our +containers, diffs that against the local database, and then downloads any +missing results. -# While in normal operation the download-results script is supposed to receive -# notification of completed jobs, in case of bugs or network outages etc, this -# script can be used to find any results which were missed and insert them. +While in normal operation the download-results script is supposed to receive +notification of completed jobs, in case of bugs or network outages etc, this +script can be used to find any results which were missed and insert them. +''' import os import sys @@ -21,10 +24,10 @@ import configparser import urllib.parse import time import http +from urllib.request import urlopen from distro_info import UbuntuDistroInfo from helpers.utils import get_test_id, init_db -from urllib.request import urlopen LOGGER = logging.getLogger(__name__) @@ -33,6 +36,9 @@ db_con = None def list_remote_container(container_url): + """ + Shows details about a remote container + """ LOGGER.debug("Listing container %s", container_url) out = [] @@ -42,10 +48,10 @@ def list_remote_container(container_url): url += f"&marker={urllib.parse.quote(start)}" LOGGER.debug('Retrieving "%s"', url) - for retry in range(5): + for _ in range(5): try: resp = urlopen(url) - except http.client.RemoteDisconnected as e: + except http.client.RemoteDisconnected as _: LOGGER.debug('Got disconnected, sleeping') time.sleep(5) continue @@ -79,20 +85,22 @@ def list_remote_container(container_url): return ret -def list_our_results(release): - LOGGER.debug("Finding already recorded results for %s", release) +def list_our_results(specified_release): + """Shows results for a specific release""" + LOGGER.debug("Finding already recorded results for %s", specified_release) c = db_con.cursor() c.execute( - "SELECT run_id FROM result INNER JOIN test ON test.id = result.test_id WHERE test.release=?", - (release,), + "SELECT run_id FROM result INNER JOIN test ON " + \ + "test.id = result.test_id WHERE test.release=?", + (specified_release,), ) return {run_id for (run_id,) in c.fetchall()} def fetch_one_result(url): """Download one result URL from swift and add it to the DB""" - (release, arch, _, src, run_id, _) = url.split("/")[-6:] - test_id = get_test_id(db_con, release, arch, src) + (release_fetch, arch, _, src, run_id, _) = url.split("/")[-6:] + test_id = get_test_id(db_con, release_fetch, arch, src) try: f = urlopen(url, timeout=30) @@ -119,7 +127,7 @@ def fetch_one_result(url): srcver = ( tar.extractfile("testpkg-version").read().decode().strip() ) - except KeyError as e: + except KeyError as _: # not found if exitcode in (4, 12, 20): # repair it @@ -135,7 +143,7 @@ def fetch_one_result(url): # requester try: requester = tar.extractfile("requester").read().decode().strip() - except KeyError as e: + except KeyError as _: requester = "" except (KeyError, ValueError, tarfile.TarError) as e: LOGGER.debug("%s is damaged, ignoring: %s", url, str(e)) @@ -161,7 +169,7 @@ def fetch_one_result(url): LOGGER.debug( "Fetched test result for %s/%s/%s/%s %s (triggers: %s): exit code %i", - release, + release_fetch, arch, src, ver, @@ -189,13 +197,11 @@ def fetch_one_result(url): LOGGER.info("%s was already recorded - skipping", run_id) -def fetch_container(release, container_url): +def fetch_container(release_to_fetch, container_url): """Download new results from a swift container""" - marker = "" - try: - our_results = list_our_results(release) + our_results = list_our_results(release_to_fetch) known_results = list_remote_container(container_url) need_to_fetch = set(known_results.keys()) - our_results @@ -206,7 +212,7 @@ def fetch_container(release, container_url): fetch_one_result(os.path.join(container_url, known_results[run_id])) except urllib.error.HTTPError as e: if e.code == 401: - LOGGER.warning(f"Couldn't access {container_url} - doesn't exist?") + LOGGER.warning("Couldn't access %s - doesn't exist?", container_url) return raise diff --git a/charms/focal/autopkgtest-web/webcontrol/download-results b/charms/focal/autopkgtest-web/webcontrol/download-results index b8d4188..0d1348b 100755 --- a/charms/focal/autopkgtest-web/webcontrol/download-results +++ b/charms/focal/autopkgtest-web/webcontrol/download-results @@ -1,4 +1,8 @@ #!/usr/bin/python3 +#pylint: disable=invalid-name, import-error, missing-function-docstring, too-many-locals +''' +Downloads database results from SQL +''' import configparser import json @@ -7,9 +11,7 @@ import os import socket import sqlite3 import urllib.parse - from helpers.utils import get_test_id, init_db -from urllib.request import urlopen import amqplib.client_0_8 as amqp @@ -27,7 +29,7 @@ def amqp_connect(): parts.hostname, userid=parts.username, password=parts.password ) logging.info( - "Connected to AMQP server at %s@%s" % (parts.username, parts.hostname) + "Connected to AMQP server at %s@%s", parts.username, parts.hostname ) return amqp_con @@ -48,7 +50,7 @@ def process_message(msg, db_con): if isinstance(body, bytes): body = body.decode("UTF-8", errors="replace") info = json.loads(body) - logging.info("Received notification of completed test {}".format(info)) + logging.info("Received notification of completed test %s", info) arch = info["architecture"] container = info["container"] @@ -61,8 +63,8 @@ def process_message(msg, db_con): (_, _, _, _, run_id) = info["swift_dir"].split("/") # we don't handle PPA requests - if container != ("autopkgtest-{}".format(release)): - logging.debug("Ignoring non-distro request: {}".format(info)) + if container != (f"autopkgtest-{release}"): + logging.debug("Ignoring non-distro request: %s", info) msg.channel.basic_ack(msg.delivery_tag) return @@ -100,20 +102,20 @@ if __name__ == "__main__": level=("DEBUG" in os.environ and logging.DEBUG or logging.INFO) ) - db_con = db_connect() - amqp_con = amqp_connect() - status_ch = amqp_con.channel() + db_connection = db_connect() + amqp_connection = amqp_connect() + status_ch = amqp_connection.channel() status_ch.access_request("/complete", active=True, read=True, write=False) status_ch.exchange_declare( EXCHANGE_NAME, "fanout", durable=True, auto_delete=False ) - queue_name = "complete-listener-%s" % socket.getfqdn() + queue_name = "complete-listener-" + socket.getfqdn() status_ch.queue_declare(queue_name, durable=True, auto_delete=False) status_ch.queue_bind(queue_name, EXCHANGE_NAME, queue_name) - logging.info("Listening to requests on %s" % queue_name) + logging.info("Listening to requests on %s", queue_name) status_ch.basic_consume( - "", callback=lambda msg: process_message(msg, db_con) + "", callback=lambda msg: process_message(msg, db_connection) ) while status_ch.callbacks: status_ch.wait() diff --git a/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py b/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py index ec74d8f..336da33 100644 --- a/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py +++ b/charms/focal/autopkgtest-web/webcontrol/helpers/utils.py @@ -1,7 +1,7 @@ ''' utilities for autopkgtest-web webcontrol ''' -#pylint: disable=protected-access +#pylint: disable=protected-access, invalid-name import logging import os import sqlite3 diff --git a/charms/focal/autopkgtest-web/webcontrol/private_results/app.py b/charms/focal/autopkgtest-web/webcontrol/private_results/app.py index 95cec50..6696d10 100644 --- a/charms/focal/autopkgtest-web/webcontrol/private_results/app.py +++ b/charms/focal/autopkgtest-web/webcontrol/private_results/app.py @@ -1,11 +1,15 @@ """Test Result Fetcher Flask App""" +#pylint: disable=import-error, consider-using-f-string, too-many-arguments, bad-option-value import os import sys import logging -import swiftclient import configparser - from html import escape +from helpers.utils import setup_key +from request.submit import Submit + +import swiftclient + from flask import (Flask, Response, request, session, redirect, render_template_string) from flask_openid import OpenID @@ -13,9 +17,6 @@ from werkzeug.middleware.proxy_fix import ProxyFix sys.path.append('..') -from helpers.utils import setup_key -from request.submit import Submit - HTML = """ <!doctype html> @@ -40,21 +41,20 @@ LOGIN = """ DENIED = "Unprivileged or unavailable." -def swift_get_object(connection, container, path): +def swift_get_object(swift_connection, container, path): """Fetch an object from swift.""" try: - _, contents = connection.get_object(container, path) - except swiftclient.exceptions.ClientException as e: - logging.error('Failed to fetch %s from container (%s)' % - (path, str(e))) + _, contents = swift_connection.get_object(container, path) + except swiftclient.exceptions.ClientException as swift_error: + logging.error('Failed to fetch %s from container (%s)', path, str(swift_error)) return None return contents -def validate_user_path(connection, container, nick, path): +def validate_user_path(swift_connection, container, nick, path): """Return true if user is allowed to view files under the given path.""" # First we need to check if this result is actually sharable - allowed_file = swift_get_object(connection, container, path) + allowed_file = swift_get_object(swift_connection, container, path) if not allowed_file: return False allowed = allowed_file.decode('utf-8').splitlines() @@ -66,10 +66,10 @@ def validate_user_path(connection, container, nick, path): for entity in allowed: (code, response) = Submit.lp_request('~%s/participants' % entity, {}) if code != 200: - logging.error('Unable to validate user %s (%s)' % (nick, code)) + logging.error('Unable to validate user %s (%s)', nick, code) return False - for e in response.get('entries', []): - if e.get('name') == nick: + for response_entry in response.get('entries', []): + if response_entry.get('name') == nick: return True return False @@ -152,13 +152,11 @@ def index_result(container, series, arch, group, src, runid, file): content_type = 'text/plain; charset=UTF-8' headers = {'Content-Encoding': 'gzip'} return Response(result, content_type=content_type, headers=headers) - else: - return result - else: - # XXX: render_template_string urlencodes its context values, so it's - # not really possible to have 'nested HTML' rendered properly. - return HTML.replace("{{ content }}", - render_template_string(LOGIN, **session)) + return result + # render_template_string urlencodes its context values, so it's + # not really possible to have 'nested HTML' rendered properly. + return HTML.replace("{{ content }}", + render_template_string(LOGIN, **session)) @app.route('/login', methods=['GET', 'POST']) diff --git a/charms/focal/autopkgtest-web/webcontrol/publish-db b/charms/focal/autopkgtest-web/webcontrol/publish-db index a44b04a..5eadb58 100755 --- a/charms/focal/autopkgtest-web/webcontrol/publish-db +++ b/charms/focal/autopkgtest-web/webcontrol/publish-db @@ -1,14 +1,15 @@ #!/usr/bin/python3 -# download/read Sources.gz for all known releases and write them into -# a copy of autopkgtest.db, which is then published to the public location. -# This is being used for statistics. +''' +download/read Sources.gz for all known releases and write them into +a copy of autopkgtest.db, which is then published to the public location. +This is being used for statistics. +''' +#pylint: disable=invalid-name, consider-using-f-string, c-extension-no-member, bad-option-value import configparser -import fcntl import gzip import logging import os -import shutil import sqlite3 import tempfile import urllib.request @@ -75,7 +76,7 @@ def init_db(path, path_current, path_rw): current_version_copied = True except sqlite3.OperationalError as e: if "no such column: pocket" not in str( - e + e ) and "no such column: component" not in str( e ): # schema upgrade @@ -111,8 +112,9 @@ def init_db(path, path_current, path_rw): return db -def get_last_checked(db_con, url): - c = db_con.cursor() +def get_last_checked(db_connection, url): + """Get the time the database was last checked""" + c = db_connection.cursor() c.execute("SELECT timestamp FROM url_last_checked WHERE url=?", (url,)) try: @@ -122,14 +124,15 @@ def get_last_checked(db_con, url): return None -def get_sources(db_con, release): +def get_sources(db_connection, release): + """Gets sources.gz for specific release""" for component in components: for pocket in (release, release + "-updates"): logging.debug("Processing %s/%s", pocket, component) try: url = f"{archive_url}/dists/{pocket}/{component}/source/Sources.gz" request = urllib.request.Request(url) - last_checked = get_last_checked(db_con, url) + last_checked = get_last_checked(db_connection, url) if last_checked: request.add_header("If-Modified-Since", last_checked) @@ -139,7 +142,7 @@ def get_sources(db_con, release): temp_file.write(response.read()) last_modified = response.getheader("Last-Modified") if last_modified: - db_con.execute( + db_connection.execute( "INSERT INTO url_last_checked (url, timestamp) " "VALUES (:url, :timestamp) " "ON CONFLICT (url) DO " @@ -147,7 +150,7 @@ def get_sources(db_con, release): {'url': url, 'timestamp': last_modified} ) - db_con.execute( + db_connection.execute( "DELETE FROM current_version " "WHERE pocket = ? AND component = ?", (pocket, component), @@ -155,7 +158,7 @@ def get_sources(db_con, release): temp_file.seek(0) with gzip.open(temp_file) as fd: for section in apt_pkg.TagFile(fd): - db_con.execute( + db_connection.execute( "INSERT INTO current_version " "(release, pocket, component, package, version) " "VALUES " @@ -171,11 +174,10 @@ def get_sources(db_con, release): 'version': section["Version"], }, ) - db_con.commit() + db_connection.commit() except urllib.error.HTTPError as e: if e.code == 304: - logging.debug("url {} not modified".format(url)) - pass + logging.debug("url %s not modified", url) if __name__ == "__main__": diff --git a/charms/focal/autopkgtest-web/webcontrol/request/app.py b/charms/focal/autopkgtest-web/webcontrol/request/app.py index 15c5b5c..b66cfad 100644 --- a/charms/focal/autopkgtest-web/webcontrol/request/app.py +++ b/charms/focal/autopkgtest-web/webcontrol/request/app.py @@ -1,4 +1,5 @@ """Test Request Flask App""" +#pylint: disable=import-error, consider-using-f-string, invalid-name, too-many-locals, no-else-return, no-value-for-parameter, too-many-return-statements, too-many-branches, too-many-statements, bad-option-value import os import logging import hmac @@ -58,24 +59,24 @@ SUCCESS = """ """ -def check_github_sig(request): - """Validate github signature of request. +def check_github_sig(req): + """Validate github signature of req. See https://developer.github.com/webhooks/securing/ """ # load key keyfile = os.path.expanduser('~/github-secrets.json') - package = request.args.get('package') + package = req.args.get('package') try: - with open(keyfile) as f: - keymap = json.load(f) + with open(keyfile, encoding='utf-8') as keyfile: + keymap = json.load(keyfile) key = keymap[package].encode('ASCII') - except (IOError, ValueError, KeyError, UnicodeEncodeError) as e: - logging.error('Failed to load GitHub key for package %s: %s', package, e) + except (IOError, ValueError, KeyError, UnicodeEncodeError) as github_sig_error: + logging.error('Failed to load GitHub key for package %s: %s', package, github_sig_error) return False - sig_sha1 = request.headers.get('X-Hub-Signature', '') - payload_sha1 = 'sha1=' + hmac.new(key, request.data, 'sha1').hexdigest() + sig_sha1 = req.headers.get('X-Hub-Signature', '') + payload_sha1 = 'sha1=' + hmac.new(key, req.data, 'sha1').hexdigest() if hmac.compare_digest(sig_sha1, payload_sha1): return True logging.error('check_github_sig: signature mismatch! received: %s calculated: %s', @@ -89,7 +90,7 @@ def invalid(message, code=400): html = LOGOUT.format(**session) else: html = '' - html += '<p>You submitted an invalid request: %s</p>' % maybe_escape(str(message)) + html += f"<p>You submitted an invalid request: {maybe_escape(str(message))}</p>" logging.error('Request failed with %i: %s', code, message) return HTML.format(html), code @@ -128,16 +129,16 @@ def index_root(): del params[getarg] except KeyError: pass - l = request.args.getlist(getarg) - if l: - params[paramname] = [maybe_escape(p) for p in l] + req_list = request.args.getlist(getarg) + if req_list: + params[paramname] = [maybe_escape(p) for p in req_list] # split "VAR1=value;VAR2=value" --env arguments, as some frameworks don't # allow multipe "env=" if 'env' in params: splitenv = [] - for e in params['env']: - splitenv += e.split(';') + for env_param in params['env']: + splitenv += env_param.split(';') params['env'] = splitenv # request from github? @@ -155,7 +156,8 @@ def index_root(): s = Submit() try: - params.setdefault('env', []).append('UPSTREAM_PULL_REQUEST=%i' % int(github_params['number'])) + params.setdefault('env', []).append('UPSTREAM_PULL_REQUEST=%i' % \ + int(github_params['number'])) statuses_url = github_params['pull_request']['statuses_url'] params['env'].append('GITHUB_STATUSES_URL=' + statuses_url) @@ -177,7 +179,7 @@ def index_root(): with open(os.path.join(PATH, 'github-pending', '%s-%s-%s-%s-%s' % (params['release'], params['arch'], params['package'], github_params['number'], - os.path.basename(statuses_url))), 'w') as f: + os.path.basename(statuses_url))), 'w', encoding='utf-8') as f: f.write(json.dumps(params)) # tell GitHub that the test is pending @@ -213,7 +215,7 @@ def index_root(): return HTML.format(LOGOUT + "<p>Deleted {} requests</p>".format(count)).format( - **ChainMap(session, params)) + **ChainMap(session, params)) if params.get('ppas'): s.send_amqp_request(context='ppa', **params) @@ -223,7 +225,7 @@ def index_root(): if not params.get('ppas'): url = 'https://autopkgtest.ubuntu.com/packages/{}/{}/{}'.format( params['package'], params['release'], params['arch']) - params['Result history'] = '<a href="{}">{}</a>'.format(url, url) + params['Result history'] = '<a href="{0}">{0}</a>'.format(url) success = SUCCESS.format( EMPTY.join(ROW.format(key, val) for key, val in sorted(params.items())) ) diff --git a/charms/focal/autopkgtest-web/webcontrol/request/submit.py b/charms/focal/autopkgtest-web/webcontrol/request/submit.py index 0e35b3f..f221d97 100644 --- a/charms/focal/autopkgtest-web/webcontrol/request/submit.py +++ b/charms/focal/autopkgtest-web/webcontrol/request/submit.py @@ -2,6 +2,7 @@ Author: Martin Pitt <martin.p...@ubuntu.com> """ +#pylint: disable=protected-access, invalid-name, consider-using-f-string, import-error, no-member, dangerous-default-value, too-many-arguments, too-many-locals, too-many-branches, too-many-statements, unused-argument, no-value-for-parameter, inconsistent-return-statements, bad-classmethod-argument, bad-option-value import os import json @@ -33,6 +34,9 @@ ALLOWED_USERS_PERPACKAGE = {'snapcraft': ['snappy-m-o']} class Submit: + ''' + Class to submit an autopkgtest request + ''' def __init__(self): cp = configparser.ConfigParser() cp.read(os.path.expanduser('~ubuntu/autopkgtest-cloud.conf')) @@ -40,7 +44,7 @@ class Submit: # read valid releases and architectures from DB self.db_con = sqlite3.connect('file:%s?mode=ro' % cp['web']['database_ro'], uri=True) self.releases = set(UbuntuDistroInfo().supported() + UbuntuDistroInfo().supported_esm()) - logging.debug('Valid releases: %s' % self.releases) + logging.debug('Valid releases: %s', self.releases) self.architectures = set() c = self.db_con.cursor() @@ -50,13 +54,13 @@ class Submit: if row is None: break self.architectures.add(row[0]) - logging.debug('Valid architectures: %s' % self.architectures) + logging.debug('Valid architectures: %s', self.architectures) # dissect AMQP URL self.amqp_creds = urllib.parse.urlsplit(cp['amqp']['uri'], allow_fragments=False) assert self.amqp_creds.scheme == 'amqp' - logging.debug('AMQP credentials: %s' % repr(self.amqp_creds)) + logging.debug('AMQP credentials: %s', repr(self.amqp_creds)) def validate_distro_request(self, release, arch, package, triggers, requester, ppas=[], **kwargs): @@ -103,7 +107,9 @@ class Submit: raise ValueError('Unknown PPA ' + ppa) # allow kernel tests for EOL vivid skip_result_check = (release == 'vivid' and triggers and triggers[0].startswith('linux')) - if not self.is_valid_package_with_results(None if (ppas or skip_result_check) else release, arch, package): + if not self.is_valid_package_with_results(None if (ppas or skip_result_check) else release, + arch, + package): raise ValueError('Package %s does not have any test results' % package) @@ -117,8 +123,8 @@ class Submit: for trigger in triggers: try: trigsrc, trigver = trigger.split('/') - except ValueError: - raise ValueError('Malformed trigger, must be srcpackage/version') + except ValueError as exc: + raise ValueError('Malformed trigger, must be srcpackage/version') from exc # Debian Policy 5.6.1 and 5.6.12 if not NAME.match(trigsrc) or not VERSION.match(trigver): raise ValueError('Malformed trigger') @@ -245,7 +251,7 @@ class Submit: routing_key=queue) @classmethod - def post_json(klass, url, data, auth_file, project): + def post_json(cls, klass, url, data, auth_file, project): """Send POST request with JSON data via basic auth. 'data' is a dictionary which will be posted to 'url' in JSON encoded @@ -259,7 +265,7 @@ class Submit: HTTPError if POST request fails. """ # look up project in auth_file - with open(auth_file) as f: + with open(auth_file, encoding='utf-8') as f: contents = f.read() for l in contents.splitlines(): if l.startswith(project + ':'): @@ -287,7 +293,7 @@ class Submit: """Check if a ppa exists""" team, _, name = ppa.partition('/') if not NAME.match(team) or not NAME.match(name): - return None + return False # https://launchpad.net/+apidoc/1.0.html#person-getPPAByName (code, response) = self.lp_request('~' + team, { 'ws.op': 'getPPAByName', @@ -298,7 +304,7 @@ class Submit: 'is_valid_ppa(%s): code %u, response %s', ppa, code, repr(response)) if code < 200 or code >= 300: - return None + return False if response.get('name') == name: return True @@ -349,8 +355,7 @@ class Submit: release, package, version, code, repr(response)) if response.get('total_size', 0) > 0: return response['entries'][0]['component_name'] - else: - return None + return None def can_upload(self, person, release, component, package): """Check if person can upload package into Ubuntu release""" @@ -366,13 +371,13 @@ class Submit: }) logging.debug('can_upload(%s, %s, %s, %s): (%u, %s)', person, release, component, package, code, repr(response)) - return code >= 200 and code < 300 + return 300 > code >= 200 def in_allowed_team(self, person, package=[], teams=[]): """Check if person is in ALLOWED_TEAMS""" for team in (teams or ALLOWED_TEAMS): - (code, response) = self.lp_request('~%s/participants' % team, {}) + (_, response) = self.lp_request('~%s/participants' % team, {}) for e in response.get('entries', []): if e.get('name') == person: return True diff --git a/charms/focal/autopkgtest-web/webcontrol/request/tests/test_app.py b/charms/focal/autopkgtest-web/webcontrol/request/tests/test_app.py index c3bd16a..6edda1e 100644 --- a/charms/focal/autopkgtest-web/webcontrol/request/tests/test_app.py +++ b/charms/focal/autopkgtest-web/webcontrol/request/tests/test_app.py @@ -1,4 +1,5 @@ """Test the Flask app.""" +#pylint: disable=no-value-for-parameter, missing-function-docstring, no-member, too-few-public-methods import os @@ -10,6 +11,9 @@ from request.submit import Submit class AppTestBase(TestCase): + ''' + Base class for testing the app + ''' def setUp(self): request.app.app.config['TESTING'] = True self.app = request.app.app.test_client() @@ -38,7 +42,8 @@ class DistroRequestTests(AppTestBase): @patch('request.app.Submit') def test_nickname(self, mock_submit): """Hitting / with a nickname in the session prompts for logout.""" - mock_submit.return_value.validate_distro_request.side_effect = ValueError('not 31337 enough') + mock_submit.return_value.validate_distro_request.side_effect = \ + ValueError('not 31337 enough') with self.app.session_transaction() as session: session['nickname'] = 'person' ret = self.app.get('/') @@ -47,7 +52,8 @@ class DistroRequestTests(AppTestBase): @patch('request.app.Submit') def test_missing_request(self, mock_submit): """Missing GET params should return 400.""" - mock_submit.return_value.validate_distro_request.side_effect = ValueError('not 31337 enough') + mock_submit.return_value.validate_distro_request.side_effect = \ + ValueError('not 31337 enough') self.prep_session() ret = self.app.get('/') self.assertEqual(ret.status_code, 400) @@ -56,7 +62,8 @@ class DistroRequestTests(AppTestBase): @patch('request.app.Submit') def test_invalid_request(self, mock_submit): """Invalid GET params should return 400.""" - mock_submit.return_value.validate_distro_request.side_effect = ValueError('not 31337 enough') + mock_submit.return_value.validate_distro_request.side_effect = \ + ValueError('not 31337 enough') self.prep_session() ret = self.app.get('/?arch=i386&package=hi&release=testy&trigger=foo/1') self.assertEqual(ret.status_code, 400) @@ -91,7 +98,8 @@ class DistroRequestTests(AppTestBase): def test_valid_request_with_ppas(self, mock_submit): """Return success with all params & ppas.""" self.prep_session() - ret = self.app.get('/?arch=i386&package=hi&release=testy&trigger=foo/1&ppa=train/overlay&ppa=train/001') + ret = self.app.get('/?arch=i386&package=hi&release=testy' + \ + '&trigger=foo/1&ppa=train/overlay&ppa=train/001') self.assertEqual(ret.status_code, 200) self.assertIn(b'ubmitted', ret.data) mock_submit.return_value.validate_distro_request.assert_called_once_with( @@ -127,7 +135,8 @@ class GitHubRequestTests(AppTestBase): def test_ping(self): ret = self.app.post('/?arch=C51&package=hi&release=testy&build-git=http://x.com/foo', content_type='application/json', - headers=[('X-Hub-Signature', 'sha1=cb59904bf33c619ad2c52095deb405c86cc5adfd'), + headers=[('X-Hub-Signature', + 'sha1=cb59904bf33c619ad2c52095deb405c86cc5adfd'), ('X-GitHub-Event', 'ping')], data=b'{"info": "https://api.github.com/xx"}') self.assertEqual(ret.status_code, 200, ret.data) @@ -139,8 +148,10 @@ class GitHubRequestTests(AppTestBase): def test_invalid_secret_file(self, mock_submit): ret = self.app.post('/?arch=C51&package=hi&release=testy&build-git=http://x.com/foo', content_type='application/json', - headers=[('X-Hub-Signature', 'sha1=8572f239e05c652710a4f85d2061cc0fcbc7b127')], - data=b'{"action": "opened", "number": 2, "pr": "https://api.github.com/xx"}') + headers=[('X-Hub-Signature', + 'sha1=8572f239e05c652710a4f85d2061cc0fcbc7b127')], + data=b'{"action": "opened", "number":' + \ + ' 2, "pr": "https://api.github.com/xx"}') self.assertEqual(ret.status_code, 403, ret.data) self.assertIn(b'GitHub signature verification failed', ret.data) @@ -177,7 +188,8 @@ class GitHubRequestTests(AppTestBase): mock_check_github_sig.return_value = True ret = self.app.post('/?arch=C51&package=hi&release=testy&build-git=http://x.com/foo', content_type='application/json', - data=b'{"action": "boring", "number": 2, "pr": "https://api.github.com/xx"}') + data=b'{"action": "boring", "number": ' + \ + '2, "pr": "https://api.github.com/xx"}') self.assertEqual(ret.status_code, 200, ret.data) self.assertIn(b'GitHub PR action boring is not relevant for testing', ret.data) self.assertFalse(mock_submit.return_value.validate_git_request.called) @@ -208,7 +220,8 @@ class GitHubRequestTests(AppTestBase): def test_valid_simple(self, mock_submit): ret = self.app.post('/?arch=C51&package=hi&release=testy&build-git=http://x.com/foo', content_type='application/json', - headers=[('X-Hub-Signature', 'sha1=1dae67d4406d21b498806968a3def61754498a21')], + headers=[('X-Hub-Signature', + 'sha1=1dae67d4406d21b498806968a3def61754498a21')], data=b'{"action": "opened", "number": 2, "pull_request":' b' {"statuses_url": "https://api.github.com/two"}}') @@ -226,7 +239,8 @@ class GitHubRequestTests(AppTestBase): # we recorded the request request.app.open.assert_called_with( os.path.join(request.app.PATH, 'github-pending', 'testy-C51-hi-2-two'), 'w') - self.assertIn('GITHUB_STATUSES_URL=https://api.github.com/two', str(request.app.open().write.call_args)) + self.assertIn('GITHUB_STATUSES_URL=https://api.github.com/two', + str(request.app.open().write.call_args)) self.assertIn('"arch": "C51"', str(request.app.open().write.call_args)) # we told GitHub about it @@ -245,7 +259,8 @@ class GitHubRequestTests(AppTestBase): ret = self.app.post('/?arch=C51&package=hi&release=testy&build-git=http://x.com/foo&' 'ppa=joe/stuff&ppa=mary/misc&env=THIS=a;THAT=b&env=THERE=c', content_type='application/json', - headers=[('X-Hub-Signature', 'sha1=f9041325575127310c304bb65f9befb0d13b1ce6')], + headers=[('X-Hub-Signature', + 'sha1=f9041325575127310c304bb65f9befb0d13b1ce6')], data=b'{"action": "opened", "number": 2, "pull_request":' b' {"statuses_url": "https://api.github.com/2"}}') @@ -271,7 +286,8 @@ class GitHubRequestTests(AppTestBase): def test_valid_generated_url(self, mock_submit): ret = self.app.post('/?arch=C51&package=hi&release=testy', content_type='application/json', - headers=[('X-Hub-Signature', 'sha1=427a20827d46f5fe8e18f08b9a7fa09ba915ea08')], + headers=[('X-Hub-Signature', + 'sha1=427a20827d46f5fe8e18f08b9a7fa09ba915ea08')], data=b'{"action": "opened", "number": 2, "pull_request":' b' {"statuses_url": "https://api.github.com/two",' b' "base": {"repo": {"clone_url": "https://github.com/joe/x.git"}}}}') @@ -315,9 +331,11 @@ class GitHubRequestTests(AppTestBase): mock_open(None, '{"hi": "1111111111111111111111111111111111111111"}'), create=True) def test_valid_testname(self, mock_submit): - ret = self.app.post('/?arch=C51&package=hi&release=testy&build-git=http://x.com/foo&testname=first', + ret = self.app.post('/?arch=C51&package=hi&release=testy&build' + \ + '-git=http://x.com/foo&testname=first', content_type='application/json', - headers=[('X-Hub-Signature', 'sha1=1dae67d4406d21b498806968a3def61754498a21')], + headers=[('X-Hub-Signature', + 'sha1=1dae67d4406d21b498806968a3def61754498a21')], data=b'{"action": "opened", "number": 2, "pull_request":' b' {"statuses_url": "https://api.github.com/two"}}') @@ -335,7 +353,8 @@ class GitHubRequestTests(AppTestBase): # we recorded the request request.app.open.assert_called_with( os.path.join(request.app.PATH, 'github-pending', 'testy-C51-hi-2-two'), 'w') - self.assertIn('GITHUB_STATUSES_URL=https://api.github.com/two', str(request.app.open().write.call_args)) + self.assertIn('GITHUB_STATUSES_URL=https://api.github.com/two', + str(request.app.open().write.call_args)) self.assertIn('"testname": "first"', str(request.app.open().write.call_args)) diff --git a/charms/focal/autopkgtest-web/webcontrol/request/tests/test_submit.py b/charms/focal/autopkgtest-web/webcontrol/request/tests/test_submit.py index bae9185..d326e46 100644 --- a/charms/focal/autopkgtest-web/webcontrol/request/tests/test_submit.py +++ b/charms/focal/autopkgtest-web/webcontrol/request/tests/test_submit.py @@ -1,7 +1,7 @@ """Submit Tests - Test all things related verifying input arguments and sending AMQP requests. """ +# pylint: disable=arguments-differ, consider-using-f-string, bad-option-value import sqlite3 @@ -33,7 +33,7 @@ class SubmitTestBase(TestCase): # mock config values cfg = {'amqp': {'uri': 'amqp://user:s3kr1t@1.2.3.4'}, 'web': {'database': '/ignored', 'database_ro': '/ignored'}, - 'autopkgtest' : { 'releases': 'testy grumpy' }} + 'autopkgtest' : {'releases': 'testy grumpy'}} mock_configparser.return_value = MagicMock() mock_configparser.return_value.__getitem__.side_effect = cfg.get @@ -60,100 +60,105 @@ class DistroRequestValidationTests(SubmitTestBase): def test_bad_release(self): """Unknown release""" - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('fooly', 'C51', 'blue', ['ab/1'], 'joe') - self.assertEqual(str(cme.exception), 'Unknown release fooly') + self.assertEqual(str(mock_response_error.exception), 'Unknown release fooly') def test_bad_arch(self): """Unknown architecture""" - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'wut', 'blue', ['ab/1'], 'joe') - self.assertEqual(str(cme.exception), 'Unknown architecture wut') + self.assertEqual(str(mock_response_error.exception), 'Unknown architecture wut') def test_bad_package(self): """Unknown package""" - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'badpkg', ['ab/1'], 'joe') - self.assertIn('Package badpkg', str(cme.exception)) + self.assertIn('Package badpkg', str(mock_response_error.exception)) def test_bad_argument(self): """Unknown argument""" - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1'], 'joe', foo='bar') - self.assertIn('Invalid argument foo', str(cme.exception)) + self.assertIn('Invalid argument foo', str(mock_response_error.exception)) def test_invalid_trigger_syntax(self): """Invalid syntax in trigger""" # invalid trigger format - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab'], 'joe') - self.assertIn('Malformed trigger', str(cme.exception)) + self.assertIn('Malformed trigger', str(mock_response_error.exception)) # invalid trigger source package name chars - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['a!b/1'], 'joe') # invalid trigger version chars - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1!1'], 'joe') - self.assertIn('Malformed trigger', str(cme.exception)) + self.assertIn('Malformed trigger', str(mock_response_error.exception)) def test_disallowed_testname(self): """testname not allowed for distro tests""" # we only allow this for GitHub requests; with distro requests it would # be cheating as proposed-migration would consider those - with self.assertRaises(ValueError) as cme: - self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe', testname='first') - self.assertIn('Invalid argument testname', str(cme.exception)) + with self.assertRaises(ValueError) as mock_response_error: + self.submit.validate_distro_request('testy', + 'C51', + 'blue', + ['ab/1.2'], + 'joe', + testname='first') + self.assertIn('Invalid argument testname', str(mock_response_error.exception)) @patch('request.submit.urllib.request.urlopen') def test_ppa(self, mock_urlopen): """PPA does not exist""" # invalid name don't even call lp - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request( 'testy', 'C51', 'foo', ['ab/1.2'], 'joe', ['b~ad/ppa']) - self.assertEqual(str(cme.exception), 'Unknown PPA b~ad/ppa') + self.assertEqual(str(mock_response_error.exception), 'Unknown PPA b~ad/ppa') self.assertEqual(mock_urlopen.call_count, 0) # mock Launchpad response: successful form, but no match - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.geturl.return_value = 'http://mock.launchpad.net' - cm.read.return_value = b'{}' - cm.return_value = cm - mock_urlopen.return_value = cm - - with self.assertRaises(ValueError) as cme: + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.geturl.return_value = 'http://mock.launchpad.net' + mock_response.read.return_value = b'{}' + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response + + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request( 'testy', 'C51', 'foo', ['ab/1.2'], 'joe', ['bad/ppa']) - self.assertEqual(str(cme.exception), 'Unknown PPA bad/ppa') + self.assertEqual(str(mock_response_error.exception), 'Unknown PPA bad/ppa') self.assertEqual(mock_urlopen.call_count, 1) # success - cm.read.return_value = b'{"name": "there"}' + mock_response.read.return_value = b'{"name": "there"}' self.assertTrue(self.submit.is_valid_ppa('hi/there')) # broken JSON response - cm.read.return_value = b'not { json}' - with self.assertRaises(ValueError) as cme: + mock_response.read.return_value = b'not { json}' + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request( 'testy', 'C51', 'foo', ['ab/1.2'], 'joe', ['broke/ness']) # same, but entirely failing query -- let's be on the safe side - cm.getcode.return_value = 404 - cm.read.return_value = b'<html>not found</html>' - with self.assertRaises(ValueError) as cme: + mock_response.getcode.return_value = 404 + mock_response.read.return_value = b'<html>not found</html>' + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request( 'testy', 'C51', 'foo', ['ab/1.2'], 'joe', ['bro/ken']) - self.assertEqual(str(cme.exception), 'Unknown PPA bro/ken') + self.assertEqual(str(mock_response_error.exception), 'Unknown PPA bro/ken') @patch('request.submit.urllib.request.urlopen') def test_nonexisting_trigger(self, mock_urlopen): @@ -161,30 +166,30 @@ class DistroRequestValidationTests(SubmitTestBase): # mock Launchpad response: successful form, but no matching # source/version - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.geturl.return_value = 'http://mock.launchpad.net' - cm.read.return_value = b'{"total_size": 0}' - cm.return_value = cm - mock_urlopen.return_value = cm - - with self.assertRaises(ValueError) as cme: + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.geturl.return_value = 'http://mock.launchpad.net' + mock_response.read.return_value = b'{"total_size": 0}' + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response + + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe') - self.assertEqual(str(cme.exception), 'ab/1.2 is not published in testy') + self.assertEqual(str(mock_response_error.exception), 'ab/1.2 is not published in testy') self.assertEqual(mock_urlopen.call_count, 1) # broken JSON response - cm.read.return_value = b'not { json}' - with self.assertRaises(ValueError) as cme: + mock_response.read.return_value = b'not { json}' + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe') # same, but entirely failing query -- let's be on the safe side - cm.getcode.return_value = 404 - cm.read.return_value = b'<html>not found</html>' - with self.assertRaises(ValueError) as cme: + mock_response.getcode.return_value = 404 + mock_response.read.return_value = b'<html>not found</html>' + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe') - self.assertEqual(str(cme.exception), 'ab/1.2 is not published in testy') + self.assertEqual(str(mock_response_error.exception), 'ab/1.2 is not published in testy') @patch('request.submit.urllib.request.urlopen') def test_bad_package_ppa(self, mock_urlopen): @@ -192,19 +197,20 @@ class DistroRequestValidationTests(SubmitTestBase): # mock Launchpad response: successful form, but no matching # source/version - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.geturl.return_value = 'http://mock.launchpad.net' - cm.read.side_effect = [b'{"name": "overlay"}', - b'{"name": "goodstuff"}'] - cm.return_value = cm - mock_urlopen.return_value = cm - - with self.assertRaises(ValueError) as cme: + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.geturl.return_value = 'http://mock.launchpad.net' + mock_response.read.side_effect = [b'{"name": "overlay"}', + b'{"name": "goodstuff"}'] + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response + + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'badpkg', ['ab/1.2'], 'joe', ppas=['team/overlay', 'joe/goodstuff']) - self.assertEqual(str(cme.exception), 'Package badpkg does not have any test results') + self.assertEqual(str(mock_response_error.exception), + 'Package badpkg does not have any test results') self.assertEqual(mock_urlopen.call_count, 2) @patch('request.submit.urllib.request.urlopen') @@ -213,20 +219,21 @@ class DistroRequestValidationTests(SubmitTestBase): # mock Launchpad response: successful form, but no matching # source/version - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.geturl.return_value = 'http://mock.launchpad.net' - cm.read.side_effect = [b'{"name": "overlay"}', - b'{"name": "goodstuff"}', - b'{"total_size": 0}'] - cm.return_value = cm - mock_urlopen.return_value = cm - - with self.assertRaises(ValueError) as cme: + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.geturl.return_value = 'http://mock.launchpad.net' + mock_response.read.side_effect = [b'{"name": "overlay"}', + b'{"name": "goodstuff"}', + b'{"total_size": 0}'] + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response + + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe', ppas=['team/overlay', 'joe/goodstuff']) - self.assertEqual(str(cme.exception), 'ab/1.2 is not published in PPA joe/goodstuff testy') + self.assertEqual(str(mock_response_error.exception), + 'ab/1.2 is not published in PPA joe/goodstuff testy') self.assertEqual(mock_urlopen.call_count, 3) @patch('request.submit.urllib.request.urlopen') @@ -235,19 +242,20 @@ class DistroRequestValidationTests(SubmitTestBase): # mock Launchpad response: successful form, matching # source/version, upload not allowed - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.read.side_effect = [b'{"total_size": 1, "entries": [{"component_name": "main"}]}', - HTTPError('https://lp/checkUpload', 403, 'Forbidden', {}, None), - HTTPError('https://lp/checkUpload', 403, 'Forbidden', {}, None), - b'{"total_size": 1, "entries": [{"name": "joe2"}]}'] - cm.return_value = cm - mock_urlopen.return_value = cm - - with self.assertRaises(ValueError) as cme: + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.read.side_effect = \ + [b'{"total_size": 1, "entries": [{"component_name": "main"}]}', + HTTPError('https://lp/checkUpload', 403, 'Forbidden', {}, None), + HTTPError('https://lp/checkUpload', 403, 'Forbidden', {}, None), + b'{"total_size": 1, "entries": [{"name": "joe2"}]}'] + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response + + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe') - self.assertIn('not allowed to upload blue or ab', str(cme.exception)) + self.assertIn('not allowed to upload blue or ab', str(mock_response_error.exception)) self.assertEqual(mock_urlopen.call_count, 4) @patch('request.submit.urllib.request.urlopen') @@ -256,13 +264,14 @@ class DistroRequestValidationTests(SubmitTestBase): # mock Launchpad response: successful form, matching # source/version, upload allowed - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.read.side_effect = [b'{"total_size": 1, "entries": [{"component_name": "main"}]}', - b'true'] - cm.return_value = cm - mock_urlopen.return_value = cm + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.read.side_effect = [b'{"total_size": 1, "entries": ' + \ + '[{"component_name": "main"}]}', + b'true'] + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe') self.assertEqual(mock_urlopen.call_count, 2) @@ -273,13 +282,14 @@ class DistroRequestValidationTests(SubmitTestBase): # mock Launchpad response: successful form, matching # source/version, upload allowed - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.read.side_effect = [b'{"total_size": 1, "entries": [{"component_name": "main"}]}', - b'true'] - cm.return_value = cm - mock_urlopen.return_value = cm + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.read.side_effect = [b'{"total_size": 1, "entries": ' + \ + '[{"component_name": "main"}]}', + b'true'] + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe', **{'all-proposed': '1'}) @@ -288,10 +298,10 @@ class DistroRequestValidationTests(SubmitTestBase): def test_distro_all_proposed_bad_value(self): """Valid distro request with invalid all-proposed value""" - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe', **{'all-proposed': 'bogus'}) - self.assertIn('nvalid all-proposed value', str(cme.exception)) + self.assertIn('nvalid all-proposed value', str(mock_response_error.exception)) @patch('request.submit.urllib.request.urlopen') def test_validate_distro_whitelisted_team(self, mock_urlopen): @@ -299,15 +309,18 @@ class DistroRequestValidationTests(SubmitTestBase): # mock Launchpad response: successful form, matching # source/version, upload allowed - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.read.side_effect = [b'{"total_size": 1, "entries": [{"component_name": "main"}]}', - HTTPError('https://lp/checkUpload', 403, 'Forbidden', {}, None), - HTTPError('https://lp/checkUpload', 403, 'Forbidden', {}, None), - b'{"total_size": 1, "entries": [{"name": "joe"}]}'] - cm.return_value = cm - mock_urlopen.return_value = cm + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.read.side_effect = [b'{"total_size": 1, "entries": ' + \ + '[{"component_name": "main"}]}', + HTTPError('https://lp/checkUpload', + 403, 'Forbidden', {}, None), + HTTPError('https://lp/checkUpload', + 403, 'Forbidden', {}, None), + b'{"total_size": 1, "entries": [{"name": "joe"}]}'] + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe') self.assertEqual(mock_urlopen.call_count, 4) @@ -318,18 +331,20 @@ class DistroRequestValidationTests(SubmitTestBase): # mock Launchpad response: successful form, matching # source/version, upload allowed - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.read.side_effect = [b'{"name": "overlay"}', - b'{"name": "goodstuff"}', - # check if package is published in PPA - b'{"total_size": 1, "entries": [{"component_name": "main"}]}', - # component name in Ubuntu archive - b'{"total_size": 1, "entries": [{"component_name": "universe"}]}', - b'true'] - cm.return_value = cm - mock_urlopen.return_value = cm + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.read.side_effect = [b'{"name": "overlay"}', + b'{"name": "goodstuff"}', + # check if package is published in PPA + b'{"total_size": 1, "entries": ' + \ + '[{"component_name": "main"}]}', + # component name in Ubuntu archive + b'{"total_size": 1, "entries": ' + \ + '[{"component_name": "universe"}]}', + b'true'] + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response self.submit.validate_distro_request('testy', 'C51', 'blue', ['ab/1.2'], 'joe', ppas=['team/overlay', 'joe/goodstuff']) @@ -340,105 +355,132 @@ class GitRequestValidationTests(SubmitTestBase): """Test verification of git branch test requests""" def test_bad_release(self): - with self.assertRaises(ValueError) as cme: - self.submit.validate_git_request('fooly', 'C51', 'ab', **{'build-git': 'https://x.com/proj'}) - self.assertEqual(str(cme.exception), 'Unknown release fooly') + """Tests invalid release""" + with self.assertRaises(ValueError) as mock_response_error: + self.submit.validate_git_request('fooly', + 'C51', + 'ab', + **{'build-git': 'https://x.com/proj'}) + self.assertEqual(str(mock_response_error.exception), 'Unknown release fooly') def test_bad_arch(self): - with self.assertRaises(ValueError) as cme: - self.submit.validate_git_request('testy', 'wut', 'a!b', **{'build-git': 'https://x.com/proj'}) - self.assertEqual(str(cme.exception), 'Unknown architecture wut') + """Tests invalid architecture""" + with self.assertRaises(ValueError) as mock_response_error: + self.submit.validate_git_request('testy', + 'wut', + 'a!b', + **{'build-git': 'https://x.com/proj'}) + self.assertEqual(str(mock_response_error.exception), 'Unknown architecture wut') def test_bad_package(self): - with self.assertRaises(ValueError) as cme: - self.submit.validate_git_request('testy', 'C51', 'a!b', **{'build-git': 'https://x.com/proj'}) - self.assertEqual(str(cme.exception), 'Malformed package') + """Tests invalid package""" + with self.assertRaises(ValueError) as mock_response_error: + self.submit.validate_git_request('testy', + 'C51', + 'a!b', + **{'build-git': 'https://x.com/proj'}) + self.assertEqual(str(mock_response_error.exception), 'Malformed package') @patch('request.submit.urllib.request.urlopen') def test_unknown_ppa(self, mock_urlopen): + """Tests invalid ppa""" # mock Launchpad response: successful form, but no match - cm = MagicMock() - cm.__enter__.return_value = cm - cm.getcode.return_value = 200 - cm.geturl.return_value = 'http://mock.launchpad.net' - cm.read.return_value = b'{}' - cm.return_value = cm - mock_urlopen.return_value = cm - - with self.assertRaises(ValueError) as cme: + mock_response = MagicMock() + mock_response.__enter__.return_value = mock_response + mock_response.getcode.return_value = 200 + mock_response.geturl.return_value = 'http://mock.launchpad.net' + mock_response.read.return_value = b'{}' + mock_response.return_value = mock_response + mock_urlopen.return_value = mock_response + + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_git_request('testy', 'C51', 'ab', ['bad/ppa'], **{'build-git': 'https://x.com/proj'}) - self.assertEqual(str(cme.exception), 'Unknown PPA bad/ppa') + self.assertEqual(str(mock_response_error.exception), 'Unknown PPA bad/ppa') self.assertEqual(mock_urlopen.call_count, 1) @patch('request.submit.Submit.is_valid_ppa') def test_bad_env(self, is_valid_ppa): + """Tests invalid env variables""" is_valid_ppa.return_value = True - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_git_request('testy', 'C51', 'ab', env=['foo=1', 'bar=1\n='], **{'build-git': 'https://x.com/proj', 'ppas': ['a/b']}) - self.assertIn('Invalid environment', str(cme.exception)) - self.assertIn('bar=1', str(cme.exception)) + self.assertIn('Invalid environment', str(mock_response_error.exception)) + self.assertIn('bar=1', str(mock_response_error.exception)) def test_no_ppa(self): """No PPA""" - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_git_request('testy', 'C51', 'ab', **{'build-git': 'https://x.com/proj'}) - self.assertEqual(str(cme.exception), 'Must specify at least one PPA (to associate results with)') + self.assertEqual(str(mock_response_error.exception), + 'Must specify at least one PPA (to associate results with)') @patch('request.submit.Submit.is_valid_ppa') def test_bad_git_url(self, is_valid_ppa): + """Tests invalid git url""" is_valid_ppa.return_value = True - with self.assertRaises(ValueError) as cme: - self.submit.validate_git_request('testy', 'C51', 'ab', **{'build-git': 'foo://x.com/proj', - 'ppas': ['a/b']}) - self.assertEqual(str(cme.exception), 'Malformed build-git') + with self.assertRaises(ValueError) as mock_response_error: + self.submit.validate_git_request('testy', + 'C51', + 'ab', + **{'build-git': 'foo://x.com/proj', + 'ppas': ['a/b']}) + self.assertEqual(str(mock_response_error.exception), 'Malformed build-git') @patch('request.submit.Submit.is_valid_ppa') def test_unknown_param(self, is_valid_ppa): + """Tests unknown parameter passed""" is_valid_ppa.return_value = True - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_git_request('testy', 'C51', 'ab', **{'build-git': 'http://x.com/proj', 'ppas': ['a/b'], 'foo': 'bar'}) - self.assertEqual(str(cme.exception), 'Unsupported arguments: foo') + self.assertEqual(str(mock_response_error.exception), 'Unsupported arguments: foo') @patch('request.submit.Submit.is_valid_ppa') def test_bad_testname(self, is_valid_ppa): + """Tests a janky testname""" is_valid_ppa.return_value = True - with self.assertRaises(ValueError) as cme: + with self.assertRaises(ValueError) as mock_response_error: self.submit.validate_git_request('testy', 'C51', 'ab', **{'build-git': 'http://x.com/proj', 'testname': 'a !', 'ppas': ['a/b']}) - self.assertEqual(str(cme.exception), 'Malformed testname') + self.assertEqual(str(mock_response_error.exception), 'Malformed testname') @patch('request.submit.Submit.is_valid_ppa') def test_valid(self, is_valid_ppa): + """Tests a valid test to make sure it works""" is_valid_ppa.return_value = True self.submit.validate_git_request('testy', 'C51', 'ab', **{'build-git': 'http://x.com/proj', - 'env': ['STATUS_URL=https://api.github.com/proj/123deadbeef'], + 'env': ['STATUS_URL=' + \ + 'https://api.github.com/proj/123deadbeef'], 'ppas': ['a/b']}) @patch('request.submit.Submit.is_valid_ppa') def test_branch(self, is_valid_ppa): + """Tests a specific branch of a package""" is_valid_ppa.return_value = True self.submit.validate_git_request('testy', 'C51', 'ab', **{'build-git': 'http://x.com/proj#refs/pull/2/head', - 'env': ['STATUS_URL=https://api.github.com/proj/123deadbeef'], + 'env': ['STATUS_URL=' + \ + 'https://api.github.com/proj/123deadbeef'], 'ppas': ['a/b']}) @patch('request.submit.Submit.is_valid_ppa') def test_valid_testname(self, is_valid_ppa): + """Tests with a valid testname""" is_valid_ppa.return_value = True self.submit.validate_git_request('testy', 'C51', 'ab', **{'build-git': 'http://x.com/proj', 'testname': 'first', - 'env': ['STATUS_URL=https://api.github.com/proj/123deadbeef'], + 'env': ['STATUS_URL=' + \ + 'https://api.github.com/proj/123deadbeef'], 'ppas': ['a/b']}) @@ -448,6 +490,7 @@ class SendAMQPTests(SubmitTestBase): @patch('request.submit.amqp.Connection') @patch('request.submit.amqp.Message') def test_valid_request(self, message_con, mock_con): + """Tests a completely valid package""" # mostly a passthrough, but ensure that we do wrap the string in Message() message_con.side_effect = lambda x: '>%s<' % x @@ -465,6 +508,7 @@ class SendAMQPTests(SubmitTestBase): @patch('request.submit.amqp.Connection') @patch('request.submit.amqp.Message') def test_valid_request_context(self, message_con, mock_con): + """Tests with valid context""" # mostly a passthrough, but ensure that we do wrap the string in Message() message_con.side_effect = lambda x: '>%s<' % x diff --git a/charms/focal/autopkgtest-web/webcontrol/update-github-jobs b/charms/focal/autopkgtest-web/webcontrol/update-github-jobs index f2bb430..d4cd611 100755 --- a/charms/focal/autopkgtest-web/webcontrol/update-github-jobs +++ b/charms/focal/autopkgtest-web/webcontrol/update-github-jobs @@ -1,5 +1,8 @@ #!/usr/bin/python3 - +''' +Processes autopkgtest-cloud jobs requested from github +''' +#pylint: disable=no-value-for-parameter, consider-using-f-string, consider-using-ternary, invalid-name, bad-option-value import os import json import configparser @@ -8,7 +11,6 @@ import time import io import sys import tarfile -import time import urllib.request import urllib.parse from urllib.error import HTTPError @@ -17,13 +19,13 @@ from request.submit import Submit PENDING_DIR = '/run/autopkgtest_webcontrol/github-pending' -swift_url = None -external_url = None +SWIFT_URL = None +EXTERNAL_URL = None def result_matches_job(result_url, params): - # download result.tar and get exit code and testinfo - for retry in range(5): + """download result.tar and get exit code and testinfo""" + for _ in range(5): try: with urllib.request.urlopen(result_url + '/result.tar') as f: tar_bytes = io.BytesIO(f.read()) @@ -33,7 +35,7 @@ def result_matches_job(result_url, params): time.sleep(1) else: logging.error('failed to download result %s', result_url) - return + return -1 try: with tarfile.open(None, 'r', tar_bytes) as tar: @@ -41,18 +43,18 @@ def result_matches_job(result_url, params): info = json.loads(tar.extractfile('testinfo.json').read().decode()) except (KeyError, ValueError, tarfile.TarError) as e: logging.error('broken result %s: %s', result_url, e) - return + return -1 try: result_env = info['custom_environment'] except KeyError: logging.info('result has no custom_environment, ignoring') - return + return -1 # if the test result has the same parameters than the job, we have a winner if result_env != params['env']: logging.debug('exit code: %i, ignoring due to different test env: %s', exitcode, result_env) - return + return -1 logging.debug('exit code: %i, test env matches job: %s', exitcode, result_env) @@ -88,8 +90,9 @@ def finish_job(jobfile, params, code, log_url): def process_job(jobfile): + """Processes requested github job""" try: - with open(jobfile) as f: + with open(jobfile, encoding='utf-8') as f: params = json.load(f) mtime = os.fstat(f.fileno()).st_mtime except json.decoder.JSONDecodeError as e: @@ -105,7 +108,7 @@ def process_job(jobfile): container += '-' + params['ppas'][-1].replace('/', '-') except (KeyError, IndexError): pass - container_url = os.path.join(swift_url, container) + container_url = os.path.join(SWIFT_URL, container) package = params['package'] pkghash = package.startswith('lib') and package[:4] or package[0] timestamp = time.strftime('%Y%m%d_%H%M%S', time.gmtime(mtime)) @@ -126,7 +129,7 @@ def process_job(jobfile): code = result_matches_job(result_url, params) if code is not None: finish_job(jobfile, params, code, - result_url.replace(swift_url, external_url) + '/log.gz') + result_url.replace(SWIFT_URL, EXTERNAL_URL) + '/log.gz') break except HTTPError as e: logging.error('job %s URL %s failed: %s', os.path.basename(jobfile), query_url, e) @@ -143,11 +146,11 @@ if __name__ == '__main__': config = configparser.ConfigParser() config.read(os.path.expanduser('~ubuntu/autopkgtest-cloud.conf')) - swift_url = config['web']['SwiftURL'] + SWIFT_URL = config['web']['SwiftURL'] try: - external_url = config['web']['ExternalURL'] + EXTERNAL_URL = config['web']['ExternalURL'] except KeyError: - external_url = swift_url + EXTERNAL_URL = SWIFT_URL jobs = sys.argv[1:] @@ -156,4 +159,3 @@ if __name__ == '__main__': for job in jobs: process_job(os.path.join(PENDING_DIR, job)) - diff --git a/ci/lint_test b/ci/lint_test index e52edc4..06a4133 100755 --- a/ci/lint_test +++ b/ci/lint_test @@ -1,5 +1,5 @@ #!/usr/bin/python3 -# pylint: disable = invalid-name, broad-except, subprocess-run-check +# pylint: disable = broad-except, unnecessary-dict-index-lookup, bad-option-value ''' Script to lint the scripts in the autopkgtest-cloud repository in CI ''' @@ -8,33 +8,33 @@ import os import sys import logging import subprocess +import argparse -def check_for_extension(input_list, output_list, extension): +def check_for_extension(input_list, output_list, file_extension): ''' - Checks filepaths in a list for a given extension + Checks filepaths in a list for a given file_extension ''' - for a in input_list: - if os.path.isfile(a): - # if str(a)[-3:] == extension: - if extension in str(a)[-6:]: - output_list.append(str(a)) + for filepath in input_list: + if os.path.isfile(filepath): + if file_extension in str(filepath)[-6:]: + output_list.append(str(filepath)) return output_list -def check_for_shebang(input_list, output_list, shebang): +def check_for_shebang(input_list, output_list, shebang_for_check): ''' - Checks filepaths in a given list for a given shebang + Checks filepaths in a given list for a given shebang_for_check ''' - for b in input_list: - if os.path.isfile(b): + for filepath in input_list: + if os.path.isfile(filepath): try: - with open(b, 'r', encoding='utf-8') as myfile: + with open(filepath, 'r', encoding='utf-8') as myfile: file = myfile.read() into_list = file.splitlines() if len(into_list) > 1: - if into_list[0] == shebang: - output_list.append(str(b)) + if into_list[0] == shebang_for_check: + output_list.append(str(filepath)) except Exception as _: pass return output_list @@ -44,42 +44,56 @@ def remove_list_from_list(input_list, remove_list): ''' Removes elements from remove_list from input_list ''' - for ff in input_list: - if os.path.isfile(ff): - if str(ff) in remove_list: - input_list.remove(ff) + for list_elem in input_list: + if os.path.isfile(list_elem): + if str(list_elem) in remove_list: + input_list.remove(list_elem) return input_list def run_lint_command(files_to_lint, lint_command, arguments=None): ''' - Runs a given lint command over a list of filepaths and stores output + Runs given lint commands over a list of filepaths and stores output and exit code ''' - exit_codes = 0 - lint_output = "" - # check lint command exists - for f in files_to_lint: - if arguments is None: - cmd = [lint_command, f] - result = subprocess.run(cmd, stdout=subprocess.PIPE) - else: - cmd = [lint_command] - for arg in arguments.split(" "): - cmd.append(arg) - cmd.append(f) - result = subprocess.run(cmd, stdout=subprocess.PIPE) - lint_output += result.stdout.decode("utf-8") + "\n" - exit_codes += result.returncode - return lint_output, exit_codes + exit_codes = [] + lint_output = [] + lint_success = True + check_for_cmd = subprocess.run(["which", lint_command], stdout=subprocess.PIPE, check=False) + if check_for_cmd.returncode != 0: + logger.error("%s not present on system - please amend before using this script.", + lint_command) + sys.exit(1) + for file in files_to_lint: + if ".git" not in file: + if arguments is None: + cmd = [lint_command, file] + result = subprocess.run(cmd, stdout=subprocess.PIPE, check=False) + else: + cmd = [lint_command] + for arg in arguments.split(" "): + cmd.append(arg) + cmd.append(file) + result = subprocess.run(cmd, stdout=subprocess.PIPE, check=False) + lint_output.append(result.stdout.decode("utf-8") + "\n") + exit_codes.append(result.returncode) + if result.returncode != 0: + lint_success = False + return lint_output, exit_codes, lint_success -if __name__=="__main__": +if __name__ == "__main__": + # pylint: disable=invalid-name + parser = argparse.ArgumentParser(description="Args for lint test") + parser.add_argument('-v', + '--verbose', + help="Verbose output from lint test (y/n)", + action='store_true') + args = parser.parse_args() logging.basicConfig(level=logging.INFO) logger = logging.getLogger('autopkgtest-cloud-linter') - start_dir = "../" - repo_dir = pathlib.Path(start_dir) + repo_dir = pathlib.Path("../") repo_dir.rglob("*") final_list_of_python_files = [] @@ -90,25 +104,28 @@ if __name__=="__main__": "files": [], "extensions": [".py"], "shebangs": ["#!/usr/bin/python3"], - "args": None, - "output": "", - "code": 0 + "args": "--disable=E0012", + "output": [], + "code": [], + "success": False }, "shellcheck": { "files": [], "extensions": [".sh", ".bash"], "shebangs": ["#!/bin/bash", "#!/bin/sh"], "args": None, - "output": "", - "code": 0 + "output": [], + "code": [], + "success": False }, 'yamllint': { "files": ["../"], "extensions": None, "shebangs": None, "args": "--no-warnings", - "output": "", - "code": 0 + "output": [], + "code": [], + "success": False } } @@ -122,19 +139,23 @@ if __name__=="__main__": data[key]["files"] = check_for_shebang(all_files, data[key]["files"], shebang) all_files = remove_list_from_list(all_files, data[key]["files"]) data[key]["output"], \ - data[key]["code"] = run_lint_command(data[key]["files"], key, data[key]["args"]) - ecodesum = 0 - for _, oec in data.items(): - ecodesum += oec["code"] - if ecodesum > 0: + data[key]["code"], \ + data[key]["success"] = run_lint_command(data[key]["files"], key, data[key]["args"]) + + exit_code_sum = 0 + for _, oec_s in data.items(): + for e_c in oec_s["code"]: + exit_code_sum += e_c + if exit_code_sum > 0: for key, item in data.items(): - if item["code"] > 0: - # logger.info("%s output: \n%s", key, item["output"]) + if not item["success"]: logger.info("%s failed!", key) - # sys.exit(1) - # temporary exit code, will be set back to 1 when python and bash scripts have been linted - # right now we are just checking yaml files - if key == "yamllint" and item["code"] != 0: - sys.exit(1) - sys.exit(0) - sys.exit(0) \ No newline at end of file + if args.verbose: + for i in range(len(item["code"])): + if item["code"][i] != 0: + logger.info("%s", item["output"][i]) + else: + logger.info("%s passed!", key) + sys.exit(1) + logger.info("All the following linting tests passed: %s\n", list(data.keys())) + sys.exit(0) diff --git a/docs/conf.py b/docs/conf.py index a46ffdc..b4b923c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,7 +1,7 @@ ''' Configuration file for the Sphinx documentation builder. ''' -#pylint: disable=redefined-builtin +#pylint: disable=redefined-builtin, invalid-name # # This file only contains a selection of the most common options. For a full # list see the documentation: diff --git a/lxc-slave-admin/cmd b/lxc-slave-admin/cmd index ff991ff..0700964 100755 --- a/lxc-slave-admin/cmd +++ b/lxc-slave-admin/cmd @@ -1,6 +1,6 @@ #!/bin/sh set -e -MYDIR=`dirname $0` +MYDIR=$(dirname "${0}") if [ -z "$1" ]; then echo "Usage: $0 <hosts> <commands or .commands file>" >&2 @@ -8,11 +8,11 @@ if [ -z "$1" ]; then fi if [ "$1" = "all" ]; then - for f in $MYDIR/*.hosts; do + for f in "${MYDIR}"/*.hosts; do hosts="$hosts -h $f"; done else - if [ -e ${1} ]; then + if [ -e "${1}" ]; then hosts="-h ${1}" elif [ -e "${1}.hosts" ]; then hosts="-h ${1}.hosts" @@ -29,8 +29,8 @@ if [ "${1%.commands}" != "$1" ]; then exit 1 fi # command file - cat "$1" | parallel-ssh -x "-F $MYDIR/ssh_config" -OUserKnownHostsFile=/dev/null -OStrictHostKeyChecking=no -OIdentitiesOnly=yes $hosts -p8 -t 0 -i -I + parallel-ssh -x "-F ${MYDIR}/ssh_config" -OUserKnownHostsFile=/dev/null -OStrictHostKeyChecking=no -OIdentitiesOnly=yes "${hosts}" -p8 -t 0 -i -I < "${1}" else # command - parallel-ssh -x "-F $MYDIR/ssh_config" -OUserKnownHostsFile=/dev/null -OStrictHostKeyChecking=no -OIdentitiesOnly=yes $hosts -p8 -t 0 -i -- "$@" + parallel-ssh -x "-F ${MYDIR}/ssh_config" -OUserKnownHostsFile=/dev/null -OStrictHostKeyChecking=no -OIdentitiesOnly=yes "${hosts}" -p8 -t 0 -i -- "$@" fi diff --git a/mojo/make-lxd-secgroup b/mojo/make-lxd-secgroup index 634e598..1c1d961 100755 --- a/mojo/make-lxd-secgroup +++ b/mojo/make-lxd-secgroup @@ -1,5 +1,5 @@ #!/bin/sh - +# shellcheck disable=SC1090 set -eu # there's apparently no way to get this dynamically @@ -24,4 +24,4 @@ done if [ -n "${ROUTER_IP:-}" ]; then nova secgroup-add-rule lxd tcp 8443 8443 "${ROUTER_IP}/32" 2>/dev/null || true # perhaps it already existed -fi +fi \ No newline at end of file diff --git a/mojo/postdeploy b/mojo/postdeploy index 0f857ae..4b0f88f 100755 --- a/mojo/postdeploy +++ b/mojo/postdeploy @@ -11,5 +11,6 @@ if [ "${MOJO_STAGE_NAME}" == "staging" ]; then fi echo "Setting up the floating IP address of the front end..." -$(dirname $0)/add-floating-ip haproxy -$(dirname $0)/add-floating-ip rabbitmq-server +directory=$(dirname "{0}") +"${directory}"/add-floating-ip haproxy +"${directory}"/add-floating-ip rabbitmq-server
-- Mailing list: https://launchpad.net/~canonical-ubuntu-qa Post to : canonical-ubuntu-qa@lists.launchpad.net Unsubscribe : https://launchpad.net/~canonical-ubuntu-qa More help : https://help.launchpad.net/ListHelp