From 882164d1af1df9e51895ef3f6e1476feb78b6bf9 Mon Sep 17 00:00:00 2001 From: Pengfei Ni Date: Tue, 23 May 2017 15:39:02 +0800 Subject: [PATCH] Fix document building Closes-Bug: 1692828 Change-Id: I0090c63da6db325717f717a7e1df95a09e5e4b16 --- {docs => doc}/Makefile | 0 doc/source/_static/support_matrix.css | 33 ++ {docs => doc}/source/conf.py | 4 +- doc/source/ext/support_matrix.py | 482 ++++++++++++++++++ doc/source/index.rst | 15 + .../source/stackube_scope_clarification.rst | 61 ++- docs/source/index.rst | 6 - test-requirements.txt | 2 + 8 files changed, 565 insertions(+), 38 deletions(-) rename {docs => doc}/Makefile (100%) create mode 100644 doc/source/_static/support_matrix.css rename {docs => doc}/source/conf.py (98%) create mode 100644 doc/source/ext/support_matrix.py create mode 100644 doc/source/index.rst rename {docs => doc}/source/stackube_scope_clarification.rst (53%) delete mode 100644 docs/source/index.rst create mode 100644 test-requirements.txt diff --git a/docs/Makefile b/doc/Makefile similarity index 100% rename from docs/Makefile rename to doc/Makefile diff --git a/doc/source/_static/support_matrix.css b/doc/source/_static/support_matrix.css new file mode 100644 index 0000000..267c342 --- /dev/null +++ b/doc/source/_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; +} diff --git a/docs/source/conf.py b/doc/source/conf.py similarity index 98% rename from docs/source/conf.py rename to doc/source/conf.py index bba5c82..bd35470 100644 --- a/docs/source/conf.py +++ b/doc/source/conf.py @@ -30,6 +30,7 @@ import os import subprocess import sys import warnings +import oslosphinx # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -123,7 +124,8 @@ modindex_common_prefix = ['stackube.'] # 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_theme_path = [os.path.join(os.path.dirname(oslosphinx.__file__), 'theme')] +html_theme = 'openstack' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/doc/source/ext/support_matrix.py b/doc/source/ext/support_matrix.py new file mode 100644 index 0000000..28fa141 --- /dev/null +++ b/doc/source/ext/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): + """: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): + """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 docutils 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 docutils 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/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..ef25df5 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,15 @@ +============================================= +Welcome to Stackube developer documentation! +============================================= + +Stackube is a multi-tenant and secure Kubernetes deployment enabled by OpenStack +core components. + +Stackube Scope +============== + +.. toctree:: + :maxdepth: 2 + + stackube_scope_clarification + diff --git a/docs/source/stackube_scope_clarification.rst b/doc/source/stackube_scope_clarification.rst similarity index 53% rename from docs/source/stackube_scope_clarification.rst rename to doc/source/stackube_scope_clarification.rst index 7d7b424..362bd14 100644 --- a/docs/source/stackube_scope_clarification.rst +++ b/doc/source/stackube_scope_clarification.rst @@ -10,13 +10,13 @@ Not another “Kubernetes on OpenStack” project Stackube is a standard upstream Kubernetes deployment with: -1. Mixed container runtime of Docker (Linux container) and HyperContainer (hypervisor-based container) +#. Mixed container runtime of Docker (Linux container) and HyperContainer (hypervisor-based container) -2. Keystone for tenant management +#. Keystone for tenant management -3. Neutron for container network +#. Neutron for container network -4. Cinder for persistent volume +#. Cinder for persistent volume The main difference between Stackube with existing container service project in OpenStack foundation (e.g. Magnum) is: **Stackube works @@ -24,51 +24,51 @@ alongside OpenStack, not on OpenStack**. This means: -1. Only standalone vanilla OpenStack components are required +#. Only standalone vanilla OpenStack components are required -2. Traditional VMs are not required because HyperContainer will provide hypervisor level isolation for containerized workloads. +#. Traditional VMs are not required because HyperContainer will provide hypervisor level isolation for containerized workloads. -3. All the components mentioned above are managed by Kubernetes plugin API. +#. All the components mentioned above are managed by Kubernetes plugin API. What‘s inside Stackube repo? ============================ -1. Keystone RBAC plugin +#. Keystone RBAC plugin -2. Neutron CNI plugin +#. Neutron CNI plugin - a. With a k8s Network object controller + * With a k8s Network object controller -3. Standard k8s upstream Cinder plugin with block device mode +#. Standard k8s upstream Cinder plugin with block device mode -4. Deployment scripts and guide +#. Deployment scripts and guide -5. Other documentations +#. Other documentations Please note: -1. Plugins above will be deployed as system Pod and DaemonSet. +#. Plugins above will be deployed as system Pod and DaemonSet. -2. All other Kubernetes volumes are also supported in Stackube, while k8s Cinder plugin with block device mode can provide better performance in mixed runtime which will be preferred by default. +#. All other Kubernetes volumes are also supported in Stackube, while k8s Cinder plugin with block device mode can provide better performance in mixed runtime which will be preferred by default. What’s the difference between other plugin projects? ==================================================== -1. Kuryr +#. Kuryr - a. This is a Neutron network plugin for Docker network model, which is not directly supported in Kubernetes. Kuryr can provide CNI interface, but Stackube also requires tenant aware network management which is not included in Kuryr. + * This is a Neutron network plugin for Docker network model, which is not directly supported in Kubernetes. Kuryr can provide CNI interface, but Stackube also requires tenant aware network management which is not included in Kuryr. -2. Fuxi +#. Fuxi - a. This is a Cinder volume plugin for Docker volume model, which is not supported in latest CRI based Kubernetes (using k8s volume plugin for now, and soon CSI). Also, Stackube prefers a “block-device to Pod” mode in volume plugin when HyperContainer runtime is enabled, which is not supported in Fuxi. + * This is a Cinder volume plugin for Docker volume model, which is not supported in latest CRI based Kubernetes (using k8s volume plugin for now, and soon CSI). Also, Stackube prefers a “block-device to Pod” mode in volume plugin when HyperContainer runtime is enabled, which is not supported in Fuxi. -3. K8s-cloud-provider +#. K8s-cloud-provider - a. This is a “Kubernetes on OpenStack” integration which requires full functioning OpenStack deployment. + * This is a “Kubernetes on OpenStack” integration which requires full functioning OpenStack deployment. -4. Zun +#. Zun - a. This is a OpenStack API container service, while Stackube exposes well-known Kubernetes API and does not require full OpenStack deployment. + * This is a OpenStack API container service, while Stackube exposes well-known Kubernetes API and does not require full OpenStack deployment. As summary, one distinguishable difference is that plugins in Stackube are designed to enable hard multi-tenancy in Kubernetes as a whole @@ -92,23 +92,22 @@ A typical deployment workflow of Stackube On control nodes: -1. Install standalone Keystone, Neutron, Cinder (ceph rbd) +#. Install standalone Keystone, Neutron, Cinder (ceph rbd) - a. This can be done by any existing tool like devstack, RDO etc + * This can be done by any existing tool like devstack, RDO etc On other nodes: -1. Install Kubernetes +#. Install Kubernetes - a. Including container runtimes, CRI shims, CNI etc - - b. This can be done by any existing tool like kubeadm etc + * Including container runtimes, CRI shims, CNI etc + * This can be done by any existing tool like kubeadm etc Deploy Stackube: -1. *kubectl apply -f stackube.yaml* +#. *kubectl apply -f stackube.yaml* - a. This will deploy all Stackube plugins as Pods and DaemonSets to the cluster + * This will deploy all Stackube plugins as Pods and DaemonSets to the cluster (You can also deploy all these components in a single node) diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 021e113..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -============== -Welcome to Stackube developer documentation! -============== - -Stackube is a multi-tenant and secure Kubernetes deployment enabled by OpenStack -core components. \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..aff56d6 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +sphinx!=1.6.1,>=1.5.1 # BSD +oslosphinx>=4.7.0 # Apache-2.0