Merge "Add redhat-rpm-config to rpm build deps" into feature/zuulv3
diff --git a/README.rst b/README.rst
index c55f7b3..16e7385 100644
--- a/README.rst
+++ b/README.rst
@@ -134,6 +134,16 @@
   is too cryptic.  In your own work, feel free to leave TODOv3 notes
   if a change would otherwise become too large or unweildy.
 
+Python Version Support
+----------------------
+
+Zuul v3 requires Python 3. It does not support Python 2.
+
+As Ansible is used for the execution of jobs, it's important to note that
+while Ansible does support Python 3, not all of Ansible's modules do. Zuul
+currently sets ``ansible_python_interpreter`` to python2 so that remote
+content will be executed with Python2.
+
 Roadmap
 -------
 
diff --git a/requirements.txt b/requirements.txt
index 746bbcb..81f930e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,7 +8,6 @@
 WebOb>=1.2.3
 paramiko>=1.8.0,<2.0.0
 GitPython>=0.3.3,<2.1.2
-ordereddict
 python-daemon>=2.0.4,<2.1.0
 extras
 statsd>=1.0.0,<3.0
@@ -17,7 +16,6 @@
 apscheduler>=3.0
 PrettyTable>=0.6,<0.8
 babel>=1.0
-six>=1.6.0
 ansible>=2.0.0.1
 kazoo
 sqlalchemy
diff --git a/tests/base.py b/tests/base.py
index 93b5785..d89469f 100755
--- a/tests/base.py
+++ b/tests/base.py
@@ -15,24 +15,20 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-from six.moves import configparser as ConfigParser
+import configparser
 import datetime
 import gc
 import hashlib
+import importlib
+from io import StringIO
 import json
 import logging
 import os
-from six.moves import queue as Queue
-from six.moves import urllib
+import queue
 import random
 import re
 import select
 import shutil
-from six.moves import reload_module
-try:
-    from cStringIO import StringIO
-except Exception:
-    from six import StringIO
 import socket
 import string
 import subprocess
@@ -42,6 +38,7 @@
 import traceback
 import time
 import uuid
+import urllib
 
 
 import git
@@ -463,7 +460,7 @@
         super(FakeGerritConnection, self).__init__(driver, connection_name,
                                                    connection_config)
 
-        self.event_queue = Queue.Queue()
+        self.event_queue = queue.Queue()
         self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
         self.change_number = 0
         self.changes = changes_db
@@ -1373,8 +1370,8 @@
                                                 ssl_ca=ssl_ca)
 
     def getJobForConnection(self, connection, peek=False):
-        for queue in [self.high_queue, self.normal_queue, self.low_queue]:
-            for job in queue:
+        for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
+            for job in job_queue:
                 if not hasattr(job, 'waiting'):
                     if job.name.startswith(b'executor:execute'):
                         job.waiting = self.hold_jobs_in_queue
@@ -1384,7 +1381,7 @@
                     continue
                 if job.name in connection.functions:
                     if not peek:
-                        queue.remove(job)
+                        job_queue.remove(job)
                         connection.related_jobs[job.handle] = job
                         job.worker_connection = connection
                     job.running = True
@@ -1879,8 +1876,8 @@
         os.environ['STATSD_PORT'] = str(self.statsd.port)
         self.statsd.start()
         # the statsd client object is configured in the statsd module import
-        reload_module(statsd)
-        reload_module(zuul.scheduler)
+        importlib.reload(statsd)
+        importlib.reload(zuul.scheduler)
 
         self.gearman_server = FakeGearmanServer(self.use_ssl)
 
@@ -2008,7 +2005,7 @@
         # This creates the per-test configuration object.  It can be
         # overriden by subclasses, but should not need to be since it
         # obeys the config_file and tenant_config_file attributes.
-        self.config = ConfigParser.ConfigParser()
+        self.config = configparser.ConfigParser()
         self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
 
         if not self.setupSimpleLayout():
@@ -2383,12 +2380,12 @@
         return True
 
     def eventQueuesEmpty(self):
-        for queue in self.event_queues:
-            yield queue.empty()
+        for event_queue in self.event_queues:
+            yield event_queue.empty()
 
     def eventQueuesJoin(self):
