Add first version of documentation 60/3260/5
authorelinuxhenrik <henrik.b.andersson@est.tech>
Wed, 8 Apr 2020 09:23:55 +0000 (11:23 +0200)
committerelinuxhenrik <henrik.b.andersson@est.tech>
Tue, 14 Apr 2020 14:47:13 +0000 (16:47 +0200)
Change-Id: I6333e54943e7a7a4a1fe2a36e08e6699fa5b7faf
Issue-ID: NONRTRIC-191
Signed-off-by: elinuxhenrik <henrik.b.andersson@est.tech>
.gitignore
.readthedocs.yaml [new file with mode: 0644]
docs/_static/logo.png [new file with mode: 0644]
docs/conf.py [new file with mode: 0644]
docs/conf.yaml [new file with mode: 0644]
docs/favicon.ico [new file with mode: 0644]
docs/index.rst [new file with mode: 0644]
docs/overview.rst [new file with mode: 0644]
docs/requirements-docs.txt [new file with mode: 0644]
docs/simulator-api.rst [new file with mode: 0644]
tox.ini [new file with mode: 0644]

index 3a4edf6..fdb56b6 100644 (file)
@@ -1 +1,8 @@
+# Documentation
+.idea/
+.tox
+docs/_build/
+.DS_STORE
+
+# IDE
 .project
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644 (file)
index 0000000..3797dc8
--- /dev/null
@@ -0,0 +1,20 @@
+---
+# .readthedocs.yml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+# Required
+version: 2
+
+formats:
+  - htmlzip
+
+build:
+  image: latest
+
+python:
+  version: 3.7
+  install:
+    - requirements: docs/requirements-docs.txt
+
+sphinx:
+  configuration: docs/conf.py
diff --git a/docs/_static/logo.png b/docs/_static/logo.png
new file mode 100644 (file)
index 0000000..c3b6ce5
Binary files /dev/null and b/docs/_static/logo.png differ
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644 (file)
index 0000000..9bde80f
--- /dev/null
@@ -0,0 +1,7 @@
+from docs_conf import *
+
+linkcheck_ignore = [
+    'http://localhost.*',
+    'http://127.0.0.1.*',
+    'https://gerrit.o-ran-sc.org.*'
+]
diff --git a/docs/conf.yaml b/docs/conf.yaml
new file mode 100644 (file)
index 0000000..6576ed2
--- /dev/null
@@ -0,0 +1,3 @@
+---
+project_cfg: oran
+project: sim-a1-interface
diff --git a/docs/favicon.ico b/docs/favicon.ico
new file mode 100644 (file)
index 0000000..00b0fd0
Binary files /dev/null and b/docs/favicon.ico differ
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644 (file)
index 0000000..14fca47
--- /dev/null
@@ -0,0 +1,24 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2020 Nordix
+
+.. |nbsp| unicode:: 0xA0
+   :trim:
+
+.. |nbh| unicode:: 0x2011
+   :trim:
+
+.. _a1-interface-simulator:
+
+======================
+A1 Interface Simulator
+======================
+
+.. toctree::
+   :maxdepth: 2
+   :caption: Contents:
+
+   ./overview.rst
+   ./simulator-api.rst
+
+* :ref:`search`
diff --git a/docs/overview.rst b/docs/overview.rst
new file mode 100644 (file)
index 0000000..196132f
--- /dev/null
@@ -0,0 +1,19 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2020 Nordix
+
+.. |nbsp| unicode:: 0xA0
+   :trim:
+
+.. |nbh| unicode:: 0x2011
+   :trim:
+
+
+A1 Simulator Overview
+=====================
+
+The A1 Simulator terminates the A1 interface and provides a way of testing services without the need for any real
+Near |nbh| RT |nbsp| RICs.
+
+Apart from providing the A1 API, the simulator also provides an administrative API to manage policy types and manipulate
+the simulator, see ":ref:`simulator-api`".
\ No newline at end of file
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
new file mode 100644 (file)
index 0000000..09a0c1c
--- /dev/null
@@ -0,0 +1,5 @@
+sphinx
+sphinx-rtd-theme
+sphinxcontrib-httpdomain
+recommonmark
+lfdocs-conf
diff --git a/docs/simulator-api.rst b/docs/simulator-api.rst
new file mode 100644 (file)
index 0000000..bb23da3
--- /dev/null
@@ -0,0 +1,920 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. SPDX-License-Identifier: CC-BY-4.0
+.. Copyright (C) 2020 Nordix
+
+.. _simulator-api:
+
+=============
+Simulator API
+=============
+
+This document describes the API used to manage policy types and manipulate the simulator.
+
+The simulator supports different versions of the A1 interface. Some functions are common for all version, and some are
+specific for a certain version.
+
+.. contents:: Operations
+   :depth: 4
+   :local:
+
+Common Functions
+================
+
+Health Check
+------------
+
+The status of the simulator.
+
+/
+~~
+
+GET
++++
+
+  Returns the status of the simulator.
+
+   **URL path:**
+    /
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      Simulator is living.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X GET "http://localhost:8085/"
+
+    Result:
+      200
+        Simulator is living (OSC_2.1.0 responds OK)
+
+Supported Interfaces
+--------------------
+
+The simulator can support different versions of the A1 interface. With this API the supported versions can be listed.
+
+/container_interfaces
+~~~~~~~~~~~~~~~~~~~~~
+
+GET
++++
+
+  Returns the status of the simulator. (Not available for A1 Standard 1.1.3)
+
+   **URL path:**
+    /container_interfaces
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      List of supported interfaces.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X GET "http://localhost:8085/container_interfaces"
+
+    Result:
+      200
+        1.1.x-alpha.2 OSC_2.1.0 STD_1.1.3
+
+Counters
+--------
+
+The simulator keeps counts of different things that can be accessed.
+
+/counter
+~~~~~~~~
+
+GET
++++
+
+  Get a counter. Counter-name can be one of the following: 'num_instances', 'num_types' or 'interface'.
+
+   **URL path:**
+    /counter/{counter-name}
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      The counter value for the given counter.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X GET "http://localhost:8085/counter/num_instances"
+
+    Result:
+      200
+        10
+
+Version Specific Functions
+==========================
+
+The methods available to control the simulator depends on the version of the A1 API the simulator is simulating.
+
+OSC_2.1.0
+---------
+
+The available functions for the OSC_2.1.0 version of A1.
+
+/deleteinstances
+~~~~~~~~~~~~~~~~
+
+POST
+++++
+
+  Delete all policy instances.
+
+   **URL path:**
+    /deleteinstances
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      All policy instances deleted.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/deleteinstances"
+
+    Result:
+      200
+        All policy instances deleted.
+
+/deleteall
+~~~~~~~~~~~~~~~~
+
+POST
+++++
+
+  Full reset.
+
+   **URL path:**
+    /deleteall
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      All policy instances and types deleted.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/deleteall"
+
+    Result:
+      200
+        All policy instances and types deleted.
+
+/policytype
+~~~~~~~~~~~
+
+PUT
++++
+
+  Create a policy type.
+
+   **URL path:**
+    /policytype?id=<policy-type-id>
+
+  **Parameters:**
+
+    id: (*Required*)
+      The ID of the policy type.
+
+  **Body:** (*Required*)
+      A JSON object containing the schema for the type.
+
+  **Responses:**
+
+    200:
+      Policy type <policy-type-id> is OK.
+
+    201:
+      Policy type <policy-type-id> is OK.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X PUT "http://localhost:8085/policytype?id=Policy%201&ric=ric1&service=Service%201&type=STD_PolicyModelUnconstrained_0.2.0"
+        -H  "Content-Type: application/json"
+        -d "
+          {
+            "$schema": "http://json-schema.org/draft-07/schema#",
+            "title": "STD_PolicyModelUnconstrained_0.2.0",
+            "description": "Standard model of a policy with unconstrained scope id combinations",
+            "type": "object",
+            "properties": {
+              "scope": {
+                "type": "object",
+                "properties": {
+                  "ueId": {"type": "string"},
+                  "groupId": {"type": "string"},
+                  "sliceId": {"type": "string"},
+                  "qosId": {"type": "string"},
+                  "cellId": {"type": "string"}
+                },
+                "minProperties": 1,
+                "additionalProperties": false
+              },
+              "qosObjectives": {
+                "type": "object",
+                "properties": {
+                  "gfbr": {"type": "number"},
+                  "mfbr": {"type": "number"},
+                  "priorityLevel": {"type": "number"},
+                  "pdb": {"type": "number"}
+                },
+                "additionalProperties": false
+              },
+              "qoeObjectives": {
+                "type": "object",
+                "properties": {
+                  "qoeScore": {"type": "number"},
+                  "initialBuffering": {"type": "number"},
+                  "reBuffFreq": {"type": "number"},
+                  "stallRatio": {"type": "number"}
+                },
+                "additionalProperties": false
+              },
+              "resources": {
+                "type": "array",
+                "items": {
+                  "type": "object",
+                  "properties": {
+                    "cellIdList": {
+                      "type": "array",
+                      "minItems": 1,
+                      "uniqueItems": true,
+                      "items": {
+                        "type": "string"
+                      }
+                    },
+                    "preference": {
+                      "type": "string",
+                      "enum": [
+                        "SHALL",
+                        "PREFER",
+                        "AVOID",
+                        "FORBID"
+                      ]
+                    },
+                    "primary": {"type": "boolean"}
+                  },
+                  "additionalProperties": false,
+                  "required": ["cellIdList", "preference"]
+                }
+              }
+            },
+            "minProperties": 2,
+            "additionalProperties": false,
+            "required": ["scope"]
+          }
+        "
+
+    Result:
+      201
+        Policy type STD_PolicyModelUnconstrained_0.2.0 is OK
+
+DELETE
+++++++
+
+  Delete a policy type.
+
+   **URL path:**
+    /policytype?id=<policy-type-id>
+
+  **Parameters:**
+
+    id: (*Required*)
+      The ID of the policy type.
+
+  **Responses:**
+
+    204:
+      Policy type <policy-type-id> is OK.
+
+  **Examples:**
+
+    Call: ::
+
+     curl -X DELETE "http://localhost:8085/policytype?id=Policy%201&ric=ric1&service=Service%201&type=STD_PolicyModelUnconstrained_0.2.0"
+
+    Result:
+      204
+        Policy type STD_PolicyModelUnconstrained_0.2.0 is OK
+
+/policytypes
+~~~~~~~~~~~~
+
+GET
++++
+
+  Get a list of policy types.
+
+   **URL path:**
+    /policytypes
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      A list of policy types.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X GET "http://localhost:8085/policytypes"
+
+    Result:
+      200
+        STD_PolicyModelUnconstrained_0.2.0
+
+/forceresponse
+~~~~~~~~~~~~~~
+
+POST
+++++
+
+  Force a specific response code for an A1 operation.
+
+   **URL path:**
+    /forceresponse?responsecode=<http-response-code>
+
+  **Parameters:**
+
+    responsecode: (*Required*)
+      The HTTP response code to return.
+
+  **Responses:**
+
+    200:
+      Force response code:  <expected code> set for one single A1 response
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/forceresponse?responsecode=400"
+
+    Result:
+      200
+        Force response code:  400 set for one single A1 response
+
+/forcedelay
+~~~~~~~~~~~
+
+POST
+++++
+
+  Force delayed response of all A1 operations.
+
+   **URL path:**
+    /forcedelay?delay=<delay-time-seconds>
+
+  **Parameters:**
+
+    delay: (*Required*)
+      The time in seconds to delay all responses.
+
+  **Responses:**
+
+    200:
+      Force delay: <expected delay> sec set for all A1 responses
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/forcedelay?delay=2"
+
+    Result:
+      200
+        Force delay: 2 sec set for all A1 responses
+
+/status
+~~~~~~~
+
+PUT
++++
+
+  Set status and optional reason, delete and time stamp.
+
+   **URL path:**
+    /status?policyid=<policyid>&status=<status>&deleted=<value>&created_at=<time-stamp>
+
+  **Parameters:**
+
+    policyid: (*Required*)
+      The ID of a policy.
+
+    status: (*Required*)
+      The status of a policy.
+
+    deleted: (*Optional*)
+      True or false for real values, but accepts anything for error testing.
+
+    created_at: (*Optional*)
+      Time stamp for the status.
+
+  **Responses:**
+
+    200:
+      Status set to <status> for policy <policy-id>
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X PUT "http://localhost:8085/policyid=Policy1&status?status=Accepted
+
+    Result:
+      200
+        Status set to Accepted for policy Policy1.
+
+A1 Standard 1.1.3
+-----------------
+
+The available functions for the A1 Standard 1.1.3 version of A1.
+
+/deleteinstances
+~~~~~~~~~~~~~~~~
+
+POST
+++++
+
+  Delete all policy instances.
+
+   **URL path:**
+    /deleteinstances
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      All policy instances deleted.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/deleteinstances"
+
+    Result:
+      200
+        All policy instances deleted.
+
+/deleteall
+~~~~~~~~~~
+
+POST
+++++
+
+  Full reset.
+
+   **URL path:**
+    /deleteinstances
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      All policy instances deleted.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/deleteall"
+
+    Result:
+      200
+        All policy instances deleted.
+
+/forceresponse
+~~~~~~~~~~~~~~
+
+POST
+++++
+
+  Force a specific response code for an A1 operation.
+
+   **URL path:**
+    /forceresponse?responsecode=<http-response-code>
+
+  **Parameters:**
+
+    responsecode: (*Required*)
+      The HTTP response code to return.
+
+  **Responses:**
+
+    200:
+      Force response code: <expected code> set for one single A1 response
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/forceresponse?responsecode=400"
+
+    Result:
+      200
+        Force response code: 400 set for one single A1 response
+
+/forcedelay
+~~~~~~~~~~~
+
+POST
+++++
+
+  Force delayed response of all A1 operations.
+
+   **URL path:**
+    /forcedelay?delay=<delay-time-seconds>
+
+  **Parameters:**
+
+    delay: (*Required*)
+      The time in seconds to delay all responses.
+
+  **Responses:**
+
+    200:
+      Force delay: <expected delay> sec set for all A1 responses
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/forcedelay?delay=2"
+
+    Result:
+      200
+        Force delay: 2 sec set for all A1 responses
+
+/status
+~~~~~~~
+
+PUT
++++
+
+  Set status and optional reason, delete and time stamp.
+
+   **URL path:**
+    /status?policyid=<policyid>&status=<status>&reason=<reason>
+
+  **Parameters:**
+
+    policyid: (*Required*)
+      The ID of a policy.
+
+    status: (*Required*)
+      The status of a policy.
+
+    reason: (*Optional*)
+      The reason for the status.
+
+  **Responses:**
+
+    200:
+      Status set to <status> for policy <policy-id>
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X PUT "http://localhost:8085/status?policyid=Policy1&status=Accepted
+
+    Result:
+      200
+        Status set to Accepted for policy Policy1
+
+/sendstatus
+~~~~~~~~~~~
+
+POST
+++++
+
+  Send status for policy.
+
+   **URL path:**
+    /sendstatus?policyid=<policy-id>
+
+  **Parameters:**
+
+    policyid: (*Required*)
+      The ID of the policy to send status for.
+
+  **Responses:**
+
+    200:
+      Is a JSON with the response of the actual post request to the callback server, whatever that is.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X POST "http://localhost:8085/sendstatus?policyid=Policy2"
+
+    Result:
+      200
+
+1.1.x-alpha.2
+-------------
+
+The available functions for the 1.1.x-alpha.2.
+
+/deleteinstances
+~~~~~~~~~~~~~~~~
+
+DELETE
+++++++
+
+  Delete all policy instances.
+
+   **URL path:**
+    /deleteinstances
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      All policy instances deleted.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X DELETE "http://localhost:8085/deleteinstances"
+
+    Result:
+      200
+        All policy instances deleted.
+
+/deletetypes
+~~~~~~~~~~~~
+
+DELETE
+++++++
+
+  Delete all policy types.
+
+   **URL path:**
+    /deletetypes
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      All policy types deleted.
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X DELETE "http://localhost:8085/deletetypes"
+
+    Result:
+      200
+        All policy types deleted.
+
+/policytypes
+~~~~~~~~~~~~
+
+PUT
++++
+
+  Create or update a policy type.
+
+   **URL path:**
+    /policytypes/{policy-type-id}
+
+  **Parameters:**
+
+    None.
+
+  **Body:** (*Required*)
+      A JSON object containing the schema for the type.
+
+  **Responses:**
+
+    200:
+      The policy type was either created or updated for policy type id: <policy-type-id>
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X PUT "http://localhost:8085/policytype/Policy%201&ric=ric1&service=Service%201&type=STD_PolicyModelUnconstrained_0.2.0"
+        -H  "Content-Type: application/json"
+        -d "
+          {
+            "$schema": "http://json-schema.org/draft-07/schema#",
+            "title": "STD_PolicyModelUnconstrained_0.2.0",
+            "description": "Standard model of a policy with unconstrained scope id combinations",
+            "type": "object",
+            "properties": {
+              "scope": {
+                "type": "object",
+                "properties": {
+                  "ueId": {"type": "string"},
+                  "groupId": {"type": "string"},
+                  "sliceId": {"type": "string"},
+                  "qosId": {"type": "string"},
+                  "cellId": {"type": "string"}
+                },
+                "minProperties": 1,
+                "additionalProperties": false
+              },
+              "qosObjectives": {
+                "type": "object",
+                "properties": {
+                  "gfbr": {"type": "number"},
+                  "mfbr": {"type": "number"},
+                  "priorityLevel": {"type": "number"},
+                  "pdb": {"type": "number"}
+                },
+                "additionalProperties": false
+              },
+              "qoeObjectives": {
+                "type": "object",
+                "properties": {
+                  "qoeScore": {"type": "number"},
+                  "initialBuffering": {"type": "number"},
+                  "reBuffFreq": {"type": "number"},
+                  "stallRatio": {"type": "number"}
+                },
+                "additionalProperties": false
+              },
+              "resources": {
+                "type": "array",
+                "items": {
+                  "type": "object",
+                  "properties": {
+                    "cellIdList": {
+                      "type": "array",
+                      "minItems": 1,
+                      "uniqueItems": true,
+                      "items": {
+                        "type": "string"
+                      }
+                    },
+                    "preference": {
+                      "type": "string",
+                      "enum": [
+                        "SHALL",
+                        "PREFER",
+                        "AVOID",
+                        "FORBID"
+                      ]
+                    },
+                    "primary": {"type": "boolean"}
+                  },
+                  "additionalProperties": false,
+                  "required": ["cellIdList", "preference"]
+                }
+              }
+            },
+            "minProperties": 2,
+            "additionalProperties": false,
+            "required": ["scope"]
+          }
+        "
+
+    Result:
+      200
+        The policy type was either created or updated for policy type id: STD_PolicyModelUnconstrained_0.2.0
+
+DELETE
+++++++
+
+  Delete a policy type.
+
+   **URL path:**
+    /policytypes/{policy-type-id}
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      policy type successfully deleted for policy type id: <policy-type-id>
+
+  **Examples:**
+
+    Call: ::
+
+     curl -X DELETE "http://localhost:8085/policytype?id=Policy%201&ric=ric1&service=Service%201&type=STD_PolicyModelUnconstrained_0.2.0"
+
+     Result:
+      200
+        policy type successfully deleted for policy type id: STD_PolicyModelUnconstrained_0.2.0
+
+/{policyId}/{enforceStatus}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+PUT
++++
+
+  Set a status to a policy instance with an enforceStatus parameter only.
+
+   **URL path:**
+    /{policyId}/{enforceStatus}
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      Status updated for policy: <policyId>
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X PUT "http://localhost:8085/Policy1/ENFORCED
+
+    Result:
+      200
+        Status updated for policy: Policy1
+
+/{policyId}/{enforceStatus}/{enforceReason}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+PUT
+++++
+
+  Send a status to a policy instance with both enforceStatus and enforceReason.
+
+   **URL path:**
+    /{policyId}/{enforceStatus}/{enforceReason}
+
+  **Parameters:**
+
+    None.
+
+  **Responses:**
+
+    200:
+      Status updated for policy: <policyId>
+
+  **Examples:**
+
+    Call: ::
+
+      curl -X PUT "http://localhost:8085/Policy1/NOT_ENFORCED/100"
+
+    Result:
+      200
+        Status updated for policy: Policy1
diff --git a/tox.ini b/tox.ini
new file mode 100644 (file)
index 0000000..26cfed7
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,43 @@
+# ==================================================================================
+#       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.
+# ==================================================================================
+[tox]
+envlist = docs,docs-linkcheck
+minversion = 2.0
+skipsdist = true
+
+# doc jobs
+[testenv:docs]
+whitelist_externals = echo
+basepython = python3
+deps =
+    sphinx
+    sphinx-rtd-theme
+    sphinxcontrib-httpdomain
+    recommonmark
+    lfdocs-conf
+commands =
+    sphinx-build -W -b html -n -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/html
+    echo "Generated docs available in {toxinidir}/docs/_build/html"
+
+[testenv:docs-linkcheck]
+skipsdist = true
+basepython = python3
+deps = sphinx
+       sphinx-rtd-theme
+       sphinxcontrib-httpdomain
+       recommonmark
+       lfdocs-conf
+commands = sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees ./docs/ {toxinidir}/docs/_build/linkcheck