From a3d2a6d6167fbff15355106f7093482c3374bdde Mon Sep 17 00:00:00 2001
From: Martin Magr <mmagr@redhat.com>
Date: Tue, 5 Feb 2013 16:03:13 +0100
Subject: [PATCH] Fail when Nova API, Nova network and Swift hosts are not set
 as IP addresses

- Refactor value validator system - cleanup, pep8, documentation, tests and allow multiple validators for each parameter

Fixes: rhbz#886603

Change-Id: I1bb3486ed7168bda2855ee877e32423033620ecc
---
 .gitignore                                    |   4 +-
 packstack/installer/engine_validators.py      | 534 ++++++------------
 packstack/installer/exceptions.py             |   9 +-
 packstack/installer/output_messages.py        |   4 +-
 packstack/installer/run_setup.py              | 134 ++---
 .../sample-project/plugins/createfile_101.py  |  62 --
 packstack/installer/setup_params.py           |  10 +-
 packstack/modules/ospluginutils.py            |   5 +-
 packstack/plugins/cinder_250.py               |  24 +-
 packstack/plugins/dashboard_500.py            |   5 +-
 packstack/plugins/glance_200.py               |   9 +-
 packstack/plugins/keystone_100.py             |  11 +-
 packstack/plugins/mysql_001.py                |   9 +-
 packstack/plugins/nova_300.py                 |  78 ++-
 packstack/plugins/openstack_client_400.py     |   5 +-
 packstack/plugins/prescript_000.py            |  13 +-
 packstack/plugins/qpid_002.py                 |   5 +-
 packstack/plugins/serverprep_901.py           |   5 +-
 packstack/plugins/sshkeys_000.py              |   4 +-
 packstack/plugins/swift_600.py                |  59 +-
 run_tests.sh                                  |   2 +
 tests/test.py                                 |  52 --
 tests/test_base.py                            |  76 +++
 tests/test_ospluginutils.py                   |   6 +-
 tests/test_plugin_serverprep.py               |   7 +-
 tests/test_validators.py                      |  82 +++
 26 files changed, 523 insertions(+), 691 deletions(-)
 delete mode 100644 packstack/installer/sample-project/plugins/createfile_101.py
 create mode 100755 run_tests.sh
 delete mode 100644 tests/test.py
 create mode 100644 tests/test_base.py
 create mode 100644 tests/test_validators.py

diff --git a/.gitignore b/.gitignore
index bd95d0df6..726885683 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
-*.pyc
+*.py[co]
 *.swp
 *.log
+.tox
+packstack.egg-info
diff --git a/packstack/installer/engine_validators.py b/packstack/installer/engine_validators.py
index 1f9ffd229..35053e855 100644
--- a/packstack/installer/engine_validators.py
+++ b/packstack/installer/engine_validators.py
@@ -1,411 +1,207 @@
+# -*- coding: utf-8 -*-
+
 """
-contains all available validation functions
+Contains all core validation functions.
 """
-import common_utils as utils
-import re
-import logging
-import output_messages
-import basedefs
-import socket
-import types
-import traceback
+
 import os
-import os.path
+import re
+import socket
+import logging
 import tempfile
-from setup_controller import Controller
+import traceback
+
+import basedefs
+import common_utils as utils
+
+from .setup_controller import Controller
+from .exceptions import ParamValidationError
 
 
+__all__ = ('ParamValidationError', 'validate_integer',
+           'validate_regexp', 'validate_port', 'validate_not_empty',
+           'validate_options', 'validate_ip', 'validate_multi_ip',
+           'validate_file', 'validate_ping', 'validate_ssh',
+           'validate_multi_ssh')
 
-# XXX: Validators should probably only validate (pass on success,
-#      raise appropriate exception on failure). We should move logging
-#      and printing probably to run_setup (preferably to single place)
 
-def validateDirSize(path, size):
-    availableSpace = utils.getAvailableSpace(_getBasePath(path))
-    if availableSpace < size:
-        print output_messages.INFO_VAL_PATH_SPACE % (path,
-                                                     utils.transformUnits(availableSpace),
-                                                     utils.transformUnits(size))
-        return False
-    return True
-
-def validateInteger(param, options=[]):
+def validate_integer(param, options=None):
+    """
+    Raises ParamValidationError if given param is not integer.
+    """
+    options = options or []
     try:
         int(param)
