From 7c9c25ea2adceb67b855b5b54f457290dc5acc5d Mon Sep 17 00:00:00 2001 From: Edan Binshtok Date: Wed, 18 Oct 2017 08:24:06 +0300 Subject: Add MVN and Update Tox Add files needed for MVN build. Update tox definitions. Change-Id: I2bec9c59efab416dd234536b7500abd68eafb20f Issue-Id: VVP-28 Signed-off-by: Edan Binshtok --- .maven-dockerignore | 2 + Dockerfile | 8 +- imagescanner/imagescanner/__init__.py | 4 - imagescanner/imagescanner/config.py | 66 +++++++++ imagescanner/imagescanner/frontend.py | 12 +- imagescanner/imagescanner/regexdispatch.py | 99 +++++++++++++ imagescanner/imagescanner/tasks.py | 156 ++++++++++++++++----- imagescanner/imagescanner/tests/__init__.py | 38 +++++ .../imagescanner/tests/test_in_temp_dir.py | 60 ++++++++ .../imagescanner/tests/test_regexdispatch.py | 61 ++++++++ imagescanner/setup.py | 11 +- tox.ini | 17 ++- 12 files changed, 482 insertions(+), 52 deletions(-) create mode 100644 .maven-dockerignore create mode 100644 imagescanner/imagescanner/config.py create mode 100644 imagescanner/imagescanner/regexdispatch.py create mode 100644 imagescanner/imagescanner/tests/__init__.py create mode 100644 imagescanner/imagescanner/tests/test_in_temp_dir.py create mode 100644 imagescanner/imagescanner/tests/test_regexdispatch.py diff --git a/.maven-dockerignore b/.maven-dockerignore new file mode 100644 index 0000000..52d95d7 --- /dev/null +++ b/.maven-dockerignore @@ -0,0 +1,2 @@ +target/docker/ + diff --git a/Dockerfile b/Dockerfile index 24308f0..0a2ed72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -61,5 +61,11 @@ ENV IMAGESCANNER_LOGS_PATH=/var/log/imagescanner \ COPY imagescanner /opt/imagescanner COPY bin/* /usr/local/bin/ RUN mkdir -p $IMAGESCANNER_MOUNTPOINT /run/clamav $IMAGESCANNER_LOGS_PATH; chown clamav /run/clamav -RUN pip3 install flask requests celery[redis] /opt/imagescanner +RUN pip3 install \ + /opt/imagescanner \ + celery[redis] \ + flask \ + requests \ + requests-aws \ + ; : EXPOSE 80 diff --git a/imagescanner/imagescanner/__init__.py b/imagescanner/imagescanner/__init__.py index 83fc05f..490ff72 100644 --- a/imagescanner/imagescanner/__init__.py +++ b/imagescanner/imagescanner/__init__.py @@ -36,7 +36,3 @@ # # ECOMP is a trademark and service mark of AT&T Intellectual Property. # -import os -from pathlib import Path -LOGS_PATH = Path(os.environ['IMAGESCANNER_LOGS_PATH']) -STATUSFILE = LOGS_PATH/'status.txt' diff --git a/imagescanner/imagescanner/config.py b/imagescanner/imagescanner/config.py new file mode 100644 index 0000000..0ff1481 --- /dev/null +++ b/imagescanner/imagescanner/config.py @@ -0,0 +1,66 @@ +# ============LICENSE_START======================================================= +# org.onap.vvp/image-scanner +# =================================================================== +# Copyright © 2017 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the “License”); +# you may not use this software except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the “License”); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============LICENSE_END============================================ +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. +# +""" +To configure imagescanner for your environment, create a module named +imagescannerconfig containing overrides for the boilerplate settings listed +here and arrange for it to be accessible from your PYTHONPATH. + +""" +import os +from pathlib import Path + +# A mapping from host names to Requests Authentication Objects; see +# http://docs.python-requests.org/en/master/user/authentication/ +AUTHS = {} +LOGS_PATH = Path(os.getenv('IMAGESCANNER_LOGS_PATH', '.')) +STATUSFILE = LOGS_PATH/'status.txt' +# A dict passed as kwargs to jenkins.Jenkins constructor. +JENKINS = { + 'url': 'http://jenkins:8080', + 'username': '', + 'password': '', + } + +try: + from imagescannerconfig import * # noqa +except ImportError: + import warnings + warnings.warn( + "Could not import imagescannerconfig; default settings are" + " probably not what you want.") diff --git a/imagescanner/imagescanner/frontend.py b/imagescanner/imagescanner/frontend.py index e27648a..cc837b5 100644 --- a/imagescanner/imagescanner/frontend.py +++ b/imagescanner/imagescanner/frontend.py @@ -42,7 +42,7 @@ from flask import ( Flask, request, redirect, send_from_directory, url_for, render_template, ) import re -from . import STATUSFILE, LOGS_PATH +from . import config from .tasks import celery_app, request_scan app = Flask(__name__) @@ -55,7 +55,7 @@ celery_inspect = celery_app.control.inspect() def show_form(): # TODO: consider storing worker status/state directly in redis try: - with STATUSFILE.open() as fp: + with config.STATUSFILE.open() as fp: status = fp.read() except FileNotFoundError: status = '(No status information available)' @@ -64,10 +64,12 @@ def show_form(): 'form.html', channel=os.getenv('DEFAULT_SLACK_CHANNEL', ''), status=status, - active=(job + active=( + job for worker, jobs in (celery_inspect.active() or {}).items() for job in jobs), - reserved=(job + reserved=( + job for worker, jobs in (celery_inspect.reserved() or {}).items() for job in jobs), ) @@ -89,6 +91,6 @@ def show_result_log(hashval): if '/' in hashval: raise ValueError("Invalid character in hashval") return send_from_directory( - LOGS_PATH, + config.LOGS_PATH, "SecurityValidation-%s.txt" % hashval, ) diff --git a/imagescanner/imagescanner/regexdispatch.py b/imagescanner/imagescanner/regexdispatch.py new file mode 100644 index 0000000..80c9380 --- /dev/null +++ b/imagescanner/imagescanner/regexdispatch.py @@ -0,0 +1,99 @@ +# ============LICENSE_START======================================================= +# org.onap.vvp/image-scanner +# =================================================================== +# Copyright © 2017 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the “License”); +# you may not use this software except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the “License”); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============LICENSE_END============================================ +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. +# + +"""A single-dispatch mechanism using regular-expression matching. + +This API intentionally apes that of functools.singledispatch, for consistency. +Where functools.singledispatch dispatches on the type of the first argument, +regexdispatch dispatches on regex matching against the first argument. + +Match a given argument against a list of regular expressions; call the function +corresponding to the first one that matches. If none match, call the method +decorated with @regexdispatch. + + @regexdispatch + def foo(bar, baz): + '''A function that will dispatch among a list of functions.''' + print("None of the regexes matched against bar") + + @foo.register(r'[a-z]') + def _(bar, baz): + print("bar contains letters") + + @foo.register(r'[0-9]') + def _(bar, baz): + print("bar contains numbers") + +""" +import re +from functools import update_wrapper + + +class regexdispatch(object): + + def __init__(self, prototype): + update_wrapper(self, prototype) + self.prototype = prototype + self.registry = [] + + def register(self, regex, fn=None): + def make_handler(fn): + fn.regex = re.compile(regex) + self.registry.append(fn) + return fn + + if fn: + return make_handler(fn) + else: + return make_handler + + def __call__(self, arg, *args, **kwargs): + """Dispatch to first handler function whose regex matches arg. + + Pass through any extra provided arguments. + Pass named groups from handler's regex as keyword arguments. + Extra provided arguments override named groups from handler's regex. + + """ + for handler in self.registry: + mo = handler.regex.match(arg) + if mo is not None: + return handler(arg, *args, **dict(mo.groupdict(), **kwargs)) + else: + return self.prototype(arg, *args, **kwargs) diff --git a/imagescanner/imagescanner/tasks.py b/imagescanner/imagescanner/tasks.py index 3610373..61abf15 100644 --- a/imagescanner/imagescanner/tasks.py +++ b/imagescanner/imagescanner/tasks.py @@ -42,19 +42,23 @@ import re import hashlib import datetime from subprocess import run +from xml.etree import ElementTree from celery import Celery import requests -from . import STATUSFILE, LOGS_PATH +from . import config from .in_temp_dir import in_temp_dir - +from .regexdispatch import regexdispatch celery_app = Celery( broker='redis://redis', backend='redis://redis', ) -repo_re = re.compile(r'.*\.git$') -direct_re = re.compile(r'http.*\.(?:img|iso|qcow2)(?:\.gz)?$') -image_re = re.compile(r'.*\.(?:img|iso|qcow2)(?:\.gz)?$') + +# direct_re will match URLs pointing directly to an image to download, over +# http and https connections, and will capture the hostname and filename in +# named groups. This includes URLs to S3 and RadosGW endpoints. +# +image_re = re.compile(r'.*\.(?:img|iso|qcow2?)(?:\.gz)?$') SLACK_TOKEN = os.getenv('SLACK_TOKEN') DOMAIN = os.getenv('DOMAIN') @@ -70,7 +74,8 @@ def sha256(path): @celery_app.task(queue='scans', ignore_result=True) @in_temp_dir() -def request_scan(source, path, recipients): +def request_scan(source, path, recipients=None, jenkins_job_name=None, + checklist_uuid=None): """Retrieve and scan all partitions of (an) image(s), and notify of the results. @@ -88,6 +93,13 @@ def request_scan(source, path, recipients): complete. Currently, this may include Slack usernames and Slack channels. + jenkins_job_name: + The name of the jenkins job that should be built to process the scan + results. + + checklist_uuid: + The UUID of the checklist that should be passed to the jenkins job. + This function assumes the current working directory is a safe workarea for retrieving and manipulating images, but is decorated with in_temp_dir which changes to a new temporary directory upon invocation. @@ -96,7 +108,7 @@ def request_scan(source, path, recipients): # TODO printing to a status file is archaic and messy; let's use the python # logging framework or storing status in redis instead. - with STATUSFILE.open('w') as statusfile: + with config.STATUSFILE.open('w') as statusfile: print( "Processing request {source} {path} in {workspace}".format( @@ -105,7 +117,8 @@ def request_scan(source, path, recipients): flush=True) for image in retrieve_images(source, path): - print("- Image file: {}...".format(image), + print( + "- Image file: {}...".format(image), file=statusfile, flush=True) if not os.path.exists(image): raise ValueError("Path not found: {}".format(image)) @@ -113,12 +126,12 @@ def request_scan(source, path, recipients): print("-- Checksumming...", file=statusfile, flush=True) checksum = sha256(image) - print("-- Scanning...", - file=statusfile, flush=True) - logfile = LOGS_PATH / 'SecurityValidation-{}.txt'.format(checksum) + print("-- Scanning...", file=statusfile, flush=True) + logfile = config.LOGS_PATH / 'SecurityValidation-{}.txt'.format( + checksum) - #for partition in image_partitions(): - # result = scan_partition(partition) + # for partition in image_partitions(): + # result = scan_partition(partition) with open(logfile, 'w') as fd: print(datetime.datetime.utcnow().ctime(), "UTC", file=fd) print("Launching image scan for {} from {} {}".format( @@ -130,25 +143,55 @@ def request_scan(source, path, recipients): stderr=fd, ) - print("-- Scheduling notification (exit code:{})...".format(result.returncode), - file=statusfile, flush=True) + if recipients: + print( + "-- Scheduling notification (exit code: {})..." + .format(result.returncode), file=statusfile, flush=True) + + slack_notify.delay( + status="Success" if result.returncode == 0 else "Failure", + source=source, + filename=image, + checksum=checksum, + recipients=recipients, + ) - slack_notify.delay( - status="Success" if result.returncode == 0 else "Failure", - source=source, - filename=image, - checksum=checksum, - recipients=recipients, - ) + elif checklist_uuid and jenkins_job_name: + print( + "-- Triggering Jenkins job {} for checklist {}" + .format(jenkins_job_name, checklist_uuid), file=statusfile, + flush=True) + + jenkins_notify.delay( + jenkins_job_name, + status=result.returncode, + checksum=checksum, + checklist_uuid=checklist_uuid, + ) + + else: + print( + "-- Skipping notification (exit code was: {})." + .format(result.returncode), file=statusfile, flush=True) print("-- Done.", file=statusfile, flush=True) print("- All images processed.", file=statusfile, flush=True) + +@regexdispatch def retrieve_images(source, path): """Generate the filenames of one or multiple disk images as they are retrieved from _source_. + Source may be one of several types of source, so we dispatch to an + appropriate function to deal with it: + + - a git url to a repo containing disk images + - a normal https url directly to a single disk image + - an https url directly to a single disk image in a radosgw (s3) bucket + - an https url to a radosgw (s3) bucket containing disk images + See the docstring for request_scan for documentation of the source and path arguments. @@ -156,15 +199,11 @@ def retrieve_images(source, path): retrieving and manipulating images. """ - if repo_re.match(source): - return retrieve_images_git(source, path) - elif direct_re.match(source): - return retrieve_image_direct(source) - else: - raise ValueError("Unknown source format {}".format(source)) + raise ValueError("Unknown source type %s" % source) -def retrieve_images_git(source, path): +@retrieve_images.register(r'.*\.git$') +def _ri_git(source, path, **kwargs): run(['/usr/bin/git', 'clone', '--depth', '1', '--single-branch', @@ -188,22 +227,57 @@ def retrieve_images_git(source, path): yield os.path.join(root, name) -def retrieve_image_direct(source): - filename = re.search(r'[^/]*$', source).group(0) +# FIXME this regex won't properly detect URLs with query-strings. +@retrieve_images.register(r'''(?x) # this is a "verbose" regex + https?:// # match an http or https url + (?P # capture the hostname: + [^/:]* # anything up to the first / or : + ) + .* # any number of path components + /(?P # capture the filename after the last / + [^/]* # anything not a / + \.(?:img|iso|qcow2?) # with one of these three extensions + (?:\.gz)? # optionally also compressed + )$''') +def _ri_direct(source, path=None, hostname=None, filename=None, **kwargs): + auth = config.AUTHS.get(hostname) with open(filename, 'wb') as fd: - r = requests.get(source, stream=True) + r = requests.get(source, stream=True, auth=auth) for chunk in r.iter_content(chunk_size=4096): fd.write(chunk) yield filename -# FIXME the slack notification should go into a different queue than the image -# requests so they don't get blocked by the scans. +@retrieve_images.register(r'''(?x) # this is a "verbose" regex + https?:// # match an http or https url + (?P # capture the hostname: + [^/:]* # anything up to the first / or : + ) + .* # any number of path components + /$ # ending with a slash + ''') +def _ri_bucket(source, path=None, hostname=None, filename=None, **kwargs): + """We assume that an HTTP(s) URL ending in / is a radosgw bucket.""" + auth = config.AUTHS.get(hostname) + # We could request ?format=json but the output is malformed; all but one + # filename is truncated. + response = requests.get(source, {'format': 'xml'}, auth=auth) + keys = ElementTree.fromstring(response.text).iter( + '{http://s3.amazonaws.com/doc/2006-03-01/}Key') + filenames = [x.text for x in keys] + for filename in filenames: + if image_re.match(filename): + yield from retrieve_images(source + filename) + + @celery_app.task(ignore_result=True) def slack_notify(status, source, filename, checksum, recipients): if not SLACK_TOKEN: print("No Slack token defined; skipping notification.") return + if not recipients: + print("No recipients specified; skipping notification.") + return # TODO replace this handrolled code with a nice slack client library @@ -234,3 +308,17 @@ def slack_notify(status, source, filename, checksum, recipients): "https://hooks.slack.com/services/%s" % SLACK_TOKEN, json=dict(payload, channel=recipient), ) + + +@celery_app.task(ignore_result=True) +def jenkins_notify(name, status, checksum, checklist_uuid): + # The frontend does not need the jenkins library, so we perform the import + # it from within the worker task. + from jenkins import Jenkins + server = Jenkins(**config.JENKINS) + logurl = "http://{}/imagescanner/result/{}".format(DOMAIN, checksum) + server.build_job(name, { + "checklist_uuid": checklist_uuid, + "status": status, + "logurl": logurl, + }) diff --git a/imagescanner/imagescanner/tests/__init__.py b/imagescanner/imagescanner/tests/__init__.py new file mode 100644 index 0000000..490ff72 --- /dev/null +++ b/imagescanner/imagescanner/tests/__init__.py @@ -0,0 +1,38 @@ +# ============LICENSE_START======================================================= +# org.onap.vvp/image-scanner +# =================================================================== +# Copyright © 2017 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the “License”); +# you may not use this software except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the “License”); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============LICENSE_END============================================ +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. +# diff --git a/imagescanner/imagescanner/tests/test_in_temp_dir.py b/imagescanner/imagescanner/tests/test_in_temp_dir.py new file mode 100644 index 0000000..3ae7d35 --- /dev/null +++ b/imagescanner/imagescanner/tests/test_in_temp_dir.py @@ -0,0 +1,60 @@ +# ============LICENSE_START======================================================= +# org.onap.vvp/image-scanner +# =================================================================== +# Copyright © 2017 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the “License”); +# you may not use this software except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the “License”); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============LICENSE_END============================================ +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. +# +import os +from ..in_temp_dir import in_temp_dir + + +def test_in_temp_dir(): + with in_temp_dir() as workspace: + with open("temporary.txt", "w") as junkfile: + junkfile.write('') + assert os.path.exists(os.path.join(workspace, junkfile.name)) + assert not os.path.exists(os.path.join(workspace, junkfile.name)) + + +@in_temp_dir() +def make_some_junk(): + with open("temporary.txt", "w") as junkfile: + junkfile.write('') + return os.path.join(os.getcwd(), junkfile.name) + + +def test_method_in_temp_dir(): + junkfile = make_some_junk() + assert not os.path.exists(junkfile) diff --git a/imagescanner/imagescanner/tests/test_regexdispatch.py b/imagescanner/imagescanner/tests/test_regexdispatch.py new file mode 100644 index 0000000..704ec12 --- /dev/null +++ b/imagescanner/imagescanner/tests/test_regexdispatch.py @@ -0,0 +1,61 @@ +# ============LICENSE_START======================================================= +# org.onap.vvp/image-scanner +# =================================================================== +# Copyright © 2017 AT&T Intellectual Property. All rights reserved. +# =================================================================== +# +# Unless otherwise specified, all software contained herein is licensed +# under the Apache License, Version 2.0 (the “License”); +# you may not use this software except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# +# Unless otherwise specified, all documentation contained herein is licensed +# under the Creative Commons License, Attribution 4.0 Intl. (the “License”); +# you may not use this documentation except in compliance with the License. +# You may obtain a copy of the License at +# +# https://creativecommons.org/licenses/by/4.0/ +# +# Unless required by applicable law or agreed to in writing, documentation +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ============LICENSE_END============================================ +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. +# +from ..regexdispatch import regexdispatch + + +@regexdispatch +def dispatch_fixture(bar, baz=None): + '''A function that will dispatch among a list of functions.''' + return "other" + + +@dispatch_fixture.register(r'[a-z]') +def _letters_handler(bar, baz=None): + return "letters" + + +@dispatch_fixture.register(r'[0-9]') +def _numbers_handler(bar, baz=None): + return "numbers" + + +def test_basic_dispatch(): + assert dispatch_fixture("abc") == "letters" + assert dispatch_fixture("123") == "numbers" + assert dispatch_fixture("---") == "other" diff --git a/imagescanner/setup.py b/imagescanner/setup.py index f387e7d..47f63b8 100644 --- a/imagescanner/setup.py +++ b/imagescanner/setup.py @@ -44,5 +44,12 @@ setup( include_package_data=True, package_data={ 'imagescanner': ['templates/*'], - } - ) + }, + install_requires=[ + 'celery[redis]', + 'flask', + 'python-jenkins', + 'requests', + 'requests-aws', + ], + ), diff --git a/tox.ini b/tox.ini index 6a671ea..a0ea6fc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ + [tox] skipsdist=True -envlist = py27,py3,style +envlist = py3,style setupdir = imagescanner/ [testenv] @@ -9,15 +10,19 @@ commands = {envpython} --version pytest --version pytest --cov=imagescanner -deps = flake8 +deps = + flake8 pytest-cov pytest -[testenv:style] -commands = flake8 -[testenv:py27] -basepython=python2.7 +[testenv:style] +commands = python -m flake8 [testenv:py3] basepython=python3.6 + +[flake8] +show-source = True +exclude=venv-tox,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build + -- cgit 1.2.3-korg