diff --git a/doc/requirements.txt b/doc/requirements.txt
new file mode 100644
index 000000000..966197a1d
--- /dev/null
+++ b/doc/requirements.txt
@@ -0,0 +1,9 @@
+# The order of packages is significant, because pip processes them in the order
+# of appearance. Changing the order has an impact on the overall integration
+# process, which may cause wedges in the gate later.
+openstackdocstheme>=1.20.0 # Apache-2.0
+sphinx>=1.6.5,!=1.6.6,!=1.6.7 # BSD
+sphinxcontrib-pecanwsme>=0.8.0 # Apache-2.0
+reno>=2.7.0 # Apache-2.0
+sphinxcontrib-apidoc>=0.2.0  # BSD
+os-api-ref>=1.4.0 # Apache-2.0
diff --git a/doc/source/conf.py b/doc/source/conf.py
index b0a7c62a8..fda641b64 100755
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -32,7 +32,7 @@ sys.path.insert(0, os.path.abspath('./'))
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
     'oslo_config.sphinxext',
-    'sphinx.ext.autodoc',
+    'sphinxcontrib.apidoc',
     'sphinx.ext.viewcode',
     'sphinxcontrib.httpdomain',
     'sphinxcontrib.pecanwsme.rest',
@@ -55,6 +55,18 @@ sample_config_basename = 'watcher'
 # text edit cycles.
 # execute "export SPHINX_DEBUG=1" in your terminal to disable
 
+# sphinxcontrib.apidoc options
+apidoc_module_dir = '../../watcher'
+apidoc_output_dir = 'api'
+apidoc_excluded_paths = [
+    'tests/*',
+    'db',
+    'decision_engine',
+    'doc',
+    'objects',
+]
+apidoc_separate_modules = True
+
 # The suffix of source filenames.
 source_suffix = '.rst'
 
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 2805c3eab..19a3f397b 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -117,7 +117,7 @@ Watcher Manual Pages
 .. toctree::
    :hidden:
 
-   api/autoindex
+   api/modules
 
 
 Indices and tables
diff --git a/setup.cfg b/setup.cfg
index 7239c2293..9b2ee5800 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -105,26 +105,6 @@ watcher_cluster_data_model_collectors =
     baremetal = watcher.decision_engine.model.collector.ironic:BaremetalClusterDataModelCollector
 
 
-[pbr]
-autodoc_index_modules = true
-autodoc_exclude_modules =
-    watcher.db.sqlalchemy.alembic.env
-    watcher.db.sqlalchemy.alembic.versions.*
-    watcher.tests.*
-    watcher.doc
-
-
-[build_sphinx]
-source-dir = doc/source
-build-dir = doc/build
-fresh_env = 1
-all_files = 1
-warning-is-error = 1
-
-[upload_sphinx]
-upload-dir = doc/build/html
-
-
 [compile_catalog]
 directory = watcher/locale
 domain = watcher
diff --git a/test-requirements.txt b/test-requirements.txt
index 3a5c09a4c..8e2b35813 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -12,17 +12,5 @@ os-testr>=1.0.0 # Apache-2.0
 testscenarios>=0.5.0 # Apache-2.0/BSD
 testtools>=2.3.0 # MIT
 stestr>=2.0.0 # Apache-2.0
-
-# Doc requirements
-openstackdocstheme>=1.20.0 # Apache-2.0
-sphinx>=1.6.5,!=1.6.6,!=1.6.7 # BSD
-sphinxcontrib-pecanwsme>=0.8.0 # Apache-2.0
-
-# api-ref
 os-api-ref>=1.4.0 # Apache-2.0
-
-# releasenotes
-reno>=2.7.0 # Apache-2.0
-
-# bandit
 bandit>=1.1.0 # Apache-2.0
diff --git a/tox.ini b/tox.ini
index 100803dc4..eb0462dad 100644
--- a/tox.ini
+++ b/tox.ini
@@ -43,13 +43,12 @@ commands =
 [testenv:docs]
 basepython = python3
 setenv = PYTHONHASHSEED=0
-commands =
-    doc8 doc/source/ CONTRIBUTING.rst HACKING.rst README.rst
-    python setup.py build_sphinx
+deps = -r{toxinidir}/doc/requirements.txt
+commands = sphinx-build -W -b html doc/source doc/build/html
 
 [testenv:api-ref]
-# This environment is called from CI scripts to test and publish
-# the API Ref to developer.openstack.org.
+basepython = python3
+deps = -r{toxinidir}/doc/requirements.txt
 whitelist_externals = bash
 commands =
   bash -c 'rm -rf api-ref/build'
@@ -93,6 +92,7 @@ ignore-path=doc/source/image_src,doc/source/man,doc/source/api
 
 [testenv:releasenotes]
 basepython = python3
+deps = -r{toxinidir}/doc/requirements.txt
 commands = sphinx-build -a -W -E -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
 
 [testenv:bandit]
diff --git a/watcher/api/app.py b/watcher/api/app.py
index 7926eda4a..66e798fcc 100644
--- a/watcher/api/app.py
+++ b/watcher/api/app.py
@@ -21,7 +21,7 @@ import pecan
 
 from watcher.api import acl
 from watcher.api import config as api_config
-from watcher.api import middleware
+from watcher.api.middleware import parsable_error
 from watcher import conf
 
 CONF = conf.CONF
