From 23fbeea112b98c79b8ebe775aa540e4ae0ff1a9b Mon Sep 17 00:00:00 2001 From: Dmitry Tantsur Date: Fri, 18 May 2018 13:11:08 +0200 Subject: [PATCH] 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 --- .gitignore | 1 + README.rst | 4 +- doc/requirements.txt | 5 ++ doc/source/conf.py | 88 ++++++++++++++++++++ doc/source/index.rst | 23 +++++ metalsmith/__init__.py | 5 +- metalsmith/_provisioner.py | 43 +++++++--- metalsmith/_scheduler.py | 18 ++-- metalsmith/_utils.py | 6 +- metalsmith/{_exceptions.py => exceptions.py} | 0 metalsmith/test/test_provisioner.py | 14 ++-- metalsmith/test/test_scheduler.py | 12 +-- metalsmith/version.py | 19 +++++ tox.ini | 17 +++- 14 files changed, 215 insertions(+), 40 deletions(-) create mode 100644 doc/requirements.txt create mode 100644 doc/source/conf.py create mode 100644 doc/source/index.rst rename metalsmith/{_exceptions.py => exceptions.py} (100%) create mode 100644 metalsmith/version.py diff --git a/.gitignore b/.gitignore index b4b8913..372f4b8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ doc/source/api/ # Packages/installer info *.egg *.egg-info +.eggs/ dist build eggs diff --git a/README.rst b/README.rst index d29e3cd..ec3e070 100644 --- a/README.rst +++ b/README.rst @@ -17,8 +17,8 @@ Installation pip install --user metalsmith -Usage ------ +CLI Usage +--------- Generic usage is as follows:: diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..5b346e7 --- /dev/null +++ b/doc/requirements.txt @@ -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 diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..ff1a4bb --- /dev/null +++ b/doc/source/conf.py @@ -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} diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..a1a3ba1 --- /dev/null +++ b/doc/source/index.rst @@ -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` diff --git a/metalsmith/__init__.py b/metalsmith/__init__.py index 536b548..3e074be 100644 --- a/metalsmith/__init__.py +++ b/metalsmith/__init__.py @@ -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'] diff --git a/metalsmith/_provisioner.py b/metalsmith/_provisioner.py index 13c5ac2..06aa27a 100644 --- a/metalsmith/_provisioner.py +++ b/metalsmith/_provisioner.py @@ -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,17 +50,22 @@ 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, - capabilities) + raise exceptions.ResourceClassNotFound(resource_class, + capabilities) # Make sure parallel executions don't try nodes in the same sequence random.shuffle(nodes) @@ -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: diff --git a/metalsmith/_scheduler.py b/metalsmith/_scheduler.py index 2aece7c..62f2c87 100644 --- a/metalsmith/_scheduler.py +++ b/metalsmith/_scheduler.py @@ -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,9 +159,9 @@ 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, - self._resource_class, - self._capabilities) + raise exceptions.CapabilitiesNotFound(message, + self._resource_class, + self._capabilities) class ValidationFilter(Filter): @@ -189,9 +189,9 @@ class ValidationFilter(Filter): def fail(self): errors = ", ".join(self._failed_validation) message = "All available nodes have failed validation: %s" % errors - raise _exceptions.ValidationFailed(message, - self._resource_class, - self._capabilities) + raise exceptions.ValidationFailed(message, + self._resource_class, + self._capabilities) class IronicReserver(Reserver): @@ -221,5 +221,5 @@ class IronicReserver(Reserver): return result def fail(self): - raise _exceptions.AllNodesReserved(self._resource_class, - self._capabilities) + raise exceptions.AllNodesReserved(self._resource_class, + self._capabilities) diff --git a/metalsmith/_utils.py b/metalsmith/_utils.py index f819698..146d0ba 100644 --- a/metalsmith/_utils.py +++ b/metalsmith/_utils.py @@ -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), diff --git a/metalsmith/_exceptions.py b/metalsmith/exceptions.py similarity index 100% rename from metalsmith/_exceptions.py rename to metalsmith/exceptions.py diff --git a/metalsmith/test/test_provisioner.py b/metalsmith/test/test_provisioner.py index df1a8ce..161dae1 100644 --- a/metalsmith/test/test_provisioner.py +++ b/metalsmith/test/test_provisioner.py @@ -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) diff --git a/metalsmith/test/test_scheduler.py b/metalsmith/test/test_scheduler.py index f185333..e177f73 100644 --- a/metalsmith/test/test_scheduler.py +++ b/metalsmith/test/test_scheduler.py @@ -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) diff --git a/metalsmith/version.py b/metalsmith/version.py new file mode 100644 index 0000000..7cf84e3 --- /dev/null +++ b/metalsmith/version.py @@ -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.""" diff --git a/tox.ini b/tox.ini index 8b14e4b..2805b51 100644 --- a/tox.ini +++ b/tox.ini @@ -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