From f53790627e230e0095d5fc4376535e50db111b2f Mon Sep 17 00:00:00 2001 From: Ankur Gupta Date: Wed, 15 Jun 2016 10:45:31 -0500 Subject: [PATCH] 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 in Oslosphinx will give all projects a common framework to use. Change-Id: Ide7769bce59d264854f50f69b1d581513c67b5d5 --- doc/source/index.rst | 1 + doc/source/support_matrix.rst | 29 ++ oslosphinx/support_matrix.py | 482 ++++++++++++++++++ .../theme/openstack/static/support_matrix.css | 33 ++ 4 files changed, 545 insertions(+) create mode 100644 doc/source/support_matrix.rst create mode 100644 oslosphinx/support_matrix.py create mode 100644 oslosphinx/theme/openstack/static/support_matrix.css diff --git a/doc/source/index.rst b/doc/source/index.rst index a665e91..453ade1 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -10,6 +10,7 @@ documentation from the OpenStack project. usage check_blueprints + support_matrix contributing Release Notes diff --git a/doc/source/support_matrix.rst b/doc/source/support_matrix.rst new file mode 100644 index 0000000..07662f7 --- /dev/null +++ b/doc/source/support_matrix.rst @@ -0,0 +1,29 @@ +================================================== +Using oslosphinx.support_matrix for Support Matrix +================================================== + +The ``oslosphinx.support_matrix`` extension provides +the framework required to build a project's feature +matrix. This framework will build a matrix similar +to Nova's implementation: +http://docs.openstack.org/developer/nova/support-matrix.html + + +Enabling +======== + +Add ``oslosphinx.support_matrix`` to the ``extensions`` list +in the ``conf.py`` file in your Sphinx project. + + +Building Matrix +=============== + +The Sphinx project will also need the appropriate support_matrix.rst +file to provide the user with enough background of the purpose of the +matrix. + +The matrix itself is build using the support_matrix.ini file which +specifies the hypervisors, backends, or plugins the matrix will describe. + +Examples to come diff --git a/oslosphinx/support_matrix.py b/oslosphinx/support_matrix.py new file mode 100644 index 0000000..039fade --- /dev/null +++ b/oslosphinx/support_matrix.py @@ -0,0 +1,482 @@ +# 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 +source/general_feature_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 + +RE_PATTERN = re.compile("[^a-zA-Z0-9_]") + + +class SupportMatrix(object): + """Represents the entire support matrix for Neutron drivers""" + + def __init__(self): + self.features = [] + self.targets = {} + + +class SupportMatrixFeature(object): + STATUS_IMMATURE = "immature" + STATUS_MATURE = "mature" + STATUS_REQUIRED = "required" + STATUS_DEPRECATED = "deprecated" + + STATUS_ALL = [STATUS_IMMATURE, STATUS_MATURE, + STATUS_REQUIRED, STATUS_DEPRECATED] + + def __init__(self, key, title, status=STATUS_IMMATURE, + 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 SupportMatrixImplementation(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, notes=None): + + self.status = status + self.notes = notes + + +STATUS_DICT = { + SupportMatrixImplementation.STATUS_COMPLETE: u"\u2714", + SupportMatrixImplementation.STATUS_INCOMPLETE: u"\u2716", + SupportMatrixImplementation.STATUS_PARTIAL: u"\u2714", + SupportMatrixImplementation.STATUS_UNKNOWN: u"?" +} + + +class SupportMatrixTarget(object): + def __init__(self, key, title, driver, plugin=None, + architecture=None, api=None): + """Target/Driver object. + + :param key: Unique identifier for plugin + :param title: Human readable name for plugin + :param driver: name of the driver + :param plugin: optional name of plugin + :param architecture: optional name of architecture + """ + self.api = api + self.key = key + self.title = title + self.driver = driver + self.plugin = plugin + self.architecture = architecture + + +class SupportMatrixDirective(rst.Directive): + + # general_feature_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 + SupportMatrix class with all the data. + + :returns: SupportMatrix instance + """ + + cfg = configparser.SafeConfigParser() + 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 = SupportMatrix() + matrix.targets = self._get_targets(cfg) + matrix.features = self._get_features(cfg, matrix.targets) + + return matrix + + def _get_targets(self, cfg): + # The 'targets' section is special - it lists all the + # backend drivers that this file records data for + + targets = {} + network_target = "networking-" + + for item in cfg.options("targets"): + if not item.startswith(network_target): + continue + + # The driver string will optionally contain + # 'networking-*' qualifier + # so we expect between 1 and 3 components + # in the name + key = item[len(network_target):] + title = cfg.get("targets", item) + name = key.split("-") + if len(name) > 3: + raise Exception("'%s' field is malformed in '[%s]' section" % + (item, "DEFAULT")) + else: + target = SupportMatrixTarget(key, title, *name) + + targets[key] = target + + return targets + + def _get_features(self, cfg, targets): + # All sections except 'targets' describe some feature of + # the Neutron backend driver. + + features = [] + + for section in cfg.sections(): + if section == "targets": + continue + if not cfg.has_option(section, "title"): + raise Exception( + "'title' field missing in '[%s]' section" % section) + + title = cfg.get(section, "title") + + status = SupportMatrixFeature.STATUS_IMMATURE + if cfg.has_option(section, "status"): + # The value is a string "status(group)" where + # the 'group' part is optional + status = cfg.get(section, "status") + offset = status.find("(") + group = None + if offset != -1: + group = status[offset + 1:-1] + status = status[0:offset] + + if status not in SupportMatrixFeature.STATUS_ALL: + raise Exception( + "'status' field value '%s' in ['%s']" + "section must be %s" % + (status, section, + ",".join(SupportMatrixFeature.STATUS_ALL))) + + cli = [] + if cfg.has_option(section, "cli"): + cli = cfg.get(section, "cli") + + api = None + if cfg.has_option(section, "api"): + api = cfg.get(section, "api") + + notes = None + if cfg.has_option(section, "notes"): + notes = cfg.get(section, "notes") + feature = SupportMatrixFeature(section, title, status, group, + notes, cli, api) + + # Now we've got the basic feature details, we must process + # the backend driver implementation for each feature + for item in cfg.options(section): + network_target = "networking-" + network_notes = "networking-notes-" + + if not item.startswith(network_target): + continue + + key = item[len(network_target):] + if key not in targets: + raise Exception( + "networking-'%s' in '[%s]' not declared" % + (item, section)) + + status = cfg.get(section, item) + if status not in SupportMatrixImplementation.STATUS_ALL: + raise Exception( + "'%s' value '%s' in '[%s]' section must be %s" % + (item, status, section, + ",".join(SupportMatrixImplementation.STATUS_ALL))) + notes_key = network_notes + item[len(network_notes):] + notes = None + if cfg.has_option(section, notes_key): + notes = cfg.get(section, notes_key) + + target = targets[key] + impl = SupportMatrixImplementation(status, notes) + feature.implementations[target.key] = impl + + for key in targets: + if key not in feature.implementations: + raise Exception("'%s' missing in '[%s]' section" % + (target.key, section)) + + features.append(feature) + + return features + + 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 + + def _build_summary(self, 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.targets.keys()) + 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.targets.keys() + impls.sort() + for key in impls: + target = matrix.targets[key] + implcol = nodes.entry() + header.append(implcol) + implcol.append(nodes.strong(text=target.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 target name linking to details + feature_id = re.sub(RE_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.targets.keys() + impls.sort() + for key in impls: + target = matrix.targets[key] + impl = feature.implementations[key] + impl_col = nodes.entry() + item.append(impl_col) + + key_id = re.sub(RE_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_DICT.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(RE_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 API Alias exists add it + 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 target + impls = nodes.bullet_list() + for key in feature.implementations: + target = matrix.targets[key] + impl = feature.implementations[key] + subitem = nodes.list_item() + + key_id = re.sub(RE_PATTERN, "_", + "{}_{}".format(feature.key, key)) + + subitem += [ + nodes.strong(text="{}: ".format(target.title)), + nodes.literal(text=impl.status, + classes=["sp_impl_{}".format(impl.status)], + ids=[key_id]), + ] + if impl.notes is not None: + subitem.append(self._create_notes_paragraph(impl.notes)) + impls.append(subitem) + + para_divers.append(impls) + item.append(para_divers) + details.append(item) + + def _build_notes(self, 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) + + def _create_cli_paragraph(self, 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 + + def _create_notes_paragraph(self, 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', SupportMatrixDirective) + app.add_stylesheet('support_matrix.css') diff --git a/oslosphinx/theme/openstack/static/support_matrix.css b/oslosphinx/theme/openstack/static/support_matrix.css new file mode 100644 index 0000000..267c342 --- /dev/null +++ b/oslosphinx/theme/openstack/static/support_matrix.css @@ -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; +}