-        for queue in self.event_queues:
-            queue.join()
+        for event_queue in self.event_queues:
+            event_queue.join()
 
     def waitUntilSettled(self):
         self.log.debug("Waiting until settled...")
@@ -2397,8 +2394,9 @@
             if time.time() - start > self.wait_timeout:
                 self.log.error("Timeout waiting for Zuul to settle")
                 self.log.error("Queue status:")
-                for queue in self.event_queues:
-                    self.log.error("  %s: %s" % (queue, queue.empty()))
+                for event_queue in self.event_queues:
+                    self.log.error("  %s: %s" %
+                                   (event_queue, event_queue.empty()))
                 self.log.error("All builds waiting: %s" %
                                (self.areAllBuildsWaiting(),))
                 self.log.error("All builds reported: %s" %
@@ -2457,11 +2455,12 @@
         # Make sure there are no orphaned jobs
         for tenant in self.sched.abide.tenants.values():
             for pipeline in tenant.layout.pipelines.values():
-                for queue in pipeline.queues:
-                    if len(queue.queue) != 0:
+                for pipeline_queue in pipeline.queues:
+                    if len(pipeline_queue.queue) != 0:
                         print('pipeline %s queue %s contents %s' % (
-                            pipeline.name, queue.name, queue.queue))
-                    self.assertEqual(len(queue.queue), 0,
+                            pipeline.name, pipeline_queue.name,
+                            pipeline_queue.queue))
+                    self.assertEqual(len(pipeline_queue.queue), 0,
                                      "Pipelines queues should be empty")
 
     def assertReportedStat(self, key, value=None, kind=None):
diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py
index 2f9d272..eb17966 100755
--- a/tests/unit/test_scheduler.py
+++ b/tests/unit/test_scheduler.py
@@ -25,8 +25,8 @@
 from unittest import skip
 
 import git
-from six.moves import urllib
 import testtools
+import urllib
 
 import zuul.change_matcher
 from zuul.driver.gerrit import gerritreporter
diff --git a/tests/unit/test_webapp.py b/tests/unit/test_webapp.py
index b2836ae..da027c1 100644
--- a/tests/unit/test_webapp.py
+++ b/tests/unit/test_webapp.py
@@ -17,8 +17,8 @@
 
 import os
 import json
+import urllib
 
-from six.moves import urllib
 import webob
 
 from tests.base import ZuulTestCase, FIXTURE_DIR
diff --git a/tools/encrypt_secret.py b/tools/encrypt_secret.py
index 4865edd..e36b24e 100644
--- a/tools/encrypt_secret.py
+++ b/tools/encrypt_secret.py
@@ -17,7 +17,7 @@
 import subprocess
 import sys
 import tempfile