-        return True
-    except:
-        logging.warn("validateInteger('%s') - failed" %(param))
-        print output_messages.INFO_VAL_NOT_INTEGER
-        return False
+    except ValueError:
+        logging.debug('validate_integer(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given value is not an integer: %s'
+        raise ParamValidationError(msg % param)
 
-def validateRe(param, options=[]):
+
+def validate_regexp(param, options=None):
+    """
+    Raises ParamValidationError if given param doesn't match at least
+    one of regular expressions given in options.
+    """
+    options = options or []
     for regex in options:
         if re.search(regex, param):
-            return True
-    logging.warn("validateRe('%s') - failed" %(param))
-    return False
+            break
+    else:
+        logging.debug('validate_regexp(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given value does not match required regular expresion: %s'
+        raise ParamValidationError(msg % param)
 
-def validatePort(param, options = []):
-    #TODO: add actual port check with socket open
-    logging.debug("Validating %s as a valid TCP Port" % (param))
-    minVal = 0
-    controller = Controller()
-    isProxyEnabled = utils.compareStrIgnoreCase(controller.CONF["OVERRIDE_HTTPD_CONFIG"], "yes")
-    if not isProxyEnabled:
-        minVal = 1024
-    if not validateInteger(param, options):
-        return False
+
+def validate_port(param, options=None):
+    """
+    Raises ParamValidationError if given param is not a decimal number
+    in range (0, 65535).
+    """
+    options = options or []
+    validate_integer(param, options)
     port = int(param)
-    if not (port > minVal and port < 65535) :
-        logging.warn(output_messages.INFO_VAL_PORT_NOT_RANGE %(minVal))
-        print output_messages.INFO_VAL_PORT_NOT_RANGE %(minVal)
-        return False
-    (portOpen, process, pid) = utils.isTcpPortOpen(param)
-    if portOpen:
-        logging.warn(output_messages.INFO_VAL_PORT_OCCUPIED % (param, process, pid))
-        print output_messages.INFO_VAL_PORT_OCCUPIED % (param, process, pid)
-        return False
-    if isProxyEnabled and not checkAndSetHttpdPortPolicy(param):
-        logging.warn(output_messages.INFO_VAL_FAILED_ADD_PORT_TO_HTTP_POLICY, port)
-        print output_messages.INFO_VAL_FAILED_ADD_PORT_TO_HTTP_POLICY % port
-        return False
-    return True
+    if not (port >= 0 and port < 65535):
+        logging.debug('validate_port(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given value is outside the range of (0, 65535): %s'
+        raise ParamValidationError(msg % param)
 
-def checkAndSetHttpdPortPolicy(port):
-    def parsePorts(portsStr):
-        ports = []
-        for part in portsStr.split(","):
-            part = part.strip().split("-")
-            if len(part) > 1:
-                for port in range(int(part[0]),int(part[1])):
-                    ports.append(port)
-            else:
-                ports.append(int(part[0]))
-        return ports
 
-    newPort = int(port)
-    cmd = [
-        basedefs.EXEC_SEMANAGE, "port", "-l",
-    ]
-    out, rc = utils.execCmd(cmdList=cmd) #, "-t", "http_port_t"])
-    if rc:
-        return False
-    httpPortsList = []
-    pattern = re.compile("^http_port_t\s*tcp\s*([0-9, \-]*)$")
-    for line in out.splitlines():
-        httpPortPolicy = re.match(pattern, line)
-        if httpPortPolicy:
-            httpPortsList = parsePorts(httpPortPolicy.groups()[0])
-    logging.debug("http_port_t = %s"%(httpPortsList))
-    if newPort in httpPortsList:
-        return True
+def validate_not_empty(param, options=None):
+    """
+    Raises ParamValidationError if given param is empty.
+    """
+    options = options or []
+    if not param and param is not False:
+        logging.debug('validate_not_empty(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given value is not allowed: %s'
+        raise ParamValidationError(msg % param)
+
+
+def validate_options(param, options=None):
+    """
+    Raises ParamValidationError if given param is not member of options.
+    """
+    options = options or []
+
+    validate_not_empty(param, options)
+    if param not in options:
+        logging.debug('validate_options(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given value is not is allowed values %s: %s'
+        raise ParamValidationError(msg % (options, param))
+
+
+def validate_ip(param, options=None):
+    """
+    Raises ParamValidationError if given parameter value is not in IPv4
+    or IPv6 address.
+    """
+    for family in (socket.AF_INET, socket.AF_INET6):
+        try:
+            socket.inet_pton(family, param)
+            break
+        except socket.error:
+            continue
     else:
-        cmd = [
-            basedefs.EXEC_SEMANAGE,
-            "port",
-            "-a",
-            "-t", "http_port_t",
-            "-p", "tcp",
-            "%d"%(newPort),
-        ]
-        out, rc = utils.execCmd(cmdList=cmd, failOnError=False, usePipeFiles=True)
-        if rc:
-            logging.error(out)
-            return False
-    return True
+        logging.debug('validate_ip(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given host is not in IP address format: %s'
+        raise ParamValidationError(msg % param)
 
 
-
-def validateRemotePort(param, options = []):
-    #Validate that the port is an integer betweeen 1024 and 65535
-    logging.debug("Validating %s as a valid TCP Port" % (param))
-    if validateInteger(param, options):
-        port = int(param)
-        if (port > 0 and port < 65535):
-            return True
-        else:
-            logging.warn("validatePort('%s') - failed" %(param))
-            print output_messages.INFO_VAL_PORT_NOT_RANGE
-
-    return False
-
-def validateStringNotEmpty(param, options=[]):
-    if type(param) != types.StringType or len(param) == 0:
-        logging.warn("validateStringNotEmpty('%s') - failed" %(param))
-        print output_messages.INFO_VAL_STRING_EMPTY %(param)
-        return False
-    else:
-        return True
-
-def validateOptions(param, options=[]):
-    logging.info("Validating %s as part of %s"%(param, options))
-    if not validateStringNotEmpty(param, options):
-        return False
-    if "yes" in options and param.lower() == "y":
-        return True
-    if "no" in options and param.lower() == "n":
-        return True
-    if param.lower() in [option.lower() for option in options]:
-        return True
-    print output_messages.INFO_VAL_NOT_IN_OPTIONS % (", ".join(options))
-    return False
-
-def validateDomain(param, options=[]):
+def validate_multi_ip(param, options=None):
     """
-    Validate domain name
+    Raises ParamValidationError if comma separated IP addresses given
+    parameter value are in IPv4 or IPv6 aformat.
     """
-    logging.info("validating %s as a valid domain string" % (param))
-    (errMsg, rc) = _validateString(param, 1, 1024, "^[\w\-\_]+\.[\w\.\-\_]+\w+$")
+    for host in param.split(','):
+        host, device = host.split('/', 1)
+        validate_ip(host.strip(), options)
 
-    # Right now we print a generic error, might want to change it in the future
-    if rc != 0:
-        print output_messages.INFO_VAL_NOT_DOMAIN
-        return False
-    else:
-        return True
 
-def validateUser(param, options=[]):
+def validate_file(param, options=None):
     """
-    Validate Auth Username
-    Setting a logical max value of 256
+    Raises ParamValidationError if provided file in param does not exist.
     """
-    logging.info("validating %s as a valid user name" % (param))
-    (errMsg, rc) = _validateString(param, 1, 256, "^\w[\w\.\-\_\%\@]{2,}$")
-
-    # Right now we print a generic error, might want to change it in the future
-    if rc != 0:
-        print output_messages.INFO_VAL_NOT_USER
-        return False
-    else:
-        return True
-
-def validateRemoteHost(param, options=[]):
-    """ Validate that the we are working with remote DB host
-    """
-    # If we received localhost, use default flow.
-    # If not local, REMOTE_DB group is run.
-    # It means returning True if remote, and False if local
-
-    if "DB_REMOTE_INSTALL" in param.keys() and param["DB_REMOTE_INSTALL"] == "remote":
-        return True
-    else:
-        return False
-
-def validateFQDN(param, options=[]):
-    logging.info("Validating %s as a FQDN"%(param))
-    if not validateDomain(param,options):
-        return False
-    try:
-        #get set of IPs
-        ipAddresses = utils.getConfiguredIps()
-        if len(ipAddresses) < 1:
-            logging.error("Could not find any configured IP address on the host")
-            raise Exception(output_messages.ERR_EXP_CANT_FIND_IP)
-
-        #resolve fqdn
-        pattern = 'Address: (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
-        resolvedAddresses = _getPatternFromNslookup(param, pattern)
-        if len(resolvedAddresses) < 1:
-            logging.error("Failed to resolve %s"%(param))
-            print output_messages.ERR_DIDNT_RESOLVED_IP%(param)
-            return False
-
-        #string is generated here since we use it in all latter error messages
-        prettyString = " ".join(["%s"%string for string in resolvedAddresses])
-
-        #compare found IP with list of local IPs and match.
-        if not resolvedAddresses.issubset(ipAddresses):
-            logging.error("the following address(es): %s are not configured on this host"%(prettyString))
-            #different grammar for plural and single
-            if len(resolvedAddresses) > 1:
-                print output_messages.ERR_IPS_NOT_CONFIGED%(prettyString, param)
-            else:
-                print output_messages.ERR_IPS_NOT_CONFIGED_ON_INT%(prettyString, param)
-            return False
-
-        #reverse resolved IP and compare with given fqdn
-        counter = 0
-        pattern = '[\w\.-]+\s+name\s\=\s([\w\.\-]+)\.'
-        for address in resolvedAddresses:
-            addressSet = _getPatternFromNslookup(address, pattern)
-            reResolvedAddress = None
-            if len(addressSet) > 0:
-                reResolvedAddress = addressSet.pop()
-            if reResolvedAddress == param:
-                counter += 1
-            else:
-                logging.warn("%s did not reverse-resolve into %s"%(address,param))
-        if counter < 1:
-            logging.error("The following addresses: %s did not reverse resolve into %s"%(prettyString, param))
-            #different grammar for plural and single
-            if len(resolvedAddresses) > 1:
-                print output_messages.ERR_IPS_HAS_NO_PTR%(prettyString, param)
-            else:
-                print output_messages.ERR_IP_HAS_NO_PTR%(prettyString, param)
-            return False
-
-        #conditions passed
-        return True
-    except:
-        logging.error(traceback.format_exc())
-        raise
-
-def validateFile(param, options=[]):
-    """
-    Check that provided param is a file
-    """
-    if not validateStringNotEmpty(param):
-        return False
+    options = options or []
+    validate_not_empty(param)
 
     if not os.path.isfile(param):
-        print "\n" + output_messages.ERR_FILE + ".\n"
-        return False
+        logging.debug('validate_file(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given file does not exist: %s'
+        raise ParamValidationError(msg % param)
 
-    return True
 
-def validatePing(param, options=[]):
+def validate_ping(param, options=None):
     """
-    Check that provided host answers to ping
+    Raises ParamValidationError if provided host does not answer to ICMP
+    echo request.
     """
-    if validateStringNotEmpty(param):
-        cmd = [
-            "/bin/ping",
-            "-c", "1",
-            "%s" % param,
-        ]
-        out, rc = utils.execCmd(cmdList=cmd)
-        if rc == 0:
-            return True
+    options = options or []
+    validate_not_empty(param)
 
-    print "\n" + output_messages.ERR_PING + " %s .\n"%param
-    return False
+    cmd = ["/bin/ping", "-c", "1", str(param)]
+    out, rc = utils.execCmd(cmdList=cmd)
+    if rc != 0:
+        logging.debug('validate_ping(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given host is unreachable: %s'
+        raise ParamValidationError(msg % param)
 
-def validateMultiPing(param, options=[]):
-    if validateStringNotEmpty(param):
-        hosts = param.split(",")
-        for host in hosts:
-            if validatePing(host.strip()) == False:
-                return False
-        return True
-    print "\n" + output_messages.ERR_PING + ".\n"
-    return False
 
-_testedPorts = []
-def validatePort(host, port):
+def validate_multi_ping(param, options=None):
     """
-    Check that provided host is listening on provided port
+    Raises ParamValidationError if comma separated host given in param
+    do not answer to ICMP echo request.
     """
-    key = "%s:%d"%(host, port)
-    # No need to keep checking the same port multiple times
-    if key in _testedPorts:
-        return True
+    options = options or []
+    validate_not_empty(param)
+    for host in param.split(","):
+        validate_ping(host.strip())
+
+
+_tested_ports = []
+def touch_port(host, port):
+    """
+    Check that provided host is listening on provided port.
+    """
+    key = "%s:%d" % (host, port)
+    if key in _tested_ports:
+        return
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect((host, port))
+    s.shutdown(socket.SHUT_RDWR)
+    s.close()
+    _tested_ports.append(key)
+
+
+def validate_ssh(param, options=None):
+    """
+    Raises ParamValidationError if provided host does not listen
+    on port 22.
+    """
+    options = options or []
     try:
-        s.connect((host, port))
-        s.shutdown(socket.SHUT_RDWR)
-        s.close()
-    except socket.error as msg:
-        return False
-    _testedPorts.append(key)
-    return True
+        touch_port(param.strip(), 22)
+    except socket.error:
+        logging.debug('validate_ssh(%s, options=%s) failed.' %
+                      (param, options))
+        msg = 'Given host does not listen on port 22: %s'
+        raise ParamValidationError(msg % param)
 
-def validateSSH(param, options=[]):
+
+def validate_multi_ssh(param, options=None):
     """
-    Check that provided host is listening on port 22
+    Raises ParamValidationError if comma separated host provided
+    in param do not listen on port 22.
     """
-    if validatePort(param.strip(), 22):
-        return True
-    print "\n" + output_messages.ERR_SSH%param
-    return False
-
-def validateMultiSSH(param, options=[]):
-    if validateStringNotEmpty(param):
-        hosts = param.split(",")
-        for host in hosts:
-            if validateSSH(host) == False:
-                return False
-        return True
-    print "\n" + output_messages.ERR_SSH%param + ".\n"
-    return False
-
-def _validateString(string, minLen, maxLen, regex=".*"):
-    """
-    Generic func to verify a string
-    match its min/max length
-    and doesn't contain illegal chars
-
-    The func returns various return codes according to the error
-    plus a default error message
-    the calling func can decide if to use to default error msg
-    or to use a more specific one according the RC.
-    Return codes:
-    1 - string length is less than min
-    2 - string length is more tham max
-    3 - string contain illegal chars
-    0 - success
-    """
-    # String length is less than minimum allowed
-    if len(string) < minLen:
-        msg = output_messages.INFO_STRING_LEN_LESS_THAN_MIN % (minLen)
-        return(msg, 1)
-    # String length is more than max allowed
-    elif len(string) > maxLen:
-        msg = output_messages.INFO_STRING_EXCEEDS_MAX_LENGTH % (maxLen)
-        return(msg, 2)
-    # String contains illegal chars
-    elif not utils.verifyStringFormat(string, regex):
-        return(output_messages.INFO_STRING_CONTAINS_ILLEGAL_CHARS, 3)
-    else:
-        # Success
-        return (None, 0)
-
-def _getPatternFromNslookup(address, pattern):
-    rePattern = re.compile(pattern)
-    addresses = set()
-    output = utils.nslookup(address)
-    list = output.splitlines()
-    #do not go over the first 2 lines in nslookup output
-    for line in list[2:]:
-        found = rePattern.search(line)
-        if found:
-            foundAddress = found.group(1)
-            logging.debug("%s resolved into %s"%(address, foundAddress))
-            addresses.add(foundAddress)
-    return addresses
-
-def _getBasePath(path):
-    if os.path.exists(path):
-        return path
-
-    # Iterate up in the tree structure until we get an
-    # existing path
-    return _getBasePath(os.path.dirname(path.rstrip("/")))
-
-def _isPathWriteable(path):
-    try:
-        logging.debug("attempting to write temp file to %s" % (path))
-        tempfile.TemporaryFile(dir=path)
-        return True
-    except:
-        logging.warning(traceback.format_exc())
-        logging.warning("%s is not writeable" % path)
-        return False
-
-def r_validateIF(server, device):
-    """ Validate that a network interface exists on a remote host """
-    server.append("ifconfig %s || ( echo Device %s does not exist && exit 1 )"%(device, device))
-
-def r_validateDevice(server, device=None):
-    if device:
-        # the device MUST exist
-        server.append('ls -l /dev/%s'%device)
-
-        # if it is not mounted then we can use it
-        server.append('grep "/dev/%s " /proc/self/mounts || exit 0'%device)
-
-        # if it is mounted then the mount point has to be in /srv/node
-        server.append('grep "/dev/%s /srv/node" /proc/self/mounts && exit 0'%device)
-
-        # if we got here without exiting then we can't use this device
-        server.append('exit 1')
-    return False
+    options = options or []
+    validate_not_empty(param)
+    for host in param.split(","):
+        validate_ssh(host)
diff --git a/packstack/installer/exceptions.py b/packstack/installer/exceptions.py
index 101ab9e9a..26c233596 100644
--- a/packstack/installer/exceptions.py
+++ b/packstack/installer/exceptions.py
@@ -20,22 +20,25 @@ class PackStackError(Exception):
 
 
 class MissingRequirements(PackStackError):
-    """Raised when minimum install requirements are not met"""
+    """Raised when minimum install requirements are not met."""
     pass
 
 
 class InstallError(PackStackError):
     pass
 
-
 class FlagValidationError(InstallError):
+    """Raised when single flag validation fails."""
+    pass
+
+class ParamValidationError(InstallError):
+    """Raised when parameter value validation fails."""
     pass
 
 
 class PluginError(PackStackError):
     pass
 
-
 class ParamProcessingError(PluginError):
     pass
 
diff --git a/packstack/installer/output_messages.py b/packstack/installer/output_messages.py
index 17b1b590c..7858e1566 100644
--- a/packstack/installer/output_messages.py
+++ b/packstack/installer/output_messages.py
@@ -40,8 +40,8 @@ INFO_VAL_PORT_OCCUPIED="Error: TCP Port %s is already open by %s (pid: %s)"
 INFO_VAL_PORT_OCCUPIED_BY_JBOSS="Error: TCP Port %s is used by JBoss"
 INFO_VAL_PASSWORD_DONT_MATCH="Error: passwords don't match"
 
-INFO_VAL_IS_HOSTNAME = ("Packstack changed given hostname %s to IP "
-                        "address %s.")
+INFO_CHANGED_VALUE = ("Packstack changed given value %s to required "
+                      "value %s")
 WARN_VAL_IS_HOSTNAME = ("Warning: Packstack failed to change given "
                         "hostname %s to IP address. Note that some "
                         "services might not run correctly when hostname"
diff --git a/packstack/installer/run_setup.py b/packstack/installer/run_setup.py
index 8d27f204c..cc39cede6 100644
--- a/packstack/installer/run_setup.py
+++ b/packstack/installer/run_setup.py
@@ -17,7 +17,7 @@ import common_utils as utils
 import engine_processors as process
 import engine_validators as validate
 import output_messages
-from .exceptions import FlagValidationError
+from .exceptions import FlagValidationError, ParamValidationError
 
 from setup_controller import Controller
 
@@ -77,8 +77,9 @@ def _getInputFromUser(param):
                     message = StringIO()
                     message.write(param.getKey("PROMPT"))
 
-                    if not param.getKey("VALIDATION_FUNC") == validate.validateRe \
-                                                and param.getKey("OPTION_LIST"):
+                    validators = param.getKey("VALIDATORS") or []
+                    if validate.validate_regexp not in validators \
+                       and param.getKey("OPTION_LIST"):
                         message.write(" [%s]" % "|".join(param.getKey("OPTION_LIST")))
 
                     if param.getKey("DEFAULT_VALUE"):
@@ -99,62 +100,33 @@ def _getInputFromUser(param):
                     userInput = param.getKey("DEFAULT_VALUE")
 
                 # Param processing
-                try:
-                    logging.debug("Processing value of parameter "
-                                  "%s." % param.getKey("CONF_NAME"))
-                    processFunc = param.getKey("PROCESSOR_FUNC")
-                    try:
-                        processArgs = param.getKey("PROCESSOR_ARGS")
-                    except KeyError:
-                        processArgs = None
-                    try:
-                        _userInput = processFunc(userInput, processArgs)
-                        if userInput != _userInput:
-                            msg = output_messages.INFO_VAL_IS_HOSTNAME
-                            print msg % (userInput, _userInput)
-                            userInput = _userInput
-                        else:
-                            logging.debug("Processor returned the original "
-                                          "value: %s" % _userInput)
-                    except process.ParamProcessingError, ex:
-                        try:
-                            cn = param.getKey("CONF_NAME")
-                            msg = param.getKey("PROCESSOR_MSG")
-                            print getattr(output_messages, msg) % cn
-                        except KeyError:
-                            logging.debug("Value processing of parameter "
-                                          "%s failed." % param.getKey("CONF_NAME"))
-                except KeyError:
-                    logging.debug("Parameter %s doesn't have value "
-                                  "processor." % param.getKey("CONF_NAME"))
+                userInput = process_param_value(param, userInput)
 
                 # If param requires validation
-                if param.getKey("VALIDATION_FUNC")(userInput, param.getKey("OPTION_LIST")):
-                    if "yes" in param.getKey("OPTION_LIST") and userInput.lower() == "y":
-                        userInput = "yes"
-                    if "no" in param.getKey("OPTION_LIST") and userInput.lower() == "n":
-                        userInput = "no"
+                try:
+                    validate_param_value(param, userInput)
                     controller.CONF[param.getKey("CONF_NAME")] = userInput
                     loop = False
-                # If validation failed but LOOSE_VALIDATION is true, ask user
-                elif param.getKey("LOOSE_VALIDATION"):
-                    answer = _askYesNo("User input failed validation, do you still wish to use it")
-                    if answer:
-                        loop = False
-                        controller.CONF[param.getKey("CONF_NAME")] = userInput
+                except ParamValidationError:
+                    if param.getKey("LOOSE_VALIDATION"):
+                        # If validation failed but LOOSE_VALIDATION is true, ask user
+                        answer = _askYesNo("User input failed validation, "
+                                           "do you still wish to use it")
+                        loop = not answer
+                        if answer:
+                            controller.CONF[param.getKey("CONF_NAME")] = userInput
+                            continue
+                        else:
+                            if commandLineValues.has_key(param.getKey("CONF_NAME")):
+                                del commandLineValues[param.getKey("CONF_NAME")]
                     else:
+                        # Delete value from commandLineValues so that we will prompt the user for input
                         if commandLineValues.has_key(param.getKey("CONF_NAME")):
                             del commandLineValues[param.getKey("CONF_NAME")]
                         loop = True
-                else:
-                    # Delete value from commandLineValues so that we will prompt the user for input
-                    if commandLineValues.has_key(param.getKey("CONF_NAME")):
-                        del commandLineValues[param.getKey("CONF_NAME")]
-                    loop = True
-
     except KeyboardInterrupt:
         print "" # add the new line so messages wont be displayed in the same line as the question
-        raise KeyboardInterrupt
+        raise
     except:
         logging.error(traceback.format_exc())
         raise Exception(output_messages.ERR_EXP_READ_INPUT_PARAM % (param.getKey("CONF_NAME")))
@@ -172,7 +144,7 @@ def input_param(param):
         confirmedParamName = param.getKey("CONF_NAME") + "_CONFIRMED"
         confirmedParam.setKey("CONF_NAME", confirmedParamName)
         confirmedParam.setKey("PROMPT", output_messages.INFO_CONF_PARAMS_PASSWD_CONFIRM_PROMPT)
-        confirmedParam.setKey("VALIDATION_FUNC", validate.validateStringNotEmpty)
+        confirmedParam.setKey("VALIDATORS", [validate.validate_not_empty])
         # Now get both values from user (with existing validations
         while True:
             _getInputFromUser(param)
@@ -285,35 +257,39 @@ def maskString(str):
             str = str.replace(password, '*'*8)
     return str
 
-def _validateParamValue(param, paramValue):
-    validateFunc = param.getKey("VALIDATION_FUNC")
-    optionsList  = param.getKey("OPTION_LIST")
-    logging.debug("validating param %s in answer file." % param.getKey("CONF_NAME"))
-    if not validateFunc(paramValue, optionsList):
-        raise Exception(output_messages.ERR_EXP_VALIDATE_PARAM % param.getKey("CONF_NAME"))
+def validate_param_value(param, value):
+    cname = param.getKey("CONF_NAME")
+    logging.debug("Validating parameter %s." % cname)
 
-def _processParamValue(param, paramValue):
-    try:
-        processFunc = param.getKey("PROCESSOR_FUNC")
-    except KeyError:
-        return paramValue
-    try:
-        processArgs = param.getKey("PROCESSOR_ARGS")
-    except KeyError:
-        processArgs = None
-    logging.debug("processing param %s in answer file." % param.getKey("CONF_NAME"))
-    try:
-        return processFunc(paramValue, processArgs)
-    except process.ParamProcessingError, ex:
-        cn = param.getKey("CONF_NAME")
-        logging.debug("processing param %s failed, falling back to "
-                      "original, reason: %s" % (cn, ex))
+    validators = param.getKey("VALIDATORS") or []
+    opt_list = param.getKey("OPTION_LIST")
+    for val_func in validators:
         try:
-            msg = param.getKey("PROCESSOR_MSG")
-            print getattr(output_messages, msg) % cn
-        except KeyError:
-            pass
-        return paramValue
+            val_func(value, opt_list)
+        except ParamValidationError as ex:
+            print 'Parameter %s failed validation: %s' % (cname, ex)
+            raise
+
+def process_param_value(param, value):
+    _value = value
+    processors = param.getKey("PROCESSORS") or []
+    for proc_func in processors:
+        logging.debug("Processing value of parameter "
+                      "%s." % param.getKey("CONF_NAME"))
+        try:
+            new_value = proc_func(_value, controller.CONF)
+            if new_value != _value:
+                msg = output_messages.INFO_CHANGED_VALUE
+                print msg % (_value, new_value)
+                _value = new_value
+            else:
+                logging.debug("Processor returned the original "
+                              "value: %s" % _value)
+        except process.ParamProcessingError, ex:
+            print ("Value processing of parameter %s "
+                   "failed.\n%s" % (param.getKey("CONF_NAME"), ex))
+            raise
+    return _value
 
 def _handleGroupCondition(config, conditionName, conditionValue):
     """
@@ -349,8 +325,8 @@ def _loadParamFromFile(config, section, paramName):
 
     # Validate param value using its validation func
     param = controller.getParamByName(paramName)
-    value = _processParamValue(param, value)
-    _validateParamValue(param, value)
+    value = process_param_value(param, value)
+    validate_param_value(param, value)
 
     # Keep param value in our never ending global conf
     controller.CONF[param.getKey("CONF_NAME")] = value
diff --git a/packstack/installer/sample-project/plugins/createfile_101.py b/packstack/installer/sample-project/plugins/createfile_101.py
deleted file mode 100644
index 94e78236b..000000000
--- a/packstack/installer/sample-project/plugins/createfile_101.py
+++ /dev/null
@@ -1,62 +0,0 @@
-"""
-Creates a Sample File
-"""
-
-import logging
-import os
-
-import engine_validators as validate
-import basedefs
-import common_utils as utils
-
-# Controller object will be initialized from main flow
-controller = None
-
-logging.debug("plugin %s loaded", __name__)
-
-def initConfig(controllerObject):
-    global controller
-    controller = controllerObject
-    logging.debug("Initialising Plugine")
-    conf_params = {"SAMPLE": [
-                  {"CMD_OPTION"      : "filename",
-                   "USAGE"           : "File to create",
-                   "PROMPT"          : "File to create",
-                   "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
-                   "DEFAULT_VALUE"   : "/tmp/samplefile.txt",
-                   "MASK_INPUT"      : False,
-                   "LOOSE_VALIDATION": True,
-                   "CONF_NAME"       : "CONFIG_FILENAME",
-                   "USE_DEFAULT"     : False,
-                   "NEED_CONFIRM"    : False,
-                   "CONDITION"       : False },
-            ]
-        }
-
-    conf_groups = [
-            { "GROUP_NAME"            : "SAMPLE",
-              "DESCRIPTION"           : "Sample config group",
-              "PRE_CONDITION"         : utils.returnYes,
-              "PRE_CONDITION_MATCH"   : "yes",
-              "POST_CONDITION"        : False,
-              "POST_CONDITION_MATCH"  : True},
-        ]
-
-    for group in conf_groups:
-        paramList = conf_params[group["GROUP_NAME"]]
-        controller.addGroup(group, paramList)
-
-
-
-def initSequences(controller):
-    preparesteps = [
-             {'title': 'Create File', 'functions':[createfile]}
-    ]
-    controller.addSequence("Creating File", [], [], preparesteps)
-
-
-def createfile():
-    with open(controller.CONF["CONFIG_FILENAME"], "a") as fp:
-        fp.write("HELLO WORLD")
-
diff --git a/packstack/installer/setup_params.py b/packstack/installer/setup_params.py
index aa4e6e99f..5a92ff5cf 100644
--- a/packstack/installer/setup_params.py
+++ b/packstack/installer/setup_params.py
@@ -3,9 +3,9 @@ Container set for groups and parameters
 """
 class Param(object):
     allowed_keys = ('CMD_OPTION','USAGE','PROMPT','OPTION_LIST',
-                    'PROCESSOR_ARGS', 'PROCESSOR_FUNC', 'PROCESSOR_MSG',
-                    'VALIDATION_FUNC','DEFAULT_VALUE','MASK_INPUT','LOOSE_VALIDATION',
-                    'CONF_NAME','USE_DEFAULT','NEED_CONFIRM','CONDITION')
+                    'PROCESSORS', 'VALIDATORS','DEFAULT_VALUE',
+                    'MASK_INPUT', 'LOOSE_VALIDATION', 'CONF_NAME',
+                    'USE_DEFAULT','NEED_CONFIRM','CONDITION')
 
     def __init__(self, attributes=None):
         if not attributes:
@@ -25,10 +25,10 @@ class Param(object):
 
     def getKey(self, key):
         self.validateKey(key)
-        return self.__ATTRIBUTES[key]
+        return self.__ATTRIBUTES.get(key)
 
     def validateKey(self, key):
-        if not self.__ATTRIBUTES.has_key(key):
+        if key not in self.allowed_keys:
             raise KeyError("%s is not a valid key" % key)
 
 class Group(Param):
diff --git a/packstack/modules/ospluginutils.py b/packstack/modules/ospluginutils.py
index 2c9067169..29f0933d5 100644
--- a/packstack/modules/ospluginutils.py
+++ b/packstack/modules/ospluginutils.py
@@ -5,6 +5,7 @@ import re
 
 from packstack.installer import basedefs
 from packstack.installer.setup_controller import Controller
+from packstack.installer.exceptions import PackStackError
 
 controller = Controller()
 
@@ -12,10 +13,6 @@ PUPPET_DIR = os.path.join(basedefs.DIR_PROJECT_DIR, "puppet")
 PUPPET_TEMPLATE_DIR = os.path.join(PUPPET_DIR, "templates")
 
 
-class PackStackError(Exception):
-    pass
-
-
 class NovaConfig(object):
     """
     Helper class to create puppet manifest entries for nova_config
diff --git a/packstack/plugins/cinder_250.py b/packstack/plugins/cinder_250.py
index cc88313fc..a613f4941 100644
--- a/packstack/plugins/cinder_250.py
+++ b/packstack/plugins/cinder_250.py
@@ -34,11 +34,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install Cinder",
                    "PROMPT"          : "Enter the IP address of the Cinder server",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_CINDER_HOST",
@@ -49,7 +46,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Cinder to access DB",
                    "PROMPT"          : "Enter the password for the Cinder DB access",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
@@ -61,7 +58,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Cinder to authenticate with Keystone",
                    "PROMPT"          : "Enter the password for the Cinder Keystone access",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
@@ -73,7 +70,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Cinder's volumes group size",
                    "PROMPT"          : "Enter Cinder's volumes group size",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : "2G",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -85,7 +82,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Cinder's volumes group path",
                    "PROMPT"          : "Enter Cinder's volumes group path",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : "/var/lib/cinder",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -97,7 +94,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Cinder's volumes group name",
                    "PROMPT"          : "Enter Cinder's volumes group name",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : "cinder-volumes",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -109,7 +106,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Create Cinder's volumes group",
                    "PROMPT"          : "Should Cinder's volumes group be created?",
                    "OPTION_LIST"     : ["y", "n"],
-                   "VALIDATION_FUNC" : createVolume,
+                   "VALIDATORS"      : [validate.validate_options, createVolume],
                    "DEFAULT_VALUE"   : "y",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -141,18 +138,19 @@ def initSequences(controller):
     controller.addSequence("Installing OpenStack Cinder", [], [], cindersteps)
 
 
-def createVolume(param, options=[]):
+def createVolume(param, options=None):
     """
     Check that provided host is listening on port 22
     """
+    options = options or []
     if param == "n":
-        return True
+        return
 
+    # XXX: For this case it probably is better (cleaner) to use processor instead of validator
     for option in ['CONFIG_CINDER_VOLUMES_SIZE', 'CONFIG_CINDER_VOLUMES_PATH']:
         param = controller.getParamByName(option)
         param.setKey('USE_DEFAULT', False)
         setup.input_param(param)
-    return True
 
 
 def checkcindervg():
diff --git a/packstack/plugins/dashboard_500.py b/packstack/plugins/dashboard_500.py
index fd6d38b89..72f7bdc37 100644
--- a/packstack/plugins/dashboard_500.py
+++ b/packstack/plugins/dashboard_500.py
@@ -30,11 +30,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install Horizon",
                    "PROMPT"          : "Enter the IP address of the Horizon server",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_HORIZON_HOST",
diff --git a/packstack/plugins/glance_200.py b/packstack/plugins/glance_200.py
index 4c85621c9..37d7bb023 100644
--- a/packstack/plugins/glance_200.py
+++ b/packstack/plugins/glance_200.py
@@ -30,11 +30,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install Glance",
                    "PROMPT"          : "Enter the IP address of the Glance server",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_GLANCE_HOST",
@@ -45,7 +42,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Glance to access DB",
                    "PROMPT"          : "Enter the password for the Glance DB access",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
@@ -57,7 +54,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Glance to authenticate with Keystone",
                    "PROMPT"          : "Enter the password for the Glance Keystone access",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
diff --git a/packstack/plugins/keystone_100.py b/packstack/plugins/keystone_100.py
index 9f2f1900c..c59179741 100644
--- a/packstack/plugins/keystone_100.py
+++ b/packstack/plugins/keystone_100.py
@@ -31,11 +31,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install Keystone",
                    "PROMPT"          : "Enter the IP address of the Keystone server",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_KEYSTONE_HOST",
@@ -46,7 +43,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Keystone to access DB",
                    "PROMPT"          : "Enter the password for the Keystone DB access",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
@@ -58,7 +55,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The token to use for the Keystone service api",
                    "PROMPT"          : "The token to use for the Keystone service api",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex,
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
@@ -70,7 +67,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Keystone admin user",
                    "PROMPT"          : "Enter the password for the Keystone admin user",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
diff --git a/packstack/plugins/mysql_001.py b/packstack/plugins/mysql_001.py
index 5a2c16c8b..61398f010 100644
--- a/packstack/plugins/mysql_001.py
+++ b/packstack/plugins/mysql_001.py
@@ -30,11 +30,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install MySQL",
                    "PROMPT"          : "Enter the IP address of the MySQL server",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_MYSQL_HOST",
@@ -45,7 +42,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Username for the MySQL admin user",
                    "PROMPT"          : "Enter the username for the MySQL admin user",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : "root",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -57,7 +54,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Password for the MySQL admin user",
                    "PROMPT"          : "Enter the password for the MySQL admin user",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": True,
diff --git a/packstack/plugins/nova_300.py b/packstack/plugins/nova_300.py
index 01ad9444a..ef85697e7 100644
--- a/packstack/plugins/nova_300.py
+++ b/packstack/plugins/nova_300.py
@@ -28,11 +28,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install the Nova API service",
                    "PROMPT"          : "Enter the IP address of the Nova API service",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ip, validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_NOVA_API_HOST",
@@ -43,11 +40,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install the Nova Cert service",
                    "PROMPT"          : "Enter the IP address of the Nova Cert service",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_NOVA_CERT_HOST",
@@ -58,11 +52,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install the Nova VNC proxy",
                    "PROMPT"          : "Enter the IP address of the Nova VNC proxy",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_NOVA_VNCPROXY_HOST",
@@ -73,11 +64,11 @@ def initConfig(controllerObject):
                    "USAGE"           : "A comma separated list of IP addresses on which to install the Nova Compute services",
                    "PROMPT"          : "Enter a comma separated list of IP addresses on which to install the Nova Compute services",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateMultiSSH,
+                   "VALIDATORS"      : [validate.validate_multi_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
-                   "CONF_NAME"       : "CONFIG_NOVA_COMPUTE_HOSTS", # TO-DO: Create processor for CSV
+                   "CONF_NAME"       : "CONFIG_NOVA_COMPUTE_HOSTS",
                    "USE_DEFAULT"     : False,
                    "NEED_CONFIRM"    : False,
                    "CONDITION"       : False },
@@ -85,7 +76,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Private interface for Flat DHCP on the Nova compute servers",
                    "PROMPT"          : "Enter the Private interface for Flat DHCP on the Nova compute servers",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : "eth1",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -97,11 +88,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install the Nova Network service",
                    "PROMPT"          : "Enter the IP address of the Nova Network service",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ip, validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_NOVA_NETWORK_HOST",
@@ -112,7 +100,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Nova to access DB",
                    "PROMPT"          : "Enter the password for the Nova DB access",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
@@ -124,7 +112,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Nova to authenticate with Keystone",
                    "PROMPT"          : "Enter the password for the Nova Keystone access",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
@@ -136,7 +124,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Public interface on the Nova network server",
                    "PROMPT"          : "Enter the Public interface on the Nova network server",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : "eth0",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -148,7 +136,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Private interface for Flat DHCP on the Nova network server",
                    "PROMPT"          : "Enter the Private interface for Flat DHCP on the Nova network server",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : "eth1",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -160,7 +148,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "IP Range for Flat DHCP",
                    "PROMPT"          : "Enter the IP Range for Flat DHCP",
                    "OPTION_LIST"     : ["^([\d]{1,3}\.){3}[\d]{1,3}/\d\d?$"],
-                   "VALIDATION_FUNC" : validate.validateRe,
+                   "VALIDATORS"      : [validate.validate_regexp],
                    "DEFAULT_VALUE"   : "192.168.32.0/22",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -172,7 +160,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "IP Range for Floating IP's",
                    "PROMPT"          : "Enter the IP Range for Floating IP's",
                    "OPTION_LIST"     : ["^([\d]{1,3}\.){3}[\d]{1,3}/\d\d?$"],
-                   "VALIDATION_FUNC" : validate.validateRe,
+                   "VALIDATORS"      : [validate.validate_regexp],
                    "DEFAULT_VALUE"   : "10.3.4.0/22",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -184,11 +172,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install the Nova Scheduler service",
                    "PROMPT"          : "Enter the IP address of the Nova Scheduler service",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_NOVA_SCHED_HOST",
@@ -204,6 +189,7 @@ def initConfig(controllerObject):
                   "POST_CONDITION_MATCH"  : True}
     controller.addGroup(groupDict, paramsList)
 
+
 def initSequences(controller):
     if controller.CONF['CONFIG_NOVA_INSTALL'] != 'y':
         return
@@ -220,59 +206,71 @@ def initSequences(controller):
     ]
     controller.addSequence("Installing OpenStack Nova API", [], [], novaapisteps)
 
+
 def createapimanifest():
     manifestfile = "%s_api_nova.pp"%controller.CONF['CONFIG_NOVA_API_HOST']
     manifestdata = getManifestTemplate("nova_api.pp")
     appendManifestFile(manifestfile, manifestdata, 'novaapi')
 
+
 def createkeystonemanifest():
     manifestfile = "%s_keystone.pp"%controller.CONF['CONFIG_KEYSTONE_HOST']
     manifestdata = getManifestTemplate("keystone_nova.pp")
     appendManifestFile(manifestfile, manifestdata)
 
+
 def createcertmanifest():
     manifestfile = "%s_nova.pp"%controller.CONF['CONFIG_NOVA_CERT_HOST']
     manifestdata = getManifestTemplate("nova_cert.pp")
     appendManifestFile(manifestfile, manifestdata)
 
+
+def check_ifcfg(host, device):
+    """
+    Raises ScriptRuntimeError if given host does not have give device.
+    """
+    server = utils.ScriptRunner(host)
+    cmd = "ifconfig %s || ( echo Device %s does not exist && exit 1 )"
+    server.append(cmd % (device, device))
+    server.execute()
+
+
 def createcomputemanifest():
     for host in controller.CONF["CONFIG_NOVA_COMPUTE_HOSTS"].split(","):
         controller.CONF["CONFIG_NOVA_COMPUTE_HOST"] = host
         manifestdata = getManifestTemplate("nova_compute.pp")
         manifestfile = "%s_nova.pp"%host
 
-        server = utils.ScriptRunner(host)
         nova_config_options = NovaConfig()
-
         if host != controller.CONF["CONFIG_NOVA_NETWORK_HOST"]:
             nova_config_options.addOption("flat_interface", controller.CONF['CONFIG_NOVA_COMPUTE_PRIVIF'])
-        validate.r_validateIF(server, controller.CONF['CONFIG_NOVA_COMPUTE_PRIVIF'])
+        check_ifcfg(host, controller.CONF['CONFIG_NOVA_COMPUTE_PRIVIF'])
 
-        server.execute()
         appendManifestFile(manifestfile, manifestdata + "\n" + nova_config_options.getManifestEntry())
 
+
 def createnetworkmanifest():
-    hostname = controller.CONF['CONFIG_NOVA_NETWORK_HOST']
+    host = controller.CONF['CONFIG_NOVA_NETWORK_HOST']
+    check_ifcfg(host, controller.CONF['CONFIG_NOVA_NETWORK_PRIVIF'])
+    check_ifcfg(host, controller.CONF['CONFIG_NOVA_NETWORK_PUBIF'])
 
-    server = utils.ScriptRunner(hostname)
-    validate.r_validateIF(server, controller.CONF['CONFIG_NOVA_NETWORK_PRIVIF'])
-    validate.r_validateIF(server, controller.CONF['CONFIG_NOVA_NETWORK_PUBIF'])
-    server.execute()
-
-    manifestfile = "%s_nova.pp"%hostname
+    manifestfile = "%s_nova.pp" % host
     manifestdata = getManifestTemplate("nova_network.pp")
     appendManifestFile(manifestfile, manifestdata)
 
+
 def createschedmanifest():
     manifestfile = "%s_nova.pp"%controller.CONF['CONFIG_NOVA_SCHED_HOST']
     manifestdata = getManifestTemplate("nova_sched.pp")
     appendManifestFile(manifestfile, manifestdata)
 
+
 def createvncproxymanifest():
     manifestfile = "%s_nova.pp"%controller.CONF['CONFIG_NOVA_VNCPROXY_HOST']
     manifestdata = getManifestTemplate("nova_vncproxy.pp")
     appendManifestFile(manifestfile, manifestdata)
 
+
 def createcommonmanifest():
     for manifestfile, marker in manifestfiles.getFiles():
         if manifestfile.endswith("_nova.pp"):
diff --git a/packstack/plugins/openstack_client_400.py b/packstack/plugins/openstack_client_400.py
index d98cff020..59f425fbf 100644
--- a/packstack/plugins/openstack_client_400.py
+++ b/packstack/plugins/openstack_client_400.py
@@ -29,11 +29,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install the OpenStack client packages. An admin \"rc\" file will also be installed",
                    "PROMPT"          : "Enter the IP address of the client server",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_OSCLIENT_HOST",
diff --git a/packstack/plugins/prescript_000.py b/packstack/plugins/prescript_000.py
index 55f124dd2..2ba6635d0 100644
--- a/packstack/plugins/prescript_000.py
+++ b/packstack/plugins/prescript_000.py
@@ -29,7 +29,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Set to 'y' if you would like Packstack to install Glance",
                    "PROMPT"          : "Should Packstack install Glance image service",
                    "OPTION_LIST"     : ["y", "n"],
-                   "VALIDATION_FUNC" : validate.validateOptions,
+                   "VALIDATORS"      : [validate.validate_options],
                    "DEFAULT_VALUE"   : "y",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -41,7 +41,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Set to 'y' if you would like Packstack to install Cinder",
                    "PROMPT"          : "Should Packstack install Cinder volume service",
                    "OPTION_LIST"     : ["y", "n"],
-                   "VALIDATION_FUNC" : validate.validateOptions,
+                   "VALIDATORS"      : [validate.validate_options],
                    "DEFAULT_VALUE"   : "y",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -53,7 +53,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Set to 'y' if you would like Packstack to install Nova",
                    "PROMPT"          : "Should Packstack install Nova compute service",
                    "OPTION_LIST"     : ["y", "n"],
-                   "VALIDATION_FUNC" : validate.validateOptions,
+                   "VALIDATORS"      : [validate.validate_options],
                    "DEFAULT_VALUE"   : "y",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -65,7 +65,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Set to 'y' if you would like Packstack to install Horizon",
                    "PROMPT"          : "Should Packstack install Horizon dashboard",
                    "OPTION_LIST"     : ["y", "n"],
-                   "VALIDATION_FUNC" : validate.validateOptions,
+                   "VALIDATORS"      : [validate.validate_options],
                    "DEFAULT_VALUE"   : "y",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -77,7 +77,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Set to 'y' if you would like Packstack to install Swift",
                    "PROMPT"          : "Should Packstack install Swift object storage",
                    "OPTION_LIST"     : ["y", "n"],
-                   "VALIDATION_FUNC" : validate.validateOptions,
+                   "VALIDATORS"      : [validate.validate_options],
                    "DEFAULT_VALUE"   : "n",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -89,7 +89,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Set to 'y' if you would like Packstack to install the OpenStack Client packages. An admin \"rc\" file will also be installed",
                    "PROMPT"          : "Should Packstack install OpenStack client tools",
                    "OPTION_LIST"     : ["y", "n"],
-                   "VALIDATION_FUNC" : validate.validateOptions,
+                   "VALIDATORS"      : [validate.validate_options],
                    "DEFAULT_VALUE"   : "y",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
@@ -101,7 +101,6 @@ def initConfig(controllerObject):
                    "USAGE"           : "Comma separated list of NTP servers. Leave plain if Packstack should not install ntpd on instances.",
                    "PROMPT"          : "Enter list of NTP server(s). Leave plain if Packstack should not install ntpd on instances.",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : lambda param, options: True,
                    "DEFAULT_VALUE"   : '',
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
diff --git a/packstack/plugins/qpid_002.py b/packstack/plugins/qpid_002.py
index 25c1aa028..a9f9c2ba3 100644
--- a/packstack/plugins/qpid_002.py
+++ b/packstack/plugins/qpid_002.py
@@ -29,11 +29,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address of the server on which to install the QPID service",
                    "PROMPT"          : "Enter the IP address of the QPID service",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
-                   "PROCESSOR_ARGS"  : {"allow_localhost": True},
-                   "PROCESSOR_FUNC"  : process.processHost,
-                   "PROCESSOR_MSG"   : "WARN_VAL_IS_HOSTNAME",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
                    "CONF_NAME"       : "CONFIG_QPID_HOST",
diff --git a/packstack/plugins/serverprep_901.py b/packstack/plugins/serverprep_901.py
index c724fbc96..0a915b6fe 100644
--- a/packstack/plugins/serverprep_901.py
+++ b/packstack/plugins/serverprep_901.py
@@ -28,7 +28,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Install OpenStack from EPEL. If set to \"y\" EPEL will be installed on each server",
                    "PROMPT"          : "Should Packstack install EPEL on each server",
                    "OPTION_LIST"     : ["y", "n"],
-                   "VALIDATION_FUNC" : validate.validateOptions,
+                   "VALIDATORS"      : [validate.validate_options],
                    "DEFAULT_VALUE"   : "n",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -40,7 +40,6 @@ def initConfig(controllerObject):
                    "USAGE"           : "A comma separated list of URLs to any additional yum repositories to install",
                    "PROMPT"          : "Enter a comma separated list of URLs to any additional yum repositories to install",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : lambda a,b: True,
                    "DEFAULT_VALUE"   : "",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -52,7 +51,6 @@ def initConfig(controllerObject):
                    "USAGE"           : "To subscribe each server with Red Hat subscription manager, include this with CONFIG_RH_PASSWORD",
                    "PROMPT"          : "To subscribe each server to Red Hat enter a username here",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : lambda a,b: True,
                    "DEFAULT_VALUE"   : "",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -64,7 +62,6 @@ def initConfig(controllerObject):
                    "USAGE"           : "To subscribe each server with Red Hat subscription manager, include this with CONFIG_RH_USERNAME",
                    "PROMPT"          : "To subscribe each server to Red Hat enter your password here",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : lambda a,b: True,
                    "DEFAULT_VALUE"   : "",
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": True,
diff --git a/packstack/plugins/sshkeys_000.py b/packstack/plugins/sshkeys_000.py
index a05d5ac04..b30996a15 100644
--- a/packstack/plugins/sshkeys_000.py
+++ b/packstack/plugins/sshkeys_000.py
@@ -32,8 +32,8 @@ def initConfig(controllerObject):
                    "USAGE"           : "Path to a Public key to install on servers. If a usable key has not been installed on the remote servers the user will be prompted for a password and this key will be installed so the password will not be required again",
                    "PROMPT"          : "Enter the path to your ssh Public key to install on servers",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateFile,
-                   "PROCESSOR_FUNC"  : process.processSSHKey,
+                   "VALIDATORS"      : [validate.validate_file],
+                   "PROCESSORS"      : [process.processSSHKey],
                    "DEFAULT_VALUE"   : (glob.glob(os.path.join(os.environ["HOME"], ".ssh/*.pub"))+[""])[0],
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": False,
diff --git a/packstack/plugins/swift_600.py b/packstack/plugins/swift_600.py
index b8f486a61..cfcd4f4fc 100644
--- a/packstack/plugins/swift_600.py
+++ b/packstack/plugins/swift_600.py
@@ -31,11 +31,11 @@ def initConfig(controllerObject):
                    "USAGE"           : "The IP address on which to install the Swift proxy service",
                    "PROMPT"          : "Enter the IP address of the Swift proxy service",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateSSH,
+                   "VALIDATORS"      : [validate.validate_ip, validate.validate_ssh],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
-                   "CONF_NAME"       : "CONFIG_SWIFT_PROXY_HOSTS", # TO-DO: Create processor for CSV
+                   "CONF_NAME"       : "CONFIG_SWIFT_PROXY_HOSTS", #XXX: Shouldn't be here CONFIG_SWIFT_PROXY_HOST?
                    "USE_DEFAULT"     : False,
                    "NEED_CONFIRM"    : False,
                    "CONDITION"       : False },
@@ -43,7 +43,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "The password to use for the Swift to authenticate with Keystone",
                    "PROMPT"          : "Enter the password for the Swift Keystone access",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty],
                    "DEFAULT_VALUE"   : uuid.uuid4().hex[:16],
                    "MASK_INPUT"      : True,
                    "LOOSE_VALIDATION": False,
@@ -55,11 +55,11 @@ def initConfig(controllerObject):
                    "USAGE"           : "A comma separated list of IP addresses on which to install the Swift Storage services, each entry should take the format <ipaddress>[/dev], for example 127.0.0.1/vdb will install /dev/vdb on 127.0.0.1 as a swift storage device, if /dev is omitted Packstack will create a loopback device for a test setup",
                    "PROMPT"          : "Enter the Swift Storage servers e.g. host/dev,host/dev",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateStringNotEmpty,
+                   "VALIDATORS"      : [validate.validate_not_empty, validate_storage],
                    "DEFAULT_VALUE"   : utils.getLocalhostIP(),
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
-                   "CONF_NAME"       : "CONFIG_SWIFT_STORAGE_HOSTS", # TO-DO: Create processor for CSV
+                   "CONF_NAME"       : "CONFIG_SWIFT_STORAGE_HOSTS",
                    "USE_DEFAULT"     : False,
                    "NEED_CONFIRM"    : False,
                    "CONDITION"       : False },
@@ -67,7 +67,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Number of swift storage zones, this number MUST be no bigger than the number of storage devices configured",
                    "PROMPT"          : "Enter the number of swift storage zones, MUST be no bigger than the number of storage devices configured",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateInteger,
+                   "VALIDATORS"      : [validate.validate_integer],
                    "DEFAULT_VALUE"   : "1",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -79,7 +79,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "Number of swift storage replicas, this number MUST be no bigger than the number of storage zones configured",
                    "PROMPT"          : "Enter the number of swift storage replicas, MUST be no bigger than the number of storage zones configured",
                    "OPTION_LIST"     : [],
-                   "VALIDATION_FUNC" : validate.validateInteger,
+                   "VALIDATORS"      : [validate.validate_integer],
                    "DEFAULT_VALUE"   : "1",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -91,7 +91,7 @@ def initConfig(controllerObject):
                    "USAGE"           : "FileSystem type for storage nodes",
                    "PROMPT"          : "Enter FileSystem type for storage nodes",
                    "OPTION_LIST"     : ['xfs','ext4'],
-                   "VALIDATION_FUNC" : validate.validateOptions,
+                   "VALIDATORS"      : [validate.validate_options],
                    "DEFAULT_VALUE"   : "ext4",
                    "MASK_INPUT"      : False,
                    "LOOSE_VALIDATION": True,
@@ -111,6 +111,12 @@ def initConfig(controllerObject):
     controller.addGroup(groupDict, paramsList)
 
 
+def validate_storage(param, options=None):
+    for host in param.split(','):
+        host = host.split('/', 1)[0]
+        validate.validate_ip(host.strip(), options)
+
+
 def initSequences(controller):
     if controller.CONF['CONFIG_SWIFT_INSTALL'] != 'y':
         return
@@ -124,12 +130,14 @@ def initSequences(controller):
     ]
     controller.addSequence("Installing OpenStack Swift", [], [], steps)
 
+
 def createkeystonemanifest():
     manifestfile = "%s_keystone.pp"%controller.CONF['CONFIG_KEYSTONE_HOST']
     controller.CONF['CONFIG_SWIFT_PROXY'] = controller.CONF['CONFIG_SWIFT_PROXY_HOSTS'].split(',')[0]
     manifestdata = getManifestTemplate("keystone_swift.pp")
     appendManifestFile(manifestfile, manifestdata)
 
+
 devices = []
 def parseDevices(config_swift_storage_hosts):
     device_number = 0
@@ -142,6 +150,7 @@ def parseDevices(config_swift_storage_hosts):
         devices.append({'host':host, 'device':device, 'device_name':'device%s'%device_number, 'zone':str(zone)})
     return devices
 
+
 # The ring file should be built and distributed befor the storage services
 # come up. Specifically the replicator crashes if the ring isn't present
 def createbuildermanifest():
@@ -163,6 +172,7 @@ def createbuildermanifest():
 
     appendManifestFile(manifestfile, manifestdata, 'swiftbuilder')
 
+
 def createproxymanifest():
     manifestfile = "%s_swift.pp"%controller.CONF['CONFIG_SWIFT_PROXY_HOSTS']
     manifestdata = getManifestTemplate("swift_proxy.pp")
@@ -171,6 +181,32 @@ def createproxymanifest():
         manifestdata += 'swift::ringsync{["account","container","object"]:\n    ring_server => "%s"\n}'%controller.CONF['CONFIG_SWIFT_BUILDER_HOST']
     appendManifestFile(manifestfile, manifestdata)
 
+
+def check_device(host, device):
+    """
+    Raises ScriptRuntimeError if given device is not mounted on given
+    host.
+    """
+    server = utils.ScriptRunner()
+
+    # the device MUST exist
+    cmd = 'ls -l /dev/%s'
+    server.append(cmd % device)
+
+    # if it is not mounted then we can use it
+    cmd = 'grep "/dev/%s " /proc/self/mounts || exit 0'
+    server.append(cmd % device)
+
+    # if it is mounted then the mount point has to be in /srv/node
+    cmd = 'grep "/dev/%s /srv/node" /proc/self/mounts && exit 0'
+    server.append(cmd % device)
+
+    # if we got here without exiting then we can't use this device
+    server.append('exit 1')
+    server.execute()
+    return False
+
+
 def createstoragemanifest():
 
     # this need to happen once per storage host
@@ -185,10 +221,8 @@ def createstoragemanifest():
         host = device['host']
         devicename = device['device_name']
         device = device['device']
-
-        server = utils.ScriptRunner(host)
-        validate.r_validateDevice(server, device)
-        server.execute()
+        if device:
+            check_device(host, device)
 
         manifestfile = "%s_swift.pp"%host
         if device:
@@ -198,6 +232,7 @@ def createstoragemanifest():
             manifestdata = "\n" + getManifestTemplate("swift_loopback.pp")
         appendManifestFile(manifestfile, manifestdata)
 
+
 def createcommonmanifest():
     for manifestfile, marker in manifestfiles.getFiles():
         if manifestfile.endswith("_swift.pp"):
diff --git a/run_tests.sh b/run_tests.sh
new file mode 100755
index 000000000..1c836eb0c
--- /dev/null
+++ b/run_tests.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+tox -epy26,py27,pep8
diff --git a/tests/test.py b/tests/test.py
deleted file mode 100644
index 861224912..000000000
--- a/tests/test.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2013, Red Hat, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file 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.
-
-import shutil
-import tempfile
-
-import subprocess
-from unittest import TestCase
-
-
-class fakePopen(object):
-    def __init__(self, returncode=0):
-        self.returncode = returncode
-        self.stdout = self.stderr = self.data = ""
-
-    def __call__(self, *args, **kwargs):
-        self.args = args
-        self.kwargs = kwargs
-        return self
-
-    def communicate(self, data):
-        self.data += data
-        return self.stdout, self.stderr
-
-
-class TestCase(TestCase):
-    def setUp(self):
-        # Creating a temp directory that can be used by tests
-        self.tempdir = tempfile.mkdtemp()
-
-        # some plugins call popen, we're replacing it for tests
-        self._Popen = subprocess.Popen
-        self.fakePopen = subprocess.Popen = fakePopen()
-
-    def tearDown(self):
-        # remove the temp directory
-        shutil.rmtree(self.tempdir)
-
-        subprocess.Popen = self._Popen
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 000000000..fc1ae9775
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013, Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file 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.
+
+import shutil
+import tempfile
+import subprocess
+
+
+class FakePopen(object):
+    def __init__(self, returncode=0):
+        self.returncode = returncode
+        self.stdout = self.stderr = self.data = ""
+
+    def __call__(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+        return self
+
+    def communicate(self, data=None):
+        self.data += data or ''
+        return self.stdout, self.stderr
+
+
+class PackstackTestCaseMixin(object):
+    """
+    Implementation of some assertion methods available by default
+    in Python2.7+ only
+    """
+    def setUp(self):
+        # Creating a temp directory that can be used by tests
+        self.tempdir = tempfile.mkdtemp()
+
+        # some plugins call popen, we're replacing it for tests
+        self._Popen = subprocess.Popen
+        self.fake_popen = subprocess.Popen = FakePopen()
+
+    def tearDown(self):
+        # remove the temp directory
+        shutil.rmtree(self.tempdir)
+        subprocess.Popen = self._Popen
+
+    def assertItemsEqual(self, list1, list2, msg=None):
+        f, s = len(list1), len(list2)
+        self.assertEqual(f, s, msg=('Element counts were not equal. '
+                                    'First has %s, Second has %s' % (f, s)))
+        for i in list1:
+            if i not in list2:
+                raise AssertionError('Given lists differ:'
+                                     '\n%(list1)s\n%(list2)s' % locals())
+
+    def assertListEqual(self, list1, list2, msg=None):
+        f, s = len(list1), len(list2)
+        self.assertEqual(f, s, msg=('Element counts were not equal. '
+                                    'First has %s, Second has %s' % (f, s)))
+        for index, item in enumerate(list1):
+            if item != list2[index]:
+                raise AssertionError('Given lists differ:'
+                                     '\n%(list1)s\n%(list2)s' % locals())
+
+    def assertIsInstance(self, obj, cls, msg=None):
+        if not isinstance(obj, cls):
+            raise AssertionError('%s is not an instance of %s' % (obj, cls))
diff --git a/tests/test_ospluginutils.py b/tests/test_ospluginutils.py
index 9021aca25..79a904ac0 100644
--- a/tests/test_ospluginutils.py
+++ b/tests/test_ospluginutils.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # vim: tabstop=4 shiftwidth=4 softtabstop=4
 
 # Copyright 2013, Red Hat, Inc.
@@ -15,14 +16,15 @@
 # under the License.
 
 import os
-from test import TestCase
+from unittest import TestCase
 
+from test_base import PackstackTestCaseMixin
 from packstack.modules.ospluginutils import gethostlist, \
                                             validate_puppet_logfile, \
                                             PackStackError
 
 
-class OSPluginUtilsTestCase(TestCase):
+class OSPluginUtilsTestCase(PackstackTestCaseMixin, TestCase):
     def test_gethostlist(self):
         conf = {"A_HOST": "1.1.1.1", "B_HOSTS": "2.2.2.2,1.1.1.1",
                 "C_HOSTS": "3.3.3.3/vdc"}
diff --git a/tests/test_plugin_serverprep.py b/tests/test_plugin_serverprep.py
index 0a7dc4cdb..e6cbb51e8 100644
--- a/tests/test_plugin_serverprep.py
+++ b/tests/test_plugin_serverprep.py
@@ -15,15 +15,16 @@
 # under the License.
 
 import os
-from test import TestCase
+from unittest import TestCase
 
+from test_base import PackstackTestCaseMixin
 from packstack.plugins import serverprep_901
 from packstack.installer.setup_controller import Controller
 
 serverprep_901.controller = Controller()
 
 
-class OSPluginUtilsTestCase(TestCase):
+class OSPluginUtilsTestCase(PackstackTestCaseMixin, TestCase):
     def test_rhn_creds_quoted(self):
         """Make sure RHN password is quoted"""
 
@@ -37,5 +38,5 @@ class OSPluginUtilsTestCase(TestCase):
         serverprep_901.serverprep()
 
         self.assertNotEqual(
-            self.fakePopen.data.find('--password="%s"' % password), -1
+            self.fake_popen.data.find('--password="%s"' % password), -1
         )
diff --git a/tests/test_validators.py b/tests/test_validators.py
new file mode 100644
index 000000000..ff11bd355
--- /dev/null
+++ b/tests/test_validators.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013, Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file 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.
+
+import os
+import shutil
+import tempfile
+from unittest import TestCase
+
+from test_base import PackstackTestCaseMixin
+from packstack.installer.engine_validators import *
+
+
+class ValidatorsTestCase(PackstackTestCaseMixin, TestCase):
+    def setUp(self):
+        # Creating a temp directory that can be used by tests
+        self.tempdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        # remove the temp directory
+        shutil.rmtree(self.tempdir)
+
+    def test_validate_integer(self):
+        validate_integer('1')
+        self.assertRaises(ParamValidationError, validate_integer, 'test')
+
+    def test_validate_regexp(self):
+        validate_regexp('Test_123', options=['\w'])
+        self.assertRaises(ParamValidationError, validate_regexp,
+                          '!#$%', options=['\w'])
+
+    def test_validate_port(self):
+        validate_port('666')
+        self.assertRaises(ParamValidationError, validate_port, 'test')
+        self.assertRaises(ParamValidationError, validate_port, '-3')
+
+    def test_validate_not_empty(self):
+        validate_not_empty('test')
+        validate_not_empty(False)
+        self.assertRaises(ParamValidationError, validate_not_empty, '')
+        self.assertRaises(ParamValidationError, validate_not_empty, [])
+        self.assertRaises(ParamValidationError, validate_not_empty, {})
+
+    def test_validate_options(self):
+        validate_options('a', options=['a', 'b'])
+        validate_options('b', options=['a', 'b'])
+        self.assertRaises(ParamValidationError, validate_options,
+                          'c', options=['a', 'b'])
+
+    def test_validate_ip(self):
+        validate_ip('127.0.0.1')
+        validate_ip('::1')
+        self.assertRaises(ParamValidationError, validate_ip, 'test')
+
+    def test_validate_file(self):
+        fname = os.path.join(self.tempdir, '.test_validate_file')
+        bad_name = os.path.join(self.tempdir, '.me_no_exists')
+        with open(fname, 'w') as f:
+            f.write('test')
+        validate_file(fname)
+        self.assertRaises(ParamValidationError, validate_file, bad_name)
+
+    def test_validate_ping(self):
+        # ping to broadcast fails
+        self.assertRaises(ParamValidationError, validate_ping, '192.168.122.0')
+
+    def test_validate_ssh(self):
+        # ssh to broadcast fails
+        self.assertRaises(ParamValidationError, validate_ssh, '192.168.122.0')