Build Common Framework for Feature Classification Matrix

The feature classification matrix will provide information about
plugins and the features they support. Acts as a launching point for
users to ready to deploy their cloud. Users can use the matrix to find
features and plugins that meet their
needs.

Pulling out the framework from Nova's implementation here:
http://docs.openstack.org/developer/nova/support-matrix.html

Neutron and Nova are currently working on implementation.

Putting the extension in its own module will give all projects a common
framework to use.

Co-Authored-By: <ankur.gupta@intel.com>
Change-Id: Icf4975b1dafefc9ba9f063bd8f9c6c54a36c1e13
This commit is contained in:
Mike Perez 2017-06-08 17:20:02 -07:00
parent 35e2190d1c
commit 1263efe416
10 changed files with 620 additions and 4 deletions

View File

@ -52,6 +52,8 @@ pygments_style = 'sphinx'
# -- Options for HTML output --------------------------------------------------
html_theme = 'openstackdocs'
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
# html_theme_path = ["."]

View File

@ -13,6 +13,7 @@ Contents:
readme
installation
usage
contributing
Indices and tables

81
doc/source/usage.rst Normal file
View File

@ -0,0 +1,81 @@
========
Usage
========
Sphinx Configuration
====================
To use the extension, add ``'sphinx-feature-classification'`` to the
``extensions`` list in the ``conf.py`` file in your Sphinx project.
Documenting Your Drivers
========================
1. This extension uses an ini file to render your driver matrix in Sphinx. You
can begin by creating the file support-matrix.ini file in your sphinx's
source directory.
2. In the INI file, create driver sections that are prefixed with driver-. The
section has various options that can be specified.
+------------+-----------+---------------------------------------+
| Field Name | Mandatory | Description |
+============+===========+=======================================+
| title | **Yes** | Friendly name of the driver. |
+------------+-----------+---------------------------------------+
| link | No | A link to documentation of the driver.|
+------------+-----------+---------------------------------------+
.. code-block:: INI
[driver.slow-driver]
title=Slow Driver
link=https://docs.openstack.org/foo/latest/some-slow-driver-doc
[driver.fast-driver]
title=Fast Driver
link=https://docs.openstack.org/foo/latest/some-fast-driver-doc
3. Next we'll create a feature section to show which drivers support it.
.. code-block:: INI
[operation.attach-volume]
title=Attach block volume to instance
status=optional
notes=The attach volume operation provides a means to hotplug
additional block storage to a running instance.
cli=nova volume-attach <server> <volume>
driver-slow-driver=complete
driver-fast-driver=complete
The 'status' field takes possible values
+---------------+------------------------------------------------------+
| Status | Description |
+===============+======================================================+
| mandatory | Unconditionally required to be implemented. |
+---------------+------------------------------------------------------+
| optional | Optional to support, nice to have. |
+---------------+------------------------------------------------------+
| choice(group) | At least one of the options within the named group |
| | must be implemented. |
+---------------+------------------------------------------------------+
| condition | Required, if the referenced condition is met. |
+---------------+------------------------------------------------------+
The value against each 'driver-XXXX' entry refers to the level
of the implementation of the feature in that driver
+---------------+------------------------------------------------------+
| Status | Description |
+===============+======================================================+
| complete | Fully implemented, expected to work at all times. |
+---------------+------------------------------------------------------+
| partial | Implemented, but with caveats about when it will |
| | work eg some configurations or hardware or guest OS |
| | may not support it. |
+---------------+------------------------------------------------------+
| missing | Not implemented at all. |
+---------------+------------------------------------------------------+

View File

@ -2,4 +2,5 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
docutils>=0.11 # OSI-Approved Open Source, Public Domain
pbr>=2.0 # Apache-2.0

View File

@ -0,0 +1,33 @@
.sp_feature_required {
font-weight: bold;
}
.sp_impl_complete {
color: rgb(0, 120, 0);
font-weight: normal;
}
.sp_impl_missing {
color: rgb(120, 0, 0);
font-weight: normal;
}
.sp_impl_partial {
color: rgb(170, 170, 0);
font-weight: normal;
}
.sp_impl_unknown {
color: rgb(170, 170, 170);
font-weight: normal;
}
.sp_impl_summary {
font-size: 2em;
}
.sp_cli {
font-family: monospace;
background-color: #F5F5F5;
}

View File