-from six.moves import urllib
+import urllib
 
 DESCRIPTION = """Encrypt a secret for Zuul.
 
diff --git a/zuul/ansible/callback/zuul_stream.py b/zuul/ansible/callback/zuul_stream.py
index 47e648e..e3d1e14 100644
--- a/zuul/ansible/callback/zuul_stream.py
+++ b/zuul/ansible/callback/zuul_stream.py
@@ -164,7 +164,7 @@
 
             hosts = self._get_task_hosts(task)
             for host in hosts:
-                if host in ('locahost', '127.0.0.1'):
+                if host in ('localhost', '127.0.0.1'):
                     # Don't try to stream from localhost
                     continue
                 ip = play_vars[host].get(
@@ -199,10 +199,10 @@
             self._stop_streamers()
         if result._task.action in ('command', 'shell'):
             stdout_lines = zuul_filter_result(result._result)
-        if is_localhost:
-            for line in stdout_lines:
-                ts, ln = (x.strip() for x in line.split(' | ', 1))
-                self._log("localhost | %s " % ln, ts=ts)
+            if is_localhost:
+                for line in stdout_lines:
+                    ts, ln = (x.strip() for x in line.split(' | ', 1))
+                    self._log("localhost | %s " % ln, ts=ts)
 
     def v2_runner_on_failed(self, result, ignore_errors=False):
         self._process_result_for_localhost(result)
diff --git a/zuul/cmd/__init__.py b/zuul/cmd/__init__.py
index d31c5b8..8610114 100755
--- a/zuul/cmd/__init__.py
+++ b/zuul/cmd/__init__.py
@@ -14,9 +14,9 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-import six
-from six.moves import configparser as ConfigParser
+import configparser
 import extras
+import io
 import logging
 import logging.config
 import os
@@ -48,7 +48,7 @@
             yappi.start()
         else:
             yappi.stop()
-            yappi_out = six.BytesIO()
+            yappi_out = io.BytesIO()
             yappi.get_func_stats().print_all(out=yappi_out)
             yappi.get_thread_stats().print_all(out=yappi_out)
             log.debug(yappi_out.getvalue())
@@ -69,7 +69,7 @@
         return "Zuul version: %s" % zuul_version_info.release_string()
 
     def read_config(self):
-        self.config = ConfigParser.ConfigParser()
+        self.config = configparser.ConfigParser()
         if self.args.config:
             locations = [self.args.config]
         else:
diff --git a/zuul/configloader.py b/zuul/configloader.py
index 5e0fe65..d4f7c43 100644
--- a/zuul/configloader.py
+++ b/zuul/configloader.py
@@ -15,7 +15,6 @@
 import copy
 import os
 import logging
-import six
 import pprint
 import textwrap
 
@@ -427,7 +426,7 @@
                 setattr(job, a, conf[k])
         if 'nodes' in conf:
             conf_nodes = conf['nodes']
-            if isinstance(conf_nodes, six.string_types):
+            if isinstance(conf_nodes, str):
                 # This references an existing named nodeset in the layout.
                 ns = layout.nodesets[conf_nodes]
             else:
@@ -576,7 +575,7 @@
     def _parseJobList(tenant, layout, conf, source_context,
                       start_mark, job_list):
         for conf_job in conf:
-            if isinstance(conf_job, six.string_types):
+            if isinstance(conf_job, str):
                 attrs = dict(name=conf_job)
             elif isinstance(conf_job, dict):
                 # A dictionary in a job tree may override params
@@ -1007,7 +1006,7 @@
 
     @staticmethod
     def _getProject(source, conf, current_include):
-        if isinstance(conf, six.string_types):
+        if isinstance(conf, str):
             # Return a project object whether conf is a dict or a str
             project = source.getProject(conf)
             project_include = current_include
@@ -1031,7 +1030,7 @@
     def _getProjects(source, conf, current_include):
         # Return a project object whether conf is a dict or a str
         projects = []
-        if isinstance(conf, six.string_types):
+        if isinstance(conf, str):
             # A simple project name string
             projects.append(TenantParser._getProject(
                 source, conf, current_include))
diff --git a/zuul/connection/__init__.py b/zuul/connection/__init__.py
index 90ab39c..3655115 100644
--- a/zuul/connection/__init__.py
+++ b/zuul/connection/__init__.py
@@ -15,11 +15,9 @@
 import abc
 
 import extras
-import six
 
 
-@six.add_metaclass(abc.ABCMeta)
-class BaseConnection(object):
+class BaseConnection(object, metaclass=abc.ABCMeta):
     """Base class for connections.
 
     A connection is a shared object that sources, triggers and reporters can
diff --git a/zuul/driver/__init__.py b/zuul/driver/__init__.py
index 0c3105d..b6c34e7 100644
--- a/zuul/driver/__init__.py
+++ b/zuul/driver/__init__.py
@@ -14,11 +14,8 @@
 
 import abc
 
-import six
 
-
-@six.add_metaclass(abc.ABCMeta)
-class Driver(object):
+class Driver(object, metaclass=abc.ABCMeta):
     """A Driver is an extension component of Zuul that supports
     interfacing with a remote system.  It can support any of the following
     interfaces (but must support at least one to be useful):
@@ -80,8 +77,7 @@
         pass
 
 
-@six.add_metaclass(abc.ABCMeta)
-class ConnectionInterface(object):
+class ConnectionInterface(object, metaclass=abc.ABCMeta):
     """The Connection interface.
 
     A driver which is able to supply a Connection should implement
@@ -124,8 +120,7 @@
         pass
 
 
-@six.add_metaclass(abc.ABCMeta)
-class TriggerInterface(object):
+class TriggerInterface(object, metaclass=abc.ABCMeta):
     """The trigger interface.
 
     A driver which is able to supply a trigger should implement this
@@ -167,8 +162,7 @@
         pass
 
 
-@six.add_metaclass(abc.ABCMeta)
-class SourceInterface(object):
+class SourceInterface(object, metaclass=abc.ABCMeta):
     """The source interface to be implemented by a driver.
 
     A driver which is able to supply a Source should implement this
@@ -216,8 +210,7 @@
         pass
 
 
-@six.add_metaclass(abc.ABCMeta)
-class ReporterInterface(object):
+class ReporterInterface(object, metaclass=abc.ABCMeta):
     """The reporter interface to be implemented by a driver.
 
     A driver which is able to supply a Reporter should implement this
@@ -256,8 +249,7 @@
         pass
 
 
-@six.add_metaclass(abc.ABCMeta)
-class WrapperInterface(object):
+class WrapperInterface(object, metaclass=abc.ABCMeta):
     """The wrapper interface to be implmeneted by a driver.
 
     A driver which wraps execution of commands executed by Zuul should
diff --git a/zuul/driver/bubblewrap/__init__.py b/zuul/driver/bubblewrap/__init__.py
index e3a9866..c165727 100644
--- a/zuul/driver/bubblewrap/__init__.py
+++ b/zuul/driver/bubblewrap/__init__.py
@@ -19,11 +19,10 @@
 import logging
 import os
 import pwd
+import shlex
 import subprocess
 import sys
 
-from six.moves import shlex_quote
-
 from zuul.driver import (Driver, WrapperInterface)
 
 
@@ -125,6 +124,7 @@
             ['{}'.format(x).encode('utf8') for x in passwd])
         (passwd_r, passwd_w) = os.pipe()
         os.write(passwd_w, passwd_bytes)
+        os.write(passwd_w, b'\n')
         os.close(passwd_w)
 
         gid = os.getgid()
@@ -133,6 +133,7 @@
             ['{}'.format(x).encode('utf8') for x in group])
         group_r, group_w = os.pipe()
         os.write(group_w, group_bytes)
+        os.write(group_w, b'\n')
         os.close(group_w)
 
         kwargs = dict(kwargs)  # Don't update passed in dict
@@ -144,7 +145,7 @@
         command = [x.format(**kwargs) for x in bwrap_command]
 
         self.log.debug("Bubblewrap command: %s",
-                       " ".join(shlex_quote(c) for c in command))
+                       " ".join(shlex.quote(c) for c in command))
 
         wrapped_popen = WrappedPopen(command, passwd_r, group_r)
 
diff --git a/zuul/driver/gerrit/gerritconnection.py b/zuul/driver/gerrit/gerritconnection.py
index 9033ee6..39a81bc 100644
--- a/zuul/driver/gerrit/gerritconnection.py
+++ b/zuul/driver/gerrit/gerritconnection.py
@@ -18,11 +18,11 @@
 import select
 import threading
 import time
-from six.moves import queue as Queue
-from six.moves import shlex_quote
 import paramiko
 import logging
 import pprint
+import shlex
+import queue
 import voluptuous as v
 
 from zuul.connection import BaseConnection
@@ -260,7 +260,7 @@
         self.keyfile = self.connection_config.get('sshkey', None)
         self.keepalive = int(self.connection_config.get('keepalive', 60))
         self.watcher_thread = None
-        self.event_queue = Queue.Queue()
+        self.event_queue = queue.Queue()
         self.client = None
 
         self.baseurl = self.connection_config.get('baseurl',
@@ -606,7 +606,7 @@
     def review(self, project, change, message, action={}):
         cmd = 'gerrit review --project %s' % project
         if message:
-            cmd += ' --message %s' % shlex_quote(message)
+            cmd += ' --message %s' % shlex.quote(message)
         for key, val in action.items():
             if val is True:
                 cmd += ' --%s' % key
diff --git a/zuul/driver/git/gitconnection.py b/zuul/driver/git/gitconnection.py
index ca88d3f..f4fe7e5 100644
--- a/zuul/driver/git/gitconnection.py
+++ b/zuul/driver/git/gitconnection.py
@@ -14,7 +14,7 @@
 # under the License.
 
 import logging
-from six.moves import urllib
+import urllib
 
 import voluptuous as v
 
diff --git a/zuul/executor/server.py b/zuul/executor/server.py
index f2aedba..94678d7 100644
--- a/zuul/executor/server.py
+++ b/zuul/executor/server.py
@@ -18,6 +18,7 @@
 import os
 import shutil
 import signal
+import shlex
 import socket
 import subprocess
 import tempfile
@@ -27,7 +28,6 @@
 from zuul.lib.yamlutil import yaml
 
 import gear
-from six.moves import shlex_quote
 
 import zuul.merger.merger
 import zuul.ansible
@@ -1288,7 +1288,7 @@
             if self.aborted:
                 return (self.RESULT_ABORTED, None)
             self.log.debug("Ansible command: ANSIBLE_CONFIG=%s %s",
-                           config_file, " ".join(shlex_quote(c) for c in cmd))
+                           config_file, " ".join(shlex.quote(c) for c in cmd))
             self.proc = popen(
                 cmd,
                 cwd=self.jobdir.work_root,
diff --git a/zuul/lib/clonemapper.py b/zuul/lib/clonemapper.py
index 57ac177..7423308 100644
--- a/zuul/lib/clonemapper.py
+++ b/zuul/lib/clonemapper.py
@@ -14,17 +14,11 @@
 # under the License.
 
 from collections import defaultdict
-import extras
+from collections import OrderedDict
 import logging
 import os
 import re
 
-import six
-
-
-OrderedDict = extras.try_imports(['collections.OrderedDict',
-                                  'ordereddict.OrderedDict'])
-
 
 class CloneMapper(object):
     log = logging.getLogger("zuul.CloneMapper")
@@ -62,17 +56,17 @@
             raise Exception("Expansion error. Check error messages above")
 
         self.log.info("Mapping projects to workspace...")
-        for project, dest in six.iteritems(ret):
+        for project, dest in ret.items():
             dest = os.path.normpath(os.path.join(workspace, dest[0]))
             ret[project] = dest
             self.log.info("  %s -> %s", project, dest)
 
         self.log.debug("Checking overlap in destination directories...")
         check = defaultdict(list)
-        for project, dest in six.iteritems(ret):
+        for project, dest in ret.items():
             check[dest].append(project)
 
-        dupes = dict((d, p) for (d, p) in six.iteritems(check) if len(p) > 1)
+        dupes = dict((d, p) for (d, p) in check.items() if len(p) > 1)
         if dupes:
             raise Exception("Some projects share the same destination: %s",
                             dupes)
diff --git a/zuul/lib/cloner.py b/zuul/lib/cloner.py
index 3070be6..3fcffbe 100644
--- a/zuul/lib/cloner.py
+++ b/zuul/lib/cloner.py
@@ -18,8 +18,6 @@
 import os
 import re
 
-import six
-
 from git import GitCommandError
 from zuul import exceptions
 from zuul.lib.clonemapper import CloneMapper
@@ -72,7 +70,7 @@
         dests = mapper.expand(workspace=self.workspace)
 
         self.log.info("Preparing %s repositories", len(dests))
-        for project, dest in six.iteritems(dests):
+        for project, dest in dests.items():
             self.prepareRepo(project, dest)
         self.log.info("Prepared all repositories")
 
diff --git a/zuul/lib/commandsocket.py b/zuul/lib/commandsocket.py
index ae62204..901291a 100644
--- a/zuul/lib/commandsocket.py
+++ b/zuul/lib/commandsocket.py
@@ -18,7 +18,7 @@
 import os
 import socket
 import threading
-from six.moves import queue
+import queue
 
 
 class CommandSocket(object):
diff --git a/zuul/model.py b/zuul/model.py
index 610b9b6..a89c6d1 100644
--- a/zuul/model.py
+++ b/zuul/model.py
@@ -13,19 +13,13 @@
 # under the License.
 
 import abc
+from collections import OrderedDict
 import copy
 import logging
 import os
 import struct
 import time
 from uuid import uuid4
-import extras
-
-import six
-
-OrderedDict = extras.try_imports(['collections.OrderedDict',
-                                  'ordereddict.OrderedDict'])
-
 
 MERGER_MERGE = 1          # "git merge"
 MERGER_MERGE_RESOLVE = 2  # "git merge -s resolve"
@@ -669,8 +663,7 @@
             path=self.path)
 
 
