diff --git a/Makefile b/Makefile
index bdbe1116..2ce4b668 100644
--- a/Makefile
+++ b/Makefile
@@ -43,6 +43,10 @@ test: docs
coverage report -m
[ -f "/usr/bin/coveralls" ] && [ -n "$(COVERALLS_REPO_TOKEN)" ] && coveralls || echo
+check:
+ @echo "*** Running pylint ***"
+ ./tests/pylint/runpylint.sh
+
clean:
-rm -rf build src/pylorax/version.py
-rm -rf build src/composer/version.py
diff --git a/tests/lib/testlib.sh b/tests/lib/testlib.sh
new file mode 100644
index 00000000..896d8849
--- /dev/null
+++ b/tests/lib/testlib.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+# Shell functions for use by anaconda tests
+#
+# Copyright (C) 2014 Red Hat, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see .
+#
+# Author: David Shea
+
+# Print a list of files to test on stdout
+# Takes filter arguments identical to the find utility, for example
+# findtestfiles -name '*.py'. Note that pruning directories will not
+# work since find is passed a list of filenames as the path arguments.
+findtestfiles()
+{
+ # If the test is being run from a git work tree, use a list of all files
+ # known to git
+ if [ -d "${top_srcdir}/.git" ]; then
+ findpath=$(git ls-files -c "${top_srcdir}")
+ # Otherwise list everything under $top_srcdir
+ else
+ findpath="${top_srcdir} -type f"
+ fi
+
+ find $findpath "$@"
+}
diff --git a/tests/pylint/pylint-false-positives b/tests/pylint/pylint-false-positives
new file mode 100644
index 00000000..449a192e
--- /dev/null
+++ b/tests/pylint/pylint-false-positives
@@ -0,0 +1,3 @@
+F0401 import-error Unable to import 'pylorax.version'
+E0611 no-name-in-module No name 'version' in module 'pylorax'
+E1101 no-member Module 'pylorax' has no 'version' member
diff --git a/tests/pylint/pylint-one.sh b/tests/pylint/pylint-one.sh
new file mode 100755
index 00000000..2c9a40c1
--- /dev/null
+++ b/tests/pylint/pylint-one.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+#
+# $1 -- python source to run pylint on
+#
+
+if [ $# -lt 1 ]; then
+ # no source, just exit
+ exit 1
+fi
+
+file_suffix="$(eval echo \$$#|sed s?/?_?g)"
+
+pylint_output="$(pylint \
+ --msg-template='{path}:{line}: {msg_id} {symbol} {msg}' \
+ -r n --disable=C,R --rcfile=/dev/null \
+ --dummy-variables-rgx=_ \
+ --ignored-classes=Popen,TransactionSet \
+ --defining-attr-methods=__init__,_grabObjects,initialize,reset,start,setUp \
+ $DISABLED_WARN_OPTIONS \
+ $DISABLED_ERR_OPTIONS \
+ $NON_STRICT_OPTIONS "$@" 2>&1 | \
+ egrep -v -f "$FALSE_POSITIVES" \
+ )"
+
+# I0011 is the informational "Locally disabling ...." message
+if [ -n "$(echo "$pylint_output" | fgrep -v '************* Module ' |\
+ grep -v '^I0011:')" ]; then
+ # Replace the Module line with the actual filename
+ pylint_output="$(echo "$pylint_output" | sed "s|\* Module .*|* Module $(eval echo \$$#)|")"
+ echo "$pylint_output" > pylint-out_$file_suffix
+ touch "pylint-$file_suffix-failed"
+fi
diff --git a/tests/pylint/runpylint.sh b/tests/pylint/runpylint.sh
new file mode 100755
index 00000000..aab61d93
--- /dev/null
+++ b/tests/pylint/runpylint.sh
@@ -0,0 +1,139 @@
+#!/bin/bash
+# This script will check the project for any pylint warning and errors using a set
+# of options minimizing false positives, in combination with filtering of any
+# warning regularexpressions listed in pylint-false-positives.
+#
+# If any warnings are found they will be stored in pylint-log and printed
+# to stdout and this script will exit with a status of 1, if no (non filtered)
+# warnings are found it exits with a status of 0
+
+# XDG_RUNTIME_DIR is "required" to be set, so make one up in case something
+# actually tries to do something with it
+if [ -z "$XDG_RUNTIME_DIR" ]; then
+ export XDG_RUNTIME_DIR="$(mktemp -d)"
+ trap "rm -rf \"$XDG_RUNTIME_DIR\"" EXIT
+fi
+
+# If $top_srcdir is set, assume this is being run from automake and we don't
+# need to keep a separate log
+export pylint_log=0
+if [ -z "$top_srcdir" ]; then
+ export pylint_log=1
+fi
+
+# Unset TERM so that things that use readline don't output terminal garbage
+unset TERM
+
+# Don't try to connect to the accessibility socket
+export NO_AT_BRIDGE=1
+
+# Force the GDK backend to X11. Otherwise if no display can be found, Gdk
+# tries every backend type, which includes "broadway," which prints an error
+# and keeps changing the content of said error.
+export GDK_BACKEND=x11
+
+# If $top_srcdir has not been set by automake, import the test environment
+if [ -z "$top_srcdir" ]; then
+ top_srcdir="$(dirname "$0")/../.."
+ . ${top_srcdir}/tests/testenv.sh
+fi
+
+. ${top_srcdir}/tests/lib/testlib.sh
+
+srcdir="${top_srcdir}/tests/pylint"
+builddir="${top_builddir}/tests/pylint"
+
+# Need to add the pylint module directory to PYTHONPATH as well.
+export PYTHONPATH="${PYTHONPATH}:${srcdir}"
+
+# Save analysis data in the pylint directory
+export PYLINTHOME="${builddir}/.pylint.d"
+[ -d "$PYLINTHOME" ] || mkdir "$PYLINTHOME"
+
+export FALSE_POSITIVES="${srcdir}"/pylint-false-positives
+
+# W0212 - Access to a protected member %s of a client class
+export NON_STRICT_OPTIONS="--disable=W0212"
+
+# E1103 - %s %r has no %r member (but some types could not be inferred)
+export DISABLED_ERR_OPTIONS="--disable=E1103"
+
+# W0110 - map/filter on lambda could be replaced by comprehension
+# W0123 - Use of eval
+# W0141 - Used builtin function %r
+# W0142 - Used * or ** magic
+# W0511 - Used when a warning note as FIXME or XXX is detected.
+# W0603 - Using the global statement
+# W0613 - Unused argument %r
+# W0614 - Unused import %s from wildcard import
+# I0011 - Locally disabling %s (i.e., pylint: disable)
+# I0012 - Locally enabling %s (i.e., pylint: enable)
+# I0013 - Ignoring entire file (i.e., pylint: skip-file)
+export DISABLED_WARN_OPTIONS="--disable=W0110,W0123,W0141,W0142,W0511,W0603,W0613,W0614,I0011,I0012,I0013"
+
+usage () {
+ echo "usage: `basename $0` [--strict] [--help] [files...]"
+ exit $1
+}
+
+# Separate the module parameters from the files list
+ARGS=
+FILES=
+while [ $# -gt 0 ]; do
+ case $1 in
+ --strict)
+ export NON_STRICT_OPTIONS=
+ ;;
+ --help)
+ usage 0
+ ;;
+ -*)
+ ARGS="$ARGS $1"
+ ;;
+ *)
+ FILES=$@
+ break
+ esac
+ shift
+done
+
+exit_status=0
+
+if [ -s pylint-log ]; then
+ rm pylint-log
+fi
+
+# run pylint one file / module at a time, otherwise it sometimes gets
+# confused
+if [ -z "$FILES" ]; then
+ # Test any file that either ends in .py or contains #!/usr/bin/python in
+ # the first line. Scan everything except old_tests
+ FILES=$(findtestfiles \( -name '*.py' -o \
+ -exec /bin/sh -c "head -1 {} | grep -q '#!/usr/bin/python'" \; \) -print | \
+ egrep -v '(|/)old_tests/')
+fi
+
+num_cpus=$(getconf _NPROCESSORS_ONLN)
+# run pylint in paralel
+echo $FILES | xargs --max-procs=$num_cpus -n 1 "$srcdir"/pylint-one.sh $ARGS || exit 1
+
+for file in $(find -name 'pylint-out*'); do
+ cat "$file" >> pylint-log
+ rm "$file"
+done
+
+fails=$(find -name 'pylint*failed' -print -exec rm '{}' \;)
+if [ -z "$fails" ]; then
+ exit_status=0
+else
+ exit_status=1
+fi
+
+if [ -s pylint-log ]; then
+ echo "pylint reports the following issues:"
+ cat pylint-log
+elif [ -e pylint-log ]; then
+ rm pylint-log
+fi
+
+exit "$exit_status"
diff --git a/tests/testenv.sh b/tests/testenv.sh
new file mode 100644
index 00000000..48074422
--- /dev/null
+++ b/tests/testenv.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+if [ -z "$top_srcdir" ]; then
+ echo "*** top_srcdir must be set"
+ exit 1
+fi
+
+# If no top_builddir is set, use top_srcdir
+: "${top_builddir:=$top_srcdir}"
+
+if [ -z "$PYTHONPATH" ]; then
+ PYTHONPATH="${top_builddir}/src/:${top_srcdir}/tests/lib"
+else
+ PYTHONPATH="${PYTHONPATH}:${top_srcdir}/src/:${top_srcdir}:${top_srcdir}/tests/lib"
+fi
+
+export PYTHONPATH
+export top_srcdir
+export top_builddir