diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index d5b831302d169f0ce8adbc8835d2909eee7fd51b..0000000000000000000000000000000000000000 --- a/.pylintrc +++ /dev/null @@ -1,9 +0,0 @@ -[MAIN] -extension-pkg-whitelist=pydantic - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -# Note that it does not contain TODO, only the default FIXME and XXX -notes=FIXME, - XXX diff --git a/Changelog.md b/Changelog.md index b0b8daefa2b5c71193b8c366899efee496dc60a2..371dcf4f9723110ce3b7f9d8da4bfb293a815e4b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## [2.22] - 2024-10-31 +- Added EdgePort, IAS and GEANT IP products and required workflows +- Refactored pydantic models for maintainability + ## [2.21] - 2024-10-22 - Fix inventory structure - Fix the `pe_router_list` in update_sdp_single_pe @@ -23,12 +27,6 @@ ## [2.18] - 2024-10-01 - Use solo pool for Celery workers -## [2.17] - 2024-09-30 -- NOTHING IS HERE (JENKINS ISSUE) - -## [2.16] - 2024-09-30 -- NOTHING IS HERE (JENKINS ISSUE) - ## [2.15] - 2024-09-30 - Show current license usage when updating Kentik license of a router - Fix the bug of clearing all the AE members and creating new objects instead of updating it. diff --git a/build-docs.sh b/build-docs.sh index 34eadabbe43f22cac9e4abd40c3fadf1c3d872a3..f68d5ad6dfa32f42e18bfdcee90a8351d6c5e8fd 100755 --- a/build-docs.sh +++ b/build-docs.sh @@ -3,6 +3,7 @@ set -o errexit set -o nounset export OSS_PARAMS_FILENAME=../gso/oss-params-example.json +export TESTING=true pip install sphinx_rtd_theme sphinxcontrib-jquery diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 6ac6193af5f17e003f5f84eba351c981e6824a2d..59b699e5f3bffd1a16fc523d6e08e3c635b1de05 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -9,6 +9,9 @@ Glossary of terms API Application Programming Interface + BFD + Bi-directional Forwarding Detection + BGP Border Gateway Protocol: a path vector routing protocol described in `RFC 4271 <https://datatracker.ietf.org/doc/html/rfc4271>`_. @@ -54,18 +57,27 @@ Glossary of terms JSON JavaScript Object Notation + LACP + Link Aggregation Control Protocol + LAG Link Aggregation: a bundle of multiple network connections. LAN Local Area Network + LLDP + Link Layer Discovery Protocol + LSO Lightweight Service Orchestrator NET Network Entity Title: used for :term:`ISIS` routing. + NREN + National Research and Education Network + OIDC OpenID Connect @@ -78,6 +90,14 @@ Glossary of terms OSS Operational Support Systems + SBP + Service Binding Point, a logical construct used in the orchestrator to attach a partner subscription to a physical + (set of) ports. + + SDP + Service Demarcation Point: A logical construct used for modeling partner subscriptions. It models the link between + the physical and the service domains. + SNMP Simple Network Management Protocol: a protocol that's used for gathering data, widely used for network management and monitoring. @@ -91,5 +111,8 @@ Glossary of terms VLAN Virtual LAN + WAN + Wide Area Network + WFO `Workflow Orchestrator <https://workfloworchestrator.org/>`_ diff --git a/docs/source/module/products/product_blocks/bgp_session.rst b/docs/source/module/products/product_blocks/bgp_session.rst new file mode 100644 index 0000000000000000000000000000000000000000..5d3fe341ee92e31dd323d06bcd00206c1120c438 --- /dev/null +++ b/docs/source/module/products/product_blocks/bgp_session.rst @@ -0,0 +1,6 @@ +``gso.products.product_blocks.bgp_session`` +=========================================== + +.. automodule:: gso.products.product_blocks.bgp_session + :members: + :show-inheritance: diff --git a/docs/source/module/products/product_blocks/edge_port.rst b/docs/source/module/products/product_blocks/edge_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..4d6dbf1f35248da7ea43b3f570fff7f2fced4a2d --- /dev/null +++ b/docs/source/module/products/product_blocks/edge_port.rst @@ -0,0 +1,6 @@ +``gso.products.product_blocks.edge_port`` +========================================= + +.. automodule:: gso.products.product_blocks.edge_port + :members: + :show-inheritance: diff --git a/docs/source/module/products/product_blocks/index.rst b/docs/source/module/products/product_blocks/index.rst index da2852b9e7b85abfc06c2ca1906b71663d538d45..f7f2c86785d04b746ad14c7750a51f8b0b3b030f 100644 --- a/docs/source/module/products/product_blocks/index.rst +++ b/docs/source/module/products/product_blocks/index.rst @@ -14,13 +14,16 @@ Submodules .. toctree:: :maxdepth: 1 - super_pop_switch - office_router + bgp_session + edge_port iptrunk + lan_switch_interconnect + nren_l3_core_service + office_router + opengear + pop_vlan router + service_binding_port site + super_pop_switch switch - lan_switch_interconnect - pop_vlan - opengear - diff --git a/docs/source/module/products/product_blocks/nren_l3_core_service.rst b/docs/source/module/products/product_blocks/nren_l3_core_service.rst new file mode 100644 index 0000000000000000000000000000000000000000..53e849d98c7b948468f21fb21280431a670274a2 --- /dev/null +++ b/docs/source/module/products/product_blocks/nren_l3_core_service.rst @@ -0,0 +1,6 @@ +``gso.products.product_blocks.nren_l3_core_service`` +==================================================== + +.. automodule:: gso.products.product_blocks.nren_l3_core_service + :members: + :show-inheritance: diff --git a/docs/source/module/products/product_blocks/service_binding_port.rst b/docs/source/module/products/product_blocks/service_binding_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..a7036dc21ef3191fda321f70362d08e89b49ffae --- /dev/null +++ b/docs/source/module/products/product_blocks/service_binding_port.rst @@ -0,0 +1,6 @@ +``gso.products.product_blocks.service_binding_port`` +==================================================== + +.. automodule:: gso.products.product_blocks.service_binding_port + :members: + :show-inheritance: diff --git a/docs/source/module/products/product_types/edge_port.rst b/docs/source/module/products/product_types/edge_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..ae7140595640e1d605f10b486df98d939a32aa8a --- /dev/null +++ b/docs/source/module/products/product_types/edge_port.rst @@ -0,0 +1,6 @@ +``gso.products.product_types.edge_port`` +======================================== + +.. automodule:: gso.products.product_types.edge_port + :members: + :show-inheritance: diff --git a/docs/source/module/products/product_types/index.rst b/docs/source/module/products/product_types/index.rst index 06c706298b1badf546a36c7c84cfb47966c96ce2..70f882d3632166aa86d3b46f85c33493c043da0f 100644 --- a/docs/source/module/products/product_types/index.rst +++ b/docs/source/module/products/product_types/index.rst @@ -14,12 +14,14 @@ Submodules .. toctree:: :maxdepth: 1 - super_pop_switch - office_router + edge_port iptrunk + lan_switch_interconnect + nren_l3_core_service + office_router + opengear + pop_vlan router site + super_pop_switch switch - lan_switch_interconnect - pop_vlan - opengear diff --git a/docs/source/module/products/product_types/nren_l3_core_service.rst b/docs/source/module/products/product_types/nren_l3_core_service.rst new file mode 100644 index 0000000000000000000000000000000000000000..4440d033bb1f7e439b51eaba6429d7d2f5f63883 --- /dev/null +++ b/docs/source/module/products/product_types/nren_l3_core_service.rst @@ -0,0 +1,6 @@ +``gso.products.product_types.nren_l3_core_service`` +=================================================== + +.. automodule:: gso.products.product_types.nren_l3_core_service + :members: + :show-inheritance: diff --git a/docs/source/module/utils/index.rst b/docs/source/module/utils/index.rst index 52f992ba035f17541d1e5314844ee59490a883bf..7289690736f520a7b462cce3c7446c282a805613 100644 --- a/docs/source/module/utils/index.rst +++ b/docs/source/module/utils/index.rst @@ -5,6 +5,14 @@ :members: :show-inheritance: +Subpackages +----------- + +.. toctree:: + :maxdepth: 1 + + types/index + Submodules ---------- diff --git a/docs/source/module/utils/types.rst b/docs/source/module/utils/types.rst deleted file mode 100644 index c70c8dd0c61a4fa29cc2f123ec4d0643ab7bdf5d..0000000000000000000000000000000000000000 --- a/docs/source/module/utils/types.rst +++ /dev/null @@ -1,6 +0,0 @@ -``gso.utils.types`` -=================== - -.. automodule:: gso.utils.types - :members: - :show-inheritance: diff --git a/docs/source/module/utils/types/base_site.rst b/docs/source/module/utils/types/base_site.rst new file mode 100644 index 0000000000000000000000000000000000000000..ac952086ae62069f8a4cd3f0fc9e757e9035d231 --- /dev/null +++ b/docs/source/module/utils/types/base_site.rst @@ -0,0 +1,6 @@ +``gso.utils.types.base_site`` +============================= + +.. automodule:: gso.utils.types.base_site + :members: + :show-inheritance: diff --git a/docs/source/module/utils/types/coordinates.rst b/docs/source/module/utils/types/coordinates.rst new file mode 100644 index 0000000000000000000000000000000000000000..85c52ba10681782a6ce049a231c8a21b2a0aede3 --- /dev/null +++ b/docs/source/module/utils/types/coordinates.rst @@ -0,0 +1,6 @@ +``gso.utils.types.coordinates`` +=============================== + +.. automodule:: gso.utils.types.coordinates + :members: + :show-inheritance: diff --git a/docs/source/module/utils/types/country_code.rst b/docs/source/module/utils/types/country_code.rst new file mode 100644 index 0000000000000000000000000000000000000000..130b77afb0254aa77fabde7f2f5f5801e27adfc4 --- /dev/null +++ b/docs/source/module/utils/types/country_code.rst @@ -0,0 +1,6 @@ +``gso.utils.types.country_code`` +================================ + +.. automodule:: gso.utils.types.country_code + :members: + :show-inheritance: diff --git a/docs/source/module/utils/types/index.rst b/docs/source/module/utils/types/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..2dcf23ec96f2d068448c3d3f3aadaf1d3ec3b805 --- /dev/null +++ b/docs/source/module/utils/types/index.rst @@ -0,0 +1,24 @@ +``gso.utils.types`` +=================== + +.. automodule:: gso.utils.types + :members: + :show-inheritance: + + +Submodules +---------- + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + base_site + coordinates + country_code + interfaces + ip_address + netbox_router + site_name + tt_number + unique_field diff --git a/docs/source/module/utils/types/interfaces.rst b/docs/source/module/utils/types/interfaces.rst new file mode 100644 index 0000000000000000000000000000000000000000..95c9a3dacf770c3963f431cb2d40a612d4b1043f --- /dev/null +++ b/docs/source/module/utils/types/interfaces.rst @@ -0,0 +1,6 @@ +``gso.utils.types.interfaces`` +============================== + +.. automodule:: gso.utils.types.interfaces + :members: + :show-inheritance: diff --git a/docs/source/module/utils/types/ip_address.rst b/docs/source/module/utils/types/ip_address.rst new file mode 100644 index 0000000000000000000000000000000000000000..68858af762bd0559a88e2fcacf88edbd9f4bd1a3 --- /dev/null +++ b/docs/source/module/utils/types/ip_address.rst @@ -0,0 +1,6 @@ +``gso.utils.types.ip_address`` +============================== + +.. automodule:: gso.utils.types.ip_address + :members: + :show-inheritance: diff --git a/docs/source/module/utils/types/netbox_router.rst b/docs/source/module/utils/types/netbox_router.rst new file mode 100644 index 0000000000000000000000000000000000000000..1d35d235e221b31a8b09e5c0a0dcef5cfc342fcc --- /dev/null +++ b/docs/source/module/utils/types/netbox_router.rst @@ -0,0 +1,6 @@ +``gso.utils.types.netbox_router`` +================================= + +.. automodule:: gso.utils.types.netbox_router + :members: + :show-inheritance: diff --git a/docs/source/module/utils/types/site_name.rst b/docs/source/module/utils/types/site_name.rst new file mode 100644 index 0000000000000000000000000000000000000000..df2934a6d0449a9129615b97e5584fd687c528b3 --- /dev/null +++ b/docs/source/module/utils/types/site_name.rst @@ -0,0 +1,6 @@ +``gso.utils.types.site_name`` +============================= + +.. automodule:: gso.utils.types.site_name + :members: + :show-inheritance: diff --git a/docs/source/module/utils/types/tt_number.rst b/docs/source/module/utils/types/tt_number.rst new file mode 100644 index 0000000000000000000000000000000000000000..43410a3536f9fa879411e512140328293543fffa --- /dev/null +++ b/docs/source/module/utils/types/tt_number.rst @@ -0,0 +1,6 @@ +``gso.utils.types.tt_number`` +============================= + +.. automodule:: gso.utils.types.tt_number + :members: + :show-inheritance: diff --git a/docs/source/module/utils/types/unique_field.rst b/docs/source/module/utils/types/unique_field.rst new file mode 100644 index 0000000000000000000000000000000000000000..3a74a0d1faea700768b41be6fd3dfe67f2d4df58 --- /dev/null +++ b/docs/source/module/utils/types/unique_field.rst @@ -0,0 +1,6 @@ +``gso.utils.types.unique_field`` +================================ + +.. automodule:: gso.utils.types.unique_field + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/edge_port/create_edge_port.rst b/docs/source/module/workflows/edge_port/create_edge_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..7cedc807890765ce40f92e07d4830d4fa0be0f50 --- /dev/null +++ b/docs/source/module/workflows/edge_port/create_edge_port.rst @@ -0,0 +1,6 @@ +``gso.workflows.edge_port.create_edge_port`` +============================================ + +.. automodule:: gso.workflows.edge_port.create_edge_port + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/edge_port/create_imported_edge_port.rst b/docs/source/module/workflows/edge_port/create_imported_edge_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..38b8d7de0ef42d82366b6ce3c363b23db8af0ee5 --- /dev/null +++ b/docs/source/module/workflows/edge_port/create_imported_edge_port.rst @@ -0,0 +1,6 @@ +``gso.workflows.edge_port.create_imported_edge_port`` +===================================================== + +.. automodule:: gso.workflows.edge_port.create_imported_edge_port + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/edge_port/import_edge_port.rst b/docs/source/module/workflows/edge_port/import_edge_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..31d54a7071095f4378701bc6bfbe380e1edb305d --- /dev/null +++ b/docs/source/module/workflows/edge_port/import_edge_port.rst @@ -0,0 +1,6 @@ +``gso.workflows.edge_port.import_edge_port`` +============================================ + +.. automodule:: gso.workflows.edge_port.import_edge_port + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/edge_port/index.rst b/docs/source/module/workflows/edge_port/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..51d11ff47730c1ff9932cb33498aa3cdbffc468d --- /dev/null +++ b/docs/source/module/workflows/edge_port/index.rst @@ -0,0 +1,20 @@ +``gso.workflows.edge_port`` +=========================== + +.. automodule:: gso.workflows.edge_port + :members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + create_edge_port + modify_edge_port + terminate_edge_port + validate_edge_port + import_edge_port + create_imported_edge_port diff --git a/docs/source/module/workflows/edge_port/modify_edge_port.rst b/docs/source/module/workflows/edge_port/modify_edge_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..88d0133603cfb5055c0d35aef1f5e14a9e848310 --- /dev/null +++ b/docs/source/module/workflows/edge_port/modify_edge_port.rst @@ -0,0 +1,6 @@ +``gso.workflows.edge_port.modify_edge_port`` +============================================ + +.. automodule:: gso.workflows.edge_port.modify_edge_port + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/edge_port/terminate_edge_port.rst b/docs/source/module/workflows/edge_port/terminate_edge_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..4613d361d6576b6b89c665f201b1eecee161f813 --- /dev/null +++ b/docs/source/module/workflows/edge_port/terminate_edge_port.rst @@ -0,0 +1,6 @@ +``gso.workflows.edge_port.terminate_edge_port`` +=============================================== + +.. automodule:: gso.workflows.edge_port.terminate_edge_port + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/edge_port/validate_edge_port.rst b/docs/source/module/workflows/edge_port/validate_edge_port.rst new file mode 100644 index 0000000000000000000000000000000000000000..a80e2eca96440d312587bea46966a2abf76bc4c2 --- /dev/null +++ b/docs/source/module/workflows/edge_port/validate_edge_port.rst @@ -0,0 +1,6 @@ +``gso.workflows.edge_port.validate_edge_port`` +============================================== + +.. automodule:: gso.workflows.edge_port.validate_edge_port + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/index.rst b/docs/source/module/workflows/index.rst index 3ee41bad0534418b8417ef5cd037795a9681628d..774573c9e67c1a069175fc5aa7e39b74c2283a32 100644 --- a/docs/source/module/workflows/index.rst +++ b/docs/source/module/workflows/index.rst @@ -12,7 +12,9 @@ Subpackages :maxdepth: 2 :titlesonly: + edge_port/index iptrunk/index + nren_l3_core_service/index office_router/index opengear/index router/index diff --git a/docs/source/module/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.rst b/docs/source/module/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.rst new file mode 100644 index 0000000000000000000000000000000000000000..249f9b824661d6ed6773b3695c6084b5aa3348be --- /dev/null +++ b/docs/source/module/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.rst @@ -0,0 +1,6 @@ +``gso.workflows.nren_l3_core_service.create_imported_nren_l3_core_service`` +=========================================================================== + +.. automodule:: gso.workflows.nren_l3_core_service.create_imported_nren_l3_core_service + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/nren_l3_core_service/create_nren_l3_core_service.rst b/docs/source/module/workflows/nren_l3_core_service/create_nren_l3_core_service.rst new file mode 100644 index 0000000000000000000000000000000000000000..e1418b4c915bd2e0c43130f9a344090ee0b4472a --- /dev/null +++ b/docs/source/module/workflows/nren_l3_core_service/create_nren_l3_core_service.rst @@ -0,0 +1,6 @@ +``gso.workflows.nren_l3_core_service.create_nren_l3_core_service`` +================================================================== + +.. automodule:: gso.workflows.nren_l3_core_service.create_nren_l3_core_service + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/nren_l3_core_service/import_nren_l3_core_service.rst b/docs/source/module/workflows/nren_l3_core_service/import_nren_l3_core_service.rst new file mode 100644 index 0000000000000000000000000000000000000000..fdb67a1e8c8b2e65774149001da612d34d5f312f --- /dev/null +++ b/docs/source/module/workflows/nren_l3_core_service/import_nren_l3_core_service.rst @@ -0,0 +1,6 @@ +``gso.workflows.nren_l3_core_service.import_nren_l3_core_service`` +================================================================== + +.. automodule:: gso.workflows.nren_l3_core_service.import_nren_l3_core_service + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/nren_l3_core_service/index.rst b/docs/source/module/workflows/nren_l3_core_service/index.rst new file mode 100644 index 0000000000000000000000000000000000000000..aa55ff08c8e565e7304ff1c4082f9d3e0b4caae3 --- /dev/null +++ b/docs/source/module/workflows/nren_l3_core_service/index.rst @@ -0,0 +1,19 @@ +``gso.workflows.nren_l3_core_service`` +====================================== + +.. automodule:: gso.workflows.nren_l3_core_service + :members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + create_nren_l3_core_service + create_imported_nren_l3_core_service + import_nren_l3_core_service + migrate_nren_l3_core_service + modify_nren_l3_core_service diff --git a/docs/source/module/workflows/nren_l3_core_service/migrate_nren_l3_core_service.rst b/docs/source/module/workflows/nren_l3_core_service/migrate_nren_l3_core_service.rst new file mode 100644 index 0000000000000000000000000000000000000000..9a550c827f43ee3e06ddcd88440897592c83eff5 --- /dev/null +++ b/docs/source/module/workflows/nren_l3_core_service/migrate_nren_l3_core_service.rst @@ -0,0 +1,6 @@ +``gso.workflows.nren_l3_core_service.migrate_nren_l3_core_service`` +=================================================================== + +.. automodule:: gso.workflows.nren_l3_core_service.migrate_nren_l3_core_service + :members: + :show-inheritance: diff --git a/docs/source/module/workflows/nren_l3_core_service/modify_nren_l3_core_service.rst b/docs/source/module/workflows/nren_l3_core_service/modify_nren_l3_core_service.rst new file mode 100644 index 0000000000000000000000000000000000000000..bd33ee2260284b4ec67883e6bb6bc17af4bdbfe5 --- /dev/null +++ b/docs/source/module/workflows/nren_l3_core_service/modify_nren_l3_core_service.rst @@ -0,0 +1,6 @@ +``gso.workflows.nren_l3_core_service.modify_nren_l3_core_service`` +================================================================== + +.. automodule:: gso.workflows.nren_l3_core_service.modify_nren_l3_core_service + :members: + :show-inheritance: diff --git a/docs/vale/.vale.ini b/docs/vale/.vale.ini index 76b4b40c4795fa870bdefc65e758f0476f747d5a..063fe579321aa2a2ee6d0613b7e9b35a8803c180 100644 --- a/docs/vale/.vale.ini +++ b/docs/vale/.vale.ini @@ -25,6 +25,7 @@ custom.Contractions = YES ; Using a "regular" - instead of an en dash is totally fine Microsoft.Negative = NO Microsoft.RangeFormat = NO +Microsoft.We = suggestion TokenIgnores = (:term:`\S+`), (:param \S+(?: \S+)?:), (:type \S+:), (:return \S+:), (:rtype: \S+), (:class:`\S+`) diff --git a/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt b/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt index 9d05c0923f215fd59a4b0729ffec2b91301d8e93..6509f0d5bdb354bcd9c17eab9cf79fb900653ee1 100644 --- a/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt +++ b/docs/vale/styles/config/vocabularies/geant-jargon/accept.txt @@ -31,3 +31,7 @@ OPA OIDC HTTPBearer Kentik +UTC +EARL +SURF +[Ee]nsure diff --git a/docs/vale/styles/custom/Contractions.yml b/docs/vale/styles/custom/Contractions.yml index 9c2b94a52ad8840de5539565d9e6d4d288cdd838..ecba3923af1c6e99fbacc60afbfbdcfc862c275d 100644 --- a/docs/vale/styles/custom/Contractions.yml +++ b/docs/vale/styles/custom/Contractions.yml @@ -7,7 +7,6 @@ swap: can't: cannot couldn't: could not didn't: did not - don't: do not doesn't: does not hasn't: has not haven't: have not diff --git a/gso/cli/imports.py b/gso/cli/imports.py index 33a4636a0b5010d57a7f3c06053e468bb37e191b..f1553d222d34c974584c45ef15cff56dfa55c371 100644 --- a/gso/cli/imports.py +++ b/gso/cli/imports.py @@ -18,8 +18,12 @@ from sqlalchemy.exc import SQLAlchemyError from gso.db.models import PartnerTable from gso.products import ProductType +from gso.products.product_blocks.bgp_session import IPFamily +from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_blocks.router import RouterRole +from gso.products.product_blocks.service_binding_port import VLAN_ID +from gso.products.product_types.nren_l3_core_service import NRENL3CoreServiceType from gso.services.partners import ( PartnerEmail, PartnerName, @@ -27,14 +31,15 @@ from gso.services.partners import ( get_partner_by_name, ) from gso.services.subscriptions import ( + get_active_edge_port_subscriptions, get_active_router_subscriptions, get_active_subscriptions_by_field_and_value, get_subscriptions, ) -from gso.utils.shared_enums import Vendor +from gso.utils.shared_enums import SBPType, Vendor from gso.utils.types.base_site import BaseSiteValidatorModel from gso.utils.types.interfaces import LAGMember, LAGMemberList, PhysicalPortCapacity -from gso.utils.types.ip_address import IPv4AddressType, IPv6AddressType, PortNumber +from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask, PortNumber app: typer.Typer = typer.Typer() @@ -161,6 +166,118 @@ class OpenGearImportModel(BaseModel): opengear_wan_gateway: IPv4AddressType +class EdgePortImportModel(BaseModel): + """Required fields for importing an existing :class:`gso.products.product_types.edge_port`.""" + + node: str + service_type: EdgePortType + speed: PhysicalPortCapacity + encapsulation: EncapsulationType + name: str + minimum_links: int + geant_ga_id: str | None + mac_address: str | None + partner: str + enable_lacp: bool + ignore_if_down: bool + ae_members: LAGMemberList[LAGMember] + description: str | None = None + + @field_validator("partner") + def check_if_partner_exists(cls, value: str) -> str: + """Validate that the partner exists.""" + try: + get_partner_by_name(value) + except PartnerNotFoundError as e: + msg = f"Partner {value} not found" + raise ValueError(msg) from e + + return value + + @field_validator("node") + def validate_node(cls, value: str) -> str: + """Check if the node is an active PE router in :term:`GSO`.""" + pe_routers = { + str(router.subscription_id) + for router in get_active_subscriptions_by_field_and_value("router_role", RouterRole.PE) + } + if value not in pe_routers: + msg = f"Router {value} not found" + raise ValueError(msg) + + return value + + @model_validator(mode="after") + def check_members(self) -> Self: + """Amount of :term:`LAG` members has to match and meet the minimum requirement.""" + if len(self.ae_members) < self.minimum_links: + msg = f"Number of members should be at least {self.minimum_links} (edge_port_minimum_links)" + raise ValueError(msg) + return self + + +class NRENL3CoreServiceImportModel(BaseModel): + """Import :term:`NREN` L3 Core Service model.""" + + class BaseBGPPeer(BaseModel): + """Base BGP Peer model.""" + + bfd_enabled: bool = False + bfd_interval: int | None = None + bfd_multiplier: int | None = None + has_custom_policies: bool = False + authentication_key: str + multipath_enabled: bool = False + send_default_route: bool = False + is_passive: bool = False + peer_address: IPAddress + families: list[IPFamily] + is_multi_hop: bool + rtbh_enabled: bool # whether Remote Triggered Blackhole is enabled + + class ServiceBindingPort(BaseModel): + """Service Binding model.""" + + edge_port: str + ap_type: str + geant_sid: str + sbp_type: SBPType = SBPType.L3 + is_tagged: bool = False + vlan_id: VLAN_ID + custom_firewall_filters: bool = False + ipv4_address: IPv4AddressType + ipv4_mask: IPV4Netmask + ipv6_address: IPv6AddressType + ipv6_mask: IPV6Netmask + is_multi_hop: bool = True + bgp_peers: list["NRENL3CoreServiceImportModel.BaseBGPPeer"] + + partner: str + service_binding_ports: list[ServiceBindingPort] + + @field_validator("partner") + def check_if_partner_exists(cls, value: str) -> str: + """Validate that the partner exists.""" + try: + get_partner_by_name(value) + except PartnerNotFoundError as e: + msg = f"Partner {value} not found" + raise ValueError(msg) from e + + return value + + @field_validator("service_binding_ports") + def validate_node(cls, value: list[ServiceBindingPort]) -> list[ServiceBindingPort]: + """Check if the Service Binding Ports are valid.""" + edge_ports = [str(subscription["subscription_id"]) for subscription in get_active_edge_port_subscriptions()] + for sbp in value: + if sbp.edge_port not in edge_ports: + msg = f"Edge Port {sbp.edge_port} not found" + raise ValueError(msg) + + return value + + T = TypeVar( "T", SiteImportModel, @@ -169,6 +286,8 @@ T = TypeVar( SuperPopSwitchImportModel, OfficeRouterImportModel, OpenGearImportModel, + EdgePortImportModel, + NRENL3CoreServiceImportModel, ) common_filepath_option = typer.Option( @@ -219,7 +338,7 @@ def _generic_import_product( successfully_imported_data = [] data = _read_data(file_path) for details in data: - details["partner"] = "GEANT" + details["partner"] = details.get("partner", "GEANT") typer.echo(f"Creating imported {name_key}: {details[name_key]}") try: initial_data = import_model(**details) @@ -297,6 +416,38 @@ def import_opengear(filepath: str = common_filepath_option) -> None: ) +@app.command() +def import_edge_port(filepath: str = common_filepath_option) -> None: + """Import Edge Port into GSO.""" + successfully_imported_data = [] + data = _read_data(Path(filepath)) + for edge_port in data: + typer.echo(f"Importing Edge Port {edge_port["name"]} on {edge_port["node"]}. ") + try: + edge_port["node"] = _get_router_subscription_id(edge_port["node"]) + initial_data = EdgePortImportModel(**edge_port) + start_process("create_imported_edge_port", [initial_data.model_dump()]) + successfully_imported_data.append(edge_port["name"]) + typer.echo(f"Successfully imported Edge Port {edge_port["name"]} on {edge_port["node"]}.") + except ValidationError as e: + typer.echo(f"Validation error: {e}") + + typer.echo("Waiting for the dust to settle before moving on the importing new products...") + time.sleep(1) + + edge_port_ids = get_subscriptions( + [ProductType.IMPORTED_EDGE_PORT], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=["subscription_id"] + ) + for subscription_id in edge_port_ids: + typer.echo(f"Migrating Edge Port {subscription_id}") + start_process("import_edge_port", [subscription_id]) + + if successfully_imported_data: + typer.echo("Successfully imported Edge Ports:") + for item in successfully_imported_data: + typer.echo(f"- {item}") + + @app.command() def import_iptrunks(filepath: str = common_filepath_option) -> None: """Import IP trunks into GSO.""" @@ -390,3 +541,43 @@ def import_partners(file_path: str = typer.Argument(..., help="Path to the CSV f typer.echo(f"Failed to import partners: {e}") finally: db.session.close() + + +@app.command() +def import_nren_l3_core_service(filepath: str = common_filepath_option) -> None: + """Import :term:`NREN` L3 Core Services into :term:`GSO`.""" + successfully_imported_data = [] + nren_l3_core_service_list = _read_data(Path(filepath)) + + for nren_l3_core_service in nren_l3_core_service_list: + partner = nren_l3_core_service["partner"] + service_type = NRENL3CoreServiceType(nren_l3_core_service["service_type"]) + typer.echo(f"Creating imported {service_type} for {partner}") + + try: + initial_data = NRENL3CoreServiceImportModel(**nren_l3_core_service) + start_process("create_imported_nren_l3_core_service", [initial_data.model_dump()]) + edge_ports = [sbp["edge_port"] for sbp in nren_l3_core_service["service_binding_ports"]] + successfully_imported_data.append(edge_ports) + typer.echo(f"Successfully created imported {service_type} for {partner}") + except ValidationError as e: + typer.echo(f"Validation error: {e}") + + typer.echo("Waiting for the dust to settle before importing new products...") + time.sleep(1) + + # Migrate new products from imported to "full" counterpart. + imported_products = get_subscriptions( + product_types=[ProductType.IMPORTED_GEANT_IP, ProductType.IMPORTED_IAS], + lifecycles=[SubscriptionLifecycle.ACTIVE], + includes=["subscription_id"], + ) + + for subscription_id in imported_products: + typer.echo(f"Importing {subscription_id}") + start_process("import_nren_l3_core_service", [subscription_id]) + + if successfully_imported_data: + typer.echo("Successfully created imported NREN L3 Core Services:") + for item in successfully_imported_data: + typer.echo(f"- {item}") diff --git a/gso/migrations/versions/2024-10-17_7412c5b7ebe4_add_import_edgeport_and_l3_core_service_.py b/gso/migrations/versions/2024-10-17_7412c5b7ebe4_add_import_edgeport_and_l3_core_service_.py new file mode 100644 index 0000000000000000000000000000000000000000..d3739209234f72cdbb8f54acb1d7e90de18a1d1c --- /dev/null +++ b/gso/migrations/versions/2024-10-17_7412c5b7ebe4_add_import_edgeport_and_l3_core_service_.py @@ -0,0 +1,99 @@ +"""Add Import EdgePort and L3 Core Service workflows.. + +Revision ID: 7412c5b7ebe4 +Revises: e1659d366925 +Create Date: 2024-10-17 13:10:38.551706 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '7412c5b7ebe4' +down_revision = 'e1659d366925' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "create_edge_port", + "target": "CREATE", + "description": "Create Edge Port", + "product_type": "EdgePort" + }, + { + "name": "modify_edge_port", + "target": "MODIFY", + "description": "Modify Edge Port", + "product_type": "EdgePort" + }, + { + "name": "terminate_edge_port", + "target": "TERMINATE", + "description": "Terminate Edge Port", + "product_type": "EdgePort" + }, + { + "name": "validate_edge_port", + "target": "SYSTEM", + "description": "Validate Edge Port Configuration", + "product_type": "EdgePort" + }, + { + "name": "create_imported_edge_port", + "target": "CREATE", + "description": "Import Edge Port", + "product_type": "ImportedEdgePort" + }, + { + "name": "import_edge_port", + "target": "MODIFY", + "description": "Import Edge Port", + "product_type": "ImportedEdgePort" + }, + { + "name": "create_nren_l3_core_service", + "target": "CREATE", + "description": "Create NREN L3 Core Service", + "product_type": "NRENL3CoreService" + }, + { + "name": "modify_nren_l3_core_service", + "target": "MODIFY", + "description": "Modify NREN L3 Core Service", + "product_type": "NRENL3CoreService" + }, + { + "name": "create_imported_nren_l3_core_service", + "target": "CREATE", + "description": "Create imported NREN L3 Core Service", + "product_type": "ImportedNRENL3CoreService" + }, + { + "name": "import_nren_l3_core_service", + "target": "MODIFY", + "description": "Import NREN L3 Core Service", + "product_type": "ImportedNRENL3CoreService" + }, + { + "name": "migrate_nren_l3_core_service", + "target": "MODIFY", + "description": "Migrate NREN L3 Core Service", + "product_type": "NRENL3CoreService" + } +] + + +def upgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + create_workflow(conn, workflow) + + +def downgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + delete_workflow(conn, workflow["name"]) diff --git a/gso/migrations/versions/2024-10-17_e1659d366925_add_edge_port_and_l3_core_service_.py b/gso/migrations/versions/2024-10-17_e1659d366925_add_edge_port_and_l3_core_service_.py new file mode 100644 index 0000000000000000000000000000000000000000..d033c263d61f6d483c44230e00e199f0cd5501a5 --- /dev/null +++ b/gso/migrations/versions/2024-10-17_e1659d366925_add_edge_port_and_l3_core_service_.py @@ -0,0 +1,536 @@ +"""Add Edge Port and L3 Core Service domain models. + +Revision ID: e1659d366925 +Revises: 51c819b28101 +Create Date: 2024-10-17 11:33:38.103939 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'e1659d366925' +down_revision = '51c819b28101' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Edge Port', 'Edge Port where a partner service terminates', 'EdgePort', 'EP', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported Edge Port', 'A pre-existing Edge Port that is imported into the service database', 'ImportedEdgePort', 'IMP_EP', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('GÉANT IP', 'A GÉANT IP subscription for R&E access', 'NRENL3CoreService', 'G_IP', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported GÉANT IP', 'A pre-existing GÉANT IP subscription that is imported into the service database', 'ImportedNRENL3CoreService', 'IMP_G_IP', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('IAS', 'An Internet Access Service for general internet access', 'NRENL3CoreService', 'IAS', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO products (name, description, product_type, tag, status) VALUES ('Imported IAS', 'A pre-existing Internet Access Service that is imported into the service database', 'ImportedNRENL3CoreService', 'IMP_IAS', 'active') RETURNING products.product_id + """)) + conn.execute(sa.text(""" +INSERT INTO fixed_inputs (name, value, product_id) VALUES ('nren_l3_core_service_type', 'IAS', (SELECT products.product_id FROM products WHERE products.name IN ('IAS'))), ('nren_l3_core_service_type', 'IMPORTED IAS', (SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS'))), ('nren_l3_core_service_type', 'GÉANT IP', (SELECT products.product_id FROM products WHERE products.name IN ('GÉANT IP'))), ('nren_l3_core_service_type', 'IMPORTED GÉANT IP', (SELECT products.product_id FROM products WHERE products.name IN ('Imported GÉANT IP'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('EdgePortBlock', 'The product block with all attributes of an Edge Port', 'EP_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('EdgePortAEMemberBlock', 'A physical interface member of an Edge Port', 'EP_AE_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('NRENL3CoreServiceBlock', 'The product block with all attributes of an NREN L3 Core Service', 'NREN_L3_CORE_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('NRENAccessPort', 'An NREN Access Port', 'NREN_AP_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('ServiceBindingPort', 'A Service Binding Port', 'SBP_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_blocks (name, description, tag, status) VALUES ('BGPSession', 'A BGP session', 'BGP_BLOCK', 'active') RETURNING product_blocks.product_block_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('send_default_route', 'This product sends a default route') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('has_custom_policies', 'This has custom policies enabled') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('is_passive', 'This product is passive') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('bfd_enabled', 'This product has BFD enabled') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('enable_lacp', 'This product has LACP enabled') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('edge_port_type', 'Type of Edge Port') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('geant_ga_id', 'GÉANT GA service ID') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('bfd_multiplier', 'BFD multiplier') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('ipv6_address', 'IPv6 Address') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('edge_port_name', 'The Edge Port name') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('encapsulation', 'Encapsulation method') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('ignore_if_down', 'Ignore if this interface goes down') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('bfd_interval', 'BFD interval') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('rtbh_enabled', 'This product has RTBH enabled') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('custom_firewall_filters', 'This product has custom firewall filters enabled') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('ap_type', 'Access Port type') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('is_multi_hop', 'This product is multi-hop') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('edge_port_description', 'Description of an Edge Port') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('is_tagged', 'This product is tagged') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('mac_address', 'A MAC address') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('geant_sid', 'GEANT SID') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('ipv4_address', 'IPV4 Address') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('peer_address', 'Peer address') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('sbp_type', 'Type of Service Binding Port') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('ipv6_mask', 'IPV6 subnet mask') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('authentication_key', 'Authentication key') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('multipath_enabled', 'Does this have multipath enabled') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('ipv4_mask', 'IPV4 netmask') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('families', 'IP families, can be v4 v6 and UNICAST or MULTICAST') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO resource_types (resource_type, description) VALUES ('member_speed', 'The member speed') RETURNING resource_types.resource_type_id + """)) + conn.execute(sa.text(""" +INSERT INTO product_product_blocks (product_id, product_block_id) VALUES ((SELECT products.product_id FROM products WHERE products.name IN ('Edge Port')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported Edge Port')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_product_blocks (product_id, product_block_id) VALUES ((SELECT products.product_id FROM products WHERE products.name IN ('IAS')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENL3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported IAS')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENL3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('GÉANT IP')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENL3CoreServiceBlock'))), ((SELECT products.product_id FROM products WHERE products.name IN ('Imported GÉANT IP')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENL3CoreServiceBlock'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENL3CoreServiceBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENAccessPort'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENAccessPort')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_name'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_description'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('enable_lacp'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('encapsulation'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('mac_address'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('member_speed'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('minimum_links'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_type'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ignore_if_down'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('geant_ga_id'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENAccessPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ap_type'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_tagged'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_id'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('sbp_type'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_mask'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_address'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_mask'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('custom_firewall_filters'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('geant_sid'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('peer_address'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_enabled'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_multiplier'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('families'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('has_custom_policies'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('authentication_key'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('multipath_enabled'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('send_default_route'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_multi_hop'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_passive'))) + """)) + conn.execute(sa.text(""" +INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('rtbh_enabled'))) + """)) + + +def downgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_name')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_name')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_description')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_description')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('enable_lacp')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('enable_lacp')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('encapsulation')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('encapsulation')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('mac_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('mac_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('member_speed')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('member_speed')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('minimum_links')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('minimum_links')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_type')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('edge_port_type')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ignore_if_down')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ignore_if_down')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('geant_ga_id')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('geant_ga_id')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENAccessPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ap_type')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENAccessPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ap_type')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_tagged')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_tagged')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_id')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('vlan_id')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('sbp_type')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('sbp_type')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_mask')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv4_mask')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_mask')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('ipv6_mask')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('custom_firewall_filters')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('custom_firewall_filters')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('geant_sid')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('geant_sid')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('peer_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('peer_address')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_enabled')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_enabled')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_interval')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_multiplier')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('bfd_multiplier')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('families')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('families')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('has_custom_policies')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('has_custom_policies')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('authentication_key')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('authentication_key')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('multipath_enabled')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('multipath_enabled')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('send_default_route')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('send_default_route')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_multi_hop')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_multi_hop')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_passive')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('is_passive')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('rtbh_enabled')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('rtbh_enabled')) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('send_default_route', 'has_custom_policies', 'is_passive', 'bfd_enabled', 'enable_lacp', 'edge_port_type', 'geant_ga_id', 'bfd_multiplier', 'ipv6_address', 'edge_port_name', 'encapsulation', 'ignore_if_down', 'bfd_interval', 'rtbh_enabled', 'custom_firewall_filters', 'ap_type', 'is_multi_hop', 'edge_port_description', 'is_tagged', 'mac_address', 'geant_sid', 'ipv4_address', 'peer_address', 'sbp_type', 'ipv6_mask', 'authentication_key', 'multipath_enabled', 'ipv4_mask', 'families', 'member_speed')) + """)) + conn.execute(sa.text(""" +DELETE FROM resource_types WHERE resource_types.resource_type IN ('send_default_route', 'has_custom_policies', 'is_passive', 'bfd_enabled', 'enable_lacp', 'edge_port_type', 'geant_ga_id', 'bfd_multiplier', 'ipv6_address', 'edge_port_name', 'encapsulation', 'ignore_if_down', 'bfd_interval', 'rtbh_enabled', 'custom_firewall_filters', 'ap_type', 'is_multi_hop', 'edge_port_description', 'is_tagged', 'mac_address', 'geant_sid', 'ipv4_address', 'peer_address', 'sbp_type', 'ipv6_mask', 'authentication_key', 'multipath_enabled', 'ipv4_mask', 'families', 'member_speed') + """)) + conn.execute(sa.text(""" +DELETE FROM product_product_blocks WHERE product_product_blocks.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('Edge Port', 'Imported Edge Port')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_product_blocks WHERE product_product_blocks.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'Imported IAS', 'GÉANT IP', 'Imported GÉANT IP')) AND product_product_blocks.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENL3CoreServiceBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('RouterBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENL3CoreServiceBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENAccessPort')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('NRENAccessPort')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('BGPSession')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('ServiceBindingPort')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortBlock')) + """)) + conn.execute(sa.text(""" +DELETE FROM fixed_inputs WHERE fixed_inputs.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'Imported IAS', 'GÉANT IP', 'Imported GÉANT IP')) AND fixed_inputs.name = 'nren_l3_core_service_type' + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instances WHERE subscription_instances.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock', 'EdgePortBlock', 'NRENL3CoreServiceBlock', 'ServiceBindingPort', 'BGPSession', 'NRENAccessPort')) + """)) + conn.execute(sa.text(""" +DELETE FROM product_blocks WHERE product_blocks.name IN ('EdgePortAEMemberBlock', 'EdgePortBlock', 'NRENL3CoreServiceBlock', 'ServiceBindingPort', 'BGPSession', 'NRENAccessPort') + """)) + conn.execute(sa.text(""" +DELETE FROM processes WHERE processes.pid IN (SELECT processes_subscriptions.pid FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'GÉANT IP', 'Imported GÉANT IP', 'Edge Port', 'Imported IAS', 'Imported Edge Port')))) + """)) + conn.execute(sa.text(""" +DELETE FROM processes_subscriptions WHERE processes_subscriptions.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'GÉANT IP', 'Imported GÉANT IP', 'Edge Port', 'Imported IAS', 'Imported Edge Port'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscription_instances WHERE subscription_instances.subscription_id IN (SELECT subscriptions.subscription_id FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'GÉANT IP', 'Imported GÉANT IP', 'Edge Port', 'Imported IAS', 'Imported Edge Port'))) + """)) + conn.execute(sa.text(""" +DELETE FROM subscriptions WHERE subscriptions.product_id IN (SELECT products.product_id FROM products WHERE products.name IN ('IAS', 'GÉANT IP', 'Imported GÉANT IP', 'Edge Port', 'Imported IAS', 'Imported Edge Port')) + """)) + conn.execute(sa.text(""" +DELETE FROM products WHERE products.name IN ('IAS', 'GÉANT IP', 'Imported GÉANT IP', 'Edge Port', 'Imported IAS', 'Imported Edge Port') + """)) diff --git a/gso/products/__init__.py b/gso/products/__init__.py index 9278fbe752d1fc4614f89c3f60c72ef3021908b7..ad3c2d2a7be2d9c863541d38d950100b0acb59a8 100644 --- a/gso/products/__init__.py +++ b/gso/products/__init__.py @@ -8,8 +8,10 @@ from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY from pydantic_forms.types import strEnum +from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort from gso.products.product_types.iptrunk import ImportedIptrunk, Iptrunk from gso.products.product_types.lan_switch_interconnect import LanSwitchInterconnect +from gso.products.product_types.nren_l3_core_service import ImportedNRENL3CoreService, NRENL3CoreService from gso.products.product_types.office_router import ImportedOfficeRouter, OfficeRouter from gso.products.product_types.opengear import ImportedOpengear, Opengear from gso.products.product_types.pop_vlan import PopVlan @@ -37,6 +39,12 @@ class ProductName(strEnum): IMPORTED_OFFICE_ROUTER = "Imported office router" OPENGEAR = "Opengear" IMPORTED_OPENGEAR = "Imported Opengear" + EDGE_PORT = "Edge Port" + IMPORTED_EDGE_PORT = "Imported Edge Port" + GEANT_IP = "GÉANT IP" + IMPORTED_GEANT_IP = "Imported GÉANT IP" + IAS = "IAS" + IMPORTED_IAS = "Imported IAS" class ProductType(strEnum): @@ -57,6 +65,12 @@ class ProductType(strEnum): IMPORTED_OFFICE_ROUTER = ImportedOfficeRouter.__name__ OPENGEAR = Opengear.__name__ IMPORTED_OPENGEAR = Opengear.__name__ + EDGE_PORT = EdgePort.__name__ + IMPORTED_EDGE_PORT = ImportedEdgePort.__name__ + GEANT_IP = NRENL3CoreService.__name__ + IMPORTED_GEANT_IP = ImportedNRENL3CoreService.__name__ + IAS = NRENL3CoreService.__name__ + IMPORTED_IAS = ImportedNRENL3CoreService.__name__ SUBSCRIPTION_MODEL_REGISTRY.update( @@ -76,5 +90,11 @@ SUBSCRIPTION_MODEL_REGISTRY.update( ProductName.IMPORTED_OFFICE_ROUTER.value: ImportedOfficeRouter, ProductName.OPENGEAR.value: Opengear, ProductName.IMPORTED_OPENGEAR.value: ImportedOpengear, + ProductName.EDGE_PORT.value: EdgePort, + ProductName.IMPORTED_EDGE_PORT.value: ImportedEdgePort, + ProductName.GEANT_IP.value: NRENL3CoreService, + ProductName.IMPORTED_GEANT_IP.value: ImportedNRENL3CoreService, + ProductName.IAS.value: NRENL3CoreService, + ProductName.IMPORTED_IAS.value: ImportedNRENL3CoreService, }, ) diff --git a/gso/products/product_blocks/bgp_session.py b/gso/products/product_blocks/bgp_session.py new file mode 100644 index 0000000000000000000000000000000000000000..65c5f0a0908e6c6750dac9abae25d23290d1367c --- /dev/null +++ b/gso/products/product_blocks/bgp_session.py @@ -0,0 +1,82 @@ +""":term:`BGP` session product block.""" + +import strawberry +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle +from pydantic import Field +from pydantic_forms.types import strEnum + +from gso.utils.types.ip_address import IPAddress + + +@strawberry.enum +class IPFamily(strEnum): + """Possible IP families of a :term:`BGP` peering.""" + + V4UNICAST = "v4unicast" + V6UNICAST = "v6unicast" + V4MULTICAST = "v4multicast" + V6MULTICAST = "v6multicast" + + +class BGPSessionInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="BGPSession"): + """A :term:`BGP` session that is currently inactive. See :class:`BGPSession`.""" + + peer_address: IPAddress | None = None + bfd_enabled: bool | None = None + bfd_interval: int | None = None + bfd_multiplier: int | None = None + families: list[IPFamily] = Field(default_factory=list) + has_custom_policies: bool | None = None + authentication_key: str | None = None + multipath_enabled: bool | None = None + send_default_route: bool | None = None + is_multi_hop: bool = False + is_passive: bool = False + rtbh_enabled: bool = False + + +class BGPSessionProvisioning(BGPSessionInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A :term:`BGP` session that is currently being provisioned. See :class:`BGPSession`.""" + + peer_address: IPAddress + bfd_enabled: bool + bfd_interval: int | None = None + bfd_multiplier: int | None = None + families: list[IPFamily] + has_custom_policies: bool + authentication_key: str + multipath_enabled: bool + send_default_route: bool + is_multi_hop: bool + is_passive: bool + rtbh_enabled: bool + + +class BGPSession(BGPSessionProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """A :term:`BGP` session that is currently deployed in the network.""" + + #: The peering address of the session. + peer_address: IPAddress + #: Whether :term:`BFD` is enabled. + bfd_enabled: bool + #: The :term:`BFD` interval, if enabled. + bfd_interval: int | None = None + #: The :term:`BFD` multiplier, if enabled. + bfd_multiplier: int | None = None + #: The list of IP families enabled for this session. + families: list[IPFamily] + #: Whether any custom policies exist for this session. + has_custom_policies: bool + #: The authentication key of the :term:`BGP` session. + authentication_key: str + #: Whether multi-path is enabled. + multipath_enabled: bool + #: Whether we send a last resort route. + send_default_route: bool + #: Whether this session is multi-hop or not. Defaults to no. + is_multi_hop: bool + #: Whether this is a passive session. + is_passive: bool + #: Whether Remote Triggered Blackhole is enabled + rtbh_enabled: bool diff --git a/gso/products/product_blocks/edge_port.py b/gso/products/product_blocks/edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..3ec09b2a9efbd11616f46bfb246f7611ffedb948 --- /dev/null +++ b/gso/products/product_blocks/edge_port.py @@ -0,0 +1,122 @@ +"""Edge port product block. + +Edge port sets the boundary between Geant network and an external entity that could also be a different technological +domain still managed by GEANT. In other words, an Edge port determines where the network ends. +""" + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle, strEnum + +from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning +from gso.utils.types.interfaces import LAGMemberList, PhysicalPortCapacity + + +class EncapsulationType(strEnum): + """Types of encapsulation for edge ports. + + Null supports a single service on the port. + Dot1Q supports multiple services for one customer or services for multiple customers. + QinQ expands VLAN space by double-tagging frames. + """ + + DOT1Q = "dot1q" + QINQ = "qinq" + NULL = "null" + + +class EdgePortType(strEnum): + """Types of edge ports.""" + + CUSTOMER = "CUSTOMER" + INFRASTRUCTURE = "INFRASTRUCTURE" + PRIVATE = "PRIVATE" + PUBLIC = "PUBLIC" + RE_INTERCONNECT = "RE_INTERCONNECT" + + +class EdgePortAEMemberBlockInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="EdgePortAEMemberBlock" +): + """An inactive Edge Port AE interface.""" + + interface_name: str | None = None + interface_description: str | None = None + + +class EdgePortAEMemberBlockProvisioning(EdgePortAEMemberBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A provisional Edge Port AE interface.""" + + interface_name: str + interface_description: str | None = None + + +class EdgePortAEMemberBlock(EdgePortAEMemberBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An Edge Port AE interface.""" + + interface_name: str + interface_description: str | None = None + + +class EdgePortBlockInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="EdgePortBlock" +): + """An edge port that's currently inactive. See :class:`EdgePortBlock`.""" + + node: RouterBlockInactive | None = None + edge_port_name: str | None = None + edge_port_description: str | None = None + enable_lacp: bool | None = None + encapsulation: EncapsulationType = EncapsulationType.DOT1Q + mac_address: str | None = None + member_speed: PhysicalPortCapacity | None = None + minimum_links: int | None = None + edge_port_type: EdgePortType | None = None + ignore_if_down: bool = False + geant_ga_id: str | None = None + edge_port_ae_members: LAGMemberList[EdgePortAEMemberBlockInactive] + + +class EdgePortBlockProvisioning(EdgePortBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An edge port that's being provisioned. See :class:`EdgePortBlock`.""" + + node: RouterBlockProvisioning + edge_port_name: str + edge_port_description: str | None = None + enable_lacp: bool + encapsulation: EncapsulationType = EncapsulationType.DOT1Q + mac_address: str | None = None + member_speed: PhysicalPortCapacity + minimum_links: int | None = None + edge_port_type: EdgePortType + ignore_if_down: bool = False + geant_ga_id: str | None = None + edge_port_ae_members: LAGMemberList[EdgePortAEMemberBlockProvisioning] # type: ignore[assignment] + + +class EdgePortBlock(EdgePortBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An edge port that's currently deployed in the network.""" + + #: The router that this edge port is connected to. + node: RouterBlock + #: The name of the edge port, in our case, corresponds to the name of the :term:`LAG` interface. + edge_port_name: str + #: A description of the edge port. + edge_port_description: str | None = None + #: Indicates whether :term:`LACP` is enabled for this edge port. + enable_lacp: bool + #: The type of encapsulation used on this edge port, by default DOT1Q. + encapsulation: EncapsulationType = EncapsulationType.DOT1Q + #: The MAC address assigned to this edge port, if applicable. + mac_address: str | None = None + #: The speed capacity of each member in the physical port. + member_speed: PhysicalPortCapacity + #: The minimum number of links required for this edge port. + minimum_links: int | None = None + #: The type of edge port (e.g., customer, private, public). + edge_port_type: EdgePortType + #: If set to True, the edge port will be ignored if it is down. + ignore_if_down: bool = False + #: The GEANT GA ID associated with this edge port, if any. + geant_ga_id: str | None = None + #: A list of :term:`LAG` members associated with this edge port. + edge_port_ae_members: LAGMemberList[EdgePortAEMemberBlock] # type: ignore[assignment] diff --git a/gso/products/product_blocks/nren_l3_core_service.py b/gso/products/product_blocks/nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..37eb7616511dc34d887db0ad324dee8b59ec4f96 --- /dev/null +++ b/gso/products/product_blocks/nren_l3_core_service.py @@ -0,0 +1,60 @@ +"""Product blocks for :class:`NREN` Layer 3 Core Service products.""" + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle +from pydantic import Field + +from gso.products.product_blocks.service_binding_port import ( + ServiceBindingPort, + ServiceBindingPortInactive, + ServiceBindingPortProvisioning, +) +from gso.utils.shared_enums import APType + + +class NRENAccessPortInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="NRENAccessPort" +): + """An access port for an R&E :term:`NREN` service that is inactive.""" + + ap_type: APType | None = None + sbp: ServiceBindingPortInactive + + +class NRENAccessPortProvisioning(NRENAccessPortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An access port for an R&E :term:`NREN` service that is being provisioned.""" + + ap_type: APType + sbp: ServiceBindingPortProvisioning + + +class NRENAccessPort(NRENAccessPortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An access port for an R&E :term:`NREN` service.""" + + #: The type of Access Port + ap_type: APType + #: The corresponding :term:`SBP` of this Access Port. + sbp: ServiceBindingPort + + +class NRENL3CoreServiceBlockInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="NRENL3CoreServiceBlock" +): + """An inactive :term:`NREN` L3 Core service subscription. See :class:`NRENL3CoreServiceBlock`.""" + + nren_ap_list: list[NRENAccessPortInactive] = Field(default_factory=list) + + +class NRENL3CoreServiceBlockProvisioning( + NRENL3CoreServiceBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING] +): + """A provisioning :term:`NREN` L3 Core Service subscription. See :class:`NRENL3CoreServiceBlock`.""" + + nren_ap_list: list[NRENAccessPortProvisioning] # type: ignore[assignment] + + +class NRENL3CoreServiceBlock(NRENL3CoreServiceBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active :term:`NREN` L3 Core Service subscription block.""" + + #: The list of Access Points where this service is present. + nren_ap_list: list[NRENAccessPort] # type: ignore[assignment] diff --git a/gso/products/product_blocks/opengear.py b/gso/products/product_blocks/opengear.py index 84ed84b3b11d122f757b3e764202ce5052ca615f..14a4db847deaf6f65bd0d849f51ddabd8debf53d 100644 --- a/gso/products/product_blocks/opengear.py +++ b/gso/products/product_blocks/opengear.py @@ -43,9 +43,9 @@ class OpengearBlock(OpengearBlockProvisioning, lifecycle=[SubscriptionLifecycle. opengear_hostname: str #: The site where the Opengear device is located. opengear_site: SiteBlock - #: The WAN address of the Opengear device. + #: The :term:`WAN` address of the Opengear device. opengear_wan_address: ipaddress.IPv4Address - #: The WAN netmask of the Opengear device. + #: The :term:`WAN` netmask of the Opengear device. opengear_wan_netmask: ipaddress.IPv4Address - #: The WAN gateway of the Opengear device. + #: The :term:`WAN` gateway of the Opengear device. opengear_wan_gateway: ipaddress.IPv4Address diff --git a/gso/products/product_blocks/service_binding_port.py b/gso/products/product_blocks/service_binding_port.py new file mode 100644 index 0000000000000000000000000000000000000000..540983821f64b86e5e3c11a9118ea8b867bff61d --- /dev/null +++ b/gso/products/product_blocks/service_binding_port.py @@ -0,0 +1,78 @@ +"""Service Binding Port. + +A service binding port is used to logically attach an edge port to a customer service using a :term:`VLAN`. +""" + +from typing import Annotated + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.types import SubscriptionLifecycle +from pydantic import Field + +from gso.products.product_blocks.bgp_session import BGPSession, BGPSessionInactive, BGPSessionProvisioning +from gso.products.product_blocks.edge_port import EdgePortBlock, EdgePortBlockInactive, EdgePortBlockProvisioning +from gso.utils.shared_enums import SBPType +from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask + +VLAN_ID = Annotated[int, Field(gt=0, lt=4096)] + + +class ServiceBindingPortInactive( + ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="ServiceBindingPort" +): + """A Service Binding Port that's currently inactive. See :class:`ServiceBindingPort`.""" + + is_tagged: bool | None = None + vlan_id: VLAN_ID | None = None + sbp_type: SBPType | None = None + ipv4_address: IPv4AddressType | None = None + ipv4_mask: IPV4Netmask | None = None + ipv6_address: IPv6AddressType | None = None + ipv6_mask: IPV6Netmask | None = None + custom_firewall_filters: bool | None = None + geant_sid: str | None = None + bgp_session_list: list[BGPSessionInactive] = Field(default_factory=list) + edge_port: EdgePortBlockInactive | None = None + + +class ServiceBindingPortProvisioning(ServiceBindingPortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A Service Binding Port that's currently being provisioned. See :class:`ServiceBindingPort`.""" + + is_tagged: bool + vlan_id: VLAN_ID | None = None + sbp_type: SBPType + ipv4_address: IPv4AddressType | None = None + ipv4_mask: IPV4Netmask | None = None + ipv6_address: IPv6AddressType | None = None + ipv6_mask: IPV6Netmask | None = None + custom_firewall_filters: bool + geant_sid: str + bgp_session_list: list[BGPSessionProvisioning] # type: ignore[assignment] + edge_port: EdgePortBlockProvisioning + + +class ServiceBindingPort(ServiceBindingPortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """A service binding port that is actively used in the network.""" + + #: Whether this :term:`VLAN` is tagged or not. + is_tagged: bool + #: The :term:`VLAN` ID. + vlan_id: VLAN_ID | None = None + #: Is this service binding port layer 2 or 3? + sbp_type: SBPType + #: If layer 3, IPv4 resources. + ipv4_address: IPv4AddressType | None = None + #: IPV4 subnet mask. + ipv4_mask: IPV4Netmask | None = None + #: If layer 3, IPv6 resources. + ipv6_address: IPv6AddressType | None = None + #: IPV6 subnet mask. + ipv6_mask: IPV6Netmask | None = None + #: Any custom firewall filters that the partner may require. + custom_firewall_filters: bool + #: The GÉANT service ID of this binding port. + geant_sid: str + #: The :term:`BGP` sessions associated with this service binding port. + bgp_session_list: list[BGPSession] # type: ignore[assignment] + #: The Edge Port on which this :term:`SBP` resides. + edge_port: EdgePortBlock diff --git a/gso/products/product_types/edge_port.py b/gso/products/product_types/edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..417b6047a8d787d852057b43d52692a38569bc32 --- /dev/null +++ b/gso/products/product_types/edge_port.py @@ -0,0 +1,42 @@ +"""Product types for Edge Port.""" + +from orchestrator.domain.base import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.edge_port import ( + EdgePortBlock, + EdgePortBlockInactive, + EdgePortBlockProvisioning, +) + + +class EdgePortInactive(SubscriptionModel, is_base=True): + """An Edge Port that is inactive.""" + + edge_port: EdgePortBlockInactive + + +class EdgePortProvisioning(EdgePortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """An Edge Port that is being provisioned.""" + + edge_port: EdgePortBlockProvisioning + + +class EdgePort(EdgePortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An Edge Port that is active.""" + + edge_port: EdgePortBlock + + +class ImportedEdgePortInactive(SubscriptionModel, is_base=True): + """An imported, inactive Edge Port.""" + + edge_port: EdgePortBlockInactive + + +class ImportedEdgePort( + ImportedEdgePortInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported Edge Port that is currently active.""" + + edge_port: EdgePortBlock diff --git a/gso/products/product_types/nren_l3_core_service.py b/gso/products/product_types/nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..20aa5ab2f766588f466a5100d1d7af69cbb696b0 --- /dev/null +++ b/gso/products/product_types/nren_l3_core_service.py @@ -0,0 +1,60 @@ +""":term:`NREN` L3 Core Service product type.""" + +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle +from pydantic_forms.types import strEnum + +from gso.products.product_blocks.nren_l3_core_service import ( + NRENL3CoreServiceBlock, + NRENL3CoreServiceBlockInactive, + NRENL3CoreServiceBlockProvisioning, +) + + +class NRENL3CoreServiceType(strEnum): + """Available types of :term:`NREN` Layer 3 Core Services. + + The core services offered include GÉANT IP for R&E access, and the Internet Access Service. + """ + + GEANT_IP = "GÉANT IP" + IMPORTED_GEANT_IP = "IMPORTED GÉANT IP" + IAS = "IAS" + IMPORTED_IAS = "IMPORTED IAS" + + +class NRENL3CoreServiceInactive(SubscriptionModel, is_base=True): + """An inactive :term:`NREN` L3 Core Service subscription.""" + + nren_l3_core_service_type: NRENL3CoreServiceType + nren_l3_core_service: NRENL3CoreServiceBlockInactive + + +class NRENL3CoreServiceProvisioning(NRENL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]): + """A :term:`NREN` L3 Core Service subscription that's being provisioned.""" + + nren_l3_core_service_type: NRENL3CoreServiceType + nren_l3_core_service: NRENL3CoreServiceBlockProvisioning + + +class NRENL3CoreService(NRENL3CoreServiceProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): + """An active :term:`NREN` L3 Core Service subscription.""" + + nren_l3_core_service_type: NRENL3CoreServiceType + nren_l3_core_service: NRENL3CoreServiceBlock + + +class ImportedNRENL3CoreServiceInactive(SubscriptionModel, is_base=True): + """An imported, inactive :term:`NREN` L3 Core Service subscription.""" + + nren_l3_core_service_type: NRENL3CoreServiceType + nren_l3_core_service: NRENL3CoreServiceBlockInactive + + +class ImportedNRENL3CoreService( + ImportedNRENL3CoreServiceInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING, SubscriptionLifecycle.ACTIVE] +): + """An imported :term:`NREN` L3 Core Service subscription.""" + + nren_l3_core_service_type: NRENL3CoreServiceType + nren_l3_core_service: NRENL3CoreServiceBlock diff --git a/gso/schedules/validate_subscriptions.py b/gso/schedules/validate_subscriptions.py index 2e55b05e02e74862c29616e3df9b202f6c31e464..d1d0a70c0690fcfe5c830753f41700659f07030f 100644 --- a/gso/schedules/validate_subscriptions.py +++ b/gso/schedules/validate_subscriptions.py @@ -13,7 +13,7 @@ logger = structlog.get_logger(__name__) @celery.task -@scheduler(CronScheduleConfig(name="Subscriptions Validator", minute="10", hour="0")) +@scheduler(CronScheduleConfig(name="Subscriptions Validator", minute="10", hour="3")) def validate_subscriptions() -> None: """Validate all subscriptions using their corresponding validation workflow.""" subscriptions = get_active_insync_subscriptions() diff --git a/gso/services/kentik_client.py b/gso/services/kentik_client.py index 7849adeeec4f6347f2545c5ec76fffaf5f6ee0e1..a958aca58e074ce4de9e32c18849ca9ebf8fc048 100644 --- a/gso/services/kentik_client.py +++ b/gso/services/kentik_client.py @@ -99,6 +99,8 @@ class KentikClient: If the site is not found, return an empty dict. + .. vale off + :param str site_slug: The name of the site, should be a three-letter slug like COR or POZ. """ sites = self.get_sites() diff --git a/gso/services/librenms_client.py b/gso/services/librenms_client.py index b7c21f888ebe281ada67e64edb3e9fd671a0a655..b514f0009c814fcc0e698b07fbb8e86cb27e3150 100644 --- a/gso/services/librenms_client.py +++ b/gso/services/librenms_client.py @@ -10,7 +10,7 @@ from requests import HTTPError, Response from requests.adapters import HTTPAdapter from gso.settings import load_oss_params -from gso.utils.types.snmp import SNMPVersion +from gso.utils.shared_enums import SNMPVersion logger = logging.getLogger(__name__) diff --git a/gso/services/netbox_client.py b/gso/services/netbox_client.py index b79283cc9baf6f887ee22d63f852f711b4e89d16..3b71f49dd54fa643c5a7c465c9cd3deb68d61c35 100644 --- a/gso/services/netbox_client.py +++ b/gso/services/netbox_client.py @@ -13,6 +13,7 @@ from gso.settings import load_oss_params from gso.utils.device_info import ( DEFAULT_SITE, FEASIBLE_IP_TRUNK_LAG_RANGE, + FEASIBLE_SERVICES_LAG_RANGE, ROUTER_ROLE, TierInfo, ) @@ -289,8 +290,8 @@ class NetboxClient: interface.lag = None interface.save() - def get_available_lags(self, router_id: UUID) -> list[str]: - """Return all available :term:`LAG` not assigned to a device.""" + def get_available_lags_in_range(self, router_id: UUID, lag_range: range) -> list[str]: + """Return all available LAGs within a given range not assigned to a device.""" router_name = Router.from_subscription(router_id).router.router_fqdn device = self.get_device_by_name(router_name) @@ -299,12 +300,20 @@ class NetboxClient: interface["name"] for interface in self.netbox.dcim.interfaces.filter(device=device.name, type="lag") ] - # Generate all feasible LAGs - all_feasible_lags = [f"lag-{i}" for i in FEASIBLE_IP_TRUNK_LAG_RANGE] + # Generate all feasible LAGs in the specified range + all_feasible_lags = [f"lag-{i}" for i in lag_range] # Return available LAGs not assigned to the device return [lag for lag in all_feasible_lags if lag not in lag_interface_names] + def get_available_lags(self, router_id: UUID) -> list[str]: + """Return all available :term:`LAG` not assigned to a device.""" + return self.get_available_lags_in_range(router_id, FEASIBLE_IP_TRUNK_LAG_RANGE) + + def get_available_services_lags(self, router_id: UUID) -> list[str]: + """Return all available Edge port LAGs not assigned to a device.""" + return self.get_available_lags_in_range(router_id, FEASIBLE_SERVICES_LAG_RANGE) + @staticmethod def calculate_speed_bits_per_sec(speed: str) -> int: """Extract the numeric part from the speed.""" diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py index 33db8fd9c56a9a58391ead9d2e3d3177936002e0..1458f000439fa8bcf936a08b864eb117296caf37 100644 --- a/gso/services/subscriptions.py +++ b/gso/services/subscriptions.py @@ -246,6 +246,20 @@ def get_active_site_subscriptions(includes: list[str] | None = None) -> list[Sub ) +def get_active_edge_port_subscriptions(includes: list[str] | None = None) -> list[SubscriptionType]: + """Retrieve active Edge Port subscriptions. + + :param includes: The fields to be included in the returned Subscription objects. + :type includes: list[str] + + :return: A list of Subscription objects for Edge Ports. + :rtype: list[Subscription] + """ + return get_subscriptions( + product_types=[ProductType.EDGE_PORT], lifecycles=[SubscriptionLifecycle.ACTIVE], includes=includes + ) + + def get_site_by_name(site_name: str) -> Site: """Get a site by its name. diff --git a/gso/settings.py b/gso/settings.py index 0979dcb3a700da9c26fc6f35ebd40356b471ee4c..3596eb1cfcd6855d38c077f10f2babb1bd10ade5 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -9,15 +9,13 @@ import json import logging import os from pathlib import Path -from typing import Annotated from orchestrator.types import UUIDstr -from pydantic import EmailStr, Field +from pydantic import EmailStr from pydantic_forms.types import strEnum from pydantic_settings import BaseSettings -from typing_extensions import Doc -from gso.utils.types.ip_address import PortNumber +from gso.utils.types.ip_address import IPV4Netmask, IPV6Netmask, PortNumber logger = logging.getLogger(__name__) @@ -62,16 +60,12 @@ class InfoBloxParams(BaseSettings): password: str -V4Netmask = Annotated[int, Field(ge=0, le=32), Doc("A valid netmask for an IPv4 network or address.")] -V6Netmask = Annotated[int, Field(ge=0, le=128), Doc("A valid netmask for an IPv6 network or address.")] - - class V4NetworkParams(BaseSettings): """A set of parameters that describe an IPv4 network in InfoBlox.""" containers: list[ipaddress.IPv4Network] networks: list[ipaddress.IPv4Network] - mask: V4Netmask + mask: IPV4Netmask class V6NetworkParams(BaseSettings): @@ -79,7 +73,7 @@ class V6NetworkParams(BaseSettings): containers: list[ipaddress.IPv6Network] networks: list[ipaddress.IPv6Network] - mask: V6Netmask + mask: IPV6Netmask class ServiceNetworkParams(BaseSettings): diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index f1ede053f813aefef202d92ffd31b6d48c3908a5..ef3a1bdeae293d10fa74fbb80a550860c3792841 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -19,6 +19,7 @@ "router_role": "Router role", "geant_s_sid": "GÉANT S-SID", + "geant_sid": "GÉANT S-SID", "iptrunk_description": "IPtrunk description", "iptrunk_type": "IPtrunk type", "iptrunk_speed": "Capacity per port (in Gbits/s)", @@ -33,7 +34,13 @@ "remove_configuration": "Remove configuration from the router", "clean_up_ipam": "Clean up related entries in IPAM", "restore_isis_metric": "Restore ISIS metric to original value", - "confirm_info": "Please verify this form looks correct." + "iptrunk_ipv4_network": "IPtrunk IPv4 network", + "iptrunk_ipv6_network": "IPtrunk IPv6 network", + "isis_metric": "ISIS metric", + + "enable_lacp": "Enable LACP", + "mac_address": "MAC address", + "geant_ga_id": "GÉANT GA-ID" } }, "workflow": { @@ -44,17 +51,23 @@ "create_router": "Create Router", "create_site": "Create Site", "create_switch": "Create Switch", + "create_edge_port": "Create Edge Port", + "create_nren_l3_core_service": "Create NREN L3 Core Service", "deploy_twamp": "Deploy TWAMP", "migrate_iptrunk": "Migrate IP Trunk", + "migrate_nren_l3_core_service": "Migrate NREN L3 Core Service", "modify_isis_metric": "Modify the ISIS metric", "modify_site": "Modify Site", "modify_trunk_interface": "Modify IP Trunk interface", "modify_connection_strategy": "Modify connection strategy", "modify_router_kentik_license": "Modify device license in Kentik", + "modify_edge_port": "Modify Edge Port", + "modify_nren_l3_core_service": "Modify NREN L3 Core Service", "terminate_iptrunk": "Terminate IP Trunk", "terminate_router": "Terminate Router", "terminate_site": "Terminate Site", "terminate_switch": "Terminate Switch", + "terminate_edge_port": "Terminate Edge Port", "redeploy_base_config": "Redeploy base config", "update_ibgp_mesh": "Update iBGP mesh", "create_imported_site": "NOT FOR HUMANS -- Import existing site", @@ -63,15 +76,20 @@ "create_imported_super_pop_switch": "NOT FOR HUMANS -- Import existing super PoP switch", "create_imported_office_router": "NOT FOR HUMANS -- Import existing office router", "create_imported_opengear": "NOT FOR HUMANS -- Import existing OpenGear", + "create_imported_edge_port": "NOT FOR HUMANS -- Import existing Edge Port", + "create_imported_nren_l3_core_service": "NOT FOR HUMANS -- Import existing NREN L3 Core Service", "import_site": "NOT FOR HUMANS -- Finalize import into a Site product", "import_router": "NOT FOR HUMANS -- Finalize import into a Router product", "import_iptrunk": "NOT FOR HUMANS -- Finalize import into an IP trunk product", "import_office_router": "NOT FOR HUMANS -- Finalize import into an Office router product", "import_super_pop_switch": "NOT FOR HUMANS -- Finalize import into a Super PoP switch", "import_opengear": "NOT FOR HUMANS -- Finalize import into an OpenGear", + "import_edge_port": "NOT FOR HUMANS -- Finalize import into an Edge Port", + "import_nren_l3_core_service": "NOT FOR HUMANS -- Finalize import into a NREN L3 Core Service", "validate_iptrunk": "Validate IP Trunk configuration", "validate_router": "Validate Router configuration", "validate_switch": "Validate Switch configuration", + "validate_edge_port": "Validate Edge Port", "task_validate_geant_products": "Validation task for GEANT products", "task_send_email_notifications": "Send email notifications for failed tasks", "task_create_partners": "Create partner task", diff --git a/gso/utils/device_info.py b/gso/utils/device_info.py index 800d56779ae607a16a50a430a88a024f9cbdeb66..6ee6004c0007590f6cb476c5fca2b57aa4e765d1 100644 --- a/gso/utils/device_info.py +++ b/gso/utils/device_info.py @@ -45,8 +45,8 @@ class TierInfo: return getattr(self, name) -# The range includes values from 1 to 10 (11 is not included) FEASIBLE_IP_TRUNK_LAG_RANGE = range(1, 11) +FEASIBLE_SERVICES_LAG_RANGE = range(20, 51) # Define default values ROUTER_ROLE = {"name": "router", "slug": "router"} diff --git a/gso/utils/helpers.py b/gso/utils/helpers.py index 4a78b989d41ddd49e2ee99b5f703a9dd519a7a07..c05517a23beed1b47a5aaf37786af5fe8b8d1eee 100644 --- a/gso/utils/helpers.py +++ b/gso/utils/helpers.py @@ -4,6 +4,7 @@ import re from typing import TYPE_CHECKING from uuid import UUID +from pydantic_forms.types import UUIDstr from pydantic_forms.validators import Choice from gso import settings @@ -11,6 +12,7 @@ from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import Router from gso.services import subscriptions from gso.services.netbox_client import NetboxClient +from gso.services.partners import get_all_partners from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import PhysicalPortCapacity from gso.utils.types.ip_address import IPv4AddressType @@ -75,6 +77,18 @@ def available_lags_choices(router_id: UUID) -> Choice | None: return Choice("ae iface", zip(side_a_ae_iface_list, side_a_ae_iface_list, strict=True)) # type: ignore[arg-type] +def available_service_lags_choices(router_id: UUID) -> Choice | None: + """Return a list of available lags for a given router for services. + + For Nokia routers, return a list of available lags. + For Juniper routers, return ``None``. + """ + if get_router_vendor(router_id) != Vendor.NOKIA: + return None + side_a_ae_iface_list = NetboxClient().get_available_services_lags(router_id) + return Choice("ae iface", zip(side_a_ae_iface_list, side_a_ae_iface_list, strict=True)) # type: ignore[arg-type] + + def get_router_vendor(router_id: UUID) -> Vendor: """Retrieve the vendor of a router. @@ -177,6 +191,16 @@ def active_router_selector() -> Choice: return Choice("Select a router", zip(router_subscriptions.keys(), router_subscriptions.items(), strict=True)) # type: ignore[arg-type] +def active_pe_router_selector() -> Choice: + """Generate a dropdown selector for choosing an active PE Router in an input form.""" + routers = { + str(router.subscription_id): router.description + for router in subscriptions.get_active_subscriptions_by_field_and_value("router_role", RouterRole.PE) + } + + return Choice("Select a router", zip(routers.keys(), routers.items(), strict=True)) # type: ignore[arg-type] + + def active_switch_selector() -> Choice: """Generate a dropdown selector for choosing an active Switch in an input form.""" switch_subscriptions = { @@ -185,3 +209,42 @@ def active_switch_selector() -> Choice: } return Choice("Select a switch", zip(switch_subscriptions.keys(), switch_subscriptions.items(), strict=True)) # type: ignore[arg-type] + + +def active_edge_port_selector(*, partner_id: UUIDstr | None = None) -> Choice: + """Generate a dropdown selector for choosing an active Edge Port in an input form.""" + edge_port_subscriptions = subscriptions.get_active_edge_port_subscriptions( + includes=["subscription_id", "description", "customer_id"] + ) + + if partner_id: + # ``partner_id`` is set, so we will filter accordingly. + edge_port_subscriptions = list( + filter(lambda subscription: bool(subscription["customer_id"] == partner_id), edge_port_subscriptions) + ) + + edge_ports = {str(port["subscription_id"]): port["description"] for port in edge_port_subscriptions} + + return Choice( + "Select an Edge Port", + zip(edge_ports.keys(), edge_ports.items(), strict=True), # type: ignore[arg-type] + ) + + +def partner_choice() -> Choice: + """Return a Choice object containing a list of available partners.""" + partners = {partner["partner_id"]: partner["name"] for partner in get_all_partners()} + + return Choice("Select a partner", zip(partners.values(), partners.items(), strict=True)) # type: ignore[arg-type] + + +def validate_edge_port_number_of_members_based_on_lacp(*, number_of_members: int, enable_lacp: bool) -> None: + """Validate the number of edge port members based on the :term:`LACP` setting. + + :param number_of_members: The number of members to validate. + :param enable_lacp: Whether :term:`LACP` is enabled or not. + :raises ValueError: If the number of members is greater than 1 and :term:`LACP` is disabled. + """ + if number_of_members > 1 and not enable_lacp: + err_msg = "Number of members must be 1 if LACP is disabled." + raise ValueError(err_msg) diff --git a/gso/utils/shared_enums.py b/gso/utils/shared_enums.py index 929e5b0bc2c2553c90b36ce8f056c1f22fed74cc..a6050c9bc72de8f7554d41785b41c1e91d38a483 100644 --- a/gso/utils/shared_enums.py +++ b/gso/utils/shared_enums.py @@ -1,5 +1,7 @@ """Shared choices for the different models.""" +from enum import StrEnum + from pydantic_forms.types import strEnum @@ -15,3 +17,25 @@ class ConnectionStrategy(strEnum): IN_BAND = "IN BAND" OUT_OF_BAND = "OUT OF BAND" + + +class SNMPVersion(StrEnum): + """An enumerator for the two relevant versions of :term:`SNMP`: v2c and 3.""" + + V2C = "v2c" + V3 = "v3" + + +class APType(strEnum): + """Enumerator of the types of Access Port.""" + + PRIMARY = "PRIMARY" + BACKUP = "BACKUP" + LOAD_BALANCED = "LOAD_BALANCED" + + +class SBPType(strEnum): + """Enumerator for the two allowed types of service binding port: layer 2 or layer 3.""" + + L2 = "l2" + L3 = "l3" diff --git a/gso/utils/types/ip_address.py b/gso/utils/types/ip_address.py index 94cb8beaf498900e4785d3e222a73685f5d35bf6..6186a1f83d6a495aa591e1e96a6a2030794de7fd 100644 --- a/gso/utils/types/ip_address.py +++ b/gso/utils/types/ip_address.py @@ -18,13 +18,29 @@ def validate_ipv4_or_ipv6(value: str) -> str: return value +def validate_ipv4_or_ipv6_network(value: str) -> str: + """Validate that a value is a valid IPv4 or IPv6 network.""" + try: + ipaddress.ip_network(value) + except ValueError as e: + msg = "Enter a valid IPv4 or IPv6 network." + raise ValueError(msg) from e + else: + return value + + def _str(value: Any) -> str: return str(value) IPv4AddressType = Annotated[ipaddress.IPv4Address, PlainSerializer(_str, return_type=str, when_used="always")] +IPv4NetworkType = Annotated[ipaddress.IPv4Network, PlainSerializer(_str, return_type=str, when_used="always")] IPv6AddressType = Annotated[ipaddress.IPv6Address, PlainSerializer(_str, return_type=str, when_used="always")] +IPv6NetworkType = Annotated[ipaddress.IPv6Network, PlainSerializer(_str, return_type=str, when_used="always")] IPAddress = Annotated[str, AfterValidator(validate_ipv4_or_ipv6)] +IPNetwork = Annotated[str, AfterValidator(validate_ipv4_or_ipv6_network)] +IPV4Netmask = Annotated[int, Field(ge=0, le=32), Doc("A valid netmask for an IPv4 network or address.")] +IPV6Netmask = Annotated[int, Field(ge=0, le=128), Doc("A valid netmask for an IPv6 network or address.")] PortNumber = Annotated[ int, Field( diff --git a/gso/utils/types/snmp.py b/gso/utils/types/snmp.py deleted file mode 100644 index 03581cf970c036db37ea901d8b159d25fc480136..0000000000000000000000000000000000000000 --- a/gso/utils/types/snmp.py +++ /dev/null @@ -1,10 +0,0 @@ -"""An enumerator of SNMP version numbers.""" - -from enum import StrEnum - - -class SNMPVersion(StrEnum): - """An enumerator for the two relevant versions of :term:`SNMP`: v2c and 3.""" - - V2C = "v2c" - V3 = "v3" diff --git a/gso/utils/types/unique_field.py b/gso/utils/types/unique_field.py index 32c3d9de43560998dea262b512625de60363de44..87962bd7a2b6c1d08297fb4912c24034ec495aac 100644 --- a/gso/utils/types/unique_field.py +++ b/gso/utils/types/unique_field.py @@ -1,21 +1,27 @@ """An input field that must be unique in the database.""" +from functools import partial from typing import Annotated, TypeVar from pydantic import AfterValidator from pydantic_core.core_schema import ValidationInfo +from pydantic_forms.types import UUIDstr from gso.services import subscriptions T = TypeVar("T") -def validate_field_is_unique(value: T, info: ValidationInfo) -> T: +def validate_field_is_unique(subscription_id: UUIDstr, value: T, info: ValidationInfo) -> T: """Validate that a field is unique.""" - if len(subscriptions.get_active_subscriptions_by_field_and_value(str(info.field_name), str(value))) > 0: + matched_subscriptions = subscriptions.get_active_subscriptions_by_field_and_value(str(info.field_name), str(value)) + matched_subscriptions = list( + filter(lambda match: str(match.subscription_id) != subscription_id, matched_subscriptions) + ) + if len(matched_subscriptions) > 0: msg = f"{info.field_name} must be unique" raise ValueError(msg) return value -UniqueField = Annotated[T, AfterValidator(validate_field_is_unique)] +UniqueField = Annotated[T, AfterValidator(partial(validate_field_is_unique, ""))] diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index d94174f2c80a722eb9bc1706e3070f7f49122a0c..5a6b9d082339add5f4890d54b3654228a989d7de 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -78,3 +78,20 @@ LazyWorkflowInstance("gso.workflows.tasks.create_partners", "task_create_partner LazyWorkflowInstance("gso.workflows.tasks.modify_partners", "task_modify_partners") LazyWorkflowInstance("gso.workflows.tasks.delete_partners", "task_delete_partners") LazyWorkflowInstance("gso.workflows.tasks.clean_old_tasks", "task_clean_old_tasks") + +# Edge port workflows +LazyWorkflowInstance("gso.workflows.edge_port.create_edge_port", "create_edge_port") +LazyWorkflowInstance("gso.workflows.edge_port.modify_edge_port", "modify_edge_port") +LazyWorkflowInstance("gso.workflows.edge_port.terminate_edge_port", "terminate_edge_port") +LazyWorkflowInstance("gso.workflows.edge_port.validate_edge_port", "validate_edge_port") +LazyWorkflowInstance("gso.workflows.edge_port.create_imported_edge_port", "create_imported_edge_port") +LazyWorkflowInstance("gso.workflows.edge_port.import_edge_port", "import_edge_port") + +# NREN L3 Core Service workflows +LazyWorkflowInstance("gso.workflows.nren_l3_core_service.create_nren_l3_core_service", "create_nren_l3_core_service") +LazyWorkflowInstance("gso.workflows.nren_l3_core_service.modify_nren_l3_core_service", "modify_nren_l3_core_service") +LazyWorkflowInstance( + "gso.workflows.nren_l3_core_service.create_imported_nren_l3_core_service", "create_imported_nren_l3_core_service" +) +LazyWorkflowInstance("gso.workflows.nren_l3_core_service.import_nren_l3_core_service", "import_nren_l3_core_service") +LazyWorkflowInstance("gso.workflows.nren_l3_core_service.migrate_nren_l3_core_service", "migrate_nren_l3_core_service") diff --git a/gso/workflows/edge_port/__init__.py b/gso/workflows/edge_port/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5e496dae50c3245dcde7e606e4e5df87cde77974 --- /dev/null +++ b/gso/workflows/edge_port/__init__.py @@ -0,0 +1 @@ +"""All workflows that can be executed on Edge port.""" diff --git a/gso/workflows/edge_port/create_edge_port.py b/gso/workflows/edge_port/create_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..5977206dfaa57ce4bf271c69464e440aa0fdc722 --- /dev/null +++ b/gso/workflows/edge_port/create_edge_port.py @@ -0,0 +1,277 @@ +"""A creation workflow for adding a new edge port to the network.""" + +from typing import Annotated, Any, Self +from uuid import uuid4 + +from annotated_types import Len +from orchestrator import step, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr +from orchestrator.utils.errors import ProcessFailureError +from orchestrator.workflow import StepList, begin, done +from orchestrator.workflows.steps import resync, set_status, store_process_subscription +from orchestrator.workflows.utils import wrap_create_initial_input_form +from pydantic import AfterValidator, ConfigDict, model_validator +from pydantic_forms.validators import validate_unique_list +from pynetbox.models.dcim import Interfaces + +from gso.products.product_blocks.edge_port import EdgePortAEMemberBlockInactive, EdgePortType, EncapsulationType +from gso.products.product_types.edge_port import EdgePortInactive, EdgePortProvisioning +from gso.products.product_types.router import Router +from gso.services.lso_client import LSOState, lso_interaction +from gso.services.netbox_client import NetboxClient +from gso.services.partners import get_partner_by_id +from gso.utils.helpers import ( + active_pe_router_selector, + available_interfaces_choices, + available_service_lags_choices, + partner_choice, + validate_edge_port_number_of_members_based_on_lacp, +) +from gso.utils.types.interfaces import LAGMember, PhysicalPortCapacity +from gso.utils.types.tt_number import TTNumber +from gso.workflows.shared import create_summary_form + + +def initial_input_form_generator(product_name: str) -> FormGenerator: + """Gather information to create a new Edge Port.""" + + class CreateEdgePortForm(FormPage): + model_config = ConfigDict(title=product_name) + + tt_number: TTNumber + node: active_pe_router_selector() # type: ignore[valid-type] + partner: partner_choice() # type: ignore[valid-type] + service_type: EdgePortType + enable_lacp: bool = False + speed: PhysicalPortCapacity + encapsulation: EncapsulationType = EncapsulationType.DOT1Q + number_of_members: int + minimum_links: int + mac_address: str | None = None + ignore_if_down: bool = False + geant_ga_id: str | None = None + + @model_validator(mode="after") + def validate_number_of_members(self) -> Self: + validate_edge_port_number_of_members_based_on_lacp( + enable_lacp=self.enable_lacp, number_of_members=self.number_of_members + ) + return self + + initial_user_input = yield CreateEdgePortForm + + class EdgePortLAGMember(LAGMember): + interface_name: available_interfaces_choices( # type: ignore[valid-type] + initial_user_input.node, initial_user_input.speed + ) + + lag_ae_members = Annotated[ + list[EdgePortLAGMember], + AfterValidator(validate_unique_list), + Len( + min_length=initial_user_input.number_of_members, + max_length=initial_user_input.number_of_members, + ), + ] + + class SelectInterfaceForm(FormPage): + model_config = ConfigDict(title="Select Interfaces") + + name: available_service_lags_choices(initial_user_input.node) # type: ignore[valid-type] + description: str | None = None + ae_members: lag_ae_members + + interface_form_input_data = yield SelectInterfaceForm + + input_forms_data = initial_user_input.model_dump() | interface_form_input_data.model_dump() + summary_form_data = input_forms_data | { + "node": Router.from_subscription(initial_user_input.node).router.router_fqdn, + "partner": get_partner_by_id(initial_user_input.partner).name, + "edge_port_ae_members": input_forms_data["ae_members"], + "edge_port_name": input_forms_data["name"], + "edge_port_description": input_forms_data["description"], + "edge_port_type": input_forms_data["service_type"], + } + summary_fields = [ + "node", + "partner", + "edge_port_type", + "speed", + "encapsulation", + "minimum_links", + "mac_address", + "ignore_if_down", + "geant_ga_id", + "enable_lacp", + "edge_port_name", + "edge_port_description", + "edge_port_ae_members", + ] + yield from create_summary_form(summary_form_data, product_name, summary_fields) + return input_forms_data + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: UUIDstr) -> State: + """Create a new subscription object.""" + subscription = EdgePortInactive.from_product_id(product, partner) + + return { + "subscription": subscription, + "subscription_id": subscription.subscription_id, + } + + +@step("Initialize subscription") +def initialize_subscription( + subscription: EdgePortInactive, + node: UUIDstr, + service_type: EdgePortType, + speed: PhysicalPortCapacity, + encapsulation: EncapsulationType, + name: str, + minimum_links: int, + geant_ga_id: str | None, + mac_address: str | None, + partner: str, + enable_lacp: bool, # noqa: FBT001 + ignore_if_down: bool, # noqa: FBT001 + ae_members: list[dict[str, Any]], + description: str | None = None, +) -> State: + """Initialise the subscription object in the service database.""" + router = Router.from_subscription(node).router + subscription.edge_port.node = router + subscription.edge_port.edge_port_type = service_type + subscription.edge_port.enable_lacp = enable_lacp + subscription.edge_port.member_speed = speed + subscription.edge_port.encapsulation = encapsulation + subscription.edge_port.edge_port_name = name + subscription.edge_port.minimum_links = minimum_links + subscription.edge_port.ignore_if_down = ignore_if_down + subscription.edge_port.geant_ga_id = geant_ga_id + subscription.edge_port.mac_address = mac_address + partner_name = get_partner_by_id(partner).name + subscription.description = f"Edge Port {name} on {router.router_fqdn}, " f"{partner_name}, {geant_ga_id or ""}" + subscription.edge_port.edge_port_description = description + for member in ae_members: + subscription.edge_port.edge_port_ae_members.append( + EdgePortAEMemberBlockInactive.new(subscription_id=uuid4(), **member) + ) + subscription = EdgePortProvisioning.from_other_lifecycle(subscription, SubscriptionLifecycle.PROVISIONING) + + return {"subscription": subscription, "partner_name": partner_name} + + +@step("Reserve interfaces in NetBox") +def reserve_interfaces_in_netbox(subscription: EdgePortProvisioning) -> State: + """Create the :term:`LAG` interfaces in NetBox and attach the lag interfaces to the physical interfaces.""" + nbclient = NetboxClient() + edge_port = subscription.edge_port + # Create :term:`LAG` interfaces + lag_interface: Interfaces = nbclient.create_interface( + iface_name=edge_port.edge_port_name, + interface_type="lag", + device_name=edge_port.node.router_fqdn, + description=str(subscription.subscription_id), + enabled=True, + ) + # Attach physical interfaces to :term:`LAG` + # Update interface description to subscription ID + # Reserve interfaces + for interface in edge_port.edge_port_ae_members: + nbclient.attach_interface_to_lag( + device_name=edge_port.node.router_fqdn, + lag_name=lag_interface.name, + iface_name=interface.interface_name, + description=str(subscription.subscription_id), + ) + nbclient.reserve_interface( + device_name=edge_port.node.router_fqdn, + iface_name=interface.interface_name, + ) + return { + "subscription": subscription, + } + + +@step("Allocate interfaces in NetBox") +def allocate_interfaces_in_netbox(subscription: EdgePortProvisioning) -> None: + """Allocate the interfaces in NetBox.""" + for interface in subscription.edge_port.edge_port_ae_members: + fqdn = subscription.edge_port.node.router_fqdn + iface_name = interface.interface_name + if not fqdn or not iface_name: + msg = "FQDN and/or interface name missing in subscription" + raise ProcessFailureError(msg, details=subscription.subscription_id) + + NetboxClient().allocate_interface(device_name=fqdn, iface_name=iface_name) + + +@step("[DRY RUN] Create edge port") +def create_edge_port_dry( + subscription: dict[str, Any], tt_number: str, process_id: UUIDstr, partner_name: str +) -> LSOState: + """Create a new edge port in the network as a dry run.""" + extra_vars = { + "dry_run": True, + "subscription": subscription, + "partner_name": partner_name, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Create Edge Port", + "verb": "create", + } + + return { + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + +@step("[FOR REAL] Create edge port") +def create_edge_port_real( + subscription: dict[str, Any], tt_number: str, process_id: UUIDstr, partner_name: str +) -> LSOState: + """Create a new edge port in the network for real.""" + extra_vars = { + "dry_run": False, + "subscription": subscription, + "partner_name": partner_name, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Create Edge Port", + "verb": "create", + } + + return { + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + +@workflow( + "Create Edge Port", + initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), + target=Target.CREATE, +) +def create_edge_port() -> StepList: + """Create a new edge port in the network. + + * Create and initialise the subscription object in the service database + * Deploy configuration on the new edge port, first as a dry run + * allocate :term:`LAG` and :term:`LAG` members in the Netbox. + """ + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> reserve_interfaces_in_netbox + >> lso_interaction(create_edge_port_dry) + >> lso_interaction(create_edge_port_real) + >> allocate_interfaces_in_netbox + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/gso/workflows/edge_port/create_imported_edge_port.py b/gso/workflows/edge_port/create_imported_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..b932175f072c4b03d414777ca9896797e9fdf81f --- /dev/null +++ b/gso/workflows/edge_port/create_imported_edge_port.py @@ -0,0 +1,118 @@ +"""A creation workflow that adds an existing Edge Port to the DB.""" + +from typing import Annotated, Any +from uuid import uuid4 + +from orchestrator import workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription +from pydantic import AfterValidator, ConfigDict +from pydantic_forms.types import UUIDstr +from pydantic_forms.validators import validate_unique_list + +from gso.products import ProductName +from gso.products.product_blocks.edge_port import EdgePortAEMemberBlockInactive, EdgePortType, EncapsulationType +from gso.products.product_types.edge_port import EdgePortInactive, ImportedEdgePortInactive +from gso.products.product_types.router import Router +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name +from gso.utils.helpers import active_pe_router_selector +from gso.utils.types.interfaces import LAGMember, PhysicalPortCapacity + + +@step("Create subscription") +def create_subscription(partner: str) -> State: + """Create a new subscription object.""" + partner_id = get_partner_by_name(partner)["partner_id"] + product_id = get_product_id_by_name(ProductName.IMPORTED_EDGE_PORT) + subscription = ImportedEdgePortInactive.from_product_id(product_id, partner_id) + + return { + "subscription": subscription, + "subscription_id": subscription.subscription_id, + } + + +def initial_input_form_generator() -> FormGenerator: + """Generate a form that is filled in using information passed through the :term:`API` endpoint.""" + + class ImportEdgePort(FormPage): + model_config = ConfigDict(title="Import Router") + + node: active_pe_router_selector() # type: ignore[valid-type] + partner: str + service_type: EdgePortType + enable_lacp: bool + speed: PhysicalPortCapacity + encapsulation: EncapsulationType = EncapsulationType.DOT1Q + minimum_links: int + mac_address: str | None = None + ignore_if_down: bool = False + geant_ga_id: str | None = None + description: str | None = None + name: str + ae_members: Annotated[list[LAGMember], AfterValidator(validate_unique_list)] + + user_input = yield ImportEdgePort + + return user_input.model_dump() + + +@step("Initialize subscription") +def initialize_subscription( + subscription: EdgePortInactive, + node: UUIDstr, + service_type: EdgePortType, + speed: PhysicalPortCapacity, + encapsulation: EncapsulationType, + name: str, + minimum_links: int, + geant_ga_id: str | None, + mac_address: str | None, + partner: str, + enable_lacp: bool, # noqa: FBT001 + ignore_if_down: bool, # noqa: FBT001 + ae_members: list[dict[str, Any]], + description: str | None = None, +) -> State: + """Initialise the subscription object in the service database.""" + router = Router.from_subscription(node).router + subscription.edge_port.node = router + subscription.edge_port.edge_port_name = name + subscription.edge_port.edge_port_description = description + subscription.edge_port.enable_lacp = enable_lacp + subscription.edge_port.encapsulation = encapsulation + subscription.edge_port.mac_address = mac_address + subscription.edge_port.member_speed = speed + subscription.edge_port.minimum_links = minimum_links + subscription.edge_port.edge_port_type = service_type + subscription.edge_port.ignore_if_down = ignore_if_down + subscription.edge_port.geant_ga_id = geant_ga_id + subscription.description = f"Edge Port {name} on {router.router_fqdn}, {partner}, {geant_ga_id or ""}" + for member in ae_members: + subscription.edge_port.edge_port_ae_members.append( + EdgePortAEMemberBlockInactive.new(subscription_id=uuid4(), **member) + ) + + return {"subscription": subscription} + + +@workflow( + "Import Edge Port", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def create_imported_edge_port() -> StepList: + """Import an Edge Port without provisioning it.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/gso/workflows/edge_port/import_edge_port.py b/gso/workflows/edge_port/import_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..3193489a6606eaff8f553e4358f5621352bca613 --- /dev/null +++ b/gso/workflows/edge_port/import_edge_port.py @@ -0,0 +1,29 @@ +"""A modification workflow for migrating an ImportedEdgePort to an EdgePort subscription.""" + +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.workflow import StepList, done, init, step, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products import ProductName +from gso.products.product_types.edge_port import EdgePort, ImportedEdgePort +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create new Edge Port subscription") +def import_edge_port_subscription(subscription_id: UUIDstr) -> State: + """Take an ImportedEdgePort subscription, and turn it into an EdgePort subscription.""" + old_edge_port = ImportedEdgePort.from_subscription(subscription_id) + new_subscription_id = get_product_id_by_name(ProductName.EDGE_PORT) + new_subscription = EdgePort.from_other_product(old_edge_port, new_subscription_id) # type: ignore[arg-type] + + return {"subscription": new_subscription} + + +@workflow("Import Edge Port", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_edge_port() -> StepList: + """Modify an ImportedEdgePort subscription into an EdgePort subscription to complete the import.""" + return ( + init >> store_process_subscription(Target.MODIFY) >> unsync >> import_edge_port_subscription >> resync >> done + ) diff --git a/gso/workflows/edge_port/modify_edge_port.py b/gso/workflows/edge_port/modify_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..b2923004e4b74d730006c1f8d0998a9c3da29287 --- /dev/null +++ b/gso/workflows/edge_port/modify_edge_port.py @@ -0,0 +1,277 @@ +"""Modify an existing edge port subscription.""" + +from typing import Annotated, Any, Self +from uuid import uuid4 + +from annotated_types import Len +from orchestrator import workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, conditional, done, step +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic import AfterValidator, ConfigDict, model_validator +from pydantic_forms.types import FormGenerator, State, UUIDstr +from pydantic_forms.validators import ReadOnlyField, validate_unique_list + +from gso.products.product_blocks.edge_port import EdgePortAEMemberBlock, EncapsulationType +from gso.products.product_types.edge_port import EdgePort +from gso.services.lso_client import LSOState, lso_interaction +from gso.services.netbox_client import NetboxClient +from gso.services.partners import get_partner_by_id +from gso.utils.helpers import ( + available_interfaces_choices, + available_interfaces_choices_including_current_members, + validate_edge_port_number_of_members_based_on_lacp, +) +from gso.utils.types.interfaces import LAGMember, PhysicalPortCapacity +from gso.utils.types.tt_number import TTNumber + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Gather input from the operator on what to change about the selected edge port subscription.""" + subscription = EdgePort.from_subscription(subscription_id) + + class ModifyEdgePortForm(FormPage): + model_config = ConfigDict(title="Modify Edge Port") + + tt_number: TTNumber + enable_lacp: bool = subscription.edge_port.enable_lacp + member_speed: PhysicalPortCapacity = subscription.edge_port.member_speed + encapsulation: EncapsulationType = subscription.edge_port.encapsulation + number_of_members: int = len(subscription.edge_port.edge_port_ae_members) + minimum_links: int | None = subscription.edge_port.minimum_links or None + mac_address: str | None = subscription.edge_port.mac_address or None + ignore_if_down: bool = subscription.edge_port.ignore_if_down + geant_ga_id: str | None = subscription.edge_port.geant_ga_id or None + + @model_validator(mode="after") + def validate_number_of_members(self) -> Self: + validate_edge_port_number_of_members_based_on_lacp( + enable_lacp=self.enable_lacp, number_of_members=self.number_of_members + ) + return self + + user_input = yield ModifyEdgePortForm + + class EdgePortLAGMember(LAGMember): + interface_name: ( # type: ignore[valid-type] + available_interfaces_choices_including_current_members( + subscription.edge_port.node.owner_subscription_id, + user_input.member_speed, + subscription.edge_port.edge_port_ae_members, + ) + if user_input.member_speed == subscription.edge_port.member_speed + else ( + available_interfaces_choices(subscription.edge_port.node.owner_subscription_id, user_input.member_speed) + ) + ) + + lag_ae_members = Annotated[ + list[EdgePortLAGMember], + AfterValidator(validate_unique_list), + Len( + min_length=user_input.number_of_members, + max_length=user_input.number_of_members, + ), + ] + + current_lag_ae_members = ( + [ + EdgePortLAGMember( + interface_name=iface.interface_name, + interface_description=iface.interface_description, + ) + for iface in subscription.edge_port.edge_port_ae_members + ] + if user_input.member_speed == subscription.edge_port.member_speed + else [] + ) + + class ModifyEdgePortInterfaceForm(FormPage): + model_config = ConfigDict(title="Modify Edge Port Interface") + + name: ReadOnlyField(subscription.edge_port.edge_port_name, default_type=str) # type: ignore[valid-type] + description: str | None = subscription.edge_port.edge_port_description or None + ae_members: lag_ae_members = current_lag_ae_members + + interface_form_input = yield ModifyEdgePortInterfaceForm + + capacity_has_changed = ( + user_input.member_speed != subscription.edge_port.member_speed + or user_input.number_of_members != len(subscription.edge_port.edge_port_ae_members) + or any( + old_interface.interface_name + not in [new_interface.interface_name for new_interface in interface_form_input.ae_members] + for old_interface in subscription.edge_port.edge_port_ae_members + ) + or len(subscription.edge_port.edge_port_ae_members) != len(interface_form_input.ae_members) + ) + return user_input.model_dump() | interface_form_input.model_dump() | {"capacity_has_changed": capacity_has_changed} + + +@step("Modify edge port subscription.") +def modify_edge_port_subscription( + subscription: EdgePort, + member_speed: PhysicalPortCapacity, + encapsulation: EncapsulationType, + minimum_links: int, + mac_address: str | None, + geant_ga_id: str | None, + enable_lacp: bool, # noqa: FBT001 + ae_members: list[dict[str, str]], + ignore_if_down: bool, # noqa: FBT001 + description: str | None = None, +) -> State: + """Modify the edge port subscription with the given parameters.""" + previous_ae_members = [ + { + "interface_name": member.interface_name, + "interface_description": member.interface_description, + } + for member in subscription.edge_port.edge_port_ae_members + ] + removed_ae_members = [member for member in previous_ae_members if member not in ae_members] + subscription.edge_port.enable_lacp = enable_lacp + subscription.edge_port.member_speed = member_speed + subscription.edge_port.encapsulation = encapsulation + subscription.edge_port.minimum_links = minimum_links + subscription.edge_port.mac_address = mac_address + subscription.edge_port.ignore_if_down = ignore_if_down + subscription.edge_port.geant_ga_id = geant_ga_id + subscription.edge_port.edge_port_description = description + subscription.description = ( + f"Edge Port {subscription.edge_port.edge_port_name} on" + f" {subscription.edge_port.node.router_fqdn}," + f" {get_partner_by_id(subscription.customer_id).name}, {geant_ga_id or ""}" + ) + subscription.edge_port.edge_port_ae_members.clear() + partner_name = get_partner_by_id(subscription.customer_id).name + for member in ae_members: + subscription.edge_port.edge_port_ae_members.append(EdgePortAEMemberBlock.new(subscription_id=uuid4(), **member)) + + return { + "subscription": subscription, + "partner_name": partner_name, + "removed_ae_members": removed_ae_members, + "previous_ae_members": previous_ae_members, + } + + +@step("Update interfaces in NetBox") +def update_interfaces_in_netbox( + subscription: EdgePort, removed_ae_members: list[dict], previous_ae_members: list[dict] +) -> State: + """Update the interfaces in NetBox.""" + nbclient = NetboxClient() + # Free removed interfaces + for removed_member in removed_ae_members: + nbclient.free_interface(subscription.edge_port.node.router_fqdn, removed_member["interface_name"]) + # Attach physical interfaces to :term:`LAG` + # Update interface description to subscription ID + # Reserve interfaces + for member in subscription.edge_port.edge_port_ae_members: + if any(prev_member["interface_name"] == member.interface_name for prev_member in previous_ae_members): + continue + nbclient.attach_interface_to_lag( + device_name=subscription.edge_port.node.router_fqdn, + lag_name=subscription.edge_port.edge_port_name, + iface_name=member.interface_name, + description=str(subscription.subscription_id), + ) + nbclient.reserve_interface(subscription.edge_port.node.router_fqdn, member.interface_name) + + return {"subscription": subscription} + + +@step("[DRY RUN] Update edge port configuration.") +def update_edge_port_dry( + subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, removed_ae_members: list[dict], partner_name: str +) -> LSOState: + """Perform a dry run of updating the edge port configuration.""" + extra_vars = { + "subscription": subscription, + "partner_name": partner_name, + "dry_run": True, + "verb": "update", + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " + f"- Update Edge Port {subscription["edge_port"]["edge_port_name"]}" + f" on {subscription["edge_port"]["node"]["router_fqdn"]}", + "removed_ae_members": removed_ae_members, + } + + return { + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + "subscription": subscription, + } + + +@step("[FOR REAL] Update edge port configuration.") +def update_edge_port_real( + subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, removed_ae_members: list[str], partner_name: str +) -> LSOState: + """Update the edge port configuration.""" + extra_vars = { + "subscription": subscription, + "partner_name": partner_name, + "dry_run": False, + "verb": "update", + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} " + f"- Update Edge Port {subscription["edge_port"]["edge_port_name"]}" + f" on {subscription["edge_port"]["node"]["router_fqdn"]}", + "removed_ae_members": removed_ae_members, + } + + return { + "subscription": subscription, + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + +@step("Allocate/Deallocate interfaces in NetBox") +def allocate_interfaces_in_netbox(subscription: EdgePort, previous_ae_members: list[dict]) -> None: + """Allocate the new interfaces in NetBox and detach the old ones from the :term:`LAG`.""" + nbclient = NetboxClient() + for member in subscription.edge_port.edge_port_ae_members: + if any(member.interface_name == prev_member["interface_name"] for prev_member in previous_ae_members): + continue + nbclient.allocate_interface( + device_name=subscription.edge_port.node.router_fqdn, + iface_name=member.interface_name, + ) + + # detach the old interfaces from lag + nbclient.detach_interfaces_from_lag( + device_name=subscription.edge_port.node.router_fqdn, lag_name=subscription.edge_port.edge_port_name + ) + + +@workflow( + "Modify Edge Port", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def modify_edge_port() -> StepList: + """Modify a new edge port in the network. + + * Modify the subscription object in the service database + * Modify configuration on the new edge port, first as a dry run + * Change :term:`LAG` and :term:`LAG` members in the Netbox. + """ + capacity_has_changed = conditional(lambda state: state["capacity_has_changed"]) + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> modify_edge_port_subscription + >> capacity_has_changed(update_interfaces_in_netbox) + >> capacity_has_changed(lso_interaction(update_edge_port_dry)) + >> capacity_has_changed(lso_interaction(update_edge_port_real)) + >> capacity_has_changed(allocate_interfaces_in_netbox) + >> resync + >> done + ) diff --git a/gso/workflows/edge_port/terminate_edge_port.py b/gso/workflows/edge_port/terminate_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..72d014c31d05b2bc3325650ff6acdea271eeffd7 --- /dev/null +++ b/gso/workflows/edge_port/terminate_edge_port.py @@ -0,0 +1,94 @@ +"""Terminate an edge port in the network.""" + +from typing import Any + +from orchestrator import workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic_forms.types import FormGenerator, UUIDstr + +from gso.products.product_types.edge_port import EdgePort +from gso.services.lso_client import LSOState, lso_interaction +from gso.services.netbox_client import NetboxClient +from gso.utils.types.tt_number import TTNumber + + +def initial_input_form_generator() -> FormGenerator: + """Let the operator decide whether to delete configuration on the router, and clear up :term:`IPAM` resources.""" + + class TerminateForm(FormPage): + tt_number: TTNumber + + user_input = yield TerminateForm + return user_input.model_dump() + + +@step("[DRY RUN] Remove Edge Port") +def remove_edge_port_dry(subscription: dict[str, Any], tt_number: str, process_id: UUIDstr) -> dict[str, Any]: + """Remove an edge port from the network.""" + extra_vars = { + "subscription": subscription, + "dry_run": True, + "verb": "terminate", + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Delete Edge Port", + } + + return { + "subscription": subscription, + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + +@step("[FOR REAL] Remove Edge Port") +def remove_edge_port_real(subscription: dict[str, Any], tt_number: str, process_id: UUIDstr) -> LSOState: + """Remove an edge port from the network.""" + extra_vars = { + "subscription": subscription, + "dry_run": False, + "verb": "terminate", + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - Delete Edge Port", + } + + return { + "subscription": subscription, + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": extra_vars, + } + + +@step("Netbox Clean Up") +def netbox_clean_up(subscription: EdgePort) -> None: + """Update Netbox to remove the edge port :term:`LAG` interface and all the :term:`LAG` members.""" + nbclient = NetboxClient() + + for member in subscription.edge_port.edge_port_ae_members: + nbclient.free_interface(subscription.edge_port.node.router_fqdn, member.interface_name) + + nbclient.delete_interface(subscription.edge_port.node.router_fqdn, subscription.edge_port.edge_port_name) + + +@workflow( + "Terminate Edge Port", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.TERMINATE, +) +def terminate_edge_port() -> StepList: + """Terminate a new edge port in the network.""" + return ( + begin + >> store_process_subscription(Target.TERMINATE) + >> unsync + >> lso_interaction(remove_edge_port_dry) + >> lso_interaction(remove_edge_port_real) + >> netbox_clean_up + >> set_status(SubscriptionLifecycle.TERMINATED) + >> resync + >> done + ) diff --git a/gso/workflows/edge_port/validate_edge_port.py b/gso/workflows/edge_port/validate_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..af82afee1bb394e836ba5cc71891c4840881c5c0 --- /dev/null +++ b/gso/workflows/edge_port/validate_edge_port.py @@ -0,0 +1,93 @@ +"""Workflow for validating an existing Edge port subscription.""" + +from typing import Any + +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.utils.errors import ProcessFailureError +from orchestrator.workflow import StepList, begin, done, step, workflow +from orchestrator.workflows.steps import resync, store_process_subscription +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products.product_types.edge_port import EdgePort +from gso.services.lso_client import LSOState, anonymous_lso_interaction +from gso.services.netbox_client import NetboxClient +from gso.services.partners import get_partner_by_id + + +@step("Prepare required keys in state") +def prepare_state(subscription_id: UUIDstr) -> State: + """Add required keys to the state for the workflow to run successfully.""" + edge_port = EdgePort.from_subscription(subscription_id) + + return {"subscription": edge_port} + + +@step("Verify NetBox entries") +def verify_netbox_entries(subscription: EdgePort) -> None: + """Validate required entries for an edge port in NetBox.""" + nbclient = NetboxClient() + netbox_errors = [] + + # Raises en exception when not found. + lag = nbclient.get_interface_by_name_and_device( + subscription.edge_port.edge_port_name, subscription.edge_port.node.router_fqdn + ) + if lag.description != str(subscription.subscription_id): + netbox_errors.append( + f"Incorrect description for '{lag}', expected " + f"'{subscription.subscription_id}' but got '{lag.description}'" + ) + if not lag.enabled: + netbox_errors.append(f"NetBox interface '{lag}' is not enabled.") + for member in subscription.edge_port.edge_port_ae_members: + interface = nbclient.get_interface_by_name_and_device( + member.interface_name, subscription.edge_port.node.router_fqdn + ) + if interface.description != str(subscription.subscription_id): + netbox_errors.append( + f"Incorrect description for '{member.interface_name}', expected " + f"'{subscription.subscription_id}' but got '{interface.description}'" + ) + if not interface.enabled: + netbox_errors.append(f"NetBox interface '{member.interface_name}' is not enabled.") + + if netbox_errors: + raise ProcessFailureError(message="NetBox misconfiguration(s) found", details=str(netbox_errors)) + + +@step("Check base config for drift") +def verify_base_config(subscription: dict[str, Any]) -> LSOState: + """Workflow step for running a playbook that checks whether base config has drifted.""" + partner_name = get_partner_by_id(subscription["customer_id"]).name + return { + "playbook_name": "gap_ansible/playbooks/edge_port.yaml", + "inventory": {"all": {"hosts": {subscription["edge_port"]["node"]["router_fqdn"]: None}}}, + "extra_vars": { + "dry_run": True, + "subscription": subscription, + "partner_name": partner_name, + "verb": "create", + "is_verification_workflow": "true", + }, + } + + +@workflow( + "Validate Edge Port Configuration", target=Target.SYSTEM, initial_input_form=wrap_modify_initial_input_form(None) +) +def validate_edge_port() -> StepList: + """Validate an existing, active Edge port subscription. + + * Check correct configuration of interfaces in NetBox. + * Verify create Edge port configuration. + """ + return ( + begin + >> store_process_subscription(Target.SYSTEM) + >> prepare_state + >> verify_netbox_entries + >> anonymous_lso_interaction(verify_base_config) + >> resync + >> done + ) diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py index e10401186d3731923dc92d398bc93a0d617465fb..9074af7c3ccfd6ab3777c021a92dbce128c144a3 100644 --- a/gso/workflows/iptrunk/create_iptrunk.py +++ b/gso/workflows/iptrunk/create_iptrunk.py @@ -45,6 +45,7 @@ from gso.utils.types.interfaces import JuniperLAGMember, LAGMember, LAGMemberLis from gso.utils.types.netbox_router import NetboxEnabledRouter from gso.utils.types.tt_number import TTNumber from gso.utils.workflow_steps import prompt_sharepoint_checklist_url +from gso.workflows.shared import create_summary_form def initial_input_form_generator(product_name: str) -> FormGenerator: @@ -160,8 +161,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: side_b_ae_members: ae_members_side_b user_input_side_b = yield CreateIptrunkSideBForm - - return ( + input_forms_data = ( initial_user_input.model_dump() | verify_minimum_links.model_dump() | user_input_router_side_a.model_dump() @@ -169,6 +169,25 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: | user_input_router_side_b.model_dump() | user_input_side_b.model_dump() ) + summary_form_data = input_forms_data | {"side_a_node": router_a_fqdn, "side_b_node": router_b_fqdn} + summary_fields = [ + "geant_s_sid", + "iptrunk_type", + "iptrunk_speed", + "iptrunk_description", + "iptrunk_minimum_links", + "side_a_node", + "side_a_ae_iface", + "side_a_ae_members", + "side_a_ae_geant_a_sid", + "side_b_node", + "side_b_ae_iface", + "side_b_ae_members", + "side_b_ae_geant_a_sid", + ] + yield from create_summary_form(summary_form_data, product_name, summary_fields) + + return input_forms_data @step("Create subscription") @@ -255,7 +274,7 @@ def dig_all_hosts_v6(new_ipv6_network: str) -> None: @step("Ping all hosts in the assigned IPv4 network") def ping_all_hosts_v4(new_ipv4_network: str) -> None: - """Ping all hosts in the IPv4 network to verify they're not in use.""" + """Ping all hosts in the IPv4 network to verify they are not in use.""" unavailable_hosts = [host for host in IPv4Network(new_ipv4_network) if ping(str(host), timeout=1)] if unavailable_hosts: @@ -265,7 +284,7 @@ def ping_all_hosts_v4(new_ipv4_network: str) -> None: @step("Ping all hosts in the assigned IPv6 network") def ping_all_hosts_v6(new_ipv6_network: str) -> State: - """Ping all hosts in the IPv6 network to verify they're not in use.""" + """Ping all hosts in the IPv6 network to verify they are not in use.""" unavailable_hosts = [host for host in IPv6Network(new_ipv6_network) if ping(str(host), timeout=1)] if unavailable_hosts: diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py index 5a7cafb03dad77270754cdb9da8a1c57ba84182e..43b43e8c454a2dba2b419149f3dceffe59974025 100644 --- a/gso/workflows/iptrunk/migrate_iptrunk.py +++ b/gso/workflows/iptrunk/migrate_iptrunk.py @@ -89,7 +89,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: ): # We want to stay on the same site, so all routers that are in different sites get skipped. continue - # If migrate_to_different_site is true, we can add ALL routers to the result map + # If migrate_to_different_site is true, we can add *all* routers to the result map routers[str(router_id)] = router["description"] new_router_enum = Choice("Select a new router", zip(routers.keys(), routers.items(), strict=True)) # type: ignore[arg-type] @@ -204,7 +204,7 @@ def calculate_old_side_data(subscription: Iptrunk, replace_index: int) -> State: @step("Check Optical PRE levels on the trunk endpoint") def check_ip_trunk_optical_levels_pre(subscription: Iptrunk) -> LSOState: - """Check Optical PRE levels on the trunk.""" + """Check Optical levels on the trunk before migration.""" extra_vars = {"wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "check": "optical_pre"} return { @@ -252,7 +252,7 @@ def check_ip_trunk_optical_levels_post( def check_ip_trunk_lldp( subscription: Iptrunk, new_node: Router, new_lag_member_interfaces: list[dict], replace_index: int ) -> LSOState: - """Check LLDP on the new trunk endpoints.""" + """Check :term:`LLDP` on the new trunk endpoints.""" extra_vars = { "wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "new_node": json.loads(json_dumps(new_node)), @@ -489,7 +489,7 @@ def update_remaining_side_bfd_real( @step("Check BFD session over trunk") def check_ip_trunk_bfd(subscription: Iptrunk, new_node: Router, replace_index: int) -> LSOState: - """Check BFD session across the new trunk.""" + """Check :term:`BFD` session across the new trunk.""" extra_vars = { "wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "new_node": json.loads(json_dumps(new_node)), @@ -830,7 +830,7 @@ def migrate_iptrunk() -> StepList: * Deploy a new :term:`ISIS` interface between routers A and C * Wait for operator confirmation that :term:`ISIS` is behaving as expected * Restore the old :term:`ISIS` metric on the new trunk - * Delete the old, disabled configuration on the routers, first as a dry run + * Delete the old configuration from the routers, first as a dry run * Reflect the changes made in :term:`IPAM` * Update the subscription model in the database * Update the reserved interfaces in Netbox diff --git a/gso/workflows/iptrunk/modify_isis_metric.py b/gso/workflows/iptrunk/modify_isis_metric.py index 7d77510728354e40b9cd8a1424672a9fc499d4b9..45865e39b68f1263f9058edab376e94319ac2830 100644 --- a/gso/workflows/iptrunk/modify_isis_metric.py +++ b/gso/workflows/iptrunk/modify_isis_metric.py @@ -13,6 +13,7 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form from gso.products.product_types.iptrunk import Iptrunk from gso.services.lso_client import LSOState, lso_interaction from gso.utils.types.tt_number import TTNumber +from gso.workflows.shared import modify_summary_form def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -24,8 +25,11 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: isis_metric: int = subscription.iptrunk.iptrunk_isis_metric user_input = yield ModifyIptrunkForm - - return user_input.model_dump() + user_input = user_input.model_dump() + yield from modify_summary_form( + {"iptrunk_isis_metric": user_input["isis_metric"]}, subscription.iptrunk, ["iptrunk_isis_metric"] + ) + return user_input @step("Update subscription") diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py index 16de246238a40f40341cf2911b59471cb956df8b..3537f64b30142892b977e87eddc7ac9efa0a718c 100644 --- a/gso/workflows/iptrunk/modify_trunk_interface.py +++ b/gso/workflows/iptrunk/modify_trunk_interface.py @@ -205,7 +205,7 @@ def check_ip_trunk_connectivity(subscription: Iptrunk) -> LSOState: @step("Check LLDP on the trunk endpoints") def check_ip_trunk_lldp(subscription: Iptrunk) -> LSOState: - """Check LLDP on trunk endpoints.""" + """Check :term:`LLDP` on trunk endpoints.""" extra_vars = {"wfo_ip_trunk_json": json.loads(json_dumps(subscription)), "check": "lldp"} return { @@ -321,7 +321,7 @@ def provision_ip_trunk_iface_dry( } return { - "playbook_name": "gap_ansible/playbooks/gap_ansible/iptrunks.yaml", + "playbook_name": "gap_ansible/playbooks/iptrunks.yaml", "inventory": { "all": { "hosts": { diff --git a/gso/workflows/iptrunk/validate_iptrunk.py b/gso/workflows/iptrunk/validate_iptrunk.py index 4ca96f8b6ad70a7e6201174827632b4bad12b0d3..f66612f56c4fcfae5676b13c24f4b755d6b5965b 100644 --- a/gso/workflows/iptrunk/validate_iptrunk.py +++ b/gso/workflows/iptrunk/validate_iptrunk.py @@ -210,7 +210,7 @@ def validate_iptrunk() -> StepList: * Verify that the :term:`LAG` interfaces are correctly configured in :term:`IPAM`. * Check correct configuration of interfaces in NetBox. * Verify the configuration on both sides of the trunk is intact. - * Check the ISIS metric of the trunk. + * Check the :term:`ISIS` metric of the trunk. * Verify that TWAMP configuration is correct. If a trunk has a Juniper router on both sides, it is considered legacy and does not require validation. diff --git a/gso/workflows/nren_l3_core_service/__init__.py b/gso/workflows/nren_l3_core_service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fb652fbe787e3e976db91a9c0cdefa7c897b61f2 --- /dev/null +++ b/gso/workflows/nren_l3_core_service/__init__.py @@ -0,0 +1 @@ +""":term:`NREN` layer 3 core service workflows.""" diff --git a/gso/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.py b/gso/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..f5a2e2ea00588c4ea9893f71bbb5772c26c7f0cf --- /dev/null +++ b/gso/workflows/nren_l3_core_service/create_imported_nren_l3_core_service.py @@ -0,0 +1,123 @@ +"""A creation workflow for adding an existing NREN L3 Core Service to the service database.""" + +from uuid import uuid4 + +from orchestrator import workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, SubscriptionLifecycle +from orchestrator.workflow import StepList, begin, done, step +from orchestrator.workflows.steps import resync, set_status, store_process_subscription +from pydantic import BaseModel +from pydantic_forms.types import UUIDstr + +from gso.products import ProductName +from gso.products.product_blocks.bgp_session import BGPSession, IPFamily +from gso.products.product_blocks.nren_l3_core_service import NRENAccessPortInactive +from gso.products.product_blocks.service_binding_port import VLAN_ID, ServiceBindingPortInactive +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.nren_l3_core_service import ImportedNRENL3CoreServiceInactive, NRENL3CoreServiceType +from gso.services.partners import get_partner_by_name +from gso.services.subscriptions import get_product_id_by_name +from gso.utils.shared_enums import SBPType +from gso.utils.types.ip_address import IPAddress, IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask + + +def initial_input_form_generator() -> FormGenerator: + """Take all information passed to this workflow by the :term:`API` endpoint that was called.""" + + class BaseBGPPeer(BaseModel): + bfd_enabled: bool = False + bfd_interval: int | None = None + bfd_multiplier: int | None = None + has_custom_policies: bool = False + authentication_key: str + multipath_enabled: bool = False + send_default_route: bool = False + is_passive: bool = False + peer_address: IPAddress + families: list[IPFamily] + is_multi_hop: bool + rtbh_enabled: bool + + class ServiceBindingPort(BaseModel): + edge_port: UUIDstr + ap_type: str + geant_sid: str + sbp_type: SBPType = SBPType.L3 + is_tagged: bool = False + vlan_id: VLAN_ID + custom_firewall_filters: bool = False + ipv4_address: IPv4AddressType + ipv4_mask: IPV4Netmask + ipv6_address: IPv6AddressType + ipv6_mask: IPV6Netmask + rtbh_enabled: bool = True + is_multi_hop: bool = True + bgp_peers: list[BaseBGPPeer] + + class ImportNRENL3CoreServiceForm(FormPage): + partner: str + service_binding_ports: list[ServiceBindingPort] + service_type: NRENL3CoreServiceType + + user_input = yield ImportNRENL3CoreServiceForm + + return user_input.model_dump() + + +@step("Create subscription") +def create_subscription(partner: str, service_type: NRENL3CoreServiceType) -> dict: + """Create a new subscription object in the database.""" + partner_id = get_partner_by_name(partner)["partner_id"] + if service_type == NRENL3CoreServiceType.GEANT_IP: + product_id = get_product_id_by_name(ProductName.IMPORTED_GEANT_IP) + elif service_type == NRENL3CoreServiceType.IAS: + product_id = get_product_id_by_name(ProductName.IMPORTED_IAS) + subscription = ImportedNRENL3CoreServiceInactive.from_product_id(product_id, partner_id) + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@step("Initialize subscription") +def initialize_subscription(subscription: ImportedNRENL3CoreServiceInactive, service_binding_ports: list) -> dict: + """Initialize the subscription with the user input.""" + for service_binding_port in service_binding_ports: + edge_port_subscription = EdgePort.from_subscription(service_binding_port.pop("edge_port")) + bgp_peers = service_binding_port.pop("bgp_peers") + sbp_bgp_session_list = [BGPSession.new(subscription_id=uuid4(), **session) for session in bgp_peers] + + service_binding_port_subscription = ServiceBindingPortInactive.new( + subscription_id=uuid4(), + edge_port=edge_port_subscription.edge_port, + sbp_bgp_session_list=sbp_bgp_session_list, + **service_binding_port, + ) + subscription.nren_l3_core_service.nren_ap_list.append( + NRENAccessPortInactive.new( + subscription_id=uuid4(), + ap_type=service_binding_port["ap_type"], + sbp=service_binding_port_subscription, + ) + ) + + subscription.description = f"{subscription.product} service" + + return {"subscription": subscription} + + +@workflow( + "Create imported NREN L3 Core Service", + initial_input_form=initial_input_form_generator, + target=Target.CREATE, +) +def create_imported_nren_l3_core_service() -> StepList: + """Import a GÉANT IP without provisioning it.""" + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/gso/workflows/nren_l3_core_service/create_nren_l3_core_service.py b/gso/workflows/nren_l3_core_service/create_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..2ac1708d525591a0aa0d9a19b68b5ef89ef9a064 --- /dev/null +++ b/gso/workflows/nren_l3_core_service/create_nren_l3_core_service.py @@ -0,0 +1,316 @@ +"""Create a new NREN L3 Core Service subscription including GÉANT IP and IAS.""" + +from typing import Annotated, Any +from uuid import uuid4 + +from orchestrator.forms import FormPage +from orchestrator.forms.validators import Label +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr +from orchestrator.workflow import StepList, begin, done, 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 AfterValidator, BaseModel, ConfigDict, Field, computed_field +from pydantic_forms.validators import Divider + +from gso.products.product_blocks.bgp_session import BGPSession, IPFamily +from gso.products.product_blocks.nren_l3_core_service import NRENAccessPortInactive +from gso.products.product_blocks.service_binding_port import VLAN_ID, ServiceBindingPortInactive +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.nren_l3_core_service import NRENL3CoreService, NRENL3CoreServiceInactive +from gso.services.lso_client import LSOState, lso_interaction +from gso.utils.helpers import ( + active_edge_port_selector, + partner_choice, +) +from gso.utils.shared_enums import APType, SBPType +from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask +from gso.utils.types.tt_number import TTNumber + + +def initial_input_form_generator(product_name: str) -> FormGenerator: + """Gather input from the operator to build a new subscription object.""" + + class CreateNRENCoreServiceForm(FormPage): + model_config = ConfigDict(title=f"{product_name} - Select partner") + + tt_number: TTNumber + partner: partner_choice() # type: ignore[valid-type] + + initial_user_input = yield CreateNRENCoreServiceForm + + class EdgePortSelection(BaseModel): + edge_port: active_edge_port_selector(partner_id=initial_user_input.partner) # type: ignore[valid-type] + ap_type: APType + + def validate_edge_ports_are_unique(edge_ports: list[EdgePortSelection]) -> list[EdgePortSelection]: + """Verify if interfaces are unique.""" + port_names = [port.edge_port for port in edge_ports] + if len(port_names) != len(set(port_names)): + msg = "Edge Ports must be unique." + raise ValueError(msg) + return edge_ports + + class EdgePortSelectionForm(FormPage): + model_config = ConfigDict(title=f"{product_name} - Select Edge Ports") + info_label: Label = Field( + f"Please select the Edge Ports where this {product_name} service will terminate", exclude=True + ) + + edge_ports: Annotated[list[EdgePortSelection], AfterValidator(validate_edge_ports_are_unique)] + + selected_edge_ports = yield EdgePortSelectionForm + ep_list = selected_edge_ports.edge_ports + + class BaseBGPPeer(BaseModel): + bfd_enabled: bool = False + bfd_interval: int | None = None + bfd_multiplier: int | None = None + has_custom_policies: bool = False + authentication_key: str + multipath_enabled: bool = False + send_default_route: bool = False + is_passive: bool = False + + class IPv4BGPPeer(BaseBGPPeer): + peer_address: IPv4AddressType + add_v4_multicast: bool = Field(default=False, exclude=True) + + @computed_field # type: ignore[misc] + @property + def families(self) -> list[IPFamily]: + return [IPFamily.V4UNICAST, IPFamily.V4MULTICAST] if self.add_v4_multicast else [IPFamily.V4UNICAST] + + class IPv6BGPPeer(BaseBGPPeer): + peer_address: IPv6AddressType + add_v6_multicast: bool = Field(default=False, exclude=True) + + @computed_field # type: ignore[misc] + @property + def families(self) -> list[IPFamily]: + return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST] + + binding_port_inputs = [] + for ep_index, edge_port in enumerate(ep_list): + + class BindingPortsInputForm(FormPage): + model_config = ConfigDict(title=f"{product_name} - Configure Edge Ports ({ep_index + 1}/{len(ep_list)})") + info_label: Label = Field("Please configure the Service Binding Ports for each Edge Port.", exclude=True) + current_ep_label: Label = Field( + f"Currently configuring on {EdgePort.from_subscription(edge_port.edge_port).description} " + f"(Access Port type: {edge_port.ap_type})", + exclude=True, + ) + + geant_sid: str + is_tagged: bool = False + vlan_id: VLAN_ID + ipv4_address: IPv4AddressType + ipv4_mask: IPV4Netmask + ipv6_address: IPv6AddressType + ipv6_mask: IPV6Netmask + custom_firewall_filters: bool = False + divider: Divider = Field(None, exclude=True) + v4_bgp_peer: IPv4BGPPeer + v6_bgp_peer: IPv6BGPPeer + + binding_port_input_form = yield BindingPortsInputForm + binding_port_inputs.append( + binding_port_input_form.model_dump() + | { + "bgp_peers": [ + binding_port_input_form.v4_bgp_peer.model_dump(), + binding_port_input_form.v6_bgp_peer.model_dump(), + ] + } + ) + + return ( + initial_user_input.model_dump() + | selected_edge_ports.model_dump() + | {"binding_port_inputs": binding_port_inputs, "product_name": product_name} + ) + + +@step("Create subscription") +def create_subscription(product: UUIDstr, partner: str) -> State: + """Create a new subscription object in the database.""" + subscription = NRENL3CoreServiceInactive.from_product_id(product, partner) + + return {"subscription": subscription, "subscription_id": subscription.subscription_id} + + +@step("Initialize subscription") +def initialize_subscription( + subscription: NRENL3CoreServiceInactive, edge_ports: list[dict], binding_port_inputs: list[dict], product_name: str +) -> State: + """Take all user inputs and use them to populate the subscription model.""" + edge_port_fqdn_list = [] + for edge_port_input, sbp_input in zip(edge_ports, binding_port_inputs, strict=False): + edge_port_subscription = EdgePort.from_subscription(edge_port_input["edge_port"]) + sbp_bgp_session_list = [ + BGPSession.new(subscription_id=uuid4(), **session, rtbh_enabled=True, is_multi_hop=True) + for session in sbp_input["bgp_peers"] + ] + service_binding_port = ServiceBindingPortInactive.new( + subscription_id=uuid4(), + **sbp_input, + bgp_session_list=sbp_bgp_session_list, + sbp_type=SBPType.L3, + edge_port=edge_port_subscription.edge_port, + ) + subscription.nren_l3_core_service.nren_ap_list.append( + NRENAccessPortInactive.new( + subscription_id=uuid4(), + ap_type=edge_port_input["ap_type"], + sbp=service_binding_port, + ) + ) + edge_port_fqdn_list.append(edge_port_subscription.edge_port.node.router_fqdn) + + subscription.description = f"{product_name} service" + + return {"subscription": subscription, "edge_port_fqdn_list": edge_port_fqdn_list} + + +@step("[DRY RUN] Deploy service binding port") +def provision_sbp_dry( + subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str] +) -> LSOState: + """Perform a dry run of deploying Service Binding Ports.""" + extra_vars = { + "subscription": subscription, + "dry_run": True, + "verb": "deploy", + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Deploy config for {subscription["description"]}", + } + + return { + "playbook_name": "manage_sbp.yaml", + "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, + "extra_vars": extra_vars, + } + + +@step("[FOR REAL] Deploy service binding port") +def provision_sbp_real( + subscription: dict[str, Any], process_id: UUIDstr, tt_number: str, edge_port_fqdn_list: list[str] +) -> LSOState: + """Deploy Service Binding Ports.""" + extra_vars = { + "subscription": subscription, + "dry_run": False, + "verb": "deploy", + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Deploy config for {subscription["description"]}", + } + + return { + "playbook_name": "manage_sbp.yaml", + "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, + "extra_vars": extra_vars, + } + + +@step("Check service binding port functionality") +def check_sbp_functionality(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState: + """Check functionality of deployed Service Binding Ports.""" + extra_vars = {"subscription": subscription, "verb": "check"} + + return { + "playbook_name": "manage_sbp.yaml", + "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, + "extra_vars": extra_vars, + } + + +@step("[DRY RUN] Deploy BGP peers") +def deploy_bgp_peers_dry( + subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr +) -> LSOState: + """Perform a dry run of deploying :term:`BGP` peers.""" + extra_vars = { + "subscription": subscription, + "verb": "deploy", + "dry_run": True, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Deploying BGP peers for {subscription["description"]}", + } + + return { + "playbook_name": "manage_sbp.yaml", + "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, + "extra_vars": extra_vars, + } + + +@step("[FOR REAL] Deploy BGP peers") +def deploy_bgp_peers_real( + subscription: dict[str, Any], edge_port_fqdn_list: list[str], tt_number: str, process_id: UUIDstr +) -> LSOState: + """Deploy :term:`BGP` peers.""" + extra_vars = { + "subscription": subscription, + "verb": "deploy", + "dry_run": False, + "commit_comment": f"GSO_PROCESS_ID: {process_id} - TT_NUMBER: {tt_number} - " + f"Deploying BGP peers for {subscription["description"]}", + } + + return { + "playbook_name": "manage_sbp.yaml", + "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, + "extra_vars": extra_vars, + } + + +@step("Check BGP peers") +def check_bgp_peers(subscription: dict[str, Any], edge_port_fqdn_list: list[str]) -> LSOState: + """Check correct deployment of :term:`BGP` peers.""" + extra_vars = {"subscription": subscription, "verb": "check"} + + return { + "playbook_name": "manage_sbp.yaml", + "inventory": {"all": {"hosts": dict.fromkeys(edge_port_fqdn_list)}}, + "extra_vars": extra_vars, + } + + +@step("Update Infoblox") +def update_dns_records(subscription: NRENL3CoreService) -> State: + """Update :term:`DNS` records in Infoblox.""" + # TODO: implement + return {"subscription": subscription} + + +@workflow( + "Create NREN L3 Core Service", + initial_input_form=wrap_create_initial_input_form(initial_input_form_generator), + target=Target.CREATE, +) +def create_nren_l3_core_service() -> StepList: + """Create a new :term:`NREN` L3 Core Service subscription including GÉANT IP and IAS. + + * Create subscription object in the service database + * Deploy service binding ports + * Deploy :term:`BGP` peers + * Update :term:`DNS` records + * Set the subscription in a provisioning state in the database + """ + return ( + begin + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> initialize_subscription + >> lso_interaction(provision_sbp_dry) + >> lso_interaction(provision_sbp_real) + >> lso_interaction(check_sbp_functionality) + >> lso_interaction(deploy_bgp_peers_dry) + >> lso_interaction(deploy_bgp_peers_real) + >> lso_interaction(check_bgp_peers) + >> update_dns_records + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done + ) diff --git a/gso/workflows/nren_l3_core_service/import_nren_l3_core_service.py b/gso/workflows/nren_l3_core_service/import_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..3951924612f0c885f1076193d75d208b687e4821 --- /dev/null +++ b/gso/workflows/nren_l3_core_service/import_nren_l3_core_service.py @@ -0,0 +1,45 @@ +"""A modification workflow for migrating an ImportedGeantIP to an GeantIP subscription.""" + +from orchestrator.targets import Target +from orchestrator.types import State, UUIDstr +from orchestrator.utils.errors import ProcessFailureError +from orchestrator.workflow import StepList, done, init, step, workflow +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from gso.products import ProductName +from gso.products.product_types.nren_l3_core_service import ( + ImportedNRENL3CoreService, + NRENL3CoreService, + NRENL3CoreServiceType, +) +from gso.services.subscriptions import get_product_id_by_name + + +@step("Create imported subscription") +def import_nren_l3_core_service_subscription(subscription_id: UUIDstr) -> State: + """Take an imported subscription, and turn it into an :term:`NREN` L3 Core Service subscription.""" + old_nren_l3_core_service = ImportedNRENL3CoreService.from_subscription(subscription_id) + if old_nren_l3_core_service.nren_l3_core_service_type == NRENL3CoreServiceType.IMPORTED_GEANT_IP: + new_subscription_id = get_product_id_by_name(ProductName.GEANT_IP) + elif old_nren_l3_core_service.nren_l3_core_service_type == NRENL3CoreServiceType.IMPORTED_IAS: + new_subscription_id = get_product_id_by_name(ProductName.IAS) + else: + msg = f"This {old_nren_l3_core_service.nren_l3_core_service_type} is already imported, nothing to do." + raise ProcessFailureError(message=msg, details=old_nren_l3_core_service) + new_subscription = NRENL3CoreService.from_other_product(old_nren_l3_core_service, new_subscription_id) # type: ignore[arg-type] + + return {"subscription": new_subscription} + + +@workflow("Import NREN L3 Core Service", target=Target.MODIFY, initial_input_form=wrap_modify_initial_input_form(None)) +def import_nren_l3_core_service() -> StepList: + """Modify an imported subscription into an :term:`NREN` L3 Core Service subscription to complete the import.""" + return ( + init + >> store_process_subscription(Target.MODIFY) + >> unsync + >> import_nren_l3_core_service_subscription + >> resync + >> done + ) diff --git a/gso/workflows/nren_l3_core_service/migrate_nren_l3_core_service.py b/gso/workflows/nren_l3_core_service/migrate_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..5d42bccdad3a97ec0f4dc52b2b1d720ab89192e5 --- /dev/null +++ b/gso/workflows/nren_l3_core_service/migrate_nren_l3_core_service.py @@ -0,0 +1,99 @@ +"""A modification workflow that migrates a L3 Core Service to a new set of Edge Ports.""" + +from typing import Annotated + +from annotated_types import Len +from orchestrator import workflow +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, done, step +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic import AfterValidator, BaseModel, ConfigDict, Field +from pydantic_forms.core import FormPage +from pydantic_forms.types import FormGenerator, State, UUIDstr +from pydantic_forms.validators import Choice, Divider + +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.nren_l3_core_service import NRENL3CoreService +from gso.services.subscriptions import get_active_edge_port_subscriptions +from gso.utils.types.tt_number import TTNumber + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Gather input from the operator on what new Edge Ports this L3 Core Service should be migrated to.""" + subscription = NRENL3CoreService.from_subscription(subscription_id) + partner_id = subscription.customer_id + edge_port_count = len(subscription.nren_l3_core_service.nren_ap_list) + + def _new_edge_port_selector(pid: UUIDstr) -> Choice: + existing_ep_name_list = [ + ap.sbp.edge_port.owner_subscription_id for ap in subscription.nren_l3_core_service.nren_ap_list + ] + edge_port_subscriptions = list( + filter( + lambda ep: bool(ep["customer_id"] == pid) and ep["subscription_id"] not in existing_ep_name_list, + get_active_edge_port_subscriptions(includes=["subscription_id", "description", "customer_id"]), + ) + ) + + edge_ports = {str(port["subscription_id"]): port["description"] for port in edge_port_subscriptions} + + return Choice( + "Select an Edge Port", + zip(edge_ports.keys(), edge_ports.items(), strict=True), # type: ignore[arg-type] + ) + + class NewEdgePortSelection(BaseModel): + old_edge_port: str + new_edge_port: _new_edge_port_selector(partner_id) | str # type: ignore[valid-type] + + def _validate_new_edge_ports_are_unique(edge_ports: list[NewEdgePortSelection]) -> list[NewEdgePortSelection]: + new_edge_ports = [str(port.new_edge_port) for port in edge_ports] + if len(new_edge_ports) != len(set(new_edge_ports)): + msg = "New Edge Ports must be unique" + raise ValueError(msg) + return edge_ports + + class NRENL3CoreServiceEdgePortSelectionForm(FormPage): + model_config = ConfigDict(title=f"Migrating {subscription.product.name} to a new set of Edge Ports") + + tt_number: TTNumber + divider: Divider = Field(None, exclude=True) + edge_port_selection: Annotated[ + list[NewEdgePortSelection], + AfterValidator(_validate_new_edge_ports_are_unique), + Len(min_length=edge_port_count, max_length=edge_port_count), + ] = [ # noqa: RUF012 + NewEdgePortSelection( + old_edge_port=f"{EdgePort.from_subscription(ap.sbp.edge_port.owner_subscription_id).description} ({ + ap.ap_type + })", + new_edge_port="", + ) + for ap in subscription.nren_l3_core_service.nren_ap_list + ] + + ep_user_input = yield NRENL3CoreServiceEdgePortSelectionForm + + return {"subscription_id": subscription_id, "subscription": subscription} | ep_user_input.model_dump() + + +@step("Update subscription model") +def update_subscription_model(subscription: NRENL3CoreService, edge_port_selection: list[dict]) -> State: + """Update the subscription model with the new list of Access Ports.""" + for index, selected_port in enumerate(edge_port_selection): + subscription.nren_l3_core_service.nren_ap_list[index].sbp.edge_port = EdgePort.from_subscription( + selected_port["new_edge_port"] + ).edge_port + + return {"subscription": subscription} + + +@workflow( + "Migrate NREN L3 Core Service", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def migrate_nren_l3_core_service() -> StepList: + """Migrate a :term:`NREN` L3 Core Service to a new set of Edge Ports.""" + return begin >> store_process_subscription(Target.MODIFY) >> unsync >> update_subscription_model >> resync >> done diff --git a/gso/workflows/nren_l3_core_service/modify_nren_l3_core_service.py b/gso/workflows/nren_l3_core_service/modify_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..4331de2bcabc767d2a53904669fa2e1be7b8d724 --- /dev/null +++ b/gso/workflows/nren_l3_core_service/modify_nren_l3_core_service.py @@ -0,0 +1,302 @@ +"""A modification workflow for a :term:`NREN` L3 Core Service subscription.""" + +from typing import Annotated, Any +from uuid import uuid4 + +from orchestrator import begin, conditional, done, step, workflow +from orchestrator.forms import FormPage +from orchestrator.targets import Target +from orchestrator.types import FormGenerator, UUIDstr +from orchestrator.workflow import StepList +from orchestrator.workflows.steps import resync, store_process_subscription, unsync +from orchestrator.workflows.utils import wrap_modify_initial_input_form +from pydantic import AfterValidator, BaseModel, ConfigDict, Field, computed_field +from pydantic_forms.types import State +from pydantic_forms.validators import Divider, Label + +from gso.products.product_blocks.bgp_session import BGPSession, IPFamily +from gso.products.product_blocks.nren_l3_core_service import NRENAccessPort +from gso.products.product_blocks.service_binding_port import VLAN_ID, ServiceBindingPort +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.nren_l3_core_service import NRENL3CoreService +from gso.utils.helpers import active_edge_port_selector +from gso.utils.shared_enums import APType, SBPType +from gso.utils.types.ip_address import IPv4AddressType, IPV4Netmask, IPv6AddressType, IPV6Netmask + + +def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: + """Get input about added, removed, and modified Access Ports.""" + subscription = NRENL3CoreService.from_subscription(subscription_id) + product_name = subscription.product.name + + class AccessPortSelection(BaseModel): + edge_port: active_edge_port_selector(partner_id=subscription.customer_id) | str # type: ignore[valid-type] + ap_type: APType + + def validate_edge_ports_are_unique(access_ports: list[AccessPortSelection]) -> list[AccessPortSelection]: + """Verify if interfaces are unique.""" + edge_ports = [str(port.edge_port) for port in access_ports] + if len(edge_ports) != len(set(edge_ports)): + msg = "Edge Ports must be unique." + raise ValueError(msg) + return access_ports + + class ModifyAccessPortsForm(FormPage): + model_config = ConfigDict(title=f"Modify {product_name}") + access_ports: Annotated[list[AccessPortSelection], AfterValidator(validate_edge_ports_are_unique)] = [ # noqa: RUF012 + AccessPortSelection( + edge_port=str(access_port.sbp.edge_port.owner_subscription_id), + ap_type=access_port.ap_type, + ) + for access_port in subscription.nren_l3_core_service.nren_ap_list + ] + + access_port_input = yield ModifyAccessPortsForm + input_ap_list = access_port_input.access_ports + input_ep_list = [str(ap.edge_port) for ap in input_ap_list] + existing_ep_list = [ + str(ap.sbp.edge_port.owner_subscription_id) for ap in subscription.nren_l3_core_service.nren_ap_list + ] + + class BaseBGPPeer(BaseModel): + bfd_enabled: bool = False + bfd_interval: int | None = None + bfd_multiplier: int | None = None + has_custom_policies: bool = False + authentication_key: str + multipath_enabled: bool = False + send_default_route: bool = False + is_passive: bool = False + + class IPv4BGPPeer(BaseBGPPeer): + peer_address: IPv4AddressType + add_v4_multicast: bool = Field(default=False, exclude=True) + + @computed_field # type: ignore[misc] + @property + def families(self) -> list[IPFamily]: + return [IPFamily.V4UNICAST, IPFamily.V4MULTICAST] if self.add_v4_multicast else [IPFamily.V4UNICAST] + + class IPv6BGPPeer(BaseBGPPeer): + peer_address: IPv6AddressType + add_v6_multicast: bool = Field(default=False, exclude=True) + + @computed_field # type: ignore[misc] + @property + def families(self) -> list[IPFamily]: + return [IPFamily.V6UNICAST, IPFamily.V6MULTICAST] if self.add_v6_multicast else [IPFamily.V6UNICAST] + + # There are three possible scenarios for Edge Ports. They can be added, removed, or their relevant SBP can be + # modified. + removed_ap_list = [ + access_port.subscription_instance_id + for access_port in subscription.nren_l3_core_service.nren_ap_list + if str(access_port.sbp.edge_port.owner_subscription_id) not in input_ep_list + ] + modified_ap_list = [ + ( + access_port, + next( + ( + ap.ap_type + for ap in input_ap_list + if str(ap.edge_port) == str(access_port.sbp.edge_port.owner_subscription_id) + ), + None, + ), + ) + for access_port in subscription.nren_l3_core_service.nren_ap_list + if str(access_port.sbp.edge_port.owner_subscription_id) in input_ep_list + ] + added_ap_list = [ + (ep, next(ap.ap_type for ap in input_ap_list if str(ap.edge_port) == ep)) + for ep in input_ep_list + if ep not in existing_ep_list + ] + + # First, the user can modify existing Edge Ports + sbp_inputs = [] + for access_port_index, ap_entry in enumerate(modified_ap_list): + access_port, new_ap_type = ap_entry + current_sbp = access_port.sbp + v4_peer = next(peer for peer in current_sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families) + v6_peer = next(peer for peer in current_sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families) + + class BindingPortModificationForm(FormPage): + model_config = ConfigDict( + title=f"{product_name} - Modify Edge Port configuration " + f"({access_port_index + 1}/{len(input_ap_list)})" + ) + current_ep_label: Label = Field( + f"Currently configuring on {access_port.sbp.edge_port.edge_port_description} " + f"(Access Port type: {access_port.ap_type})", + exclude=True, + ) + + geant_sid: str = current_sbp.geant_sid + is_tagged: bool = current_sbp.is_tagged + # The SBP model doesn't require these three fields, but in the case of GÉANT IP OR IAS this will never + # occur since it's a layer 3 service. The ignore statements are there to put our type checker at ease. + vlan_id: VLAN_ID = current_sbp.vlan_id # type: ignore[assignment] + ipv4_address: IPv4AddressType = current_sbp.ipv4_address # type: ignore[assignment] + ipv4_mask: IPV4Netmask = current_sbp.ipv4_mask # type: ignore[assignment] + ipv6_address: IPv6AddressType = current_sbp.ipv6_address # type: ignore[assignment] + ipv6_mask: IPV6Netmask = current_sbp.ipv6_mask # type: ignore[assignment] + custom_firewall_filters: bool = current_sbp.custom_firewall_filters + divider: Divider = Field(None, exclude=True) + v4_bgp_peer: IPv4BGPPeer = IPv4BGPPeer( + **v4_peer.model_dump(exclude=set("families")), + add_v4_multicast=bool(IPFamily.V4MULTICAST in v4_peer.families), + ) + v6_bgp_peer: IPv6BGPPeer = IPv6BGPPeer( + **v6_peer.model_dump(exclude=set("families")), + add_v6_multicast=bool(IPFamily.V6MULTICAST in v6_peer.families), + ) + + binding_port_input_form = yield BindingPortModificationForm + sbp_inputs.append( + binding_port_input_form.model_dump() + | { + "new_ap_type": new_ap_type, + "current_sbp_id": current_sbp.subscription_instance_id, + } + ) + + # Second, newly added Edge Ports are configured + binding_port_inputs = [] + for ap_index, access_port_tuple in enumerate(added_ap_list): + edge_port_id, ap_type = access_port_tuple + + class BindingPortInputForm(FormPage): + model_config = ConfigDict( + title=f"{product_name} - Configure new Edge Port " + f"({len(modified_ap_list) + ap_index + 1}/{len(input_ap_list)})" + ) + info_label: Label = Field( + "Please configure the Service Binding Ports for each newly added Edge Port", exclude=True + ) + current_ep_label: Label = Field( + f"Currently configuring on {EdgePort.from_subscription(edge_port_id).description} " + f"(Access Port type: {ap_type})", + exclude=True, + ) + + geant_sid: str + is_tagged: bool = False + vlan_id: VLAN_ID + ipv4_address: IPv4AddressType + ipv6_address: IPv6AddressType + custom_firewall_filters: bool = False + divider: Divider = Field(None, exclude=True) + v4_bgp_peer: IPv4BGPPeer + v6_bgp_peer: IPv6BGPPeer + + binding_port_input_form = yield BindingPortInputForm + binding_port_inputs.append( + binding_port_input_form.model_dump() + | { + "bgp_peers": [ + binding_port_input_form.v4_bgp_peer.model_dump(), + binding_port_input_form.v6_bgp_peer.model_dump(), + ], + "edge_port_id": edge_port_id, + "ap_type": ap_type, + } + ) + + return access_port_input.model_dump() | { + "added_service_binding_ports": binding_port_inputs, + "removed_access_ports": removed_ap_list, + "modified_sbp_list": sbp_inputs, + } + + +@step("Clean up removed Edge Ports") +def remove_old_sbp_blocks(subscription: NRENL3CoreService, removed_access_ports: list[UUIDstr]) -> State: + """Remove old :term:`SBP` product blocks from the GÉANT IP subscription.""" + subscription.nren_l3_core_service.nren_ap_list = [ + ap + for ap in subscription.nren_l3_core_service.nren_ap_list + if str(ap.subscription_instance_id) not in removed_access_ports + ] + + return {"subscription": subscription} + + +@step("Modify existing Service Binding Ports") +def modify_existing_sbp_blocks(subscription: NRENL3CoreService, modified_sbp_list: list[dict[str, Any]]) -> State: + """Update the subscription model.""" + for access_port in subscription.nren_l3_core_service.nren_ap_list: + current_sbp = access_port.sbp + modified_sbp_data = next( + sbp for sbp in modified_sbp_list if sbp["current_sbp_id"] == str(current_sbp.subscription_instance_id) + ) + + v4_peer = next(peer for peer in current_sbp.bgp_session_list if IPFamily.V4UNICAST in peer.families) + for attribute in modified_sbp_data["v4_bgp_peer"]: + setattr(v4_peer, attribute, modified_sbp_data["v4_bgp_peer"][attribute]) + + v6_peer = next(peer for peer in current_sbp.bgp_session_list if IPFamily.V6UNICAST in peer.families) + for attribute in modified_sbp_data["v6_bgp_peer"]: + setattr(v6_peer, attribute, modified_sbp_data["v6_bgp_peer"][attribute]) + + current_sbp.bgp_session_list = [v4_peer, v6_peer] + current_sbp.vlan_id = modified_sbp_data["vlan_id"] + current_sbp.geant_sid = modified_sbp_data["geant_sid"] + current_sbp.is_tagged = modified_sbp_data["is_tagged"] + current_sbp.ipv4_address = modified_sbp_data["ipv4_address"] + current_sbp.ipv6_address = modified_sbp_data["ipv6_address"] + current_sbp.custom_firewall_filters = modified_sbp_data["custom_firewall_filters"] + access_port.ap_type = modified_sbp_data["new_ap_type"] + + return {"subscription": subscription} + + +@step("Instantiate new Service Binding Ports") +def create_new_sbp_blocks(subscription: NRENL3CoreService, added_service_binding_ports: list[dict[str, Any]]) -> State: + """Add new two :term:`SBP` to the :term:`NREN` L3 Core Service subscription.""" + for sbp_input in added_service_binding_ports: + edge_port = EdgePort.from_subscription(sbp_input["edge_port_id"]) + bgp_session_list = [ + BGPSession.new(subscription_id=uuid4(), **session, rtbh_enabled=True, is_multi_hop=True) + for session in sbp_input["bgp_peers"] + ] + service_binding_port = ServiceBindingPort.new( + subscription_id=uuid4(), + **sbp_input, + bgp_session_list=bgp_session_list, + sbp_type=SBPType.L3, + edge_port=edge_port.edge_port, + ) + subscription.nren_l3_core_service.nren_ap_list.append( + NRENAccessPort.new( + subscription_id=uuid4(), + ap_type=sbp_input["ap_type"], + sbp=service_binding_port, + ) + ) + + return {"subscription": subscription} + + +@workflow( + "Modify NREN L3 Core Service", + initial_input_form=wrap_modify_initial_input_form(initial_input_form_generator), + target=Target.MODIFY, +) +def modify_nren_l3_core_service() -> StepList: + """Modify a NRN L3 Core Service subscription.""" + access_ports_are_removed = conditional(lambda state: bool(len(state["removed_access_ports"]) > 0)) + access_ports_are_modified = conditional(lambda state: bool(len(state["modified_sbp_list"]) > 0)) + access_ports_are_added = conditional(lambda state: bool(len(state["added_service_binding_ports"]) > 0)) + + return ( + begin + >> store_process_subscription(Target.MODIFY) + >> unsync + >> access_ports_are_removed(remove_old_sbp_blocks) + >> access_ports_are_modified(modify_existing_sbp_blocks) + >> access_ports_are_added(create_new_sbp_blocks) + >> resync + >> done + ) diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index 04d38b750f5fa4ca87591f7affe04674208685bf..edc4ec0b7877313f28af8508c3da4833e9161c55 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -4,7 +4,7 @@ from typing import Self from orchestrator.config.assignee import Assignee from orchestrator.forms import FormPage -from orchestrator.forms.validators import Choice, Label +from orchestrator.forms.validators import Label from orchestrator.targets import Target from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr from orchestrator.utils.errors import ProcessFailureError @@ -17,13 +17,13 @@ from pydantic_forms.validators import ReadOnlyField from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import RouterInactive, RouterProvisioning from gso.products.product_types.site import Site -from gso.services import infoblox, subscriptions +from gso.services import infoblox from gso.services.lso_client import lso_interaction from gso.services.netbox_client import NetboxClient from gso.services.partners import get_partner_by_name from gso.services.sharepoint import SharePointClient from gso.settings import load_oss_params -from gso.utils.helpers import generate_fqdn, iso_from_ipv4 +from gso.utils.helpers import active_site_selector, generate_fqdn, iso_from_ipv4 from gso.utils.shared_enums import Vendor from gso.utils.types.ip_address import PortNumber from gso.utils.types.tt_number import TTNumber @@ -33,15 +33,7 @@ from gso.utils.workflow_steps import ( prompt_sharepoint_checklist_url, run_checks_after_base_config, ) - - -def _site_selector() -> Choice: - site_subscriptions = {} - for site in subscriptions.get_active_site_subscriptions(includes=["subscription_id", "description"]): - site_subscriptions[str(site["subscription_id"])] = site["description"] - - # noinspection PyTypeChecker - return Choice("Select a site", zip(site_subscriptions.keys(), site_subscriptions.items(), strict=True)) # type: ignore[arg-type] +from gso.workflows.shared import create_summary_form def initial_input_form_generator(product_name: str) -> FormGenerator: @@ -53,7 +45,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: tt_number: TTNumber partner: ReadOnlyField("GEANT", default_type=str) # type: ignore[valid-type] vendor: Vendor - router_site: _site_selector() # type: ignore[valid-type] + router_site: active_site_selector() # type: ignore[valid-type] hostname: str ts_port: PortNumber router_role: RouterRole @@ -74,8 +66,11 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: return self user_input = yield CreateRouterForm + user_input = user_input.model_dump() + summary_fields = ["hostname", "router_site", "vendor", "ts_port", "router_role"] + yield from create_summary_form(user_input, product_name, summary_fields) - return user_input.model_dump() + return user_input @step("Create subscription") diff --git a/gso/workflows/router/modify_connection_strategy.py b/gso/workflows/router/modify_connection_strategy.py index 65b29b8d1b57851d925ffb89909c215e94889f06..890329d845bfe1c3a18accb318e303734378e667 100644 --- a/gso/workflows/router/modify_connection_strategy.py +++ b/gso/workflows/router/modify_connection_strategy.py @@ -34,8 +34,8 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: def update_subscription_model(subscription: Router, connection_strategy: str) -> State: """Update the database model to reflect the new connection strategy. - If the connection strategy is set to IN-BAND, then access_via_ts should be set to False. - Conversely, if the connection strategy is set to OUT-OF-BAND, access_via_ts should be set to True. + If the connection strategy is set to in-band, then access_via_ts should be set to False. + Conversely, if the connection strategy is set to out-of-band, access_via_ts should be set to True. """ subscription.router.router_access_via_ts = connection_strategy == ConnectionStrategy.OUT_OF_BAND diff --git a/gso/workflows/router/promote_p_to_pe.py b/gso/workflows/router/promote_p_to_pe.py index a82b4823f4374772ef2b5365f3f57ea56fad1017..8bf78ea033cc00a8fe936ab10d0223ae9ee1f9dc 100644 --- a/gso/workflows/router/promote_p_to_pe.py +++ b/gso/workflows/router/promote_p_to_pe.py @@ -236,7 +236,7 @@ def deploy_routing_instances_real(subscription: dict[str, Any], tt_number: str, @step("Remove ISIS overload") def remove_isis_overload(subscription: dict[str, Any], tt_number: str, process_id: UUIDstr) -> LSOState: - """Remove ISIS overload.""" + """Remove :term:`ISIS` overload.""" extra_vars = { "dry_run": False, "subscription": subscription, diff --git a/gso/workflows/router/update_ibgp_mesh.py b/gso/workflows/router/update_ibgp_mesh.py index 298c0878a2f82449173c78d24604e82ae8eb5bb1..fc071ab8fc657cd2f5fb001a165d84175c0a513b 100644 --- a/gso/workflows/router/update_ibgp_mesh.py +++ b/gso/workflows/router/update_ibgp_mesh.py @@ -18,7 +18,7 @@ from gso.services import librenms_client from gso.services.lso_client import LSOState, lso_interaction from gso.services.subscriptions import get_trunks_that_terminate_on_router from gso.utils.helpers import generate_inventory_for_active_routers -from gso.utils.types.snmp import SNMPVersion +from gso.utils.shared_enums import SNMPVersion from gso.utils.types.tt_number import TTNumber from gso.utils.workflow_steps import ( add_all_p_to_pe_dry, diff --git a/gso/workflows/shared.py b/gso/workflows/shared.py new file mode 100644 index 0000000000000000000000000000000000000000..8504deba80150c29c48a7f151e281d705b95d04c --- /dev/null +++ b/gso/workflows/shared.py @@ -0,0 +1,40 @@ +"""Shared functions for the workflows.""" + +from collections.abc import Generator +from typing import cast + +from orchestrator.domain.base import ProductBlockModel +from orchestrator.forms import FormPage +from orchestrator.forms.validators import MigrationSummary, migration_summary +from pydantic import ConfigDict + + +def summary_form(product_name: str, summary_data: dict) -> Generator: + """Generate a summary form for the product.""" + + class SummaryForm(FormPage): + model_config = ConfigDict(title=f"{product_name} summary") + + product_summary: cast(type[MigrationSummary], migration_summary(summary_data)) # type: ignore[valid-type] + + yield SummaryForm + + +def create_summary_form(user_input: dict, product_name: str, fields: list[str]) -> Generator: + """Create a summary form for the product.""" + columns = [[str(user_input[nm]) for nm in fields]] + yield from summary_form(product_name, {"labels": fields, "columns": columns}) + + +def modify_summary_form(user_input: dict, block: ProductBlockModel, fields: list[str]) -> Generator: + """Modify the summary form for the product.""" + before = [str(getattr(block, nm)) for nm in fields] + after = [str(user_input[nm]) for nm in fields] + yield from summary_form( + block.subscription.product.name, + { + "labels": fields, + "headers": ["Before", "After"], + "columns": [before, after], + }, + ) diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py index eb5409d778c019e41ae82c85376ec48fcf4bdcc6..384dd59f0945885f54bfd5c51b51f477b3a30f69 100644 --- a/gso/workflows/site/create_site.py +++ b/gso/workflows/site/create_site.py @@ -15,6 +15,7 @@ from gso.services.partners import get_partner_by_name from gso.utils.types.base_site import BaseSiteValidatorModel from gso.utils.types.coordinates import LatitudeCoordinate, LongitudeCoordinate from gso.utils.types.ip_address import IPAddress +from gso.workflows.shared import create_summary_form def initial_input_form_generator(product_name: str) -> FormGenerator: @@ -25,8 +26,23 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: partner: ReadOnlyField("GEANT", default_type=str) # type: ignore[valid-type] user_input = yield CreateSiteForm - - return user_input.model_dump() + user_input = user_input.model_dump() + summary_fields = [ + "site_name", + "site_bgp_community_id", + "site_internal_id", + "site_tier", + "site_ts_address", + "site_country_code", + "site_city", + "site_country", + "site_latitude", + "site_longitude", + "partner", + ] + yield from create_summary_form(user_input, product_name, summary_fields) + + return user_input @step("Create subscription") diff --git a/gso/workflows/site/modify_site.py b/gso/workflows/site/modify_site.py index 9c94e55032ae9712856603463b71299cf8e37fe7..a522a714d52ed5db066ff18cbeacc7175ba4a907 100644 --- a/gso/workflows/site/modify_site.py +++ b/gso/workflows/site/modify_site.py @@ -1,5 +1,6 @@ """A modification workflow for a site.""" +from functools import partial from typing import Annotated from orchestrator.forms import FormPage @@ -13,14 +14,15 @@ from orchestrator.workflows.steps import ( unsync, ) from orchestrator.workflows.utils import wrap_modify_initial_input_form -from pydantic import ConfigDict +from pydantic import AfterValidator, ConfigDict from pydantic_forms.validators import ReadOnlyField from gso.products.product_blocks.site import SiteTier from gso.products.product_types.site import Site from gso.utils.types.coordinates import LatitudeCoordinate, LongitudeCoordinate from gso.utils.types.ip_address import IPAddress -from gso.utils.types.unique_field import UniqueField +from gso.utils.types.unique_field import validate_field_is_unique +from gso.workflows.shared import modify_summary_form def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: @@ -36,14 +38,30 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator: site_country_code: ReadOnlyField(subscription.site.site_country_code, default_type=str) # type: ignore[valid-type] site_latitude: LatitudeCoordinate = subscription.site.site_latitude site_longitude: LongitudeCoordinate = subscription.site.site_longitude - site_bgp_community_id: UniqueField[int] = subscription.site.site_bgp_community_id - site_internal_id: UniqueField[int] = subscription.site.site_internal_id + site_bgp_community_id: Annotated[int, AfterValidator(partial(validate_field_is_unique, subscription_id))] = ( + subscription.site.site_bgp_community_id + ) + site_internal_id: Annotated[int, AfterValidator(partial(validate_field_is_unique, subscription_id))] = ( + subscription.site.site_internal_id + ) site_tier: ReadOnlyField(subscription.site.site_tier, default_type=SiteTier) # type: ignore[valid-type] - site_ts_address: Annotated[IPAddress, UniqueField] | None = subscription.site.site_ts_address + site_ts_address: ( + Annotated[IPAddress, AfterValidator(partial(validate_field_is_unique, subscription_id))] | None + ) = subscription.site.site_ts_address user_input = yield ModifySiteForm - - return user_input.model_dump() + user_input = user_input.model_dump() + summary_fields = [ + "site_bgp_community_id", + "site_internal_id", + "site_ts_address", + "site_city", + "site_latitude", + "site_longitude", + ] + yield from modify_summary_form(user_input, subscription.site, summary_fields) + + return user_input @step("Modify subscription") diff --git a/gso/workflows/tasks/delete_partners.py b/gso/workflows/tasks/delete_partners.py index 6eac752bbb417500d383d43fbc6ce2c3567031d1..8a557c9953079ee4e6723bb89bf60d1bc7a9dfca 100644 --- a/gso/workflows/tasks/delete_partners.py +++ b/gso/workflows/tasks/delete_partners.py @@ -7,23 +7,18 @@ from orchestrator.targets import Target from orchestrator.types import FormGenerator, State, UUIDstr from orchestrator.workflow import StepList, begin, done, step, workflow from pydantic import ConfigDict, EmailStr, field_validator -from pydantic_forms.validators import Choice -from gso.services.partners import delete_partner, get_all_partners, get_partner_by_name +from gso.services.partners import delete_partner, get_partner_by_name from gso.services.subscriptions import get_subscriptions +from gso.utils.helpers import partner_choice def initial_input_form_generator() -> FormGenerator: """Gather input from the user needed for deleting a partner.""" - partners = {} - for partner in get_all_partners(): - partners[partner["partner_id"]] = partner["name"] - - partner_choice = Choice("Select a partner", zip(partners.values(), partners.items(), strict=True)) # type: ignore[arg-type] class SelectPartnerForm(FormPage): model_config = ConfigDict(title="Delete a Partner") - partners: partner_choice # type: ignore[valid-type] + partners: partner_choice() # type: ignore[valid-type] @field_validator("partners") def validate_partners(cls, value: Enum) -> Enum: diff --git a/gso/workflows/tasks/modify_partners.py b/gso/workflows/tasks/modify_partners.py index cb2daddac8d646c87e20d80449024707cb319d21..c557203eede28c35dde9997f74a383002fb3e6fd 100644 --- a/gso/workflows/tasks/modify_partners.py +++ b/gso/workflows/tasks/modify_partners.py @@ -5,30 +5,24 @@ from orchestrator.targets import Target from orchestrator.types import FormGenerator, State, UUIDstr from orchestrator.workflow import StepList, begin, done, step, workflow from pydantic import ConfigDict, EmailStr, field_validator -from pydantic_forms.validators import Choice from gso.services.partners import ( ModifiedPartnerSchema, edit_partner, filter_partners_by_email, filter_partners_by_name, - get_all_partners, get_partner_by_name, ) +from gso.utils.helpers import partner_choice def initial_input_form_generator() -> FormGenerator: """Gather input from the user needed for modifying a partner.""" - partners = {} - for partner in get_all_partners(): - partners[partner["partner_id"]] = partner["name"] - - partner_choice = Choice("Select a partner", zip(partners.values(), partners.items(), strict=True)) # type: ignore[arg-type] class SelectPartnerForm(FormPage): model_config = ConfigDict(title="Choose a Partner") - partners: partner_choice # type: ignore[valid-type] + partners: partner_choice() # type: ignore[valid-type] initial_user_input = yield SelectPartnerForm diff --git a/pyproject.toml b/pyproject.toml index e9979242b773ca0eb5812755a4a240733cca96fa..2ae64df63bb2609ee3196b6d31605957a5b78a0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,3 +116,6 @@ filterwarnings = [ "ignore", "default:::gso", ] + +[tool.coverage.run] +omit = ["gso/migrations/*"] diff --git a/setup.py b/setup.py index 511db02051c9e54bf193d9e65ac18a4fe2d73978..3ec7a018f534de6d21b39ba0c786eba2e04042ca 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="2.21", + version="2.22", author="GÉANT Orchestration and Automation Team", author_email="goat@geant.org", description="GÉANT Service Orchestrator", diff --git a/start-worker.sh b/start-worker.sh index 3c18dd4422ae6d60e16c78f45aa7189659dd5ca9..92cd6304d95db2a697ca1b77e4e705c5b9350b1a 100755 --- a/start-worker.sh +++ b/start-worker.sh @@ -4,4 +4,4 @@ set -o errexit set -o nounset cd /app -python -m celery -A gso.worker worker --loglevel=info +python -m celery -A gso.worker worker --loglevel=info --concurrency=1 --pool=solo diff --git a/test/cli/test_imports.py b/test/cli/test_imports.py index 983323869339b67329f3824ee7310a495c6c5a24..33ab32dd2acc6b1746322a1694528fa2323ee48d 100644 --- a/test/cli/test_imports.py +++ b/test/cli/test_imports.py @@ -5,17 +5,22 @@ from unittest.mock import patch import pytest from gso.cli.imports import ( + import_edge_port, import_iptrunks, + import_nren_l3_core_service, import_office_routers, import_opengear, import_routers, import_sites, import_super_pop_switches, ) -from gso.products import Router, Site +from gso.products.product_blocks.bgp_session import IPFamily +from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType from gso.products.product_blocks.iptrunk import IptrunkType from gso.products.product_blocks.router import RouterRole from gso.products.product_blocks.site import SiteTier +from gso.products.product_types.router import Router +from gso.products.product_types.site import Site from gso.utils.helpers import iso_from_ipv4 from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import PhysicalPortCapacity @@ -195,6 +200,138 @@ def opengear_data(temp_file, faker, site_subscription_factory): return _opengear_data +@pytest.fixture() +def edge_port_data(temp_file, faker, router_subscription_factory, partner_factory): + def _edge_port_data(**kwargs): + edge_port_data = { + "node": Router.from_subscription(router_subscription_factory(vendor=Vendor.NOKIA)).router.router_fqdn, + "service_type": EdgePortType.CUSTOMER, + "speed": PhysicalPortCapacity.TEN_GIGABIT_PER_SECOND, + "encapsulation": EncapsulationType.DOT1Q, + "name": "lag34", + "minimum_links": 2, + "geant_ga_id": faker.geant_gid(), + "mac_address": faker.mac_address(), + "partner": partner_factory()["name"], + "enable_lacp": True, + "ignore_if_down": False, + "ae_members": [ + { + "interface_name": faker.network_interface(), + "interface_description": faker.sentence(), + }, + { + "interface_name": faker.network_interface(), + "interface_description": faker.sentence(), + }, + ], + "description": faker.sentence(), + } + edge_port_data.update(**kwargs) + + temp_file.write_text(json.dumps([edge_port_data])) + return {"path": str(temp_file), "data": edge_port_data} + + return _edge_port_data + + +@pytest.fixture() +def nren_l3_core_service_data(temp_file, faker, partner_factory, edge_port_subscription_factory): + def _nren_l3_core_service_data(**kwargs): + nren_l3_core_service_data = { + "partner": partner_factory()["name"], + "service_type": "IMPORTED IAS", + "service_binding_ports": [ + { + "edge_port": edge_port_subscription_factory(), + "ap_type": "PRIMARY", + "geant_sid": faker.geant_sid(), + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv4_mask": faker.ipv4_netmask(), + "ipv6_address": faker.ipv6(), + "ipv6_mask": faker.ipv6_netmask(), + "bgp_peers": [ + { + "bfd_enabled": True, + "bfd_interval": faker.pyint(), + "bfd_multiplier": faker.pyint(), + "has_custom_policies": True, + "authentication_key": faker.password(), + "multipath_enabled": False, + "send_default_route": True, + "is_passive": True, + "peer_address": faker.ipv4(), + "families": [IPFamily.V4UNICAST, IPFamily.V4MULTICAST], + "is_multi_hop": False, + "rtbh_enabled": True, + }, + { + "bfd_enabled": True, + "bfd_interval": faker.pyint(), + "bfd_multiplier": faker.pyint(), + "has_custom_policies": True, + "authentication_key": faker.password(), + "multipath_enabled": False, + "send_default_route": True, + "is_passive": True, + "peer_address": faker.ipv6(), + "families": [IPFamily.V6UNICAST], + "is_multi_hop": False, + "rtbh_enabled": True, + }, + ], + }, + { + "edge_port": edge_port_subscription_factory(), + "ap_type": "BACKUP", + "geant_sid": faker.geant_sid(), + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv4_mask": faker.ipv4_netmask(), + "ipv6_address": faker.ipv6(), + "ipv6_mask": faker.ipv6_netmask(), + "bgp_peers": [ + { + "bfd_enabled": True, + "bfd_interval": faker.pyint(), + "bfd_multiplier": faker.pyint(), + "has_custom_policies": True, + "authentication_key": faker.password(), + "multipath_enabled": False, + "send_default_route": True, + "is_passive": True, + "peer_address": faker.ipv4(), + "families": [IPFamily.V4UNICAST, IPFamily.V4MULTICAST], + "is_multi_hop": False, + "rtbh_enabled": True, + }, + { + "bfd_enabled": True, + "bfd_interval": faker.pyint(), + "bfd_multiplier": faker.pyint(), + "has_custom_policies": True, + "authentication_key": faker.password(), + "multipath_enabled": False, + "send_default_route": True, + "is_passive": True, + "peer_address": faker.ipv6(), + "families": [IPFamily.V6UNICAST], + "is_multi_hop": False, + "rtbh_enabled": True, + }, + ], + }, + ], + } + nren_l3_core_service_data.update(**kwargs) + + temp_file.write_text(json.dumps([nren_l3_core_service_data])) + return {"path": str(temp_file), "data": nren_l3_core_service_data} + + return _nren_l3_core_service_data + + ########### # TESTS # ########### @@ -377,3 +514,128 @@ def test_import_super_pop_switch_success(mock_start_process, mock_sleep, super_p def test_import_opengear_success(mock_start_process, opengear_data): import_opengear(opengear_data()["path"]) assert mock_start_process.call_count == 1 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_edge_port_successful(mock_start_process, mock_sleep, edge_port_data): + import_edge_port(edge_port_data()["path"]) + assert mock_start_process.call_count == 1 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_edge_port_with_invalid_router( + mock_start_process, mock_sleep, edge_port_data, capfd, router_subscription_factory +): + p_router = router_subscription_factory(vendor=Vendor.NOKIA, router_role=RouterRole.P) + broken_data = edge_port_data(node=Router.from_subscription(p_router).router.router_fqdn) + import_edge_port(broken_data["path"]) + + captured_output, _ = capfd.readouterr() + assert f"Router {p_router} not found" in captured_output + assert mock_start_process.call_count == 0 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_edge_port_with_invalid_partner(mock_start_process, mock_sleep, edge_port_data, capfd): + broken_data = edge_port_data(partner="INVALID") + import_edge_port(broken_data["path"]) + + captured_output, _ = capfd.readouterr() + assert "Partner INVALID not found" in captured_output + assert mock_start_process.call_count == 0 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_nren_l3_core_service_success(mock_start_process, mock_sleep, nren_l3_core_service_data, capfd): + import_nren_l3_core_service(nren_l3_core_service_data()["path"]) + assert mock_start_process.call_count == 1 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_nren_l3_core_service_with_invalid_partner( + mock_start_process, mock_sleep, nren_l3_core_service_data, capfd +): + broken_data = nren_l3_core_service_data(partner="INVALID") + import_nren_l3_core_service(broken_data["path"]) + + captured_output, _ = capfd.readouterr() + assert "Partner INVALID not found" in captured_output + assert mock_start_process.call_count == 0 + + +@patch("gso.cli.imports.time.sleep") +@patch("gso.cli.imports.start_process") +def test_import_nren_l3_core_service_with_invalid_edge_port( + mock_start_process, mock_sleep, faker, nren_l3_core_service_data, edge_port_subscription_factory, capfd +): + fake_uuid = faker.uuid4() + broken_data = nren_l3_core_service_data( + service_binding_ports=[ + { + "edge_port": fake_uuid, + "ap_type": "PRIMARY", + "geant_sid": faker.geant_sid(), + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv4_mask": faker.ipv4_netmask(), + "ipv6_address": faker.ipv6(), + "ipv6_mask": faker.ipv6_netmask(), + "bgp_peers": [ + { + "bfd_enabled": False, + "authentication_key": faker.password(), + "peer_address": faker.ipv4(), + "families": [IPFamily.V4UNICAST], + "is_multi_hop": False, + "rtbh_enabled": True, + }, + { + "bfd_enabled": False, + "authentication_key": faker.password(), + "peer_address": faker.ipv6(), + "families": [IPFamily.V6UNICAST], + "is_multi_hop": False, + "rtbh_enabled": True, + }, + ], + }, + { + "edge_port": edge_port_subscription_factory(), + "ap_type": "BACKUP", + "geant_sid": faker.geant_sid(), + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv4_mask": faker.ipv4_netmask(), + "ipv6_address": faker.ipv6(), + "ipv6_mask": faker.ipv6_netmask(), + "bgp_peers": [ + { + "bfd_enabled": False, + "authentication_key": faker.password(), + "peer_address": faker.ipv4(), + "families": [IPFamily.V4UNICAST], + "is_multi_hop": False, + "rtbh_enabled": True, + }, + { + "bfd_enabled": False, + "authentication_key": faker.password(), + "peer_address": faker.ipv6(), + "families": [IPFamily.V6UNICAST], + "is_multi_hop": False, + "rtbh_enabled": True, + }, + ], + }, + ] + ) + import_nren_l3_core_service(broken_data["path"]) + + captured_output, _ = capfd.readouterr() + assert f"Edge Port {fake_uuid} not found" in captured_output + assert mock_start_process.call_count == 0 diff --git a/test/conftest.py b/test/conftest.py index ed4cb631e966caa4149356f0c258c2c164d850e3..981768655dfde2eaf4700853488e2a2c6bedb049 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -36,14 +36,18 @@ from gso.main import init_gso_app from gso.services.partners import PartnerSchema, create_partner from gso.utils.types.interfaces import LAGMember, LAGMemberList from test.fixtures import ( # noqa: F401 + bgp_session_subscription_factory, + edge_port_subscription_factory, iptrunk_side_subscription_factory, iptrunk_subscription_factory, + nren_access_port_factory, + nren_l3_core_service_subscription_factory, office_router_subscription_factory, opengear_subscription_factory, router_subscription_factory, + service_binding_port_factory, site_subscription_factory, super_pop_switch_subscription_factory, - test_workflow, ) logging.getLogger("faker.factory").setLevel(logging.WARNING) @@ -97,6 +101,12 @@ class FakerProvider(BaseProvider): return site_name + def ipv4_netmask(self) -> int: + return self.generator.random_int(min=1, max=32) + + def ipv6_netmask(self) -> int: + return self.generator.random_int(min=1, max=128) + def network_interface(self) -> str: return self.generator.numerify("ge-@#/@#/@#") @@ -115,6 +125,9 @@ class FakerProvider(BaseProvider): for i in range(iface_amount) ] + def vlan_id(self) -> int: + return self.generator.random_int(min=1, max=4095) + @pytest.fixture(scope="session") def faker() -> Faker: @@ -265,15 +278,15 @@ def test_client(fastapi_app): @pytest.fixture(scope="session") -def partner_factory(): +def partner_factory(faker): def _create_partner( - name: str, - email: str, + name: str | None = None, + email: str | None = None, ) -> dict: return create_partner( PartnerSchema( - name=name, - email=email, + name=name or faker.company(), + email=email or faker.email(), ) ) diff --git a/test/fixtures.py b/test/fixtures.py index 6c3928555854b130deea0cf30fc95d3afaed62cd..e2b905c1736879af3087c31d09bc3a605130e43b 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -1,4 +1,3 @@ -import ipaddress from collections.abc import Generator from typing import Any from uuid import uuid4 @@ -6,462 +5,15 @@ from uuid import uuid4 import pytest from orchestrator import step, workflow from orchestrator.config.assignee import Assignee -from orchestrator.db import ( - ProductTable, - SubscriptionInstanceTable, - SubscriptionInstanceValueTable, - SubscriptionTable, - db, -) -from orchestrator.domain import SubscriptionModel -from orchestrator.types import SubscriptionLifecycle, UUIDstr -from orchestrator.utils.datetime import nowtz +from orchestrator.types import UUIDstr from orchestrator.workflow import done, init, inputstep from pydantic_forms.core import FormPage -from pydantic_forms.types import FormGenerator, SubscriptionMapping +from pydantic_forms.types import FormGenerator from pydantic_forms.validators import Choice -from gso.products import ProductName -from gso.products.product_blocks.iptrunk import ( - IptrunkInterfaceBlock, - IptrunkSideBlock, - IptrunkType, -) -from gso.products.product_blocks.router import RouterRole -from gso.products.product_blocks.site import SiteTier -from gso.products.product_types.iptrunk import ImportedIptrunkInactive, IptrunkInactive -from gso.products.product_types.office_router import ImportedOfficeRouterInactive, OfficeRouterInactive -from gso.products.product_types.opengear import ImportedOpengearInactive, OpengearInactive -from gso.products.product_types.router import ImportedRouterInactive, Router, RouterInactive -from gso.products.product_types.site import ImportedSiteInactive, Site, SiteInactive -from gso.products.product_types.super_pop_switch import ImportedSuperPopSwitchInactive, SuperPopSwitchInactive -from gso.services import subscriptions -from gso.utils.helpers import iso_from_ipv4 -from gso.utils.shared_enums import Vendor -from gso.utils.types.interfaces import PhysicalPortCapacity -from gso.utils.types.ip_address import IPv4AddressType, IPv6AddressType from test.workflows import WorkflowInstanceForTests -@pytest.fixture() -def site_subscription_factory(faker, geant_partner): - def subscription_create( - description=None, - start_date="2023-05-24T00:00:00+00:00", - site_name=None, - site_city=None, - site_country=None, - site_country_code=None, - site_latitude=None, - site_longitude=None, - site_bgp_community_id=None, - site_internal_id=None, - site_tier=SiteTier.TIER1, - site_ts_address=None, - status: SubscriptionLifecycle | None = None, - partner: dict | None = None, - *, - is_imported: bool | None = True, - ) -> UUIDstr: - if partner is None: - partner = geant_partner - - description = description or "Site Subscription" - site_name = site_name or faker.site_name() - site_city = site_city or faker.city() - site_country = site_country or faker.country() - site_country_code = site_country_code or faker.country_code() - site_latitude = site_latitude or str(faker.latitude()) - site_longitude = site_longitude or str(faker.longitude()) - site_bgp_community_id = site_bgp_community_id or faker.pyint() - site_internal_id = site_internal_id or faker.pyint() - site_ts_address = site_ts_address or faker.ipv4() - - if is_imported: - product_id = subscriptions.get_product_id_by_name(ProductName.SITE) - site_subscription = SiteInactive.from_product_id(product_id, customer_id=partner["partner_id"], insync=True) - else: - product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_SITE) - site_subscription = ImportedSiteInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - - site_subscription.site.site_city = site_city - site_subscription.site.site_name = site_name - site_subscription.site.site_country = site_country - site_subscription.site.site_country_code = site_country_code - site_subscription.site.site_latitude = site_latitude - site_subscription.site.site_longitude = site_longitude - site_subscription.site.site_bgp_community_id = site_bgp_community_id - site_subscription.site.site_internal_id = site_internal_id - site_subscription.site.site_tier = site_tier - site_subscription.site.site_ts_address = site_ts_address - - site_subscription = SubscriptionModel.from_other_lifecycle(site_subscription, SubscriptionLifecycle.ACTIVE) - site_subscription.description = description - site_subscription.start_date = start_date - if status: - site_subscription.status = status - - site_subscription.save() - db.session.commit() - - return str(site_subscription.subscription_id) - - return subscription_create - - -@pytest.fixture() -def router_subscription_factory(site_subscription_factory, faker, geant_partner): - def subscription_create( - description: str | None = None, - start_date: str | None = "2023-05-24T00:00:00+00:00", - router_fqdn: str | None = None, - router_ts_port: int | None = None, - router_lo_ipv4_address: IPv4AddressType | None = None, - router_lo_ipv6_address: IPv6AddressType | None = None, - router_lo_iso_address: str | None = None, - router_role: RouterRole | None = RouterRole.PE, - router_site=None, - status: SubscriptionLifecycle | None = None, - partner: dict | None = None, - vendor: Vendor | None = Vendor.NOKIA, - *, - router_access_via_ts: bool | None = None, - is_imported: bool | None = True, - ) -> UUIDstr: - if partner is None: - partner = geant_partner - if is_imported: - product_id = subscriptions.get_product_id_by_name(ProductName.ROUTER) - router_subscription = RouterInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - else: - product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_ROUTER) - router_subscription = ImportedRouterInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - - router_subscription.router.router_fqdn = router_fqdn or faker.domain_name(levels=4) - router_subscription.router.router_ts_port = router_ts_port or faker.port_number(is_user=True) - router_subscription.router.router_access_via_ts = router_access_via_ts or faker.boolean() - router_subscription.router.router_lo_ipv4_address = router_lo_ipv4_address or ipaddress.IPv4Address( - faker.ipv4() - ) - router_subscription.router.router_lo_ipv6_address = router_lo_ipv6_address or ipaddress.IPv6Address( - faker.ipv6() - ) - router_subscription.router.router_lo_iso_address = router_lo_iso_address or iso_from_ipv4(faker.ipv4()) - router_subscription.router.router_role = router_role - router_subscription.router.router_site = Site.from_subscription(router_site or site_subscription_factory()).site - router_subscription.router.vendor = vendor - - router_subscription = SubscriptionModel.from_other_lifecycle(router_subscription, SubscriptionLifecycle.ACTIVE) - router_subscription.insync = True - router_subscription.description = description or faker.text(max_nb_chars=30) - router_subscription.start_date = start_date - - if status: - router_subscription.status = status - - router_subscription.save() - db.session.commit() - - return str(router_subscription.subscription_id) - - return subscription_create - - -@pytest.fixture() -def iptrunk_side_subscription_factory(router_subscription_factory, faker): - def subscription_create( - iptrunk_side_node=None, - iptrunk_side_ae_iface=None, - iptrunk_side_ae_geant_a_sid=None, - iptrunk_side_ae_members=None, - iptrunk_side_ae_members_description=None, - ) -> IptrunkSideBlock: - iptrunk_side_node_id = iptrunk_side_node or router_subscription_factory() - iptrunk_side_node = Router.from_subscription(iptrunk_side_node_id).router - iptrunk_side_ae_iface = iptrunk_side_ae_iface or faker.pystr() - iptrunk_side_ae_geant_a_sid = iptrunk_side_ae_geant_a_sid or faker.geant_sid() - iptrunk_side_ae_members = iptrunk_side_ae_members or [ - IptrunkInterfaceBlock.new( - faker.uuid4(), - interface_name=faker.network_interface(), - interface_description=faker.sentence(), - ), - IptrunkInterfaceBlock.new( - faker.uuid4(), - interface_name=faker.network_interface(), - interface_description=faker.sentence(), - ), - ] - - return IptrunkSideBlock.new( - faker.uuid4(), - iptrunk_side_node=iptrunk_side_node, - iptrunk_side_ae_iface=iptrunk_side_ae_iface, - iptrunk_side_ae_geant_a_sid=iptrunk_side_ae_geant_a_sid, - iptrunk_side_ae_members=iptrunk_side_ae_members, - iptrunk_side_ae_members_description=iptrunk_side_ae_members_description, - ) - - return subscription_create - - -@pytest.fixture() -def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker, geant_partner): - def subscription_create( - description=None, - start_date="2023-05-24T00:00:00+00:00", - geant_s_sid=None, - iptrunk_description=None, - iptrunk_type=IptrunkType.LEASED, - iptrunk_speed=PhysicalPortCapacity.ONE_GIGABIT_PER_SECOND, - iptrunk_isis_metric=None, - iptrunk_ipv4_network=None, - iptrunk_ipv6_network=None, - iptrunk_sides=None, - status: SubscriptionLifecycle | None = None, - partner: dict | None = None, - *, - is_imported: bool | None = True, - ) -> UUIDstr: - if partner is None: - partner = geant_partner - - if is_imported: - product_id = subscriptions.get_product_id_by_name(ProductName.IP_TRUNK) - iptrunk_subscription = IptrunkInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - else: - product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_IP_TRUNK) - iptrunk_subscription = ImportedIptrunkInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - - description = description or faker.sentence() - geant_s_sid = geant_s_sid or faker.geant_sid() - iptrunk_description = iptrunk_description or faker.sentence() - iptrunk_isis_metric = iptrunk_isis_metric or faker.pyint() - iptrunk_ipv4_network = iptrunk_ipv4_network or faker.ipv4_network(max_subnet=31) - iptrunk_ipv6_network = iptrunk_ipv6_network or faker.ipv6_network(max_subnet=126) - iptrunk_minimum_links = 1 - iptrunk_side_a = iptrunk_side_subscription_factory() - iptrunk_side_b = iptrunk_side_subscription_factory() - iptrunk_sides = iptrunk_sides or [iptrunk_side_a, iptrunk_side_b] - - iptrunk_subscription.iptrunk.geant_s_sid = geant_s_sid - iptrunk_subscription.iptrunk.iptrunk_description = iptrunk_description - iptrunk_subscription.iptrunk.iptrunk_type = iptrunk_type - iptrunk_subscription.iptrunk.iptrunk_speed = iptrunk_speed - iptrunk_subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links - iptrunk_subscription.iptrunk.iptrunk_isis_metric = iptrunk_isis_metric - iptrunk_subscription.iptrunk.iptrunk_ipv4_network = iptrunk_ipv4_network - iptrunk_subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network - iptrunk_subscription.iptrunk.iptrunk_sides = iptrunk_sides - - iptrunk_subscription = SubscriptionModel.from_other_lifecycle( - iptrunk_subscription, - SubscriptionLifecycle.ACTIVE, - ) - - if status: - iptrunk_subscription.status = status - - iptrunk_subscription.description = description - iptrunk_subscription.start_date = start_date - iptrunk_subscription.save() - db.session.commit() - - return str(iptrunk_subscription.subscription_id) - - return subscription_create - - -@pytest.fixture() -def office_router_subscription_factory(site_subscription_factory, faker, geant_partner): - def subscription_create( - description=None, - start_date="2023-05-24T00:00:00+00:00", - office_router_fqdn=None, - office_router_ts_port=None, - office_router_lo_ipv4_address=None, - office_router_lo_ipv6_address=None, - office_router_site=None, - status: SubscriptionLifecycle | None = None, - partner: dict | None = None, - *, - is_imported: bool | None = True, - ) -> UUIDstr: - if partner is None: - partner = geant_partner - - description = description or faker.text(max_nb_chars=30) - office_router_fqdn = office_router_fqdn or faker.domain_name(levels=4) - office_router_ts_port = office_router_ts_port or faker.random_int(min=1, max=49151) - office_router_lo_ipv4_address = office_router_lo_ipv4_address or ipaddress.IPv4Address(faker.ipv4()) - office_router_lo_ipv6_address = office_router_lo_ipv6_address or ipaddress.IPv6Address(faker.ipv6()) - office_router_site = office_router_site or site_subscription_factory() - - if is_imported: - product_id = subscriptions.get_product_id_by_name(ProductName.OFFICE_ROUTER) - office_router_subscription = OfficeRouterInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - else: - product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_OFFICE_ROUTER) - office_router_subscription = ImportedOfficeRouterInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - - office_router_subscription.office_router.office_router_fqdn = office_router_fqdn - office_router_subscription.office_router.office_router_ts_port = office_router_ts_port - office_router_subscription.office_router.office_router_lo_ipv4_address = office_router_lo_ipv4_address - office_router_subscription.office_router.office_router_lo_ipv6_address = office_router_lo_ipv6_address - office_router_subscription.office_router.office_router_site = Site.from_subscription(office_router_site).site - office_router_subscription.office_router.vendor = Vendor.NOKIA - - office_router_subscription = SubscriptionModel.from_other_lifecycle( - office_router_subscription, SubscriptionLifecycle.ACTIVE - ) - office_router_subscription.description = description - office_router_subscription.start_date = start_date - - if status: - office_router_subscription.status = status - - office_router_subscription.save() - db.session.commit() - - return str(office_router_subscription.subscription_id) - - return subscription_create - - -@pytest.fixture() -def super_pop_switch_subscription_factory(site_subscription_factory, faker, geant_partner): - def subscription_create( - description=None, - start_date="2023-05-24T00:00:00+00:00", - super_pop_switch_fqdn=None, - super_pop_switch_ts_port=None, - super_pop_switch_mgmt_ipv4_address=None, - super_pop_switch_site=None, - status: SubscriptionLifecycle | None = None, - partner: dict | None = None, - *, - is_imported: bool | None = True, - ) -> UUIDstr: - if partner is None: - partner = geant_partner - - description = description or faker.text(max_nb_chars=30) - super_pop_switch_fqdn = super_pop_switch_fqdn or faker.domain_name(levels=4) - super_pop_switch_ts_port = super_pop_switch_ts_port or faker.random_int(min=1, max=49151) - super_pop_switch_mgmt_ipv4_address = super_pop_switch_mgmt_ipv4_address or ipaddress.IPv4Address(faker.ipv4()) - super_pop_switch_site = super_pop_switch_site or site_subscription_factory() - - if is_imported: - product_id = subscriptions.get_product_id_by_name(ProductName.SUPER_POP_SWITCH) - super_pop_switch_subscription = SuperPopSwitchInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - else: - product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_SUPER_POP_SWITCH) - super_pop_switch_subscription = ImportedSuperPopSwitchInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - - super_pop_switch_subscription.super_pop_switch.super_pop_switch_fqdn = super_pop_switch_fqdn - super_pop_switch_subscription.super_pop_switch.super_pop_switch_ts_port = super_pop_switch_ts_port - super_pop_switch_subscription.super_pop_switch.super_pop_switch_mgmt_ipv4_address = ( - super_pop_switch_mgmt_ipv4_address - ) - super_pop_switch_subscription.super_pop_switch.super_pop_switch_site = Site.from_subscription( - super_pop_switch_site - ).site - super_pop_switch_subscription.super_pop_switch.vendor = Vendor.NOKIA - - super_pop_switch_subscription = SubscriptionModel.from_other_lifecycle( - super_pop_switch_subscription, SubscriptionLifecycle.ACTIVE - ) - super_pop_switch_subscription.description = description - super_pop_switch_subscription.start_date = start_date - - if status: - super_pop_switch_subscription.status = status - - super_pop_switch_subscription.save() - db.session.commit() - - return str(super_pop_switch_subscription.subscription_id) - - return subscription_create - - -@pytest.fixture() -def opengear_subscription_factory(site_subscription_factory, faker, geant_partner): - def subscription_create( - description=None, - start_date="2023-05-24T00:00:00+00:00", - opengear_site=None, - opengear_hostname=None, - opengear_wan_address=None, - opengear_wan_netmask=None, - opengear_wan_gateway=None, - status: SubscriptionLifecycle | None = None, - partner: dict | None = None, - *, - is_imported: bool | None = True, - ) -> UUIDstr: - if partner is None: - partner = geant_partner - - description = description or faker.text(max_nb_chars=30) - opengear_site = opengear_site or site_subscription_factory() - opengear_hostname = opengear_hostname or faker.domain_name(levels=4) - opengear_wan_address = opengear_wan_address or faker.ipv4() - opengear_wan_netmask = opengear_wan_netmask or faker.ipv4() - opengear_wan_gateway = opengear_wan_gateway or faker.ipv4() - - if is_imported: - product_id = subscriptions.get_product_id_by_name(ProductName.OPENGEAR) - opengear_subscription = OpengearInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - else: - product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_OPENGEAR) - opengear_subscription = ImportedOpengearInactive.from_product_id( - product_id, customer_id=partner["partner_id"], insync=True - ) - - opengear_subscription.opengear.opengear_site = Site.from_subscription(opengear_site).site - opengear_subscription.opengear.opengear_hostname = opengear_hostname - opengear_subscription.opengear.opengear_wan_address = opengear_wan_address - opengear_subscription.opengear.opengear_wan_netmask = opengear_wan_netmask - opengear_subscription.opengear.opengear_wan_gateway = opengear_wan_gateway - - opengear_subscription = SubscriptionModel.from_other_lifecycle( - opengear_subscription, SubscriptionLifecycle.ACTIVE - ) - opengear_subscription.description = description - opengear_subscription.start_date = start_date - - if status: - opengear_subscription.status = status - - opengear_subscription.save() - db.session.commit() - - return str(opengear_subscription.subscription_id) - - return subscription_create - - @pytest.fixture() def test_workflow(generic_subscription_1: UUIDstr, generic_product_type_1) -> Generator: _, generic_product_one = generic_product_type_1 @@ -497,73 +49,3 @@ def test_workflow(generic_subscription_1: UUIDstr, generic_product_type_1) -> Ge with WorkflowInstanceForTests(workflow_for_testing_processes_py, "workflow_for_testing_processes_py") as wf: yield wf - - -def create_subscription_for_mapping( - product: ProductTable, mapping: SubscriptionMapping, values: dict[str, Any], **kwargs: Any -) -> SubscriptionTable: - """Create a subscription in the test coredb for the given subscription_mapping and values. - - This function handles optional resource types starting with a ? in the mapping not supplied in the values array. - - Args: - product: the ProductTable to create a sub for - mapping: the subscription_mapping belonging to that product - values: a dictionary of keys from the sub_map and their corresponding test values - kwargs: The rest of the arguments - - Returns: The conforming subscription. - """ - - def build_instance(name, value_mapping): - block = product.find_block_by_name(name) - - def build_value(rt, value): - resource_type = block.find_resource_type_by_name(rt) - return SubscriptionInstanceValueTable(resource_type_id=resource_type.resource_type_id, value=value) - - return SubscriptionInstanceTable( - product_block_id=block.product_block_id, - values=[ - build_value(resource_type, values[value_key]) for (resource_type, value_key) in value_mapping.items() - ], - ) - - # recreate the mapping: leave out the ?keys if no value supplied for them - mapping = { - name: [ - { - **{k: value_map[k] for k in value_map if not value_map[k].startswith("?")}, - **{ - k: value_map[k][1:] - for k in value_map - if value_map[k].startswith("?") and value_map[k][1:] in values - }, - } - for value_map in mapping[name] - ] - for name in mapping - } - - instances = [ - build_instance(name, value_mapping) - for (name, value_mappings) in mapping.items() - for value_mapping in value_mappings - ] - - return create_subscription(instances=instances, product=product, **kwargs) - - -def create_subscription(**kwargs): - attrs = { - "description": "A subscription.", - "customer_id": kwargs.get("customer_id", "85938c4c-0a11-e511-80d0-005056956c1a"), - "start_date": nowtz(), - "status": "active", - "insync": True, - **kwargs, - } - o = SubscriptionTable(**attrs) - db.session.add(o) - db.session.commit() - return o diff --git a/test/fixtures/__init__.py b/test/fixtures/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0b69c5fb506e854a84e6e0e908826fcc250ec08e --- /dev/null +++ b/test/fixtures/__init__.py @@ -0,0 +1,28 @@ +from test.fixtures.edge_port_fixtures import edge_port_subscription_factory +from test.fixtures.iptrunk_fixtures import iptrunk_side_subscription_factory, iptrunk_subscription_factory +from test.fixtures.nren_l3_core_service_fixtures import ( + bgp_session_subscription_factory, + nren_access_port_factory, + nren_l3_core_service_subscription_factory, + service_binding_port_factory, +) +from test.fixtures.office_router_fixtures import office_router_subscription_factory +from test.fixtures.opengear_fixtures import opengear_subscription_factory +from test.fixtures.router_fixtures import router_subscription_factory +from test.fixtures.site_fixtures import site_subscription_factory +from test.fixtures.super_pop_switch_fixtures import super_pop_switch_subscription_factory + +__all__ = [ + "bgp_session_subscription_factory", + "edge_port_subscription_factory", + "iptrunk_side_subscription_factory", + "iptrunk_subscription_factory", + "nren_access_port_factory", + "nren_l3_core_service_subscription_factory", + "office_router_subscription_factory", + "opengear_subscription_factory", + "router_subscription_factory", + "service_binding_port_factory", + "site_subscription_factory", + "super_pop_switch_subscription_factory", +] diff --git a/test/fixtures/common_fixtures.py b/test/fixtures/common_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..aeac5a991c3afda7b32c7da7ff9b5e223f630aab --- /dev/null +++ b/test/fixtures/common_fixtures.py @@ -0,0 +1,81 @@ +from typing import Any + +from orchestrator.db import ( + ProductTable, + SubscriptionInstanceTable, + SubscriptionInstanceValueTable, + SubscriptionTable, + db, +) +from orchestrator.utils.datetime import nowtz +from pydantic_forms.types import SubscriptionMapping + + +def create_subscription_for_mapping( + product: ProductTable, mapping: SubscriptionMapping, values: dict[str, Any], **kwargs: Any +) -> SubscriptionTable: + """Create a subscription in the test coredb for the given subscription_mapping and values. + + This function handles optional resource types starting with a ? in the mapping not supplied in the values array. + + Args: + product: the ProductTable to create a sub for + mapping: the subscription_mapping belonging to that product + values: a dictionary of keys from the sub_map and their corresponding test values + kwargs: The rest of the arguments + + Returns: The conforming subscription. + """ + + def build_instance(name, value_mapping): + block = product.find_block_by_name(name) + + def build_value(rt, value): + resource_type = block.find_resource_type_by_name(rt) + return SubscriptionInstanceValueTable(resource_type_id=resource_type.resource_type_id, value=value) + + return SubscriptionInstanceTable( + product_block_id=block.product_block_id, + values=[ + build_value(resource_type, values[value_key]) for (resource_type, value_key) in value_mapping.items() + ], + ) + + # recreate the mapping: leave out the ?keys if no value supplied for them + mapping = { + name: [ + { + **{k: value_map[k] for k in value_map if not value_map[k].startswith("?")}, + **{ + k: value_map[k][1:] + for k in value_map + if value_map[k].startswith("?") and value_map[k][1:] in values + }, + } + for value_map in mapping[name] + ] + for name in mapping + } + + instances = [ + build_instance(name, value_mapping) + for (name, value_mappings) in mapping.items() + for value_mapping in value_mappings + ] + + return create_subscription(instances=instances, product=product, **kwargs) + + +def create_subscription(**kwargs): + attrs = { + "description": "A subscription.", + "customer_id": kwargs.get("customer_id", "85938c4c-0a11-e511-80d0-005056956c1a"), + "start_date": nowtz(), + "status": "active", + "insync": True, + **kwargs, + } + o = SubscriptionTable(**attrs) + db.session.add(o) + db.session.commit() + return o diff --git a/test/fixtures/edge_port_fixtures.py b/test/fixtures/edge_port_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..669d42ff90b4e0894ca1d117dde58448daab0903 --- /dev/null +++ b/test/fixtures/edge_port_fixtures.py @@ -0,0 +1,93 @@ +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_blocks.edge_port import ( + EdgePortAEMemberBlock, + EdgePortType, + EncapsulationType, +) +from gso.products.product_types.edge_port import EdgePortInactive, ImportedEdgePortInactive +from gso.products.product_types.router import Router +from gso.services import subscriptions +from gso.utils.shared_enums import Vendor +from gso.utils.types.interfaces import PhysicalPortCapacity + + +@pytest.fixture() +def edge_port_subscription_factory(faker, partner_factory, router_subscription_factory): + def subscription_create( + description=None, + partner: dict | None = None, + start_date="2023-05-24T00:00:00+00:00", + node=None, + name=None, + edge_port_description=None, + encapsulation=EncapsulationType.DOT1Q, + mac_address=None, + member_speed=PhysicalPortCapacity.HUNDRED_GIGABIT_PER_SECOND, + minimum_links=None, + edge_port_type=EdgePortType.PUBLIC, + geant_ga_id=None, + edge_port_ae_members=None, + status: SubscriptionLifecycle | None = None, + *, + enable_lacp=True, + ignore_if_down=False, + is_imported=True, + ) -> UUIDstr: + partner = partner or partner_factory() + node = Router.from_subscription(router_subscription_factory(vendor=Vendor.NOKIA)).router + if is_imported: + product_id = subscriptions.get_product_id_by_name(ProductName.EDGE_PORT) + edge_port_subscription = EdgePortInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + else: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_EDGE_PORT) + edge_port_subscription = ImportedEdgePortInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + + edge_port_subscription.edge_port.edge_port_description = description or faker.text(max_nb_chars=30) + edge_port_subscription.edge_port.geant_ga_id = geant_ga_id or faker.geant_sid() + edge_port_subscription.edge_port.node = node or node + edge_port_subscription.edge_port.edge_port_name = name or f"lag-{faker.pyint(21, 50)}" + edge_port_subscription.edge_port.edge_port_description = edge_port_description or faker.sentence() + edge_port_subscription.edge_port.enable_lacp = enable_lacp + edge_port_subscription.edge_port.encapsulation = encapsulation + edge_port_subscription.edge_port.mac_address = mac_address or faker.mac_address() + edge_port_subscription.edge_port.member_speed = member_speed + edge_port_subscription.edge_port.minimum_links = minimum_links or faker.pyint(1, 2) + edge_port_subscription.edge_port.edge_port_type = edge_port_type + edge_port_subscription.edge_port.ignore_if_down = ignore_if_down + edge_port_subscription.edge_port.edge_port_ae_members = edge_port_ae_members or [ + EdgePortAEMemberBlock.new( + faker.uuid4(), + interface_name="Interface2", + interface_description=faker.sentence(), + ), + EdgePortAEMemberBlock.new( + faker.uuid4(), + interface_name="Interface3", + interface_description=faker.sentence(), + ), + ] + edge_port_subscription = SubscriptionModel.from_other_lifecycle( + edge_port_subscription, + SubscriptionLifecycle.ACTIVE, + ) + + if status: + edge_port_subscription.status = status + + edge_port_subscription.description = description or faker.text(max_nb_chars=30) + edge_port_subscription.start_date = start_date + edge_port_subscription.save() + db.session.commit() + + return str(edge_port_subscription.subscription_id) + + return subscription_create diff --git a/test/fixtures/iptrunk_fixtures.py b/test/fixtures/iptrunk_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..70ccdeb547d75ae1fc605b9c1f9e9dfb4bafa6ca --- /dev/null +++ b/test/fixtures/iptrunk_fixtures.py @@ -0,0 +1,125 @@ +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_blocks.iptrunk import ( + IptrunkInterfaceBlock, + IptrunkSideBlock, + IptrunkType, +) +from gso.products.product_types.iptrunk import ImportedIptrunkInactive, IptrunkInactive +from gso.products.product_types.router import Router +from gso.services import subscriptions +from gso.utils.shared_enums import Vendor +from gso.utils.types.interfaces import PhysicalPortCapacity + + +@pytest.fixture() +def iptrunk_side_subscription_factory(router_subscription_factory, faker): + def subscription_create( + iptrunk_side_node=None, + iptrunk_side_ae_iface=None, + iptrunk_side_ae_geant_a_sid=None, + iptrunk_side_ae_members=None, + iptrunk_side_ae_members_description=None, + ) -> IptrunkSideBlock: + iptrunk_side_node_id = iptrunk_side_node or router_subscription_factory(vendor=Vendor.NOKIA) + iptrunk_side_node = Router.from_subscription(iptrunk_side_node_id).router + iptrunk_side_ae_iface = iptrunk_side_ae_iface or faker.pystr() + iptrunk_side_ae_geant_a_sid = iptrunk_side_ae_geant_a_sid or faker.geant_sid() + iptrunk_side_ae_members = iptrunk_side_ae_members or [ + IptrunkInterfaceBlock.new( + faker.uuid4(), + interface_name=faker.network_interface(), + interface_description=faker.sentence(), + ), + IptrunkInterfaceBlock.new( + faker.uuid4(), + interface_name=faker.network_interface(), + interface_description=faker.sentence(), + ), + ] + + return IptrunkSideBlock.new( + faker.uuid4(), + iptrunk_side_node=iptrunk_side_node, + iptrunk_side_ae_iface=iptrunk_side_ae_iface, + iptrunk_side_ae_geant_a_sid=iptrunk_side_ae_geant_a_sid, + iptrunk_side_ae_members=iptrunk_side_ae_members, + iptrunk_side_ae_members_description=iptrunk_side_ae_members_description, + ) + + return subscription_create + + +@pytest.fixture() +def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker, geant_partner): + def subscription_create( + description=None, + start_date="2023-05-24T00:00:00+00:00", + geant_s_sid=None, + iptrunk_description=None, + iptrunk_type=IptrunkType.LEASED, + iptrunk_speed=PhysicalPortCapacity.ONE_GIGABIT_PER_SECOND, + iptrunk_isis_metric=None, + iptrunk_ipv4_network=None, + iptrunk_ipv6_network=None, + iptrunk_sides=None, + status: SubscriptionLifecycle | None = None, + partner: dict | None = None, + *, + is_imported: bool | None = True, + ) -> UUIDstr: + if partner is None: + partner = geant_partner + + if is_imported: + product_id = subscriptions.get_product_id_by_name(ProductName.IP_TRUNK) + iptrunk_subscription = IptrunkInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + else: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_IP_TRUNK) + iptrunk_subscription = ImportedIptrunkInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + + description = description or faker.sentence() + geant_s_sid = geant_s_sid or faker.geant_sid() + iptrunk_description = iptrunk_description or faker.sentence() + iptrunk_isis_metric = iptrunk_isis_metric or faker.pyint() + iptrunk_ipv4_network = iptrunk_ipv4_network or faker.ipv4_network(max_subnet=31) + iptrunk_ipv6_network = iptrunk_ipv6_network or faker.ipv6_network(max_subnet=126) + iptrunk_minimum_links = 1 + iptrunk_side_a = iptrunk_side_subscription_factory() + iptrunk_side_b = iptrunk_side_subscription_factory() + iptrunk_sides = iptrunk_sides or [iptrunk_side_a, iptrunk_side_b] + + iptrunk_subscription.iptrunk.geant_s_sid = geant_s_sid + iptrunk_subscription.iptrunk.iptrunk_description = iptrunk_description + iptrunk_subscription.iptrunk.iptrunk_type = iptrunk_type + iptrunk_subscription.iptrunk.iptrunk_speed = iptrunk_speed + iptrunk_subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links + iptrunk_subscription.iptrunk.iptrunk_isis_metric = iptrunk_isis_metric + iptrunk_subscription.iptrunk.iptrunk_ipv4_network = iptrunk_ipv4_network + iptrunk_subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network + iptrunk_subscription.iptrunk.iptrunk_sides = iptrunk_sides + + iptrunk_subscription = SubscriptionModel.from_other_lifecycle( + iptrunk_subscription, + SubscriptionLifecycle.ACTIVE, + ) + + if status: + iptrunk_subscription.status = status + + iptrunk_subscription.description = description + iptrunk_subscription.start_date = start_date + iptrunk_subscription.save() + db.session.commit() + + return str(iptrunk_subscription.subscription_id) + + return subscription_create diff --git a/test/fixtures/nren_l3_core_service_fixtures.py b/test/fixtures/nren_l3_core_service_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..d0fd069a03b4b969e2fd286aaa7f57f5716f10c7 --- /dev/null +++ b/test/fixtures/nren_l3_core_service_fixtures.py @@ -0,0 +1,173 @@ +import random +from uuid import uuid4 + +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_blocks.bgp_session import BGPSession, IPFamily +from gso.products.product_blocks.nren_l3_core_service import NRENAccessPort +from gso.products.product_blocks.service_binding_port import ServiceBindingPort +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.nren_l3_core_service import ( + ImportedNRENL3CoreService, + NRENL3CoreServiceInactive, + NRENL3CoreServiceType, +) +from gso.services import subscriptions +from gso.utils.shared_enums import APType, SBPType +from gso.utils.types.ip_address import IPAddress + + +@pytest.fixture() +def bgp_session_subscription_factory(faker): + def create_bgp_session( + peer_address: IPAddress | None = None, + bfd_interval: int = 2, + bfd_multiplier: int = 2, + families: list[IPFamily] | None = None, + authentication_key: str | None = None, + *, + is_multi_hop: bool = False, + has_custom_policies: bool = False, + bfd_enabled: bool = True, + multipath_enabled: bool | None = True, + send_default_route: bool | None = True, + is_passive: bool | None = False, + rtbh_enabled: bool | None = False, + ): + return BGPSession.new( + subscription_id=uuid4(), + peer_address=peer_address or faker.ipv4(), + bfd_enabled=bfd_enabled, + families=families or [IPFamily.V4UNICAST], + has_custom_policies=has_custom_policies, + authentication_key=authentication_key or faker.password(), + multipath_enabled=multipath_enabled, + send_default_route=send_default_route, + is_multi_hop=is_multi_hop, + bfd_interval=bfd_interval, + bfd_multiplier=bfd_multiplier, + rtbh_enabled=rtbh_enabled, + is_passive=is_passive, + ) + + return create_bgp_session + + +@pytest.fixture() +def service_binding_port_factory(faker, bgp_session_subscription_factory, edge_port_subscription_factory): + def create_service_binding_port( + bgp_session_list: list[BGPSession] | None = None, + geant_sid: str | None = None, + sbp_type: SBPType = SBPType.L3, + ipv4_address: str | None = None, + ipv4_mask: int | None = None, + ipv6_address: str | None = None, + ipv6_mask: int | None = None, + vlan_id: int | None = None, + edge_port: EdgePort | None = None, + *, + custom_firewall_filters: bool = False, + is_tagged: bool = False, + ): + return ServiceBindingPort.new( + subscription_id=uuid4(), + is_tagged=is_tagged, + vlan_id=vlan_id or faker.vlan_id(), + sbp_type=sbp_type, + ipv4_address=ipv4_address or faker.ipv4(), + ipv4_mask=ipv4_mask or faker.ipv4_netmask(), + ipv6_address=ipv6_address or faker.ipv6(), + ipv6_mask=ipv6_mask or faker.ipv6_netmask(), + custom_firewall_filters=custom_firewall_filters, + geant_sid=geant_sid or faker.geant_sid(), + bgp_session_list=bgp_session_list + or [ + bgp_session_subscription_factory(families=[IPFamily.V4UNICAST]), + bgp_session_subscription_factory(families=[IPFamily.V6UNICAST], peer_address=faker.ipv6()), + ], + edge_port=edge_port or EdgePort.from_subscription(edge_port_subscription_factory()).edge_port, + ) + + return create_service_binding_port + + +@pytest.fixture() +def nren_access_port_factory(faker, service_binding_port_factory): + def create_nren_access_port( + nren_ap_type: APType | None = None, + service_binding_port: ServiceBindingPort | None = None, + ): + return NRENAccessPort.new( + subscription_id=uuid4(), + ap_type=nren_ap_type or random.choice(list(APType)), # noqa: S311 + sbp=service_binding_port or service_binding_port_factory(), + ) + + return create_nren_access_port + + +@pytest.fixture() +def nren_l3_core_service_subscription_factory( + faker, + partner_factory, + nren_access_port_factory, +): + def create_nren_l3_core_service_subscription( + nren_l3_core_service_type: NRENL3CoreServiceType, + description=None, + partner: dict | None = None, + nren_ap_list: list[NRENAccessPort] | None = None, + start_date="2023-05-24T00:00:00+00:00", + status: SubscriptionLifecycle | None = None, + ) -> UUIDstr: + partner = partner or partner_factory() + match nren_l3_core_service_type: + case NRENL3CoreServiceType.GEANT_IP: + product_id = subscriptions.get_product_id_by_name(ProductName.GEANT_IP) + nren_l3_core_service_subscription = NRENL3CoreServiceInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case NRENL3CoreServiceType.IMPORTED_GEANT_IP: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_GEANT_IP) + nren_l3_core_service_subscription = ImportedNRENL3CoreService.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case NRENL3CoreServiceType.IAS: + product_id = subscriptions.get_product_id_by_name(ProductName.IAS) + nren_l3_core_service_subscription = NRENL3CoreServiceInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case NRENL3CoreServiceType.IMPORTED_IAS: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_IAS) + nren_l3_core_service_subscription = ImportedNRENL3CoreService.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + case _: + msg = f"NREN L3 Core Service type not found: {nren_l3_core_service_type}" + raise ValueError(msg) + + # Default nren_ap_list creation with primary and backup access ports + nren_l3_core_service_subscription.nren_l3_core_service.nren_ap_list = nren_ap_list or [ + nren_access_port_factory(nren_ap_type=APType.PRIMARY), + nren_access_port_factory(nren_ap_type=APType.BACKUP), + ] + + # Update subscription with description, start date, and status + nren_l3_core_service_subscription = SubscriptionModel.from_other_lifecycle( + nren_l3_core_service_subscription, + SubscriptionLifecycle.ACTIVE, + ) + nren_l3_core_service_subscription.description = description or faker.sentence() + nren_l3_core_service_subscription.start_date = start_date + nren_l3_core_service_subscription.status = status or SubscriptionLifecycle.ACTIVE + nren_l3_core_service_subscription.save() + + db.session.commit() + + return str(nren_l3_core_service_subscription.subscription_id) + + return create_nren_l3_core_service_subscription diff --git a/test/fixtures/office_router_fixtures.py b/test/fixtures/office_router_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..0c55ada08ae12b730a491f15eedd42af15e23b6d --- /dev/null +++ b/test/fixtures/office_router_fixtures.py @@ -0,0 +1,72 @@ +import ipaddress + +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_types.office_router import ImportedOfficeRouterInactive, OfficeRouterInactive +from gso.products.product_types.site import Site +from gso.services import subscriptions +from gso.utils.shared_enums import Vendor + + +@pytest.fixture() +def office_router_subscription_factory(site_subscription_factory, faker, geant_partner): + def subscription_create( + description=None, + start_date="2023-05-24T00:00:00+00:00", + office_router_fqdn=None, + office_router_ts_port=None, + office_router_lo_ipv4_address=None, + office_router_lo_ipv6_address=None, + office_router_site=None, + status: SubscriptionLifecycle | None = None, + partner: dict | None = None, + *, + is_imported: bool | None = True, + ) -> UUIDstr: + if partner is None: + partner = geant_partner + + description = description or faker.text(max_nb_chars=30) + office_router_fqdn = office_router_fqdn or faker.domain_name(levels=4) + office_router_ts_port = office_router_ts_port or faker.random_int(min=1, max=49151) + office_router_lo_ipv4_address = office_router_lo_ipv4_address or ipaddress.IPv4Address(faker.ipv4()) + office_router_lo_ipv6_address = office_router_lo_ipv6_address or ipaddress.IPv6Address(faker.ipv6()) + office_router_site = office_router_site or site_subscription_factory() + + if is_imported: + product_id = subscriptions.get_product_id_by_name(ProductName.OFFICE_ROUTER) + office_router_subscription = OfficeRouterInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + else: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_OFFICE_ROUTER) + office_router_subscription = ImportedOfficeRouterInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + + office_router_subscription.office_router.office_router_fqdn = office_router_fqdn + office_router_subscription.office_router.office_router_ts_port = office_router_ts_port + office_router_subscription.office_router.office_router_lo_ipv4_address = office_router_lo_ipv4_address + office_router_subscription.office_router.office_router_lo_ipv6_address = office_router_lo_ipv6_address + office_router_subscription.office_router.office_router_site = Site.from_subscription(office_router_site).site + office_router_subscription.office_router.vendor = Vendor.NOKIA + + office_router_subscription = SubscriptionModel.from_other_lifecycle( + office_router_subscription, SubscriptionLifecycle.ACTIVE + ) + office_router_subscription.description = description + office_router_subscription.start_date = start_date + + if status: + office_router_subscription.status = status + + office_router_subscription.save() + db.session.commit() + + return str(office_router_subscription.subscription_id) + + return subscription_create diff --git a/test/fixtures/opengear_fixtures.py b/test/fixtures/opengear_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..b6dbff50f0fdc8c786caadddccade778311a83e1 --- /dev/null +++ b/test/fixtures/opengear_fixtures.py @@ -0,0 +1,68 @@ +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_types.opengear import ImportedOpengearInactive, OpengearInactive +from gso.products.product_types.site import Site +from gso.services import subscriptions + + +@pytest.fixture() +def opengear_subscription_factory(site_subscription_factory, faker, geant_partner): + def subscription_create( + description=None, + start_date="2023-05-24T00:00:00+00:00", + opengear_site=None, + opengear_hostname=None, + opengear_wan_address=None, + opengear_wan_netmask=None, + opengear_wan_gateway=None, + status: SubscriptionLifecycle | None = None, + partner: dict | None = None, + *, + is_imported: bool | None = True, + ) -> UUIDstr: + if partner is None: + partner = geant_partner + + description = description or faker.text(max_nb_chars=30) + opengear_site = opengear_site or site_subscription_factory() + opengear_hostname = opengear_hostname or faker.domain_name(levels=4) + opengear_wan_address = opengear_wan_address or faker.ipv4() + opengear_wan_netmask = opengear_wan_netmask or faker.ipv4() + opengear_wan_gateway = opengear_wan_gateway or faker.ipv4() + + if is_imported: + product_id = subscriptions.get_product_id_by_name(ProductName.OPENGEAR) + opengear_subscription = OpengearInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + else: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_OPENGEAR) + opengear_subscription = ImportedOpengearInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + + opengear_subscription.opengear.opengear_site = Site.from_subscription(opengear_site).site + opengear_subscription.opengear.opengear_hostname = opengear_hostname + opengear_subscription.opengear.opengear_wan_address = opengear_wan_address + opengear_subscription.opengear.opengear_wan_netmask = opengear_wan_netmask + opengear_subscription.opengear.opengear_wan_gateway = opengear_wan_gateway + + opengear_subscription = SubscriptionModel.from_other_lifecycle( + opengear_subscription, SubscriptionLifecycle.ACTIVE + ) + opengear_subscription.description = description + opengear_subscription.start_date = start_date + + if status: + opengear_subscription.status = status + + opengear_subscription.save() + db.session.commit() + + return str(opengear_subscription.subscription_id) + + return subscription_create diff --git a/test/fixtures/router_fixtures.py b/test/fixtures/router_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..04c77cede9d10f0d573e217983bd3a1462281d3e --- /dev/null +++ b/test/fixtures/router_fixtures.py @@ -0,0 +1,77 @@ +import ipaddress + +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_blocks.router import RouterRole +from gso.products.product_types.router import ImportedRouterInactive, RouterInactive +from gso.products.product_types.site import Site +from gso.services import subscriptions +from gso.utils.helpers import iso_from_ipv4 +from gso.utils.shared_enums import Vendor +from gso.utils.types.ip_address import IPv4AddressType, IPv6AddressType + + +@pytest.fixture() +def router_subscription_factory(site_subscription_factory, faker, geant_partner): + def subscription_create( + description: str | None = None, + start_date: str | None = "2023-05-24T00:00:00+00:00", + router_fqdn: str | None = None, + router_ts_port: int | None = None, + router_lo_ipv4_address: IPv4AddressType | None = None, + router_lo_ipv6_address: IPv6AddressType | None = None, + router_lo_iso_address: str | None = None, + router_role: RouterRole | None = RouterRole.PE, + router_site=None, + status: SubscriptionLifecycle | None = None, + partner: dict | None = None, + vendor: Vendor | None = Vendor.NOKIA, + *, + router_access_via_ts: bool | None = None, + is_imported: bool | None = True, + ) -> UUIDstr: + if partner is None: + partner = geant_partner + if is_imported: + product_id = subscriptions.get_product_id_by_name(ProductName.ROUTER) + router_subscription = RouterInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + else: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_ROUTER) + router_subscription = ImportedRouterInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + + router_subscription.router.router_fqdn = router_fqdn or faker.domain_name(levels=4) + router_subscription.router.router_ts_port = router_ts_port or faker.port_number(is_user=True) + router_subscription.router.router_access_via_ts = router_access_via_ts or faker.boolean() + router_subscription.router.router_lo_ipv4_address = router_lo_ipv4_address or ipaddress.IPv4Address( + faker.ipv4() + ) + router_subscription.router.router_lo_ipv6_address = router_lo_ipv6_address or ipaddress.IPv6Address( + faker.ipv6() + ) + router_subscription.router.router_lo_iso_address = router_lo_iso_address or iso_from_ipv4(faker.ipv4()) + router_subscription.router.router_role = router_role + router_subscription.router.router_site = Site.from_subscription(router_site or site_subscription_factory()).site + router_subscription.router.vendor = vendor + + router_subscription = SubscriptionModel.from_other_lifecycle(router_subscription, SubscriptionLifecycle.ACTIVE) + router_subscription.insync = True + router_subscription.description = description or faker.text(max_nb_chars=30) + router_subscription.start_date = start_date + + if status: + router_subscription.status = status + + router_subscription.save() + db.session.commit() + + return str(router_subscription.subscription_id) + + return subscription_create diff --git a/test/fixtures/site_fixtures.py b/test/fixtures/site_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..6fa904183f0d8effe142f2c199f180494cbfd7c2 --- /dev/null +++ b/test/fixtures/site_fixtures.py @@ -0,0 +1,69 @@ +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_blocks.site import SiteTier +from gso.products.product_types.site import ImportedSiteInactive, SiteInactive +from gso.services import subscriptions +from gso.utils.types.coordinates import LatitudeCoordinate, LongitudeCoordinate +from gso.utils.types.ip_address import IPAddress +from gso.utils.types.site_name import SiteName + + +@pytest.fixture() +def site_subscription_factory(faker, geant_partner): + def subscription_create( + description: str | None = None, + site_name: SiteName | 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_bgp_community_id: int | None = None, + site_internal_id: int | None = None, + site_tier: SiteTier | None = None, + site_ts_address: IPAddress | None = None, + status: SubscriptionLifecycle | None = None, + partner: dict | None = None, + start_date="2023-05-24T00:00:00+00:00", + *, + is_imported: bool | None = True, + ) -> UUIDstr: + if partner is None: + partner = geant_partner + + if is_imported: + product_id = subscriptions.get_product_id_by_name(ProductName.SITE) + site_subscription = SiteInactive.from_product_id(product_id, customer_id=partner["partner_id"], insync=True) + else: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_SITE) + site_subscription = ImportedSiteInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + + site_subscription.site.site_city = site_city or faker.city() + site_subscription.site.site_name = site_name or faker.site_name() + site_subscription.site.site_country = site_country or faker.country() + site_subscription.site.site_country_code = site_country_code or faker.country_code() + site_subscription.site.site_latitude = site_latitude or str(faker.latitude()) + site_subscription.site.site_longitude = site_longitude or str(faker.longitude()) + site_subscription.site.site_bgp_community_id = site_bgp_community_id or faker.pyint() + site_subscription.site.site_internal_id = site_internal_id or faker.pyint() + site_subscription.site.site_tier = site_tier or SiteTier.TIER1 + site_subscription.site.site_ts_address = site_ts_address or faker.ipv4() + + site_subscription = SubscriptionModel.from_other_lifecycle(site_subscription, SubscriptionLifecycle.ACTIVE) + site_subscription.description = description or "Site Subscription" + site_subscription.start_date = start_date + if status: + site_subscription.status = status + + site_subscription.save() + db.session.commit() + + return str(site_subscription.subscription_id) + + return subscription_create diff --git a/test/fixtures/super_pop_switch_fixtures.py b/test/fixtures/super_pop_switch_fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..5350e2a70b8eead32521ee16e5de095f24585133 --- /dev/null +++ b/test/fixtures/super_pop_switch_fixtures.py @@ -0,0 +1,73 @@ +import ipaddress + +import pytest +from orchestrator.db import db +from orchestrator.domain import SubscriptionModel +from orchestrator.types import SubscriptionLifecycle, UUIDstr + +from gso.products import ProductName +from gso.products.product_types.site import Site +from gso.products.product_types.super_pop_switch import ImportedSuperPopSwitchInactive, SuperPopSwitchInactive +from gso.services import subscriptions +from gso.utils.shared_enums import Vendor + + +@pytest.fixture() +def super_pop_switch_subscription_factory(site_subscription_factory, faker, geant_partner): + def subscription_create( + description=None, + start_date="2023-05-24T00:00:00+00:00", + super_pop_switch_fqdn=None, + super_pop_switch_ts_port=None, + super_pop_switch_mgmt_ipv4_address=None, + super_pop_switch_site=None, + status: SubscriptionLifecycle | None = None, + partner: dict | None = None, + *, + is_imported: bool | None = True, + ) -> UUIDstr: + if partner is None: + partner = geant_partner + + description = description or faker.text(max_nb_chars=30) + super_pop_switch_fqdn = super_pop_switch_fqdn or faker.domain_name(levels=4) + super_pop_switch_ts_port = super_pop_switch_ts_port or faker.random_int(min=1, max=49151) + super_pop_switch_mgmt_ipv4_address = super_pop_switch_mgmt_ipv4_address or ipaddress.IPv4Address(faker.ipv4()) + super_pop_switch_site = super_pop_switch_site or site_subscription_factory() + + if is_imported: + product_id = subscriptions.get_product_id_by_name(ProductName.SUPER_POP_SWITCH) + super_pop_switch_subscription = SuperPopSwitchInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + else: + product_id = subscriptions.get_product_id_by_name(ProductName.IMPORTED_SUPER_POP_SWITCH) + super_pop_switch_subscription = ImportedSuperPopSwitchInactive.from_product_id( + product_id, customer_id=partner["partner_id"], insync=True + ) + + super_pop_switch_subscription.super_pop_switch.super_pop_switch_fqdn = super_pop_switch_fqdn + super_pop_switch_subscription.super_pop_switch.super_pop_switch_ts_port = super_pop_switch_ts_port + super_pop_switch_subscription.super_pop_switch.super_pop_switch_mgmt_ipv4_address = ( + super_pop_switch_mgmt_ipv4_address + ) + super_pop_switch_subscription.super_pop_switch.super_pop_switch_site = Site.from_subscription( + super_pop_switch_site + ).site + super_pop_switch_subscription.super_pop_switch.vendor = Vendor.NOKIA + + super_pop_switch_subscription = SubscriptionModel.from_other_lifecycle( + super_pop_switch_subscription, SubscriptionLifecycle.ACTIVE + ) + super_pop_switch_subscription.description = description + super_pop_switch_subscription.start_date = start_date + + if status: + super_pop_switch_subscription.status = status + + super_pop_switch_subscription.save() + db.session.commit() + + return str(super_pop_switch_subscription.subscription_id) + + return subscription_create diff --git a/test/services/conftest.py b/test/services/conftest.py index 9557b1c5b091e51150ab78cf4b58c62f8018df8e..3467d545d2c571e947c75022c8a574197394e723 100644 --- a/test/services/conftest.py +++ b/test/services/conftest.py @@ -12,10 +12,22 @@ class MockedNetboxClient: def get_device_by_name(self): return self.BaseMockObject(id=1, name="test") + @staticmethod + def get_interface_by_name_and_device(interface_name: str, device_name: str): + return { + "name": f"{interface_name}", + "module": {"display": f"Module{interface_name}"}, + "description": f"Description{interface_name}-{device_name}", + } + @staticmethod def get_available_lags() -> list[str]: return [f"lag-{lag}" for lag in range(1, 5)] + @staticmethod + def get_available_services_lags() -> list[str]: + return [f"lag-{lag}" for lag in range(21, 50)] + @staticmethod def get_available_interfaces(): interfaces = [] diff --git a/test/services/test_librenms_client.py b/test/services/test_librenms_client.py index c64b5933ba2f4b845fe34c288cd2be34c8655aca..18b922da43913430628ab869f63685f99fc7d0c6 100644 --- a/test/services/test_librenms_client.py +++ b/test/services/test_librenms_client.py @@ -5,7 +5,7 @@ import pytest from requests import HTTPError from gso.services.librenms_client import LibreNMSClient -from gso.utils.types.snmp import SNMPVersion +from gso.utils.shared_enums import SNMPVersion @pytest.fixture() diff --git a/test/utils/test_helpers.py b/test/utils/test_helpers.py index 0df4ced9588e514f2cad00bbc8c22eaa2acdbfb5..880893b628183ff7a131745ca3be7c80164d15fa 100644 --- a/test/utils/test_helpers.py +++ b/test/utils/test_helpers.py @@ -3,9 +3,9 @@ from unittest.mock import patch import pytest from orchestrator.types import SubscriptionLifecycle -from gso.products import Router from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock from gso.products.product_blocks.router import RouterRole +from gso.products.product_types.router import Router from gso.utils.helpers import ( available_interfaces_choices_including_current_members, generate_inventory_for_active_routers, diff --git a/test/workflows/edge_port/__init__.py b/test/workflows/edge_port/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/workflows/edge_port/test_create_edge_port.py b/test/workflows/edge_port/test_create_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..ad804c91f5143148098b7cc4fbfc427d99783d29 --- /dev/null +++ b/test/workflows/edge_port/test_create_edge_port.py @@ -0,0 +1,129 @@ +from os import PathLike +from unittest.mock import patch + +import pytest +from pydantic_forms.exceptions import FormValidationError + +from gso.products import ProductName +from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType +from gso.products.product_types.edge_port import EdgePort +from gso.products.product_types.router import Router +from gso.services.subscriptions import get_product_id_by_name +from gso.utils.shared_enums import Vendor +from gso.utils.types.interfaces import PhysicalPortCapacity +from test.services.conftest import MockedNetboxClient +from test.workflows import ( + assert_complete, + assert_lso_interaction_success, + extract_state, + run_workflow, +) + + +@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_services_lags") as mock_available_services_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_available_services_lags.return_value = MockedNetboxClient().get_available_services_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(request, router_subscription_factory, partner_factory, faker): + create_edge_port_step = { + "tt_number": faker.tt_number(), + "node": router_subscription_factory(vendor=Vendor.NOKIA), + "partner": partner_factory(name="GAAR", email=faker.email())["partner_id"], + "service_type": EdgePortType.PUBLIC, + "geant_ga_id": faker.geant_gid(), + "enable_lacp": True, + "speed": PhysicalPortCapacity.HUNDRED_GIGABIT_PER_SECOND, + "encapsulation": EncapsulationType.DOT1Q, + "number_of_members": 2, + "minimum_links": 1, + } + create_edge_port_interface_step = { + "name": "lag-21", + "description": faker.sentence(), + "ae_members": [ + { + "interface_name": f"Interface{interface}", + "interface_description": faker.sentence(), + } + for interface in range(2) + ], + } + summary_view_step = {} + + return [ + create_edge_port_step, + create_edge_port_interface_step, + summary_view_step, + ] + + +@pytest.mark.workflow() +@patch("gso.services.lso_client._send_request") +def test_successful_edge_port_creation( + mock_execute_playbook, + responses, + input_form_wizard_data, + faker, + _netbox_client_mock, # noqa: PT019 + data_config_filename: PathLike, + test_client, +): + product_id = get_product_id_by_name(ProductName.EDGE_PORT) + initial_data = [{"product": product_id}, *input_form_wizard_data] + result, process_stat, step_log = run_workflow("create_edge_port", initial_data) + + for _ in range(2): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = EdgePort.from_subscription(subscription_id) + + assert subscription.status == "active" + ga_id = input_form_wizard_data[0]["geant_ga_id"] + router_fqdn = Router.from_subscription(input_form_wizard_data[0]["node"]).router.router_fqdn + assert subscription.description == f"Edge Port lag-21 on {router_fqdn}, GAAR, {ga_id}" + assert len(subscription.edge_port.edge_port_ae_members) == 2 + assert mock_execute_playbook.call_count == 2 + + +def test_edge_port_creation_with_invalid_input( + input_form_wizard_data, + faker, + _netbox_client_mock, # noqa: PT019 + data_config_filename: PathLike, + test_client, +): + product_id = get_product_id_by_name(ProductName.EDGE_PORT) + # If the number of members is greater than 1 then :term:`LACP` must be enabled. + input_form_wizard_data[0]["enable_lacp"] = False + initial_data = [{"product": product_id}, *input_form_wizard_data] + + with pytest.raises(FormValidationError) as error: + run_workflow("create_edge_port", initial_data) + + error = error.value.errors[0] + assert error["msg"] == "Number of members must be 1 if LACP is disabled." + assert error["loc"][0] == "__root__" diff --git a/test/workflows/edge_port/test_create_imported_edge_port.py b/test/workflows/edge_port/test_create_imported_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..c5590fe4f99c03c38c7a62b7129aa91ef8caef9a --- /dev/null +++ b/test/workflows/edge_port/test_create_imported_edge_port.py @@ -0,0 +1,44 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.edge_port import EdgePortType, EncapsulationType +from gso.products.product_types.edge_port import ImportedEdgePort +from gso.utils.shared_enums import Vendor +from gso.utils.types.interfaces import PhysicalPortCapacity +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.fixture() +def imported_edge_port_creation_input_form_data(router_subscription_factory, partner_factory, faker): + return { + "node": router_subscription_factory(vendor=Vendor.NOKIA), + "service_type": EdgePortType.CUSTOMER, + "speed": PhysicalPortCapacity.TEN_GIGABIT_PER_SECOND, + "encapsulation": EncapsulationType.DOT1Q, + "name": "lag34", + "minimum_links": 2, + "geant_ga_id": faker.geant_gid(), + "mac_address": faker.mac_address(), + "partner": partner_factory()["name"], + "enable_lacp": True, + "ignore_if_down": False, + "ae_members": [ + { + "interface_name": faker.network_interface(), + "interface_description": faker.sentence(), + }, + { + "interface_name": faker.network_interface(), + "interface_description": faker.sentence(), + }, + ], + "description": faker.sentence(), + } + + +def test_create_imported_edge_port_success(faker, imported_edge_port_creation_input_form_data): + result, _, _ = run_workflow("create_imported_edge_port", [imported_edge_port_creation_input_form_data]) + state = extract_state(result) + subscription = ImportedEdgePort.from_subscription(state["subscription_id"]) + assert_complete(result) + assert subscription.status == SubscriptionLifecycle.ACTIVE diff --git a/test/workflows/edge_port/test_import_edge_port.py b/test/workflows/edge_port/test_import_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..7fb1fcbdf851c9fb7b73aa30933e508c679b8743 --- /dev/null +++ b/test/workflows/edge_port/test_import_edge_port.py @@ -0,0 +1,18 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_types.edge_port import EdgePort +from test.workflows import assert_complete, run_workflow + + +@pytest.mark.workflow() +def test_import_edge_port_success(edge_port_subscription_factory): + imported_edge_port = edge_port_subscription_factory(is_imported=False) + result, _, _ = run_workflow("import_edge_port", [{"subscription_id": imported_edge_port}]) + subscription = EdgePort.from_subscription(imported_edge_port) + + assert_complete(result) + assert subscription.product.name == ProductName.EDGE_PORT + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.insync is True diff --git a/test/workflows/edge_port/test_modify_edge_port.py b/test/workflows/edge_port/test_modify_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..a7c824283f031c4dcb14cda53a7c8dfbb35527bb --- /dev/null +++ b/test/workflows/edge_port/test_modify_edge_port.py @@ -0,0 +1,168 @@ +from unittest.mock import patch + +import pytest + +from gso.products.product_types.edge_port import EdgePort +from gso.utils.types.interfaces import PhysicalPortCapacity +from test.workflows import ( + assert_complete, + assert_lso_interaction_success, + extract_state, + run_workflow, +) +from test.workflows.iptrunk.test_create_iptrunk import MockedNetboxClient + + +@pytest.fixture() +def input_form_wizard_data(request, faker, edge_port_subscription_factory, partner_factory): + subscription_id = edge_port_subscription_factory() + + return [ + {"subscription_id": subscription_id}, + { + "tt_number": faker.tt_number(), + "geant_ga_id": faker.geant_gid(), + "member_speed": PhysicalPortCapacity.FOUR_HUNDRED_GIGABIT_PER_SECOND, + "number_of_members": 1, + }, + { + "description": faker.sentence(), + "ae_members": [ + { + "interface_name": "Interface1", + "interface_description": faker.sentence(), + } + ], + }, + ] + + +@pytest.mark.workflow() +@patch("gso.services.lso_client._send_request") +@patch("gso.services.netbox_client.NetboxClient.get_available_interfaces") +@patch("gso.services.netbox_client.NetboxClient.attach_interface_to_lag") +@patch("gso.services.netbox_client.NetboxClient.reserve_interface") +@patch("gso.services.netbox_client.NetboxClient.allocate_interface") +@patch("gso.services.netbox_client.NetboxClient.free_interface") +@patch("gso.services.netbox_client.NetboxClient.detach_interfaces_from_lag") +@patch("gso.services.netbox_client.NetboxClient.get_interface_by_name_and_device") +def test_modify_edge_port_with_changing_capacity( + mocked_get_interface_by_name_and_device, + mocked_detach_interfaces_from_lag, + mocked_free_interface, + mocked_allocate_interface, + mocked_reserve_interface, + mocked_attach_interface_to_lag, + mocked_get_available_interfaces, + mocked_execute_playbook, + input_form_wizard_data, + faker, + data_config_filename, +): + # Set up mock return values + mocked_netbox = MockedNetboxClient() + mocked_get_available_interfaces.return_value = mocked_netbox.get_available_interfaces() + mocked_attach_interface_to_lag.return_value = mocked_netbox.attach_interface_to_lag() + mocked_reserve_interface.return_value = mocked_netbox.reserve_interface() + mocked_allocate_interface.return_value = mocked_netbox.allocate_interface() + mocked_free_interface.return_value = mocked_netbox.free_interface() + mocked_detach_interfaces_from_lag.return_value = mocked_netbox.detach_interfaces_from_lag() + mocked_get_interface_by_name_and_device.side_effect = mocked_netbox.get_interface_by_name_and_device + + # Run workflow + result, process_stat, step_log = run_workflow("modify_edge_port", input_form_wizard_data) + + for _ in range(2): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + assert_complete(result) + # Validate the final state and subscription data + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = EdgePort.from_subscription(subscription_id) + + assert subscription.status == "active" + assert mocked_execute_playbook.call_count == 2 + + # The number of members have been changed from 2 to 1 + assert mocked_reserve_interface.call_count == 1 + assert mocked_attach_interface_to_lag.call_count == 1 + assert mocked_free_interface.call_count == 2 + assert mocked_detach_interfaces_from_lag.call_count == 1 + assert subscription.edge_port.geant_ga_id == input_form_wizard_data[1]["geant_ga_id"] + assert len(subscription.edge_port.edge_port_ae_members) == 1 + + +@pytest.fixture() +def input_form_wizard_without_changing_capacity(request, faker, edge_port_subscription_factory, partner_factory): + subscription_id = edge_port_subscription_factory() + subscription = EdgePort.from_subscription(subscription_id) + + return [ + {"subscription_id": subscription_id}, + {"tt_number": faker.tt_number(), "geant_ga_id": faker.geant_gid()}, + { + "description": faker.sentence(), + "ae_members": [ + { + "interface_name": interface.interface_name, + "interface_description": interface.interface_description, + } + for interface in subscription.edge_port.edge_port_ae_members + ], + }, + ] + + +@pytest.mark.workflow() +@patch("gso.services.lso_client._send_request") +@patch("gso.services.netbox_client.NetboxClient.get_available_interfaces") +@patch("gso.services.netbox_client.NetboxClient.attach_interface_to_lag") +@patch("gso.services.netbox_client.NetboxClient.reserve_interface") +@patch("gso.services.netbox_client.NetboxClient.allocate_interface") +@patch("gso.services.netbox_client.NetboxClient.free_interface") +@patch("gso.services.netbox_client.NetboxClient.detach_interfaces_from_lag") +@patch("gso.services.netbox_client.NetboxClient.get_interface_by_name_and_device") +def test_modify_edge_port_without_changing_capacity( + mocked_get_interface_by_name_and_device, + mocked_detach_interfaces_from_lag, + mocked_free_interface, + mocked_allocate_interface, + mocked_reserve_interface, + mocked_attach_interface_to_lag, + mocked_get_available_interfaces, + mocked_execute_playbook, + input_form_wizard_without_changing_capacity, + faker, + data_config_filename, +): + # Set up mock return values + mocked_netbox = MockedNetboxClient() + mocked_get_available_interfaces.return_value = mocked_netbox.get_available_interfaces() + mocked_attach_interface_to_lag.return_value = mocked_netbox.attach_interface_to_lag() + mocked_reserve_interface.return_value = mocked_netbox.reserve_interface() + mocked_allocate_interface.return_value = mocked_netbox.allocate_interface() + mocked_free_interface.return_value = mocked_netbox.free_interface() + mocked_detach_interfaces_from_lag.return_value = mocked_netbox.detach_interfaces_from_lag() + mocked_get_interface_by_name_and_device.side_effect = mocked_netbox.get_interface_by_name_and_device + + # Run workflow + result, _, _ = run_workflow("modify_edge_port", input_form_wizard_without_changing_capacity) + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = EdgePort.from_subscription(subscription_id) + + assert subscription.status == "active" + + # The capacity has not been changed so the following methods should not be called + assert mocked_execute_playbook.call_count == 0 + assert mocked_reserve_interface.call_count == 0 + assert mocked_attach_interface_to_lag.call_count == 0 + assert mocked_free_interface.call_count == 0 + assert mocked_detach_interfaces_from_lag.call_count == 0 + + assert subscription.edge_port.geant_ga_id == input_form_wizard_without_changing_capacity[1]["geant_ga_id"] + assert len(subscription.edge_port.edge_port_ae_members) == 2 + assert subscription.edge_port.edge_port_description == input_form_wizard_without_changing_capacity[2]["description"] diff --git a/test/workflows/edge_port/test_terminate_edge_port.py b/test/workflows/edge_port/test_terminate_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..b5a16866548235c3f878190d757f9ebe9507b35e --- /dev/null +++ b/test/workflows/edge_port/test_terminate_edge_port.py @@ -0,0 +1,56 @@ +from unittest.mock import patch + +import pytest + +from gso.products.product_types.edge_port import EdgePort +from test.services.conftest import MockedNetboxClient +from test.workflows import ( + assert_complete, + assert_lso_interaction_success, + extract_state, + run_workflow, +) + + +@pytest.mark.workflow() +@patch("gso.services.lso_client._send_request") +@patch("gso.services.netbox_client.NetboxClient.delete_interface") +@patch("gso.services.netbox_client.NetboxClient.free_interface") +def test_successful_edge_port_termination( + mocked_free_interface, + mocked_delete_interface, + mock_execute_playbook, + edge_port_subscription_factory, + faker, + data_config_filename, +): + # Set up mock return values + subscription_id = edge_port_subscription_factory() + mocked_netbox = MockedNetboxClient() + mocked_delete_interface.return_value = mocked_netbox.delete_interface() + mocked_free_interface.return_value = mocked_netbox.free_interface() + + # Run workflow + initial_data = [ + {"subscription_id": subscription_id}, + { + "tt_number": faker.tt_number(), + }, + ] + result, process_stat, step_log = run_workflow("terminate_edge_port", initial_data) + + for _ in range(2): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + assert_complete(result) + + # Check NetboxClient calls + assert mocked_delete_interface.call_count == 1 # Delete the lag + assert mocked_free_interface.call_count == 2 # Free interfaces attached to the lag which is 2 + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = EdgePort.from_subscription(subscription_id) + + assert subscription.status == "terminated" + assert mock_execute_playbook.call_count == 2 diff --git a/test/workflows/edge_port/test_validate_edge_port.py b/test/workflows/edge_port/test_validate_edge_port.py new file mode 100644 index 0000000000000000000000000000000000000000..3aff8081a8a4986751997db723cba7c293250c5c --- /dev/null +++ b/test/workflows/edge_port/test_validate_edge_port.py @@ -0,0 +1,62 @@ +from unittest.mock import patch + +import pytest + +from gso.products.product_types.edge_port import EdgePort +from test.services.conftest import MockedNetboxClient +from test.workflows import ( + assert_complete, + assert_lso_success, + extract_state, + run_workflow, +) + + +@pytest.mark.workflow() +@patch("gso.services.lso_client._send_request") +@patch("gso.services.netbox_client.NetboxClient.get_interface_by_name_and_device") +def test_validate_edge_port_success( + mock_get_interface_by_name_and_device, + mock_execute_playbook, + edge_port_subscription_factory, + faker, + data_config_filename, +): + subscription_id = edge_port_subscription_factory() + mock_get_interface_by_name_and_device.side_effect = [ + MockedNetboxClient.BaseMockObject( + name="iFace1", + module=MockedNetboxClient.BaseMockObject(display="display1"), + description=subscription_id, + enabled=True, + ), + MockedNetboxClient.BaseMockObject( + name="iFace2", + module=MockedNetboxClient.BaseMockObject(display="display2"), + description=subscription_id, + enabled=True, + ), + MockedNetboxClient.BaseMockObject( + name="Iface3", + module=MockedNetboxClient.BaseMockObject(display="display3"), + description=subscription_id, + enabled=True, + ), + ] + + # Run workflow + initial_data = [{"subscription_id": subscription_id}] + result, process_stat, step_log = run_workflow("validate_edge_port", initial_data) + + state = extract_state(result) + subscription_id = state["subscription_id"] + + for _ in range(1): + result, step_log = assert_lso_success(result, process_stat, step_log) + + assert_complete(result) + subscription = EdgePort.from_subscription(subscription_id) + assert subscription.status == "active" + assert mock_execute_playbook.call_count == 1 + # One time for getting the :term:`LAG` and two times for getting the interfaces + assert mock_get_interface_by_name_and_device.call_count == 3 diff --git a/test/workflows/iptrunk/test_activate_iptrunk.py b/test/workflows/iptrunk/test_activate_iptrunk.py index 837f340c6adc77e1aa148760e425429288e4d01f..d1ed9b3347e52f902952dbff1eec2bbc5e1ffaf1 100644 --- a/test/workflows/iptrunk/test_activate_iptrunk.py +++ b/test/workflows/iptrunk/test_activate_iptrunk.py @@ -1,6 +1,6 @@ import pytest -from gso.products import Iptrunk +from gso.products.product_types.iptrunk import Iptrunk from test.workflows import ( assert_complete, assert_suspended, diff --git a/test/workflows/iptrunk/test_create_iptrunk.py b/test/workflows/iptrunk/test_create_iptrunk.py index 6be42faed62222a5d899e4f5977a26e813c4f2f7..7a38a234a7e0ba98dea56081b07e0bcc8be21c7e 100644 --- a/test/workflows/iptrunk/test_create_iptrunk.py +++ b/test/workflows/iptrunk/test_create_iptrunk.py @@ -4,8 +4,9 @@ from unittest.mock import patch import pytest from infoblox_client.objects import HostRecord -from gso.products import Iptrunk, ProductName +from gso.products import ProductName from gso.products.product_blocks.iptrunk import IptrunkType +from gso.products.product_types.iptrunk import Iptrunk from gso.services.subscriptions import get_product_id_by_name from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import PhysicalPortCapacity @@ -89,6 +90,7 @@ def input_form_wizard_data(request, router_subscription_factory, faker): "side_b_ae_geant_a_sid": faker.geant_sid(), "side_b_ae_members": side_b_members, } + summary_view_step = {} return [ create_ip_trunk_step, @@ -97,6 +99,7 @@ def input_form_wizard_data(request, router_subscription_factory, faker): create_ip_trunk_side_a_step, create_ip_trunk_side_b_router_name, create_ip_trunk_side_b_step, + summary_view_step, ] diff --git a/test/workflows/iptrunk/test_deploy_twamp.py b/test/workflows/iptrunk/test_deploy_twamp.py index e65feac79a0a1fd9988eff99fe0bf2e55f68899b..1b475140658105d076e6f1d0465d5bd9b5e78281 100644 --- a/test/workflows/iptrunk/test_deploy_twamp.py +++ b/test/workflows/iptrunk/test_deploy_twamp.py @@ -2,7 +2,7 @@ from unittest.mock import patch import pytest -from gso.products import Iptrunk +from gso.products.product_types.iptrunk import Iptrunk from test.workflows import ( assert_complete, assert_lso_interaction_success, diff --git a/test/workflows/iptrunk/test_import_iptrunk.py b/test/workflows/iptrunk/test_import_iptrunk.py index 99cdbfd93fc1cd84c7ebf55e7375b1df1d420b97..52a52c329dbad987d3b360315522d0bda827354c 100644 --- a/test/workflows/iptrunk/test_import_iptrunk.py +++ b/test/workflows/iptrunk/test_import_iptrunk.py @@ -15,4 +15,4 @@ def test_import_iptrunk_success(iptrunk_subscription_factory): assert_complete(result) assert subscription.product.name == ProductName.IP_TRUNK assert subscription.status == SubscriptionLifecycle.ACTIVE - assert subscription.insync + assert subscription.insync is True diff --git a/test/workflows/iptrunk/test_modify_isis_metric.py b/test/workflows/iptrunk/test_modify_isis_metric.py index 26a9bbd490eb4c73728a2e11defd836bf3701316..f81baac86f23d5abb4e85d84b61ac84edccaeb92 100644 --- a/test/workflows/iptrunk/test_modify_isis_metric.py +++ b/test/workflows/iptrunk/test_modify_isis_metric.py @@ -2,7 +2,7 @@ from unittest.mock import patch import pytest -from gso.products import Iptrunk +from gso.products.product_types.iptrunk import Iptrunk from test.workflows import ( assert_complete, assert_lso_interaction_success, @@ -26,6 +26,7 @@ def test_iptrunk_modify_isis_metric_success( initial_iptrunk_data = [ {"subscription_id": product_id}, {"tt_number": faker.tt_number(), "isis_metric": new_isis_metric}, + {}, ] result, process_stat, step_log = run_workflow("modify_isis_metric", initial_iptrunk_data) diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py index 359308cdf880ef7b16a341073ce203410749d15d..77470e79d7865cd68aa3254ae14b67a497171004 100644 --- a/test/workflows/iptrunk/test_modify_trunk_interface.py +++ b/test/workflows/iptrunk/test_modify_trunk_interface.py @@ -2,8 +2,8 @@ from unittest.mock import patch import pytest -from gso.products import Iptrunk from gso.products.product_blocks.iptrunk import IptrunkType +from gso.products.product_types.iptrunk import Iptrunk from gso.utils.shared_enums import Vendor from gso.utils.types.interfaces import LAGMemberList, PhysicalPortCapacity from test.conftest import UseJuniperSide diff --git a/test/workflows/iptrunk/test_terminate_iptrunk.py b/test/workflows/iptrunk/test_terminate_iptrunk.py index c745a615ad6c1178703951e1c87247ee0760b5af..83e0324a0f1baa661dbd473bf11d179d17588f95 100644 --- a/test/workflows/iptrunk/test_terminate_iptrunk.py +++ b/test/workflows/iptrunk/test_terminate_iptrunk.py @@ -2,8 +2,8 @@ from unittest.mock import patch import pytest -from gso.products import Iptrunk from gso.products.product_blocks.router import RouterRole +from gso.products.product_types.iptrunk import Iptrunk from gso.settings import load_oss_params from test.services.conftest import MockedNetboxClient from test.workflows import ( diff --git a/test/workflows/nren_l3_core_service/__init__.py b/test/workflows/nren_l3_core_service/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/workflows/nren_l3_core_service/test_create_imported_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_create_imported_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..63aab373915f348d3a8e8e7f7bf161c2e1e5c190 --- /dev/null +++ b/test/workflows/nren_l3_core_service/test_create_imported_nren_l3_core_service.py @@ -0,0 +1,68 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_blocks.bgp_session import IPFamily +from gso.products.product_types.nren_l3_core_service import ImportedNRENL3CoreService, NRENL3CoreServiceType +from gso.utils.shared_enums import SBPType +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) +def test_create_imported_nren_l3_core_service_success( + faker, partner_factory, edge_port_subscription_factory, l3_core_service_type +): + creation_form_input_data = { + "partner": partner_factory()["name"], + "service_type": l3_core_service_type, + "service_binding_ports": [ + { + "edge_port": edge_port_subscription_factory(), + "ap_type": "PRIMARY", + "geant_sid": faker.geant_sid(), + "sbp_type": SBPType.L3, + "is_tagged": faker.boolean(), + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv4_mask": faker.ipv4_netmask(), + "ipv6_address": faker.ipv6(), + "ipv6_mask": faker.ipv6_netmask(), + "custom_firewall_filters": faker.boolean(), + "bgp_peers": [ + { + "bfd_enabled": faker.boolean(), + "bfd_interval": faker.pyint(), + "bfd_multiplier": faker.pyint(), + "has_custom_policies": faker.boolean(), + "authentication_key": faker.password(), + "multipath_enabled": faker.boolean(), + "send_default_route": faker.boolean(), + "is_passive": faker.boolean(), + "peer_address": faker.ipv4(), + "families": [IPFamily.V4UNICAST, IPFamily.V4MULTICAST], + "is_multi_hop": faker.boolean(), + "rtbh_enabled": faker.boolean(), + }, + { + "bfd_enabled": faker.boolean(), + "bfd_interval": faker.pyint(), + "bfd_multiplier": faker.pyint(), + "has_custom_policies": faker.boolean(), + "authentication_key": faker.password(), + "multipath_enabled": faker.boolean(), + "send_default_route": faker.boolean(), + "is_passive": faker.boolean(), + "peer_address": faker.ipv6(), + "families": [IPFamily.V6UNICAST], + "is_multi_hop": faker.boolean(), + "rtbh_enabled": faker.boolean(), + }, + ], + } + ], + } + + result, _, _ = run_workflow("create_imported_nren_l3_core_service", [creation_form_input_data]) + state = extract_state(result) + subscription = ImportedNRENL3CoreService.from_subscription(state["subscription_id"]) + assert_complete(result) + assert subscription.status == SubscriptionLifecycle.ACTIVE diff --git a/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..1287bd01c9288d4f577512f8a832f565ed38a01a --- /dev/null +++ b/test/workflows/nren_l3_core_service/test_create_nren_l3_core_service.py @@ -0,0 +1,81 @@ +from unittest.mock import patch + +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products import ProductName +from gso.products.product_types.nren_l3_core_service import NRENL3CoreService +from gso.services.subscriptions import get_product_id_by_name +from gso.utils.shared_enums import APType +from test.workflows import assert_complete, assert_lso_interaction_success, extract_state, run_workflow + + +@pytest.fixture() +def base_bgp_peer_input(faker): + def _base_bgp_peer_input(): + bfd_enabled = faker.boolean() + return { + "bfd_enabled": bfd_enabled, + "bfd_interval": faker.pyint() if bfd_enabled else None, + "bfd_multiplier": faker.pyint() if bfd_enabled else None, + "has_custom_policies": faker.boolean(), + "authentication_key": faker.password(), + "multipath_enabled": faker.boolean(), + "send_default_route": faker.boolean(), + "is_passive": faker.boolean(), + } + + return _base_bgp_peer_input + + +@pytest.mark.parametrize("l3_core_type", [ProductName.GEANT_IP, ProductName.IAS]) +@pytest.mark.workflow() +@patch("gso.services.lso_client._send_request") +def test_create_nren_l3_core_service_success( + mock_lso_client, + l3_core_type, + responses, + faker, + partner_factory, + edge_port_subscription_factory, + base_bgp_peer_input, + data_config_filename, +): + partner = partner_factory() + product_id = get_product_id_by_name(l3_core_type) + edge_port_a = edge_port_subscription_factory(partner=partner) + + form_input_data = [ + {"product": product_id}, + {"tt_number": faker.tt_number(), "partner": partner["partner_id"]}, + {"edge_ports": [{"edge_port": edge_port_a, "ap_type": APType.PRIMARY}]}, + { + "geant_sid": faker.geant_sid(), + "is_tagged": faker.boolean(), + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv4_mask": faker.ipv4_netmask(), + "ipv6_address": faker.ipv6(), + "ipv6_mask": faker.ipv6_netmask(), + "custom_firewall_filters": faker.boolean(), + "v4_bgp_peer": base_bgp_peer_input() | {"add_v4_multicast": faker.boolean(), "peer_address": faker.ipv4()}, + "v6_bgp_peer": base_bgp_peer_input() | {"add_v6_multicast": faker.boolean(), "peer_address": faker.ipv6()}, + }, + ] + lso_interaction_count = 6 + + result, process_stat, step_log = run_workflow("create_nren_l3_core_service", form_input_data) + + for _ in range(lso_interaction_count): + result, step_log = assert_lso_interaction_success(result, process_stat, step_log) + + assert_complete(result) + state = extract_state(result) + subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) + assert mock_lso_client.call_count == lso_interaction_count + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert len(subscription.nren_l3_core_service.nren_ap_list) == 1 + assert ( + str(subscription.nren_l3_core_service.nren_ap_list[0].sbp.edge_port.owner_subscription_id) + == form_input_data[2]["edge_ports"][0]["edge_port"] + ) diff --git a/test/workflows/nren_l3_core_service/test_import_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_import_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..13095de6ead5ad679d2ccb9ceb1106d986a79470 --- /dev/null +++ b/test/workflows/nren_l3_core_service/test_import_nren_l3_core_service.py @@ -0,0 +1,23 @@ +import pytest +from orchestrator.types import SubscriptionLifecycle + +from gso.products.product_types.nren_l3_core_service import NRENL3CoreService, NRENL3CoreServiceType +from test.workflows import assert_complete, run_workflow + + +@pytest.mark.parametrize( + "l3_core_service_type", [NRENL3CoreServiceType.IMPORTED_GEANT_IP, NRENL3CoreServiceType.IMPORTED_IAS] +) +@pytest.mark.workflow() +def test_import_nren_l3_core_service_success(nren_l3_core_service_subscription_factory, l3_core_service_type): + imported_nren_l3_core_service = nren_l3_core_service_subscription_factory( + nren_l3_core_service_type=l3_core_service_type + ) + result, _, _ = run_workflow("import_nren_l3_core_service", [{"subscription_id": imported_nren_l3_core_service}]) + subscription = NRENL3CoreService.from_subscription(imported_nren_l3_core_service) + + assert_complete(result) + # Remove the "IMPORTED_" prefix with ``[9:]`` + assert subscription.nren_l3_core_service_type == NRENL3CoreServiceType(l3_core_service_type.value[9:]) + assert subscription.status == SubscriptionLifecycle.ACTIVE + assert subscription.insync is True diff --git a/test/workflows/nren_l3_core_service/test_migrate_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_migrate_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..a4ad8a9f95a0c834536fae3e76478b65e35264cc --- /dev/null +++ b/test/workflows/nren_l3_core_service/test_migrate_nren_l3_core_service.py @@ -0,0 +1,49 @@ +import pytest + +from gso.products.product_types.nren_l3_core_service import NRENL3CoreService, NRENL3CoreServiceType +from test.workflows import assert_complete, extract_state, run_workflow + + +@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) +@pytest.mark.workflow() +def test_migrate_nren_l3_core_service_success( + faker, + edge_port_subscription_factory, + partner_factory, + nren_l3_core_service_subscription_factory, + l3_core_service_type, +): + partner = partner_factory() + subscription_id = nren_l3_core_service_subscription_factory( + partner=partner, nren_l3_core_service_type=l3_core_service_type + ) + new_edge_port_1 = edge_port_subscription_factory(partner=partner) + new_edge_port_2 = edge_port_subscription_factory(partner=partner) + subscription = NRENL3CoreService.from_subscription(subscription_id) + + form_input_data = [ + {"subscription_id": subscription_id}, + { + "tt_number": faker.tt_number(), + "edge_port_selection": [ + { + "old_edge_port": subscription.nren_l3_core_service.nren_ap_list[0].sbp.edge_port.description, + "new_edge_port": new_edge_port_1, + }, + { + "old_edge_port": subscription.nren_l3_core_service.nren_ap_list[1].sbp.edge_port.description, + "new_edge_port": new_edge_port_2, + }, + ], + }, + ] + + result, _, _ = run_workflow("migrate_nren_l3_core_service", form_input_data) + + assert_complete(result) + state = extract_state(result) + subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) + assert subscription.insync is True + assert len(subscription.nren_l3_core_service.nren_ap_list) == 2 + assert str(subscription.nren_l3_core_service.nren_ap_list[0].sbp.edge_port.owner_subscription_id) == new_edge_port_1 + assert str(subscription.nren_l3_core_service.nren_ap_list[1].sbp.edge_port.owner_subscription_id) == new_edge_port_2 diff --git a/test/workflows/nren_l3_core_service/test_modify_nren_l3_core_service.py b/test/workflows/nren_l3_core_service/test_modify_nren_l3_core_service.py new file mode 100644 index 0000000000000000000000000000000000000000..197be36c55baf085b5d624bd9bcee8946c7c157e --- /dev/null +++ b/test/workflows/nren_l3_core_service/test_modify_nren_l3_core_service.py @@ -0,0 +1,264 @@ +import pytest + +from gso.products.product_blocks.bgp_session import IPFamily +from gso.products.product_types.nren_l3_core_service import NRENL3CoreService, NRENL3CoreServiceType +from gso.utils.shared_enums import APType +from test.workflows import extract_state, run_workflow + + +@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) +@pytest.mark.workflow() +def test_modify_nren_l3_core_service_remove_edge_port_success( + nren_l3_core_service_subscription_factory, l3_core_service_type +): + subscription_id = nren_l3_core_service_subscription_factory(nren_l3_core_service_type=l3_core_service_type) + subscription = NRENL3CoreService.from_subscription(subscription_id) + access_port = subscription.nren_l3_core_service.nren_ap_list[0] + input_form_data = [ + {"subscription_id": subscription_id}, + { + "access_ports": [ + { + "edge_port": str(access_port.sbp.edge_port.owner_subscription_id), + "ap_type": APType.LOAD_BALANCED, + } + ] # The factory generates a subscription with two Access Ports, this will remove the second one. + }, + {}, + ] + + result, _, _ = run_workflow("modify_nren_l3_core_service", input_form_data) + + state = extract_state(result) + subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) + assert len(subscription.nren_l3_core_service.nren_ap_list) == 1 + assert subscription.nren_l3_core_service.nren_ap_list[0].ap_type == APType.LOAD_BALANCED + + +@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) +@pytest.mark.workflow() +def test_modify_nren_l3_core_service_add_new_edge_port_success( + nren_l3_core_service_subscription_factory, + edge_port_subscription_factory, + partner_factory, + faker, + l3_core_service_type, +): + partner = partner_factory() + new_edge_port = edge_port_subscription_factory(partner=partner) + subscription_id = nren_l3_core_service_subscription_factory( + partner=partner, nren_l3_core_service_type=l3_core_service_type + ) + subscription = NRENL3CoreService.from_subscription(subscription_id) + input_form_data = [ + {"subscription_id": subscription_id}, + { + "access_ports": [ + { + "edge_port": str(port.sbp.edge_port.owner_subscription_id), + "ap_type": port.ap_type, + } + for port in subscription.nren_l3_core_service.nren_ap_list + ] + + [ + { + "edge_port": str(new_edge_port), + "ap_type": APType.BACKUP, + } + ] + }, + {}, # The existing SBPs are unchanged + {}, + { # Adding configuration for the new SBP + "geant_sid": faker.geant_sid(), + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv6_address": faker.ipv6(), + "v4_bgp_peer": { + "authentication_key": faker.password(), + "peer_address": faker.ipv4(), + }, + "v6_bgp_peer": { + "authentication_key": faker.password(), + "peer_address": faker.ipv6(), + }, + }, + ] + + result, _, _ = run_workflow("modify_nren_l3_core_service", input_form_data) + + state = extract_state(result) + subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) + assert len(subscription.nren_l3_core_service.nren_ap_list) == 3 + + +@pytest.fixture() +def sbp_input_form_data(faker): + def _generate_form_data(): + return { + "geant_sid": faker.geant_sid(), + "is_tagged": True, + "vlan_id": faker.vlan_id(), + "ipv4_address": faker.ipv4(), + "ipv6_address": faker.ipv6(), + "custom_firewall_filters": True, + "v4_bgp_peer": { + "bfd_enabled": True, + "bfd_interval": faker.pyint(), + "bfd_multiplier": faker.pyint(), + "has_custom_policies": True, + "authentication_key": faker.password(), + "multipath_enabled": True, + "send_default_route": True, + "is_passive": True, + "peer_address": faker.ipv4(), + "add_v4_multicast": True, + }, + "v6_bgp_peer": { + "bfd_enabled": True, + "bfd_interval": faker.pyint(), + "bfd_multiplier": faker.pyint(), + "has_custom_policies": True, + "authentication_key": faker.password(), + "multipath_enabled": True, + "send_default_route": True, + "is_passive": True, + "peer_address": faker.ipv6(), + "add_v6_multicast": True, + }, + } + + return _generate_form_data + + +@pytest.mark.parametrize("l3_core_service_type", [NRENL3CoreServiceType.GEANT_IP, NRENL3CoreServiceType.IAS]) +@pytest.mark.workflow() +def test_modify_nren_l3_core_service_modify_edge_port_success( + faker, nren_l3_core_service_subscription_factory, l3_core_service_type, sbp_input_form_data +): + subscription_id = nren_l3_core_service_subscription_factory(nren_l3_core_service_type=l3_core_service_type) + subscription = NRENL3CoreService.from_subscription(subscription_id) + new_sbp_data = [sbp_input_form_data(), sbp_input_form_data()] + input_form_data = [ + {"subscription_id": subscription_id}, + { + "access_ports": [ + { + "edge_port": str(port.sbp.edge_port.owner_subscription_id), + "ap_type": port.ap_type, + } + for port in subscription.nren_l3_core_service.nren_ap_list + ] + }, + {**new_sbp_data[0]}, + {**new_sbp_data[1]}, + ] + + result, _, _ = run_workflow("modify_nren_l3_core_service", input_form_data) + + state = extract_state(result) + subscription = NRENL3CoreService.from_subscription(state["subscription_id"]) + assert len(subscription.nren_l3_core_service.nren_ap_list) == 2 + + for i in range(2): + assert subscription.nren_l3_core_service.nren_ap_list[i].sbp.geant_sid == new_sbp_data[i]["geant_sid"] + assert subscription.nren_l3_core_service.nren_ap_list[i].sbp.is_tagged == new_sbp_data[i]["is_tagged"] + assert subscription.nren_l3_core_service.nren_ap_list[i].sbp.vlan_id == new_sbp_data[i]["vlan_id"] + assert ( + str(subscription.nren_l3_core_service.nren_ap_list[i].sbp.ipv4_address) == new_sbp_data[i]["ipv4_address"] + ) + assert ( + str(subscription.nren_l3_core_service.nren_ap_list[i].sbp.ipv6_address) == new_sbp_data[i]["ipv6_address"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.custom_firewall_filters + == new_sbp_data[i]["custom_firewall_filters"] + ) + + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].bfd_enabled + == new_sbp_data[i]["v4_bgp_peer"]["bfd_enabled"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].bfd_interval + == new_sbp_data[i]["v4_bgp_peer"]["bfd_interval"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].bfd_multiplier + == new_sbp_data[i]["v4_bgp_peer"]["bfd_multiplier"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].has_custom_policies + == new_sbp_data[i]["v4_bgp_peer"]["has_custom_policies"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].authentication_key + == new_sbp_data[i]["v4_bgp_peer"]["authentication_key"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].multipath_enabled + == new_sbp_data[i]["v4_bgp_peer"]["multipath_enabled"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].send_default_route + == new_sbp_data[i]["v4_bgp_peer"]["send_default_route"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].is_passive + == new_sbp_data[i]["v4_bgp_peer"]["is_passive"] + ) + assert ( + str(subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].peer_address) + == new_sbp_data[i]["v4_bgp_peer"]["peer_address"] + ) + assert ( + bool( + IPFamily.V4MULTICAST + in subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[0].families + ) + == new_sbp_data[i]["v4_bgp_peer"]["add_v4_multicast"] + ) + + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].bfd_enabled + == new_sbp_data[i]["v6_bgp_peer"]["bfd_enabled"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].bfd_interval + == new_sbp_data[i]["v6_bgp_peer"]["bfd_interval"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].bfd_multiplier + == new_sbp_data[i]["v6_bgp_peer"]["bfd_multiplier"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].has_custom_policies + == new_sbp_data[i]["v6_bgp_peer"]["has_custom_policies"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].authentication_key + == new_sbp_data[i]["v6_bgp_peer"]["authentication_key"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].multipath_enabled + == new_sbp_data[i]["v6_bgp_peer"]["multipath_enabled"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].send_default_route + == new_sbp_data[i]["v6_bgp_peer"]["send_default_route"] + ) + assert ( + subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].is_passive + == new_sbp_data[i]["v6_bgp_peer"]["is_passive"] + ) + assert ( + str(subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].peer_address) + == new_sbp_data[i]["v6_bgp_peer"]["peer_address"] + ) + assert ( + bool( + IPFamily.V6MULTICAST + in subscription.nren_l3_core_service.nren_ap_list[i].sbp.bgp_session_list[1].families + ) + == new_sbp_data[i]["v6_bgp_peer"]["add_v6_multicast"] + ) diff --git a/test/workflows/office_router/test_import_office_router.py b/test/workflows/office_router/test_import_office_router.py index c9894e98eeb5abb69843f849e04c6209daff8983..e86e3ed1978271adb61cc4c0024759434ba9953a 100644 --- a/test/workflows/office_router/test_import_office_router.py +++ b/test/workflows/office_router/test_import_office_router.py @@ -15,4 +15,4 @@ def test_import_office_router_success(office_router_subscription_factory): assert_complete(result) assert subscription.product.name == ProductName.OFFICE_ROUTER assert subscription.status == SubscriptionLifecycle.ACTIVE - assert subscription.insync + assert subscription.insync is True diff --git a/test/workflows/opengear/test_create_imported_opengear.py b/test/workflows/opengear/test_create_imported_opengear.py index 0f6d9bf171103c5b5152937b47114a9b666bad5c..9058c602b96ed8fc08d4d9145706b3c688a321b1 100644 --- a/test/workflows/opengear/test_create_imported_opengear.py +++ b/test/workflows/opengear/test_create_imported_opengear.py @@ -1,7 +1,8 @@ import pytest from orchestrator.types import SubscriptionLifecycle -from gso.products import ImportedOpengear, ProductName +from gso.products import ProductName +from gso.products.product_types.opengear import ImportedOpengear from gso.products.product_types.site import Site from test.workflows import ( assert_complete, diff --git a/test/workflows/opengear/test_import_opengear.py b/test/workflows/opengear/test_import_opengear.py index 37c7d1718444341b8f645942b0c6baaf24f7e3a6..6ca1da2523951ffc5ff8fe3e296bfd46fed9a76f 100644 --- a/test/workflows/opengear/test_import_opengear.py +++ b/test/workflows/opengear/test_import_opengear.py @@ -15,4 +15,4 @@ def test_import_office_router_success(opengear_subscription_factory): assert_complete(result) assert subscription.product.name == ProductName.OPENGEAR assert subscription.status == SubscriptionLifecycle.ACTIVE - assert subscription.insync + assert subscription.insync is True diff --git a/test/workflows/router/test_activate_router.py b/test/workflows/router/test_activate_router.py index da0c24a6b3826fef0269bb54e348bff8d13be62a..c118dcaf4d1e4cb1efac97d50b3c8b7870c47593 100644 --- a/test/workflows/router/test_activate_router.py +++ b/test/workflows/router/test_activate_router.py @@ -1,6 +1,6 @@ import pytest -from gso.products import Router +from gso.products.product_types.router import Router from test.workflows import ( assert_complete, assert_suspended, diff --git a/test/workflows/router/test_create_router.py b/test/workflows/router/test_create_router.py index a6bf456f3ef249d52817b2381c9cc3fcd40a5c86..f9fb37fcfa38159a5d92c1aca8a9bd0aea2c5051 100644 --- a/test/workflows/router/test_create_router.py +++ b/test/workflows/router/test_create_router.py @@ -3,9 +3,10 @@ from unittest.mock import patch import pytest from infoblox_client import objects -from gso.products import ProductName, Site +from gso.products import ProductName from gso.products.product_blocks.router import RouterRole from gso.products.product_types.router import Router +from gso.products.product_types.site import Site from gso.services.subscriptions import get_product_id_by_name from gso.utils.shared_enums import Vendor from test import USER_CONFIRM_EMPTY_FORM @@ -67,7 +68,7 @@ def test_create_nokia_router_success( mock_sharepoint_client.return_value = MockedSharePointClient # Run workflow - initial_router_data = [{"product": product_id}, router_creation_input_form_data] + initial_router_data = [{"product": product_id}, router_creation_input_form_data, {}] result, process_stat, step_log = run_workflow("create_router", initial_router_data) state = extract_state(result) @@ -175,7 +176,7 @@ def test_create_nokia_router_lso_failure( # Run workflow product_id = get_product_id_by_name(ProductName.ROUTER) - initial_router_data = [{"product": product_id}, router_creation_input_form_data] + initial_router_data = [{"product": product_id}, router_creation_input_form_data, {}] result, process_stat, step_log = run_workflow("create_router", initial_router_data) result, step_log = assert_lso_interaction_success(result, process_stat, step_log) diff --git a/test/workflows/router/test_import_router.py b/test/workflows/router/test_import_router.py index 3c06b3385b501f031b90ca9769da038295becbb8..2bab9ac22a03bb9a0ec5f60da14fb56403668033 100644 --- a/test/workflows/router/test_import_router.py +++ b/test/workflows/router/test_import_router.py @@ -15,4 +15,4 @@ def test_import_site_success(router_subscription_factory): assert_complete(result) assert subscription.product.name == ProductName.ROUTER assert subscription.status == SubscriptionLifecycle.ACTIVE - assert subscription.insync + assert subscription.insync is True diff --git a/test/workflows/router/test_modify_connection_stratey.py b/test/workflows/router/test_modify_connection_strategy.py similarity index 94% rename from test/workflows/router/test_modify_connection_stratey.py rename to test/workflows/router/test_modify_connection_strategy.py index e460f2e9b5f36627908434f0e9b6af6a019a4667..fd3a01357a9fbb7558a38a0797d6ff55d23eeeb9 100644 --- a/test/workflows/router/test_modify_connection_stratey.py +++ b/test/workflows/router/test_modify_connection_strategy.py @@ -1,6 +1,6 @@ import pytest -from gso.products import Router +from gso.products.product_types.router import Router from gso.utils.shared_enums import ConnectionStrategy from test.workflows import assert_complete, run_workflow diff --git a/test/workflows/router/test_redeploy_base_config.py b/test/workflows/router/test_redeploy_base_config.py index c9697cb73cfda6ec60699ee547badbbbb4c0dfd3..2023ec04e005cab469e45251061de6138b563c8d 100644 --- a/test/workflows/router/test_redeploy_base_config.py +++ b/test/workflows/router/test_redeploy_base_config.py @@ -2,7 +2,7 @@ from unittest.mock import patch import pytest -from gso.products import Router +from gso.products.product_types.router import Router from test.workflows import ( assert_complete, assert_lso_interaction_success, diff --git a/test/workflows/router/test_terminate_router.py b/test/workflows/router/test_terminate_router.py index a182cc744ab35a093b84cdea0e884a5fb93e84f0..d2dca96f7492c67f2396fb8935801670798b43b4 100644 --- a/test/workflows/router/test_terminate_router.py +++ b/test/workflows/router/test_terminate_router.py @@ -2,8 +2,8 @@ from unittest.mock import patch import pytest -from gso.products import Router from gso.products.product_blocks.router import RouterRole +from gso.products.product_types.router import Router from test.services.conftest import MockedKentikClient from test.workflows import assert_complete, assert_lso_interaction_success, extract_state, run_workflow diff --git a/test/workflows/router/test_update_ibgp_mesh.py b/test/workflows/router/test_update_ibgp_mesh.py index 023cd2cb5e03bc0420635830e1e6e54c5bb066b8..693bfd671160abe62d35a6340a08c8220c31a59f 100644 --- a/test/workflows/router/test_update_ibgp_mesh.py +++ b/test/workflows/router/test_update_ibgp_mesh.py @@ -5,8 +5,8 @@ from orchestrator.types import SubscriptionLifecycle from orchestrator.workflow import StepStatus from pydantic_forms.exceptions import FormValidationError -from gso.products import Iptrunk from gso.products.product_blocks.router import RouterRole +from gso.products.product_types.iptrunk import Iptrunk from test import USER_CONFIRM_EMPTY_FORM from test.workflows import ( assert_lso_interaction_success, diff --git a/test/workflows/site/test_create_site.py b/test/workflows/site/test_create_site.py index 7642debea0c2739b1a5845db4404f35c5205884a..112e078f39c78a1545ada8d14be18dbd61da7858 100644 --- a/test/workflows/site/test_create_site.py +++ b/test/workflows/site/test_create_site.py @@ -25,6 +25,7 @@ def test_create_site(responses, faker): "site_tier": SiteTier.TIER1, "site_ts_address": faker.ipv4(), }, + {}, ] result, _, _ = run_workflow("create_site", initial_site_data) assert_complete(result) @@ -66,6 +67,7 @@ def test_site_name_is_incorrect(responses, faker): "site_ts_address": faker.ipv4(), "partner": "GEANT", }, + {}, ] with pytest.raises(FormValidationError, match=expected_exception_msg): diff --git a/test/workflows/site/test_import_site.py b/test/workflows/site/test_import_site.py index ac476c107498f342e2e206e42d6457c966fd6732..f706a4737b5c2747693529ab084646c47848d32e 100644 --- a/test/workflows/site/test_import_site.py +++ b/test/workflows/site/test_import_site.py @@ -15,4 +15,4 @@ def test_import_site_success(site_subscription_factory): assert_complete(result) assert subscription.product.name == ProductName.SITE assert subscription.status == SubscriptionLifecycle.ACTIVE - assert subscription.insync + assert subscription.insync is True diff --git a/test/workflows/site/test_modify_site.py b/test/workflows/site/test_modify_site.py index 0db1a50fcd9f7880aeb574fa465afc08e832e808..fb45a9576e6917e66af9370dab3a69435bdf20db 100644 --- a/test/workflows/site/test_modify_site.py +++ b/test/workflows/site/test_modify_site.py @@ -6,15 +6,16 @@ from test.workflows import assert_complete, extract_state, run_workflow @pytest.mark.workflow() -def test_modify_site(responses, site_subscription_factory): +def test_modify_site(responses, site_subscription_factory, faker): subscription_id = site_subscription_factory() initial_site_data = [ {"subscription_id": subscription_id}, { - "site_bgp_community_id": 10, - "site_internal_id": 20, - "site_ts_address": "127.0.0.1", + "site_bgp_community_id": faker.pyint(), + "site_internal_id": faker.pyint(), + "site_ts_address": faker.ipv4(), }, + {}, ] result, _, _ = run_workflow("modify_site", initial_site_data) assert_complete(result) @@ -28,16 +29,57 @@ def test_modify_site(responses, site_subscription_factory): @pytest.mark.workflow() -def test_modify_site_with_invalid_data(responses, site_subscription_factory): - subscription_a = Site.from_subscription(site_subscription_factory()) - subscription_b = Site.from_subscription(site_subscription_factory()) +def test_modify_site_with_duplicate_bgp_community_id(faker, site_subscription_factory): + duplicate_bgp_community_id = faker.pyint() + + site_subscription_factory(site_bgp_community_id=duplicate_bgp_community_id) + subscription_b = site_subscription_factory() initial_site_data = [ - {"subscription_id": subscription_b.subscription_id}, + {"subscription_id": subscription_b}, { - "site_bgp_community_id": subscription_a.site.site_bgp_community_id, + "site_bgp_community_id": duplicate_bgp_community_id, }, + {}, ] with pytest.raises(FormValidationError, match="site_bgp_community_id must be unique"): run_workflow("modify_site", initial_site_data) + + +@pytest.mark.workflow() +def test_modify_site_with_duplicate_internal_id(faker, site_subscription_factory): + duplicate_internal_id = faker.pyint() + + site_subscription_factory(site_internal_id=duplicate_internal_id) + subscription_b = site_subscription_factory() + + initial_site_data = [ + {"subscription_id": subscription_b}, + { + "site_internal_id": duplicate_internal_id, + }, + {}, + ] + + with pytest.raises(FormValidationError, match="site_internal_id must be unique"): + run_workflow("modify_site", initial_site_data) + + +@pytest.mark.workflow() +def test_modify_site_with_duplicate_ts_address(faker, site_subscription_factory): + duplicate_ts_address = faker.ipv4() + + site_subscription_factory(site_ts_address=duplicate_ts_address) + subscription_b = site_subscription_factory() + + initial_site_data = [ + {"subscription_id": subscription_b}, + { + "site_ts_address": duplicate_ts_address, + }, + {}, + ] + + with pytest.raises(FormValidationError, match="site_ts_address must be unique"): + run_workflow("modify_site", initial_site_data) diff --git a/test/workflows/super_pop_switch/test_import_super_pop_switch.py b/test/workflows/super_pop_switch/test_import_super_pop_switch.py index 4d20774c7d91470f6210da9e19d15632794bbf7c..2961b53988122eec82bcb4ccbde63f85acbfb864 100644 --- a/test/workflows/super_pop_switch/test_import_super_pop_switch.py +++ b/test/workflows/super_pop_switch/test_import_super_pop_switch.py @@ -15,4 +15,4 @@ def test_import_super_pop_switch_success(super_pop_switch_subscription_factory): assert_complete(result) assert subscription.product.name == ProductName.SUPER_POP_SWITCH assert subscription.status == SubscriptionLifecycle.ACTIVE - assert subscription.insync + assert subscription.insync is True diff --git a/test/workflows/tasks/test_delete_partners.py b/test/workflows/tasks/test_delete_partners.py index b0964bdfc93be71624a8f947876cae956a4810b2..3fb36c8415016939ac22cad68f2e3590d5c9399f 100644 --- a/test/workflows/tasks/test_delete_partners.py +++ b/test/workflows/tasks/test_delete_partners.py @@ -6,7 +6,7 @@ from pydantic_forms.exceptions import FormValidationError from sqlalchemy import select from gso.services.partners import filter_partners_by_name -from test.fixtures import create_subscription_for_mapping +from test.fixtures.common_fixtures import create_subscription_for_mapping from test.workflows import assert_complete, run_workflow CORRECT_SUBSCRIPTION = str(uuid4()) diff --git a/tox.ini b/tox.ini index a6d04258b1967deeb79007b5ba99e27d12559547..b5cb43cbef816cce1a45cc953c6d6f7aa7441b8c 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ commands = ruff check --respect-gitignore --preview . ruff format --respect-gitignore --preview --check . mypy . - sh -c 'if [ $SKIP_ALL_TESTS = 1 ]; then echo "Skipping coverage report"; else pytest --cov=gso --cov-report=xml --cov-report=html --cov-fail-under=85 -n auto {posargs}; fi' + sh -c 'if [ $SKIP_ALL_TESTS = 1 ]; then echo "Skipping coverage report"; else pytest --cov=gso --cov-report=xml --cov-report=html --cov-fail-under=90 -n auto {posargs}; fi' allowlist_externals = sh