diff --git a/MANIFEST.in b/MANIFEST.in
index b0e9aee289f44131d6ac6fb0cf8fc2a28a28ddd9..c0097b2d5db1dab47f80d3902abdebd19cbddbf4 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,3 +2,4 @@ include gso/migrations/*.py
 include gso/migrations/*.mako
 include gso/migrations/versions/*.py
 include gso/translations/*.json
+include gso/alembic.ini
diff --git a/build-docs.sh b/build-docs.sh
index e7c1a5fb1d9ac0fa5646cf6a4d4a345536947001..016b34edae07b74c348218478f5a149c22a50765 100755
--- a/build-docs.sh
+++ b/build-docs.sh
@@ -1,4 +1,4 @@
-pip install sphinx_rtd_theme myst-parser
+pip install sphinx_rtd_theme sphinxcontrib-jquery
 
 cd docs || exit 1
 sphinx-build source build
diff --git a/docs/.gitlab-ci.yml b/docs/.gitlab-ci.yml
index c9d2fff925f56888c96d6fbf324116bce305e285..68d7fd2a3ea36f9a3a4b331e014b6cecef156d36 100644
--- a/docs/.gitlab-ci.yml
+++ b/docs/.gitlab-ci.yml
@@ -7,7 +7,7 @@ build-documentation:
   image: sphinxdoc/sphinx:latest
 
   before_script:
-    - pip install sphinx_rtd_theme sphinx-autodoc2 myst-parser
+    - pip install sphinx_rtd_theme sphinxcontrib-jquery
     - cd $CI_PROJECT_DIR/docs/source
   script:
     - make html
diff --git a/docs/source/apidocs/.gitignore b/docs/source/apidocs/.gitignore
deleted file mode 100644
index 72e8ffc0db8aad71a934dd11e5968bd5109e54b4..0000000000000000000000000000000000000000
--- a/docs/source/apidocs/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 2d75bf85feb81b24b2b5cffc40e4881eb9dfa56c..3f4d36bd86b823b9fe642011cd62c8efa55ee16a 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -4,18 +4,10 @@ copyright = '2023, GÉANT Vereniging'
 author = 'GÉANT Orchestration and Automation Team'
 
 # -- General configuration ---------------------------------------------------
-extensions = ['sphinx_rtd_theme', 'myst_parser', 'sphinx.ext.autodoc']
+extensions = ['sphinx_rtd_theme', 'sphinx.ext.autodoc', 'sphinxcontrib.jquery']
 
 templates_path = ['templates']
 exclude_patterns = ['build', 'Thumbs.db', '.DS_Store', 'venv', 'vale', '__init__.py']
-source_suffix = {
-    '.md': 'markdown'
-}
-
-# -- Options for Markdown support --------------------------------------------
-myst_enable_extensions = ['attrs_block', 'deflist', 'replacements', 'smartquotes', 'strikethrough', 'fieldlist']
-suppress_warnings = ['myst.strikethrough']
-
 
 # -- Options for HTML output -------------------------------------------------
 html_theme = 'sphinx_rtd_theme'
@@ -24,11 +16,12 @@ html_theme_options = {
     'style_nav_header_background': 'rgb(0 63 95)',
 }
 html_css_files = ['custom.css']
+html_js_files = ['custom.js']
 html_logo = 'static/geant_logo_white.svg'
 
-# Both the class' and the __init__ method's docstring are concatenated and inserted.
+# Both the class' and the ``__init__`` method's docstring are concatenated and inserted.
 autoclass_content = 'both'
-# autodoc_typehints = 'none'
+autodoc_typehints = 'none'
 
 # Display todos by setting to True
 todo_include_todos = True
diff --git a/docs/source/glossary.md b/docs/source/glossary.md
deleted file mode 100644
index 7035fa3bbb5019fbde57c70deeb66694924ff233..0000000000000000000000000000000000000000
--- a/docs/source/glossary.md
+++ /dev/null
@@ -1,41 +0,0 @@
-# Glossary of terms
-
-{.glossary}
-BGP
-: Border Gateway Protocol: a path vector routing protocol described in 
-<a href="https://datatracker.ietf.org/doc/html/rfc4271" target="_blank">RFC 4271</a>.
-
-CNAME
-: A type of DNS record that is used as an alias from one hostname to another
-
-CRUD
-: Create, Read, Update, Delete
-
-FQDN
-: Fully Quantified Domain Name
-
-GSO
-: GÉANT Service Orchestrator
-
-IPAM
-: IP Address Management
-
-IS-IS
-: Intermediate System to Intermediate System: a routing protocol described in 
-<a href="https://datatracker.ietf.org/doc/html/rfc7142" target="_blank">RFC 7142</a>.
-
-ISO
-: International Organisation for Standardisation
-
-LSO
-: Lightweight Service Orchestrator
-
-NET
-: Network Entity Title: used for {term}`IS-IS` routing.
-
-SNMP
-: Simple Network Management Protocol: a protocol that's used for gathering data, widely used for network management and
-monitoring.
-
-WFO
-: <a href="https://workfloworchestrator.org/" target="_blank">Workflow Orchestrator</a>
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
new file mode 100644
index 0000000000000000000000000000000000000000..bb7a2a69ca2c76900be5d138f6f3db610528538b
--- /dev/null
+++ b/docs/source/glossary.rst
@@ -0,0 +1,56 @@
+Glossary of terms
+=================
+
+.. glossary::
+
+  BGP
+    Border Gateway Protocol: a path vector routing protocol described in
+    `RFC 4271 <https://datatracker.ietf.org/doc/html/rfc4271>`_.
+
+  CIDR
+    Classless Inter-Domain Routing. A method for denoting IP ranges in the form of ``9.9.0.0/16`` or
+    ``fe80:1234:abcd::/48``.
+
+  CNAME
+    A type of DNS record that is used as an alias from one hostname to another
+
+  CRUD
+    Create, Read, Update, Delete
+
+  FQDN
+    Fully Quantified Domain Name
+
+  GSO
+    GÉANT Service Orchestrator
+
+  IPAM
+    IP Address Management
+
+  IS-IS
+    Intermediate System to Intermediate System: a routing protocol described in
+    `RFC 7142 <https://datatracker.ietf.org/doc/html/rfc7142>`_.
+
+  ISO
+    International Organisation for Standardisation
+
+  LAG
+    Link Aggregation: a bundle of multiple network connections.
+
+  LSO
+    Lightweight Service Orchestrator
+
+  NET
+    Network Entity Title: used for :term:`IS-IS` routing.
+
+  OSS
+    Operational Support Systems
+
+  SNMP
+    Simple Network Management Protocol: a protocol that's used for gathering data, widely used for network management
+    and monitoring.
+
+  UUID
+    Universally Unique Identifier
+
+  WFO
+    `Workflow Orchestrator <https://workfloworchestrator.org/>`_
diff --git a/docs/source/index.md b/docs/source/index.md
deleted file mode 100644
index 8ed5ec92290d7b9b5c564cdb20cdd6ebcae9234b..0000000000000000000000000000000000000000
--- a/docs/source/index.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# GÉANT Service Orchestrator ({term}`GSO`)
-
-Welcome to the documentation of the GÉANT Service Orchestrator, or {term}`GSO` for short.
-
-This documentation has the following sections:
-
-```{toctree}
-:caption: Contents
-:maxdepth: 1
-quickstart.md
-apidocs/gso/gso
-glossary.md
-```
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..5818da8afb4db3fb33ab27b8bf1da24d589b23d1
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,14 @@
+GÉANT Service Orchestrator
+==========================
+
+Welcome to the documentation of the GÉANT Service Orchestrator, or :term:`GSO` for short.
+
+This documentation has the following sections:
+
+.. toctree::
+   :caption: Contents
+   :maxdepth: 2
+
+   quickstart
+   modules
+   glossary
diff --git a/docs/source/module/api/index.rst b/docs/source/module/api/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c1e75155b0e7ffcfb829f6b42ef8e72eb62fed62
--- /dev/null
+++ b/docs/source/module/api/index.rst
@@ -0,0 +1,15 @@
+``gso.api``
+===========
+
+.. automodule:: gso.api
+   :members:
+   :show-inheritance:
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   v1/index
diff --git a/docs/source/module/api/v1/imports.rst b/docs/source/module/api/v1/imports.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b40e8edaed706c8656407fa1fc8d1b3d9bc060be
--- /dev/null
+++ b/docs/source/module/api/v1/imports.rst
@@ -0,0 +1,6 @@
+``gso.api.v1.imports``
+======================
+
+.. automodule:: gso.api.v1.imports
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/api/v1/index.rst b/docs/source/module/api/v1/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c5a4a0c55a90ccc798d55190e87c52336c56f1c4
--- /dev/null
+++ b/docs/source/module/api/v1/index.rst
@@ -0,0 +1,15 @@
+``gso.api.v1``
+==============
+
+.. automodule:: gso.api.v1
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   imports
diff --git a/docs/source/module/cli/import_sites.rst b/docs/source/module/cli/import_sites.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f7e19edef1744d40c3fddacf60efb6f48417aa54
--- /dev/null
+++ b/docs/source/module/cli/import_sites.rst
@@ -0,0 +1,6 @@
+``gso.cli.import_sites``
+========================
+
+.. automodule:: gso.cli.import_sites
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/cli/index.rst b/docs/source/module/cli/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..02bb0773058c3fb779f121c483a87fec5e56b0fa
--- /dev/null
+++ b/docs/source/module/cli/index.rst
@@ -0,0 +1,16 @@
+``gso.cli``
+===========
+
+.. automodule:: gso.cli
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   import_sites
+   netbox
diff --git a/docs/source/module/cli/netbox.rst b/docs/source/module/cli/netbox.rst
new file mode 100644
index 0000000000000000000000000000000000000000..8d379335ee1211f930d76d79a576bafe16a2fe68
--- /dev/null
+++ b/docs/source/module/cli/netbox.rst
@@ -0,0 +1,6 @@
+``gso.cli.netbox``
+==================
+
+.. automodule:: gso.cli.netbox
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/products/index.rst b/docs/source/module/products/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..c68072cc071e570f268408b67e656b63a1ed4bf6
--- /dev/null
+++ b/docs/source/module/products/index.rst
@@ -0,0 +1,21 @@
+``gso.products``
+================
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 1
+
+   product_blocks/index
+   product_types/index
+
+Submodules
+----------
+
+``gso.products.shared`` module
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. automodule:: gso.products.shared
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/products/product_blocks/index.rst b/docs/source/module/products/product_blocks/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..309e18b62f81f25b4cfffe6c60fd77b46bd07d06
--- /dev/null
+++ b/docs/source/module/products/product_blocks/index.rst
@@ -0,0 +1,19 @@
+``gso.products.product_blocks``
+===============================
+
+``gso.products.product_blocks`` package
+---------------------------------------
+
+.. automodule:: gso.products.product_blocks
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 1
+
+   iptrunk
+   router
+   site
diff --git a/docs/source/module/products/product_blocks/iptrunk.rst b/docs/source/module/products/product_blocks/iptrunk.rst
new file mode 100644
index 0000000000000000000000000000000000000000..150b7cbad3da767e202598bb803a6680651494dc
--- /dev/null
+++ b/docs/source/module/products/product_blocks/iptrunk.rst
@@ -0,0 +1,6 @@
+``gso.products.product_blocks.iptrunk``
+=======================================
+
+.. automodule:: gso.products.product_blocks.iptrunk
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/products/product_blocks/router.rst b/docs/source/module/products/product_blocks/router.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4ef96d9cbf0e5cf64345113f1fd91da8a063b1e3
--- /dev/null
+++ b/docs/source/module/products/product_blocks/router.rst
@@ -0,0 +1,6 @@
+``gso.products.product_blocks.router``
+======================================
+
+.. automodule:: gso.products.product_blocks.router
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/products/product_blocks/site.rst b/docs/source/module/products/product_blocks/site.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4b1415f44545d3944eae88c29cb3390e5a5b4007
--- /dev/null
+++ b/docs/source/module/products/product_blocks/site.rst
@@ -0,0 +1,6 @@
+``gso.products.product_blocks.site``
+====================================
+
+.. automodule:: gso.products.product_blocks.site
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/products/product_types/index.rst b/docs/source/module/products/product_types/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..80b73721a04437a9b4e54060927bd6de6ef4dc31
--- /dev/null
+++ b/docs/source/module/products/product_types/index.rst
@@ -0,0 +1,9 @@
+``gso.products.product_types``
+==============================
+
+``gso.products.product_types`` package
+--------------------------------------
+
+.. automodule:: gso.products.product_types
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/schemas/enums.rst b/docs/source/module/schemas/enums.rst
new file mode 100644
index 0000000000000000000000000000000000000000..54dc21f967c8a02160ff4c1413d13719b5ad8be9
--- /dev/null
+++ b/docs/source/module/schemas/enums.rst
@@ -0,0 +1,6 @@
+``gso.schemas.enums``
+=====================
+
+.. automodule:: gso.schemas.enums
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/schemas/imports.rst b/docs/source/module/schemas/imports.rst
new file mode 100644
index 0000000000000000000000000000000000000000..2015ea3efeb9a85beb62b83753455bcdabcc39aa
--- /dev/null
+++ b/docs/source/module/schemas/imports.rst
@@ -0,0 +1,6 @@
+``gso.schemas.imports``
+=======================
+
+.. automodule:: gso.schemas.imports
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/schemas/index.rst b/docs/source/module/schemas/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a56c90903015287946b1d7d9da853537c6a8523f
--- /dev/null
+++ b/docs/source/module/schemas/index.rst
@@ -0,0 +1,17 @@
+``gso.schemas``
+===============
+
+.. automodule:: gso.schemas
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   enums
+   imports
+   types
diff --git a/docs/source/module/schemas/types.rst b/docs/source/module/schemas/types.rst
new file mode 100644
index 0000000000000000000000000000000000000000..58b064999b4d89c88794dede5ba7eb1f065e1f1d
--- /dev/null
+++ b/docs/source/module/schemas/types.rst
@@ -0,0 +1,6 @@
+``gso.schemas.types``
+=====================
+
+.. automodule:: gso.schemas.types
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/services/crm.rst b/docs/source/module/services/crm.rst
new file mode 100644
index 0000000000000000000000000000000000000000..cee4e5018343626463809b6a9cb7a4bb499937ff
--- /dev/null
+++ b/docs/source/module/services/crm.rst
@@ -0,0 +1,6 @@
+``gso.services.crm``
+====================
+
+.. automodule:: gso.services.crm
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/services/index.rst b/docs/source/module/services/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..26190e2968a848d3fb06e491b52572dd4fca8faa
--- /dev/null
+++ b/docs/source/module/services/index.rst
@@ -0,0 +1,19 @@
+``gso.services``
+================
+
+.. automodule:: gso.services
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   crm
+   infoblox
+   netbox_client
+   provisioning_proxy
+   subscriptions
diff --git a/docs/source/module/services/infoblox.rst b/docs/source/module/services/infoblox.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d05316c9a246659376d301cbfb657ce3ac558aaa
--- /dev/null
+++ b/docs/source/module/services/infoblox.rst
@@ -0,0 +1,6 @@
+``gso.services.infoblox``
+=========================
+
+.. automodule:: gso.services.infoblox
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/services/netbox_client.rst b/docs/source/module/services/netbox_client.rst
new file mode 100644
index 0000000000000000000000000000000000000000..26891ede7814fe84c2f0c39031367cf9d7679e45
--- /dev/null
+++ b/docs/source/module/services/netbox_client.rst
@@ -0,0 +1,6 @@
+``gso.services.netbox_client``
+==============================
+
+.. automodule:: gso.services.netbox_client
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/services/provisioning_proxy.rst b/docs/source/module/services/provisioning_proxy.rst
new file mode 100644
index 0000000000000000000000000000000000000000..756edf2642d865e3104af928fbddd047c3d4ed32
--- /dev/null
+++ b/docs/source/module/services/provisioning_proxy.rst
@@ -0,0 +1,6 @@
+``gso.services.provisioning_proxy``
+===================================
+
+.. automodule:: gso.services.provisioning_proxy
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/services/subscriptions.rst b/docs/source/module/services/subscriptions.rst
new file mode 100644
index 0000000000000000000000000000000000000000..8b2bfbed50c0f873814049cfd3a6aed3409500a9
--- /dev/null
+++ b/docs/source/module/services/subscriptions.rst
@@ -0,0 +1,6 @@
+``gso.services.subscriptions``
+==============================
+
+.. automodule:: gso.services.subscriptions
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/utils/device_info.rst b/docs/source/module/utils/device_info.rst
new file mode 100644
index 0000000000000000000000000000000000000000..4a332bd7d9caab2d64555fed4f9efab4ddb8d272
--- /dev/null
+++ b/docs/source/module/utils/device_info.rst
@@ -0,0 +1,6 @@
+``gso.utils.device_info``
+=========================
+
+.. automodule:: gso.utils.device_info
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/utils/exceptions.rst b/docs/source/module/utils/exceptions.rst
new file mode 100644
index 0000000000000000000000000000000000000000..98414a525231a1488358aeaeed8cb4aea4025729
--- /dev/null
+++ b/docs/source/module/utils/exceptions.rst
@@ -0,0 +1,6 @@
+``gso.utils.exceptions``
+========================
+
+.. automodule:: gso.utils.exceptions
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/utils/index.rst b/docs/source/module/utils/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..1b0b55295fbef85d1eb3fb539efc7a48b96dbd9c
--- /dev/null
+++ b/docs/source/module/utils/index.rst
@@ -0,0 +1,16 @@
+``gso.utils``
+=============
+
+.. automodule:: gso.utils
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   device_info
+   exceptions
diff --git a/docs/source/module/workflows/index.rst b/docs/source/module/workflows/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..b89cf2bce0132dda7478def4ff5a55f6b18a4096
--- /dev/null
+++ b/docs/source/module/workflows/index.rst
@@ -0,0 +1,27 @@
+``gso.workflows``
+=================
+
+.. automodule:: gso.workflows
+   :members:
+   :show-inheritance:
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   iptrunk/index
+   router/index
+   site/index
+   tasks/index
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   utils
diff --git a/docs/source/module/workflows/iptrunk/create_iptrunk.rst b/docs/source/module/workflows/iptrunk/create_iptrunk.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3105f051175521d0c7dd7f40fdc51902b83ccc89
--- /dev/null
+++ b/docs/source/module/workflows/iptrunk/create_iptrunk.rst
@@ -0,0 +1,6 @@
+``gso.workflows.iptrunk.create_iptrunk``
+========================================
+
+.. automodule:: gso.workflows.iptrunk.create_iptrunk
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/iptrunk/index.rst b/docs/source/module/workflows/iptrunk/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3bfaec18bfb011680bf0ef5b902b587aeb6b3e6c
--- /dev/null
+++ b/docs/source/module/workflows/iptrunk/index.rst
@@ -0,0 +1,20 @@
+``gso.workflows.iptrunk``
+=========================
+
+.. automodule:: gso.workflows.iptrunk
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   create_iptrunk
+   migrate_iptrunk
+   modify_isis_metric
+   modify_trunk_interface
+   terminate_iptrunk
+   utils
diff --git a/docs/source/module/workflows/iptrunk/migrate_iptrunk.rst b/docs/source/module/workflows/iptrunk/migrate_iptrunk.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3ae52e5f71428e25c1281f8e35b8d2f4eb0adc90
--- /dev/null
+++ b/docs/source/module/workflows/iptrunk/migrate_iptrunk.rst
@@ -0,0 +1,6 @@
+``gso.workflows.iptrunk.migrate_iptrunk``
+=========================================
+
+.. automodule:: gso.workflows.iptrunk.migrate_iptrunk
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/iptrunk/modify_isis_metric.rst b/docs/source/module/workflows/iptrunk/modify_isis_metric.rst
new file mode 100644
index 0000000000000000000000000000000000000000..3f3a17deb07f87911a507f256e25bfcbba46cadf
--- /dev/null
+++ b/docs/source/module/workflows/iptrunk/modify_isis_metric.rst
@@ -0,0 +1,6 @@
+``gso.workflows.iptrunk.modify_isis_metric``
+============================================
+
+.. automodule:: gso.workflows.iptrunk.modify_isis_metric
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/iptrunk/modify_trunk_interface.rst b/docs/source/module/workflows/iptrunk/modify_trunk_interface.rst
new file mode 100644
index 0000000000000000000000000000000000000000..aad531eadc83b6c78c0e744931c41fa0502aa138
--- /dev/null
+++ b/docs/source/module/workflows/iptrunk/modify_trunk_interface.rst
@@ -0,0 +1,6 @@
+``gso.workflows.iptrunk.modify_trunk_interface``
+================================================
+
+.. automodule:: gso.workflows.iptrunk.modify_trunk_interface
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/iptrunk/terminate_iptrunk.rst b/docs/source/module/workflows/iptrunk/terminate_iptrunk.rst
new file mode 100644
index 0000000000000000000000000000000000000000..9a11c0c4bb82ddadd68f5e96ba8821fd9c91f91b
--- /dev/null
+++ b/docs/source/module/workflows/iptrunk/terminate_iptrunk.rst
@@ -0,0 +1,6 @@
+``gso.workflows.iptrunk.terminate_iptrunk``
+===========================================
+
+.. automodule:: gso.workflows.iptrunk.terminate_iptrunk
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/iptrunk/utils.rst b/docs/source/module/workflows/iptrunk/utils.rst
new file mode 100644
index 0000000000000000000000000000000000000000..96f665efadcf6e1286eef83b061ee27e574898e4
--- /dev/null
+++ b/docs/source/module/workflows/iptrunk/utils.rst
@@ -0,0 +1,6 @@
+``gso.workflows.iptrunk.utils``
+===============================
+
+.. automodule:: gso.workflows.iptrunk.utils
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/router/create_router.rst b/docs/source/module/workflows/router/create_router.rst
new file mode 100644
index 0000000000000000000000000000000000000000..e30abc03c292c14d9420bc7d0eef0da07e68eca6
--- /dev/null
+++ b/docs/source/module/workflows/router/create_router.rst
@@ -0,0 +1,6 @@
+``gso.workflows.router.create_router``
+======================================
+
+.. automodule:: gso.workflows.router.create_router
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/router/index.rst b/docs/source/module/workflows/router/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..11c3f71c734ac62f2e3ea194222eb6478816b335
--- /dev/null
+++ b/docs/source/module/workflows/router/index.rst
@@ -0,0 +1,16 @@
+``gso.workflows.router``
+========================
+
+.. automodule:: gso.workflows.router
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   create_router
+   terminate_router
diff --git a/docs/source/module/workflows/router/terminate_router.rst b/docs/source/module/workflows/router/terminate_router.rst
new file mode 100644
index 0000000000000000000000000000000000000000..0ac1ec450f4c88d79150227f4d7f4f08c25404ac
--- /dev/null
+++ b/docs/source/module/workflows/router/terminate_router.rst
@@ -0,0 +1,6 @@
+``gso.workflows.router.terminate_router``
+=========================================
+
+.. automodule:: gso.workflows.router.terminate_router
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/site/create_site.rst b/docs/source/module/workflows/site/create_site.rst
new file mode 100644
index 0000000000000000000000000000000000000000..45a9ef49d0d8bbc38a8ead230288330dc67e081f
--- /dev/null
+++ b/docs/source/module/workflows/site/create_site.rst
@@ -0,0 +1,6 @@
+``gso.workflows.site.create_site``
+==================================
+
+.. automodule:: gso.workflows.site.create_site
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/site/index.rst b/docs/source/module/workflows/site/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..396ddca109acad7044fb5e5fb05bc7d02608de5c
--- /dev/null
+++ b/docs/source/module/workflows/site/index.rst
@@ -0,0 +1,15 @@
+``gso.workflows.site``
+======================
+
+.. automodule:: gso.workflows.site
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   create_site
diff --git a/docs/source/module/workflows/tasks/import_iptrunk.rst b/docs/source/module/workflows/tasks/import_iptrunk.rst
new file mode 100644
index 0000000000000000000000000000000000000000..24a4be08f99a22f03e107ebef8d1cb58e5d0815b
--- /dev/null
+++ b/docs/source/module/workflows/tasks/import_iptrunk.rst
@@ -0,0 +1,6 @@
+``gso.workflows.tasks.import_iptrunk``
+======================================
+
+.. automodule:: gso.workflows.tasks.import_iptrunk
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/tasks/import_router.rst b/docs/source/module/workflows/tasks/import_router.rst
new file mode 100644
index 0000000000000000000000000000000000000000..65a6e60741a9d13b4dd3fc2dc3d796550b12b22e
--- /dev/null
+++ b/docs/source/module/workflows/tasks/import_router.rst
@@ -0,0 +1,6 @@
+``gso.workflows.tasks.import_router``
+=====================================
+
+.. automodule:: gso.workflows.tasks.import_router
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/tasks/import_site.rst b/docs/source/module/workflows/tasks/import_site.rst
new file mode 100644
index 0000000000000000000000000000000000000000..eb9280dff87c304ac2d7853931ce64203929851e
--- /dev/null
+++ b/docs/source/module/workflows/tasks/import_site.rst
@@ -0,0 +1,6 @@
+``gso.workflows.tasks.import_site``
+===================================
+
+.. automodule:: gso.workflows.tasks.import_site
+   :members:
+   :show-inheritance:
diff --git a/docs/source/module/workflows/tasks/index.rst b/docs/source/module/workflows/tasks/index.rst
new file mode 100644
index 0000000000000000000000000000000000000000..8feb3de360ac547ed291e2dfceb07d84e5724c75
--- /dev/null
+++ b/docs/source/module/workflows/tasks/index.rst
@@ -0,0 +1,17 @@
+``gso.workflows.tasks``
+=======================
+
+.. automodule:: gso.workflows.tasks
+   :members:
+   :show-inheritance:
+
+Submodules
+----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   import_iptrunk
+   import_router
+   import_site
diff --git a/docs/source/module/workflows/utils.rst b/docs/source/module/workflows/utils.rst
new file mode 100644
index 0000000000000000000000000000000000000000..85c007f728710bf73912dc96609e6d63084a945a
--- /dev/null
+++ b/docs/source/module/workflows/utils.rst
@@ -0,0 +1,6 @@
+``gso.workflows.utils``
+=======================
+
+.. automodule:: gso.workflows.utils
+   :members:
+   :show-inheritance:
diff --git a/docs/source/modules.rst b/docs/source/modules.rst
new file mode 100644
index 0000000000000000000000000000000000000000..37bab35941863cced5a394247e5da7262b6f9f92
--- /dev/null
+++ b/docs/source/modules.rst
@@ -0,0 +1,31 @@
+=========================
+Sub-packages and -modules
+=========================
+
+This page lists references to the documentation of all sub-packages and -modules that make up :term:`GSO`.
+
+Subpackages
+-----------
+
+.. toctree::
+   :maxdepth: 2
+   :titlesonly:
+
+   module/api/index
+   module/cli/index
+   module/products/index
+   module/schemas/index
+   module/services/index
+   module/utils/index
+   module/workflows/index
+
+Submodules
+----------
+
+``gso.settings`` module
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. automodule:: gso.settings
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/quickstart.md b/docs/source/quickstart.md
deleted file mode 100644
index 75921374869c27bd07ec2363ac4609eddf64bba7..0000000000000000000000000000000000000000
--- a/docs/source/quickstart.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# Quickstart
-
-## Development environment and dependencies
-
-- Install python 3.10 if you don't have it already:
-	- ``add-apt-repository ppa:deadsnakes/ppa``
-	- ``apt install python3.10 python3.10-distutils``
-- Follow Steps 1 and 2 from here to install dependencies and setup DB: https://workfloworchestrator.org/orchestrator-core/workshops/beginner/debian/
-- To install the orchestrator GUI, you can follow the steps 5 and 6 from the previous link.
-- Create a virtual environment:
-	- ``source /usr/share/virtualenvwrapper/virtualenvwrapper.sh``
-	- ``mkvirtualenv --python python3.10 gso``
-- To use the virtual environment:
-	- `source /usr/share/virtualenvwrapper/virtualenvwrapper.sh`
-	- `workon gso`
-
-## Installation
-
-Do all this inside the virtual environment.
-- Clone this repository and ``cd`` into it
-- ``pip install -r requirements.txt`` 
-	- If you get an error because you pip version is too old, run this: `curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10`
-- ``pip install -e .``
-- `cd` into `gso` directory and create ``oss-params.json`` based on the ``oss-params-example.json`` file.
-- Export the oss-params file: ``export OSS_PARAMS_FILENAME="/path/to/oss-params.json"``
-- Upgrade DB to latest revision: `PYTHONPATH=. python main.py db upgrade heads`
-
-## Run
-
-- Start the workflow orchestrator core with ``uvicorn --host 0.0.0.0 --port 8080 main:app`` from inside the ``gso`` directory of this repository.
-- Start the GUI with ``yarn start``.
-
-
-## Useful workflow orchestrator resources
-
-- WFO API (Swagger): <https://workfloworchestrator.org/orchestrator-core/architecture/application/api/>
-- WFO API (repository): <https://github.com/workfloworchestrator/orchestrator-core/tree/main/orchestrator/api/api_v1/endpoints>
-- Carolina's notes: <https://wiki.geant.org/pages/viewpage.action?pageId=562921625>
-
-### API examples
-
-*Note: update IP address in the request to your own where you're running WFO.*
-
-**Creating subscriptions** (look at the `initial_input_form_generator` method of the workflow to find what properties need to be in the body of the POST request)
-
-Create process that produces a new subscription (CREATE workflow)
-
-``curl -X POST -H "Content-Type: application/json" http://10.98.1.62:8080/api/processes/create_trunk --data '[{"product": "321045fc-21ec-476b-b67b-5f211e47c3d7"},{"trunk_name": "mytrunkfromapi", "geant_s_sid": "a1b2c3"}]'``
-
-Create process that manipulates the state of a subscription (TERMINATE workflow)
-
-``curl -X POST -H "Content-Type: application/json" http://10.98.1.62:8080/api/processes/terminate_trunk --data '[{"subscription_id": "910e7044-c427-46d1-afc3-7817e221d45d"}, {"are_you_sure": "yes"}]'``
-
-**Deleting subscriptions** (not the same as running a TERMINATE workflow for a subscription, but to actually erase it from the WFO database)
-
-To do this, you first have to delete the processes first:
-
-``curl -X 'DELETE' 'http://10.98.1.62:8080/api/processes/7447a3e9-7c58-4ae7-8804-c0a75c39c924'``
-
-Delete a subscription:
-
-``curl -X 'DELETE' 'http://10.98.1.62:8080/api/subscriptions/3f16532b-0a48-4fe3-802a-36b3f54eed0e'``
diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst
new file mode 100644
index 0000000000000000000000000000000000000000..bc9d7e0fb5d15dcabc903fde0f2b05ed99601f04
--- /dev/null
+++ b/docs/source/quickstart.rst
@@ -0,0 +1,47 @@
+Quickstart
+==========
+
+Development environment and dependencies
+----------------------------------------
+
+- Install python 3.10 if you do not have it already:
+    - ``add-apt-repository ppa:deadsnakes/ppa``
+    - ``apt install python3.10 python3.10-distutils``
+- Follow Steps 1 and 2 from here to install dependencies and setup DB:
+  `<https://workfloworchestrator.org/orchestrator-core/workshops/beginner/debian/>`_
+- To install the orchestrator GUI, you can follow the steps 5 and 6 from the previous link.
+- Create a virtual environment:
+    - ``source /usr/share/virtualenvwrapper/virtualenvwrapper.sh``
+    - ``mkvirtualenv --python python3.10 gso``
+- To use the virtual environment:
+    - ``source /usr/share/virtualenvwrapper/virtualenvwrapper.sh``
+    - ``workon gso``
+
+Installation
+------------
+
+Do all this inside the virtual environment.
+
+- Clone this repository
+- ``pip install -r requirements.txt``
+    - If you get an error because you pip version is too old, run this:
+      ``curl -sS https://bootstrap.pypa.io/get-pip.py | python3.10``
+- ``pip install -e .``
+- Create an ``oss-params.json`` based on the ``oss-params-example.json`` file inside ``/gso``.
+- Export the oss-params file: ``export OSS_PARAMS_FILENAME="/path/to/oss-params.json"``
+- Upgrade DB to latest revision: ``PYTHONPATH=. python main.py db upgrade heads``
+
+Run
+---
+
+- Start the workflow orchestrator core with ``uvicorn --host 0.0.0.0 --port 8080 main:app`` from inside the ``gso``
+  directory of this repository.
+- Start the GUI with ``yarn start``.
+
+
+Useful workflow orchestrator resources
+--------------------------------------
+
+- WFO API (Swagger): `<https://workfloworchestrator.org/orchestrator-core/architecture/application/api/>`_
+- WFO API (repository):
+  `<https://github.com/workfloworchestrator/orchestrator-core/tree/main/orchestrator/api/api_v1/endpoints>`_
diff --git a/docs/source/static/custom.js b/docs/source/static/custom.js
new file mode 100644
index 0000000000000000000000000000000000000000..184b5ae8b5a7a11a2e9c9d2407db9186d055e4a7
--- /dev/null
+++ b/docs/source/static/custom.js
@@ -0,0 +1,3 @@
+$(document).ready(function () {
+    $('a.external').attr('target', '_blank');
+});
diff --git a/docs/vale/.vale.ini b/docs/vale/.vale.ini
index cc1a032a86a01aac5d50a594a348c7fa8a3979f6..76b4b40c4795fa870bdefc65e758f0476f747d5a 100644
--- a/docs/vale/.vale.ini
+++ b/docs/vale/.vale.ini
@@ -6,8 +6,8 @@ Vocab = geant-jargon, Sphinx
 
 Packages = proselint, Microsoft
 
-[*.{md,py}]
-; We only lint .md and .py files
+[*.{py,rst}]
+; We only lint .rst and .py files
 BasedOnStyles = Vale, proselint, Microsoft
 ; Some headers are generated and we have no real influence over them
 Microsoft.Headings = NO
@@ -22,10 +22,13 @@ proselint.Typography = warning
 ; Same applies for not using contractions
 Microsoft.Contractions = NO
 custom.Contractions = YES
+; Using a "regular" - instead of an en dash is totally fine
+Microsoft.Negative = NO
+Microsoft.RangeFormat = NO
 
-TokenIgnores = ({term}), (:param \S+:), (:type \S+:)
+TokenIgnores = (:term:`\S+`), (:param \S+(?: \S+)?:), (:type \S+:), (:return \S+:), (:rtype: \S+), (:class:`\S+`)
 
-[*/glossary.md]
+[*/glossary.rst]
 ; Ignore acronyms being undefined in the file that defines all acronyms by definition.
 Microsoft.Acronyms = NO
 Microsoft.Contractions = NO
@@ -33,4 +36,4 @@ custom.Contractions = YES
 Microsoft.Passive = NO
 
 [formats]
-py = md
+py = rst
diff --git a/gso/api/v1/imports.py b/gso/api/v1/imports.py
index ae2b7c7df25af8ea8a27334ce02346ca63383f5d..bd8c0d5f2cb41b654e198d84f093d8d204714215 100644
--- a/gso/api/v1/imports.py
+++ b/gso/api/v1/imports.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Any
 from uuid import UUID
 
 from fastapi import Depends, HTTPException, status
@@ -30,20 +30,16 @@ def _start_process(process_name: str, data: dict) -> UUID:
 
 
 @router.post("/sites", status_code=status.HTTP_201_CREATED, response_model=ImportResponseModel)
-def import_site(site: SiteImportModel) -> Dict[str, Any]:
+def import_site(site: SiteImportModel) -> dict[str, Any]:
     """Import a site by running the import_site workflow.
 
-    Args:
-    ----
-    site (SiteImportModel): The site information to be imported.
+    :param site: The site information to be imported.
+    :type site: SiteImportModel
 
-    Returns:
-    -------
-    dict: A dictionary containing the process id of the started process and detail message.
+    :return: A dictionary containing the process id of the started process and detail message.
+    :rtype: dict[str, Any]
 
-    Raises:
-    ------
-    HTTPException: If the site already exists or if there's an error in the process.
+    :raises HTTPException: If the site already exists or if there's an error in the process.
     """
     try:
         subscription = subscriptions.retrieve_subscription_by_subscription_instance_value(
@@ -59,20 +55,16 @@ def import_site(site: SiteImportModel) -> Dict[str, Any]:
 
 
 @router.post("/routers", status_code=status.HTTP_201_CREATED, response_model=ImportResponseModel)
-def import_router(router_data: RouterImportModel) -> Dict[str, Any]:
+def import_router(router_data: RouterImportModel) -> dict[str, Any]:
     """Import a router by running the import_router workflow.
 
-    Args:
-    ----
-    router_data (RouterImportModel): The router information to be imported.
+    :param router_data: The router information to be imported.
+    :type router_data: RouterImportModel
 
-    Returns:
-    -------
-    dict: A dictionary containing the process id of the started process and detail message.
+    :return: A dictionary containing the process id of the started process and detail message.
+    :rtype: dict[str, Any]
 
-    Raises:
-    ------
-    HTTPException: If there's an error in the process.
+    :raises HTTPException: If there's an error in the process.
     """
 
     pid = _start_process("import_router", router_data.dict())
@@ -80,20 +72,16 @@ def import_router(router_data: RouterImportModel) -> Dict[str, Any]:
 
 
 @router.post("/iptrunks", status_code=status.HTTP_201_CREATED, response_model=ImportResponseModel)
-def import_iptrunk(iptrunk_data: IptrunkImportModel) -> Dict[str, Any]:
+def import_iptrunk(iptrunk_data: IptrunkImportModel) -> dict[str, Any]:
     """Import an iptrunk by running the import_iptrunk workflow.
 
-    Args:
-    ----
-    iptrunk_data (IptrunkImportModel): The iptrunk information to be imported.
+    :param iptrunk_data: The iptrunk information to be imported.
+    :type iptrunk_data: IptrunkImportModel
 
-    Returns:
-    -------
-    dict: A dictionary containing the process id of the started process and detail message.
+    :return: A dictionary containing the process id of the started process and detail message.
+    :rtype: dict[str, Any]
 
-    Raises:
-    ------
-    HTTPException: If there's an error in the process.
+    :raises HTTPException: If there's an error in the process.
     """
 
     pid = _start_process("import_iptrunk", iptrunk_data.dict())
diff --git a/gso/cli/netbox.py b/gso/cli/netbox.py
index 97acdcf84bf0bd70320594c022087a6400f07486..97ee7bc16fedd17f976853faeb88dbd9bb3406f8 100644
--- a/gso/cli/netbox.py
+++ b/gso/cli/netbox.py
@@ -1,7 +1,8 @@
 import typer
 from pynetbox import RequestError
 
-from gso.services.netbox_client import NetBoxClient
+from gso.services.netbox_client import NetboxClient
+from gso.utils.device_info import DEFAULT_SITE, ROUTER_ROLE
 
 app: typer.Typer = typer.Typer()
 
@@ -11,24 +12,24 @@ def netbox_initial_setup() -> None:
     """Set up NetBox for the first time.
 
     It includes:
-    - Creating a default site (GEANT)
+    - Creating a default site (GÉANT)
     - Creating device roles (Router)
     """
     typer.echo("Initial setup of NetBox ...")
     typer.echo("Connecting to NetBox ...")
 
-    nbclient = NetBoxClient()
+    nbclient = NetboxClient()
 
-    typer.echo("Creating GEANT site ...")
+    typer.echo("Creating GÉANT site ...")
     try:
-        nbclient.create_device_site("GEANT", "geant")
+        nbclient.create_device_site(DEFAULT_SITE["name"], DEFAULT_SITE["slug"])
         typer.echo("Site created successfully.")
     except RequestError as e:
         typer.echo(f"Error creating site: {e}")
 
     typer.echo("Creating Router device role ...")
     try:
-        nbclient.create_device_role("router", "router")
+        nbclient.create_device_role(ROUTER_ROLE["name"], ROUTER_ROLE["slug"])
         typer.echo("Device role created successfully.")
     except RequestError as e:
         typer.echo(f"Error creating device role: {e}")
diff --git a/gso/main.py b/gso/main.py
index e05aac889c02934fb9842afaaeae39c89bb0dce0..112bd535c9df2054cae59225d3c3d16f9c38e242 100644
--- a/gso/main.py
+++ b/gso/main.py
@@ -1,4 +1,4 @@
-"""The main module that runs {term}`GSO`."""
+"""The main module that runs :term:`GSO`."""
 import typer
 from orchestrator import OrchestratorCore
 from orchestrator.cli.main import app as core_cli
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index 16da3f77b14b198d5e980704057f705edfd9c632..df2529a3ff74a248f29490cd6fa49bbdd318fb65 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -1,4 +1,4 @@
-"""Module that updates the domain model of {term}`GSO`. Should contain all types of subscriptions."""
+"""Module that updates the domain model of :term:`GSO`. Should contain all types of subscriptions."""
 from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY
 
 from gso.products.product_types.iptrunk import Iptrunk
diff --git a/gso/products/product_blocks/iptrunk.py b/gso/products/product_blocks/iptrunk.py
index 89f54e7211a0974e97b18ee63e51c42b2ea7c7df..3eb992d189955e52ea7ee3fce27c55658fd98f1c 100644
--- a/gso/products/product_blocks/iptrunk.py
+++ b/gso/products/product_blocks/iptrunk.py
@@ -1,7 +1,7 @@
 """IP trunk product block that has all parameters of a subscription throughout its lifecycle."""
 
 import ipaddress
-from typing import Optional, TypeVar
+from typing import TypeVar
 
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.forms.validators import UniqueConstrainedList
@@ -28,24 +28,24 @@ class IptrunkSideBlockInactive(
     ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IptrunkSideBlock"
 ):
     iptrunk_side_node: RouterBlockInactive
-    iptrunk_side_ae_iface: Optional[str] = None
-    iptrunk_side_ae_geant_a_sid: Optional[str] = None
+    iptrunk_side_ae_iface: str | None = None
+    iptrunk_side_ae_geant_a_sid: str | None = None
     iptrunk_side_ae_members: list[str] = Field(default_factory=list)
     iptrunk_side_ae_members_description: list[str] = Field(default_factory=list)
 
 
 class IptrunkSideBlockProvisioning(IptrunkSideBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     iptrunk_side_node: RouterBlockProvisioning
-    iptrunk_side_ae_iface: Optional[str] = None
-    iptrunk_side_ae_geant_a_sid: Optional[str] = None
+    iptrunk_side_ae_iface: str | None = None
+    iptrunk_side_ae_geant_a_sid: str | None = None
     iptrunk_side_ae_members: list[str] = Field(default_factory=list)
     iptrunk_side_ae_members_description: list[str] = Field(default_factory=list)
 
 
 class IptrunkSideBlock(IptrunkSideBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     iptrunk_side_node: RouterBlock
-    iptrunk_side_ae_iface: Optional[str] = None
-    iptrunk_side_ae_geant_a_sid: Optional[str] = None
+    iptrunk_side_ae_iface: str | None = None
+    iptrunk_side_ae_geant_a_sid: str | None = None
     iptrunk_side_ae_members: list[str] = Field(default_factory=list)
     iptrunk_side_ae_members_description: list[str] = Field(default_factory=list)
 
@@ -53,31 +53,31 @@ class IptrunkSideBlock(IptrunkSideBlockProvisioning, lifecycle=[SubscriptionLife
 class IptrunkBlockInactive(
     ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IptrunkBlock"
 ):
-    """A trunk that's currently inactive, see {class}`IptrunkBlock`."""
-
-    geant_s_sid: Optional[str] = None
-    iptrunk_description: Optional[str] = None
-    iptrunk_type: Optional[IptrunkType] = None
-    iptrunk_speed: Optional[str] = None
-    iptrunk_minimum_links: Optional[int] = None
-    iptrunk_isis_metric: Optional[int] = None
-    iptrunk_ipv4_network: Optional[ipaddress.IPv4Network] = None
-    iptrunk_ipv6_network: Optional[ipaddress.IPv6Network] = None
+    """A trunk that's currently inactive, see :class:`IptrunkBlock`."""
+
+    geant_s_sid: str | None = None
+    iptrunk_description: str | None = None
+    iptrunk_type: IptrunkType | None = None
+    iptrunk_speed: str | None = None
+    iptrunk_minimum_links: int | None = None
+    iptrunk_isis_metric: int | None = None
+    iptrunk_ipv4_network: ipaddress.IPv4Network | None = None
+    iptrunk_ipv6_network: ipaddress.IPv6Network | None = None
     #
     iptrunk_sides: IptrunkSides[IptrunkSideBlockInactive]
 
 
 class IptrunkBlockProvisioning(IptrunkBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A trunk that's currently being provisioned, see {class}`IptrunkBlock`."""
-
-    geant_s_sid: Optional[str] = None
-    iptrunk_description: Optional[str] = None
-    iptrunk_type: Optional[IptrunkType] = None
-    iptrunk_speed: Optional[str] = None
-    iptrunk_minimum_links: Optional[int] = None
-    iptrunk_isis_metric: Optional[int] = None
-    iptrunk_ipv4_network: Optional[ipaddress.IPv4Network] = None
-    iptrunk_ipv6_network: Optional[ipaddress.IPv6Network] = None
+    """A trunk that's currently being provisioned, see :class:`IptrunkBlock`."""
+
+    geant_s_sid: str | None = None
+    iptrunk_description: str | None = None
+    iptrunk_type: IptrunkType | None = None
+    iptrunk_speed: str | None = None
+    iptrunk_minimum_links: int | None = None
+    iptrunk_isis_metric: int | None = None
+    iptrunk_ipv4_network: ipaddress.IPv4Network | None = None
+    iptrunk_ipv6_network: ipaddress.IPv6Network | None = None
     #
     iptrunk_sides: IptrunkSides[IptrunkSideBlockProvisioning]
 
@@ -85,20 +85,21 @@ class IptrunkBlockProvisioning(IptrunkBlockInactive, lifecycle=[SubscriptionLife
 class IptrunkBlock(IptrunkBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """A trunk that's currently deployed in the network."""
 
+    #:  GÉANT service ID associated with this trunk.
     geant_s_sid: str
-    """GÉANT service ID associated with this trunk. """
+    #:  A human-readable description of this trunk.
     iptrunk_description: str
-    """A human-readable description of this trunk."""
+    #:  The type of trunk, can be either dark fibre or leased capacity.
     iptrunk_type: IptrunkType
-    """The type of trunk, can be either dark fibre or leased capacity."""
+    #:  The speed of the trunk, measured per interface associated with it.
     iptrunk_speed: str  # FIXME: should be of PhyPortCapacity type
-    """The speed of the trunk, measured per interface associated with it."""
+    #:  The minimum amount of links the trunk should consist of.
     iptrunk_minimum_links: int
-    """The minimum amount of links the trunk should consist of."""
+    #:  The :term:`IS-IS` metric of this link
     iptrunk_isis_metric: int
-    """The {term}`IS-IS` metric of this link"""
+    #:  The IPv4 network used for this trunk.
     iptrunk_ipv4_network: ipaddress.IPv4Network
-    """The IPv4 network used for this trunk."""
+    #:  The IPv6 network used for this trunk.
     iptrunk_ipv6_network: ipaddress.IPv6Network
-    """The IPv6 network used for this trunk."""
+    #:  The two sides that the trunk is connected to.
     iptrunk_sides: IptrunkSides[IptrunkSideBlock]
diff --git a/gso/products/product_blocks/router.py b/gso/products/product_blocks/router.py
index 547b73c462731c577d6ce768ab0f43e18b458ee6..58cf1d3602ffe3a10bc226f71064c88286a0bbf4 100644
--- a/gso/products/product_blocks/router.py
+++ b/gso/products/product_blocks/router.py
@@ -1,6 +1,5 @@
-"""Product block for {class}`Router` products."""
+"""Product block for :class:`Router` products."""
 import ipaddress
-from typing import Optional
 
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle, strEnum
@@ -27,21 +26,21 @@ class RouterRole(strEnum):
 class RouterBlockInactive(
     ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="RouterBlock"
 ):
-    """A router that's being currently inactive. See {class}`RouterBlock`."""
-
-    router_fqdn: Optional[str] = None
-    router_ts_port: Optional[PortNumber] = None
-    router_access_via_ts: Optional[bool] = None
-    router_lo_ipv4_address: Optional[ipaddress.IPv4Address] = None
-    router_lo_ipv6_address: Optional[ipaddress.IPv6Address] = None
-    router_lo_iso_address: Optional[str] = None
-    router_si_ipv4_network: Optional[ipaddress.IPv4Network] = None
-    router_ias_lt_ipv4_network: Optional[ipaddress.IPv4Network] = None
-    router_ias_lt_ipv6_network: Optional[ipaddress.IPv6Network] = None
-    router_vendor: Optional[RouterVendor] = None
-    router_role: Optional[RouterRole] = None
-    router_site: Optional[SiteBlockInactive]
-    router_is_ias_connected: Optional[bool] = None
+    """A router that's being currently inactive. See :class:`RouterBlock`."""
+
+    router_fqdn: str | None = None
+    router_ts_port: PortNumber | None = None
+    router_access_via_ts: bool | None = None
+    router_lo_ipv4_address: ipaddress.IPv4Address | None = None
+    router_lo_ipv6_address: ipaddress.IPv6Address | None = None
+    router_lo_iso_address: str | None = None
+    router_si_ipv4_network: ipaddress.IPv4Network | None = None
+    router_ias_lt_ipv4_network: ipaddress.IPv4Network | None = None
+    router_ias_lt_ipv6_network: ipaddress.IPv6Network | None = None
+    router_vendor: RouterVendor | None = None
+    router_role: RouterRole | None = None
+    router_site: SiteBlockInactive | None
+    router_is_ias_connected: bool | None = None
 
 
 def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str:
@@ -49,49 +48,49 @@ def generate_fqdn(hostname: str, site_name: str, country_code: str) -> str:
 
 
 class RouterBlockProvisioning(RouterBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A router that's being provisioned. See {class}`RouterBlock`."""
+    """A router that's being provisioned. See :class:`RouterBlock`."""
 
     router_fqdn: str
     router_ts_port: PortNumber
-    router_access_via_ts: Optional[bool] = None
-    router_lo_ipv4_address: Optional[ipaddress.IPv4Address] = None
-    router_lo_ipv6_address: Optional[ipaddress.IPv6Address] = None
-    router_lo_iso_address: Optional[str] = None
-    router_si_ipv4_network: Optional[ipaddress.IPv4Network] = None
-    router_ias_lt_ipv4_network: Optional[ipaddress.IPv4Network] = None
-    router_ias_lt_ipv6_network: Optional[ipaddress.IPv6Network] = None
-    router_vendor: Optional[RouterVendor] = None
-    router_role: Optional[RouterRole] = None
-    router_site: Optional[SiteBlockProvisioning]
-    router_is_ias_connected: Optional[bool] = None
+    router_access_via_ts: bool | None = None
+    router_lo_ipv4_address: ipaddress.IPv4Address | None = None
+    router_lo_ipv6_address: ipaddress.IPv6Address | None = None
+    router_lo_iso_address: str | None = None
+    router_si_ipv4_network: ipaddress.IPv4Network | None = None
+    router_ias_lt_ipv4_network: ipaddress.IPv4Network | None = None
+    router_ias_lt_ipv6_network: ipaddress.IPv6Network | None = None
+    router_vendor: RouterVendor | None = None
+    router_role: RouterRole | None = None
+    router_site: SiteBlockProvisioning | None
+    router_is_ias_connected: bool | None = None
 
 
 class RouterBlock(RouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """A router that's currently deployed in the network."""
 
+    #:  :term:`FQDN` of a router.
     router_fqdn: str
-    """{term}`FQDN` of a router."""
+    #:  The port of the terminal server that this router is connected to. Used to offer out of band access.
     router_ts_port: PortNumber
-    """The port of the terminal server that this router is connected to. Used to offer out of band access."""
+    #:  Whether this router should be accessed through the terminal server, or through its loopback address.
     router_access_via_ts: bool
-    """Whether this router should be accessed through the terminal server, or through its loopback address."""
+    #:  The IPv4 loopback address of the router.
     router_lo_ipv4_address: ipaddress.IPv4Address
-    """The IPv4 loopback address of the router."""
+    #:  The IPv6 loopback address of the router.
     router_lo_ipv6_address: ipaddress.IPv6Address
-    """The IPv6 loopback address of the router."""
+    #:  The :term:`ISO` :term:`NET` of the router, used for :term:`IS-IS` support.
     router_lo_iso_address: str
-    """The {term}`ISO` {term}`NET` of the router, used for {term}`IS-IS` support."""
-    router_si_ipv4_network: Optional[ipaddress.IPv4Network]
-    """The SI IPv4 network of the router."""
-    router_ias_lt_ipv4_network: Optional[ipaddress.IPv4Network]
-    """The IAS LT IPv4 network of the router."""
-    router_ias_lt_ipv6_network: Optional[ipaddress.IPv6Network]
-    """The IAS LT IPv6 network of the router."""
+    #:  The SI IPv4 network of the router.
+    router_si_ipv4_network: ipaddress.IPv4Network | None
+    #:  The IAS LT IPv4 network of the router.
+    router_ias_lt_ipv4_network: ipaddress.IPv4Network | None
+    #:  The IAS LT IPv6 network of the router.
+    router_ias_lt_ipv6_network: ipaddress.IPv6Network | None
+    #:  The vendor of the router, can be any of the values defined in :class:`RouterVendor`.
     router_vendor: RouterVendor
-    """The vendor of the router, can be any of the values defined in {class}`RouterVendor`."""
+    #:  The role of the router, which can be any of the values defined in :class:`RouterRole`.
     router_role: RouterRole
-    """The role of the router, which can be any of the values defined in {class}`RouterRole`."""
+    #:  The :class:`Site` that this router resides in. Both physically and computationally.
     router_site: SiteBlock
-    """The {class}`Site` that this router resides in. Both physically and computationally."""
+    #:  The router is going to have a LT interface between inet0 and IAS
     router_is_ias_connected: bool
-    """The router is going to have a LT interface between inet0 and IAS"""
diff --git a/gso/products/product_blocks/site.py b/gso/products/product_blocks/site.py
index 4cd0f6874f97562dcf66504dc0f499d88260e1d3..74f4cf25dd1266424e222f301439a0a9f3dde694 100644
--- a/gso/products/product_blocks/site.py
+++ b/gso/products/product_blocks/site.py
@@ -1,6 +1,4 @@
 """The product block that describes a site subscription."""
-from typing import Optional
-
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle, strEnum
 
@@ -21,59 +19,59 @@ class SiteTier(strEnum):
 
 
 class SiteBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="SiteBlock"):
-    """A site that's currently inactive, see {class}`SiteBlock`."""
+    """A site that's currently inactive, see :class:`SiteBlock`."""
 
-    site_name: Optional[str] = None
-    site_city: Optional[str] = None
-    site_country: Optional[str] = None
-    site_country_code: Optional[str] = None
-    site_latitude: Optional[LatitudeCoordinate] = None
-    site_longitude: Optional[LongitudeCoordinate] = None
-    site_internal_id: Optional[int] = None
-    site_bgp_community_id: Optional[int] = None
-    site_tier: Optional[SiteTier] = None
-    site_ts_address: Optional[str] = None
+    site_name: str | None = None
+    site_city: str | None = None
+    site_country: str | None = None
+    site_country_code: str | None = None
+    site_latitude: LatitudeCoordinate | None = None
+    site_longitude: LongitudeCoordinate | None = None
+    site_internal_id: int | None = None
+    site_bgp_community_id: int | None = None
+    site_tier: SiteTier | None = None
+    site_ts_address: str | None = None
 
 
 class SiteBlockProvisioning(SiteBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
-    """A site that's currently being provisioned, see {class}`SiteBlock`."""
+    """A site that's currently being provisioned, see :class:`SiteBlock`."""
 
-    site_name: Optional[str] = None
-    site_city: Optional[str] = None
-    site_country: Optional[str] = None
-    site_country_code: Optional[str] = None
-    site_latitude: Optional[LatitudeCoordinate] = None
-    site_longitude: Optional[LongitudeCoordinate] = None
-    site_internal_id: Optional[int] = None
-    site_bgp_community_id: Optional[int] = None
-    site_tier: Optional[SiteTier] = None
-    site_ts_address: Optional[str] = None
+    site_name: str | None = None
+    site_city: str | None = None
+    site_country: str | None = None
+    site_country_code: str | None = None
+    site_latitude: LatitudeCoordinate | None = None
+    site_longitude: LongitudeCoordinate | None = None
+    site_internal_id: int | None = None
+    site_bgp_community_id: int | None = None
+    site_tier: SiteTier | None = None
+    site_ts_address: str | None = None
 
 
 class SiteBlock(SiteBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     """A site that's currently available for routers and services to be hosted at."""
 
+    #:  The name of the site, that will dictate part of the :term:`FQDN` of routers that are hosted at this site. For
+    #:  example: ``router.X.Y.geant.net``, where X denotes the name of the site.
     site_name: str
-    """The name of the site, that will dictate part of the {term}`FQDN` of routers that are hosted at this site. For
-    example: `router.X.Y.geant.net`, where X denotes the name of the site."""
+    #:  The city at which the site is located.
     site_city: str
-    """The city at which the site is located."""
+    #:  The country in which the site is located.
     site_country: str
-    """The country in which the site is located."""
+    #:  The code of the corresponding country. This is also used for the :term:`FQDN`, following the example given for
+    #:  the site name, the country code would end up in the Y position.
     site_country_code: str
-    """The code of the corresponding country. This is also used for the {term}`FQDN`, following the example given for
-    the site name, the country code would end up in the Y position."""
+    #:  The latitude of the site, used for :term:`SNMP` purposes.
     site_latitude: LatitudeCoordinate
-    """The latitude of the site, used for {term}`SNMP` purposes."""
+    #:  Similar to the latitude, the longitude of a site.
     site_longitude: LongitudeCoordinate
-    """Similar to the latitude, the longitude of a site."""
+    #:  The internal ID used within GÉANT to denote a site.
     site_internal_id: int
-    """The internal ID used within GÉANT to denote a site."""
+    #:  The :term:`BGP` community ID of a site, used to advertise routes learned at this site.
     site_bgp_community_id: int
-    """The {term}`BGP` community ID of a site, used to advertise routes learned at this site."""
+    #:  The tier of a site, as described in :class:`SiteTier`.
     site_tier: SiteTier
-    """The tier of a site, as described in {class}`SiteTier`."""
-    site_ts_address: Optional[str] = None
-    """The address of the terminal server that this router is connected to. The terminal server provides out of band
-    access. This is required in case a link goes down, or when a router is initially added to the network and it does
-    not have any IP trunks connected to it yet."""
+    #:  The address of the terminal server that this router is connected to. The terminal server provides out of band
+    #:  access. This is required in case a link goes down, or when a router is initially added to the network and it
+    #:  does not have any IP trunks connected to it yet.
+    site_ts_address: str | None = None
diff --git a/gso/schemas/types.py b/gso/schemas/types.py
index 94aced74e3c6ba4b6703b7afee01d962603cfbd4..114e573611b457a6adf3360517c3bd599021e08b 100644
--- a/gso/schemas/types.py
+++ b/gso/schemas/types.py
@@ -7,8 +7,8 @@ from pydantic import ConstrainedStr
 class LatitudeCoordinate(ConstrainedStr):
     """A latitude coordinate, modeled as a constrained string.
 
-    The coordinate must match the format conforming to the latitude
-    range of `-`90 to +90 degrees. It can be a floating-point number or an integer.
+    The coordinate must match the format conforming to the latitude range of -90 to +90 degrees. It can be a
+    floating-point number or an integer.
     Valid examples: 40.7128, -74.0060, 90, -90, 0
     """
 
@@ -26,7 +26,7 @@ class LongitudeCoordinate(ConstrainedStr):
     """A longitude coordinate, modeled as a constrained string.
 
     The coordinate must match the format conforming to the longitude
-    range of `-`180 to 180 degrees. It can be a floating point number or an integer.
+    range of -180 to +180 degrees. It can be a floating point number or an integer.
     Valid examples: 40.7128, -74.0060, 180, -180, 0
     """
 
diff --git a/gso/services/crm.py b/gso/services/crm.py
index 7568e3116eb2e685d45d1a6c67ff11d9b0bf8533..8c2af4698aa2c16478f372388df88ae6d125c308 100644
--- a/gso/services/crm.py
+++ b/gso/services/crm.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Any
 
 
 class CustomerNotFoundError(Exception):
@@ -16,7 +16,7 @@ def all_customers() -> list[dict]:
     ]
 
 
-def get_customer_by_name(name: str) -> Dict[str, Any]:
+def get_customer_by_name(name: str) -> dict[str, Any]:
     for customer in all_customers():
         if customer["name"] == name:
             return customer
diff --git a/gso/services/infoblox.py b/gso/services/infoblox.py
index 556a760953cae6cc5c55eb6f093f1ef08b9e7549..0082f9bf2525d3ba32f19a6e3918cbe32348b2de 100644
--- a/gso/services/infoblox.py
+++ b/gso/services/infoblox.py
@@ -20,8 +20,8 @@ class DeletionError(Exception):
 def _setup_connection() -> tuple[connector.Connector, IPAMParams]:
     """Set up a new connection with an Infoblox instance.
 
-    :return: A tuple that has an Infoblox `Connector` instance, and {term}`IPAM` parameters.
-    :rtype: tuple[{class}`infoblox_client.connector.Connector`, IPAMParams]
+    :return: A tuple that has an Infoblox ``Connector`` instance, and :term:`IPAM` parameters.
+    :rtype: tuple[:class:`Connector`, IPAMParams]
     """
     oss = load_oss_params().IPAM
     options = {
@@ -44,15 +44,15 @@ def _allocate_network(
     """Allocate a new network in Infoblox.
 
     The function will go over all given containers, and try to allocate a network within the available IP space. If no
-    space is available, this method raises an {class}`AllocationError`.
+    space is available, this method raises an :class:`AllocationError`.
 
     :param conn: An active Infoblox connection.
-    :type conn: {class}`infoblox_client.connector.Connector`
+    :type conn: :class:`infoblox_client.connector.Connector`
     :param dns_view: The Infoblox `dns_view` in which the network should be allocated.
     :type dns_view: str
     :param netmask: The netmask of the desired network. Can be up to 32 for v4 networks, and 128 for v6 networks.
     :type netmask: int
-    :param containers: A list of network containers in which the network should be allocated, given in {term}`CIDR`
+    :param containers: A list of network containers in which the network should be allocated, given in :term:`CIDR`
                        notation.
     :type containers: list[str]
     :param comment: Optionally, a comment can be added to the network allocation.
@@ -72,9 +72,11 @@ def _allocate_network(
 def hostname_available(hostname: str) -> bool:
     """Check whether a hostname is still available **in Infoblox**.
 
-    Check whether Infoblox already contains a {class}`infoblox_client.objects.HostRecord` that matches the given
-    hostname. Be aware that this method only checks within the Infoblox instance, and not the rest of the internet.
-    The hostname could therefore still be taken elsewhere.
+    Check whether Infoblox already has a :class:`infoblox_client.objects.HostRecord` that matches the given hostname.
+
+    .. warning::
+       This method only checks within the Infoblox instance, and not the rest of the internet. The hostname could
+       therefore still be taken elsewhere.
 
     :param hostname: The hostname to be checked.
     :type hostname: str
@@ -86,8 +88,8 @@ def hostname_available(hostname: str) -> bool:
 def allocate_v4_network(service_type: str, comment: str | None = "") -> ipaddress.IPv4Network:
     """Allocate a new IPv4 network in Infoblox.
 
-    Allocate an IPv4 network for a specific service type. The service type should be defined in the {term}`OSS`
-    parameters of {term}`GSO`, from which the containers and netmask will be used.
+    Allocate an IPv4 network for a specific service type. The service type should be defined in the :term:`OSS`
+    parameters of :term:`GSO`, from which the containers and netmask will be used.
 
     :param service_type: The service type for which the network is allocated.
     :type service_type: str
@@ -105,8 +107,8 @@ def allocate_v4_network(service_type: str, comment: str | None = "") -> ipaddres
 def allocate_v6_network(service_type: str, comment: str | None = "") -> ipaddress.IPv6Network:
     """Allocate a new IPv6 network in Infoblox.
 
-    Allocate an IPv6 network for a specific service type. The service type should be defined in the {term}`OSS`
-    parameters of {term}`GSO`, from which the containers and netmask will be used.
+    Allocate an IPv6 network for a specific service type. The service type should be defined in the :term:`OSS`
+    parameters of :term:`GSO`, from which the containers and netmask will be used.
 
     :param service_type: The service type for which the network is allocated.
     :type service_type: str
@@ -122,9 +124,9 @@ def allocate_v6_network(service_type: str, comment: str | None = "") -> ipaddres
 
 
 def find_network_by_cidr(ip_network: ipaddress.IPv4Network | ipaddress.IPv6Network) -> objects.Network | None:
-    """Find a network in Infoblox by its {term}`CIDR`.
+    """Find a network in Infoblox by its :term:`CIDR`.
 
-    :param ip_network: The {term}`CIDR` that is searched.
+    :param ip_network: The :term:`CIDR` that is searched.
     :type ip_network: ipaddress.IPv4Network | ipaddress.IPv6Network
     """
     conn, _ = _setup_connection()
@@ -134,8 +136,8 @@ def find_network_by_cidr(ip_network: ipaddress.IPv4Network | ipaddress.IPv6Netwo
 def delete_network(ip_network: ipaddress.IPv4Network | ipaddress.IPv6Network) -> None:
     """Delete a network in Infoblox.
 
-    Delete a network that is allocated in Infoblox, by passing the {term}`CIDR` to be deleted. The {term}`CIDR` must
-    exactly match an existing entry in Infoblox, otherwise this method raises a {class}`DeletionError`
+    Delete a network that is allocated in Infoblox, by passing the :term:`CIDR` to be deleted. The :term:`CIDR` must
+    exactly match an existing entry in Infoblox, otherwise this method raises a :class:`DeletionError`
 
     :param ip_network: The network that should get deleted.
     :type ip_network: ipaddress.IPv4Network | ipaddress.IPv6Network
@@ -154,17 +156,17 @@ def allocate_host(
 
     Create a new host record in Infoblox, by providing a hostname, and the service type that is associated with this new
     host. Most likely to be a loopback interface. If the hostname is not available in Infoblox (due to a potential
-    collision) this method raises an {class}`AllocationError`.
+    collision) this method raises an :class:`AllocationError`.
 
-    :param hostname: The {term}`FQDN` of the new host
+    :param hostname: The :term:`FQDN` of the new host
     :type hostname: str
     :param service_type: The service type from which IP resources should be used.
     :type service_type: str
-    :param cname_aliases: A list of any {term}`CNAME` aliases that should be associated with this host. Most often this
+    :param cname_aliases: A list of any :term:`CNAME` aliases that should be associated with this host. Most often this
                           will be a single loopback address.
     :type cname_aliases: list[str]
     :param comment: A comment that is added to the host record in Infoblox, should be the `subscription_id` of the new
-                    {class}`Router` subscription.
+                    :class:`Router` subscription.
     :type comment: str
     """
     if not hostname_available(hostname):
@@ -226,9 +228,9 @@ def find_host_by_ip(ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> o
 
 
 def find_host_by_fqdn(fqdn: str) -> objects.HostRecord | None:
-    """Find a host record by its associated {term}`FQDN`.
+    """Find a host record by its associated :term:`FQDN`.
 
-    :param fqdn: The {term}`FQDN` of a host that is searched for.
+    :param fqdn: The :term:`FQDN` of a host that is searched for.
     :type fqdn: str
     """
     conn, _ = _setup_connection()
@@ -239,7 +241,7 @@ def delete_host_by_ip(ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address) ->
     """Delete a host from Infoblox.
 
     Delete a host record in Infoblox, by providing the IP address that is associated with the record. Raises a
-    {class}`DeletionError` if no record can be found in Infoblox.
+    :class:`DeletionError` if no record can be found in Infoblox.
 
     :param ip_addr: The IP address of the host record that should get deleted.
     :type ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address
@@ -254,10 +256,10 @@ def delete_host_by_ip(ip_addr: ipaddress.IPv4Address | ipaddress.IPv6Address) ->
 def delete_host_by_fqdn(fqdn: str) -> None:
     """Delete a host from Infoblox.
 
-    Delete a host record in Infoblox, by providing the {term}`FQDN` that is associated with the record. Raises a
-    {class}`DeletionError` if no record can be found in Infoblox.
+    Delete a host record in Infoblox, by providing the :term:`FQDN` that is associated with the record. Raises a
+    :class:`DeletionError` if no record can be found in Infoblox.
 
-    :param fqdn: The FQDN of the host record that should get deleted.
+    :param fqdn: The :term:`FQDN` of the host record that should get deleted.
     :type fqdn: str
     """
     host = find_host_by_fqdn(fqdn)
diff --git a/gso/services/netbox_client.py b/gso/services/netbox_client.py
index a66aae8692b91249a1c460b5aa5d120c74df6237..45b0906b2ad28c212b995de483810c51052b02b5 100644
--- a/gso/services/netbox_client.py
+++ b/gso/services/netbox_client.py
@@ -7,7 +7,7 @@ from pynetbox.models.dcim import Devices, DeviceTypes, Interfaces
 
 from gso.products import Router
 from gso.settings import load_oss_params
-from gso.utils.device_info import FEASIBLE_IP_TRUNK_LAG_RANGE, TierInfo
+from gso.utils.device_info import DEFAULT_SITE, FEASIBLE_IP_TRUNK_LAG_RANGE, ROUTER_ROLE, TierInfo
 from gso.utils.exceptions import NotFoundError, WorkflowStateError
 
 
@@ -45,7 +45,7 @@ class Site(pydantic.BaseModel):
     slug: str
 
 
-class NetBoxClient:
+class NetboxClient:
     """Implement all methods to communicate with the NetBox API."""
 
     def __init__(self) -> None:
@@ -67,17 +67,23 @@ class NetBoxClient:
             self.netbox.dcim.interfaces.filter(device_id=device.id, enabled=False, mark_connected=False, speed=speed)
         )
 
-    def create_interface(self, iface_name: str, type: str, speed: str, device_name: str) -> Interfaces:
+    def create_interface(
+        self, iface_name: str, type: str, device_name: str, description: str | None = None, enabled: bool = False
+    ) -> Interfaces:
         """Create new interface on a device, where device is defined by name.
 
-        The type parameter can be 1000base-t, 10gbase-t, lag...
-        For more details on type definition have  a look in
-        choices.py in the netbox API implementation in module DCIM.
+        The type parameter can be 1000base-t, 10gbase-t, lag, etc.
+        For more details on type definition have  a look in choices.py in the netbox API implementation in module DCIM.
         Returns the new interface object as dict.
         """
         device = self.get_device_by_name(device_name)
         return self.netbox.dcim.interfaces.create(
-            name=iface_name, type=type, speed=speed, enabled=False, mark_connected=False, device=device.id
+            name=iface_name,
+            type=type,
+            enabled=enabled,
+            mark_connected=False,
+            device=device.id,
+            description=description,
         )
 
     def create_device_type(self, manufacturer: str, model: str, slug: str) -> DeviceTypes:
@@ -100,6 +106,15 @@ class NetBoxClient:
         device_manufacturer = Manufacturer(**{"name": name, "slug": slug})
         return self.netbox.dcim.manufacturers.create(dict(device_manufacturer))
 
+    @staticmethod
+    def calculate_interface_speed(interface: Interfaces) -> int | None:
+        """Calculate the interface speed in bits per second."""
+
+        type_parts = interface.type.value.split("-")
+        if "gbase" in type_parts[0]:
+            return int("".join(filter(str.isdigit, type_parts[0]))) * 1000000
+        return None
+
     def create_device(self, router_name: str, site_tier: str) -> Devices:
         """Create a new device in netbox."""
 
@@ -108,10 +123,10 @@ class NetBoxClient:
         device_type = self.netbox.dcim.device_types.get(model=tier_info.device_type)
 
         # Get device role id
-        device_role = self.netbox.dcim.device_roles.get(name="router")
+        device_role = self.netbox.dcim.device_roles.get(name=ROUTER_ROLE["name"])
 
         # Get site id
-        device_site = self.netbox.dcim.sites.get(name="Amsterdam")
+        device_site = self.netbox.dcim.sites.get(name=DEFAULT_SITE["name"])
 
         # Create new device
         device = self.netbox.dcim.devices.create(
@@ -119,40 +134,54 @@ class NetBoxClient:
         )
         module_bays = list(self.netbox.dcim.module_bays.filter(device_id=device.id))
         card_type = self.netbox.dcim.module_types.get(model=tier_info.module_type)
-        for module_bay in module_bays:
+        valid_module_bays = [bay for bay in module_bays if int(bay.position) in tier_info.module_bays_slots]
+        for module_bay in valid_module_bays:
             self.netbox.dcim.modules.create(
                 device=device.id,
                 module_bay=module_bay.id,
                 module_type=card_type.id,
                 status="active",
+                enabled=False,
                 comments="Installed via pynetbox",
             )
 
+        for interface in self.netbox.dcim.interfaces.filter(device_id=device.id):
+            interface.speed = self.calculate_interface_speed(interface)
+            interface.enabled = False
+            interface.save()
+
         return device
 
     def delete_device(self, router_name: str) -> None:
         self.netbox.dcim.devices.get(name=router_name).delete()
         return
 
-    def attach_interface_to_lag(self, device_name: str, lag_name: str, iface_name: str) -> Interfaces:
-        """Assign a given interface to a lag.
+    def attach_interface_to_lag(
+        self, device_name: str, lag_name: str, iface_name: str, description: str | None = None
+    ) -> Interfaces:
+        """Assign a given interface to a LAG.
 
-        Returns the lag object with the assignend interfaces
+        Returns the interface object after assignment.
         """
         # Get device id
         device = self.get_device_by_name(device_name)
 
-        # Now get interface for device
+        # Get interface for device
         iface = self.netbox.dcim.interfaces.get(name=iface_name, device_id=device.id)
 
-        # Get lag
+        # Get LAG
         lag = self.netbox.dcim.interfaces.get(name=lag_name, device_id=device.id)
 
-        # Assign interface to lag
+        # Assign interface to LAG
         iface.lag = lag.id
 
-        # Update interface
-        return self.netbox.dcim.interfaces.update(iface)
+        # Set description if provided
+        if description:
+            iface.description = description
+
+        iface.save()
+
+        return iface
 
     def reserve_interface(self, device_name: str, iface_name: str) -> Interfaces:
         """Reserve an interface by enabling it."""
@@ -203,7 +232,7 @@ class NetBoxClient:
 
         # Get the existing lag interfaces for the device
         lag_interface_names = [
-            interface["name"] for interface in self.netbox.dcim.interfaces.filter(device_name=device.id, type="lag")
+            interface["name"] for interface in self.netbox.dcim.interfaces.filter(device=device.name, type="lag")
         ]
 
         # Generate all feasible lags
@@ -211,3 +240,21 @@ class NetBoxClient:
 
         # Return available lags not assigned to the device
         return [lag for lag in all_feasible_lags if lag not in lag_interface_names]
+
+    @staticmethod
+    def calculate_speed_bits_per_sec(speed: str) -> int:
+        """Extract the numeric part from the speed."""
+
+        numeric_part = int("".join(filter(str.isdigit, speed)))
+        # Convert to bits per second
+        return numeric_part * 1000000
+
+    def get_available_interfaces(self, router_id: UUID, speed: str) -> Interfaces:
+        """Return all available interfaces of a device filtered by speed."""
+
+        router = Router.from_subscription(router_id).router.router_fqdn
+        device = self.get_device_by_name(router)
+        speed_bps = self.calculate_speed_bits_per_sec(speed)
+        return self.netbox.dcim.interfaces.filter(
+            device=device.name, enabled=False, mark_connected=False, speed=speed_bps
+        )
diff --git a/gso/services/provisioning_proxy.py b/gso/services/provisioning_proxy.py
index f3e18eb8503b15fe8e7e1bc561548811d0bd8f4c..5a6ba11d79fedcfa8431d1dc0c8a283e23c9d73e 100644
--- a/gso/services/provisioning_proxy.py
+++ b/gso/services/provisioning_proxy.py
@@ -1,6 +1,6 @@
-"""The Provisioning Proxy service, which interacts with {term}`LSO` running externally.
+"""The Provisioning Proxy service, which interacts with :term:`LSO` running externally.
 
-{term}`LSO` is responsible for executing Ansible playbooks, that deploy subscriptions.
+:term:`LSO` is responsible for executing Ansible playbooks, that deploy subscriptions.
 """
 import json
 import logging
@@ -26,7 +26,7 @@ DEFAULT_LABEL = "Provisioning proxy is running. Please come back later for the r
 
 
 class CUDOperation(strEnum):
-    """Enumerator for different {term}`CRUD` operations that the provisioning proxy supports.
+    """Enumerator for different :term:`CRUD` operations that the provisioning proxy supports.
 
     Read is not applicable, hence the missing R.
     """
@@ -37,9 +37,9 @@ class CUDOperation(strEnum):
 
 
 def _send_request(endpoint: str, parameters: dict, process_id: UUIDstr, operation: CUDOperation) -> None:
-    """Send a request to {term}`LSO`. The callback address is derived using the process ID provided.
+    """Send a request to :term:`LSO`. The callback address is derived using the process ID provided.
 
-    :param endpoint: The {term}`LSO`-specific endpoint to call, depending on the type of service object that's acted
+    :param endpoint: The :term:`LSO`-specific endpoint to call, depending on the type of service object that's acted
         upon.
     :type endpoint: str
     :param parameters: JSON body for the request, which will almost always at least consist of a subscription object,
@@ -49,7 +49,7 @@ def _send_request(endpoint: str, parameters: dict, process_id: UUIDstr, operatio
         playbook is completed.
     :type process_id: UUIDstr
     :param operation: The specific operation that's performed with the request.
-    :type operation: {class}`CUDOperation`
+    :type operation: :class:`CUDOperation`
     :rtype: None
     """
     oss = settings.load_oss_params()
@@ -82,10 +82,10 @@ def _send_request(endpoint: str, parameters: dict, process_id: UUIDstr, operatio
 def provision_router(
     subscription: RouterProvisioning, process_id: UUIDstr, tt_number: str, dry_run: bool = True
 ) -> None:
-    """Provision a new router using {term}`LSO`.
+    """Provision a new router using :term:`LSO`.
 
     :param subscription: The subscription object that's to be provisioned.
-    :type subscription: {class}`RouterProvisioning`
+    :type subscription: :class:`RouterProvisioning`
     :param process_id: The related process ID, used for callback.
     :type process_id: UUIDstr
     :param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`.
@@ -105,10 +105,10 @@ def provision_router(
 def provision_ip_trunk(
     subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str, config_object: str, dry_run: bool = True
 ) -> None:
-    """Provision an IP trunk service using {term}`LSO`.
+    """Provision an IP trunk service using :term:`LSO`.
 
     :param subscription: The subscription object that's to be provisioned.
-    :type subscription: {class}`IptrunkProvisioning`
+    :type subscription: :class:`IptrunkProvisioning`
     :param process_id: The related process ID, used for callback.
     :type process_id: UUIDstr
     :param config_object: The type of object that's deployed.
@@ -130,10 +130,10 @@ def provision_ip_trunk(
 
 
 def check_ip_trunk(subscription: IptrunkProvisioning, process_id: UUIDstr, tt_number: str, check_name: str) -> None:
-    """Provision an IP trunk service using {term}`LSO`.
+    """Provision an IP trunk service using :term:`LSO`.
 
     :param subscription: The subscription object that's to be provisioned.
-    :type subscription: {class}`IptrunkProvisioning`
+    :type subscription: :class:`IptrunkProvisioning`
     :param process_id: The related process ID, used for callback.
     :type process_id: UUIDstr
     :param check_name: The name of the check to execute
@@ -150,10 +150,10 @@ def check_ip_trunk(subscription: IptrunkProvisioning, process_id: UUIDstr, tt_nu
 
 
 def deprovision_ip_trunk(subscription: Iptrunk, process_id: UUIDstr, tt_number: str, dry_run: bool = True) -> None:
-    """Deprovision an IP trunk service using {term}`LSO`.
+    """Deprovision an IP trunk service using :term:`LSO`.
 
     :param subscription: The subscription object that's to be provisioned.
-    :type subscription: {class}`IptrunkProvisioning`
+    :type subscription: :class:`IptrunkProvisioning`
     :param process_id: The related process ID, used for callback.
     :type process_id: UUIDstr
     :param dry_run: A boolean indicating whether this should be a dry run or not, defaults to `True`.
@@ -183,15 +183,15 @@ def migrate_ip_trunk(
     config_object: str,
     dry_run: bool = True,
 ) -> None:
-    """Migrate an IP trunk service using {term}`LSO`.
+    """Migrate an IP trunk service using :term:`LSO`.
 
     :param subscription: The subscription object that's to be migrated.
-    :type subscription: {class}`Iptrunk`
+    :type subscription: :class:`Iptrunk`
     :param new_node: The new node that is being migrated to
-    :type new_node: {class}`Router`
+    :type new_node: :class:`Router`
     :param new_lag_interface: The name of the new aggregated Ethernet interface
     :type new_lag_interface: str
-    :param new_lag_member_interfaces: The new list of interfaces that are part of the {term}`LAG`
+    :param new_lag_member_interfaces: The new list of interfaces that are part of the :term:`LAG`
     :type new_lag_member_interfaces: list[str]
     :param replace_index: The index of the side that is going to be replaced as part of the existing trunk,
                           can be `0` or `1`.
@@ -233,13 +233,13 @@ def _await_pp_results(subscription: SubscriptionModel, label_text: str = DEFAULT
     output of the provisioning proxy.
 
     :param subscription: The current subscription that the provisioning proxy is acting on.
-    :type subscription: {class}`orchestrator.domain.SubscriptionModel`
+    :type subscription: :class:`orchestrator.domain.SubscriptionModel`
     :param label_text: A label that's displayed to the operator when the provisioning proxy has not returned its
         results yet. Defaults to `DEFAULT_LABEL`.
     :type label_text: str
     :return: The input that's given by the provisioning proxy, that should contain run results, and a `confirm`
         boolean set to `True`.
-    :rtype: {class}`orchestrator.types.FormGenerator`
+    :rtype: :class:`orchestrator.types.FormGenerator`
     """
 
     class ProvisioningResultPage(FormPage):
@@ -266,7 +266,7 @@ def _reset_pp_success_state() -> State:
     """Reset the boolean that indicates a successful provisioning proxy result in the state of a running workflow.
 
     :return: A new state of the workflow, where the key `pp_did_succeed` has been (re)set to false.
-    :rtype: {class}`orchestrator.types.State`
+    :rtype: :class:`orchestrator.types.State`
     """
     return {"pp_did_succeed": False}
 
@@ -280,14 +280,20 @@ def _confirm_pp_results(state: State) -> FormGenerator:
     execution will be retried. This will happen up to two times, after which the workflow will fail.
 
     :param state: The current state of the workflow.
-    :type state: {class}`orchestrator.types.State`
+    :type state: :class:`orchestrator.types.State`
     :return: Confirmation from the user, when presented with the run results.
-    :rtype: {class}`orchestrator.types.FormGenerator`
+    :rtype: :class:`orchestrator.types.FormGenerator`
     """
     if "pp_run_results" not in state:
         # FIXME: dirty hack that makes the skipping """work"""
         return {"pp_did_succeed": True}
 
+    class ContinueForm(FormPage):
+        class Config:
+            title = "Continue to see the result?"
+
+    yield ContinueForm
+
     successful_run = state["pp_run_results"]["return_code"] == 0
 
     class ConfirmRunPage(FormPage):
@@ -322,7 +328,7 @@ def pp_interaction(provisioning_step: Step, attempts: int, abort_on_failure: boo
         - The input step that suspends the workflow, and will wait for results from the provisioning proxy.
         - An input step that presents the user with the results, where they must be confirmed.
 
-    All these steps are wrapped in a {class}`orchestrator.workflow.conditional`. This ensures that when provisioning was
+    All these steps are wrapped in a :class:`orchestrator.workflow.conditional`. This ensures that when provisioning was
     already successful, these steps are skipped. This mechanism is quite a dirty hack, and it's planned to be addressed
     in a later release.
 
@@ -330,13 +336,13 @@ def pp_interaction(provisioning_step: Step, attempts: int, abort_on_failure: boo
     it's still not successful, the workflow will be aborted if so indicated with the `abort_on_failure` boolean.
 
     :param provisioning_step: The step that executes an interaction with the provisioning proxy.
-    :type provisioning_step: {class}`orchestrator.workflow.Step`
+    :type provisioning_step: :class:`orchestrator.workflow.Step`
     :param attempts: The maximum amount of times that a provisioning can be retried.
     :type attempts: int
     :param abort_on_failure: A boolean value that indicates whether a workflow should abort if the provisioning has
                              failed the maximum amount of tries. Defaults to `True`.
     :return: A list of three steps that form one interaction with the provisioning proxy.
-    :rtype: {class}`orchestrator.workflow.StepList`
+    :rtype: :class:`orchestrator.workflow.StepList`
     """
     should_retry_pp_steps = conditional(lambda state: not state.get("pp_did_succeed"))
 
diff --git a/gso/services/resource_manager.py b/gso/services/resource_manager.py
deleted file mode 100644
index 88ef5e189e29382fca6b92a258d5f62de0cf6d72..0000000000000000000000000000000000000000
--- a/gso/services/resource_manager.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# mypy: ignore-errors
-from enum import Enum, auto
-from typing import List
-
-from gso import settings
-
-# TODO
-# - fill in the implementations
-# - consider the extra API methods
-# - decided what to do with various error conditions (currently assertions)
-
-
-class InterfaceAllocationState(Enum):
-    AVAILABLE = auto()
-    RESERVED = auto()
-    ALLOCATED = auto()
-
-
-def _dummy_router_interfaces():
-    return {
-        "lags": [],
-        "physical": [{"name": f"ifc-{x}", "state": InterfaceAllocationState.AVAILABLE} for x in range(250)],
-    }
-
-
-_DUMMY_INVENTORY = {
-    "fqdn-a": _dummy_router_interfaces(),
-    "fqdn-b": _dummy_router_interfaces(),
-    "fqdn-c": _dummy_router_interfaces(),
-    "fqdn-d": _dummy_router_interfaces(),
-}
-
-
-def import_new_router(new_router_fqdn: str, subscription_id: str, oss_params=settings.OSSParams):
-    # TODO: this is a dummy implementation
-
-    # TODO: specifiy if this should be an error (and if now, what it means)
-    assert new_router_fqdn not in _DUMMY_INVENTORY
-    _DUMMY_INVENTORY[new_router_fqdn] = _dummy_router_interfaces()
-
-
-def next_lag(router_fqdn: str, subscription_id: str, oss_params=settings.OSSParams) -> str:
-    # TODO: this is a dummy implementation
-
-    assert router_fqdn in _DUMMY_INVENTORY
-
-    lag_idx = 0
-    while True:
-        lag_name = f"ae-{lag_idx}"
-        if lag_name not in _DUMMY_INVENTORY[router_fqdn]["lags"]:
-            _DUMMY_INVENTORY[router_fqdn]["lags"].append(lag_name)
-            return lag_name
-        lag_idx += 1
-
-
-def available_physical_interfaces(router_fqdn: str, oss_params=settings.OSSParams) -> List[str]:
-    # TODO: this is a dummy implementation
-
-    assert router_fqdn in _DUMMY_INVENTORY
-
-    return [
-        ifc["name"]
-        for ifc in _DUMMY_INVENTORY[router_fqdn]["physical"]
-        if ifc["state"] == InterfaceAllocationState.AVAILABLE
-    ]
-
-
-def _find_physical(router_fqdn: str, interface_name: str) -> dict:
-    assert router_fqdn in _DUMMY_INVENTORY
-    for ifc in _DUMMY_INVENTORY[router_fqdn]["physical"]:
-        if ifc["name"] == interface_name:
-            return ifc
-    raise AssertionError(f"interface {interface_name} not found on {router_fqdn}")
-
-
-def reserve_physical_interface(
-    router_fqdn: str, interface_name: str, subscription_id: str, oss_params=settings.OSSParams
-):
-    # TODO: this is a dummy implementation
-
-    ifc = _find_physical(router_fqdn, interface_name)
-    assert (
-        ifc["state"] == InterfaceAllocationState.AVAILABLE
-    ), f"interface {router_fqdn}:{interface_name} is not available"
-    ifc["state"] = InterfaceAllocationState.RESERVED
-
-
-def allocate_physical_interface(router_fqdn: str, interface_name: str, oss_params=settings.OSSParams):
-    # TODO: this is a dummy implementation
-
-    ifc = _find_physical(router_fqdn, interface_name)
-    # TODO: is there a use case for moving
-    #  directly from AVAILABLE to ALLOCATED?
-    assert (
-        ifc["state"] == InterfaceAllocationState.RESERVED
-    ), f"interface {router_fqdn}:{interface_name} is not reserved"
-    ifc["state"] = InterfaceAllocationState.RESERVED
-
-
-def free_physical_interface(router_fqdn: str, interface_name: str, oss_params=settings.OSSParams):
-    # TODO: this is a dummy implementation
-
-    ifc = _find_physical(router_fqdn, interface_name)
-    # TODO: is this truly an error that should be handled?
-    # or is it ok to ignore this?
-    assert (
-        ifc["state"] != InterfaceAllocationState.AVAILABLE
-    ), f"interface {router_fqdn}:{interface_name} is already available"
-    ifc["state"] = InterfaceAllocationState.AVAILABLE
-
-
-def free_lag(router_fqdn: str, lag_name: str, oss_params=settings.OSSParams):
-    # TODO: is this a use case that should be handled?
-    # e.g. who keeps track of the bundled physical interfaces?
-    pass
-
-
-def remove_router(router_fqdn: str, oss_params=settings.OSSParams):
-    # TODO: is this a use case that should be handled?
-    pass
-
-
-def all_lags(router_fqdn: str, oss_params=settings.OSSParams) -> List[str]:
-    # TODO: is this a use case that should be handled?
-    assert router_fqdn in _DUMMY_INVENTORY
-    return _DUMMY_INVENTORY[router_fqdn]["lags"]
diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py
index 97c709647f04f35338147cbbc7ae09774c03442c..5e234813f34dba4818da1e7504e0d33a023070ab 100644
--- a/gso/services/subscriptions.py
+++ b/gso/services/subscriptions.py
@@ -18,14 +18,13 @@ def get_active_subscriptions(
 ) -> list[Subscription]:
     """Retrieve active subscriptions for a specific product type.
 
-    Args:
-    ----
-    product_type (str): The type of the product for which to retrieve subscriptions.
-    fields (list[str]): List of fields to be included in the returned Subscription objects.
-
-    Returns:
-    -------
-    list[Subscription]: A list of Subscription objects that match the query.
+    :param product_type: The type of the product for which to retrieve subscriptions.
+    :type product_type: str
+    :param fields: List of fields to be included in the returned Subscription objects.
+    :type fields: list[str]
+
+    :return: A list of Subscription objects that match the query.
+    :rtype: list[Subscription]
     """
     dynamic_fields = [getattr(SubscriptionTable, field) for field in fields]
 
@@ -43,13 +42,11 @@ def get_active_subscriptions(
 def get_active_site_subscriptions(fields: list[str]) -> list[Subscription]:
     """Retrieve active subscriptions specifically for sites.
 
-    Args:
-    ----
-    fields (list[str]): The fields to be included in the returned Subscription objects.
+    :param fields: The fields to be included in the returned Subscription objects.
+    :type fields: list[str]
 
-    Returns:
-    -------
-    list[Subscription]: A list of Subscription objects for sites.
+    :return: A list of Subscription objects for sites.
+    :rtype: list[Subscription]
     """
     return get_active_subscriptions(ProductType.SITE, fields)
 
@@ -57,27 +54,23 @@ def get_active_site_subscriptions(fields: list[str]) -> list[Subscription]:
 def get_active_router_subscriptions(fields: list[str]) -> list[Subscription]:
     """Retrieve active subscriptions specifically for routers.
 
-    Args:
-    ----
-    fields (list[str]): The fields to be included in the returned Subscription objects.
+    :param fields: The fields to be included in the returned Subscription objects.
+    :type fields: list[str]
 
-    Returns:
-    -------
-    list[Subscription]: A list of Subscription objects for routers.
+    :return: A list of Subscription objects for routers.
+    :rtype: list[Subscription]
     """
     return get_active_subscriptions(product_type=ProductType.ROUTER, fields=fields)
 
 
 def get_product_id_by_name(product_name: ProductType) -> UUID:
-    """Retrieve the {term}`UUID` of a product by its name.
+    """Retrieve the :term:`UUID` of a product by its name.
 
-    Args:
-    ----
-    product_name (ProductType): The name of the product.
+    :param product_name: The name of the product.
+    :type product_name: ProductType
 
-    Returns:
-    -------
-    {term}`UUID`: The {term}`UUID` of the product.
+    :return UUID: The :term:`UUID` of the product.
+    :rtype: UUID
     """
     return ProductTable.query.filter_by(name=product_name).first().product_id
 
@@ -85,13 +78,11 @@ def get_product_id_by_name(product_name: ProductType) -> UUID:
 def get_active_site_subscription_by_name(site_name: str) -> Subscription:
     """Retrieve an active subscription for a site by the site's name.
 
-    Args:
-    ----
-    site_name (str): The name of the site for which to retrieve the subscription.
+    :param site_name: The name of the site for which to retrieve the subscription.
+    :type site_name: str
 
-    Returns:
-    -------
-    Subscription: The Subscription object for the site.
+    :return: The Subscription object for the site.
+    :rtype: Subscription
     """
     return (
         SubscriptionTable.query.join(ProductTable)
diff --git a/gso/settings.py b/gso/settings.py
index f6ecc5e08e5cf9e45485b1931b2719661d56fc36..701f65fddc74029e2fba309d76f5ec275fd1d9e8 100644
--- a/gso/settings.py
+++ b/gso/settings.py
@@ -1,4 +1,4 @@
-"""{term}`GSO` settings.
+""":term:`GSO` settings.
 
 Ensuring that the required parameters are set correctly.
 """
@@ -14,10 +14,10 @@ logger = logging.getLogger(__name__)
 
 
 class GeneralParams(BaseSettings):
-    """General parameters for a {term}`GSO` configuration file."""
+    """General parameters for a :term:`GSO` configuration file."""
 
     public_hostname: str
-    """The hostname that {term}`GSO` is publicly served at, used for building the callback URL that the provisioning
+    """The hostname that :term:`GSO` is publicly served at, used for building the callback URL that the provisioning
     proxy uses."""
 
 
@@ -60,7 +60,7 @@ class ServiceNetworkParams(BaseSettings):
 
 
 class IPAMParams(BaseSettings):
-    """A set of parameters related to {term}`IPAM`."""
+    """A set of parameters related to :term:`IPAM`."""
 
     INFOBLOX: InfoBloxParams
     LO: ServiceNetworkParams
@@ -87,7 +87,7 @@ class NetBoxParams(BaseSettings):
 
 
 class OSSParams(BaseSettings):
-    """The set of parameters required for running {term}`GSO`."""
+    """The set of parameters required for running :term:`GSO`."""
 
     GENERAL: GeneralParams
     IPAM: IPAMParams
diff --git a/gso/utils/device_info.py b/gso/utils/device_info.py
index 1c193557f8c21515103b210517f4c1e5ab58a597..5a139889229efd80918e45079c805f18461e9dbb 100644
--- a/gso/utils/device_info.py
+++ b/gso/utils/device_info.py
@@ -12,16 +12,16 @@ class ModuleInfo(BaseModel):
 class TierInfo:
     def __init__(self) -> None:
         self.Tier1 = ModuleInfo(
-            device_type="7750-SR7s",
+            device_type="7750 SR-7s",
             module_bays_slots=[1, 2],
-            module_type="XCM2s-XMA2s-36p-800g",
+            module_type="XMA2-s-36p-400g",
             breakout_interfaces_per_slot=[36, 35, 34, 33],
             total_10g_interfaces=80,
         )
         self.Tier2 = ModuleInfo(
             device_type="7750-SR7s",
             module_bays_slots=[1, 2],
-            module_type="XCM2s-XMA2s-36p-400g",
+            module_type="XMA2-s-36p-400g",
             breakout_interfaces_per_slot=[36, 35, 34, 33],
             total_10g_interfaces=60,
         )
@@ -30,4 +30,9 @@ class TierInfo:
         return getattr(self, name)
 
 
-FEASIBLE_IP_TRUNK_LAG_RANGE = range(1, 10)
+# The range includes values from 1 to 10 (11 is not included)
+FEASIBLE_IP_TRUNK_LAG_RANGE = range(1, 11)
+
+# Define default values
+ROUTER_ROLE = {"name": "router", "slug": "router"}
+DEFAULT_SITE = {"name": "GEANT", "slug": "geant"}
diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py
index 32ffca79cc43d7e566515c88ee31206f029ac5f4..65a84b5d64e63f09074d2da0e7cd9660322e92ae 100644
--- a/gso/workflows/__init__.py
+++ b/gso/workflows/__init__.py
@@ -1,4 +1,4 @@
-"""Initialisation class that imports all workflows into {term}`GSO`."""
+"""Initialisation class that imports all workflows into :term:`GSO`."""
 from orchestrator.workflows import LazyWorkflowInstance
 
 LazyWorkflowInstance("gso.workflows.iptrunk.create_iptrunk", "create_iptrunk")
diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py
index ae2c8cf9301e1e634d3695c8b5078c834705970d..b08da1292fc6f34459a8af0419cc14d21c251eb6 100644
--- a/gso/workflows/iptrunk/create_iptrunk.py
+++ b/gso/workflows/iptrunk/create_iptrunk.py
@@ -1,18 +1,28 @@
 from orchestrator.forms import FormPage
-from orchestrator.forms.validators import Choice, UniqueConstrainedList
+from orchestrator.forms.validators import Choice, ChoiceList, UniqueConstrainedList
 from orchestrator.targets import Target
 from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
 from orchestrator.workflow import StepList, done, init, step, workflow
 from orchestrator.workflows.steps import resync, set_status, store_process_subscription
 from orchestrator.workflows.utils import wrap_create_initial_input_form
+from pydantic import validator
+from pynetbox.models.dcim import Interfaces
 
 from gso.products.product_blocks import PhyPortCapacity
 from gso.products.product_blocks.iptrunk import IptrunkType
+from gso.products.product_blocks.router import RouterVendor
 from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
 from gso.products.product_types.router import Router
 from gso.services import infoblox, provisioning_proxy, subscriptions
+from gso.services.netbox_client import NetboxClient
 from gso.services.provisioning_proxy import pp_interaction
-from gso.workflows.utils import customer_selector
+from gso.workflows.utils import (
+    available_interfaces_choices,
+    available_lags_choices,
+    customer_selector,
+    get_router_vendor,
+    validate_router_in_netbox,
+)
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:
@@ -39,28 +49,73 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
     initial_user_input = yield CreateIptrunkForm
 
-    class AeMembersListA(UniqueConstrainedList[str]):
+    router_enum_a = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore
+
+    class SelectRouterSideA(FormPage):
+        class Config:
+            title = "Select a router for side A of the trunk."
+
+        iptrunk_sideA_node_id: router_enum_a  # type: ignore[valid-type]
+
+        @validator("iptrunk_sideA_node_id", allow_reuse=True)
+        def validate_device_exists_in_netbox(cls, iptrunk_sideA_node_id: UUIDstr) -> str | None:
+            return validate_router_in_netbox(iptrunk_sideA_node_id)
+
+    user_input_router_side_a = yield SelectRouterSideA
+    router_a = user_input_router_side_a.iptrunk_sideA_node_id.name
+    side_a_ae_iface = available_lags_choices(router_a) or str
+
+    class AeMembersListA(ChoiceList):
+        min_items = initial_user_input.iptrunk_minimum_links
+        item_type = available_interfaces_choices(router_a, initial_user_input.iptrunk_speed)  # type: ignore
+        unique_items = True
+
+    class JuniperAeMembers(UniqueConstrainedList[str]):
         min_items = initial_user_input.iptrunk_minimum_links
+        unique_items = True
 
-    RouterEnumA = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore[arg-type]
+    ae_members_side_a = AeMembersListA if get_router_vendor(router_a) == RouterVendor.NOKIA else JuniperAeMembers
+
+    class AeMembersDescriptionListA(UniqueConstrainedList[str]):
+        min_items = initial_user_input.iptrunk_minimum_links
 
     class CreateIptrunkSideAForm(FormPage):
         class Config:
             title = "Provide subscription details for side A of the trunk."
 
-        iptrunk_sideA_node_id: RouterEnumA  # type: ignore[valid-type]
-        iptrunk_sideA_ae_iface: str
+        iptrunk_sideA_ae_iface: side_a_ae_iface  # type: ignore[valid-type]
         iptrunk_sideA_ae_geant_a_sid: str
-        iptrunk_sideA_ae_members: AeMembersListA
-        iptrunk_sideA_ae_members_descriptions: AeMembersListA
+        iptrunk_sideA_ae_members: ae_members_side_a  # type: ignore[valid-type]
+        iptrunk_sideA_ae_members_descriptions: AeMembersDescriptionListA
 
     user_input_side_a = yield CreateIptrunkSideAForm
-
     # Remove the selected router for side A, to prevent any loops
-    routers.pop(str(user_input_side_a.iptrunk_sideA_node_id.name))
-    RouterEnumB = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore[arg-type]
+    routers.pop(str(user_input_router_side_a.iptrunk_sideA_node_id.name))
+    router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore
+
+    class SelectRouterSideB(FormPage):
+        class Config:
+            title = "Select a router for side B of the trunk."
+
+        iptrunk_sideB_node_id: router_enum_b  # type: ignore[valid-type]
+
+        @validator("iptrunk_sideB_node_id", allow_reuse=True)
+        def validate_device_exists_in_netbox(cls, iptrunk_sideB_node_id: UUIDstr) -> str | None:
+            return validate_router_in_netbox(iptrunk_sideB_node_id)
+
+    user_input_router_side_b = yield SelectRouterSideB
+    router_b = user_input_router_side_b.iptrunk_sideB_node_id.name
+    side_b_ae_iface = available_lags_choices(router_b) or str
 
-    class AeMembersListB(UniqueConstrainedList[str]):
+    class AeMembersListB(ChoiceList):
+        min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
+        max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
+        item_type = available_interfaces_choices(router_b, initial_user_input.iptrunk_speed)  # type: ignore
+        unique_items = True
+
+    ae_members_side_b = AeMembersListB if get_router_vendor(router_b) == RouterVendor.NOKIA else JuniperAeMembers
+
+    class AeMembersDescriptionListB(UniqueConstrainedList[str]):
         min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
         max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
 
@@ -68,15 +123,20 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         class Config:
             title = "Provide subscription details for side B of the trunk."
 
-        iptrunk_sideB_node_id: RouterEnumB  # type: ignore[valid-type]
-        iptrunk_sideB_ae_iface: str
+        iptrunk_sideB_ae_iface: side_b_ae_iface  # type: ignore[valid-type]
         iptrunk_sideB_ae_geant_a_sid: str
-        iptrunk_sideB_ae_members: AeMembersListB
-        iptrunk_sideB_ae_members_descriptions: AeMembersListB
+        iptrunk_sideB_ae_members: ae_members_side_b  # type: ignore[valid-type]
+        iptrunk_sideB_ae_members_descriptions: AeMembersDescriptionListB
 
     user_input_side_b = yield CreateIptrunkSideBForm
 
-    return initial_user_input.dict() | user_input_side_a.dict() | user_input_side_b.dict()
+    return (
+        initial_user_input.dict()
+        | user_input_router_side_a.dict()
+        | user_input_side_a.dict()
+        | user_input_router_side_b.dict()
+        | user_input_side_b.dict()
+    )
 
 
 @step("Create subscription")
@@ -161,7 +221,7 @@ def provision_ip_trunk_iface_real(subscription: IptrunkProvisioning, process_id:
 
     return {
         "subscription": subscription,
-        "label_text": "[COMMIT] Provisioning a trunk interface, please refresh to get the results of the playbook.",
+        "label_text": "Provisioning a trunk interface, please refresh to get the results of the playbook.",
     }
 
 
@@ -205,6 +265,57 @@ def check_ip_trunk_isis(subscription: IptrunkProvisioning, process_id: UUIDstr,
     }
 
 
+@step("NextBox integration")
+def reserve_interfaces_in_netbox(subscription: IptrunkProvisioning) -> State:
+    """Create the LAG interfaces in NetBox and attach the lag interfaces to the physical interfaces."""
+
+    nbclient = NetboxClient()
+    for side in range(0, 2):
+        if subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_vendor == RouterVendor.NOKIA:
+            # Create LAG interfaces
+            lag_interface: Interfaces = nbclient.create_interface(
+                iface_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_iface,
+                type="lag",
+                device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
+                description=str(subscription.subscription_id),
+                enabled=True,
+            )
+            # Attach physical interfaces to LAG
+            # Update interface description to subscription ID
+            # Reserve interfaces
+            for interface in subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_members:
+                nbclient.attach_interface_to_lag(
+                    device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
+                    lag_name=lag_interface.name,
+                    iface_name=interface,
+                    description=str(subscription.subscription_id),
+                )
+                nbclient.reserve_interface(
+                    device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
+                    iface_name=interface,
+                )
+    return {
+        "subscription": subscription,
+        "label_text": "NextBox integration: Reserved interfaces.",
+    }
+
+
+@step("Allocate interfaces in Netbox")
+def allocate_interfaces_in_netbox(subscription: IptrunkProvisioning) -> State:
+    """Allocate the LAG interfaces in NetBox and attach the lag interfaces to the physical interfaces."""
+    for side in range(0, 2):
+        if subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_vendor == RouterVendor.NOKIA:
+            for interface in subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_members:
+                NetboxClient().allocate_interface(
+                    device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
+                    iface_name=interface,
+                )
+    return {
+        "subscription": subscription,
+        "label_text": "NextBox integration: Allocated interfaces.",
+    }
+
+
 @workflow(
     "Create IP trunk",
     initial_input_form=wrap_create_initial_input_form(initial_input_form_generator),
@@ -217,12 +328,14 @@ def create_iptrunk() -> StepList:
         >> store_process_subscription(Target.CREATE)
         >> initialize_subscription
         >> get_info_from_ipam
+        >> reserve_interfaces_in_netbox
         >> pp_interaction(provision_ip_trunk_iface_dry, 3)
         >> pp_interaction(provision_ip_trunk_iface_real, 3)
         >> pp_interaction(check_ip_trunk_connectivity, 2, False)
         >> pp_interaction(provision_ip_trunk_isis_iface_dry, 3)
         >> pp_interaction(provision_ip_trunk_isis_iface_real, 3)
         >> pp_interaction(check_ip_trunk_isis, 2, False)
+        >> allocate_interfaces_in_netbox
         >> set_status(SubscriptionLifecycle.ACTIVE)
         >> resync
         >> done
diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py
index b492606b5641274d94f26bb90ef88f9152af9ceb..3b2c1b4d160dcfa3a51815c6b1b898f7e38e4dfb 100644
--- a/gso/workflows/iptrunk/migrate_iptrunk.py
+++ b/gso/workflows/iptrunk/migrate_iptrunk.py
@@ -1,6 +1,6 @@
 import re
 from logging import getLogger
-from typing import NoReturn, Optional
+from typing import NoReturn
 
 from orchestrator import step, workflow
 from orchestrator.config.assignee import Assignee
@@ -47,7 +47,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
         tt_number: str
         replace_side: ReplacedSide  # type: ignore[valid-type]
         warning_label: Label = "Are we moving to a different Site?"  # type: ignore[assignment]
-        migrate_to_different_site: Optional[bool] = False
+        migrate_to_different_site: bool | None = False
 
     old_side_input = yield OldSideIptrunkForm
 
@@ -285,19 +285,6 @@ def deploy_new_isis(
     }
 
 
-# Leaving it like this as a placeholder
-# @step("Check ISIS metric")
-# def check_isis(subscription: Iptrunk, process_id: UUIDstr) -> State:
-#    provisioning_proxy.check_ip_trunk(subscription, process_id, "VERB NEEDS TO BE UPDATED")
-#
-#    logger.warning("Playbook verb is not yet properly set.")
-#
-#    return {
-#        "subscription": subscription,
-#        "label_text": "Checking ISIS functionality, please refresh to get the results of the playbook.",
-#    }
-
-
 @inputstep("Wait for confirmation", assignee=Assignee.SYSTEM)
 def confirm_continue_restore_isis() -> FormGenerator:
     class ProvisioningResultPage(FormPage):
diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py
index 2d28c06e64879a28f3a2d031a42e7a3771705675..91e7e82a3355af50e3159a1e9dc4fa524db83aff 100644
--- a/gso/workflows/router/create_router.py
+++ b/gso/workflows/router/create_router.py
@@ -16,7 +16,7 @@ from gso.products.product_types.router import RouterInactive, RouterProvisioning
 from gso.products.product_types.site import Site
 from gso.products.shared import PortNumber
 from gso.services import infoblox, provisioning_proxy, subscriptions
-from gso.services.netbox_client import NetBoxClient
+from gso.services.netbox_client import NetboxClient
 from gso.services.provisioning_proxy import pp_interaction
 from gso.workflows.utils import customer_selector, iso_from_ipv4
 
@@ -157,11 +157,12 @@ def provision_router_real(subscription: RouterProvisioning, process_id: UUIDstr,
 
 @step("Create NetBox Device")
 def create_netbox_device(subscription: RouterProvisioning) -> State:
-    NetBoxClient().create_device(
-        subscription.router.router_fqdn, subscription.router.router_site.site_tier  # type: ignore
-    )
-
-    return {"subscription": subscription}
+    if subscription.router.router_vendor == RouterVendor.NOKIA:
+        NetboxClient().create_device(
+            subscription.router.router_fqdn, subscription.router.router_site.site_tier  # type: ignore
+        )
+        return {"subscription": subscription, "label_text": "Creating NetBox device"}
+    return {"subscription": subscription, "label_text": "Skipping NetBox device creation for Juniper router."}
 
 
 @step("Verify IPAM resources for loopback interface")
diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py
index b02f90cae1f328e148440ffc3f146d29cb1aa198..4989823fc1d94ddd883654ae187753abca493f0a 100644
--- a/gso/workflows/router/terminate_router.py
+++ b/gso/workflows/router/terminate_router.py
@@ -9,9 +9,10 @@ from orchestrator.workflow import StepList, conditional, done, init, step, workf
 from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
+from gso.products.product_blocks.router import RouterVendor
 from gso.products.product_types.router import Router
 from gso.services import infoblox
-from gso.services.netbox_client import NetBoxClient
+from gso.services.netbox_client import NetboxClient
 
 logger = logging.getLogger(__name__)
 
@@ -61,7 +62,8 @@ def remove_config_from_router() -> None:
 
 @step("Remove Device from NetBox")
 def remove_device_from_netbox(subscription: Router) -> dict[str, Router]:
-    NetBoxClient().delete_device(subscription.router.router_fqdn)
+    if subscription.router.router_vendor == RouterVendor.NOKIA:
+        NetboxClient().delete_device(subscription.router.router_fqdn)
     return {"subscription": subscription}
 
 
diff --git a/gso/workflows/tasks/import_router.py b/gso/workflows/tasks/import_router.py
index 4d157709cb8f1b3b2e9a391e27af0f8278ad6c93..0821e31414c822197cb419101562a4d74a5afaec 100644
--- a/gso/workflows/tasks/import_router.py
+++ b/gso/workflows/tasks/import_router.py
@@ -1,5 +1,4 @@
 import ipaddress
-from typing import Optional
 from uuid import UUID
 
 from orchestrator import workflow
@@ -61,9 +60,9 @@ def initial_input_form_generator() -> FormGenerator:
         router_lo_ipv4_address: ipaddress.IPv4Address
         router_lo_ipv6_address: ipaddress.IPv6Address
         router_lo_iso_address: str
-        router_si_ipv4_network: Optional[ipaddress.IPv4Network] = None
-        router_ias_lt_ipv4_network: Optional[ipaddress.IPv4Network] = None
-        router_ias_lt_ipv6_network: Optional[ipaddress.IPv6Network] = None
+        router_si_ipv4_network: ipaddress.IPv4Network | None = None
+        router_ias_lt_ipv4_network: ipaddress.IPv4Network | None = None
+        router_ias_lt_ipv6_network: ipaddress.IPv6Network | None = None
 
     user_input = yield ImportRouter
 
@@ -78,13 +77,13 @@ def initialize_subscription(
     router_vendor: router_pb.RouterVendor,
     router_site: str,
     router_role: router_pb.RouterRole,
-    is_ias_connected: Optional[bool] = None,
-    router_lo_ipv4_address: Optional[ipaddress.IPv4Address] = None,
-    router_lo_ipv6_address: Optional[ipaddress.IPv6Address] = None,
-    router_lo_iso_address: Optional[str] = None,
-    router_si_ipv4_network: Optional[ipaddress.IPv4Network] = None,
-    router_ias_lt_ipv4_network: Optional[ipaddress.IPv4Network] = None,
-    router_ias_lt_ipv6_network: Optional[ipaddress.IPv6Network] = None,
+    is_ias_connected: bool | None = None,
+    router_lo_ipv4_address: ipaddress.IPv4Address | None = None,
+    router_lo_ipv6_address: ipaddress.IPv6Address | None = None,
+    router_lo_iso_address: str | None = None,
+    router_si_ipv4_network: ipaddress.IPv4Network | None = None,
+    router_ias_lt_ipv4_network: ipaddress.IPv4Network | None = None,
+    router_ias_lt_ipv6_network: ipaddress.IPv6Network | None = None,
 ) -> State:
     subscription.router.router_ts_port = ts_port
     subscription.router.router_vendor = router_vendor
diff --git a/gso/workflows/utils.py b/gso/workflows/utils.py
index 02ebed3eaf5a12f55027063b30728ac4a8336ca1..cd461b81131917b704fcc6b7875f8a2ed7264470 100644
--- a/gso/workflows/utils.py
+++ b/gso/workflows/utils.py
@@ -1,9 +1,14 @@
 import re
 from ipaddress import IPv4Address
+from uuid import UUID
 
 from orchestrator.forms.validators import Choice
+from orchestrator.types import UUIDstr
 
+from gso.products.product_blocks.router import RouterVendor
+from gso.products.product_types.router import Router
 from gso.services.crm import all_customers
+from gso.services.netbox_client import NetboxClient
 
 
 def customer_selector() -> Choice:
@@ -14,13 +19,74 @@ def customer_selector() -> Choice:
     return Choice("Select a customer", zip(customers.keys(), customers.items()))  # type: ignore[arg-type]
 
 
+def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None:
+    """Return a list of available interfaces for a given router and speed.
+
+    For Nokia routers, return a list of available interfaces.
+    For Juniper routers, return a string.
+    """
+    if Router.from_subscription(router_id).router.router_vendor != RouterVendor.NOKIA:
+        return None
+    interfaces = {
+        interface["name"]: f"{interface['name']} - {interface['module']['display']} - {interface['description']}"
+        for interface in NetboxClient().get_available_interfaces(router_id, speed)
+    }
+    return Choice("ae member", zip(interfaces.keys(), interfaces.items()))  # type: ignore[arg-type]
+
+
+def available_lags_choices(router_id: UUID) -> Choice | None:
+    """Return a list of available lags for a given router.
+
+    For Nokia routers, return a list of available lags.
+    For Juniper routers, return a string.
+    """
+
+    if Router.from_subscription(router_id).router.router_vendor != RouterVendor.NOKIA:
+        return None
+    side_a_ae_iface_list = NetboxClient().get_available_lags(router_id)
+    return Choice("ae iface", zip(side_a_ae_iface_list, side_a_ae_iface_list))  # type: ignore[arg-type]
+
+
+def get_router_vendor(router_id: UUID) -> str:
+    """Retrieve the vendor of a router.
+
+    Args:
+    ----
+    router_id (UUID): The {term}`UUID` of the router.
+
+    Returns:
+    -------
+    str: The vendor of the router.
+    """
+    return Router.from_subscription(router_id).router.router_vendor
+
+
 def iso_from_ipv4(ipv4_address: IPv4Address) -> str:
-    """Calculate an {term}`ISO` address, based on an IPv4 address.
+    """Calculate an :term:`ISO` address, based on an IPv4 address.
 
     :param IPv4Address ipv4_address: The address that's to be converted
-    :returns: An {term}`ISO`-formatted address.
+    :returns: An :term:`ISO`-formatted address.
     """
     padded_octets = [f"{x:>03}" for x in str(ipv4_address).split(".")]
     joined_octets = "".join(padded_octets)
     re_split = ".".join(re.findall("....", joined_octets))
     return ".".join(["49.51e5.0001", re_split, "00"])
+
+
+def validate_router_in_netbox(subscription_id: UUIDstr) -> UUIDstr | None:
+    """Verify if a device exists in Netbox.
+
+    Args:
+    ----
+    subscription_id (UUID): The {term}`UUID` of the router subscription.
+
+    Returns:
+    -------
+    UUID: The {term}`UUID` of the router subscription or raises an error.
+    """
+    router = Router.from_subscription(subscription_id).router
+    if router.router_vendor == RouterVendor.NOKIA:
+        device = NetboxClient().get_device_by_name(router.router_fqdn)
+        if not device:
+            raise ValueError("The selected router does not exist in Netbox.")
+    return subscription_id
diff --git a/requirements.txt b/requirements.txt
index b103da38a717e01e8c339a38c8e5534d778183e6..8509293b655fcf5b739c9a92b75fef43eaf9ce0f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,15 +2,17 @@ orchestrator-core==1.3.0
 requests==2.31.0
 infoblox-client~=0.6.0
 pycountry==22.3.5
+pynetbox==7.2.0
+
+# Test and linting dependencies
 pytest==7.4.2
-faker==19.6.2
+faker==19.10.0
 responses==0.23.3
 black==23.9.1
 isort==5.12.0
 flake8==6.1.0
-mypy==1.5.1
-ruff==0.0.290
+mypy==1.6.0
+ruff==0.0.292
 sphinx==7.2.6
 sphinx-rtd-theme==1.3.0
-urllib3_mock==0.3.3
-pynetbox==7.2.0
+urllib3_mock==0.3.3
\ No newline at end of file
diff --git a/test/test_resource_manager.py b/test/test_resource_manager.py
deleted file mode 100644
index 34cf53506c94d37bc36cfbae31d6998cf88a7a30..0000000000000000000000000000000000000000
--- a/test/test_resource_manager.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import random
-import string
-
-from gso.services import resource_manager
-
-
-def _random_string(n=None, letters=string.ascii_letters + string.digits + string.punctuation):
-    # ignoring S311 because this is a test
-    if not n:
-        n = random.randint(1, 20)  # noqa: S311
-    return "".join(random.choices(letters, k=n))  # noqa: S311
-
-
-def test_new_router():
-    router_name = _random_string(10)
-    assert router_name not in resource_manager._DUMMY_INVENTORY
-    resource_manager.import_new_router(new_router_fqdn=router_name, subscription_id=_random_string(10))
-    assert router_name in resource_manager._DUMMY_INVENTORY
-
-
-def test_new_lag():
-    router_name = list(resource_manager._DUMMY_INVENTORY.keys())[0]
-    new_lags = {
-        resource_manager.next_lag(router_fqdn=router_name, subscription_id=_random_string(10)) for _ in range(10)
-    }
-    assert len(new_lags) == 10
-    assert new_lags <= set(resource_manager._DUMMY_INVENTORY[router_name]["lags"])
-
-
-def test_physical_allocation_lifecycle_happy():
-    router_name = list(resource_manager._DUMMY_INVENTORY.keys())[0]
-
-    def _interfaces():
-        return resource_manager.available_physical_interfaces(router_fqdn=router_name)
-
-    initial_available = _interfaces()
-
-    interface_name = initial_available[0]
-
-    resource_manager.reserve_physical_interface(
-        router_fqdn=router_name, subscription_id=_random_string(10), interface_name=interface_name
-    )
-
-    current_available = _interfaces()
-    assert interface_name not in current_available
-
-    resource_manager.allocate_physical_interface(router_fqdn=router_name, interface_name=interface_name)
-    resource_manager.free_physical_interface(router_fqdn=router_name, interface_name=interface_name)
-
-    current_available = _interfaces()
-    assert interface_name in current_available
diff --git a/test/workflows/__init__.py b/test/workflows/__init__.py
index 2b42a33928c6c9932b3512bd64b66d56267e9d28..19707ff0ff1b4e53e3309f9c3509c8e24001a9fc 100644
--- a/test/workflows/__init__.py
+++ b/test/workflows/__init__.py
@@ -2,7 +2,7 @@ import difflib
 import pprint
 from copy import deepcopy
 from itertools import chain, repeat
-from typing import Callable, Dict, List, Optional, Tuple, Union, cast
+from typing import Callable, cast
 from uuid import uuid4
 
 import structlog
@@ -64,7 +64,7 @@ def assert_state(result, expected):
     assert expected == actual, f"Invalid state. Expected superset of: {expected}, but was: {actual}"
 
 
-def assert_state_equal(result: ProcessTable, expected: Dict, excluded_keys: Optional[List[str]] = None) -> None:
+def assert_state_equal(result: ProcessTable, expected: dict, excluded_keys: list[str] | None = None) -> None:
     """Test state with certain keys excluded from both actual and expected state."""
     if excluded_keys is None:
         excluded_keys = ["process_id", "workflow_target", "workflow_name"]
@@ -143,7 +143,7 @@ class WorkflowInstanceForTests(LazyWorkflowInstance):
         return f"WorkflowInstanceForTests('{self.workflow}','{self.name}')"
 
 
-def _store_step(step_log: List[Tuple[Step, WFProcess]]) -> Callable[[ProcessStat, Step, WFProcess], WFProcess]:
+def _store_step(step_log: list[tuple[Step, WFProcess]]) -> Callable[[ProcessStat, Step, WFProcess], WFProcess]:
     def __store_step(pstat: ProcessStat, step: Step, state: WFProcess) -> WFProcess:
         try:
             state = state.map(lambda s: json_loads(json_dumps(s)))
@@ -155,23 +155,23 @@ def _store_step(step_log: List[Tuple[Step, WFProcess]]) -> Callable[[ProcessStat
     return __store_step
 
 
-def _sanitize_input(input_data: Union[State, List[State]]) -> List[State]:
+def _sanitize_input(input_data: State | list[State]) -> list[State]:
     # To be backwards compatible convert single dict to list
-    if not isinstance(input_data, List):
+    if not isinstance(input_data, list):
         input_data = [input_data]
 
     # We need a copy here and we want to mimic the actual code that returns a serialized version of the state
-    return cast(List[State], json_loads(json_dumps(input_data)))
+    return cast(list[State], json_loads(json_dumps(input_data)))
 
 
-def run_workflow(workflow_key: str, input_data: Union[State, List[State]]) -> Tuple[WFProcess, ProcessStat, List]:
+def run_workflow(workflow_key: str, input_data: State | list[State]) -> tuple[WFProcess, ProcessStat, list]:
     # ATTENTION!! This code needs to be as similar as possible to `server.services.processes.start_process`
     # The main differences are: we use a different step log function and we don't run in
     # a sepperate thread
     user_data = _sanitize_input(input_data)
     user = "john.doe"
 
-    step_log: List[Tuple[Step, WFProcess]] = []
+    step_log: list[tuple[Step, WFProcess]] = []
 
     process_id = uuid4()
     workflow = get_workflow(workflow_key)
@@ -201,8 +201,8 @@ def run_workflow(workflow_key: str, input_data: Union[State, List[State]]) -> Tu
 
 
 def resume_workflow(
-    process: ProcessStat, step_log: List[Tuple[Step, WFProcess]], input_data: State
-) -> Tuple[WFProcess, List]:
+    process: ProcessStat, step_log: list[tuple[Step, WFProcess]], input_data: State
+) -> tuple[WFProcess, list]:
     # ATTENTION!! This code needs to be as similar as possible to `server.services.processes.resume_process`
     # The main differences are: we use a different step log function, and we don't run in a separate thread
     user_data = _sanitize_input(input_data)
@@ -228,8 +228,8 @@ def resume_workflow(
 
 
 def run_form_generator(
-    form_generator: FormGenerator, extra_inputs: Optional[List[State]] = None
-) -> Tuple[List[dict], State]:
+    form_generator: FormGenerator, extra_inputs: list[State] | None = None
+) -> tuple[list[dict], State]:
     """Run a form generator to get the resulting forms and result.
 
     Warning! This does not run the actual pydantic validation on purpose. However, you should
@@ -239,13 +239,13 @@ def run_form_generator(
     Args:
     ----
     form_generator (FormGenerator): The form generator that will be run.
-    extra_inputs (Optional[List[State]]): List of user input dicts for each page in the generator.
+    extra_inputs (list[State] | None): list of user input dicts for each page in the generator.
                                          If no input is given for a page, an empty dict is used.
                                          The default value from the form is used as the default value for a field.
 
     Returns:
     -------
-        Tuple[List[dict], State]: A list of generated forms and the result state for the whole generator.
+        tuple[list[dict], State]: A list of generated forms and the result state for the whole generator.
 
     Example:
     -------
@@ -276,7 +276,7 @@ def run_form_generator(
         {'field': 'baz', 'bar': 42}
 
     """
-    forms: List[dict] = []
+    forms: list[dict] = []
     result: State = {"s": 3}
     if extra_inputs is None:
         extra_inputs = []
diff --git a/test/workflows/iptrunks/iptrunks/test_create_iptrunks.py b/test/workflows/iptrunks/iptrunks/test_create_iptrunks.py
index dfb6fa4f98e7ebf0ec7afd1b5e09855d09add6f8..cf50c2ccbf1d342816372913be351dfe73fa3c82 100644
--- a/test/workflows/iptrunks/iptrunks/test_create_iptrunks.py
+++ b/test/workflows/iptrunks/iptrunks/test_create_iptrunks.py
@@ -1,3 +1,4 @@
+from os import PathLike
 from unittest.mock import patch
 
 import pytest
@@ -19,6 +20,65 @@ from test.workflows import (
 )
 
 
+class MockedNetboxClient:
+    class BaseMockObject:
+        def __init__(self, **kwargs):
+            for key, value in kwargs.items():
+                setattr(self, key, value)
+
+    def get_device_by_name(self):
+        return self.BaseMockObject(id=1, name="test")
+
+    def get_available_lags(self) -> list[str]:
+        return [f"LAG{lag}" for lag in range(1, 5)]
+
+    def get_available_interfaces(self):
+        interfaces = []
+        for interface in range(1, 5):
+            interface_data = {
+                "name": f"Interface{interface}",
+                "module": {"display": f"Module{interface}"},
+                "description": f"Description{interface}",
+            }
+            interfaces.append(interface_data)
+        return interfaces
+
+    def create_interface(self):
+        return self.BaseMockObject(id=1, name="test")
+
+    def attach_interface_to_lag(self):
+        return self.BaseMockObject(id=1, name="test")
+
+    def reserve_interface(self):
+        return self.BaseMockObject(id=1, name="test")
+
+    def allocate_interface(self):
+        return {"id": 1, "name": "test"}
+
+
+@pytest.fixture
+def netbox_client_mock():
+    # Mock NetboxClient methods
+    with (
+        patch("gso.services.netbox_client.NetboxClient.get_device_by_name") as mock_get_device_by_name,
+        patch("gso.services.netbox_client.NetboxClient.get_available_interfaces") as mock_get_available_interfaces,
+        patch("gso.services.netbox_client.NetboxClient.get_available_lags") as mock_get_available_lags,
+        patch("gso.services.netbox_client.NetboxClient.create_interface") as mock_create_interface,
+        patch("gso.services.netbox_client.NetboxClient.attach_interface_to_lag") as mock_attach_interface_to_lag,
+        patch("gso.services.netbox_client.NetboxClient.reserve_interface") as mock_reserve_interface,
+        patch("gso.services.netbox_client.NetboxClient.allocate_interface") as mock_allocate_interface,
+    ):
+        mock_get_device_by_name.return_value = MockedNetboxClient().get_device_by_name()
+        mock_get_available_interfaces.return_value = MockedNetboxClient().get_available_interfaces()
+        mock_get_available_lags.return_value = MockedNetboxClient().get_available_lags()
+        mock_create_interface.return_value = MockedNetboxClient().create_interface()
+        mock_attach_interface_to_lag.return_value = MockedNetboxClient().attach_interface_to_lag()
+        mock_reserve_interface.return_value = MockedNetboxClient().reserve_interface()
+        mock_allocate_interface.return_value = MockedNetboxClient().allocate_interface()
+
+        yield
+
+
 @pytest.fixture
 def input_form_wizard_data(router_subscription_factory, faker):
     router_side_a = router_subscription_factory()
@@ -31,25 +91,31 @@ def input_form_wizard_data(router_subscription_factory, faker):
         "iptrunk_type": IptrunkType.DARK_FIBER,
         "iptrunk_description": faker.sentence(),
         "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND,
-        "iptrunk_minimum_links": 5,
+        "iptrunk_minimum_links": 2,
     }
+    create_ip_trunk_side_a_router_name = {"iptrunk_sideA_node_id": router_side_a}
     create_ip_trunk_side_a_step = {
-        "iptrunk_sideA_node_id": router_side_a,
-        "iptrunk_sideA_ae_iface": faker.pystr(),
+        "iptrunk_sideA_ae_iface": "LAG1",
         "iptrunk_sideA_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideA_ae_members": [faker.pystr() for _ in range(5)],
-        "iptrunk_sideA_ae_members_descriptions": [faker.sentence() for _ in range(5)],
+        "iptrunk_sideA_ae_members": ["Interface1", "Interface2"],
+        "iptrunk_sideA_ae_members_descriptions": ["Interface1 Description", "Interface2 Description"],
     }
 
+    create_ip_trunk_side_b_router_name = {"iptrunk_sideB_node_id": router_side_b}
     create_ip_trunk_side_b_step = {
-        "iptrunk_sideB_node_id": router_side_b,
-        "iptrunk_sideB_ae_iface": faker.pystr(),
+        "iptrunk_sideB_ae_iface": "LAG1",
         "iptrunk_sideB_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideB_ae_members": [faker.pystr() for _ in range(5)],
-        "iptrunk_sideB_ae_members_descriptions": [faker.sentence() for _ in range(5)],
+        "iptrunk_sideB_ae_members": ["Interface1", "Interface2"],
+        "iptrunk_sideB_ae_members_descriptions": ["Interface1 Description", "Interface2 Description"],
     }
 
-    return [create_ip_trunk_step, create_ip_trunk_side_a_step, create_ip_trunk_side_b_step]
+    return [
+        create_ip_trunk_step,
+        create_ip_trunk_side_a_router_name,
+        create_ip_trunk_side_a_step,
+        create_ip_trunk_side_b_router_name,
+        create_ip_trunk_side_b_step,
+    ]
 
 
 def _user_accept_and_assert_suspended(process_stat, step_log, extra_data=None):
@@ -73,6 +139,8 @@ def test_successful_iptrunk_creation_with_standard_lso_result(
     responses,
     input_form_wizard_data,
     faker,
+    data_config_filename: PathLike,
+    netbox_client_mock,
 ):
     mock_allocate_v4_network.return_value = faker.ipv4_network()
     mock_allocate_v6_network.return_value = faker.ipv6_network()
@@ -92,10 +160,10 @@ def test_successful_iptrunk_creation_with_standard_lso_result(
     }
     for _ in range(5):
         result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
-        result, step_log = _user_accept_and_assert_suspended(process_stat, step_log)
+        result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
 
     result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
-    result, step_log = resume_workflow(process_stat, step_log, {})
+    result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
     assert_complete(result)
 
     state = extract_state(result)
@@ -122,6 +190,8 @@ def test_iptrunk_creation_fails_when_lso_return_code_is_one(
     responses,
     input_form_wizard_data,
     faker,
+    netbox_client_mock,
+    data_config_filename: PathLike,
 ):
     mock_allocate_v4_network.return_value = faker.ipv4_network()
     mock_allocate_v6_network.return_value = faker.ipv6_network()
@@ -144,10 +214,10 @@ def test_iptrunk_creation_fails_when_lso_return_code_is_one(
     attempts = 3
     for _ in range(0, attempts - 1):
         result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
-        result, step_log = _user_accept_and_assert_suspended(process_stat, step_log)
+        result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
 
     result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
-    result, step_log = resume_workflow(process_stat, step_log, {})
+    result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
     assert_aborted(result)
 
     assert mock_provision_ip_trunk.call_count == attempts
diff --git a/utils/netboxcli.py b/utils/netboxcli.py
index b934f5e7c94b1f43d3692865041bdd02d2af95bf..1d46bbb3db64520017c1bcedd07545b48d099166 100644
--- a/utils/netboxcli.py
+++ b/utils/netboxcli.py
@@ -4,7 +4,7 @@ from typing import Any, Dict, List
 import click
 import pandas as pd
 
-from gso.services.netbox_client import NetBoxClient
+from gso.services.netbox_client import NetboxClient
 
 
 def convert_to_table(data: List[Dict[str, Any]], fields: List[str]) -> pd.DataFrame:
@@ -33,7 +33,7 @@ def create() -> None:
 @click.option("--model", default="vmx", help="Device model")
 def device(fqdn: str, model: str) -> None:
     click.echo(f"Creating device: fqdn={fqdn}, model={model}")
-    new_device = NetBoxClient().create_device(fqdn, model)
+    new_device = NetboxClient().create_device(fqdn, model)
     click.echo(new_device)
 
 
@@ -44,7 +44,7 @@ def device(fqdn: str, model: str) -> None:
 @click.option("--fqdn", help="Device where to create interface")
 def interface(name: str, type: str, speed: str, fqdn: str) -> None:
     click.echo(f"Creating interface: name={name}, speed={speed}, fqdn={fqdn}")
-    new_interface = NetBoxClient().create_interface(name, type, speed, fqdn)
+    new_interface = NetboxClient().create_interface(name, type, speed, fqdn)
     click.echo(new_interface)
 
 
@@ -53,7 +53,7 @@ def interface(name: str, type: str, speed: str, fqdn: str) -> None:
 @click.option("--slug", help="Short name for manufacturer")
 def manufacturer(name: str, slug: str) -> None:
     click.echo(f"Creating manufacturer: name={name}")
-    manufacturer = NetBoxClient().create_device_manufacturer(name, slug)
+    manufacturer = NetboxClient().create_device_manufacturer(name, slug)
     click.echo(manufacturer)
 
 
@@ -63,7 +63,7 @@ def manufacturer(name: str, slug: str) -> None:
 @click.option("--slug", help="Short name for manufacturer")
 def device_type(manufacturer: str, model: str, slug: str) -> None:
     click.echo(f"Creating device type: manufacturer={manufacturer} model = {model}")
-    device_type = NetBoxClient().create_device_type(manufacturer, model, slug)
+    device_type = NetboxClient().create_device_type(manufacturer, model, slug)
     click.echo(device_type)
 
 
@@ -72,7 +72,7 @@ def device_type(manufacturer: str, model: str, slug: str) -> None:
 @click.option("--slug", help="Short name for device role")
 def device_role(name: str, slug: str) -> None:
     click.echo(f"Creating device role: name={name}")
-    device_role = NetBoxClient().create_device_role(name, slug)
+    device_role = NetboxClient().create_device_role(name, slug)
     click.echo(device_role)
 
 
@@ -81,7 +81,7 @@ def device_role(name: str, slug: str) -> None:
 @click.option("--slug", help="Short name for device site")
 def device_site(name: str, slug: str) -> None:
     click.echo(f"Creating device site: name={name}")
-    device_site = NetBoxClient().create_device_site(name, slug)
+    device_site = NetboxClient().create_device_site(name, slug)
     click.echo(device_site)
 
 
@@ -104,7 +104,7 @@ def list() -> None:
 @click.option("--speed", default="1000", help="Interface speed to list interfaces (default 1000=1G)")
 def interfaces(fqdn: str, speed: str) -> None:
     click.echo(f"Listing all interfaces for: device with fqdn={fqdn}, speed={speed}")
-    interface_list = NetBoxClient().get_interfaces_by_device(fqdn, speed)
+    interface_list = NetboxClient().get_interfaces_by_device(fqdn, speed)
     display_fields = ["name", "enabled", "mark_connected", "custom_fields", "lag", "speed"]
     iface_list = []
     for iface in interface_list:
@@ -117,7 +117,7 @@ def interfaces(fqdn: str, speed: str) -> None:
 @list.command()
 def devices() -> None:
     click.echo("Listing all devices:")
-    device_list = NetBoxClient().get_all_devices()
+    device_list = NetboxClient().get_all_devices()
     display_fields = ["name", "device_type"]
     devices = []
     for device in device_list:
@@ -143,7 +143,7 @@ def attach() -> None:
 @click.option("--lag", help="LAG name to attach interface")
 def interface_to_lag(fqdn: str, iface: str, lag: str) -> None:
     click.echo(f"Attaching interface to lag: device ={fqdn}, interface name={iface} to lag={lag}")
-    new_iface = NetBoxClient().attach_interface_to_lag(fqdn, lag, iface)
+    new_iface = NetboxClient().attach_interface_to_lag(fqdn, lag, iface)
     click.echo(new_iface)
 
 
@@ -161,7 +161,7 @@ def reserve() -> None:
 @click.option("--iface", help="Interface name to reserve")
 def reserve_interface(fqdn: str, iface: str) -> None:
     click.echo(f"Reserving interface: device ={fqdn}, interface name={iface}")
-    reserved_iface = NetBoxClient().reserve_interface(fqdn, iface)
+    reserved_iface = NetboxClient().reserve_interface(fqdn, iface)
     click.echo(reserved_iface)