Documentation of PM Producer 48/10948/3
authorPatrikBuhr <patrik.buhr@est.tech>
Thu, 6 Apr 2023 11:17:36 +0000 (13:17 +0200)
committerPatrikBuhr <patrik.buhr@est.tech>
Wed, 19 Apr 2023 09:39:37 +0000 (11:39 +0200)
Documentation.

Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
Issue-ID: NONRTRIC-863
Change-Id: Ie67d2aaeca4fef054fea2a08891515c89afe0086

21 files changed:
pmproducer/api/api.json
pmproducer/api/api.yaml
pmproducer/docs/Architecture.png [new file with mode: 0644]
pmproducer/docs/Pictures.pptx [new file with mode: 0644]
pmproducer/docs/_static/logo.png [new file with mode: 0644]
pmproducer/docs/api-docs.rst [new file with mode: 0644]
pmproducer/docs/conf.py [new file with mode: 0644]
pmproducer/docs/conf.yaml [new file with mode: 0644]
pmproducer/docs/dedicatedTopics.png [new file with mode: 0644]
pmproducer/docs/developer-guide.rst [new file with mode: 0644]
pmproducer/docs/favicon.ico [new file with mode: 0644]
pmproducer/docs/images/swagger.png [new file with mode: 0644]
pmproducer/docs/images/yaml_logo.png [new file with mode: 0644]
pmproducer/docs/index.rst [new file with mode: 0644]
pmproducer/docs/overview.rst [new file with mode: 0644]
pmproducer/docs/requirements-docs.txt [new file with mode: 0644]
pmproducer/docs/sharedTopics.png [new file with mode: 0644]
pmproducer/src/main/java/org/oran/pmproducer/SwaggerConfig.java
pmproducer/src/test/java/org/oran/pmproducer/ApplicationTest.java
pmproducer/src/test/java/org/oran/pmproducer/IntegrationWithKafka.java
pmproducer/tox.ini [new file with mode: 0644]

index 95ff8fd..90456e7 100644 (file)
             "name": "Copyright (C) 2023 Nordix Foundation. Licensed under the Apache License.",
             "url": "http://www.apache.org/licenses/LICENSE-2.0"
         },