@@ -42,7 +42,7 @@ def setup_app(config=None):
         app_conf.pop('root'),
         logging=getattr(config, 'logging', {}),
         debug=CONF.debug,
-        wrap_app=middleware.ParsableErrorMiddleware,
+        wrap_app=parsable_error.ParsableErrorMiddleware,
         **app_conf
     )
 
diff --git a/watcher/api/middleware/__init__.py b/watcher/api/middleware/__init__.py
index 6141cb90d..e69de29bb 100644
--- a/watcher/api/middleware/__init__.py
+++ b/watcher/api/middleware/__init__.py
@@ -1,25 +0,0 @@
-# -*- encoding: utf-8 -*-
-#
-# 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.
-
-
-from watcher.api.middleware import auth_token
-from watcher.api.middleware import parsable_error
-
-
-ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
-AuthTokenMiddleware = auth_token.AuthTokenMiddleware
-
-__all__ = (ParsableErrorMiddleware,
-           AuthTokenMiddleware)
diff --git a/watcher/decision_engine/goal/__init__.py b/watcher/decision_engine/goal/__init__.py
index 15a21354b..ec1295c30 100644
--- a/watcher/decision_engine/goal/__init__.py
+++ b/watcher/decision_engine/goal/__init__.py
@@ -21,7 +21,7 @@ ServerConsolidation = goals.ServerConsolidation
 ThermalOptimization = goals.ThermalOptimization
 Unclassified = goals.Unclassified
 WorkloadBalancing = goals.WorkloadBalancing
-NoisyNeighbor = goals.NoisyNeighborOptimization
+NoisyNeighborOptimization = goals.NoisyNeighborOptimization
 SavingEnergy = goals.SavingEnergy
 HardwareMaintenance = goals.HardwareMaintenance
 
diff --git a/watcher/decision_engine/model/model_root.py b/watcher/decision_engine/model/model_root.py
index a72f41d4b..012f2c586 100644
--- a/watcher/decision_engine/model/model_root.py
+++ b/watcher/decision_engine/model/model_root.py
@@ -87,10 +87,11 @@ class ModelRoot(nx.DiGraph, base.Model):
     def map_instance(self, instance, node):
         """Map a newly created instance to a node
 
-        :param instance: :py:class:`~.Instance` object or instance UUID
-        :type instance: str or :py:class:`~.Instance`
-        :param node: :py:class:`~.ComputeNode` object or node UUID
-        :type node: str or :py:class:`~.Instance`
+        :param instance: :py:class:`~.instance.Instance` object or instance
+           UUID
+        :type instance: str or :py:class:`~.instance.Instance`
+        :param node: :py:class:`~.node.ComputeNode` object or node UUID
+        :type node: str or :py:class:`~.instance.Instance`
         """
         if isinstance(instance, six.string_types):
             instance = self.get_instance_by_uuid(instance)
@@ -309,8 +310,8 @@ class StorageModelRoot(nx.DiGraph, base.Model):
     def map_pool(self, pool, node):
         """Map a newly created pool to a node
 
-        :param pool: :py:class:`~.Pool` object or pool name
-        :param node: :py:class:`~.StorageNode` object or node host
+        :param pool: :py:class:`~.node.Pool` object or pool name
+        :param node: :py:class:`~.node.StorageNode` object or node host
         """
         if isinstance(pool, six.string_types):
             pool = self.get_pool_by_pool_name(pool)
@@ -325,8 +326,8 @@ class StorageModelRoot(nx.DiGraph, base.Model):
     def unmap_pool(self, pool, node):
         """Unmap a pool from a node
 
-        :param pool: :py:class:`~.Pool` object or pool name
-        :param node: :py:class:`~.StorageNode` object or node name
+        :param pool: :py:class:`~.node.Pool` object or pool name
+        :param node: :py:class:`~.node.StorageNode` object or node name
         """
         if isinstance(pool, six.string_types):
             pool = self.get_pool_by_pool_name(pool)
@@ -353,8 +354,8 @@ class StorageModelRoot(nx.DiGraph, base.Model):
     def map_volume(self, volume, pool):
         """Map a newly created volume to a pool
 
-        :param volume: :py:class:`~.Volume` object or volume UUID
-        :param pool: :py:class:`~.Pool` object or pool name
+        :param volume: :py:class:`~.volume.Volume` object or volume UUID
+        :param pool: :py:class:`~.node.Pool` object or pool name
         """
         if isinstance(volume, six.string_types):
             volume = self.get_volume_by_uuid(volume)
@@ -369,8 +370,8 @@ class StorageModelRoot(nx.DiGraph, base.Model):
     def unmap_volume(self, volume, pool):
         """Unmap a volume from a pool
 
-        :param volume: :py:class:`~.Volume` object or volume UUID
-        :param pool: :py:class:`~.Pool` object or pool name
+        :param volume: :py:class:`~.volume.Volume` object or volume UUID
+        :param pool: :py:class:`~.node.Pool` object or pool name
         """
         if isinstance(volume, six.string_types):
             volume = self.get_volume_by_uuid(volume)
@@ -392,7 +393,7 @@ class StorageModelRoot(nx.DiGraph, base.Model):
     def get_node_by_name(self, name):
         """Get a node by node name
 
-        :param node: :py:class:`~.StorageNode` object or node name
+        :param node: :py:class:`~.node.StorageNode` object or node name
         """
         try:
             return self._get_by_name(name.split("#")[0])