@ -0,0 +1,452 @@
# 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.
"""
This provides a sphinx extension able to render the support-matrix.ini
file into the developer documentation.
It is used via a single directive in the .rst file
.. support_matrix::
"""
import re
from docutils import nodes
from docutils.parsers import rst
from six.moves import configparser
KEY_PATTERN = re.compile("[^a-zA-Z0-9_]")
DRIVER_PREFIX = "driver."
FEATURE_PREFIX = 'operation.'
DRIVER_NOTES_PREFIX = "driver-notes."
class Matrix(object):
"""Represents the entire support matrix for project drivers"""
def __init__(self, cfg):
self.drivers = self._set_drivers(cfg)
self.features = self._set_features(cfg)
@staticmethod
def _set_drivers(cfg):
drivers = {}
for section in cfg.sections():
if not section.startswith(DRIVER_PREFIX):
continue
title = cfg.get(section, "title")
link = None
if cfg.has_option(section, 'link'):
link = cfg.get(section, "link")
driver = Driver(title, link)
drivers[section] = driver
return drivers
def _set_features(self, cfg):
features = []
def _process_feature(section):
if not cfg.has_option(section, "title"):
raise Exception(
"'title' option missing in '[%s]' section" % section)
title = cfg.get(section, "title")
status = Feature.STATUS_OPTIONAL
group = None
if cfg.has_option(section, "status"):
# The value is a string "status(group)" where
# the 'group' part is optional
status, group = re.match('^([^(]+)(?:\(([^)]+)\))?$',
cfg.get(section, "status")).groups()
if status not in Feature.STATUS_ALL:
raise ValueError(
"'status' option value '%s' in ['%s']"
"section must be one of (%s)" %
(status, section,
", ".join(Feature.STATUS_ALL)))
cli = []
if cfg.has_option(section, "cli"):
cli = cfg.get(section, "cli")
notes = None
if cfg.has_option(section, "notes"):
notes = cfg.get(section, "notes")
return Feature(section, title, status, group, notes, cli)
def _process_implementation(section, option, feature):
if option not in self.drivers:
raise Exception(
"'%s' section is not declared in the "
"INI file." % (option))
status = cfg.get(section, option)
if status not in Implementation.STATUS_ALL:
raise ValueError(
"%s is set to %s in '[%s]' section but must be "
"one of (%s)" % (option, status, section, ", ".join(
Implementation.STATUS_ALL)))
impl = Implementation(status)
feature.implementations[option] = impl
return feature
for section in cfg.sections():
if not section.startswith(FEATURE_PREFIX):
continue
feature = _process_feature(section)
# Now we've got the basic feature details, we must process
# the backend driver implementation for each feature
for option in cfg.options(section):
if not option.startswith(DRIVER_PREFIX):
continue
implementation = _process_implementation(section, option,
feature)
features.append(implementation)
return features
class Feature(object):
STATUS_CHOICE = "choice"
STATUS_CONDITION = "condition"
STATUS_REQUIRED = "required"
STATUS_OPTIONAL = "optional"
STATUS_MATURE = "mature"
STATUS_IMMATURE = "immature"
STATUS_ALL = [STATUS_REQUIRED, STATUS_OPTIONAL, STATUS_CHOICE,
STATUS_CONDITION, STATUS_MATURE, STATUS_IMMATURE]
def __init__(self, key, title, status=STATUS_OPTIONAL,
group=None, notes=None, cli=(), api=None):
self.key = key
self.title = title
self.status = status
self.group = group
self.notes = notes
self.cli = cli
self.api = api
self.implementations = {}
class Implementation(object):
STATUS_COMPLETE = "complete"
STATUS_PARTIAL = "partial"
STATUS_INCOMPLETE = "incomplete"
STATUS_UNKNOWN = "unknown"
STATUS_ALL = [STATUS_COMPLETE, STATUS_INCOMPLETE,
STATUS_PARTIAL, STATUS_UNKNOWN]
def __init__(self, status=STATUS_INCOMPLETE):
self.status = status
STATUS_SYMBOLS = {
Implementation.STATUS_COMPLETE: u"\u2714",
Implementation.STATUS_INCOMPLETE: u"\u2716",
Implementation.STATUS_PARTIAL: u"\u2714",
Implementation.STATUS_UNKNOWN: u"?"
}
class Driver(object):
def __init__(self, title, link=None):
"""Driver object.
:param title: Human readable name for plugin
:param link: A URL to documentation about the driver.
"""
self.title = title
self.link = link
class Directive(rst.Directive):
# support-matrix.ini is the arg
required_arguments = 1
def run(self):
matrix = self._load_support_matrix()
return self._build_markup(matrix)
def _load_support_matrix(self):
"""Parse support-matrix.ini file.
Reads the support-matrix.ini file and populates an instance of the
Matrix class with all the data.
:returns: Matrix instance
"""
cfg = configparser.ConfigParser()
env = self.state.document.settings.env
fname = self.arguments[0]
rel_fpath, fpath = env.relfn2path(fname)
with open(fpath) as fp:
cfg.readfp(fp)
# This ensures that the docs are rebuilt whenever the
# .ini file changes
env.note_dependency(rel_fpath)
matrix = Matrix(cfg)
return matrix
def _build_markup(self, matrix):
"""Constructs the docutils content for the support matrix."""
content = []
self._build_summary(matrix, content)
self._build_details(matrix, content)
self._build_notes(content)
return content
@staticmethod
def _build_summary(matrix, content):
"""Constructs the content for the summary of the support matrix.
The summary consists of a giant table, with one row
for each feature, and a column for each backend
driver. It provides an 'at a glance' summary of the
status of each driver.
"""
summary_title = nodes.subtitle(text="Summary")
summary = nodes.table()
cols = len(matrix.drivers.keys())
# Add two columns for the Feature and Status columns.
cols += 2
summary_group = nodes.tgroup(cols=cols)
summary_body = nodes.tbody()
summary_head = nodes.thead()
for i in range(cols):
summary_group.append(nodes.colspec(colwidth=1))
summary_group.append(summary_head)
summary_group.append(summary_body)
summary.append(summary_group)
content.append(summary_title)
content.append(summary)
# This sets up all the column headers - two fixed
# columns for feature name & status
header = nodes.row()
blank = nodes.entry()
blank.append(nodes.emphasis(text="Feature"))
header.append(blank)
blank = nodes.entry()
blank.append(nodes.emphasis(text="Status"))
header.append(blank)
summary_head.append(header)
# then one column for each backend driver
impls = matrix.drivers.keys()
impls.sort()
for key in impls:
driver = matrix.drivers[key]
implcol = nodes.entry()
header.append(implcol)
implcol.append(nodes.strong(text=driver.title))
# We now produce the body of the table, one row for
# each feature to report on
for feature in matrix.features:
item = nodes.row()
# the hyperlink driver name linking to details
feature_id = re.sub(KEY_PATTERN, "_", feature.key)
# first the fixed columns for title/status
key_col = nodes.entry()
item.append(key_col)
key_ref = nodes.reference(refid=feature_id)
key_txt = nodes.inline()
key_col.append(key_txt)
key_txt.append(key_ref)
key_ref.append(nodes.strong(text=feature.title))
status_col = nodes.entry()
item.append(status_col)
status_col.append(nodes.inline(
text=feature.status,
classes=["sp_feature_" + feature.status]))
# and then one column for each backend driver
impls = matrix.drivers.keys()
impls.sort()
for key in impls:
impl = feature.implementations[key]
impl_col = nodes.entry()
item.append(impl_col)
key_id = re.sub(KEY_PATTERN, "_",
"{}_{}".format(feature.key, key))
impl_ref = nodes.reference(refid=key_id)
impl_txt = nodes.inline()
impl_col.append(impl_txt)
impl_txt.append(impl_ref)
status = STATUS_SYMBOLS.get(impl.status, "")
impl_ref.append(nodes.literal(
text=status,
classes=["sp_impl_summary", "sp_impl_" + impl.status]))
summary_body.append(item)
def _build_details(self, matrix, content):
"""Constructs the content for the details of the support matrix."""
details_title = nodes.subtitle(text="Details")
details = nodes.bullet_list()
content.append(details_title)
content.append(details)
# One list entry for each feature we're reporting on
for feature in matrix.features:
item = nodes.list_item()
status = feature.status
if feature.group is not None:
status += "({})".format(feature.group)
feature_id = re.sub(KEY_PATTERN, "_", feature.key)
# Highlight the feature title name
item.append(nodes.strong(text=feature.title, ids=[feature_id]))
# Add maturity status
para = nodes.paragraph()
para.append(nodes.strong(text="Status: {}. ".format(status)))
item.append(para)
if feature.api is not None:
para = nodes.paragraph()
para.append(
nodes.strong(text="API Alias: {} ".format(feature.api)))
item.append(para)
if feature.cli:
item.append(self._create_cli_paragraph(feature))
if feature.notes is not None:
item.append(self._create_notes_paragraph(feature.notes))
para_divers = nodes.paragraph()
para_divers.append(nodes.strong(text="Driver Support:"))
# A sub-list giving details of each backend driver
impls = nodes.bullet_list()
for key in feature.implementations:
driver = matrix.drivers[key]
impl = feature.implementations[key]
subitem = nodes.list_item()
key_id = re.sub(KEY_PATTERN, "_",
"{}_{}".format(feature.key, key))
subitem += [
nodes.strong(text="{}: ".format(driver.title)),
nodes.literal(text=impl.status,
classes=["sp_impl_{}".format(impl.status)],
ids=[key_id]),
]
impls.append(subitem)
para_divers.append(impls)
item.append(para_divers)
details.append(item)
@staticmethod
def _build_notes(content):
"""Constructs a list of notes content for the support matrix.
This is generated as a bullet list.
"""
notes_title = nodes.subtitle(text="Notes:")
notes = nodes.bullet_list()
content.append(notes_title)
content.append(notes)
for note in ["This document is a continuous work in progress"]:
item = nodes.list_item()
item.append(nodes.strong(text=note))
notes.append(item)
@staticmethod
def _create_cli_paragraph(feature):
"""Create a paragraph which represents the CLI commands of the feature
The paragraph will have a bullet list of CLI commands.
"""
para = nodes.paragraph()
para.append(nodes.strong(text="CLI commands:"))
commands = nodes.bullet_list()
for c in feature.cli.split(";"):
cli_command = nodes.list_item()
cli_command += nodes.literal(text=c, classes=["sp_cli"])
commands.append(cli_command)
para.append(commands)
return para
@staticmethod
def _create_notes_paragraph(notes):
"""Constructs a paragraph which represents the implementation notes
The paragraph consists of text and clickable URL nodes if links were
given in the notes.
"""
para = nodes.paragraph()
para.append(nodes.strong(text="Notes: "))
# links could start with http:// or https://
link_idxs = [m.start() for m in re.finditer('https?://', notes)]
start_idx = 0
for link_idx in link_idxs:
# assume the notes start with text (could be empty)
para.append(nodes.inline(text=notes[start_idx:link_idx]))
# create a URL node until the next text or the end of the notes
link_end_idx = notes.find(" ", link_idx)
if link_end_idx == -1:
# In case the notes end with a link without a blank
link_end_idx = len(notes)
uri = notes[link_idx:link_end_idx + 1]
para.append(nodes.reference("", uri, refuri=uri))
start_idx = link_end_idx + 1
# get all text after the last link (could be empty) or all of the
# text if no link was given
para.append(nodes.inline(text=notes[start_idx:]))
return para
def setup(app):
app.add_directive('support_matrix', Directive)
app.add_stylesheet('support-matrix.css')

View File

@ -0,0 +1,14 @@
[driver.foo]
title=Foo Driver
link=https://docs.openstack.org
[driver.bar]
title=Bar Driver
link=https://docs.openstack.org
[operation.Cool_Feature]
title=Cool Feature
status=optional
notes=A pretty darn cool feature.
driver.foo=complete
driver.bar=partial

View File

@ -18,11 +18,43 @@ test_sphinx_feature_classification
Tests for `sphinx_feature_classification` module.
"""
import os
from sphinx_feature_classification import support_matrix
from sphinx_feature_classification.tests import base
import ddt
from six.moves import configparser
class TestSphinx_feature_classification(base.TestCase):
def test_something(self):
pass
@ddt.ddt
class MatrixTestCase(base.TestCase):
def setUp(self):
super(MatrixTestCase, self).setUp()
cfg = configparser.ConfigParser()
directory = os.path.dirname(os.path.abspath(__file__))
config_file = os.path.join(directory, 'fakes', 'support-matrix.ini')
with open(config_file) as fp:
cfg.readfp(fp)
self.matrix = support_matrix.Matrix(cfg)
def test_features_set(self):
fake_feature = self.matrix.features[0]
self.assertEqual('Cool Feature', fake_feature.title)
self.assertEqual('optional', fake_feature.status)
self.assertEqual('A pretty darn cool feature.',
fake_feature.notes)
@ddt.unpack
@ddt.data({'key': 'driver.foo', 'title': 'Foo Driver',
'link': 'https://docs.openstack.org'},
{'key': 'driver.bar', 'title': 'Bar Driver',
'link': 'https://docs.openstack.org'})
def test_drivers_set(self, key, title, link):
fake_driver = self.matrix.drivers[key]
self.assertEqual(title, fake_driver.title)
self.assertEqual(link, fake_driver.link)

View File

@ -3,10 +3,10 @@
# process, which may cause wedges in the gate later.
hacking>=0.12.0,<0.13 # Apache-2.0
coverage>=4.0,!=4.4 # Apache-2.0
openstackdocstheme>=1.17.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
ddt>=1.0.1 # MIT
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx!=1.6.1,>=1.5.1 # BSD
testrepository>=0.0.18 # Apache-2.0/BSD