summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.maven-dockerignore2
-rw-r--r--Dockerfile8
-rw-r--r--imagescanner/imagescanner/__init__.py4
-rw-r--r--imagescanner/imagescanner/config.py66
-rw-r--r--imagescanner/imagescanner/frontend.py12
-rw-r--r--imagescanner/imagescanner/regexdispatch.py99
-rw-r--r--imagescanner/imagescanner/tasks.py156
-rw-r--r--imagescanner/imagescanner/tests/__init__.py38
-rw-r--r--imagescanner/imagescanner/tests/test_in_temp_dir.py60
-rw-r--r--imagescanner/imagescanner/tests/test_regexdispatch.py61
-rw-r--r--imagescanner/setup.py11
-rw-r--r--tox.ini17
12 files changed, 482 insertions, 52 deletions
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<hostname> # capture the hostname:
+ [^/:]* # anything up to the first / or :
+ )
+ .* # any number of path components
+ /(?P<filename> # 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<hostname> # 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
+