-@six.add_metaclass(abc.ABCMeta)
-class Role(object):
+class Role(object, metaclass=abc.ABCMeta):
     """A reference to an ansible role."""
 
     def __init__(self, target_name):
diff --git a/zuul/reporter/__init__.py b/zuul/reporter/__init__.py
index 89830d5..0ac5766 100644
--- a/zuul/reporter/__init__.py
+++ b/zuul/reporter/__init__.py
@@ -15,11 +15,8 @@
 import abc
 import logging
 
-import six
 
-
-@six.add_metaclass(abc.ABCMeta)
-class BaseReporter(object):
+class BaseReporter(object, metaclass=abc.ABCMeta):
     """Base class for reporters.
 
     Defines the exact public methods that must be supplied.
diff --git a/zuul/rpclistener.py b/zuul/rpclistener.py
index 0079ab8..c1ee50e 100644
--- a/zuul/rpclistener.py
+++ b/zuul/rpclistener.py
@@ -19,7 +19,6 @@
 import traceback
 
 import gear
-import six
 
 from zuul import model
 
@@ -179,8 +178,7 @@
         # TODO: use args to filter by pipeline etc
         running_items = []
         for tenant in self.sched.abide.tenants.values():
-            for pipeline_name, pipeline in six.iteritems(
-                    tenant.layout.pipelines):
+            for pipeline_name, pipeline in tenant.layout.pipelines.items():
                 for queue in pipeline.queues:
                     for item in queue.queue:
                         running_items.append(item.formatJSON())
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index a63d270..2076163 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -20,8 +20,7 @@
 import logging
 import os
 import pickle
-import six
-from six.moves import queue as Queue
+import queue
 import socket
 import sys
 import threading
@@ -49,7 +48,9 @@
     def wait(self, timeout=None):
         self._wait_event.wait(timeout)
         if self._exc_info:
-            six.reraise(*self._exc_info)
+            # http://python3porting.com/differences.html#raise
+            e, v, t = self._exc_info
+            raise e(v).with_traceback(t)
         return self._wait_event.is_set()
 
 
@@ -217,9 +218,9 @@
         self.triggers = dict()
         self.config = config
 
-        self.trigger_event_queue = Queue.Queue()
-        self.result_event_queue = Queue.Queue()
-        self.management_event_queue = Queue.Queue()
+        self.trigger_event_queue = queue.Queue()
+        self.result_event_queue = queue.Queue()
+        self.management_event_queue = queue.Queue()
         self.abide = model.Abide()
 
         if not testonly:
diff --git a/zuul/source/__init__.py b/zuul/source/__init__.py
index 68baf0e..b37aeb4 100644
--- a/zuul/source/__init__.py
+++ b/zuul/source/__init__.py
@@ -14,11 +14,8 @@
 
 import abc
 
-import six
 
-
-@six.add_metaclass(abc.ABCMeta)
-class BaseSource(object):
+class BaseSource(object, metaclass=abc.ABCMeta):
     """Base class for sources.
 
     A source class gives methods for fetching and updating changes. Each
diff --git a/zuul/trigger/__init__.py b/zuul/trigger/__init__.py
index a5406d6..a67c99b 100644
--- a/zuul/trigger/__init__.py
+++ b/zuul/trigger/__init__.py
@@ -14,11 +14,8 @@
 
 import abc
 
-import six
 
-
-@six.add_metaclass(abc.ABCMeta)
-class BaseTrigger(object):
+class BaseTrigger(object, metaclass=abc.ABCMeta):
     """Base class for triggers.
 
     Defines the exact public methods that must be supplied."""