-        "description": "Reads data from DMaaP and Kafka and posts it further to information consumers",
-        "title": "Generic Dmaap and Kafka Information Producer",
+        "description": "Distributes PM Measuremenet Data to consumers",
+        "title": "PM Measuremenet Data Producer",
         "version": "1.0"
     },
     "tags": [{
index d3437e5..859428a 100644 (file)
@@ -1,8 +1,7 @@
 openapi: 3.0.1
 info:
-  title: Generic Dmaap and Kafka Information Producer
-  description: Reads data from DMaaP and Kafka and posts it further to information
-    consumers
+  title: PM Measuremenet Data Producer
+  description: Distributes PM Measuremenet Data to consumers
   license:
     name: Copyright (C) 2023 Nordix Foundation. Licensed under the Apache License.
     url: http://www.apache.org/licenses/LICENSE-2.0
diff --git a/pmproducer/docs/Architecture.png b/pmproducer/docs/Architecture.png
new file mode 100644 (file)
index 0000000..9c28f98
Binary files /dev/null and b/pmproducer/docs/Architecture.png differ
diff --git a/pmproducer/docs/Pictures.pptx b/pmproducer/docs/Pictures.pptx
new file mode 100644 (file)
index 0000000..070cc77
Binary files /dev/null and b/pmproducer/docs/Pictures.pptx differ
diff --git a/pmproducer/docs/_static/logo.png b/pmproducer/docs/_static/logo.png
new file mode 100644 (file)
index 0000000..c3b6ce5
Binary files /dev/null and b/pmproducer/docs/_static/logo.png differ
diff --git a/pmproducer/docs/api-docs.rst b/pmproducer/docs/api-docs.rst
new file mode 100644 (file)
index 0000000..5157446
--- /dev/null
@@ -0,0 +1,34 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. Copyright (C) 2021 Nordix
+
+.. _api_docs:
+
+.. |swagger-icon| image:: ./images/swagger.png
+                  :width: 40px
+
+.. |yaml-icon| image:: ./images/yaml_logo.png
+                  :width: 40px
+
+
+========
+API-Docs
+========
+
+Here we describe the APIs to access the Non-RT PM Producer.
+
+PM Producer
+=============
+
+The PM Producer provides support for delivery of PM measurement data over Kafka.
+
+See `PM Producer API <./pm-producer-api.html>`_ for full details of the API.
+
+The API is also described in Swagger-JSON and YAML:
+
+
+.. csv-table::
+   :header: "API name", "|swagger-icon|", "|yaml-icon|"
+   :widths: 10,5, 5
+
+   "PM Producer API", ":download:`link <../api/api.json>`", ":download:`link <../api/api.yaml>`"
diff --git a/pmproducer/docs/conf.py b/pmproducer/docs/conf.py
new file mode 100644 (file)
index 0000000..ca7bb8f
--- /dev/null
@@ -0,0 +1,48 @@
+#  ============LICENSE_START===============================================
+#  Copyright (C) 2021-2022 Nordix Foundation. 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.
+#  ============LICENSE_END=================================================
+#
+
+from docs_conf.conf import *
+
+#branch configuration
+
+branch = 'latest'
+
+language = 'en'
+
+linkcheck_ignore = [
+    'http://localhost.*',
+    'http://127.0.0.1.*',
+    'https://gerrit.o-ran-sc.org.*',
+    './pm-producer-api.html', #Generated file that doesn't exist at link check.
+]
+
+extensions = ['sphinxcontrib.redoc', 'sphinx.ext.intersphinx',]
+
+redoc = [
+            {
+                'name': 'PM Producer API',
+                'page': 'pm-producer-api',
+                'spec': '../api/api.json',
+            }
+        ]
+
+redoc_uri = 'https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js'
+
+#intershpinx mapping with other projects
+intersphinx_mapping = {}
+
+intersphinx_mapping['nonrtric'] = ('https://docs.o-ran-sc.org/projects/o-ran-sc-nonrtric/en/%s' % branch, None)
diff --git a/pmproducer/docs/conf.yaml b/pmproducer/docs/conf.yaml
new file mode 100644 (file)
index 0000000..8f24fdb
--- /dev/null
@@ -0,0 +1,3 @@
+---
+project_cfg: oran
+project: nonrtric-plt-pmproducer
diff --git a/pmproducer/docs/dedicatedTopics.png b/pmproducer/docs/dedicatedTopics.png
new file mode 100644 (file)
index 0000000..cf4a7b8
Binary files /dev/null and b/pmproducer/docs/dedicatedTopics.png differ
diff --git a/pmproducer/docs/developer-guide.rst b/pmproducer/docs/developer-guide.rst
new file mode 100644 (file)
index 0000000..3226f5e
--- /dev/null
@@ -0,0 +1,30 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2022 Nordix
+
+Developer Guide
+===============
+
+This document provides a quickstart for developers of the Non-RT RIC PM Producer.
+
+Additional developer guides are available on the `O-RAN SC NONRTRIC Developer wiki <https://wiki.o-ran-sc.org/display/RICNR/Release+E>`_.
+
+PM Producer Service
+---------------------
+
+The following properties in the application.yaml file have to be modified:
+* server.ssl.key-store=./config/keystore.jks
+* app.webclient.trust-store=./config/truststore.jks
+* app.configuration-filepath=./src/test/resources/test_application_configuration.json
+
+Kubernetes deployment
+=====================
+
+Non-RT RIC can be also deployed in a Kubernetes cluster, `it/dep repository <https://gerrit.o-ran-sc.org/r/admin/repos/it/dep>`_.
+hosts deployment and integration artifacts. Instructions and helm charts to deploy the Non-RT-RIC functions in the
+OSC NONRTRIC integrated test environment can be found in the *./nonrtric* directory.
+
+For more information on installation of NonRT-RIC in Kubernetes, see `Deploy NONRTRIC in Kubernetes <https://wiki.o-ran-sc.org/display/RICNR/Deploy+NONRTRIC+in+Kubernetes>`_.
+
+For more information see `Integration and Testing documentation on the O-RAN-SC wiki <https://docs.o-ran-sc.org/projects/o-ran-sc-it-dep/en/latest/index.html>`_.
+
diff --git a/pmproducer/docs/favicon.ico b/pmproducer/docs/favicon.ico
new file mode 100644 (file)
index 0000000..00b0fd0
Binary files /dev/null and b/pmproducer/docs/favicon.ico differ
diff --git a/pmproducer/docs/images/swagger.png b/pmproducer/docs/images/swagger.png
new file mode 100644 (file)
index 0000000..f5a9e0c
Binary files /dev/null and b/pmproducer/docs/images/swagger.png differ
diff --git a/pmproducer/docs/images/yaml_logo.png b/pmproducer/docs/images/yaml_logo.png
new file mode 100644 (file)
index 0000000..0492eb4
Binary files /dev/null and b/pmproducer/docs/images/yaml_logo.png differ
diff --git a/pmproducer/docs/index.rst b/pmproducer/docs/index.rst
new file mode 100644 (file)
index 0000000..a0ef6fc
--- /dev/null
@@ -0,0 +1,14 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2023 Nordix
+
+Non-RT RIC PM Producer
+======================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+   ./overview.rst
+   ./developer-guide.rst
+   ./api-docs.rst
diff --git a/pmproducer/docs/overview.rst b/pmproducer/docs/overview.rst
new file mode 100644 (file)
index 0000000..524f3e4
--- /dev/null
@@ -0,0 +1,369 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2023 Nordix
+
+
+PM Producer
+~~~~~~~~~~~~~
+
+************
+Introduction
+************
+
+The task of the PM Producer is to process PM reports and to distribute requested information to subscribers.
+The main use case is:
+
+* The PM Producer receives a Json object from Kafka which notifies that a new PM report is fetched and is available to processed.
+
+* The actual PM report is in a file, which is stored in an S3 Object store bucket or in the file system (in a mounted volume). The file has the same structure as 3GPP TS 32.432/3GPP TS 32.435, but is converted to json and is extended to contain the information that is encoded the 3GPP measurement report xml file name.
+
+* The PM Producer loads the file and distribute the contents to the subscribers over Kafka according to their subscription parameters. These subscription parameters defines wanted measurement types from given parts of of the network.
+
+The PM Producer registers itself as an information producer of PM measurement data in Information Coordination Service (ICS).
+
+A data consumer can create an information job (data subscription) using the ICS consumer API (for rApps) or the A1-EI (Enrichment Information) API (for NearRT-RICs).
+The PM Producer will get notified when information jobs of type 'PM measurements' are created.
+
+The service is implemented in Java Spring Boot.
+
+.. image:: ./Architecture.png
+   :width: 500pt
+
+This product is a part of :doc:`NONRTRIC <nonrtric:index>`.
+
+**************
+Delivered data
+**************
+When a data consumer (e.g an rApp) creates an Information Job, a Kafka Topic is given as output for the job.
+After filtering, the data will be delivered to the output topic.
+
+The format of the delivered PM measurement is the same as the input format (which in turn is a Json mapping done from
+3GPP TS 32.432/3GPP TS 32.435).
+
+The result of the PM filtering preserves the structure of a 3GPP PM report.
+Here follows an example of a resulting delivered PM report.
+
+.. code-block:: javascript
+
+   {
+      "event":{
+         "commonEventHeader":{
+            "domain":"perf3gpp",
+            "eventId":"9efa1210-f285-455f-9c6a-3a659b1f1882",
+            "eventName":"perf3gpp_gnb-Ericsson_pmMeasResult",
+            "sourceName":"O-DU-1122",
+            "reportingEntityName":"",
+            "startEpochMicrosec":951912000000,
+            "lastEpochMicrosec":951912900000,
+            "timeZoneOffset":"+00:00"
+         },
+         "perf3gppFields":{
+            "perf3gppFieldsVersion":"1.0",
+            "measDataCollection":{
+               "granularityPeriod":900,
+               "measuredEntityUserName":"RNC Telecomville",
+               "measuredEntityDn":"SubNetwork=CountryNN,MeContext=MEC-Gbg-1,ManagedElement=RNC-Gbg-1",
+               "measuredEntitySoftwareVersion":"",
+               "measInfoList":[
+                  {
+                     "measInfoId":{
+                        "sMeasInfoId":""
+                     },
+                     "measTypes":{
+                        "map":{
+                           "succImmediateAssignProcs":1
+                        },
+                        "sMeasTypesList":[
+                           "succImmediateAssignProcs"
+                        ]
+                     },
+                     "measValuesList":[
+                        {
+                           "measObjInstId":"RncFunction=RF-1,UtranCell=Gbg-997",
+                           "suspectFlag":"false",
+                           "measResults":[
+                              {
+                                 "p":1,
+                                 "sValue":"1113"
+                              }
+                           ]
+                        },
+                        {
+                           "measObjInstId":"RncFunction=RF-1,UtranCell=Gbg-998",
+                           "suspectFlag":"false",
+                           "measResults":[
+                              {
+                                 "p":1,
+                                 "sValue":"234"
+                              }
+                           ]
+                        },
+                        {
+                           "measObjInstId":"RncFunction=RF-1,UtranCell=Gbg-999",
+                           "suspectFlag":"true",
+                           "measResults":[
+                              {
+                                 "p":1,
+                                 "sValue":"789"
+                              }
+                           ]
+                        }
+                     ]
+                  }
+               ]
+            }
+         }
+      }
+   }
+
+==================
+Sent Kafka headers
+==================
+
+For each filtered result sent to a Kafka topic, there will the following proerties in the Kafa header:
+
+* type-id, this propery is used to indicate the ID of the information type. The value is a string.
+* gzip, if this property exists the object is gzipped (otherwise not). The property has no value.
+* source-name, the name of the source traffical element for the measurements.
+
+
+******************
+Configuration File
+******************
+
+The configuration file defines Kafka topics that should be listened to and registered as subscribeable information types.
+There is an example configuration file in config/application_configuration.json
+
+Each entry will be registered as a subscribe information type in ICS. The following attributes can be used in each entry:
+
+* id, the information type identifier.
+
+* kafkaInputTopic, a Kafka topic to listen to for new file events.
+
+* inputJobType, the information type for new file events subscription.
+
+* inputJobDefinition, the parameters for the new file events subscription.
+
+The last two parameters are used to create the subscription for the input to this component (subscription of file ready events).
+
+
+Below follows an example of a configuration file.
+
+.. code-block:: javascript
+
+ {
+   "types": [
+      {
+         "id": "PmDataOverKafka",
+         "kafkaInputTopic": "FileReadyEvent",
+         "inputJobType": "xml-file-data-to-filestore",
+         "inputJobDefinition": {
+            "kafkaOutputTopic": "FileReadyEvent",
+            "filestore-output-bucket": "pm-files-json",
+            "filterType": "pmdata",
+            "filter": {
+               "inputCompression": "xml.gz",
+               "outputCompression": "none"
+            }
+         }
+      }
+   ]
+ }
+
+**************************
+Information Job Parameters
+**************************
+
+The schema for the parameters for PM measurements subscription is defined in file src/main/resources/typeSchemaPmData.json.
+
+=====================
+typeSchemaPmData.json
+=====================
+
+The type specific json schema for the subscription of PM measurement:
+
+.. code-block:: javascript
+
+   {
+   "$schema": "http://json-schema.org/draft-04/schema#",
+   "type": "object",
+   "additionalProperties": false,
+   "properties": {
+      "filter": {
+         "type": "object",
+         "additionalProperties": false,
+         "properties": {
+            "sourceNames": {
+               "type": "array",
+               "items": [
+                  {
+                     "type": "string"
+                  }
+               ]
+            },
+            "measObjInstIds": {
+               "type": "array",
+               "items": [
+                  {
+                     "type": "string"
+                  }
+               ]
+            },
+            "measTypeSpecs": {
+               "type": "array",
+               "items": [
+                  {
+                     "type": "object",
+                     "properties": {
+                        "measuredObjClass": {
+                           "type": "string"
+                        },
+                        "measTypes": {
+                           "type": "array",
+                           "items": [
+                              {
+                                 "type": "string"
+                              }
+                           ]
+                        }
+                     },
+                     "required": [
+                        "measuredObjClass"
+                     ]
+                  }
+               ]
+            },
+            "measuredEntityDns": {
+               "type": "array",
+               "items": [
+                  {
+                     "type": "string"
+                  }
+               ]
+            },
+            "pmRopStartTime": {
+               "type": "string"
+            },
+            "pmRopEndTime": {
+               "type": "string"
+            }
+         }
+      },
+      "deliveryInfo": {
+         "type": "object",
+         "additionalProperties": false,
+         "properties": {
+            "topic": {
+               "type": "string"
+            },
+            "bootStrapServers": {
+               "type": "string"
+            }
+         },
+         "required": [
+            "topic"
+         ]
+      }
+   },
+   "required": [
+      "filter", "deliveryInfo"
+   ]
+   }
+
+
+The following properties are defined:
+
+* filter, the value of the filter expression. This selects which data to subscribe for. All fields are optional and excluding a field means that everything is selected.
+
+   * sourceNames, section of the names of the reporting traffical nodes
+   * measObjInstIds, selection of the measured resources. This is the Relative Distingusished Name of the MO that
+     has the counter.
+     If a given value is contained in the filter definition, it will match (partial matching).
+     For instance a value like "NRCellCU" will match "ManagedElement=seliitdus00487,GNBCUCPFunction=1,NRCellCU=32".
+   * measTypeSpecs, selection of measurement types (counters). This consists of:
+
+      * measuredObjClass, the name of the class of the measured resources.
+      * measTypes, the name of the measurement type (counter). The measurement type name is only
+        unique in the scope of an MO class (measured resource).
+
+   * measuredEntityDns, selection of DNs for the traffical elements.
+
+   * pmRopStartTime, if this parameter is specified already collected PM measurements files will be scanned to retrieve historical data.
+     The start file is the time from when the information shall be returned.
+     In this case, the query is only done for files from the given "sourceNames".
+     If this parameter is excluded, only "new" reports will be delivered as they are collected from the traffical nodes.
+
+   * pmRopEndTime, for querying already collected PM measurements. Only relevant if pmRopStartTime.
+     If this parameters is given, no reports will be sent as new files are collected.
+
+* deliveryInfo, defines where the subscribed PM measurements shall be sent.
+
+  * topic, the name of the kafka topic
+  * bootStrapServers, reference to the kafka bus to used. This is optional, if this is omitted the default configured kafka bus is used (which is configured in the application.yaml file).
+
+
+
+Below follows examples of some filters.
+
+.. code-block:: javascript
+
+    {
+      "filter":{
+        "sourceNames":[
+           "O-DU-1122"
+        ],
+        "measObjInstIds":[
+           "UtranCell=Gbg-997"
+        ],
+        "measTypeSpecs":[
+           {
+              "measuredObjClass":"UtranCell",
+              "measTypes":[
+                 "succImmediateAssignProcs"
+              ]
+            {
+        ]
+      }
+   }
+
+Here follows an example of a filter that will
+match two counters from all cells in two traffical nodes.
+
+.. code-block:: javascript
+
+    {
+      "filterType":"pmdata",
+      "filter": {
+        "sourceNames":[
+           "O-DU-1122", "O-DU-1123"
+        ],
+        "measTypeSpecs":[
+             {
+                "measuredObjClass":"NRCellCU",
+                "measTypes":[
+                   "pmCounterNumber0", "pmCounterNumber1"
+                ]
+             }
+          ],
+
+      }
+    }
+
+
+****************************
+PM measurements subscription
+****************************
+
+The sequence is that a "new file event" is received (from a Kafka topic).
+The file is read from local storage (file storage or S3 object store). For each Job, the specified PM filter is applied to the data
+and the result is sent to the Kafka topic specified by the Job (by the data consumer).
+
+.. image:: ./dedicatedTopics.png
+   :width: 500pt
+
+If several jobs publish to the same Kafka topic (shared topic), the resulting filtered output will be an aggregate of all matching filters.
+So, each consumer will then get more data than requested.
+
+.. image:: ./sharedTopics.png
+   :width: 500pt
+
diff --git a/pmproducer/docs/requirements-docs.txt b/pmproducer/docs/requirements-docs.txt
new file mode 100644 (file)
index 0000000..692a79f
--- /dev/null
@@ -0,0 +1,12 @@
+tox
+Sphinx
+doc8
+docutils
+setuptools
+six
+sphinx_rtd_theme
+sphinxcontrib-needs
+sphinxcontrib-swaggerdoc
+sphinx_bootstrap_theme
+sphinxcontrib-redoc
+lfdocs-conf
\ No newline at end of file
diff --git a/pmproducer/docs/sharedTopics.png b/pmproducer/docs/sharedTopics.png
new file mode 100644 (file)
index 0000000..99324be
Binary files /dev/null and b/pmproducer/docs/sharedTopics.png differ
index e131eed..009433b 100644 (file)
@@ -38,6 +38,6 @@ import io.swagger.v3.oas.annotations.info.License;
 public class SwaggerConfig {
     private SwaggerConfig() {}
 
-    static final String API_TITLE = "Generic Dmaap and Kafka Information Producer";
-    static final String DESCRIPTION = "Reads data from DMaaP and Kafka and posts it further to information consumers";
+    static final String API_TITLE = "PM Measuremenet Data Producer";
+    static final String DESCRIPTION = "Distributes PM Measuremenet Data to consumers";
 }
index 04e8eed..76ccb48 100644 (file)
@@ -106,7 +106,7 @@ import reactor.test.StepVerifier;
         "app.webclient.trust-store=./config/truststore.jks", //
         "app.webclient.trust-store-used=true", //
         "app.configuration-filepath=./src/test/resources/test_application_configuration.json", //
-        "app.pm-files-path=/tmp/dmaapadaptor", //
+        "app.pm-files-path=/tmp/pmproducer", //
         "app.s3.endpointOverride=" //
 })
 class ApplicationTest {
index 990e7b2..170f555 100644 (file)
@@ -86,8 +86,8 @@ import reactor.kafka.sender.SenderRecord;
         "app.configuration-filepath=./src/test/resources/test_application_configuration.json", //
         "app.pm-files-path=./src/test/resources/", //
         "app.s3.locksBucket=ropfilelocks", //
-        "app.pm-files-path=/tmp/dmaapadaptor", //
-        "app.s3.bucket=dmaaptest", //
+        "app.pm-files-path=/tmp/pmproducer", //
+        "app.s3.bucket=pmproducertest", //
         "app.auth-token-file=src/test/resources/jwtToken.b64", //
         "app.kafka.use-oath-token=false"}) //
 class IntegrationWithKafka {
diff --git a/pmproducer/tox.ini b/pmproducer/tox.ini
new file mode 100644 (file)
index 0000000..2705e16
--- /dev/null
@@ -0,0 +1,37 @@
+# ==================================================================================
+#       Copyright (c) 2020 Nordix
+#
+#   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.
+# ==================================================================================
+
+# documentation only
+[tox]
+minversion = 2.0
+envlist =
+    docs,
+    docs-linkcheck,
+skipsdist = true
+
+[testenv:docs]
+basepython = python3
+deps = -r{toxinidir}/docs/requirements-docs.txt
+
+commands =
+    sphinx-build -W -b html -n -d {envtmpdir}/docs/doctrees ./docs/ {toxinidir}/docs/_build/html
+    echo "Generated docs available in {toxinidir}/docs/_build/html"
+whitelist_externals = echo
+
+[testenv:docs-linkcheck]
+basepython = python3
+deps = -r{toxinidir}/docs/requirements-docs.txt
+commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck