Add docs generation (+ clean up)

Moved public access to exceptions from metalsmith itself to
metalsmith.exceptions to avoid clutterning the former.

Updated tox.ini to use upper-constraints.

Change-Id: I136e036749171dc6d36d644e79c6fcfeef6242af
This commit is contained in:
Dmitry Tantsur 2018-05-18 13:11:08 +02:00
parent 86ef953d49
commit 23fbeea112
14 changed files with 215 additions and 40 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ doc/source/api/
# Packages/installer info
*.egg
*.egg-info
.eggs/
dist
build
eggs

View File

@ -17,8 +17,8 @@ Installation
pip install --user metalsmith
Usage
-----
CLI Usage
---------
Generic usage is as follows::

5
doc/requirements.txt Normal file
View File

@ -0,0 +1,5 @@
# 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.
sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
sphinxcontrib-apidoc>=0.2.0 # BSD

88
doc/source/conf.py Normal file
View File

@ -0,0 +1,88 @@
# 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 sys
sys.path.insert(0, os.path.abspath('../..'))
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
'sphinxcontrib.apidoc',
]
autoclass_content = 'both'
apidoc_module_dir = '../../metalsmith'
apidoc_output_dir = 'api'
apidoc_excluded_paths = ['test']
apidoc_separate_modules = True
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'metalsmith'
copyright = u'2018, MetalSmith Developers '
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
from metalsmith import version as ms_version # noqa
# The full version, including alpha/beta/rc tags.
release = ms_version.version_info.release_string()
# The short X.Y version.
version = ms_version.version_info.version_string()
# If true, '()' will be appended to :func: etc. cross-reference text.
add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
# html_theme_path = ["."]
# html_theme = '_theme'
# html_static_path = ['static']
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'%s.tex' % project,
u'%s Documentation' % project,
u'MetalSmith Developers', 'manual'),
]
# Example configuration for intersphinx: refer to the Python standard library.
# intersphinx_mapping = {'http://docs.python.org/': None}

23
doc/source/index.rst Normal file
View File

@ -0,0 +1,23 @@
.. include:: ../../README.rst
Python API
----------
The main entry point to the API is :py:class:`metalsmith.Provisioner`.
.. toctree::
:maxdepth: 3
api/metalsmith
.. toctree::
:hidden:
api/modules
Indexes
-------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -13,5 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from ._exceptions import * # noqa
from ._provisioner import Provisioner # noqa
from metalsmith._provisioner import Provisioner
__all__ = ['Provisioner']

View File

@ -20,10 +20,10 @@ import sys
import six
from metalsmith import _exceptions
from metalsmith import _os_api
from metalsmith import _scheduler
from metalsmith import _utils
from metalsmith import exceptions
LOG = logging.getLogger(__name__)
@ -33,7 +33,15 @@ _ATTACHED_PORTS = 'metalsmith_attached_ports'
class Provisioner(object):
"""API to deploy/undeploy nodes with OpenStack."""
"""API to deploy/undeploy nodes with OpenStack.
:param session: `Session` object (from ``keystoneauth``) to use when
making API requests. Mutually exclusive with **cloud_region**.
:param cloud_region: cloud configuration object (from ``openstacksdk``)
to use when making API requests. Mutually exclusive with **session**.
:param dry_run: boolean value, set to ``True`` to prevent any API calls
from being actually made.
"""
def __init__(self, session=None, cloud_region=None, dry_run=False):
self._api = _os_api.API(session=session, cloud_region=cloud_region)
@ -42,16 +50,21 @@ class Provisioner(object):
def reserve_node(self, resource_class, capabilities=None):
"""Find and reserve a suitable node.
Example::
node = provisioner.reserve_node("compute",
capabilities={"boot_mode": "uefi"})
:param resource_class: Requested resource class.
:param capabilities: Requested capabilities as a dict.
:return: reserved Node object
:raises: ReservationFailed
:return: reserved `Node` object.
:raises: :py:class:`metalsmith.exceptions.ReservationFailed`
"""
capabilities = capabilities or {}
nodes = self._api.list_nodes(resource_class=resource_class)
if not nodes:
raise _exceptions.ResourceClassNotFound(resource_class,
raise exceptions.ResourceClassNotFound(resource_class,
capabilities)
# Make sure parallel executions don't try nodes in the same sequence
@ -70,6 +83,14 @@ class Provisioner(object):
ssh_keys=None, netboot=False, wait=None):
"""Provision the node with the given image.
Example::
provisioner.provision_node("compute-1", "centos",
nics=[{"network": "private"},
{"network": "external"}],
root_disk_size=50,
wait=3600)
:param node: Node object, UUID or name.
:param image_ref: Image name or UUID to provision.
:param nics: List of virtual NICs to attach to physical ports.
@ -83,7 +104,8 @@ class Provisioner(object):
:param netboot: Whether to use networking boot for final instances.
:param wait: How many seconds to wait for the deployment to finish,
None to return immediately.
:return: Reservation
:return: provisioned `Node` object.
:raises: :py:class:`metalsmith.exceptions.Error`
"""
created_ports = []
attached_ports = []
@ -96,7 +118,7 @@ class Provisioner(object):
try:
image = self._api.get_image_info(image_ref)
except Exception as exc:
raise _exceptions.InvalidImage(
raise exceptions.InvalidImage(
'Cannot find image %(image)s: %(error)s' %
{'image': image_ref, 'error': exc})
@ -188,7 +210,7 @@ class Provisioner(object):
try:
network = self._api.get_network(nic_id)
except Exception as exc:
raise _exceptions.InvalidNIC(
raise exceptions.InvalidNIC(
'Cannot find network %(net)s: %(error)s' %
{'net': nic_id, 'error': exc})
else:
@ -197,7 +219,7 @@ class Provisioner(object):
try:
port = self._api.get_port(nic_id)
except Exception as exc:
raise _exceptions.InvalidNIC(
raise exceptions.InvalidNIC(
'Cannot find port %(port)s: %(error)s' %
{'port': nic_id, 'error': exc})
else:
@ -263,6 +285,7 @@ class Provisioner(object):
:param node: node object, UUID or name.
:param wait: How many seconds to wait for the process to finish,
None to return immediately.
:return: nothing.
"""
node = self._api.get_node(node)
if self._dry_run:

View File

@ -19,8 +19,8 @@ import logging
import six
from metalsmith import _exceptions
from metalsmith import _utils
from metalsmith import exceptions
LOG = logging.getLogger(__name__)
@ -159,7 +159,7 @@ class CapabilitiesFilter(Filter):
message = ("No available nodes found with capabilities %(req)s, "
"existing capabilities: %(exist)s" %
{'req': requested, 'exist': existing or 'none'})
raise _exceptions.CapabilitiesNotFound(message,
raise exceptions.CapabilitiesNotFound(message,
self._resource_class,
self._capabilities)
@ -189,7 +189,7 @@ class ValidationFilter(Filter):
def fail(self):
errors = ", ".join(self._failed_validation)
message = "All available nodes have failed validation: %s" % errors
raise _exceptions.ValidationFailed(message,
raise exceptions.ValidationFailed(message,
self._resource_class,
self._capabilities)
@ -221,5 +221,5 @@ class IronicReserver(Reserver):
return result
def fail(self):
raise _exceptions.AllNodesReserved(self._resource_class,
raise exceptions.AllNodesReserved(self._resource_class,
self._capabilities)

View File

@ -19,7 +19,7 @@ import os
import shutil
import tempfile
from metalsmith import _exceptions
from metalsmith import exceptions
def log_node(node):
@ -74,11 +74,11 @@ def get_root_disk(root_disk_size, node):
try:
assert int(node.properties['local_gb']) > 0
except KeyError:
raise _exceptions.UnknownRootDiskSize(
raise exceptions.UnknownRootDiskSize(
'No local_gb for node %s and no root disk size requested' %
log_node(node))
except (TypeError, ValueError, AssertionError):
raise _exceptions.UnknownRootDiskSize(
raise exceptions.UnknownRootDiskSize(
'The local_gb for node %(node)s is invalid: '
'expected positive integer, got %(value)s' %
{'node': log_node(node),

View File

@ -16,9 +16,9 @@
import mock
import testtools
from metalsmith import _exceptions
from metalsmith import _os_api
from metalsmith import _provisioner
from metalsmith import exceptions
class Base(testtools.TestCase):
@ -44,7 +44,7 @@ class TestReserveNode(Base):
def test_no_nodes(self):
self.api.list_nodes.return_value = []
self.assertRaises(_exceptions.ResourceClassNotFound,
self.assertRaises(exceptions.ResourceClassNotFound,
self.pr.reserve_node, 'control')
self.assertFalse(self.api.reserve_node.called)
@ -315,7 +315,7 @@ class TestProvisionNode(Base):
def test_missing_image(self):
self.api.get_image_info.side_effect = RuntimeError('Not found')
self.assertRaisesRegex(_exceptions.InvalidImage, 'Not found',
self.assertRaisesRegex(exceptions.InvalidImage, 'Not found',
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
@ -324,7 +324,7 @@ class TestProvisionNode(Base):
def test_invalid_network(self):
self.api.get_network.side_effect = RuntimeError('Not found')
self.assertRaisesRegex(_exceptions.InvalidNIC, 'Not found',
self.assertRaisesRegex(exceptions.InvalidNIC, 'Not found',
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
@ -334,7 +334,7 @@ class TestProvisionNode(Base):
def test_invalid_port(self):
self.api.get_port.side_effect = RuntimeError('Not found')
self.assertRaisesRegex(_exceptions.InvalidNIC, 'Not found',
self.assertRaisesRegex(exceptions.InvalidNIC, 'Not found',
self.pr.provision_node,
self.node, 'image', [{'port': 'port1'}])
self.api.update_node.assert_called_once_with(self.node, CLEAN_UP)
@ -344,7 +344,7 @@ class TestProvisionNode(Base):
def test_no_local_gb(self):
self.node.properties = {}
self.assertRaises(_exceptions.UnknownRootDiskSize,
self.assertRaises(exceptions.UnknownRootDiskSize,
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.assertFalse(self.api.create_port.called)
@ -354,7 +354,7 @@ class TestProvisionNode(Base):
def test_invalid_local_gb(self):
for value in (None, 'meow', -42, []):
self.node.properties = {'local_gb': value}
self.assertRaises(_exceptions.UnknownRootDiskSize,
self.assertRaises(exceptions.UnknownRootDiskSize,
self.pr.provision_node,
self.node, 'image', [{'network': 'network'}])
self.assertFalse(self.api.create_port.called)

View File

@ -16,8 +16,8 @@
import mock
import testtools
from metalsmith import _exceptions
from metalsmith import _scheduler
from metalsmith import exceptions
class TestScheduleNode(testtools.TestCase):
@ -114,7 +114,7 @@ class TestCapabilitiesFilter(testtools.TestCase):
def test_fail_no_capabilities(self):
fltr = _scheduler.CapabilitiesFilter('rsc', {'profile': 'compute'})
self.assertRaisesRegex(_exceptions.CapabilitiesNotFound,
self.assertRaisesRegex(exceptions.CapabilitiesNotFound,
'No available nodes found with capabilities '
'profile=compute, existing capabilities: none',
fltr.fail)
@ -146,7 +146,7 @@ class TestCapabilitiesFilter(testtools.TestCase):
properties={'capabilities': 'profile:control'},
spec=['properties', 'name', 'uuid'])
self.assertFalse(fltr(node))
self.assertRaisesRegex(_exceptions.CapabilitiesNotFound,
self.assertRaisesRegex(exceptions.CapabilitiesNotFound,
'No available nodes found with capabilities '
'profile=compute, existing capabilities: '
r'profile=control \(1 node\(s\)\)',
@ -158,7 +158,7 @@ class TestCapabilitiesFilter(testtools.TestCase):
node = mock.Mock(properties={'capabilities': cap},
spec=['properties', 'name', 'uuid'])
self.assertFalse(fltr(node))
self.assertRaisesRegex(_exceptions.CapabilitiesNotFound,
self.assertRaisesRegex(exceptions.CapabilitiesNotFound,
'No available nodes found with capabilities '
'profile=compute, existing capabilities: none',
fltr.fail)
@ -181,7 +181,7 @@ class TestValidationFilter(testtools.TestCase):
self.api.validate_node.side_effect = RuntimeError('boom')
self.assertFalse(self.fltr(node))
self.assertRaisesRegex(_exceptions.ValidationFailed,
self.assertRaisesRegex(exceptions.ValidationFailed,
'All available nodes have failed validation: '
'Node .* failed validation: boom',
self.fltr.fail)
@ -198,7 +198,7 @@ class TestIronicReserver(testtools.TestCase):
self.reserver = _scheduler.IronicReserver(self.api, 'rsc', {})
def test_fail(self, mock_validation):
self.assertRaisesRegex(_exceptions.AllNodesReserved,
self.assertRaisesRegex(exceptions.AllNodesReserved,
'All the candidate nodes are already reserved',
self.reserver.fail)

19
metalsmith/version.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright 2018 Red Hat, Inc.
# All Rights Reserved.
#
# 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 pbr.version
version_info = pbr.version.VersionInfo('metalsmith')
"""Package version reported by pbr."""

17
tox.ini
View File

@ -4,6 +4,8 @@ envlist = py3,py27,pep8
[testenv]
usedevelop = True
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands =
coverage run --branch --include "metalsmith*" -m unittest discover metalsmith.test
@ -14,12 +16,25 @@ passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY \
OS_USER_DOMAIN_NAME OS_PROJECT_DOMAIN_NAME
[testenv:venv]
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/doc/requirements.txt
commands = {posargs}
[testenv:pep8]
commands =
flake8 metalsmith
doc8 README.rst
doc8 README.rst doc/source
[testenv:docs]
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
commands =
sphinx-build -a -E -W -b html doc/source doc/build/html
[flake8]
max-complexity=15