From 53e32341e1a76e654fecdfa848c7e3e444f01422 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Tue, 12 May 2026 20:56:01 +0300 Subject: [PATCH 01/12] Initial import --- .classpath | 51 + .gitignore | 4 + .project | 23 + .settings/org.eclipse.core.resources.prefs | 6 + .settings/org.eclipse.jdt.apt.core.prefs | 5 + .settings/org.eclipse.jdt.core.prefs | 17 + .settings/org.eclipse.m2e.core.prefs | 4 + Dockerfile | 26 + LICENSE | 201 + README.md | 363 +- ...tf-teas-ietf-network-slice-nbi-yang-25.txt | 7056 +++++++++++++++++ doc/NSC/examples/ietf_green_request.json | 172 + .../slice_request_backhaul_control.json | 162 + .../examples/slice_request_backhaul_user.json | 164 + doc/NSC/nsc_swagger.json | 971 +++ doc/bootstrap_phase.puml | 56 + doc/resource_creation_lifecycle.puml | 67 + pom.xml | 372 + .../ietf/ns/IETFNSGCSpringBoot.java | 60 + .../ns/api/CategoryConfigurationService.java | 131 + .../ietf/ns/api/PartnerRouteBuilder.java | 91 + ...ResourceSpecificationTemplateRegistry.java | 84 + .../api/SloSleTemplateBootstrapService.java | 1125 +++ .../api/config/ActiveMQComponentConfig.java | 22 + .../api/domain/model/AttachmentCircuit.java | 19 + .../ns/api/domain/model/ConnectionGroup.java | 27 + .../domain/model/ConnectivityConstruct.java | 27 + .../ns/api/domain/model/ConnectivityType.java | 13 + .../ns/api/domain/model/ConstructStatus.java | 17 + .../ns/api/domain/model/MatchCriterion.java | 22 + .../ietf/ns/api/domain/model/MatchType.java | 12 + .../domain/model/NetworkSliceServices.java | 117 + .../ietf/ns/api/domain/model/PolicyRef.java | 17 + .../ietf/ns/api/domain/model/QosPolicy.java | 20 + .../Rfc9543SliceServiceDeserializer.java | 283 + .../ietf/ns/api/domain/model/SDP.java | 30 + .../ietf/ns/api/domain/model/SdpMember.java | 16 + .../ns/api/domain/model/ServiceStatus.java | 19 + .../ietf/ns/api/domain/model/ServiceTag.java | 17 + .../ns/api/domain/model/ServiceTagType.java | 10 + .../ns/api/domain/model/SliceService.java | 42 + .../ns/api/domain/model/SloSleTemplate.java | 156 + .../model/slo_sle/AvailabilityType.java | 35 + .../api/domain/model/slo_sle/Diversity.java | 35 + .../api/domain/model/slo_sle/MetricBound.java | 101 + .../domain/model/slo_sle/PathConstraints.java | 40 + .../model/slo_sle/ServiceFunctions.java | 56 + .../model/slo_sle/ServiceIsolationType.java | 24 + .../model/slo_sle/ServiceSecurityType.java | 23 + .../model/slo_sle/ServiceSloMetricType.java | 42 + .../api/domain/model/slo_sle/SlePolicy.java | 100 + .../model/slo_sle/SliceTemplateRef.java | 29 + .../api/domain/model/slo_sle/SloPolicy.java | 83 + .../model/slo_sle/TePathDisjointness.java | 26 + .../ietf/ns/api/restconf/RestconfClient.java | 130 + .../ns/api/restconf/RestconfClientImpl.java | 536 ++ .../ietf/ns/api/restconf/RestconfConfig.java | 70 + .../api/restconf/RestconfConsumerService.java | 354 + .../ns/api/restconf/RestconfException.java | 63 + .../ns/api/restconf/Rfc9543JsonConverter.java | 260 + ...c9543NetworkSliceServicesDeserializer.java | 154 + .../Rfc9543SliceServiceDeserializer.java | 109 + .../Rfc9543SloSleTemplateDeserializer.java | 350 + .../ietf/ns/demo/RestconfServerDemo.java | 43 + .../ietf/ns/demo/Rfc9543DemoService.java | 223 + .../Rfc9543NetworkSliceServicesResponse.java | 136 + .../ietf/ns/demo/Rfc9543RestController.java | 119 + .../ietf/ns/demo/Rfc9543SloSleTemplate.java | 173 + .../ietf/ns/demo/SecurityConfig.java | 103 + .../ns/domain/common/ExcludeFromMapping.java | 46 + .../common/LogicalResourceMappable.java | 239 + .../RelatedManagedResourceReference.java | 67 + .../EntityToLogicalResourceMapper.java | 462 ++ .../EntityToLogicalResourceSpecMapper.java | 453 ++ .../LogicalResourceToEntityMapper.java | 521 ++ .../ietf/ns/mappers/TransformationUtils.java | 33 + .../repository/SloSleTemplateRepository.java | 93 + .../TMFResourceInventoryRepository.java | 77 + .../repository/TMFResourceSpecRepository.java | 56 + .../repository/impl/ResourceRepoService.java | 217 + .../repository/impl/SimpleResourceMapper.java | 14 + .../TMFResourceInventoryRepositoryImpl.java | 268 + .../impl/TMFResourceSpecRepositoryImpl.java | 292 + src/main/resources/application-demo.yaml | 75 + src/main/resources/application.yml | 119 + src/main/resources/banner.txt | 11 + src/main/resources/ietf_green_request.json | 172 + .../slice_request_backhaul_control.json | 162 + .../slice_request_backhaul_user.json | 164 + .../Rfc9543SliceServiceDeserializerTest.java | 249 + .../api/restconf/RestconfClientImplTest.java | 444 ++ 91 files changed, 19692 insertions(+), 56 deletions(-) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 .settings/org.eclipse.jdt.apt.core.prefs create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 .settings/org.eclipse.m2e.core.prefs create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 doc/NSC/draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt create mode 100644 doc/NSC/examples/ietf_green_request.json create mode 100644 doc/NSC/examples/slice_request_backhaul_control.json create mode 100644 doc/NSC/examples/slice_request_backhaul_user.json create mode 100644 doc/NSC/nsc_swagger.json create mode 100644 doc/bootstrap_phase.puml create mode 100644 doc/resource_creation_lifecycle.puml create mode 100644 pom.xml create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/IETFNSGCSpringBoot.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/CategoryConfigurationService.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/PartnerRouteBuilder.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/ResourceSpecificationTemplateRegistry.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/config/ActiveMQComponentConfig.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/AttachmentCircuit.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectionGroup.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityConstruct.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityType.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConstructStatus.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/MatchCriterion.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/MatchType.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/PolicyRef.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/QosPolicy.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializer.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SDP.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SdpMember.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceStatus.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceTag.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceTagType.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SliceService.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/AvailabilityType.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/Diversity.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/PathConstraints.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceFunctions.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceIsolationType.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSecurityType.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSloMetricType.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SliceTemplateRef.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/TePathDisjointness.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClient.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConfig.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfException.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/Rfc9543JsonConverter.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543NetworkSliceServicesDeserializer.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SliceServiceDeserializer.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SloSleTemplateDeserializer.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/RestconfServerDemo.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543DemoService.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543NetworkSliceServicesResponse.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543RestController.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543SloSleTemplate.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/SecurityConfig.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/ExcludeFromMapping.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceMappable.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/RelatedManagedResourceReference.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceMapper.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/LogicalResourceToEntityMapper.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/TransformationUtils.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/repository/SloSleTemplateRepository.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/repository/TMFResourceInventoryRepository.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/repository/TMFResourceSpecRepository.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/SimpleResourceMapper.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/TMFResourceInventoryRepositoryImpl.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/TMFResourceSpecRepositoryImpl.java create mode 100644 src/main/resources/application-demo.yaml create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/banner.txt create mode 100644 src/main/resources/ietf_green_request.json create mode 100644 src/main/resources/slice_request_backhaul_control.json create mode 100644 src/main/resources/slice_request_backhaul_user.json create mode 100644 src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializerTest.java create mode 100644 src/test/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImplTest.java diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..a6b9e8a --- /dev/null +++ b/.classpath @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3317e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +/.apt_generated/ +/.apt_generated_tests/ +/.factorypath diff --git a/.project b/.project new file mode 100644 index 0000000..b2664e6 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + org.etsi.osl.controllers.ietf.ns + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..29abf99 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding//src/test/resources=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 0000000..fa6bcfb --- /dev/null +++ b/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,5 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=.apt_generated +org.eclipse.jdt.apt.genTestSrcDir=.apt_generated_tests +org.eclipse.jdt.apt.reconcileEnabled=true diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..d9cfd05 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,17 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..50d98a5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Multi-stage build for TFS Controller +# Stage 1: Build the application +FROM maven:3.9.6-eclipse-temurin-17 AS builder + +WORKDIR /build + +# Copy pom.xml and download dependencies +COPY pom.xml . +RUN mvn dependency:go-offline -B + +# Copy source code +COPY src ./src + +# Build the application +RUN mvn clean package -DskipTests + +# Stage 2: Runtime image +FROM eclipse-temurin:17-jdk-alpine + +WORKDIR /app + +# Copy the built JAR from builder stage +COPY --from=builder /build/target/org.etsi.osl.controllers.ietf.ns-0.0.1-SNAPSHOT.jar app.jar + +# Run the application +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 956fc3d..08c9356 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,344 @@ -# org.etsi.osl.controllers.ietf.ns +# IETF NS Controller - OpenSlice IETF RFC 9543 Network Slice Service Resource Controller for Teraflow SDN +## Overview +The **IETF NS Controller** is a Spring Boot microservice implementing the customer part of the **IETF RFC 9543 Network Slice Service** for Teraflow SDN. This microservice acts as an **OSL Resource Controller** that bridges +OpenSlice microservices (like TMF API, Service Orchestrator (OSOM)) and Network Slice Service Providers (e.g., TerflowSDN) through a **message-driven architecture** using Apache Camel and ActiveMQ. -## Getting started +**WARNING: This artifact is still under testing and not fully functional** -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +### Architectural approach -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! +``` +┌─────────────────────────────────────────────────────────────┐ +│ OpenSlice TMF API, Service Orchestrator (OSOM) │ +│ (Network Slice Service Customer per RFC 9543) │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ActiveMQ/JMS Message Queues (CREATE/UPDATE/DELETE) + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ IETF NS Controller (This Service) │ +│ - Resource Controller for Network Slice Services │ +│ - Listens on JMS queues for resource operations │ +│ - Transforms between TMF and RFC 9543 models │ +│ - Manages resource specifications & instances │ +└──────────────────────┬──────────────────────────────────────┘ + │ + RESTCONF Protocol (RFC 9543) + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Network Slice Service Provider (e.g. TerflowSDN) │ +│ (RESTCONF Server implementing RFC 9543) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Key Responsibilities + +1. **Resource Controller Registration** - Automatically registers as a Resource Controller of resource specifications under category (ns.ietf.controllers.osl.etsi.org/v1alpha) with OpenSlice on startup +2. **Message-Driven Operations** - Listens on ActiveMQ queues for CREATE/UPDATE/DELETE requests +3. **Model Transformation** - Converts between IETF RFC 9543 Network Slice models and TMF resource specifications +4. **RESTCONF Provisioning** - Provisions network slices to RFC 9543-compliant Network Slice Service Providers +5. **Catalog Management** - Manages resource specifications and instances in the OpenSlice TMF catalog +6. **SLO/SLE Template System** - Handles Service Level Objectives and Expectations based on RFC 9543 + +## Message-Driven Architecture -## Add your files +### ActiveMQ Queue Pattern -* [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -* [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command: +The controller listens for messages on queues following this pattern: ``` -cd existing_repo -git remote add origin https://labs.etsi.org/rep/osl/code/addons/org.etsi.osl.controllers.ietf.ns.git -git branch -M main -git push -uf origin main +{CREATE|UPDATE|DELETE}/{category}/{version} ``` -## Integrate with your tools +For this controller: +- **Category:** `ns.ietf.controllers.osl.etsi.org/v1alpha` +- **Version:** `0.1.0` -* [Set up project integrations](https://labs.etsi.org/rep/osl/code/addons/org.etsi.osl.controllers.ietf.ns/-/settings/integrations) +**Example queue names:** +- `CREATE/ns.ietf.controllers.osl.etsi.org/v1alpha/0.1.0` +- `UPDATE/ns.ietf.controllers.osl.etsi.org/v1alpha/0.1.0` +- `DELETE/ns.ietf.controllers.osl.etsi.org/v1alpha/0.1.0` -## Collaborate with your team +### Message Flow -* [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -* [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -* [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -* [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -* [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/) +1. **OpenSlice OSOM** sends a resource request to the controller's CREATE queue +2. **Apache Camel route** intercepts the message and triggers `ResourceRepoService` +3. **ResourceRepoService** extracts the RFC 9543 JSON payload from the message +4. **RFC 9543 Deserializer** parses the JSON to `SliceService` domain objects +5. **RESTCONF Consumer** provisions the slice service to the Network Slice Service Provider +6. **Response** is sent back via reply queues or catalog updates -## Test and Deploy +### Message Headers -Use the built-in continuous integration in GitLab. +Important metadata is included in message headers: -* [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/) -* [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -* [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -* [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -* [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +| Header | Description | +|--------|-------------| +| `org.etsi.osl.resourceId` | UUID of the resource created in OpenSlice (must be updated with status) | +| `org.etsi.osl.serviceOrderId` | UUID of the related service order | +| `org.etsi.osl.serviceId` | UUID of the related service | + +**Note:** it updates the resource status in the TMF API back to OpenSlice (e.g., `AVAILABLE`, `TERMINATED`, `UNKNOWN`) to reflect provisioning success/failure. + +## RFC 9543 Integration + +### IETF Network Slice Service Framework + +This controller implements the IETF RFC 9543 Network Slice Service specification: + +- **Network Slice Customer:** OpenSlice OSOM (service orchestrator requesting network slices) +- **Network Slice Controller:** IETF NS Controller (resource controller managing slice lifecycle) +- **Network Slice Service Provider:** RFC 9543-compliant systems (e.g., TerflowSDN) + +### Supported Features + +- **Slice Service Management** - CREATE/UPDATE/DELETE network slice services +- **SLO/SLE Templates** - Service Level Objective/Expectation policies + +### Field Mapping + +The controller automatically maps RFC 9543 hyphenated JSON field names to Java camelCase properties: + +| RFC 9543 JSON | Java Property | Type | +|---|---|---| +| `service-tags` | `serviceTags` | `List` | +| `slo-sle-policy` | `sloSleTemplate` | `SloSleTemplate` | +| `connection-groups` | `connectionGroups` | `List` | +| `sdp-ip-address` | `sdpIpAddress` | `List` | + +## OpenSlice Integration + +### Resource Specification Registration + +On startup, the `SloSleTemplateBootstrapService` performs the following steps: + +1. **Retrieves SLO/SLE templates** from the RESTCONF provider +2. **Registers each template** as a `LogicalResourceSpecification` in the OpenSlice catalog **Category:** `ns.ietf.controllers.osl.etsi.org/v1alpha`. Each template contains a **jsonRequest** (for testing/reference) that is used as an example +3. These `LogicalResourceSpecification`s can be used for service orders. The **jsonRequest** is important since it is the payload that the IETF NS Controller will POST to NSC (**WARNING: This needs testing**) + +**Bootstrap Sequence Diagram:** + +See: [`doc/bootstrap_phase.puml`](doc/bootstrap_phase.puml) +![`doc/bootstrap_phase.puml`](//www.plantuml.com/plantuml/png/VLJ1Zjem4BtdAqPxQWz0jxr5rMhPR5XLeWjAqbEfAYiPWbl7JiTEwFvzFS8A66ttG33XlUStRyQ-jqwG6pe53yOuwqZqFxS7OJ4HjJC4DzMgXCneHqOff1iG5lohfFSiMSjUQ0St1LfN6xttE3jqI2NIA6jaC1JP_y1AedO14qCgfBqon_BnUQVV5NbPPPld5R0gqljWmVyPaqfbIeKLThqI3gTgBhqyR3PLMHNBRSpCX1FAj1U6icMrN6-UOjYcHrqghmNLrnKijryOokiauP1cTtVd3L9Ozht72YUDBb0qBv2FNfrJbQDmUE4bcPQimO6bKA0ZYIE22_LOs9Ffe2SpoWRfhCFv9luHk2ayvHKiH2yNsYx63mlBZcVsbC9iahiKO3xJJwccC2NEKeH_1hHduo7wYfym2vkotu4qn3tu_YDfCEQTjfhAv3Znr26FXgDqXxSqF2dKUXNsMbhtEIRUnRmoZZbYhmo1nrvldUxqxHmoGbPOczPtKnLepK3USu-rt8V-xlJ7EJoXHc8a_XMUZ_3B-iwVmjlfJtDODitbEwWFWhn19EzTrjVsmWHoigq78BtfOEhEDBb9MB0OpsWAsqrPWJCG1132sFCaxxJ_WQsXbnJStixhwS3RkR5gZixAkQ5sCTuArH_4UJyUV_-1MMJwLPDGSvJO-CP4zCdgTLVBeQxHsajyWBxHpD2lq8LemdPwRmWS-hnr--gnAvqXl1e3a1ewx7mshunUe3IHMZXAwVbAXTgm6uTJBV4De8r3C2CIynA0-D85QK6R8n3V8zn2mXhbY1wO5VcooXVVje_yzPYlyog73gqLBTe4W0lMw8w6VeFbA23S13P1tG3lczpxrT2fv1y0) + +### jsonRequest and SliceService Model + +The **jsonRequest** is a critical characteristic that carries the RFC 9543 Network Slice Service specification in JSON format during the service order: + +**What is jsonRequest?** +- A JSON string containing a complete RFC 9543 **SliceService** definition +- Stored in the `LogicalResourceSpecification` as a resource characteristic +- Generated automatically during bootstrap with example values for each SLO/SLE template +- Passed through the message queue when OSOM creates a resource instance + +**SliceService Structure** (RFC 9543 Format) + +The jsonRequest contains a SliceService object with: + +| Field | Description | Example | +|-------|-------------|---------| +| `id` | Unique service identifier | `slice-service-001` | +| `description` | Service description | `Example network slice with SLO/SLE template B` | +| `service-tags` | Tags for service classification | `["tag1", "tag2"]` | +| `slo-sle-policy` | Reference to SLO/SLE template | `{"id": "template-B"}` | +| `connection-groups` | Connectivity requirements | Array of connection group definitions | +| `sdp` | Service Demarcation Points | Array with node-id and sdp-ip-address | + +**Example jsonRequest:** +```json +{ + "id": "example-slice-001", + "description": "Example network slice service using template B", + "service-tags": ["production", "voice"], + "slo-sle-policy": { + "id": "B", + "availabilityLevel": "four-nines", + "mtuSize": 1500 + }, + "connection-groups": [ + { + "id": "A_B", + "connectivity-type": "ietf-vpn-common:any-to-any", + "sdp-directions": ["A", "B"] + } + ], + "sdp": [ + { + "id": "CU-N32", + "node-id": "A", + "sdp-ip-address": ["10.60.11.3"] + }, + { + "id": "UPF-N32", + "node-id": "B", + "sdp-ip-address": ["10.60.10.6"] + } + ] +} +``` + +**How jsonRequest is Used:** -*** +1. **Bootstrap Phase** - `SloSleTemplateBootstrapService` generates example jsonRequest for each template +2. **Resource Registration** - jsonRequest is stored as a characteristic in the `LogicalResourceSpecification` +3. **Service Ordering** - When OSOM creates a resource instance using the resource spec, it passes the jsonRequest +4. **Resource Creation** - The jsonRequest is included in the CREATE message to the IETF NS Controller's queue +5. **Deserialization** - The `ResourceRepoService` extracts jsonRequest and deserializes it to a `SliceService` object using `Rfc9543SliceServiceDeserializer` +6. **RESTCONF Provisioning** - The `RestconfConsumerService` uses the SliceService object to provision the network slice via RESTCONF to the Network Slice Service Provider (TerflowSDN) -# Editing this README +**Field Name Mapping:** -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +The custom `Rfc9543SliceServiceDeserializer` automatically handles the conversion from RFC 9543's hyphenated field names to Java camelCase properties: -## Suggestions for a good README +- `service-tags` → `serviceTags` (List) +- `slo-sle-policy` → `sloSleTemplate` (SloSleTemplate) +- `connection-groups` → `connectionGroups` (List) +- `sdp-ip-address` → `sdpIpAddress` (List) +- `connectivity-type` → `connectivityType` (ConnectivityType enum: P2P, P2MP, A2A) -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +### Resource Lifecycle -## Name -Choose a self-explaining name for your project. +When OpenSlice OSOM creates a resource using a registered resource specification: -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +1. OSOM sends a `CREATE` message to the controller's queue, i.e. `CREATE/ns.ietf.controllers.osl.etsi.org/v1alpha/0.1.0` +2. Controller parses the resource request from the message +3. Controller provisions the network slice to the RFC 9543 provider (TeraflowSDN - TFS) +4. Controller updates the resource status in OpenSlice: + - **Success:** Status = `AVAILABLE`, Message includes provisioned service ID + - **Failure:** Status = `UNKNOWN`, Health = `Unhealthy`, Message describes error -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +**Resource Creation Sequence Diagram:** -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +See: [`doc/resource_creation_lifecycle.puml`](doc/resource_creation_lifecycle.puml) +![`doc/resource_creation_lifecycle.puml`](//www.plantuml.com/plantuml/png/TLN1afj64Btp5LqwhQjMqhgI7AAgkAnbMKVsIkm2SWuXXmcq5kpH39aPj5PIlkO3yedvaZJ3I8DmvGA2UQ_xTVUZzLORfaoxvm4hZ5GmYHiKAQyBX3YDrnDWslu86eBLHTqmOM2oB4nzmFKJt2hv6PL5FMsZRJI2DQoe44AsHsunuN8TFLnY4jIoLGKcsCWKrO4oMsYIf2FWhlZnX--_izeSC1Ttg57LZDT_EXLtKHebtsGmXHIxK0hTxb5TP0b7MJ9KEovVqhjDBgwIACt2U7CF7GNLAql9xkdmaXGomxXy3dcDmX0lMz_3yd7rojMSBH_YTq7GjH6cRzxqdLh0Ovnc42RHCejWZrgpPohegSKM5-xrV3QRpG-l6MygDh-PlPxTvE9MbiS5_ALSsrRbDNpIKYJuHulQN0DHlWQicmypw8OIs9lDRIUmW4Is1azPFRoVJs1l5avJM42ZP478qwH2XOIzSkHNdjsDBA2BPqPVZABZeKBOARdFnKa_51Nh8AXgJGtLb_oFDDcIGWy3-1HsrWlOiwP1DIDL9U5Rl1g0lJhdZC3UXlGrQw0wDXKAGfMmgv6N6epCnNjsO51qvWsPnguDbCVKg1UvqBs9feOzQ_Ztxt_0lSZecKwvd6gEqqQIMu_zEPSfnmpt3QSykIYa44ZQf9W-AzasVHJ_zfrzUP9xxiZG_Y1__qJWeCHHaRw9OJ--Gwqfv91xfGSFmnc6tGp3dcwjJaRTPZ1dJJqNUdwucLlcrDKxpyMl5vhUf_7cDn-l_rhi5QPdIMbRT8uQbVDIcmfMTxuUH-y5-HuydWh2yYB0a4YCaHUtZNNIsNZQCDDsewp5JgvxCyDFC77QTYtdnrmqFXTzTQxeLVbraeiA6JTLQiDqn9_jyt7YpNq-MtvShXwMjm-Hr-JMmhUwQW5kV-HYy9KxlTSmaElRxA85xZveLdAAszeyvR193kr43TP3ACccE48JwBmQ_1fpgwHmZ271OjtkiOnk0ovW6NuOejoA4f9-omCgYmh02wyr2FdYUzXrwFmlHK0cIX1eNgJw3EJDdZ6O6qW1Vgq7sC4bAGreN10Bk6SrRrKNwS8l8JD51VYbmCPGdF5nYWR63jYAwlyziDrysCI_RCP-GwMaWX8DgT0-oSWVxkFOMQdEkmuz79oj1Skknk0jNTez_my0) -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +## Project Structure -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +``` +src/main/java/org/etsi/osl/controllers/ietf/ns/ +├── IETFNSGCSpringBoot.java (Application entry point) +├── api/ +│ ├── SloSleTemplateBootstrapService (Registration & example creation) +│ ├── ResourceRepoService (Message handler & provisioning) +│ ├── PartnerRouteBuilder (Camel EIP routes) +│ ├── config/ +│ │ ├── CatalogClient (TMF Catalog API) +│ │ ├── ActiveMQComponentConfig (Message broker setup) +│ │ └── RestconfConfig (RESTCONF client) +│ ├── restconf/ +│ │ ├── RestconfClient (RESTCONF interface) +│ │ ├── RestconfClientImpl (HTTP implementation) +│ │ ├── RestconfConsumerService (Business logic) +│ │ └── RestconfException (Error handling) +│ └── domain/model/ +│ ├── SliceService.java (RFC 9543 slice service) +│ ├── SloSleTemplate.java (SLO/SLE policies) +│ ├── Rfc9543SliceServiceDeserializer (JSON field mapping) +│ └── ... (connection groups, SDPs, metrics) +└── repository/ + ├── SloSleTemplateRepository (Template persistence) + └── impl/InMemorySloSleTemplateRepository (In-memory storage) +``` -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +## Getting Started -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +### Prerequisites -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +- Java 17+ +- Maven 3.6+ +- ActiveMQ/Artemis running on `tcp://localhost:61616` (default: artemis/artemis) +- OpenSlice OSOM with TMF Catalog API +- RFC 9543 Network Slice Service Provider (RESTCONF endpoint) -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +### Build and Run -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +```bash +# Clean build and run tests +mvn clean test + +# Build the application +mvn clean package + +# Run locally +mvn spring-boot:run + +# Run packaged JAR +java -jar target/org.etsi.osl.controllers.ietf.ns-0.0.1-SNAPSHOT.jar +``` + +### Docker Deployment + +```bash +# Build Docker image +docker build -t ns-ietf-controller:latest . + +# Run container +docker run -p 8080:8080 \ + -e SPRING_ACTIVEMQ_BROKERURL=tcp://activemq:61616 \ + -e SPRING_APPLICATION_NAME=ns-ietf-controller \ + ns-ietf-controller:latest +``` -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +## Configuration + +All runtime settings are configured in `application.yml`: + +- **Server port:** 0 (OS assigns available port, see startup logs - in general it is not used) +- **ActiveMQ broker URL:** `tcp://localhost:61616` +- **Queue names:** 21 configured endpoints (see `application.yml` for full list) +- **RESTCONF provider URL:** Configure in `RestconfConfig` +- **OAuth signing key:** For resource update signatures + +## Testing + + +### Unit Tests + +Comprehensive unit tests validate: +- RFC 9543 JSON deserialization +- Field name mapping (hyphenated → camelCase) +- SLO/SLE template parsing +- Connection group configuration +- SDP IP address handling + +Run tests: +```bash +mvn clean test +``` + +## Documentation + +Key documentation and diagrams in `doc/NSC/`: + +**Architecture Diagrams:** +- **bootstrap_phase.puml** - Bootstrap sequence showing template retrieval and registration +- **resource_creation_lifecycle.puml** - Resource creation workflow from OSOM to RESTCONF provisioning + +**Implementation Guides:** +- **RESTCONF_CONSUMER_GUIDE.md** - RFC 9543 overview and RESTCONF operations +- **RESTCONF_IMPLEMENTATION.md** - Implementation patterns with examples +- **SLO_SLE_TEMPLATE_GUIDE.md** - Complete API reference +- **SLO_SLE_REFACTORING_SUMMARY.md** - Refactoring details + +**Test Resources:** +- **examples/** - JSON test requests and payloads +- **draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt** - RFC 9543 specification + +## Technology Stack + +- **Framework:** Spring Boot 3.2.2 +- **Messaging:** Apache Camel 4.0.0-RC2, Apache ActiveMQ (JMS) +- **RESTCONF:** Custom implementation per RFC 9543 +- **Object Mapping:** MapStruct 1.5.3.Final +- **JSON Processing:** Jackson 2.8.11 with custom deserializers +- **Security:** Spring Security, OAuth2, Keycloak 22.0.1 +- **Utilities:** Lombok 1.18.28, Guava 32.0.0 ## License -For open source projects, say how it is licensed. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +This project is part of the OpenSlice OSL ETSI SDG, licensed under Apache 2.0 + diff --git a/doc/NSC/draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt b/doc/NSC/draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt new file mode 100644 index 0000000..dc78cd4 --- /dev/null +++ b/doc/NSC/draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt @@ -0,0 +1,7056 @@ + + + + +TEAS B. Wu +Internet-Draft D. Dhody +Intended status: Standards Track Huawei Technologies +Expires: 10 November 2025 R. Rokui + Ciena + T. Saad + J. Mullooly + Cisco Systems, Inc + 9 May 2025 + + + A YANG Data Model for the RFC 9543 Network Slice Service + draft-ietf-teas-ietf-network-slice-nbi-yang-25 + +Abstract + + This document defines a YANG data model for RFC 9543 Network Slice + Service. The model can be used in the Network Slice Service + interface between a customer and a provider that offers RFC 9543 + Network Slice Services. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 10 November 2025. + +Copyright Notice + + Copyright (c) 2025 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + + + +Wu, et al. Expires 10 November 2025 [Page 1] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 + 2. Conventions and Definitions . . . . . . . . . . . . . . . . . 4 + 2.1. Acronyms . . . . . . . . . . . . . . . . . . . . . . . . 5 + 3. Network Slice Service Overview . . . . . . . . . . . . . . . 5 + 4. Network Slice Service Model (NSSM) Usage . . . . . . . . . . 7 + 5. Network Slice Service Model (NSSM) Description . . . . . . . 8 + 5.1. SLO and SLE Templates . . . . . . . . . . . . . . . . . . 10 + 5.2. Network Slice Services . . . . . . . . . . . . . . . . . 12 + 5.2.1. Service Demarcation Points . . . . . . . . . . . . . 13 + 5.2.2. Connectivity Constructs . . . . . . . . . . . . . . . 19 + 5.2.3. SLO and SLE Policy . . . . . . . . . . . . . . . . . 21 + 5.2.4. Network Slice Service Performance Monitoring . . . . 24 + 5.2.5. Custom Topology Constraints . . . . . . . . . . . . . 25 + 5.2.6. Network Slice Service Feasibility Check . . . . . . . 26 + 6. Network Slice Service Module . . . . . . . . . . . . . . . . 27 + 7. Security Considerations . . . . . . . . . . . . . . . . . . . 57 + 8. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 59 + 9. Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . 59 + 10. Contributors . . . . . . . . . . . . . . . . . . . . . . . . 60 + 11. References . . . . . . . . . . . . . . . . . . . . . . . . . 60 + 11.1. Normative References . . . . . . . . . . . . . . . . . . 60 + 11.2. Informative References . . . . . . . . . . . . . . . . . 63 + 11.3. References . . . . . . . . . . . . . . . . . . . . . . . 64 + Appendix A. Augmentation Considerations . . . . . . . . . . . . 64 + Appendix B. Examples of Network Slice Services . . . . . . . . . 66 + B.1. Example-1: Two A2A Slice Services with Different Match + Approaches . . . . . . . . . . . . . . . . . . . . . . . 66 + B.2. Example-2: Two P2P Slice Services with Different Match + Approaches . . . . . . . . . . . . . . . . . . . . . . . 73 + B.3. Example-3: A Hub and Spoke Slice Service with a P2MP + Connectivity Construct . . . . . . . . . . . . . . . . . 85 + B.4. Example-4: An A2A Slice Service with Multiple SLOs and DSCP + Matching . . . . . . . . . . . . . . . . . . . . . . . . 92 + B.5. Example-5: An A2A Network Slice Service with SLO Precedence + Policies . . . . . . . . . . . . . . . . . . . . . . . . 99 + B.6. Example-6: SDP at CE, L3 A2A Slice Service . . . . . . . 106 + B.7. Example-7: SDP at CE, L3 A2A Slice Service with Network + Abstraction . . . . . . . . . . . . . . . . . . . . . . . 112 + Appendix C. Complete Model Tree Structure . . . . . . . . . . . 117 + Appendix D. Comparison with the Design Choice of ACTN VN Model + Augmentation . . . . . . . . . . . . . . . . . . . . . . 125 + Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 126 + + + +Wu, et al. Expires 10 November 2025 [Page 2] + +Internet-Draft Network Slice Service YANG Model May 2025 + + +1. Introduction + + [RFC9543] outlines a framework and an interface for Network Slice + using IETF technologies, and it introduces the term "IETF Network + Slice Service", referred to as RFC 9543 Network Slice Service. This + document uses the term "Network Slice Service" to refer to this + concept for brevity and consistency. + + This document defines a YANG [RFC7950] data model for [RFC9543] + Network Slice Service. The Network Slice Service Model (NSSM) can be + used in the Network Slice Service Interface exposed by a provider to + its customers (including for provider's internal use) in order to + manage (e.g., subscribe, delete, or change) Network Slice Services. + The agreed service will then trigger the appropriate Network Slice + operation, such as instantiating, modifying, or deleting a Network + Slice. + + The NSSM focuses on the requirements of a Network Slice Service from + the point of view of the customer, not how it is implemented within a + provider network. As discussed in [RFC9543], the mapping between a + Network Slice Service and its realization is implementation and + deployment specific. + + The NSSM is classified as a customer service model (Section 2 of + [RFC8309]). + + The NSSM conforms to the Network Management Datastore Architecture + (NMDA) [RFC8342]. + + Editorial Note: (To be removed by RFC Editor) + + This document contains several placeholder values that need to be + replaced with finalized values at the time of publication. Please + apply the following replacements: + + * AAAA --> the assigned RFC value for this draft both in this draft + and in the YANG models under the revision statement + + * BBBB --> the assigned RFC value for + [I-D.ietf-opsawg-teas-common-ac] + + * CCCC --> the assigned RFC value for + [I-D.ietf-opsawg-teas-attachment-circuit] + + * DDDD --> the assigned RFC value for [I-D.ietf-teas-rfc8776-update] + + * The "revision" date in model, in the format XXXX-XX-XX, needs to + be updated with the date the draft gets approved + + + +Wu, et al. Expires 10 November 2025 [Page 3] + +Internet-Draft Network Slice Service YANG Model May 2025 + + +2. Conventions and Definitions + + The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in + BCP14, [RFC2119], [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + The following terms are defined in [RFC6241] and are used in this + specification: + + * client + + * configuration data + + * state data + + This document makes use of the terms defined in [RFC7950]. + + The tree diagrams used in this document follow the notation defined + in [RFC8340]. + + This document also makes use of the terms defined in [RFC9543]: + + * Customer: See Section 3.2 of [RFC9543]. + + * Customer Higher-level Operation System: See Section 6.3.1 of + [RFC9543]. + + In addition, this document defines the following term: + + * Connection Group: Refers to one or more Connectivity Constructs + that are grouped for administrative purposes, such as the + following: + + - Combine multiple Connectivity Constructs to support a set of + well-known connectivity service types, such as bidirectional + unicast service, multipoint-to-point (MP2P) service, or hub- + and-spoke service. + + - Assign the same Service Level Objectives (SLOs)/Service Level + Expectations (SLEs) policies to multiple Connectivity + Constructs unless the SLOs/SLEs policy is explicitly overridden + at the individual Connectivity Construct level. + + - Share specific SLO limits within multiple Connectivity + Constructs. + + + + +Wu, et al. Expires 10 November 2025 [Page 4] + +Internet-Draft Network Slice Service YANG Model May 2025 + + +2.1. Acronyms + + The following acronyms are used in the document: + + A2A Any-to-any + AC Attachment Circuit, as defined in Section 3.2 of [RFC9543] + CC Connectivity Construct: See Sections 3.2 and 4.2.1 of + [RFC9543] + CE Customer Edge, see Section 3.2 of [RFC9543] + MTU Maximum Transmission Unit + NSC Network Slice Controller, defined in Section 6.3 of [RFC9543] + NSS Network Slice Service, defined in Section 4.2 of [RFC9543] + NSSM Network Slice Service Model, defined in this document + P2P Point-to-point + P2MP Point-to-multipoint + PE Provider Edge, see Section 3.2 of [RFC9543] + QoS Quality of Service + SDP Service Demarcation Point, defined in Sections 3.2 and 5.2 + [RFC9543] + SLE Service Level Expectation, defined in Section 5.1.2 of + [RFC9543] + SLO Service Level Objective, defined in Section 5.1.1 of + [RFC9543] + + +3. Network Slice Service Overview + + As defined in Section 3.2 of [RFC9543], a Network Slice Service is + specified in terms of a set of Service Demarcation Points (SDPs), a + set of one or more Connectivity Constructs between subsets of these + SDPs, and a set of Service Level Objectives (SLOs) and Service Level + Expectations (SLEs) for each SDP sending to each Connectivity + Construct. A communication type (point-to-point (P2P), point-to- + multipoint (P2MP), or any-to-any (A2A)) is specified for each + Connectivity Construct. + + The SDPs serve as the Network Slice Service ingress/egress points. + An SDP is identified by a unique identifier in the context of a + Network Slice Service. + + Examples of Network Slice Services that contain only one Connectivity + Construct are shown in Figure 1. + + + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 5] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + +----------------------------------------------+ + | | + | | + | Slice Service 1 with 1 P2P CC | + SDP1 O------------------->--------------------------O SDP2 + | | + | | + | Slice Service 2 with 1 P2MP CC + | +---------------------------O SDP4 + SDP3 O----------->------+ | + | +---------------------------O SDP5 + | | + | | + | Slice Service 3 with 1 A2A CC + SDP6 O-----------<>-----+---------<>----------------O SDP8 + | | | + SDP7 O-----------<>-----+---------<>----------------O SDP9 + | | + | | + +----------------------------------------------+ + |<------------Network Slice Services---------->| + | between endpoints SDP1 to SDP9 | + + CC: Connectivity Construct + O: Represents an SDP + ----: Represents Connectivity Construct + < > : Inbound/outbound directions + + Figure 1: Examples of Network Slice Services of Single + Connectivity Construct + + An example of Network Slice Services that contains multiple + Connectivity Constructs is shown in Figure 2. + + + + + + + + + + + + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 6] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + +----------------------------------------------+ + | | + | Slice Service 4 with 2 P2P CCs | + SDP10 O------------------->--------------------------O SDP12 + SDP11 O------------------->--------------------------O SDP13 + | | + | | + | Slice Service 5 with 2 P2P CCs | + | +----------------->-----------------------+ | + SDP14 O/ \ O SDP15 + |\ / | + | +-----------------<-----------------------+ | + | | + +----------------------------------------------+ + |<-----------Network Slice Services----------->| + | between endpoints SDP10 to SDP15 | + + + Slice Service: Network Slice Service + CC: Connectivity Construct + O: Represents an SDP + ----: Represents Connectivity Construct + < > : Inbound/outbound directions + + Figure 2: Examples of Network Slice Services of Multiple Connectivity + Constructs + + As shown in Figure 2, the Network Slice Service 4 contains two P2P + Connectivity Constructs between the set of SDPs. The Network Slice + Service 5 is a bidirectional unicast service between SDP14 and SDP15 + that consists of two unidirectional P2P Connectivity Constructs. + +4. Network Slice Service Model (NSSM) Usage + + The NSSM can be used by a provider to expose its Network Slice + Services, and by a customer to manage its Network Slices Services + (e.g., request, delete, or modify). The details about how service + requests are handled by a provider (specifically, a controller), + including which network operations are triggered, are internal to the + provider. The details of the Network Slices realization are hidden + from customers. + + The Network Slices are applicable to use cases, such as (but not + limited to) 5G, network wholesale services, network infrastructure + sharing among operators, Network Function Virtualization (NFV) + connectivity, and Data Center interconnect. + [I-D.ietf-teas-ietf-network-slice-use-cases] provides a more detailed + description of the use cases for Network Slices. + + + +Wu, et al. Expires 10 November 2025 [Page 7] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + A Network Slice Controller (NSC) is an entity that exposes the + Network Slice Service Interface to customers to manage Network Slice + Services. Typically, an NSC receives requests from its customer- + facing interface (e.g., from a management system). During service + creation, this interface can convey data objects that the Network + Slice Service customer provides, describing the needed Network Slices + Service in terms of SDPs, the associated Connectivity Constructs, and + the service objectives that the customer wishes to be fulfilled. + Depending on whether the requirements and authorization checks are + successfully met, these service requirements are then translated into + technology-specific actions that are implemented in the underlying + network(s) using a network-facing interface. The details of how the + Network Slices are put into effect are out of scope for this + document. + + As shown in Figure 3, the NSSM is used by the Customer Higher-level + Operation System to communicate with an NSC for life cycle management + of Network Slice Services including both enablement and monitoring. + For example, in the 5G End-to-end network slicing use case, the 5G + network slice orchestrator acts as the higher layer system to manage + the Network Slice Services. The interface is used to support Network + Slice management to facilitate end-to-end 5G network slice services. + + +----------------------------------------+ + | Network Slice Customer | + | (e.g., 5G network slice orchestrator)| + +----------------+-----------------------+ + | + | + | Network Slice Service Model (NSSM) + | + +---------------------+--------------------------+ + | Network Slice Controller (NSC) | + +------------------------------------------------+ + + Figure 3: Network Slice Service Reference Architecture + + Note: The NSSM can be used recursively (hierarchical mode), i.e., an + NSS can map to child NSSes. As described in Section A.5 of + [RFC9543], the Network Slice Service can support a recursive + composite architecture that allows one layer of Network Slice + Services to be used by other layers. + +5. Network Slice Service Model (NSSM) Description + + The NSSM, "ietf-network-slice-service", includes two main data nodes: + "slo-sle-templates" and "slice-service" ( Figure 4). + + + + +Wu, et al. Expires 10 November 2025 [Page 8] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + module: ietf-network-slice-service + +--rw network-slice-services + +--rw slo-sle-templates + | +--rw slo-sle-template* [id] + | ... + +--rw slice-service* [id] + +--rw id string + +--rw description? string + +--rw service-tags + | ... + +--rw (slo-sle-policy)? + | ... + +--rw test-only? empty + +--rw status + | ... + +--rw sdps + | ... + +--rw connection-groups + | ... + +--rw custom-topology + ... + + Figure 4: The NSSM Overall Tree Structure + + The "slo-sle-templates" container is used by an NSC to maintain a set + of common Network Slice SLO and SLE templates that apply to one or + several Network Slice Services. Refer to Section 5.1 for further + details on the properties of NSS templates. + + The "slice-service" list includes the set of Network Slice Services + that are maintained by a provider for a given customer. "slice- + service" is the data structure that abstracts the Network Slice + Service. Under the "slice-service", the "sdp" list is used to + abstract the SDPs. The "connection-group" is used to abstract + Connectivity Constructs between SDPs. Refer to Section 5.2 for + further details on the properties of an NSS. + + To ensure scalability of the Network Slice Service as the number of + slices increases, "slo-sle-templates" can be utilized to reuse + existing SLO/SLE policies. And the SDPs and connection constructs + can be incrementally updated to minimize the overhead associated with + frequent modifications. + + + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 9] + +Internet-Draft Network Slice Service YANG Model May 2025 + + +5.1. SLO and SLE Templates + + The "slo-sle-templates" container (Figure 5) is used by a Network + Slice Service provider to define and maintain a set of common Network + Slice Service templates that apply to one or several Network Slice + Services. The templates are assumed to be known to both the + customers and the provider. The exact definition of the templates is + deployment specific. + + +--rw slo-sle-templates + | +--rw slo-sle-template* [id] + | +--rw id string + | +--rw description? string + | +--rw template-ref? slice-template-ref + | +--rw slo-policy + | | +--rw metric-bound* [metric-type] + | | | +--rw metric-type identityref + | | | +--rw metric-unit string + | | | +--rw value-description? string + | | | +--rw percentile-value? percentile + | | | +--rw bound? uint64 + | | +--rw availability? identityref + | | +--rw mtu? uint32 + | +--rw sle-policy + | +--rw security* identityref + | +--rw isolation* identityref + | +--rw max-occupancy-level? uint8 + | +--rw path-constraints + | +--rw service-functions + | +--rw diversity + | +--rw diversity-type? + | te-types:te-path-disjointness + + Figure 5: SLO SLE Templates Subtree Structure + + The NSSM provides the SLO and SLE templates identifiers and + templates, and the common attributes of the templates are defined in + Section 5.1 of [RFC9543]. Standard templates provided by the + provider as well as custom "service-slo-sle-policy" are defined, + since there are many attributes defined and some attributes could + vary with service requirements, e.g., bandwidth or latency. A + customer may choose either a standard template provided by the + provider or a customized "service-slo-sle-policy". + + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 10] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + 1. Standard template: The exact definition of the templates is + deployment specific. The attributes configuration of a standard + template is optional. When specifying attributes, a standard + template can use "template-ref" to inherit some attributes of a + predefined standard template and override the specific + attributes. + + 2. Custom "service-slo-sle-policy": More description is provided in + Section 5.2.3. + + Figure 6 shows an example where two standard network slice templates + are retrieved by the customers. + + ========== NOTE: '\' line wrapping per RFC 8792 =========== + + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "PLATINUM-template", + "description": "Two-way bandwidth: 1 Gbps,\ + 95th percentile latency 50ms", + "slo-policy": { + "metric-bound": [ + { + "metric-type": "two-way-bandwidth", + "metric-unit": "Gbps", + "bound": "1" + }, + { + "metric-type": "two-way-delay-percentile", + "metric-unit": "milliseconds", + "percentile-value": "95.000", + "bound": "50" + } + ] + }, + "sle-policy": { + "isolation": ["traffic-isolation"] + } + }, + { + "id": "GOLD-template", + "description": "Two-way bandwidth: 1 Gbps,\ + maximum latency 100ms", + "slo-policy": { + "metric-bound": [ + + + +Wu, et al. Expires 10 November 2025 [Page 11] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + { + "metric-type": "two-way-bandwidth", + "metric-unit": "Gbps", + "bound": "1" + }, + { + "metric-type": "two-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": "100" + } + ] + }, + "sle-policy": { + "isolation": ["traffic-isolation"] + } + } + ] + } + } + } + + Figure 6: Example of Template Retrieval + + Figure 6 uses folding as defined in [RFC8792] for long lines. + +5.2. Network Slice Services + + The "slice-service" (Figure 7) is the data structure that abstracts a + Network Slice Service. Each "slice-service" is uniquely identified + within an NSC by "id". + + +--rw slice-service* [id] + +--rw id string + +--rw description? string + +--rw service-tags + | ... + +--rw (slo-sle-policy)? + | ... + +--rw test-only? empty + +--rw status + | ... + +--rw sdps + | ... + +--rw connection-groups + | ... + +--rw custom-topology + ... + + + + +Wu, et al. Expires 10 November 2025 [Page 12] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + Figure 7: Network Slice Service Subtree Structure + + A Network Slice Service has the following main data nodes: + + * "description": Provides a textual description of a Network Slice + Service. + + * "service-tags": Indicates a management tag (e.g., "customer name" + ) that is used to correlate the operational information of + Customer Higher-level Operation System and Network Slices. It + might be used by a Network Slice Service provider to provide + additional information to an NSC during the operation of the + Network Slices. For example, adding tags with "customer name" + when multiple actual customers use the same Network Slice Service. + Another use case for "service-tag" might be for a provider to + provide additional attributes to an NSC which might be used during + the realization of Network Slice Services such as type of services + (e.g., use Layer 2 or Layer 3 technology for the realization). + These additional attributes can also be used by an NSC for various + purposes such as monitoring and assurance of the Network Slice + Services where the NSC can issue notifications to the customer + system. All these attributes are optional. + + * "slo-sle-policy": Defines SLO and SLE policies for the "slice- + service". More details are provided in Section 5.2.3. + + * "test-only": Is used to check the feasibility of the service + before instantiating a Network Slice Service in a network. More + details are provided in Section 5.2.6. + + * "status": Indicates both the operational and administrative status + of a Network Slice Service. Mismatches between the admin/oper + status can be used as an indicator to detect Network Slice Service + anomalies. + + * "sdps": Represents a set of SDPs that are involved in the Network + Slice Service. More details are provided in Section 5.2.1. + + * "connection-groups": Abstracts the connections to the set of SDPs + of the Network Slice Service. + + * "custom-topology": Represents custom topology constraints for the + Network Slice Service. More details are provided in Section 5.2.5 + +5.2.1. Service Demarcation Points + + A Network Slice Service involves two or more SDPs. A Network Slice + Service can be modified by adding new "sdp"s. + + + +Wu, et al. Expires 10 November 2025 [Page 13] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + +--rw sdps + +--rw sdp* [id] + +--rw id string + +--rw description? string + +--rw geo-location + | ... + +--rw node-id? string + +--rw sdp-ip-address* inet:ip-address + +--rw tp-ref? leafref + +--rw service-match-criteria + | ... + +--rw incoming-qos-policy + | ... + +--rw outgoing-qos-policy + | ... + +--rw sdp-peering + | ... + +--rw ac-svc-ref* + | ac-svc:attachment-circuit-reference + +--rw ce-mode? boolean + +--rw attachment-circuits + | ... + +--rw status + | ... + +--ro sdp-monitoring + ... + + Figure 8: SDP Subtree Structure + + Section 5.2 of [RFC9543] describes four possible ways in which an SDP + may be placed: + + * Within a CE + + * Provider-facing ports on a CE + + * Customer-facing ports on a PE + + * Within a PE + + Although there are four options, they can be categorized into two + categories: CE-based or PE-based. + + In the four options, the Attachment Circuit (AC) may be part of the + Network Slice Service or may be external to it. Based on the AC + definition in Section 5.2 of [RFC9543], the customer and provider may + agree on a per {Network Slice Service, Connectivity Construct, and + SLOs/SLEs} basis to police or shape traffic on the AC in both the + + + +Wu, et al. Expires 10 November 2025 [Page 14] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + ingress (CE to PE) direction and egress (PE to CE) direction, which + ensures that the traffic is within the capacity profile that is + agreed in a Network Slice Service. Excess traffic is dropped by + default, unless specific out-of-profile policies are agreed between + the customer and the provider. + + To abstract the SDP options and SLOs/SLEs profiles, an SDP has the + following characteristics: + + * "id": Uniquely identifies the SDP within an NSC. The identifier + is a string that allows any encoding for the local administration + of the Network Slice Service. + + * "geo-location": Indicates SDP location information, which helps + the NSC to identify an SDP. + + * "node-id": A reference to the node that hosts the SDP, which helps + the NSC to identify an SDP. This document assumes that the + Customer Higher-level Operation System can obtain the node + information, PE and CE, prior to the service requests. For + example, Service Attachment Points (SAPs) [RFC9408] can obtain PE- + related node information. The implementation details are left to + the NSC provider. + + * "sdp-ip-address": The SDP IP addresses, which help the NSC to + identify an SDP. + + * "tp-ref": A reference to a Termination Point (TP) in the custom + topology defined in Section 5.2.5. + + * "service-match-criteria": Defines matching policies for the + Network Slice Service traffic to apply on a given SDP. + + * "incoming-qos-policy" and "outgoing-qos-policy": Sets the incoming + and outgoing QoS policies to apply on a given SDP, including QoS + policy and specific ingress and egress traffic limits to ensure + access security. When applied in the incoming direction, the + policy is applicable to the traffic that passes through the AC + from the customer network or from another provider's network to + the Network Slice. When applied in the outgoing direction, the + policy is applied to the traffic from the Network Slice towards + the customer network or towards another provider's network. If an + SDP has multiple ACs, the "rate-limits" of "attachment-circuit" + can be set to an AC specific value, but the rate cannot exceed the + "rate-limits" of the SDP. If an SDP only contains a single AC, + then the "rate-limits" of "attachment-circuit" is the same with + the SDP. The definition of AC refers to Section 5.2 [RFC9543]. + + + + +Wu, et al. Expires 10 November 2025 [Page 15] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + * "sdp-peering": Specifies the peers and peering protocols for an + SDP to exchange control-plane information, e.g., Layer 1 signaling + protocol or Layer 3 routing protocols, etc. As shown in Figure 9 + + +--rw sdp-peering + | +--rw peer-sap-id* string + | +--rw protocols + + Figure 9: SDP Peering Subtree Structure + + - "peer-sap-id": Indicates the references to the remote endpoints + of attachment circuits. This information can be used for + correlation purposes, such as identifying Service Attachment + Points (SAPs) defined in [RFC9408], which defines a model of an + abstract view of the provider network topology that contains + the points from which the services can be attached. + + - "protocols": Serves as an augmentation target. Appendix A + shows an example where BGP and static routing are augmented to + the model. + + * "ac-svc-ref": Refers to the ACs that have been created, which is + defined in Section 5.2 of + [I-D.ietf-opsawg-teas-attachment-circuit]. When both "ac-svc-ref" + and the attributes of "attachment-circuits" are defined, the "ac- + svc-ref" may take precedence or act as the parent AC depending on + the use cases. + + * "ce-mode": A flag node that marks the SDP is located on the CE. + + * "attachment-circuits": Specifies the list of ACs by which the + service traffic is received. This is an optional SDP attribute. + When an SDP has multiple ACs and some AC specific attributes are + needed, each "attachment-circuit" can specify attributes, such as + interface specific IP addresses, service MTU, and other + attributes. + + * "status": Enables the control of the administrative status and + reporting of the operational status of the SDP. These status + values can be used as indicators to detect SDP anomalies. + + * "sdp-monitoring": Provides SDP bandwidth statistics. + + Depending on the requirements of different cases, "service-match- + criteria" can be used for the following purposes: + + * Specify the AC type: physical or logical connection. + + + + +Wu, et al. Expires 10 November 2025 [Page 16] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + * Distinguish the SDP traffic if the SDP is located in the CE or PE. + + * Distinguish the traffic of different Connection Groups (CGs) or + Connectivity Constructs (CCs) when multiple CGs/CCs of different + SLO/SLE may be set up between the same pair of SDPs, as + illustrated in Figure 10. Traffic needs to be explicitly mapped + into the Network Slice's specific Connectivity Construct. The + policies, "service-match-criteria", are based on the values in + which combination of Layer 2 and Layer 3 header and payload fields + within a packet to identify to which {Network Slice Service, + Connectivity Construct, and SLOs/SLEs} that packet is assigned. + For example, VLAN ([IEEE802.1Q]), C-VLAN/S-VLAN ([IEEE802.1ad]), + or IP addresses. + + * Define specific out-of-profile policies: The customer may choose + to use an explicit "service-match-criteria" to map any SDP's + traffic or a subset of the SDP's traffic to a specific Connection + Group or Connectivity Construct. If a subset of traffic is + matched (e.g., "dscp" and/or IP addresses) and mapped to a + Connectivity Construct, the customer may choose to add a + subsequent "match-any" to explicitly map the remaining SDP traffic + to a separate Connectivity Construct. If the customer chooses to + implicitly map remaining traffic and if there are no additional + Connectivity Constructs where the "sdp/id" source is specified, + then that traffic will be dropped. + + | | + | Slice Service 6 with 2 P2P CCs | + | +--x-x-x-x-x-x---->---x-x-x-x-x-x-x-x-x---+ | + SDP16 o / \ o SDP17 + | \ / | + | +--%-%-%-%-%-%---->---%-%-%-%-%-%-%-%-%---+ | + | | + +-----------------------------------------------+ + |<---------- Network Slice Services ----------->| + | between endpoints SDP16 to SDP17 | + + - "x": Match DSCP + - "%": Match destination-ip-prefix + + Figure 10: Application of Match Criteria + + If an SDP is placed at the port of a CE or PE, and there is only one + single Connectivity Construct with a source at the SDP, traffic can + be implicitly mapped to this Connectivity Construct since the AC + information (e.g., VLAN tag) can be used to unambiguously identify + the traffic and the SDP is the only source of the connectivity- + construct. Appendix B.1 shows an example of both the implicit and + + + +Wu, et al. Expires 10 November 2025 [Page 17] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + explicit approaches. While explicit matching is optional in some use + cases, it provides a more clear and readable implementation, but the + choice is left to the operator. + + Figure 11 and Figure 12 provide examples that illustrate the use of + SDP options. How an NSC realizes the mapping is out of scope for + this document. + +* SDPs at customer-facing ports on the PEs: As shown in Figure 11, a + customer of the Network Slice Service would like to connect two + SDPs to satisfy specific service needs, e.g., network wholesale + services. In this case, the Network Slice SDPs are mapped to + customer-facing ports of PE nodes. The NSC uses "node-id" (PE + device ID), "attachment-circuits", or "ac-svc-ref" to map SDPs to + the customer-facing ports on the PEs. + + SDP1 SDP2 + (With PE1 parameters) (with PE2 parameters) + o<--------- Network Slice (NS) 1 -------->o + + | | + + + |<----------- S1 ----------->| + + + | | + + + | |<------ T1 ------>| | + + + v v v v + + + +----+ +----+ + + +-----+ | | PE1|==================| PE2| +-----+ + | |----------X | | | | | | + | | | | | | X----------| | + | |----------X | | | | | | + +-----+ | | |==================| | | +-----+ + AC +----+ +----+ AC + Customer Provider Provider Customer + Edge 1 Edge 1 Edge 2 Edge 2 + + + Legend: + o: Representation of an SDP + +: Mapping of an SDP to customer-facing ports on the PE + X: Physical interfaces used for realization of the NS Service + S1: L0/L1/L2/L3 services used for realization of NS Service + T1: Tunnels used for realization of NS Service + + Figure 11: An Example of SDPs Placing at PEs + +* SDPs within CEs: As shown in Figure 12, a customer of the Network + Slice Service would like to connect two SDPs to provide + connectivity between transport portion of 5G RAN to 5G Core + network functions. In this scenario, the NSC uses "node-id" (CE + + + +Wu, et al. Expires 10 November 2025 [Page 18] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + device ID), "geo-location", "sdp-ip-address" (IP address of SDP + for management), "service-match-criteria" (VLAN tag), "attachment- + circuits" or "ac-svc-ref" (CE ACs) to map SDPs to the CE. The NSC + can use these CE parameters (and optionally other information to + uniquely identify a CE within an NSC, such as "peer-sap-id" + [RFC9408]) to retrieve the corresponding PE device, interface and + AC mapping details to complete the Network Slice Service + provisioning. + + SDP3 SDP4 + (With CE1 parameters) (with CE2 parameters) + +o<--------------- Network Slice (NS) 2 --------------->o + + + + +|<------------------------- S2 ---------------------->|+ + +| |+ + +| |<------ T2 ------>| |+ + +| v v |+ + +v +----+ +----+ v+ + +--+--+ | | PE1|==================| PE2| | +-+---+ + | + X----------X | | | | | + | + | o | | | | | X----------X o | + | X----------X | | | | | | + +-----+ | | |==================| | | +-----+ + AC +----+ +----+ AC + Customer Provider Provider Customer + Edge 1 Edge 1 Edge 2 Edge 2 + + +Legend: + o: Representation of an SDP + +: Mapping of an SDP to CE + X: Physical interfaces used for realization of the NS Service +S2: L0/L1/L2/L3 services used for realization of the NS Service +T2: Tunnels used for realization of NS Service + + Figure 12: An Example of SDPs Placing at CEs + +5.2.2. Connectivity Constructs + + Section 4.2.1 of [RFC9543] defines the basic Connectivity Construct + (CC) and CC types of a Network Slice Service, including P2P, P2MP, + and A2A. + + A Network Slice Service involves one or more Connectivity Constructs. + The "connection-groups" container is used to abstract CC, CC groups, + and their SLOs/SLEs policies and the structure is shown in Figure 13. + + + + + +Wu, et al. Expires 10 November 2025 [Page 19] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + +--rw connection-groups + +--rw connection-group* [id] + +--rw id string + +--rw connectivity-type? + | identityref + +--rw (slo-sle-policy)? + | +--:(standard) + | | ... + | +--:(custom) + | ... + +--rw service-slo-sle-policy-override? + | identityref + +--rw connectivity-construct* [id] + | +--rw id + | | string + | +--rw (type)? + | | ... + | +--rw (slo-sle-policy)? + | | ... + | +--rw service-slo-sle-policy-override? + | | identityref + | +--rw status + | | ... + + Figure 13: Connection Groups Subtree Structure + + The description of the "connection-groups" data nodes is as follows: + + * "connection-group": Represents a group of CCs. In the case of hub + and spoke connectivity of the Slice Service, it may be inefficient + when there are a large number of SDPs with multiple CCs. As + illustrated in Appendix B.3, "connectivity-type" of "ietf-vpn- + common:hub-spoke" and "connection-group-sdp-role" of "ietf-vpn- + common:hub-role" or "ietf-vpn-common:spoke-role" can be specified + [RFC9181]. Another use is for optimizing "slo-sle-policy" + configurations, treating CCs with the same SLO and SLE + characteristics as a Connection Group such that the Connectivity + Construct can inherit the SLO/SLE from the group if not explicitly + defined. + + * "connectivity-type": Indicates the type of the Connection Group, + extending "vpn-common:vpn-topology" specified [RFC9181] with the + NS connectivity type, e.g., P2P or P2MP. + + * "connectivity-construct": Represents single Connectivity + Construct, and "slo-sle-policy" under it represents the per- + Connectivity Construct SLO and SLE requirements. + + + + +Wu, et al. Expires 10 November 2025 [Page 20] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + * "slo-sle-policy" and "service-slo-sle-policy-override": The + details of "slo-sle-policy" are defined in Section 5.2.3. + +5.2.3. SLO and SLE Policy + + As defined in Section 5 of [RFC9543], the SLO and SLE policy of the + Network Slice Services define some common attributes. + + "slo-sle-policy" is used to represent these SLO and SLE policies. + During the creation of a Network Slice Service, the policy can be + specified either by a standard SLO and SLE template or a customized + SLO and SLE policy. + + Two types of precedence rules are defined to resolve conflicts when + assigning policies to a Network Slice Service, Connection Group + "connection group", or Connectivity Construct "connectivity- + construct". + + Scope-based Precedence: In case of conflicts, policies with a + narrower scope (e.g., subset of a Network Slice Service) take + precedence over policies with a broader scope (e.g., Network Slice + Service). The precedence order is as follows (from highest to lowest + precedence): + + * Connectivity-construct at an individual sending SDP + + * Connectivity-construct + + * Connection-group + + * Slice-level + + For example, a policy assigned at the sending SDP level takes + precedence over a policy assigned at the connectivity-construct + level, which in turn takes precedence over a slice-level policy. + Appendix B.5 gives an example of the preceding policy, which shows a + Slice Service having an A2A connectivity as default and several + specific SLO connections. + + Explicit Precedence: "service-slo-sle-policy-override" node is + designed to enable the complete or partial replacement of an existing + "slo-sle-policy" with new values for complex SLO-SLE requirements. + For example, if a particular "connection-group" or a "connectivity- + construct" has a unique bandwidth or latency setting, that are + different from those defined in the Slice Service, a new set of SLOs/ + SLEs with full or partial override can be applied. + + + + + +Wu, et al. Expires 10 November 2025 [Page 21] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + In a partial override, only the newly specified parameters replace + those in the original template, while pre-existing unspecified + parameters remain unchanged. + + In a full override, all pre-existing parameters are removed, and a + new set of SLOs/SLEs is applied. + + The SLO attributes include performance metric attributes, + availability, and MTU. The SLO structure is shown in Figure 14. + Figure 27 shows an example "slice5" with a custom network slice "slo- + policy". + + +--rw slo-policy + | +--rw metric-bound* [metric-type] + | | +--rw metric-type + | | | identityref + | | +--rw metric-unit string + | | +--rw value-description? string + | | +--rw percentile-value? + | | | percentile + | | +--rw bound? uint64 + | +--rw availability? identityref + | +--rw mtu? uint16 + + Figure 14: SLO Policy Subtree Structure + + The list "metric-bound" supports the generic performance metric + variations and the combinations and each "metric-bound" could specify + a particular "metric-type". "metric-type" is defined with YANG + identity and supports the following options: + + "one-way-bandwidth": Indicates the guaranteed minimum bandwidth + between any two SDPs. The bandwidth is unidirectional. + + "two-way-bandwidth": Indicates the guaranteed minimum bandwidth + between any two SDPs. The bandwidth is bidirectional. + + "shared-bandwidth": Indicates the shared SLO bandwidth bound, + which is the limit on the bandwidth that can be shared among a + group of Connectivity Constructs of a Slice Service. + + "one-way-delay-maximum": Indicates the maximum one-way latency + between two SDPs, defined in [RFC7679]. + + "two-way-delay-maximum": Indicates the maximum round-trip latency + between two SDPs, defined in [RFC2681]. + + "one-way-delay-percentile": Indicates the percentile objective of + + + +Wu, et al. Expires 10 November 2025 [Page 22] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + the one-way latency between two SDPs ( [RFC7679]). + + "two-way-delay-percentile": Indicates the percentile objective of + the round-trip latency between two SDPs (See [RFC2681]). + + "one-way-delay-variation-maximum": Indicates the jitter constraint + of the slice maximum permissible delay variation, and is measured + by the difference in the one-way latency between sequential + packets in a flow, as defined in [RFC3393]. + + "two-way-delay-variation-maximum": Indicates the jitter constraint + of the slice maximum permissible delay variation, and is measured + by the difference in the two-way latency between sequential + packets in a flow, as defined in [RFC3393]. + + "one-way-delay-variation-percentile": Indicates the percentile + objective of the delay variation, and is measured by the + difference in the one-way latency between sequential packets in a + flow, as defined in [RFC3393]. + + "two-way-delay-variation-percentile": Indicates the percentile + objective of the delay variation, and is measured by the + difference in the two-way latency between sequential packets in a + flow, as defined in [RFC5481]. + + "one-way-packet-loss": Indicates maximum permissible packet loss + rate (See [RFC7680], which is defined by the ratio of packets + dropped to packets transmitted between two SDPs. + + "two-way-packet-loss": Indicates maximum permissible packet loss + rate (See [RFC7680], which is defined by the ratio of packets + dropped to packets transmitted between two SDPs. + + "availability": Specifies service availability defined as the ratio + of uptime to the sum of uptime and downtime, where uptime is the time + the Network Slice is available in accordance with the SLOs associated + with it. + + "mtu": Specifies the maximum length of Layer 2 data packets of the + Slice Service, in bytes. If the customer sends packets that are + longer than the requested service MTU, the network may discard them + (or for IPv4, fragment them). This service MTU takes precedence over + the MTUs of all ACs. The value needs to be smaller than or equal to + the minimum MTU value of all ACs in the SDPs. + + As shown in Figure 15, the following SLEs data nodes are defined. + + "security": The security leaf-list defines the list of security + + + +Wu, et al. Expires 10 November 2025 [Page 23] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + functions that the customer requests the operator to apply to + traffic between the two SDPs, including authentication, + encryption, etc., which is defined in Section 5.1.2.1 [RFC9543]. + + "isolation": Specifies the isolation types that a customer + expects, as defined in Section 8 of [RFC9543]. + + "max-occupancy-level": Specifies the number of flows that the + operator admits (See Section 5.1.2.1 of [RFC9543]). + + "path-constraints": Specifies the path constraints the customer + requests for the Network Slice Service, including geographic + restrictions and diversity which is defined in Section 5.1.2.1 of + [RFC9543]. + + +--rw sle-policy + +--rw security* identityref + +--rw isolation* identityref + +--rw max-occupancy-level? uint8 + +--rw path-constraints + +--rw service-functions + +--rw diversity + +--rw diversity-type? + te-types:te-path-disjointness + + Figure 15: SLE Policy Subtree Structure + +5.2.4. Network Slice Service Performance Monitoring + + The operation and performance status of Network Slice Services is + also a key component of the NSSM. The model provides SLO monitoring + information with the following granularity: + + * Per SDP: The incoming and outgoing bandwidths of an SDP are + specified in "sdp-monitoring" under the "sdp". + + * Per Connectivity Construct: The delay, delay variation, and packet + loss status are specified in "connectivity-construct-monitoring" + under the "connectivity-construct". + + * Per Connection Group: The delay, delay variation, and packet loss + status are specified in "connection-group-monitoring" under the + "connection-group". + + [RFC8639] and [RFC8641] define a subscription mechanism and a push + mechanism for YANG datastores. These mechanisms currently allow the + user to subscribe to notifications on a per-client basis and specify + either periodic or on-demand notifications. By specifying subtree + + + +Wu, et al. Expires 10 November 2025 [Page 24] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + filters or xpath filters to "sdp", "connectivity-construct", or + "connection-group", so that only interested contents will be sent. + The example in Figure 24 shows the way for a customer to subscribe to + the monitoring information for a particular Network Slice Service. + + Additionally, a customer can use the NSSM to obtain a snapshot of the + Network Slice Service performance status through [RFC8040] or + [RFC6241] interfaces. For example, retrieve the per-connectivity- + construct data by specifying "connectivity-construct" as the filter + in the RESTCONF GET request. + +5.2.5. Custom Topology Constraints + + A Slice Service customer might request for some level of control over + the topology or resource constraints. "custom-topology" is defined as + an augmentation target that references the context topology. The + leaf "network-ref" under this container is used to reference a + predefined topology as a customized topology constraint for a Network + Slice Service. Section 1 of [RFC8345] defines a general abstract + topology concept to accommodate both the provider's resource + capability and the customer's preferences. The abstract topology is + a topology that contains abstract topological elements (nodes, links, + and termination points). + + This document defines only the minimum attributes of a custom + topology, which can be extended based on the implementation + requirements. + + The following nodes are defined for the custom topology: + + "custom-topology": This container serves as an augmentation target + for the Slice Service topology context, which can be multiple. + This node is located directly under the "slice-service" list. + + "network-ref": This leaf is under the container "custom-topology", + which is defined to reference a predefined topology as a + customized topology constraint for a Network Slice Service, e.g., + a SAP topology to request SDP feasibility checks on SAPs network + described in Section 3 of [RFC9408], an abstract Traffic + Engineering (TE) topology defined in Section 3.13 of [RFC8795] to + customize the service paths in a Network Slice Service. + + "tp-ref": A reference to a Termination Point (TP) in the custom + topology, under the list "sdp", can be used to associate an SDP + with a TP of the customized topology. The example TPs could be + parent termination points of the SAP topology. + + + + + +Wu, et al. Expires 10 November 2025 [Page 25] + +Internet-Draft Network Slice Service YANG Model May 2025 + + +5.2.6. Network Slice Service Feasibility Check + + A Network Slice Service customer may request to check the feasibility + of a request before instantiating or modifying a Network Slice + Service, e.g., network resources such as service access points for + service delivery. In such a case, this document introduces a "test- + only" mode (semantics derived from NETCONF [RFC6241] test operation), + which differs from standard management operations. + + A "test-only" Network Slice Service is configured as usual with the + associated per slice SLOs/SLEs. The NSC computes the feasible + Connectivity Constructs to the configured SLOs/SLEs. This + computation does not create the Network Slice or reserve any + resources in the provider's network, it simply computes the resulting + Network Slice based on the request. The Network Slice "admin-status" + and the Connection Groups or Connectivity Construct list are used to + convey the result. For example, "admin-up" can be used to indicate a + status of success. Customers can query the "test-only" Connectivity + Constructs attributes, or can subscribe to be notified when the + Connectivity Constructs status change. If the check fails, the + feedback is conveyed through the "rejected" value of "admin-status", + indicating the reasons for the failure, such as insufficient + resources or constraint violation. + + As defined in Section 6.3 of [RFC9543] for multi-domain requirements, + when a Network Slice spans multiple administrative domains, the + 'test-only' mode relies on the NSC to aggregate and validate + information across these domains. This could include: + + 1. Validating end-to-end Network Slice requests to ensure they can + be realized across all domains. + + 2. Checking resource availability and constraints within each domain + to confirm feasibility. + + 3. Identifying potential conflicts or bottlenecks between domains + that may impact the Network Slice's performance or realization. + + The "test-only" applies only if the data model is used with a + protocol that does not intrinsically support such operation, e.g., + [RFC8040]. When using NETCONF, the "test-only" value of the parameter in the operation (Section 7.2 of + [RFC6241]) also applies. + + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 26] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + +--------+ +--------+ + |customer| | NSC | + +--------+ +--------+ + | | + | Request a Network Slice Service | + | feasibility check ("test-only") | + |---------------------------------------->| Validate the NS + | | based on + | | SDPs and + | | SLOs/SLEs + | Validated NS and status | + |<----------------------------------------| + | | + + NS: Network Slice + + Figure 16: An Example of NSS Feasibility Check + +6. Network Slice Service Module + + The "ietf-network-slice-service" module uses types defined in + [RFC6991], [RFC8294], [RFC8345], [RFC8519], [RFC9179], [RFC9181], + [I-D.ietf-opsawg-teas-attachment-circuit], + [I-D.ietf-opsawg-teas-common-ac], and [I-D.ietf-teas-rfc8776-update]. + + file "ietf-network-slice-service@2025-05-09.yang" + module ietf-network-slice-service { + yang-version 1.1; + namespace + "urn:ietf:params:xml:ns:yang:ietf-network-slice-service"; + prefix ietf-nss; + + import ietf-inet-types { + prefix inet; + reference + "RFC 6991: Common YANG Types"; + } + import ietf-routing-types { + prefix rt-types; + reference + "RFC 8294: Common YANG Data Types for the Routing Area"; + } + import ietf-yang-types { + prefix yang; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-geo-location { + + + +Wu, et al. Expires 10 November 2025 [Page 27] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + prefix geo; + reference + "RFC 9179: A YANG Grouping for Geographic Locations"; + } + import ietf-vpn-common { + prefix vpn-common; + reference + "RFC 9181: A Common YANG Data Model for Layer 2 and Layer 3 + VPNs"; + } + import ietf-network { + prefix nw; + reference + "RFC 8345: A YANG Data Model for Network Topologies"; + } + import ietf-network-topology { + prefix nt; + reference + "RFC 8345: A YANG Data Model for Network + Topologies, Section 6.2"; + } + import ietf-ac-common { + prefix ac-common; + reference + "RFC BBBB: A Common YANG Data Model for Attachment Circuits"; + } + import ietf-ac-svc { + prefix ac-svc; + reference + "RFC CCCC: YANG Data Models for Bearers and 'Attachment + Circuits'-as-a-Service (ACaaS)"; + } + import ietf-te-types { + prefix te-types; + reference + "RFC DDDD: Common YANG Types for Traffic Engineering"; + } + import ietf-te-packet-types { + prefix te-packet-types; + reference + "RFC DDDD: Common YANG Data Types for Traffic Engineering"; + } + + organization + "IETF Traffic Engineering Architecture and Signaling (TEAS) + Working Group"; + contact + "WG Web: + + + +Wu, et al. Expires 10 November 2025 [Page 28] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + WG List: + + Editor: Bo Wu + + Editor: Dhruv Dhody + + Editor: Reza Rokui + + Editor: Tarek Saad + + Editor: John Mullooly + "; + description + "This YANG module defines a service model for the RFC 9543 + Network Slice Service. + + Copyright (c) 2025 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject to + the license terms contained in, the Revised BSD License set + forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC AAAA; see the + RFC itself for full legal notices."; + + revision 2025-05-09 { + description + "Initial revision."; + reference + "RFC AAAA: A YANG Data Model for the RFC 9543 Network Slice + Service"; + } + + /* Identities */ + + identity service-tag-type { + description + "Base identity of Network Slice Service tag type, which is + used for management purposes, such as classification + (e.g., customer names) and policy constraints + (e.g., Layer 2 or Layer 3 technology realization)."; + } + + identity customer { + + + +Wu, et al. Expires 10 November 2025 [Page 29] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + base service-tag-type; + description + "The Network Slice Service customer name tag type, + e.g., adding tags with 'customer name' when multiple actual + customers use the same Network Slice Service."; + } + + identity service { + base service-tag-type; + description + "The Network Slice Service tag type, which can indicate the + technical constraints used during service realization + (for example, Layer 2 or Layer 3 technologies)."; + } + + identity opaque { + base service-tag-type; + description + "An opaque type, which can be used for future use, + such as filtering of services."; + } + + identity attachment-circuit-tag-type { + description + "Base identity for the Attachment Circuit tag type."; + } + + identity vlan-id { + base attachment-circuit-tag-type; + description + "Identity for VLAN ID tag type, 802.1Q dot1Q."; + reference + "IEEE Std 802.1Q: IEEE Standard for Local and Metropolitan + Area Networks--Bridges and Bridged + Networks"; + } + + identity cvlan-id { + base attachment-circuit-tag-type; + description + "Identity for C-VLAN ID tag type, 802.1ad QinQ VLAN IDs."; + reference + "IEEE Std 802.1ad: IEEE Standard for Local and Metropolitan + Area Networks---Virtual Bridged Local + Area Networks---Amendment 4: Provider + Bridges"; + } + + + + +Wu, et al. Expires 10 November 2025 [Page 30] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + identity svlan-id { + base attachment-circuit-tag-type; + description + "Identity for S-VLAN ID tag type, 802.1ad QinQ VLAN IDs."; + reference + "IEEE Std 802.1ad: IEEE Standard for Local and Metropolitan + Area Networks---Virtual Bridged Local + Area Networks---Amendment 4: Provider + Bridges"; + } + + identity ip-address-mask { + base attachment-circuit-tag-type; + description + "Identity for IP address mask tag type."; + } + + identity service-isolation-type { + description + "Base identity for Network Slice Service isolation type."; + } + + identity traffic-isolation { + base service-isolation-type; + description + "Specify the requirement for separating the traffic of the + customer's Network Slice Service from other services, + which may be provided by the service provider using VPN + technologies, such as L3VPN, L2VPN, EVPN, or others."; + } + + identity service-security-type { + description + "Base identity for Network Slice Service security type."; + } + + identity authentication { + base service-security-type; + description + "Indicates that the Slice Service requires authentication."; + } + + identity integrity { + base service-security-type; + description + "Indicates that the Slice Service requires data integrity."; + } + + + + +Wu, et al. Expires 10 November 2025 [Page 31] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + identity encryption { + base service-security-type; + description + "Indicates that the Slice Service requires data encryption."; + } + + identity point-to-point { + base vpn-common:vpn-topology; + description + "Identity for point-to-point Network Slice + Service connectivity."; + } + + identity point-to-multipoint { + base vpn-common:vpn-topology; + description + "Identity for point-to-multipoint Network Slice + Service connectivity."; + } + + identity multipoint-to-multipoint { + base vpn-common:vpn-topology; + description + "Identity for multipoint-to-multipoint Network Slice + Service connectivity."; + } + + identity multipoint-to-point { + base vpn-common:vpn-topology; + description + "Identity for multipoint-to-point Network Slice + Service connectivity."; + } + + identity sender-role { + base vpn-common:role; + description + "Indicates that an SDP is acting as a sender."; + } + + identity receiver-role { + base vpn-common:role; + description + "Indicates that an SDP is acting as a receiver."; + } + + identity service-slo-metric-type { + description + + + +Wu, et al. Expires 10 November 2025 [Page 32] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "Base identity for Network Slice Service SLO metric type."; + } + + identity one-way-bandwidth { + base service-slo-metric-type; + description + "SLO bandwidth metric. Minimum guaranteed bandwidth between + two SDPs at any time and is measured unidirectionally."; + } + + identity two-way-bandwidth { + base service-slo-metric-type; + description + "SLO bandwidth metric. Minimum guaranteed bandwidth between + two SDPs at any time."; + } + + identity shared-bandwidth { + base service-slo-metric-type; + description + "The shared SLO bandwidth bound. It is the limit on the + bandwidth that can be shared among a group of + Connectivity Constructs of a Slice Service."; + } + + identity one-way-delay-maximum { + base service-slo-metric-type; + description + "The SLO objective of this metric is the upper bound of network + delay when transmitting between two SDPs."; + reference + "RFC 7679: A One-Way Delay Metric for IP Performance + Metrics (IPPM)"; + } + + identity one-way-delay-percentile { + base service-slo-metric-type; + description + "The SLO objective of this metric is percentile objective of + network delay when transmitting between two SDPs. + The metric is defined in RFC7679."; + reference + "RFC 7679: A One-Way Delay Metric for IP Performance + Metrics (IPPM)"; + } + + identity two-way-delay-maximum { + base service-slo-metric-type; + + + +Wu, et al. Expires 10 November 2025 [Page 33] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + description + "SLO two-way delay is the upper bound of network delay when + transmitting between two SDPs"; + reference + "RFC 2681: A Round-trip Delay Metric for IPPM"; + } + + identity two-way-delay-percentile { + base service-slo-metric-type; + description + "The SLO objective of this metric is the percentile + objective of network delay when the traffic transmitting + between two SDPs."; + reference + "RFC 2681: A Round-trip Delay Metric for IPPM"; + } + + identity one-way-delay-variation-maximum { + base service-slo-metric-type; + description + "The SLO objective of this metric is maximum bound of the + difference in the one-way delay between sequential packets + between two SDPs."; + reference + "RFC 3393: IP Packet Delay Variation Metric for IP Performance + Metrics (IPPM)"; + } + + identity one-way-delay-variation-percentile { + base service-slo-metric-type; + description + "The SLO objective of this metric is the percentile objective + in the one-way delay between sequential packets between two + SDPs."; + reference + "RFC 3393: IP Packet Delay Variation Metric for IP Performance + Metrics (IPPM)"; + } + + identity two-way-delay-variation-maximum { + base service-slo-metric-type; + description + "SLO two-way delay variation is the difference in the + round-trip delay between sequential packets between two + SDPs."; + reference + "RFC 5481: Packet Delay Variation Applicability Statement"; + } + + + +Wu, et al. Expires 10 November 2025 [Page 34] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + identity two-way-delay-variation-percentile { + base service-slo-metric-type; + description + "The SLO objective of this metric is the percentile objective + in the round-trip delay between sequential packets between + two SDPs."; + reference + "RFC 5481: Packet Delay Variation Applicability Statement"; + } + + identity one-way-packet-loss { + base service-slo-metric-type; + description + "This metric type refers to the ratio of packets dropped + to packets transmitted between two SDPs in one-way."; + reference + "RFC 7680: A One-Way Loss Metric for IP Performance + Metrics (IPPM)"; + } + + identity two-way-packet-loss { + base service-slo-metric-type; + description + "This metric type refers to the ratio of packets dropped + to packets transmitted between two SDPs in two-way."; + reference + "RFC 7680: A One-Way Loss Metric for IP Performance + Metrics (IPPM)"; + } + + identity availability-type { + description + "Base identity for availability."; + } + + identity six-nines { + base availability-type; + description + "Specifies the availability level: 99.9999%"; + } + + identity five-nines { + base availability-type; + description + "Specifies the availability level: 99.999%"; + } + + identity four-nines { + + + +Wu, et al. Expires 10 November 2025 [Page 35] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + base availability-type; + description + "Specifies the availability level: 99.99%"; + } + + identity three-nines { + base availability-type; + description + "Specifies the availability level: 99.9%"; + } + + identity two-nines { + base availability-type; + description + "Specifies the availability level: 99%"; + } + + identity service-match-type { + description + "Base identity for Network Slice Service traffic + match type."; + } + + identity phy-interface { + base service-match-type; + description + "Uses the physical interface as match criteria for + Slice Service traffic."; + } + + identity vlan { + base service-match-type; + description + "Uses the VLAN ID as match criteria for the Slice Service + traffic."; + } + + identity label { + base service-match-type; + description + "Uses the MPLS label as match criteria for the Slice Service + traffic."; + } + + identity source-ip-prefix { + base service-match-type; + description + "Uses source IP prefix as match criteria for the Slice Service + + + +Wu, et al. Expires 10 November 2025 [Page 36] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + traffic. Examples of 'value' of this match type are + '192.0.2.0/24' and '2001:db8::1/64'."; + } + + identity destination-ip-prefix { + base service-match-type; + description + "Uses destination IP prefix as match criteria for the Slice + Service traffic. Examples of 'value' of this match type are + '203.0.113.1/32' and '2001:db8::2/128'."; + } + + identity dscp { + base service-match-type; + description + "Uses DSCP field in the IP packet header as match criteria + for the Slice Service traffic."; + } + + identity acl { + base service-match-type; + description + "Uses Access Control List (ACL) as match criteria + for the Slice Service traffic."; + reference + "RFC 8519: YANG Data Model for Network Access Control + Lists (ACLs)"; + } + + identity any { + base service-match-type; + description + "Matches any Slice Service traffic."; + } + + identity slo-sle-policy-override { + description + "Base identity for SLO/SLE policy override options."; + } + + identity full-override { + base slo-sle-policy-override; + description + "The SLO/SLE policy defined at the child level overrides a + parent SLO/SLE policy, which means that no SLO/SLEs are + inherited from parent if a child SLO/SLE policy exists."; + } + + + + +Wu, et al. Expires 10 November 2025 [Page 37] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + identity partial-override { + base slo-sle-policy-override; + description + "The SLO/SLE policy defined at the child level updates the + parent SLO/SLE policy. For example, if a specific SLO is + defined at the child level, that specific SLO overrides + the one inherited from a parent SLO/SLE policy, while all + other SLOs in the parent SLO-SLE policy still apply."; + } + + /* Typedef */ + + typedef percentage { + type uint8 { + range "0..100"; + } + description + "Integer indicating a percentage value."; + } + + typedef percentile { + type decimal64 { + fraction-digits 3; + range "0..100"; + } + description + "The percentile is a value between 0 and 100 + to 3 decimal places, e.g., 10.000, 99.900,99.990, etc. + For example, for a given one-way delay measurement, + if the percentile is set to 95.000 and the 95th percentile + one-way delay is 2 milliseconds, then the 95 percent of + the sample value is less than or equal to 2 milliseconds."; + } + + typedef ns-compute-status { + type te-types:te-common-status; + description + "A type definition for representing the Network Slice + compute status. Note that all statuses apart from up and down + are considered as unknown."; + } + + typedef slice-template-ref { + type leafref { + path "/ietf-nss:network-slice-services" + + "/ietf-nss:slo-sle-templates" + + "/ietf-nss:slo-sle-template" + + "/ietf-nss:id"; + + + +Wu, et al. Expires 10 November 2025 [Page 38] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + description + "This type is used by data models that need to reference + Network Slice templates."; + } + + typedef slice-service-ref { + type leafref { + path + "/ietf-nss:network-slice-services/ietf-nss:slice-service" + + "/ietf-nss:id"; + } + description + "Defines a reference to a slice service that can be used + by other modules."; + } + + /* Groupings */ + + grouping service-slos { + description + "A reusable grouping for directly measurable objectives of + a Slice Service."; + container slo-policy { + description + "Contains the SLO policy."; + list metric-bound { + key "metric-type"; + description + "List of Slice Service metric bounds."; + leaf metric-type { + type identityref { + base service-slo-metric-type; + } + description + "Identifies SLO metric type of the Slice Service."; + } + leaf metric-unit { + type string; + mandatory true; + description + "The metric unit of the parameter. For example, + for time units, where the options are hours, minutes, + seconds, milliseconds, microseconds, and nanoseconds; + for bandwidth units, where the options are bps, Kbps, + Mbps, Gbps; for the packet loss rate unit, + the options can be a percentage."; + } + + + +Wu, et al. Expires 10 November 2025 [Page 39] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + leaf value-description { + type string; + description + "The description of the provided value."; + } + leaf percentile-value { + type percentile; + description + "The percentile value of the metric type."; + } + leaf bound { + type uint64; + description + "The bound on the Slice Service connection metric. + When set to zero, this indicates an unbounded + upper limit for the specific metric-type."; + } + } + leaf availability { + type identityref { + base availability-type; + } + description + "Service availability level."; + } + leaf mtu { + type uint32; + units "bytes"; + description + "Specifies the maximum length of Layer 2 data + packets of the Slice Service. + If the customer sends packets that are longer than the + requested service MTU, the network may discard them + (or for IPv4, fragment them). + This service MTU takes precedence over the MTUs of + all Attachment Circuits (ACs). The value needs to be + less than or equal to the minimum MTU value of + all ACs in the SDPs."; + } + } + } + + grouping service-sles { + description + "A reusable grouping for indirectly measurable objectives of + a Slice Service."; + container sle-policy { + description + + + +Wu, et al. Expires 10 November 2025 [Page 40] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "Contains the SLE policy."; + leaf-list security { + type identityref { + base service-security-type; + } + description + "The security functions (e.g., `authentication` and + `encryption`) that the customer requests the operator to + apply to traffic between the two SDPs."; + } + leaf-list isolation { + type identityref { + base service-isolation-type; + } + description + "The Slice Service isolation requirement."; + } + leaf max-occupancy-level { + type uint8 { + range "1..100"; + } + description + "The maximal occupancy level specifies the number of flows + to be admitted and optionally a maximum number of + countable resource units (e.g., IP or MAC addresses) + a Network Slice Service can consume."; + } + container path-constraints { + description + "Container for the policy of path constraints + applicable to the Slice Service."; + container service-functions { + description + "Container for the policy of service function + applicable to the Slice Service."; + } + container diversity { + description + "Container for the policy of disjointness + applicable to the Slice Service."; + leaf diversity-type { + type te-types:te-path-disjointness; + description + "The type of disjointness on Slice Service, i.e., + across all Connectivity Constructs."; + } + } + } + + + +Wu, et al. Expires 10 November 2025 [Page 41] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + } + + grouping slice-service-template { + description + "A reusable grouping for Slice Service templates."; + container slo-sle-templates { + description + "Contains a set of Slice Service templates."; + list slo-sle-template { + key "id"; + description + "List for SLO and SLE template identifiers."; + leaf id { + type string; + description + "Identification of the Service Level Objective (SLO) + and Service Level Expectation (SLE) template to be used. + Local administration meaning."; + } + leaf description { + type string; + description + "Describes the SLO and SLE policy template."; + } + leaf template-ref { + type slice-template-ref; + description + "The reference to a standard template. When set it + indicates the base template over which further + SLO/SLE policy changes are made."; + } + uses service-slos; + uses service-sles; + } + } + } + + grouping service-slo-sle-policy { + description + "Slice service policy grouping."; + choice slo-sle-policy { + description + "Choice for SLO and SLE policy template. + Can be standard template or customized template."; + case standard { + description + "Standard SLO template."; + + + +Wu, et al. Expires 10 November 2025 [Page 42] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + leaf slo-sle-template { + type slice-template-ref; + description + "Standard SLO and SLE template to be used."; + } + } + case custom { + description + "Customized SLO and SLE template."; + container service-slo-sle-policy { + description + "Contains the SLO and SLE policy."; + leaf description { + type string; + description + "Describes the SLO and SLE policy."; + } + uses service-slos; + uses service-sles; + } + } + } + } + + grouping service-qos { + description + "Grouping for the Slice Service QoS policy."; + container incoming-qos-policy { + description + "The QoS policy imposed on ingress direction of the traffic, + from the customer network or from another provider's + network."; + leaf qos-policy-name { + type string; + description + "The name of the QoS policy that is applied to the + Attachment Circuit. The name can reference a QoS + profile that is pre-provisioned on the device."; + } + container rate-limits { + description + "Container for the asymmetric traffic control."; + uses ac-common:bandwidth-parameters; + container classes { + description + "Container for service class bandwidth control."; + list cos { + key "cos-id"; + + + +Wu, et al. Expires 10 November 2025 [Page 43] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + description + "List of Class of Services."; + leaf cos-id { + type uint8; + description + "Identifier of the CoS, indicated by + a Differentiated Services Code Point + (DSCP) or a CE-CLAN CoS (802.1p) + value in the service frame."; + reference + "IEEE Std 802.1Q: Bridges and Bridged + Networks"; + } + uses ac-common:bandwidth-parameters; + } + } + } + } + container outgoing-qos-policy { + description + "The QoS policy imposed on egress direction of the traffic, + towards the customer network or towards another + provider's network."; + leaf qos-policy-name { + type string; + description + "The name of the QoS policy that is applied to the + Attachment Circuit. The name can reference a QoS + profile that is pre-provisioned on the device."; + } + container rate-limits { + description + "The rate-limit imposed on outgoing traffic."; + uses ac-common:bandwidth-parameters; + container classes { + description + "Container for classes."; + list cos { + key "cos-id"; + description + "List of Class of Services."; + leaf cos-id { + type uint8; + description + "Identifier of the CoS, indicated by + a Differentiated Services Code Point + (DSCP) or a CE-CLAN CoS (802.1p) + value in the service frame."; + + + +Wu, et al. Expires 10 November 2025 [Page 44] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + reference + "IEEE Std 802.1Q: Bridges and Bridged + Networks"; + } + uses ac-common:bandwidth-parameters; + } + } + } + } + } + + grouping service-slo-sle-policy-override { + description + "Slice Service policy override grouping."; + leaf service-slo-sle-policy-override { + type identityref { + base slo-sle-policy-override; + } + description + "SLO/SLE policy override option."; + } + } + + grouping connectivity-construct-monitoring-metrics { + description + "Grouping for Connectivity Construct monitoring metrics."; + uses + te-packet-types:one-way-performance-metrics-gauge-packet; + uses + te-packet-types:two-way-performance-metrics-gauge-packet; + } + + /* Main Network Slice Services Container */ + + container network-slice-services { + description + "Contains a list of Network Slice Services"; + uses slice-service-template; + list slice-service { + key "id"; + description + "A Slice Service is identified by a service id."; + leaf id { + type string; + description + "A unique Slice Service identifier within an NSC."; + } + leaf description { + + + +Wu, et al. Expires 10 November 2025 [Page 45] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + type string; + description + "Textual description of the Slice Service."; + } + container service-tags { + description + "Container for a list of service tags for management + purposes, such as policy constraints + (e.g., Layer 2 or Layer 3 technology realization), + classification (e.g., customer names, opaque values)."; + list tag-type { + key "tag-type"; + description + "The service tag list."; + leaf tag-type { + type identityref { + base service-tag-type; + } + description + "Slice Service tag type, e.g., realization technology + constraints, customer name, or other customer-defined + opaque types."; + } + leaf-list tag-type-value { + type string; + description + "The tag values, e.g., 5G customer names when multiple + customers share the same Slice Service in 5G scenario, + or Slice realization technology (such as Layer 2 or + Layer 3)."; + } + } + } + uses service-slo-sle-policy; + leaf test-only { + type empty; + description + "When present, this is a feasibility check. That is, no + resources are reserved in the network."; + } + uses ac-common:service-status; + container sdps { + description + "Slice Service SDPs."; + list sdp { + key "id"; + min-elements 2; + description + + + +Wu, et al. Expires 10 November 2025 [Page 46] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "List of SDPs in this Slice Service."; + leaf id { + type string; + description + "The unique identifier of the SDP within the scope of + an NSC."; + } + leaf description { + type string; + description + "Provides a description of the SDP."; + } + uses geo:geo-location; + leaf node-id { + type string; + description + "A unique identifier of an edge node of the SDP + within the scope of the NSC."; + } + leaf-list sdp-ip-address { + type inet:ip-address; + description + "IPv4 or IPv6 address of the SDP."; + } + leaf tp-ref { + type leafref { + path + "/nw:networks/nw:network[nw:network-id=" + + "current()/../../../custom-topology/network-ref]/" + + "nw:node/nt:termination-point/nt:tp-id"; + } + description + "A reference to Termination Point (TP) in the custom + topology"; + reference + "RFC 8345: A YANG Data Model for Network Topologies"; + } + container service-match-criteria { + description + "Describes the Slice Service match criteria."; + list match-criterion { + key "index"; + description + "List of the Slice Service traffic match criteria."; + leaf index { + type uint32; + description + "The identifier of a match criteria."; + + + +Wu, et al. Expires 10 November 2025 [Page 47] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + list match-type { + key "type"; + description + "List of the Slice Service traffic match types."; + leaf type { + type identityref { + base service-match-type; + } + description + "Indicates the match type of the entry in the + list of the Slice Service match criteria."; + } + choice value { + description + "Choice for value of the match type."; + case interface { + when "derived-from-or-self" + + "(type,'ietf-nss:phy-interface')" { + description + "Match type is a physical interface."; + } + leaf-list interface-name { + type string; + description + "Physical interface name for the + match criteria."; + } + } + case vlan { + when "derived-from-or-self" + + "(type, 'ietf-nss:vlan')" { + description + "Match type is a VLAN ID."; + } + leaf-list vlan { + type uint16 { + range "0..4095"; + } + description + "VLAN ID value for the match criteria."; + } + } + case label { + when "derived-from-or-self" + + "(type, 'ietf-nss:label')" { + description + "Match type is an MPLS label."; + + + +Wu, et al. Expires 10 November 2025 [Page 48] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + leaf-list label { + type rt-types:mpls-label; + description + "MPLS label value for the match + criteria."; + } + } + case ip-prefix { + when + "derived-from-or-self" + + "(type, 'ietf-nss:source-ip-prefix') or " + + "derived-from-or-self" + + "(type, 'ietf-nss:destination-ip-prefix')" { + description + "Match type is an IP prefix."; + } + leaf-list ip-prefix { + type inet:ip-prefix; + description + "IP prefix value for the match criteria."; + } + } + case dscp { + when "derived-from-or-self" + + "(type, 'ietf-nss:dscp')" { + description + "Match type is a DSCP value."; + } + leaf-list dscp { + type inet:dscp; + description + "DSCP value for the match criteria."; + } + } + case acl { + when "derived-from-or-self" + + "(type, 'ietf-nss:acl')" { + description + "Match type is an ACL name."; + } + leaf-list acl-name { + type string { + length "1..64"; + } + description + "ACL name value for the match + criteria."; + + + +Wu, et al. Expires 10 November 2025 [Page 49] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + } + /* Add more cases as needed for other + match types */ + } + } + leaf target-connection-group-id { + type leafref { + path + "../../../../../ietf-nss:connection-groups" + + "/ietf-nss:connection-group" + + "/ietf-nss:id"; + } + mandatory true; + description + "Reference to the Slice Service Connection Group."; + } + leaf connection-group-sdp-role { + type identityref { + base vpn-common:role; + } + default "vpn-common:any-to-any-role"; + description + "Specifies the role of SDP in the Connection Group + When the service connection type is MP2MP, + such as hub and spoke service connection type. + In addition, this helps to create Connectivity + Construct automatically, rather than explicitly + specifying each one."; + } + leaf target-connectivity-construct-id { + type leafref { + path + "../../../../../ietf-nss:connection-groups" + + "/ietf-nss:connection-group[ietf-nss:id=" + + "current()/../target-connection-group-id]" + + "/ietf-nss:connectivity-construct/ietf-nss:id"; + } + description + "Reference to a Network Slice Connectivity + Construct."; + } + } + } + uses service-qos; + container sdp-peering { + description + "Describes SDP peering attributes."; + + + +Wu, et al. Expires 10 November 2025 [Page 50] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + leaf-list peer-sap-id { + type string; + description + "Indicates the reference to the remote endpoints of + the Attachment Circuits. This information can be + used for correlation purposes, such as identifying + SAPs of provider equipments when requesting + a service with CE based SDP attributes."; + reference + "RFC 9408: A YANG Network Data Model for Service + Attachment Points (SAPs)"; + } + container protocols { + description + "Serves as an augmentation target. + Protocols can be augmented into this container, + e.g., BGP or static routing."; + } + } + leaf-list ac-svc-ref { + type ac-svc:attachment-circuit-reference; + description + "A reference to the ACs that have been created before + the slice creation."; + reference + "RFC CCCC: YANG Data Models for Bearers and + 'Attachment Circuits'-as-a-Service (ACaaS)"; + } + leaf ce-mode { + type boolean; + description + "When set to 'true', this indicates the SDP is located + on the CE."; + } + container attachment-circuits { + description + "List of Attachment Circuits."; + list attachment-circuit { + key "id"; + description + "The Network Slice Service SDP Attachment Circuit + related parameters."; + leaf id { + type string; + description + "The identifier of Attachment Circuit."; + } + leaf description { + + + +Wu, et al. Expires 10 November 2025 [Page 51] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + type string; + description + "The Attachment Circuit's description."; + } + leaf ac-svc-ref { + type ac-svc:attachment-circuit-reference; + description + "A reference to the AC service that has been + created before the slice creation."; + reference + "RFC CCCC: YANG Data Models for Bearers and + 'Attachment Circuits'-as-a-Service (ACaaS)"; + } + leaf ac-node-id { + type string; + description + "The Attachment Circuit node ID in the case of + multi-homing."; + } + leaf ac-tp-id { + type string; + description + "The termination port ID of the + Attachment Circuit."; + } + leaf ac-ipv4-address { + type inet:ipv4-address; + description + "The IPv4 address of the AC."; + } + leaf ac-ipv4-prefix-length { + type uint8 { + range "0..32"; + } + description + "The length of the IPv4 subnet prefix."; + } + leaf ac-ipv6-address { + type inet:ipv6-address; + description + "The IPv6 address of the AC."; + } + leaf ac-ipv6-prefix-length { + type uint8 { + range "0..128"; + } + description + "The length of IPv6 subnet prefix."; + + + +Wu, et al. Expires 10 November 2025 [Page 52] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + leaf mtu { + type uint32; + units "bytes"; + description + "Maximum size of the Slice Service Layer 2 data + packet that can traverse an SDP."; + } + container ac-tags { + description + "Container for the Attachment Circuit tags."; + list ac-tag { + key "tag-type"; + description + "The Attachment Circuit tag list."; + leaf tag-type { + type identityref { + base attachment-circuit-tag-type; + } + description + "The Attachment Circuit tag type."; + } + leaf-list tag-type-value { + type string; + description + "The Attachment Circuit tag values. + For example, the tag may indicate + multiple VLAN identifiers."; + } + } + } + uses service-qos; + container sdp-peering { + description + "Describes SDP peering attributes."; + leaf peer-sap-id { + type string; + description + "Indicates a reference to the remote endpoints + of an Attachment Circuit. This information can + be used for correlation purposes, such as + identifying a Service Attachment Point (SAP) + of a provider equipment when requesting a + service with CE based SDP attributes."; + reference + "RFC 9408: A YANG Network Data Model for + Service Attachment Points (SAPs)"; + } + + + +Wu, et al. Expires 10 November 2025 [Page 53] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + container protocols { + description + "Serves as an augmentation target. + Protocols can be augmented into this container, + e.g., BGP or static routing."; + } + } + uses ac-common:service-status; + } + } + uses ac-common:service-status; + container sdp-monitoring { + config false; + description + "Container for SDP monitoring metrics."; + leaf incoming-bw-value { + type yang:gauge64; + units "bps"; + description + "Indicates the absolute value of the incoming + bandwidth at an SDP from the customer network or + from another provider's network."; + } + leaf incoming-bw-percent { + type percentage; + units "percent"; + description + "Indicates a percentage of the incoming bandwidth + at an SDP from the customer network or + from another provider's network."; + } + leaf outgoing-bw-value { + type yang:gauge64; + units "bps"; + description + "Indicates the absolute value of the outgoing + bandwidth at an SDP towards the customer network or + towards another provider's network."; + } + leaf outgoing-bw-percent { + type percentage; + units "percent"; + description + "Indicates a percentage of the outgoing bandwidth + at an SDP towards the customer network or towards + another provider's network."; + } + } + + + +Wu, et al. Expires 10 November 2025 [Page 54] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + } + container connection-groups { + description + "Contains Connection Groups."; + list connection-group { + key "id"; + description + "List of Connection Groups."; + leaf id { + type string; + description + "The Connection Group identifier."; + } + leaf connectivity-type { + type identityref { + base vpn-common:vpn-topology; + } + default "vpn-common:any-to-any"; + description + "Connection Group connectivity type."; + } + uses service-slo-sle-policy; + /* Per Connection Group SLO/SLE policy + * overrides the per Slice SLO/SLE policy. + */ + uses service-slo-sle-policy-override; + list connectivity-construct { + key "id"; + description + "List of Connectivity Constructs."; + leaf id { + type string; + description + "The Connectivity Construct identifier."; + } + choice type { + default "p2p"; + description + "Choice for Connectivity Construct type."; + case p2p { + description + "P2P Connectivity Construct."; + leaf p2p-sender-sdp { + type leafref { + path "../../../../sdps/sdp/id"; + } + description + + + +Wu, et al. Expires 10 November 2025 [Page 55] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "Reference to a sender SDP."; + } + leaf p2p-receiver-sdp { + type leafref { + path "../../../../sdps/sdp/id"; + } + description + "Reference to a receiver SDP."; + } + } + case p2mp { + description + "P2MP Connectivity Construct."; + leaf p2mp-sender-sdp { + type leafref { + path "../../../../sdps/sdp/id"; + } + description + "Reference to a sender SDP."; + } + leaf-list p2mp-receiver-sdp { + type leafref { + path "../../../../sdps/sdp/id"; + } + description + "Reference to a receiver SDP."; + } + } + case a2a { + description + "A2A Connectivity Construct."; + list a2a-sdp { + key "sdp-id"; + description + "List of included A2A SDPs."; + leaf sdp-id { + type leafref { + path "../../../../../sdps/sdp/id"; + } + description + "Reference to an SDP."; + } + uses service-slo-sle-policy; + } + } + } + uses service-slo-sle-policy; + /* Per Connectivity Construct SLO/SLE policy + + + +Wu, et al. Expires 10 November 2025 [Page 56] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + * overrides the per slice SLO/SLE policy. + */ + uses service-slo-sle-policy-override; + uses ac-common:service-status; + container connectivity-construct-monitoring { + config false; + description + "SLO status per Connectivity Construct."; + uses connectivity-construct-monitoring-metrics; + } + } + container connection-group-monitoring { + config false; + description + "SLO status per Connection Group."; + uses connectivity-construct-monitoring-metrics; + } + } + } + container custom-topology { + description + "Serves as an augmentation target. + Container for custom topology, which is indicated by the + referenced topology predefined, e.g., an abstract RFC8345 + topology."; + uses nw:network-ref; + } + } + } + } + + + Figure 17: Network Slice Service YANG Module + + +7. Security Considerations + + This section is modeled after the template described in Section 3.7 + of [I-D.ietf-netmod-rfc8407bis]. + + The "ietf-network-slice-service" YANG module defines a data model + that is designed to be accessed via YANG-based management protocols, + such as NETCONF [RFC6241] or RESTCONF [RFC8040]. These protocols + have to use a secure transport layer (e.g., SSH [RFC4252], TLS + [RFC8446], and QUIC [RFC9000]). The YANG-based management protocols + also have to use mutual authentication. + + + + + +Wu, et al. Expires 10 November 2025 [Page 57] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + Servers MUST verify that requesting clients are entitled to access + and manipulate a given Network Slice Service. The Network + Configuration Access Control Model (NACM) [RFC8341] provides the + means to restrict access for particular NETCONF or RESTCONF users to + a preconfigured subset of all available NETCONF or RESTCONF protocol + operations and content. + + There are a number of data nodes defined in these YANG modules that + are writable/creatable/deletable (i.e., config true, which is the + default). All writable data nodes are likely to be reasonably + sensitive or vulnerable in some network environments. Write + operations (e.g., edit-config) and delete operations to these data + nodes without proper protection or authentication can have a negative + effect on network operations. Specifically, the following subtrees + and data nodes have particular sensitivities/vulnerabilities in the + "ietf-network-slice-service" module: + + * /ietf-network-slice-service/network-slice-services/slo-sle- + templates + + This subtree specifies the Network Slice Service SLO templates and + SLE templates. Modifying the configuration in the subtree will + change the related Network Slice Service configuration in the future. + By making such modifications, a malicious attacker may degrade the + Slice Service functions configured at a certain time in the future. + + * /ietf-network-slice-service/network-slice-services/slice-service + + The entries in the list above include the whole network + configurations corresponding with the Network Slice Service which the + higher management system requests, and indirectly create or modify + the PE or P device configurations. Unexpected changes to these + entries could lead to service disruption and/or network misbehavior. + + Some of the readable data nodes in these YANG modules may be + considered sensitive or vulnerable in some network environments. It + is thus important to control read access (e.g., via get, get-config, + or notification) to these data nodes. Specifically, the following + subtrees and data nodes have particular sensitivity/vulnerability in + the "ietf-network-slice-service" module: + + * /ietf-network-slice-service/network-slice-services/slo-sle- + templates + + Unauthorized access to the subtree may disclose the SLO and SLE + templates of the Network Slice Service. + + * /ietf-network-slice-service/network-slice-services/slice-service + + + +Wu, et al. Expires 10 November 2025 [Page 58] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + Unauthorized access to the subtree may disclose the operation status + information of the Network Slice Service. + + * /ietf-network-slice-service/network-slice-services/slice-service/ + service-tags + + Unauthorized access to the subtree may disclose privacy data such as + customer names of the Network Slice Service. + +8. IANA Considerations + + This document requests to register the following URI in the IETF XML + registry [RFC3688]: + + URI: urn:ietf:params:xml:ns:yang:ietf-network-slice-service + Registrant Contact: The IESG. + XML: N/A, the requested URI is an XML namespace. + + This document requests to register the following YANG module in the + YANG Module Names registry [RFC6020]. + + Name: ietf-network-slice-service + Namespace: urn:ietf:params:xml:ns:yang:ietf-network-slice-service + Prefix: ietf-nss + Maintained by IANA? N + Reference: RFC AAAA + +9. Acknowledgments + + The authors wish to thank Mohamed Boucadair, Kenichi Ogaki, Sergio + Belotti, Qin Wu, Yao Zhao, Eric Grey, Daniele Ceccarelli, Ryan + Hoffman, Adrian Farrel, Aihua Guo, Italo Busi, and many others for + their helpful comments and suggestions. + + Thanks to Ladislav Lhotka for the YANG Doctors review. + + Thanks to Alvaro Retana and Susan Hares for the rtgdir reviews, Per + Andersson for the opsdir review, Mike Ounsworth for the secdir + review, Kyle Rose for the tsvdir review, and Ines Robles for the + genart review. + + Thanks to Mahesh Jethanandani, Gorry Fairhurst, Eric Vyncke, Ketan + Talaulikar, Erik Kline, Mike Bishop, Deb Cooley, and Paul Wouters for + the IESG review. + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 59] + +Internet-Draft Network Slice Service YANG Model May 2025 + + +10. Contributors + + The following authors contributed significantly to this document: + + Luis M. Contreras + Telefonica + Spain + Email: luismiguel.contrerasmurillo@telefonica.com + + Liuyan Han + China Mobile + Email: hanliuyan@chinamobile.com + +11. References + +11.1. Normative References + + [I-D.ietf-opsawg-teas-attachment-circuit] + Boucadair, M., Roberts, R., de Dios, O. G., Barguil, S., + and B. Wu, "YANG Data Models for Bearers and 'Attachment + Circuits'-as-a-Service (ACaaS)", Work in Progress, + Internet-Draft, draft-ietf-opsawg-teas-attachment-circuit- + 20, 23 January 2025, + . + + [I-D.ietf-opsawg-teas-common-ac] + Boucadair, M., Roberts, R., de Dios, O. G., Barguil, S., + and B. Wu, "A Common YANG Data Model for Attachment + Circuits", Work in Progress, Internet-Draft, draft-ietf- + opsawg-teas-common-ac-15, 23 January 2025, + . + + [I-D.ietf-teas-rfc8776-update] + Busi, I., Guo, A., Liu, X., Saad, T., and I. Bryskin, + "Common YANG Data Types for Traffic Engineering", Work in + Progress, Internet-Draft, draft-ietf-teas-rfc8776-update- + 17, 21 February 2025, + . + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, + DOI 10.17487/RFC2119, March 1997, + . + + + + + +Wu, et al. Expires 10 November 2025 [Page 60] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + [RFC2681] Almes, G., Kalidindi, S., and M. Zekauskas, "A Round-trip + Delay Metric for IPPM", RFC 2681, DOI 10.17487/RFC2681, + September 1999, . + + [RFC3393] Demichelis, C. and P. Chimento, "IP Packet Delay Variation + Metric for IP Performance Metrics (IPPM)", RFC 3393, + DOI 10.17487/RFC3393, November 2002, + . + + [RFC3688] Mealling, M., "The IETF XML Registry", BCP 81, RFC 3688, + DOI 10.17487/RFC3688, January 2004, + . + + [RFC6020] Bjorklund, M., Ed., "YANG - A Data Modeling Language for + the Network Configuration Protocol (NETCONF)", RFC 6020, + DOI 10.17487/RFC6020, October 2010, + . + + [RFC6991] Schoenwaelder, J., Ed., "Common YANG Data Types", + RFC 6991, DOI 10.17487/RFC6991, July 2013, + . + + [RFC7679] Almes, G., Kalidindi, S., Zekauskas, M., and A. Morton, + Ed., "A One-Way Delay Metric for IP Performance Metrics + (IPPM)", STD 81, RFC 7679, DOI 10.17487/RFC7679, January + 2016, . + + [RFC7680] Almes, G., Kalidindi, S., Zekauskas, M., and A. Morton, + Ed., "A One-Way Loss Metric for IP Performance Metrics + (IPPM)", STD 82, RFC 7680, DOI 10.17487/RFC7680, January + 2016, . + + [RFC7950] Bjorklund, M., Ed., "The YANG 1.1 Data Modeling Language", + RFC 7950, DOI 10.17487/RFC7950, August 2016, + . + + [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC + 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, + May 2017, . + + [RFC8294] Liu, X., Qu, Y., Lindem, A., Hopps, C., and L. Berger, + "Common YANG Data Types for the Routing Area", RFC 8294, + DOI 10.17487/RFC8294, December 2017, + . + + [RFC8340] Bjorklund, M. and L. Berger, Ed., "YANG Tree Diagrams", + BCP 215, RFC 8340, DOI 10.17487/RFC8340, March 2018, + . + + + +Wu, et al. Expires 10 November 2025 [Page 61] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + [RFC8341] Bierman, A. and M. Bjorklund, "Network Configuration + Access Control Model", STD 91, RFC 8341, + DOI 10.17487/RFC8341, March 2018, + . + + [RFC8342] Bjorklund, M., Schoenwaelder, J., Shafer, P., Watsen, K., + and R. Wilton, "Network Management Datastore Architecture + (NMDA)", RFC 8342, DOI 10.17487/RFC8342, March 2018, + . + + [RFC8345] Clemm, A., Medved, J., Varga, R., Bahadur, N., + Ananthakrishnan, H., and X. Liu, "A YANG Data Model for + Network Topologies", RFC 8345, DOI 10.17487/RFC8345, March + 2018, . + + [RFC8519] Jethanandani, M., Agarwal, S., Huang, L., and D. Blair, + "YANG Data Model for Network Access Control Lists (ACLs)", + RFC 8519, DOI 10.17487/RFC8519, March 2019, + . + + [RFC8639] Voit, E., Clemm, A., Gonzalez Prieto, A., Nilsen-Nygaard, + E., and A. Tripathy, "Subscription to YANG Notifications", + RFC 8639, DOI 10.17487/RFC8639, September 2019, + . + + [RFC8641] Clemm, A. and E. Voit, "Subscription to YANG Notifications + for Datastore Updates", RFC 8641, DOI 10.17487/RFC8641, + September 2019, . + + [RFC9179] Hopps, C., "A YANG Grouping for Geographic Locations", + RFC 9179, DOI 10.17487/RFC9179, February 2022, + . + + [RFC9181] Barguil, S., Gonzalez de Dios, O., Ed., Boucadair, M., + Ed., and Q. Wu, "A Common YANG Data Model for Layer 2 and + Layer 3 VPNs", RFC 9181, DOI 10.17487/RFC9181, February + 2022, . + + [RFC9408] Boucadair, M., Ed., Gonzalez de Dios, O., Barguil, S., Wu, + Q., and V. Lopez, "A YANG Network Data Model for Service + Attachment Points (SAPs)", RFC 9408, DOI 10.17487/RFC9408, + June 2023, . + + [RFC9543] Farrel, A., Ed., Drake, J., Ed., Rokui, R., Homma, S., + Makhijani, K., Contreras, L., and J. Tantsura, "A + Framework for Network Slices in Networks Built from IETF + Technologies", RFC 9543, DOI 10.17487/RFC9543, March 2024, + . + + + +Wu, et al. Expires 10 November 2025 [Page 62] + +Internet-Draft Network Slice Service YANG Model May 2025 + + +11.2. Informative References + + [I-D.ietf-netmod-rfc8407bis] + Bierman, A., Boucadair, M., and Q. Wu, "Guidelines for + Authors and Reviewers of Documents Containing YANG Data + Models", Work in Progress, Internet-Draft, draft-ietf- + netmod-rfc8407bis-24, 18 April 2025, + . + + [I-D.ietf-teas-ietf-network-slice-use-cases] + Contreras, L. M., Homma, S., Ordonez-Lucena, J. A., + Tantsura, J., and H. Nishihara, "IETF Network Slice Use + Cases and Attributes for the Slice Service Interface of + IETF Network Slice Controllers", Work in Progress, + Internet-Draft, draft-ietf-teas-ietf-network-slice-use- + cases-01, 24 October 2022, + . + + [IEEE802.1ad] + "Amendment to IEEE 802.1Q-2005. IEEE Standard for Local + and Metropolitan Area Networks - Virtual Bridged Local + Area Networks Revision-Amendment 4: Provider Bridges", + IEEE Std 802.1ad, 2005. + + [IEEE802.1Q] + "IEEE Standard for Local and metropolitan area networks-- + Bridges and Bridged Networks", IEEE Std 802.1Q, 2022. + + [RFC4252] Ylonen, T. and C. Lonvick, Ed., "The Secure Shell (SSH) + Authentication Protocol", RFC 4252, DOI 10.17487/RFC4252, + January 2006, . + + [RFC5481] Morton, A. and B. Claise, "Packet Delay Variation + Applicability Statement", RFC 5481, DOI 10.17487/RFC5481, + March 2009, . + + [RFC6241] Enns, R., Ed., Bjorklund, M., Ed., Schoenwaelder, J., Ed., + and A. Bierman, Ed., "Network Configuration Protocol + (NETCONF)", RFC 6241, DOI 10.17487/RFC6241, June 2011, + . + + [RFC8040] Bierman, A., Bjorklund, M., and K. Watsen, "RESTCONF + Protocol", RFC 8040, DOI 10.17487/RFC8040, January 2017, + . + + + + + +Wu, et al. Expires 10 November 2025 [Page 63] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + [RFC8309] Wu, Q., Liu, W., and A. Farrel, "Service Models + Explained", RFC 8309, DOI 10.17487/RFC8309, January 2018, + . + + [RFC8446] Rescorla, E., "The Transport Layer Security (TLS) Protocol + Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018, + . + + [RFC8650] Voit, E., Rahman, R., Nilsen-Nygaard, E., Clemm, A., and + A. Bierman, "Dynamic Subscription to YANG Events and + Datastores over RESTCONF", RFC 8650, DOI 10.17487/RFC8650, + November 2019, . + + [RFC8792] Watsen, K., Auerswald, E., Farrel, A., and Q. Wu, + "Handling Long Lines in Content of Internet-Drafts and + RFCs", RFC 8792, DOI 10.17487/RFC8792, June 2020, + . + + [RFC8795] Liu, X., Bryskin, I., Beeram, V., Saad, T., Shah, H., and + O. Gonzalez de Dios, "YANG Data Model for Traffic + Engineering (TE) Topologies", RFC 8795, + DOI 10.17487/RFC8795, August 2020, + . + + [RFC9000] Iyengar, J., Ed. and M. Thomson, Ed., "QUIC: A UDP-Based + Multiplexed and Secure Transport", RFC 9000, + DOI 10.17487/RFC9000, May 2021, + . + + [RFC9731] Lee, Y., Ed., Dhody, D., Ed., Ceccarelli, D., Bryskin, I., + and B. Yoon, "A YANG Data Model for Virtual Network (VN) + Operations", RFC 9731, DOI 10.17487/RFC9731, March 2025, + . + +11.3. References + +Appendix A. Augmentation Considerations + + The NSSM defines the minimum attributes of Slice Services. In some + scenarios, further extension, e.g., the definition of AC technology + specific attributes and the "isolation" SLE characteristics are + required. + + + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 64] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + For AC technology specific attributes, if the customer and provider + need to agree, through configuration, on the technology parameter + values, such as the protocol types and protocol parameters between + the PE and the CE. The following shows an example where BGP and + static routing are augmented to the Network Slice Service model. The + protocol types and definitions can reference + [I-D.ietf-opsawg-teas-common-ac]. + + module: ietf-network-slice-service-proto-ex + augment /ietf-nss:network-slice-services/ietf-nss:slice-service + /ietf-nss:sdps/ietf-nss:sdp/ietf-nss:sdp-peering + /ietf-nss:protocols: + +--rw bgp + | +--rw name? string + | +--ro local-as? inet:as-number + | +--rw peer-as? inet:as-number + | +--rw address-family? identityref + +--rw static-routing-ipv4 + | +--rw lan? inet:ipv4-prefix + | +--rw lan-tag? string + | +--rw next-hop? union + | +--rw metric? uint32 + +--rw static-routing-ipv6 + +--rw lan? inet:ipv6-prefix + +--rw lan-tag? string + +--rw next-hop? union + +--rw metric? uint32 + + Figure 18: Example YANG Tree Augmenting SDP Peering Protocols + + In some scenarios, for example, when multiple Slice Services share + one or more ACs, independent AC services, defined in + [I-D.ietf-opsawg-teas-attachment-circuit], can be used. + + For "isolation" SLE characteristics, the following identities can be + defined. + + identity service-interference-isolation-dedicated { + base service-isolation-type; + description + "Specify the requirement that the Slice Service is not impacted + by the existence of other customers or services in the same + network, which may be provided by the service provider using + dedicated network resources, similar to a dedicated + private network."; + } + + Figure 19: Example "isolation" Identity Augmentation + + + +Wu, et al. Expires 10 November 2025 [Page 65] + +Internet-Draft Network Slice Service YANG Model May 2025 + + +Appendix B. Examples of Network Slice Services + +B.1. Example-1: Two A2A Slice Services with Different Match Approaches + + Figure 20 shows an example of two Network Slice Service instances + where the SDPs are the customer-facing ports on the PE: + + * Network Slice 1 on SDP1, SDP11a, and SDP4, with an A2A + connectivity type. This is an L3 Slice Service that uses the + uniform low latency "slo-sle-template" policy between all SDPs. + These SDPs will also have AC eBGP peering sessions with unmanaged + CE elements (not shown) using an AC augmentation model such as the + one shown above. + + * Network Slice 2 on SDP2, SDP11b, with A2A connectivity type. This + is an L3 Slice Service that uses the uniform high bandwidth "slo- + sle-template" policy between all SDPs. + + Slice 1 uses the explicit match approach for mapping SDP traffic to a + "connectivity-construct", while slice 2 uses the implicit approach. + Both approaches are supported. The "slo-sle-templates" templates are + known to the customer. + + Note: These two slices both use service-tags of "L3". This "service- + tag" is operator defined and has no specific meaning in the YANG + model other than to give a hint to the NSC on the service expectation + being L3 forwarding. This tag may be omitted in other examples, as + its usage depends entirely on the needs of the operator and the NSC. + + +--------+ 192.0.2.1/26 + |CE1 o------/ VLAN100 + +--------+ | SDP1 +------+ + +--------+ +------o| PE A+-------------+ + |CE2 o-------/-----o| | | + +--------+ SDP2 +---+--+ | + 198.51.100.1/26| | 192.0.2.129/26 + VLAN200 | +---+--+ VLAN100 + | | | SDP4 +--------+ + | |PE C o-----/-----o CE21 | + +--------+ 192.0.2.65/26 | +---+--+ +--------+ + | o------/ VLAN101 | | + | | | SDP11a+---+---+ | + |CE11 | +------o|PE B +------------+ + | o-------/-----o| | + +--------+ SDP11b+------ + + 198.51.100.65/26 + VLAN201 + + + + +Wu, et al. Expires 10 November 2025 [Page 66] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + Figure 20: Example of Two A2A Slice Services + + Figure 21 shows an example YANG JSON data for the body of the Network + Slice Service instances request. + + ============== NOTE: '\' line wrapping per RFC 8792 =============== + + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "high-BW-template", + "description": "take the highest BW forwarding path" + }, + { + "id": "low-latency-template", + "description": \ + "lowest possible latency forwarding behavior" + } + ] + }, + "slice-service": [ + { + "id": "slice1", + "description": "example slice1", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L3" + ] + } + ] + }, + "slo-sle-template": "low-latency-template", + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "1", + "node-id": "PE-A", + "service-match-criteria": { + + + +Wu, et al. Expires 10 November 2025 [Page 67] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix1", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac1", + "description": "AC1 connected to device 1", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet5/0/0/0.100", + "ac-ipv4-address": "192.0.2.1", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + }, + { + "id": "3a", + "node-id": "PE-B", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + + + +Wu, et al. Expires 10 November 2025 [Page 68] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "target-connection-group-id": "matrix1", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac3a", + "description": "AC3a connected to device 3", + "ac-node-id": "PE-B", + "ac-tp-id": "GigabitEthernet8/0/0/4.101", + "ac-ipv4-address": "192.0.2.65", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "101" + ] + } + ] + } + } + ] + } + }, + { + "id": "4", + "node-id": "PE-C", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix1", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + + + +Wu, et al. Expires 10 November 2025 [Page 69] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "id": "ac4", + "description": "AC4 connected to device 4", + "ac-node-id": "PE-C", + "ac-tp-id": "GigabitEthernet4/0/0/3.100", + "ac-ipv4-address": "192.0.2.129", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "matrix1", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": "1", + "a2a-sdp": [ + { + "sdp-id": "1" + }, + { + "sdp-id": "3a" + }, + { + "sdp-id": "4" + } + ] + } + ] + } + ] + } + }, + { + + + +Wu, et al. Expires 10 November 2025 [Page 70] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "id": "slice2", + "description": "example slice2", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L3" + ] + } + ] + }, + "slo-sle-template": "high-BW-template", + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "2", + "node-id": "PE-A", + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac2", + "description": "AC2 connected to device 2", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet7/0/0/3.200", + "ac-ipv4-address": "198.51.100.1", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + }, + { + "id": "3b", + + + +Wu, et al. Expires 10 November 2025 [Page 71] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "node-id": "PE-B", + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac3b", + "description": "AC3b connected to device 3", + "ac-node-id": "PE-B", + "ac-tp-id": "GigabitEthernet8/0/0/4.201", + "ac-ipv4-address": "198.51.100.65", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "201" + ] + } + ] + } + } + ] + } + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "matrix2", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": "1", + "a2a-sdp": [ + { + "sdp-id": "2" + }, + { + "sdp-id": "3b" + } + ] + } + ] + } + ] + } + } + + + +Wu, et al. Expires 10 November 2025 [Page 72] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + ] + } + } + + + Figure 21: Example of a Message Body to Create Two A2A Slice Services + +B.2. Example-2: Two P2P Slice Services with Different Match Approaches + + Figure 22 shows an example of two Network Slice Service instances + where the SDPs are the customer-facing ports on the PE: + + * Network Slice 3 on SDP5 and SDP7a with P2P connectivity type. + This is an L2 Slice Service that uses the uniform low-latency + "slo-sle-template" policies between the SDPs. A connectivity- + group level slo-policy has been applied with a delay-based metric + bound of 10ms which will apply to both connectivity-constructs. + + * Network Slice 4 on SDP6 and SDP7b, with P2P connectivity type. + This is an L2 Slice Service that uses the high bandwidth "slo-sle- + template" policies between the SDPs. Traffic from SDP6 and SDP7b + is requesting a bandwidth of 1000Mbps, while in the reverse + direction from SDP7b to SDP6, 5000Mbps is being requested. + + Slice 3 uses the explicit match approach for mapping SDP traffic to a + "connectivity-group", while slice 2 uses the implicit approach. Both + approaches are supported. + + Note: These two slices both use service-tags of "L2". This "service- + tag" is operator defined and simply indicateas L2 forwarding + expectation to the NSC. This tag may be omitted in other examples, + as its usage depends on the needs of the operator and the NSC. + + + + + + + + + + + + + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 73] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + +--------+ + | CE5 o------/ VLAN100 + +--------+ | SDP5 +------+ + +--------+ +------o| PE A +---------------+ + | CE6 o-------/-----o| | | + +--------+ SDP6 +---+--+ | + VLAN200 | | + | +---+--+ + | | | + | | PE C o + +--------+ | +---+--+ + | o------/ VLAN101 | | + | | | SDP7a +---+--+ | + | CE7 | +------o| PE B +---------------+ + | o-------/-----o| | + +--------+ SDP7b +------+ + VLAN201 + + Figure 22: Example of Two P2P Slice Services + + Figure 23 shows an example YANG JSON data for the body of the Network + Slice Service instances request. + + ============== NOTE: '\' line wrapping per RFC 8792 =============== + + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "high-BW-template", + "description": "take the highest BW forwarding path" + }, + { + "id": "low-latency-template", + "description": \ + "lowest possible latency forwarding behavior" + } + ] + }, + "slice-service": [ + { + "id": "slice3", + "description": "example slice3", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + + + +Wu, et al. Expires 10 November 2025 [Page 74] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "tag-type-value": [ + "L2" + ] + } + ] + }, + "slo-sle-template": "low-latency-template", + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "5", + "node-id": "PE-A", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix3" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac5", + "description": "AC5 connected to device 5", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet5/0/0/1", + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + + + +Wu, et al. Expires 10 November 2025 [Page 75] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + ] + } + }, + { + "id": "7a", + "node-id": "PE-B", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix3" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac7a", + "description": "AC7a connected to device 7", + "ac-node-id": "PE-B", + "ac-tp-id": "GigabitEthernet8/0/0/5", + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "200" + ] + } + ] + } + } + ] + } + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "matrix3", + "connectivity-type": "point-to-point", + "service-slo-sle-policy": { + + + +Wu, et al. Expires 10 November 2025 [Page 76] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": "10" + } + ] + } + }, + "connectivity-construct": [ + { + "id": "1", + "p2p-sender-sdp": "5", + "p2p-receiver-sdp": "7a" + }, + { + "id": "2", + "p2p-sender-sdp": "7a", + "p2p-receiver-sdp": "5" + } + ] + } + ] + } + }, + { + "id": "slice4", + "description": "example slice4", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L2" + ] + } + ] + }, + "slo-sle-template": "high-BW-template", + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + + + +Wu, et al. Expires 10 November 2025 [Page 77] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "id": "6", + "node-id": "PE-A", + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac6", + "description": "AC6 connected to device 6", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet7/0/0/4", + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "101" + ] + } + ] + } + } + ] + } + }, + { + "id": "7b", + "node-id": "PE-B", + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac7b", + "description": "AC7b connected to device 7", + "ac-node-id": "PE-B", + "ac-tp-id": "GigabitEthernet8/0/0/5", + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "201" + ] + } + ] + } + } + ] + } + } + ] + + + +Wu, et al. Expires 10 November 2025 [Page 78] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + }, + "connection-groups": { + "connection-group": [ + { + "id": "matrix4", + "connectivity-type": "point-to-point", + "connectivity-construct": [ + { + "id": "1", + "p2p-sender-sdp": "6", + "p2p-receiver-sdp": "7b", + "service-slo-sle-policy": { + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-bandwidth", + "metric-unit": "Mbps", + "bound": "1000" + } + ] + } + } + }, + { + "id": "2", + "p2p-sender-sdp": "7b", + "p2p-receiver-sdp": "6", + "service-slo-sle-policy": { + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-bandwidth", + "metric-unit": "Mbps", + "bound": "5000" + } + ] + } + } + } + ] + } + ] + } + } + ] + } + } + + + + +Wu, et al. Expires 10 November 2025 [Page 79] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + Figure 23: Example of a Message Body to Create Two P2P Slice Services + + The example shown in Figure 24 illustrates how a customer might + subscribe to the monitoring information of "slice3" with the + "establish-subscription" RPC [RFC8650]. The customer is interested + in the operational and performance status of SDPs and Connectivity + Constructs. + + ============== NOTE: '\' line wrapping per RFC 8792 =============== + + POST /restconf/operations/ietf-subscribed-notifications:establish-\ + subscription + Host: example.com + Content-Type: application/yang-data+json + + { + "ietf-subscribed-notifications:input": { + "ietf-yang-push:datastore": "ietf-datastores:running", + "ietf-yang-push:datastore-subtree-filter": { + "ietf-network-slice-service:network-slice-services": { + "slice-service": [ + { + "id": "slice3", + "sdps": { + "sdp": [ + { + "id": "5", + "status": { + "oper-status": { + "status": {} + } + }, + "sdp-monitoring": { + "incoming-bw-value": {}, + "outgoing-bw-value": {} + } + }, + { + "id": "7a", + "status": { + "oper-status": { + "status": {} + } + }, + "sdp-monitoring": { + "incoming-bw-value": {}, + "outgoing-bw-value": {} + } + + + +Wu, et al. Expires 10 November 2025 [Page 80] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "matrix3", + "connectivity-type": "point-to-point", + "connectivity-construct": [ + { + "id": "1", + "p2p-sender-sdp": "5", + "p2p-receiver-sdp": "7a", + "status": { + "oper-status": { + "status": "{}" + } + }, + "connectivity-construct-monitoring": { + "one-way-min-delay": {}, + "one-way-max-delay": {} + } + }, + { + "id": "2", + "p2p-sender-sdp": "7a", + "p2p-receiver-sdp": "5", + "status": { + "oper-status": { + "status": {} + } + }, + "connectivity-construct-monitoring": { + "one-way-min-delay": {}, + "one-way-max-delay": {} + } + } + ] + } + ] + } + } + ] + } + }, + "ietf-yang-push:periodic": { + "period": "500" + } + + + +Wu, et al. Expires 10 November 2025 [Page 81] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + } + + Figure 24: Example of a Message Body to Subscribe Monitoring + Information of the Slice Service + + The example Figure 25 shows a snapshot of YANG JSON data for the body + of operational and performance status of the Network Slice Service + "slice3". + + + { + "ietf-network-slice-service:network-slice-services": { + "slice-service": [ + { + "id": "slice3", + "description": "example slice3", + "slo-sle-template": "low-latency-template", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "5", + "node-id": "PE-A", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "sdp-monitoring": { + "incoming-bw-value": "10000", + "outgoing-bw-value": "10000" + } + }, + { + "id": "7a", + "node-id": "PE-B", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "sdp-monitoring": { + "incoming-bw-value": "10000", + + + +Wu, et al. Expires 10 November 2025 [Page 82] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "outgoing-bw-value": "10000" + } + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "matrix3", + "connectivity-type": "point-to-point", + "connectivity-construct": [ + { + "id": "1", + "p2p-sender-sdp": "5", + "p2p-receiver-sdp": "7a", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "connectivity-construct-monitoring": { + "one-way-min-delay": "15", + "one-way-max-delay": "20" + } + }, + { + "id": "2", + "p2p-sender-sdp": "7a", + "p2p-receiver-sdp": "5", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "connectivity-construct-monitoring": { + "one-way-min-delay": "15", + "one-way-max-delay": "20" + } + } + ] + } + ] + } + }, + { + "id": "slice4", + "description": "example slice4", + "slo-sle-template": "high-BW-template", + + + +Wu, et al. Expires 10 November 2025 [Page 83] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "6", + "node-id": "PE-A", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "sdp-monitoring": { + "incoming-bw-value": "10000000", + "outgoing-bw-value": "10000000" + } + }, + { + "id": "7b", + "node-id": "PE-B", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "sdp-monitoring": { + "incoming-bw-value": "10000000", + "outgoing-bw-value": "10000000" + } + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "matrix4", + "connectivity-type": "point-to-point", + "connectivity-construct": [ + { + "id": "1", + "p2p-sender-sdp": "6", + "p2p-receiver-sdp": "7b", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + + + +Wu, et al. Expires 10 November 2025 [Page 84] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + }, + "connectivity-construct-monitoring": { + "one-way-min-delay": "150", + "one-way-max-delay": "200" + } + }, + { + "id": "2", + "p2p-sender-sdp": "7b", + "p2p-receiver-sdp": "6", + "status": { + "oper-status": { + "status": "ietf-vpn-common:op-up" + } + }, + "connectivity-construct-monitoring": { + "one-way-min-delay": "150", + "one-way-max-delay": "200" + } + } + ] + } + ] + } + } + ] + } + } + + + Figure 25: Example of a Message Body of a Snapshot of Monitoring + of the Slice Service + +B.3. Example-3: A Hub and Spoke Slice Service with a P2MP Connectivity + Construct + + Figure 26 shows an example of one Network Slice Service instance + where the SDPs are the customer-facing ports on the PE: + + Network Slice 5 is a hub-spoke slice with SDP14 as the hub and + SDP11, SDP12, SDP13a, SDP13b as spokes. This is an L3 Slice + Service that uses the uniform low-latency "slo-sle-template" + policies between all spokes and the hub SDPs, but using an + explicit set of SLO policies with a latency metric of 10ms for hub + to spoke traffic. + + + + + +Wu, et al. Expires 10 November 2025 [Page 85] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + +--------+ 192.0.2.1/26 + |Device11o------/ VLAN100 + +--------+ | SDP11+------+ + +--------+ +------o| A +-------------+ + |Device12o-------/-----o| | | + +--------+ SDP12+---+--+ | + 198.51.100.1/26 | | 192.0.2.129/26 + VLAN200 | +---+--+ VLAN100 + | | | SDP14 +--------+ + | | C o-----/-----oDevice14| + +--------+ 192.0.2.65/26 | +---+--+ +--------+ + | o------/ VLAN101 | | + | | | SDP13a+---+--+ | + |Device13| +------o| B +-------------+ + | o-------/-----o| | + +--------+ SDP13b+------+ + 198.51.100.65/26 + VLAN201 + + Figure 26: Example of A Hub and Spoke Slice Service + + Figure 27 shows an example YANG JSON data for the body of the hub- + spoke Network Slice Service instances request. + + ============== NOTE: '\' line wrapping per RFC 8792 =============== + + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "high-BW-template", + "description": "take the highest BW forwarding path" + }, + { + "id": "low-latency-template", + "description": \ + "lowest possible latency forwarding behavior" + } + ] + }, + "slice-service": [ + { + "id": "slice5", + "description": "example slice5", + "service-tags": { + "tag-type": [ + { + + + +Wu, et al. Expires 10 November 2025 [Page 86] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "tag-type": "service", + "tag-type-value": [ + "L3" + ] + } + ] + }, + "service-slo-sle-policy": { + "description": "video-policy", + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-bandwidth", + "metric-unit": "Mbps", + "bound": "1000" + }, + { + "metric-type": "two-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": "100" + } + ], + "availability": "three-nines", + "mtu": 1500 + } + }, + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "11", + "node-id": "PE-A", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix5", + "connection-group-sdp-role": \ + "ietf-vpn-common:spoke-role" + + + +Wu, et al. Expires 10 November 2025 [Page 87] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac11", + "description": "AC11 connected to device 11", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet5/0/0/2", + "ac-ipv4-address": "192.0.2.1", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + }, + { + "id": "12", + "node-id": "PE-A", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix5", + "connection-group-sdp-role": \ + "ietf-vpn-common:spoke-role" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac12", + + + +Wu, et al. Expires 10 November 2025 [Page 88] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "description": "AC12 connected to device 12", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet7/0/0/5", + "ac-ipv4-address": "198.51.100.1", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "200" + ] + } + ] + } + } + ] + } + }, + { + "id": "13a", + "node-id": "PE-B", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix5", + "connection-group-sdp-role": \ + "ietf-vpn-common:spoke-role" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac13a", + "description": "AC13a connected to device 13", + "ac-node-id": "PE-B", + "ac-tp-id": "GigabitEthernet8/0/0/6", + "ac-ipv4-address": "192.0.2.65", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + + + +Wu, et al. Expires 10 November 2025 [Page 89] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + { + "tag-type": "vlan-id", + "tag-type-value": [ + "101" + ] + } + ] + } + } + ] + } + }, + { + "id": "13b", + "node-id": "PE-B", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix5", + "connection-group-sdp-role": \ + "ietf-vpn-common:spoke-role" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac13b", + "description": "AC3b connected to device 13", + "ac-node-id": "PE-B", + "ac-tp-id": "GigabitEthernet8/0/0/4", + "ac-ipv4-address": "198.51.100.65", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "201" + ] + } + ] + + + +Wu, et al. Expires 10 November 2025 [Page 90] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + } + ] + } + }, + { + "id": "14", + "node-id": "PE-C", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix5", + "connection-group-sdp-role": \ + "ietf-vpn-common:hub-role" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac14", + "description": "AC14 connected to device 14", + "ac-node-id": "PE-C", + "ac-tp-id": "GigabitEthernet4/0/0/3", + "ac-ipv4-address": "192.0.2.129", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + } + ] + }, + + + +Wu, et al. Expires 10 November 2025 [Page 91] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "connection-groups": { + "connection-group": [ + { + "id": "matrix5", + "connectivity-type": "ietf-vpn-common:hub-spoke", + "connectivity-construct": [ + { + "id": "1", + "p2mp-sender-sdp": "14", + "p2mp-receiver-sdp": [ + "11", + "12", + "13a", + "13b" + ], + "service-slo-sle-policy": { + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": "10" + } + ] + } + } + } + ] + } + ] + } + } + ] + } + } + + + Figure 27: Example of a Message Body to Create A Hub and Spoke + Slice Service + +B.4. Example-4: An A2A Slice Service with Multiple SLOs and DSCP + Matching + + Figure 28 shows an example of a Network slice instance where the SDPs + are the customer-facing ports on the PE: + + Network Slice 6 on SDP21, SDP23a, and SDP24, with A2A connectivity + + + + +Wu, et al. Expires 10 November 2025 [Page 92] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + type. This is an L3 Slice Service that uses the uniform + "standard" slo-sle-template policies between all SDPs. For + traffic matching the DSCP of EF, a slo-sle-template policy of + "low-latency" will be used. The slice uses the explicit match + approach for mapping SDP traffic to a Connectivity Construct. + + In some use cases, the Slice Service may also need to map traffic + based on a combination of the DSCP and IP address, not DSCP only, + which is shown in the example of "service-match-criteria" + Figure 30. + + +--------+ 192.0.2.1/24 + | CE21 o------/ VLAN100 + +--------+ | SDP21+------+ + +------o| PE A +-------------+ + | | | + +---+--+ | + | | 203.0.113.1/24 + | +---+--+ VLAN100 + | | | SDP24 +--------+ + | | PE C o-----/-----o CE24 | + +--------+ 198.51.100.1/24 | +---+--+ +--------+ + | o------/ VLAN101 | | + | | | SDP23a+---+--+ | + |CE23 | +------o| PE B +-------------+ + | o | | + +--------+ +------+ + + Figure 28: Example of An A2A Slice Service with DSCP Matching + + Figure 29 shows an example YANG JSON data for the body of the Network + Slice Service instances request. + + ============== NOTE: '\' line wrapping per RFC 8792 =============== + + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "high-BW-template", + "description": "take the highest BW forwarding path" + }, + { + "id": "low-latency-template", + "description": \ + "lowest possible latency forwarding behavior" + }, + + + +Wu, et al. Expires 10 November 2025 [Page 93] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + { + "id": "standard-template", + "description": "take the standard forwarding path" + } + ] + }, + "slice-service": [ + { + "id": "slice6", + "description": "example slice6", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L3" + ] + } + ] + }, + "slo-sle-template": "standard-template", + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "21", + "node-id": "PE-A", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "dscp", + "dscp-value": [ + 46 + ] + } + ], + "target-connection-group-id": "matrix6", + "target-connectivity-construct-id": "2" + }, + { + "index": 2, + + + +Wu, et al. Expires 10 November 2025 [Page 94] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix6", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac21", + "description": "AC21 connected to device 21", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet5/0/0/0", + "ac-ipv4-address": "192.0.2.1", + "ac-ipv4-prefix-length": 24, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + }, + { + "id": "23a", + "node-id": "PE-B", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "dscp", + "dscp-value": [ + 46 + ] + } + ], + + + +Wu, et al. Expires 10 November 2025 [Page 95] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "target-connection-group-id": "matrix6", + "target-connectivity-construct-id": "2" + }, + { + "index": 2, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix6", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac23a", + "description": "AC23a connected to device 23", + "ac-node-id": "PE-B", + "ac-tp-id": "GigabitEthernet8/0/0/4", + "ac-ipv4-address": "198.51.100.1", + "ac-ipv4-prefix-length": 24, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "101" + ] + } + ] + } + } + ] + } + }, + { + "id": "24", + "node-id": "PE-C", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "dscp", + + + +Wu, et al. Expires 10 November 2025 [Page 96] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "dscp-value": [ + 46 + ] + } + ], + "target-connection-group-id": "matrix6", + "target-connectivity-construct-id": "2" + }, + { + "index": 2, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix6", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac24", + "description": "AC24 connected to device 24", + "ac-node-id": "PE-C", + "ac-tp-id": "GigabitEthernet4/0/0/3", + "ac-ipv4-address": "203.0.113.1", + "ac-ipv4-prefix-length": 24, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + } + ] + }, + "connection-groups": { + "connection-group": [ + { + + + +Wu, et al. Expires 10 November 2025 [Page 97] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "id": "matrix6", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": "1", + "a2a-sdp": [ + { + "sdp-id": "21" + }, + { + "sdp-id": "23a" + }, + { + "sdp-id": "24" + } + ] + }, + { + "id": "2", + "a2a-sdp": [ + { + "sdp-id": "21" + }, + { + "sdp-id": "23a" + }, + { + "sdp-id": "24", + "slo-sle-template": "low-latency-template" + } + ] + } + ] + } + ] + } + } + ] + } + } + + + Figure 29: Example of a Message Body to Create An A2A Slice + Service with DSCP Matching + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 98] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + Figure 30 shows an example of "service-match-criteria" with a + combination of both DSCP and IP Address for the Slice Service traffic + matching. + + { + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "dscp", + "dscp-value": [ + 46 + ] + }, + { + "type": " destination-ip-prefix", + "ip-prefix": [ + "192.0.2.254" + ] + } + ], + "target-connection-group-id": "matrix6", + "target-connectivity-construct-id": "2" + } + ] + } + } + + Figure 30: An Example of Match Criterion with Combination of DSCP + and IP Address Matching + +B.5. Example-5: An A2A Network Slice Service with SLO Precedence + Policies + + Figure 31 shows an example of a Network slice instance "slice-7" with + four SDPs: SDP1, SDP2, SDP3 and SDP4 with A2A connectivity type. All + SDPs are designated as customer-facing ports on the PE. + + The service is realized using a single A2A Connectivity Construct, + and a low-bandwidth "slo-sle-template" policy applied to SDP4 and + SDP3, while a high-bandwidth "slo-sle-template" policy applied to + SDP1 and SDP2. Notice that the "slo-sle-templates" at the + Connectivity Construct level takes precedence over the one specified + at the group level. + + + + + +Wu, et al. Expires 10 November 2025 [Page 99] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + +--------+ 2001:db8:0:1::1 2001:db8:0:3::1 + |CE1 o------/ VLAN100 VLAN100 + +--------+ | SDP1 +------+ +------+ SDP3 + +------o| PE A +---------|-PE C | +--------+ + | | | |-----/-----o CE3 | + +---+--+ +------+ +--------+ + | | + | | + | | + | | + +--------+ 2001:db8:0:2::1 | | + |CE2 o------/ VLAN100 | | 2001:db8:0:4::1 + +--------+ | SDP2 +---+--+ +---+--+ VLAN100 + +------o| PE B +---------|PE D | SDP4 +--------+ + | | | o-----/-----o CE4 | + +------+ +------+ +--------+ + + Figure 31: Example of An A2A Slice Service with SLO Precedence + + Figure 32 shows an example YANG JSON data for the body of the Network + Slice Service instances request. + + ============== NOTE: '\' line wrapping per RFC 8792 =============== + + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "high-BW-template", + "description": \ + "take the highest BW forwarding path" + }, + { + "id": "low-BW-template", + "description": "lowest BW forwarding behavior" + } + ] + }, + "slice-service": [ + { + "id": "slice-7", + "description": "Foo", + "service-tags": { + "tag-type": [ + { + "tag-type": "customer", + "tag-type-value": [ + + + +Wu, et al. Expires 10 November 2025 [Page 100] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "Customer-FOO" + ] + }, + { + "tag-type": "service", + "tag-type-value": [ + "L3" + ] + } + ] + }, + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "SDP1", + "description": "Central Office 1 at location PE-A", + "node-id": "PE-A", + "sdp-ip-address": [ + "2001:db8:0:1::1" + ], + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "vlan", + "vlan": [ + 100 + ] + } + ], + "target-connection-group-id": "matrix1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "AC-SDP1", + "description": "Device 1 to PE-A", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet1/0/0/0", + + + +Wu, et al. Expires 10 November 2025 [Page 101] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "ac-ipv6-address": "2001:db8:0:1::1", + "ac-ipv6-prefix-length": 64, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + }, + "incoming-qos-policy": { + "qos-policy-name": "QoS-Gold", + "rate-limits": { + "cir": "1000000", + "cbs": "1000", + "pir": "5000000", + "pbs": "1000" + } + } + } + ] + } + }, + { + "id": "SDP2", + "description": "Central Office 2 at location PE-B", + "node-id": "PE-B", + "sdp-ip-address": [ + "2001:db8:0:2::1" + ], + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "vlan", + "vlan": [ + 100 + ] + } + ], + "target-connection-group-id": "matrix1" + } + ] + }, + + + +Wu, et al. Expires 10 November 2025 [Page 102] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "AC-SDP2", + "description": "Device 2 to PE-B", + "ac-node-id": "PE-B", + "ac-tp-id": "GigabitEthernet2/0/0/0", + "ac-ipv6-address": "2001:db8:0:2::1", + "ac-ipv6-prefix-length": 64, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + }, + "incoming-qos-policy": { + "qos-policy-name": "QoS-Gold", + "rate-limits": { + "cir": "1000000", + "cbs": "1000", + "pir": "5000000", + "pbs": "1000" + } + } + } + ] + } + }, + { + "id": "SDP3", + "description": "Remote Office 1 at location PE-C", + "node-id": "PE-C", + "sdp-ip-address": [ + "2001:db8:0:3::1" + ], + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "vlan", + "vlan": [ + 100 + + + +Wu, et al. Expires 10 November 2025 [Page 103] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + ] + } + ], + "target-connection-group-id": "matrix1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "AC-SDP3", + "description": "Device 3 to PE-C", + "ac-node-id": "PE-C", + "ac-tp-id": "GigabitEthernet3/0/0/0", + "ac-ipv6-address": "2001:db8:0:3::1", + "ac-ipv6-prefix-length": 64, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + }, + "incoming-qos-policy": { + "qos-policy-name": "QoS-Gold", + "rate-limits": { + "cir": "1000000", + "cbs": "1000", + "pir": "5000000", + "pbs": "1000" + } + } + } + ] + } + }, + { + "id": "SDP4", + "description": "Remote Office 2 at location PE-D", + "node-id": "PE-D", + "sdp-ip-address": [ + "2001:db8:0:4::1" + ], + "service-match-criteria": { + "match-criterion": [ + + + +Wu, et al. Expires 10 November 2025 [Page 104] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + { + "index": 1, + "match-type": [ + { + "type": "vlan", + "vlan": [ + 100 + ] + } + ], + "target-connection-group-id": "matrix1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "AC-SDP4", + "description": "Device 4 to PE-D", + "ac-node-id": "PE-A", + "ac-tp-id": "GigabitEthernet4/0/0/0", + "ac-ipv6-address": "2001:db8:0:4::1", + "ac-ipv6-prefix-length": 64, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + }, + "incoming-qos-policy": { + "qos-policy-name": "QoS-Gold", + "rate-limits": { + "cir": "1000000", + "cbs": "1000", + "pir": "5000000", + "pbs": "1000" + } + } + } + ] + } + } + ] + }, + + + +Wu, et al. Expires 10 November 2025 [Page 105] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "connection-groups": { + "connection-group": [ + { + "id": "matrix1", + "slo-sle-template": "low-BW-template", + "connectivity-construct": [ + { + "id": "1", + "a2a-sdp": [ + { + "sdp-id": "SDP1", + "slo-sle-template": "high-BW-template" + }, + { + "sdp-id": "SDP2", + "slo-sle-template": "high-BW-template" + }, + { + "sdp-id": "SDP3" + }, + { + "sdp-id": "SDP4" + } + ] + } + ] + } + ] + } + } + ] + } + } + + + Figure 32: Example of a Message Body to Create an A2A Slice + Service with SLO Precedence + +B.6. Example-6: SDP at CE, L3 A2A Slice Service + + Figure 33 shows an example of one Network slice instance where the + SDPs are located at the PE-facing ports on the CE: + + * Network Slice 8 with SDP31 on CE Device1, SDP33 (with two ACs) on + Device 3 and SDP34 on Device 4, with an A2A connectivity type. + This is an L3 Slice Service that uses the uniform low-latency slo- + sle-template policy between all SDPs. + + + + +Wu, et al. Expires 10 November 2025 [Page 106] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + * This example also introduces the optional attribute of "sdp-ip", + which could be a loopback interface on the device. How this "sdp- + ip" is used by the NSC is out-of-scope here, but, for example, + this could be the management interface of the device. The SDP and + AC details are from the perspective of the CE in this example. + How the CE ACs are mapped to the PE ACs is up to the NSC + implementation and out-of-scope in this example. + + SDP31 AC "id"=ac31, "node-id"=Device1, interface: GigabitEthernet0 + vlan 100 + + SDP33 AC "id"=ac33a, "node-id"=Device3, interface: + GigabitEthernet0 vlan 101 + + SDP33 AC "id"=ac33b, "node-id"=Device3, interface: + GigabitEthernet1 vlan 201 + + SDP34 AC "id"=ac34, "node-id"=Device4, interface: GigabitEthernet3 + vlan 100 + + SDP31 + SDP-ip 203.0.113.1 + (Loopback) + | + | 192.0.2.2/26 + v VLAN100 +------+ + +--------+ ac31 | PE A +-------------+ + |Device1 o-------/-----o| | | SDP34 + +--------+ +---+--+ | SDP-ip 203.0.113.129 + | | | + SDP33 | | | + SDP-ip 203.0.113.65 | +---+--+ v + | 192.0.2.66/26 | | | +--------+ + v VLAN101 | | PE C o-----/-----o Device4| + +--------+ ac33a | +---+--+ ac34 +--------+ + | o------/ | | VLAN100 + | | | +---+---+ | 198.51.100.66/26 + |Device3 | +------o| PE B +------------+ + | o-------/-----o| | + +--------+ ac33b +-------+ + VLAN201 + 198.51.100.2/26 + + Figure 33: Example of an A2A Slice Service with CE Based SDP + + Figure 34 shows an example YANG JSON data for the body of the Network + Slice Service instances request. + + + + +Wu, et al. Expires 10 November 2025 [Page 107] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + ============== NOTE: '\' line wrapping per RFC 8792 =============== + + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "high-BW-template", + "description": \ + "take the highest BW forwarding path" + }, + { + "id": "low-latency-template", + "description": \ + "lowest possible latency forwarding behavior" + } + ] + }, + "slice-service": [ + { + "id": "slice8", + "description": "slice-8", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L3" + ] + } + ] + }, + "slo-sle-template": "low-latency-template", + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "31", + "node-id": "Device-1", + "sdp-ip-address": [ + "203.0.113.1" + ], + "service-match-criteria": { + "match-criterion": [ + + + +Wu, et al. Expires 10 November 2025 [Page 108] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix1", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac31", + "description": "AC1 connected to PE-A", + "ac-node-id": "Device-1", + "ac-tp-id": "GigabitEthernet0", + "ac-ipv4-address": "192.0.2.2", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + }, + { + "id": "33", + "node-id": "Device-3", + "sdp-ip-address": [ + "203.0.113.65" + ], + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + + + +Wu, et al. Expires 10 November 2025 [Page 109] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + } + ], + "target-connection-group-id": "matrix1", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac33a", + "description": "AC33a connected to PE-B", + "ac-node-id": "Device-3", + "ac-tp-id": "GigabitEthernet0", + "ac-ipv4-address": "192.0.2.66", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "101" + ] + } + ] + } + }, + { + "id": "ac33b", + "description": "AC33b connected to PE-B", + "ac-node-id": "Device-3", + "ac-tp-id": "GigabitEthernet1", + "ac-ipv4-address": "198.51.100.2", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "201" + ] + } + ] + } + } + ] + } + }, + + + +Wu, et al. Expires 10 November 2025 [Page 110] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + { + "id": "34", + "node-id": "Device-4", + "sdp-ip-address": [ + "203.0.113.129" + ], + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix1", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac34", + "description": "AC34 connected to PE-C", + "ac-node-id": "Device-4", + "ac-tp-id": "GigabitEthernet3", + "ac-ipv4-address": "198.51.100.66", + "ac-ipv4-prefix-length": 26, + "ac-tags": { + "ac-tag": [ + { + "tag-type": "vlan-id", + "tag-type-value": [ + "100" + ] + } + ] + } + } + ] + } + } + ] + }, + "connection-groups": { + "connection-group": [ + { + + + +Wu, et al. Expires 10 November 2025 [Page 111] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "id": "matrix1", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": "1", + "a2a-sdp": [ + { + "sdp-id": "31" + }, + { + "sdp-id": "33" + }, + { + "sdp-id": "34" + } + ] + } + ] + } + ] + } + } + ] + } + } + + + Figure 34: Example of a Message Body to Create an CE based A2A + Slice Services + +B.7. Example-7: SDP at CE, L3 A2A Slice Service with Network + Abstraction + + Figure 35 shows an example of one Network slice instance where the + SDPs are located at the PE-facing ports on the CE. + + In this example, it is assumed that the NSC already has circuit + binding details between the CE and PE which were previously assigned + (method is out-of-scope) or the NSC has mechanisms to determine this + mapping. While the NSC capabilities are out-of-scope of this + document, the NSC may use the CE device name, "sdp-id", "sdp-ip", AC + "id" or the "peer-sap-id" to complete this AC circuit binding. + + This example introduces the "peer-sap-id", which in this case, is an + operator provided identifier that the slice requester can use for the + NSC to identify the service attachment point (saps) in an abstracted + way. How the NSC uses the "peer-sap-id" is out of scope of this + document, but a possible implementation would be that the NSC was + + + +Wu, et al. Expires 10 November 2025 [Page 112] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + previously provisioned with a "peer-sap-id" to PE device/interface/ + VLAN mapping table. Alternatively, the NSC can request this mapping + from an external database. + + * Network Slice 9 with SDP31 on CPE Device1, SDP33 (with two ACs) on + Device 3 and SDP34 on Device 4, with an A2A connectivity type. + This is an L3 Slice Service that uses the uniform low-latency slo- + sle-template policy between all SDPs. + + SDP31 AC "id"=ac31, "node-id"=Device1, "peer-sap-id"= foo.com- + circuitID-12345 + + SDP33 AC "id"=ac33a, "node-id"=Device3, "peer-sap-id"=foo.com- + circuitID-67890 + + SDP33 AC "id"=ac33b, "node-id"=Device3, "peer-sap-id"=foo.com- + circuitID-54321ABC + + SDP34 AC "id"=ac34, "node-id"=Device4, "peer-sap-id"=foo.com- + circuitID-9876 + + SDP31 + 2001:db8:0:1::1 + (Loopback,etc) + | + | + v +-----------------------+ + +--------+ ac31 | | + |Device1 o-------/-----o|sap | SDP34 + +--------+ | | 2001:db8:0:3::1 + | Abstracted | | + SDP33 | Provider Network | | + 2001:db8:0:2::1 | | v + | | | +--------+ + v | sap|-----/-----o Device4| + +--------+ ac33a | | ac41 +--------+ + | o------/ | | + | | | | | + |Device3 | +------o|sap | + | o-------/-----o|sap | + +--------+ ac33b +-----------------------+ + + Figure 35: Example of a Message Body to Create an A2A CE Based + Slice Service with Abstraction + + Figure 36 shows an example YANG JSON data for the body of the Network + Slice Service instances request. + + + + +Wu, et al. Expires 10 November 2025 [Page 113] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + ============== NOTE: '\' line wrapping per RFC 8792 =============== + + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "high-BW-template", + "description": "take the highest BW forwarding path" + }, + { + "id": "low-latency-template", + "description": \ + "lowest possible latency forwarding behavior" + } + ] + }, + "slice-service": [ + { + "id": "slice-9", + "description": "example slice7", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L3" + ] + } + ] + }, + "slo-sle-template": "low-latency-template", + "status": { + "admin-status": { + "status": "ietf-vpn-common:admin-up" + } + }, + "sdps": { + "sdp": [ + { + "id": "31", + "node-id": "Device-1", + "sdp-ip-address": [ + "2001:db8:0:1::1" + ], + "service-match-criteria": { + "match-criterion": [ + { + + + +Wu, et al. Expires 10 November 2025 [Page 114] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac31", + "sdp-peering": { + "peer-sap-id": "foo.com-circuitID-12345" + } + } + ] + } + }, + { + "id": "33", + "node-id": "Device-3", + "sdp-ip-address": [ + "2001:db8:0:2::1" + ], + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix1", + "target-connectivity-construct-id": "1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac33a", + "sdp-peering": { + "peer-sap-id": "foo.com-circuitID-67890" + } + + + +Wu, et al. Expires 10 November 2025 [Page 115] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + }, + { + "id": "ac33b", + "sdp-peering": { + "peer-sap-id": "foo.com-circuitID-54321ABC" + } + } + ] + } + }, + { + "id": "34", + "node-id": "Device-4", + "sdp-ip-address": [ + "2001:db8:0:3::1" + ], + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": [ + { + "type": "any" + } + ], + "target-connection-group-id": "matrix1" + } + ] + }, + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "ac34", + "sdp-peering": { + "peer-sap-id": "foo.com-circuitID-9876" + } + } + ] + } + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "matrix1", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + + + +Wu, et al. Expires 10 November 2025 [Page 116] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + { + "id": "1", + "a2a-sdp": [ + { + "sdp-id": "31" + }, + { + "sdp-id": "33" + }, + { + "sdp-id": "34" + } + ] + } + ] + } + ] + } + } + ] + } + } + + + Figure 36: Example of a Message Body to Create an A2A Slice + Service with Abstraction + +Appendix C. Complete Model Tree Structure + + module: ietf-network-slice-service + +--rw network-slice-services + +--rw slo-sle-templates + | +--rw slo-sle-template* [id] + | +--rw id string + | +--rw description? string + | +--rw template-ref? slice-template-ref + | +--rw slo-policy + | | +--rw metric-bound* [metric-type] + | | | +--rw metric-type identityref + | | | +--rw metric-unit string + | | | +--rw value-description? string + | | | +--rw percentile-value? percentile + | | | +--rw bound? uint64 + | | +--rw availability? identityref + | | +--rw mtu? uint32 + | +--rw sle-policy + | +--rw security* identityref + | +--rw isolation* identityref + + + +Wu, et al. Expires 10 November 2025 [Page 117] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + | +--rw max-occupancy-level? uint8 + | +--rw path-constraints + | +--rw service-functions + | +--rw diversity + | +--rw diversity-type? + | te-types:te-path-disjointness + +--rw slice-service* [id] + +--rw id string + +--rw description? string + +--rw service-tags + | +--rw tag-type* [tag-type] + | +--rw tag-type identityref + | +--rw tag-type-value* string + +--rw (slo-sle-policy)? + | +--:(standard) + | | +--rw slo-sle-template? slice-template-ref + | +--:(custom) + | +--rw service-slo-sle-policy + | +--rw description? string + | +--rw slo-policy + | | +--rw metric-bound* [metric-type] + | | | +--rw metric-type identityref + | | | +--rw metric-unit string + | | | +--rw value-description? string + | | | +--rw percentile-value? percentile + | | | +--rw bound? uint64 + | | +--rw availability? identityref + | | +--rw mtu? uint32 + | +--rw sle-policy + | +--rw security* identityref + | +--rw isolation* identityref + | +--rw max-occupancy-level? uint8 + | +--rw path-constraints + | +--rw service-functions + | +--rw diversity + | +--rw diversity-type? + | te-types:te-path-disjointness + +--rw test-only? empty + +--rw status + | +--rw admin-status + | | +--rw status? identityref + | | +--ro last-change? yang:date-and-time + | +--ro oper-status + | +--ro status? identityref + | +--ro last-change? yang:date-and-time + +--rw sdps + | +--rw sdp* [id] + | +--rw id string + + + +Wu, et al. Expires 10 November 2025 [Page 118] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + | +--rw description? string + | +--rw geo-location + | | +--rw reference-frame + | | | +--rw alternate-system? string + | | | | {alternate-systems}? + | | | +--rw astronomical-body? string + | | | +--rw geodetic-system + | | | +--rw geodetic-datum? string + | | | +--rw coord-accuracy? decimal64 + | | | +--rw height-accuracy? decimal64 + | | +--rw (location)? + | | | +--:(ellipsoid) + | | | | +--rw latitude? decimal64 + | | | | +--rw longitude? decimal64 + | | | | +--rw height? decimal64 + | | | +--:(cartesian) + | | | +--rw x? decimal64 + | | | +--rw y? decimal64 + | | | +--rw z? decimal64 + | | +--rw velocity + | | | +--rw v-north? decimal64 + | | | +--rw v-east? decimal64 + | | | +--rw v-up? decimal64 + | | +--rw timestamp? yang:date-and-time + | | +--rw valid-until? yang:date-and-time + | +--rw node-id? string + | +--rw sdp-ip-address* inet:ip-address + | +--rw tp-ref? leafref + | +--rw service-match-criteria + | | +--rw match-criterion* [index] + | | +--rw index uint32 + | | +--rw match-type* [type] + | | | +--rw type identityref + | | | +--rw (value)? + | | | +--:(interface) + | | | | +--rw interface-name* string + | | | +--:(vlan) + | | | | +--rw vlan* uint16 + | | | +--:(label) + | | | | +--rw label* + | | | | rt-types:mpls-label + | | | +--:(ip-prefix) + | | | | +--rw ip-prefix* inet:ip-prefix + | | | +--:(dscp) + | | | | +--rw dscp* inet:dscp + | | | +--:(acl) + | | | +--rw acl-name* string + | | +--rw target-connection-group-id leafref + + + +Wu, et al. Expires 10 November 2025 [Page 119] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + | | +--rw connection-group-sdp-role? + | | | identityref + | | +--rw target-connectivity-construct-id? leafref + | +--rw incoming-qos-policy + | | +--rw qos-policy-name? string + | | +--rw rate-limits + | | +--rw cir? uint64 + | | +--rw cbs? uint64 + | | +--rw eir? uint64 + | | +--rw ebs? uint64 + | | +--rw pir? uint64 + | | +--rw pbs? uint64 + | | +--rw classes + | | +--rw cos* [cos-id] + | | +--rw cos-id uint8 + | | +--rw cir? uint64 + | | +--rw cbs? uint64 + | | +--rw eir? uint64 + | | +--rw ebs? uint64 + | | +--rw pir? uint64 + | | +--rw pbs? uint64 + | +--rw outgoing-qos-policy + | | +--rw qos-policy-name? string + | | +--rw rate-limits + | | +--rw cir? uint64 + | | +--rw cbs? uint64 + | | +--rw eir? uint64 + | | +--rw ebs? uint64 + | | +--rw pir? uint64 + | | +--rw pbs? uint64 + | | +--rw classes + | | +--rw cos* [cos-id] + | | +--rw cos-id uint8 + | | +--rw cir? uint64 + | | +--rw cbs? uint64 + | | +--rw eir? uint64 + | | +--rw ebs? uint64 + | | +--rw pir? uint64 + | | +--rw pbs? uint64 + | +--rw sdp-peering + | | +--rw peer-sap-id* string + | | +--rw protocols + | +--rw ac-svc-ref* + | | ac-svc:attachment-circuit-reference + | +--rw ce-mode? boolean + | +--rw attachment-circuits + | | +--rw attachment-circuit* [id] + | | +--rw id string + + + +Wu, et al. Expires 10 November 2025 [Page 120] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + | | +--rw description? string + | | +--rw ac-svc-ref? + | | | ac-svc:attachment-circuit-reference + | | +--rw ac-node-id? string + | | +--rw ac-tp-id? string + | | +--rw ac-ipv4-address? inet:ipv4-address + | | +--rw ac-ipv4-prefix-length? uint8 + | | +--rw ac-ipv6-address? inet:ipv6-address + | | +--rw ac-ipv6-prefix-length? uint8 + | | +--rw mtu? uint32 + | | +--rw ac-tags + | | | +--rw ac-tag* [tag-type] + | | | +--rw tag-type identityref + | | | +--rw tag-type-value* string + | | +--rw incoming-qos-policy + | | | +--rw qos-policy-name? string + | | | +--rw rate-limits + | | | +--rw cir? uint64 + | | | +--rw cbs? uint64 + | | | +--rw eir? uint64 + | | | +--rw ebs? uint64 + | | | +--rw pir? uint64 + | | | +--rw pbs? uint64 + | | | +--rw classes + | | | +--rw cos* [cos-id] + | | | +--rw cos-id uint8 + | | | +--rw cir? uint64 + | | | +--rw cbs? uint64 + | | | +--rw eir? uint64 + | | | +--rw ebs? uint64 + | | | +--rw pir? uint64 + | | | +--rw pbs? uint64 + | | +--rw outgoing-qos-policy + | | | +--rw qos-policy-name? string + | | | +--rw rate-limits + | | | +--rw cir? uint64 + | | | +--rw cbs? uint64 + | | | +--rw eir? uint64 + | | | +--rw ebs? uint64 + | | | +--rw pir? uint64 + | | | +--rw pbs? uint64 + | | | +--rw classes + | | | +--rw cos* [cos-id] + | | | +--rw cos-id uint8 + | | | +--rw cir? uint64 + | | | +--rw cbs? uint64 + | | | +--rw eir? uint64 + | | | +--rw ebs? uint64 + + + +Wu, et al. Expires 10 November 2025 [Page 121] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + | | | +--rw pir? uint64 + | | | +--rw pbs? uint64 + | | +--rw sdp-peering + | | | +--rw peer-sap-id? string + | | | +--rw protocols + | | +--rw status + | | +--rw admin-status + | | | +--rw status? identityref + | | | +--ro last-change? yang:date-and-time + | | +--ro oper-status + | | +--ro status? identityref + | | +--ro last-change? yang:date-and-time + | +--rw status + | | +--rw admin-status + | | | +--rw status? identityref + | | | +--ro last-change? yang:date-and-time + | | +--ro oper-status + | | +--ro status? identityref + | | +--ro last-change? yang:date-and-time + | +--ro sdp-monitoring + | +--ro incoming-bw-value? yang:gauge64 + | +--ro incoming-bw-percent? percentage + | +--ro outgoing-bw-value? yang:gauge64 + | +--ro outgoing-bw-percent? percentage + +--rw connection-groups + | +--rw connection-group* [id] + | +--rw id string + | +--rw connectivity-type? identityref + | +--rw (slo-sle-policy)? + | | +--:(standard) + | | | +--rw slo-sle-template? + | | | slice-template-ref + | | +--:(custom) + | | +--rw service-slo-sle-policy + | | +--rw description? string + | | +--rw slo-policy + | | | +--rw metric-bound* [metric-type] + | | | | +--rw metric-type identityref + | | | | +--rw metric-unit string + | | | | +--rw value-description? string + | | | | +--rw percentile-value? percentile + | | | | +--rw bound? uint64 + | | | +--rw availability? identityref + | | | +--rw mtu? uint32 + | | +--rw sle-policy + | | +--rw security* identityref + | | +--rw isolation* identityref + | | +--rw max-occupancy-level? uint8 + + + +Wu, et al. Expires 10 November 2025 [Page 122] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + | | +--rw path-constraints + | | +--rw service-functions + | | +--rw diversity + | | +--rw diversity-type? + | | te-types:te-path-disjointness + | +--rw service-slo-sle-policy-override? identityref + | +--rw connectivity-construct* [id] + | | +--rw id string + | | +--rw (type)? + | | | +--:(p2p) + | | | | +--rw p2p-sender-sdp? + | | | | | -> ../../../../sdps/sdp/id + | | | | +--rw p2p-receiver-sdp? + | | | | -> ../../../../sdps/sdp/id + | | | +--:(p2mp) + | | | | +--rw p2mp-sender-sdp? + | | | | | -> ../../../../sdps/sdp/id + | | | | +--rw p2mp-receiver-sdp* + | | | | -> ../../../../sdps/sdp/id + | | | +--:(a2a) + | | | +--rw a2a-sdp* [sdp-id] + | | | +--rw sdp-id + | | | | -> ../../../../../sdps/sdp/id + | | | +--rw (slo-sle-policy)? + | | | +--:(standard) + | | | | +--rw slo-sle-template? + | | | | slice-template-ref + | | | +--:(custom) + | | | +--rw service-slo-sle-policy + | | | +--rw description? string + | | | +--rw slo-policy + | | | | +--rw metric-bound* + | | | | | [metric-type] + | | | | | +--rw metric-type + | | | | | | identityref + | | | | | +--rw metric-unit + | | | | | | string + | | | | | +--rw value-description? + | | | | | | string + | | | | | +--rw percentile-value? + | | | | | | percentile + | | | | | +--rw bound? + | | | | | uint64 + | | | | +--rw availability? + | | | | | identityref + | | | | +--rw mtu? uint32 + | | | +--rw sle-policy + | | | +--rw security* + + + +Wu, et al. Expires 10 November 2025 [Page 123] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + | | | | identityref + | | | +--rw isolation* + | | | | identityref + | | | +--rw max-occupancy-level? + | | | | uint8 + | | | +--rw path-constraints + | | | +--rw service-functions + | | | +--rw diversity + | | | +--rw diversity-type? + | | | te-types: + te-path-disjointness + | | +--rw (slo-sle-policy)? + | | | +--:(standard) + | | | | +--rw slo-sle-template? + | | | | slice-template-ref + | | | +--:(custom) + | | | +--rw service-slo-sle-policy + | | | +--rw description? string + | | | +--rw slo-policy + | | | | +--rw metric-bound* [metric-type] + | | | | | +--rw metric-type + | | | | | | identityref + | | | | | +--rw metric-unit string + | | | | | +--rw value-description? string + | | | | | +--rw percentile-value? percentile + | | | | | +--rw bound? uint64 + | | | | +--rw availability? identityref + | | | | +--rw mtu? uint32 + | | | +--rw sle-policy + | | | +--rw security* identityref + | | | +--rw isolation* identityref + | | | +--rw max-occupancy-level? uint8 + | | | +--rw path-constraints + | | | +--rw service-functions + | | | +--rw diversity + | | | +--rw diversity-type? + | | | te-types: + te-path-disjointness + | | +--rw service-slo-sle-policy-override? + | | | identityref + | | +--rw status + | | | +--rw admin-status + | | | | +--rw status? identityref + | | | | +--ro last-change? yang:date-and-time + | | | +--ro oper-status + | | | +--ro status? identityref + | | | +--ro last-change? yang:date-and-time + | | +--ro connectivity-construct-monitoring + + + +Wu, et al. Expires 10 November 2025 [Page 124] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + | | +--ro one-way-min-delay? yang:gauge64 + | | +--ro one-way-max-delay? yang:gauge64 + | | +--ro one-way-delay-variation? yang:gauge64 + | | +--ro one-way-packet-loss? decimal64 + | | +--ro two-way-min-delay? yang:gauge64 + | | +--ro two-way-max-delay? yang:gauge64 + | | +--ro two-way-delay-variation? yang:gauge64 + | | +--ro two-way-packet-loss? decimal64 + | +--ro connection-group-monitoring + | +--ro one-way-min-delay? yang:gauge64 + | +--ro one-way-max-delay? yang:gauge64 + | +--ro one-way-delay-variation? yang:gauge64 + | +--ro one-way-packet-loss? decimal64 + | +--ro two-way-min-delay? yang:gauge64 + | +--ro two-way-max-delay? yang:gauge64 + | +--ro two-way-delay-variation? yang:gauge64 + | +--ro two-way-packet-loss? decimal64 + +--rw custom-topology + +--rw network-ref? -> /nw:networks/network/network-id + +Appendix D. Comparison with the Design Choice of ACTN VN Model + Augmentation + + The difference between the ACTN VN model and the Network Slice + Service requirements is that the Network Slice Service interface is a + technology-agnostic interface, whereas the VN model is bound to the + TE Topologies. The realization of the Network Slice does not + necessarily require the slice network to support the TE technology. + + The ACTN VN (Virtual Network) model introduced in [RFC9731] is the + abstract customer view of the TE network. Its YANG structure + includes four components: + + * VN: A Virtual Network (VN) is a network provided by a service + provider to a customer for use and two types of VN have been + defined. The Type 1 VN can be seen as a set of edge-to-edge + abstract links. Each link is an abstraction of the underlying + network which can encompass edge points of the customer's network, + access links, intra-domain paths, and inter-domain links. + + * AP: An AP is a logical identifier used to identify the access link + which is shared between the customer and the IETF scoped Network. + + * VN-AP: A VN-AP is a logical binding between an AP and a given VN. + + * VN-member: A VN-member is an abstract edge-to-edge link between + any two APs or VN-APs. Each link is formed as an E2E tunnel + across the underlying networks. + + + +Wu, et al. Expires 10 November 2025 [Page 125] + +Internet-Draft Network Slice Service YANG Model May 2025 + + + The Type 1 VN can be used to describe Network Slice Service + connection requirements. However, the Network Slice SLOs and Network + Slice SDPs are not clearly defined and there's no direct equivalent. + For example, the SLO requirement of the VN is defined through the TE + Topologies YANG model, but the TE Topologies model is related to a + specific implementation technology. Also, VN-AP does not define + "service-match-criteria" to specify a specific SDP belonging to a + Network Slice Service. + +Authors' Addresses + + Bo Wu + Huawei Technologies + 101 Software Avenue, Yuhua District + Nanjing + Jiangsu, 210012 + China + Email: lana.wubo@huawei.com + + + Dhruv Dhody + Huawei Technologies + Divyashree Techno Park + Bangalore 560066 + Karnataka + India + Email: dhruv.ietf@gmail.com + + + Reza Rokui + Ciena + Email: rrokui@ciena.com + + + Tarek Saad + Cisco Systems, Inc + Email: tsaad@cisco.com + + + John Mullooly + Cisco Systems, Inc + Email: jmullool@cisco.com + + + + + + + + + +Wu, et al. Expires 10 November 2025 [Page 126] diff --git a/doc/NSC/examples/ietf_green_request.json b/doc/NSC/examples/ietf_green_request.json new file mode 100644 index 0000000..9430f28 --- /dev/null +++ b/doc/NSC/examples/ietf_green_request.json @@ -0,0 +1,172 @@ +{ + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "B", + "description": "", + "slo-policy": { + "metric-bound": [ + { + "metric-type": "energy_consumption", + "metric-unit": "kWh", + "bound": 20200 + }, + { + "metric-type": "energy_efficiency", + "metric-unit": "Wats/bps", + "bound": 6 + }, + { + "metric-type": "carbon_emission", + "metric-unit": "grams of CO2 per kWh", + "bound": 750 + }, + { + "metric-type": "renewable_energy_usage", + "metric-unit": "rate", + "bound": 0.5 + } + ] + }, + "sle-policy": { + "security": "", + "isolation": "", + "path-constraints": { + "service-functions": "", + "diversity": { + "diversity": { + "diversity-type": "" + } + } + } + } + } + ] + }, + "slice-service": [ + { + "id": "slice-service-88a585f7-a432-4312-8774-6210fb0b2342", + "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L2" + ] + } + ] + }, + "slo-sle-policy": { + "slo-sle-template": "B" + }, + "status": {}, + "sdps": { + "sdp": [ + { + "id": "CU-N32", + "geo-location": "", + "node-id": "A", + "sdp-ip-address": "10.60.11.3", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "101", + "target-connection-group-id": "A_B" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "100", + "ac-ipv4-address": "10.60.11.3", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "4.4.4.4" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + }, + { + "id": "UPF-N32", + "geo-location": "", + "node-id": "B", + "sdp-ip-address": "10.60.10.6", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "101", + "target-connection-group-id": "A_B" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "200", + "ac-ipv4-address": "10.60.10.6", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "5.5.5.5" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "A_B", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": 1, + "a2a-sdp": [ + { + "sdp-id": "CU-N32" + }, + { + "sdp-id": "UPF-N32" + } + ] + } + ], + "status": {} + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/doc/NSC/examples/slice_request_backhaul_control.json b/doc/NSC/examples/slice_request_backhaul_control.json new file mode 100644 index 0000000..f215078 --- /dev/null +++ b/doc/NSC/examples/slice_request_backhaul_control.json @@ -0,0 +1,162 @@ +{ + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "A", + "description": "", + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-bandwidth", + "metric-unit": "kbps", + "bound": 2000 + }, + { + "metric-type": "one-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": 5 + } + ] + }, + "sle-policy": { + "security": "", + "isolation": "", + "path-constraints": { + "service-functions": "", + "diversity": { + "diversity": { + "diversity-type": "" + } + } + } + } + } + ] + }, + "slice-service": [ + { + "id": "slice-service-11327140-7361-41b3-aa45-e84a7fb40be9", + "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L2" + ] + } + ] + }, + "slo-sle-policy": { + "slo-sle-template": "A" + }, + "status": {}, + "sdps": { + "sdp": [ + { + "id": "", + "geo-location": "", + "node-id": "CU-N2", + "sdp-ip-address": "10.60.11.3", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "100", + "target-connection-group-id": "CU-N2_AMF-N2" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "100", + "ac-ipv4-address": "10.60.11.3", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "1.1.1.1" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + }, + { + "id": "", + "geo-location": "", + "node-id": "AMF-N2", + "sdp-ip-address": "10.60.60.105", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "100", + "target-connection-group-id": "CU-N2_AMF-N2" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "200", + "ac-ipv4-address": "10.60.60.105", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "3.3.3.3" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "CU-N2_AMF-N2", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": 1, + "a2a-sdp": [ + { + "sdp-id": "01" + }, + { + "sdp-id": "02" + } + ] + } + ], + "status": {} + } + ] + } + } + ] + } + } \ No newline at end of file diff --git a/doc/NSC/examples/slice_request_backhaul_user.json b/doc/NSC/examples/slice_request_backhaul_user.json new file mode 100644 index 0000000..efd1666 --- /dev/null +++ b/doc/NSC/examples/slice_request_backhaul_user.json @@ -0,0 +1,164 @@ +[ + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "C", + "description": "", + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-bandwidth", + "metric-unit": "kbps", + "bound": 100 + }, + { + "metric-type": "one-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": 10 + } + ] + }, + "sle-policy": { + "security": "", + "isolation": "", + "path-constraints": { + "service-functions": "", + "diversity": { + "diversity": { + "diversity-type": "" + } + } + } + } + } + ] + }, + "slice-service": [ + { + "id": "slice-service-181e303a-a051-42e5-b2f2-4060732c631f", + "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", + "service-tags": { + "tag-type": [ + { + "tag-type": "", + "tag-type-value": [ + "" + ] + } + ] + }, + "slo-sle-policy": { + "slo-sle-template": "C" + }, + "status": {}, + "sdps": { + "sdp": [ + { + "id": "", + "geo-location": "", + "node-id": "CU-N31", + "sdp-ip-address": "10.60.11.3", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "102", + "target-connection-group-id": "CU-N31_UPF-N31" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "100", + "ac-ipv4-address": "10.60.11.3", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "4.4.4.4" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + }, + { + "id": "", + "geo-location": "", + "node-id": "UPF-N31", + "sdp-ip-address": "10.60.60.106", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "102", + "target-connection-group-id": "CU-N31_UPF-N31" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "200", + "ac-ipv4-address": "10.60.60.106", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "5.5.5.5" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "CU-N31_UPF-N31", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": 1, + "a2a-sdp": [ + { + "sdp-id": "01" + }, + { + "sdp-id": "02" + } + ] + } + ], + "status": {} + } + ] + } + } + ] + } + } +] \ No newline at end of file diff --git a/doc/NSC/nsc_swagger.json b/doc/NSC/nsc_swagger.json new file mode 100644 index 0000000..b65ca34 --- /dev/null +++ b/doc/NSC/nsc_swagger.json @@ -0,0 +1,971 @@ +{ + "swagger": "2.0", + "basePath": "/", + "paths": { + "/e2e/slice": { + "post": { + "responses": { + "500": { + "description": "Internal server error" + }, + "400": { + "description": "Invalid request format" + }, + "200": { + "description": "No service to process." + }, + "201": { + "description": "Slice created successfully", + "schema": { + "$ref": "#/definitions/SliceResponse" + } + } + }, + "summary": "Submit a new slice request with a file", + "description": "This endpoint allows clients to submit transport network slice requests using a JSON payload.", + "operationId": "post_e2_e_slice_list", + "parameters": [ + { + "name": "file", + "in": "formData", + "type": "file", + "description": "File to upload" + }, + { + "name": "json_data", + "in": "formData", + "type": "string", + "description": "JSON Data in string format" + } + ], + "consumes": [ + "multipart/form-data" + ], + "tags": [ + "E2E" + ] + }, + "delete": { + "responses": { + "500": { + "description": "Internal server error" + }, + "204": { + "description": "All transport network slices deleted successfully." + } + }, + "summary": "Delete all slices", + "description": "Deletes all transport network slices from the slice controller.", + "operationId": "delete_e2_e_slice_list", + "tags": [ + "E2E" + ] + }, + "get": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slices not found" + }, + "200": { + "description": "Slices returned", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + }, + "summary": "Retrieve all slices", + "description": "Returns all transport network slices from the slice controller.", + "operationId": "get_e2_e_slice_list", + "tags": [ + "E2E" + ] + } + }, + "/e2e/slice/{slice_id}": { + "parameters": [ + { + "description": "The ID of the slice to retrieve or modify", + "name": "slice_id", + "in": "path", + "required": true, + "type": "string" + } + ], + "delete": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "204": { + "description": "Transport network slice deleted successfully." + } + }, + "summary": "Delete a slice", + "description": "Deletes a specific transport network slice from the slice controller based on the provided `slice_id`.", + "operationId": "delete_e2_e_slice", + "tags": [ + "E2E" + ] + }, + "get": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "200": { + "description": "Slice returned", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + }, + "summary": "Retrieve a specific slice", + "description": "Returns specific information related to a slice by providing its id", + "operationId": "get_e2_e_slice", + "tags": [ + "E2E" + ] + }, + "put": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "200": { + "description": "Slice modified", + "schema": { + "$ref": "#/definitions/SliceResponse" + } + } + }, + "summary": "Modify a slice", + "description": "Returns a specific slice that has been modified", + "operationId": "put_e2_e_slice", + "parameters": [ + { + "name": "payload", + "required": true, + "in": "body", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + ], + "tags": [ + "E2E" + ] + } + }, + "/ixia/slice": { + "post": { + "responses": { + "500": { + "description": "Internal server error" + }, + "400": { + "description": "Invalid request format" + }, + "200": { + "description": "No service to process." + }, + "201": { + "description": "Slice created successfully", + "schema": { + "$ref": "#/definitions/SliceResponse" + } + } + }, + "summary": "Submit a new slice request with a file", + "description": "This endpoint allows clients to submit transport network slice requests using a JSON payload.", + "operationId": "post_ixia_slice_list", + "parameters": [ + { + "name": "file", + "in": "formData", + "type": "file", + "description": "Archivo a subir" + }, + { + "name": "json_data", + "in": "formData", + "type": "string", + "description": "Datos JSON en formato string" + } + ], + "consumes": [ + "multipart/form-data" + ], + "tags": [ + "ixia" + ] + }, + "delete": { + "responses": { + "500": { + "description": "Internal server error" + }, + "204": { + "description": "All transport network slices deleted successfully." + } + }, + "summary": "Delete all slices", + "description": "Deletes all transport network slices from the slice controller.", + "operationId": "delete_ixia_slice_list", + "tags": [ + "ixia" + ] + }, + "get": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slices not found" + }, + "200": { + "description": "Slices returned", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + }, + "summary": "Retrieve all slices", + "description": "Returns all transport network slices from the slice controller.", + "operationId": "get_ixia_slice_list", + "tags": [ + "ixia" + ] + } + }, + "/ixia/slice/{slice_id}": { + "parameters": [ + { + "description": "The ID of the slice to retrieve or modify", + "name": "slice_id", + "in": "path", + "required": true, + "type": "string" + } + ], + "delete": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "204": { + "description": "Transport network slice deleted successfully." + } + }, + "summary": "Delete a slice", + "description": "Deletes a specific transport network slice from the slice controller based on the provided `slice_id`.", + "operationId": "delete_ixia_slice", + "tags": [ + "ixia" + ] + }, + "get": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "200": { + "description": "Slice returned", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + }, + "summary": "Retrieve a specific slice", + "description": "Returns specific information related to a slice by providing its id", + "operationId": "get_ixia_slice", + "tags": [ + "ixia" + ] + }, + "put": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "200": { + "description": "Slice modified", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + }, + "summary": "Modify a slice", + "description": "Returns a specific slice that has been modified", + "operationId": "put_ixia_slice", + "parameters": [ + { + "name": "payload", + "required": true, + "in": "body", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + ], + "tags": [ + "ixia" + ] + } + }, + "/tfs/slice": { + "post": { + "responses": { + "500": { + "description": "Internal server error" + }, + "400": { + "description": "Invalid request format" + }, + "200": { + "description": "No service to process." + }, + "201": { + "description": "Slice created successfully", + "schema": { + "$ref": "#/definitions/SliceResponse" + } + } + }, + "summary": "Submit a new slice request with a file", + "description": "This endpoint allows clients to submit transport network slice requests using a JSON payload.", + "operationId": "post_tfs_slice_list", + "parameters": [ + { + "name": "file", + "in": "formData", + "type": "file", + "description": "File to upload" + }, + { + "name": "json_data", + "in": "formData", + "type": "string", + "description": "JSON Data in string format" + } + ], + "consumes": [ + "multipart/form-data" + ], + "tags": [ + "tfs" + ] + }, + "delete": { + "responses": { + "500": { + "description": "Internal server error" + }, + "204": { + "description": "All transport network slices deleted successfully." + } + }, + "summary": "Delete all slices", + "description": "Deletes all transport network slices from the slice controller.", + "operationId": "delete_tfs_slice_list", + "tags": [ + "tfs" + ] + }, + "get": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slices not found" + }, + "200": { + "description": "Slices returned", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + }, + "summary": "Retrieve all slices", + "description": "Returns all transport network slices from the slice controller.", + "operationId": "get_tfs_slice_list", + "tags": [ + "tfs" + ] + } + }, + "/tfs/slice/{slice_id}": { + "parameters": [ + { + "description": "The ID of the slice to retrieve or modify", + "name": "slice_id", + "in": "path", + "required": true, + "type": "string" + } + ], + "delete": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "204": { + "description": "Transport network slice deleted successfully." + } + }, + "summary": "Delete a slice", + "description": "Deletes a specific transport network slice from the slice controller based on the provided `slice_id`.", + "operationId": "delete_tfs_slice", + "tags": [ + "tfs" + ] + }, + "get": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "200": { + "description": "Slice returned", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + }, + "summary": "Retrieve a specific slice", + "description": "Returns specific information related to a slice by providing its id", + "operationId": "get_tfs_slice", + "tags": [ + "tfs" + ] + }, + "put": { + "responses": { + "500": { + "description": "Internal server error" + }, + "404": { + "description": "Transport network slice not found." + }, + "200": { + "description": "Slice modified", + "schema": { + "$ref": "#/definitions/SliceResponse" + } + } + }, + "summary": "Modify a slice", + "description": "Returns a specific slice that has been modified", + "operationId": "put_tfs_slice", + "parameters": [ + { + "name": "payload", + "required": true, + "in": "body", + "schema": { + "$ref": "#/definitions/ddbb_model" + } + } + ], + "tags": [ + "tfs" + ] + } + } + }, + "info": { + "title": "Network Slice Controller (NSC) API", + "version": "1.0", + "description": "API for orchestrating and realizing transport network slice requests" + }, + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [ + { + "name": "tfs", + "description": "Operations related to transport network slices with TeraflowSDN (TFS) controller" + }, + { + "name": "ixia", + "description": "Operations related to transport network slices with IXIA NEII" + }, + { + "name": "E2E", + "description": "Operations related to transport network slices with E2E Orchestrator" + } + ], + "definitions": { + "SliceResponse": { + "properties": { + "success": { + "type": "boolean", + "description": "Indicates if the request was successful", + "example": true + }, + "data": { + "$ref": "#/definitions/SliceData" + }, + "error": { + "type": "string", + "description": "Error message if request failed" + } + }, + "type": "object" + }, + "SliceData": { + "properties": { + "slices": { + "type": "array", + "description": "List of slices", + "items": { + "$ref": "#/definitions/SliceDetails" + } + }, + "setup_time": { + "type": "number", + "description": "Slice setup time in milliseconds", + "example": 12.57 + } + }, + "type": "object" + }, + "SliceDetails": { + "properties": { + "id": { + "type": "string", + "description": "Slice ID", + "example": "slice-service-11327140-7361-41b3-aa45-e84a7fb40be9" + }, + "source": { + "type": "string", + "description": "Source IP", + "example": "10.60.11.3" + }, + "destination": { + "type": "string", + "description": "Destination IP", + "example": "10.60.60.105" + }, + "vlan": { + "type": "string", + "description": "VLAN ID", + "example": "100" + }, + "requirements": { + "type": "array", + "description": "List of requirements for the slice", + "items": { + "$ref": "#/definitions/SliceRequirement" + } + } + }, + "type": "object" + }, + "SliceRequirement": { + "properties": { + "constraint_type": { + "type": "string", + "description": "Type of constraint", + "example": "one-way-bandwidth[kbps]" + }, + "constraint_value": { + "type": "string", + "description": "Constraint value", + "example": "2000" + } + }, + "type": "object" + }, + "ddbb_model": { + "properties": { + "slice_id": { + "type": "string" + }, + "intent": { + "type": "array", + "items": { + "$ref": "#/definitions/NetworkSliceService" + } + }, + "controller": { + "type": "string" + } + }, + "type": "object" + }, + "NetworkSliceService": { + "properties": { + "ietf-network-slice-service:network-slice-services": { + "$ref": "#/definitions/NetworkSliceServices" + } + }, + "type": "object" + }, + "NetworkSliceServices": { + "properties": { + "slo-sle-templates": { + "$ref": "#/definitions/SloSleTemplates" + }, + "slice-service": { + "type": "array", + "items": { + "$ref": "#/definitions/SliceService" + } + } + }, + "type": "object" + }, + "SloSleTemplates": { + "properties": { + "slo-sle-template": { + "type": "array", + "items": { + "$ref": "#/definitions/SloSleTemplate" + } + } + }, + "type": "object" + }, + "SloSleTemplate": { + "properties": { + "id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "slo-policy": { + "$ref": "#/definitions/SloPolicy" + }, + "sle-policy": { + "$ref": "#/definitions/SlePolicy" + } + }, + "type": "object" + }, + "SloPolicy": { + "properties": { + "metric-bound": { + "type": "array", + "items": { + "$ref": "#/definitions/MetricBound" + } + } + }, + "type": "object" + }, + "MetricBound": { + "properties": { + "metric-type": { + "type": "string" + }, + "metric-unit": { + "type": "string" + }, + "bound": { + "type": "integer" + } + }, + "type": "object" + }, + "SlePolicy": { + "properties": { + "security": { + "type": "string" + }, + "isolation": { + "type": "string" + }, + "path-constraints": { + "$ref": "#/definitions/PathConstraints" + } + }, + "type": "object" + }, + "PathConstraints": { + "properties": { + "service-functions": { + "type": "string" + }, + "diversity": { + "$ref": "#/definitions/Diversity" + } + }, + "type": "object" + }, + "Diversity": { + "properties": { + "diversity-type": { + "type": "string" + } + }, + "type": "object" + }, + "SliceService": { + "properties": { + "id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "service-tags": { + "$ref": "#/definitions/ServiceTags" + }, + "slo-sle-policy": { + "$ref": "#/definitions/SloSlePolicy" + }, + "status": { + "type": "string" + }, + "sdps": { + "$ref": "#/definitions/Sdps" + }, + "connection-groups": { + "$ref": "#/definitions/ConnectionGroups" + } + }, + "type": "object" + }, + "ServiceTags": { + "properties": { + "tag-type": { + "$ref": "#/definitions/TagType" + } + }, + "type": "object" + }, + "TagType": { + "properties": { + "tag-type": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "SloSlePolicy": { + "properties": { + "slo-sle-template": { + "type": "string" + } + }, + "type": "object" + }, + "Sdps": { + "properties": { + "sdp": { + "type": "array", + "items": { + "$ref": "#/definitions/Sdp" + } + } + }, + "type": "object" + }, + "Sdp": { + "properties": { + "id": { + "type": "string" + }, + "geo-location": { + "type": "string" + }, + "node-id": { + "type": "string" + }, + "sdp-ip-address": { + "type": "string" + }, + "tp-ref": { + "type": "string" + }, + "service-match-criteria": { + "$ref": "#/definitions/ServiceMatchCriteria" + }, + "incoming-qos-policy": { + "type": "string" + }, + "outgoing-qos-policy": { + "type": "string" + }, + "sdp-peering": { + "$ref": "#/definitions/SdpPeering" + }, + "ac-svc-ref": { + "type": "array", + "items": { + "type": "string" + } + }, + "attachment-circuits": { + "type": "array", + "items": { + "$ref": "#/definitions/AttachmentCircuit" + } + }, + "status": { + "type": "string" + }, + "sdp-monitoring": { + "type": "string" + } + }, + "type": "object" + }, + "ServiceMatchCriteria": { + "properties": { + "match-criterion": { + "type": "array", + "items": { + "$ref": "#/definitions/MatchCriterion" + } + } + }, + "type": "object" + }, + "MatchCriterion": { + "properties": { + "index": { + "type": "integer" + }, + "match-type": { + "type": "string" + }, + "value": { + "type": "string" + }, + "target-connection-group-id": { + "type": "string" + } + }, + "type": "object" + }, + "SdpPeering": { + "properties": { + "peer-sap-id": { + "type": "string" + }, + "protocols": { + "type": "string" + } + }, + "type": "object" + }, + "AttachmentCircuit": { + "properties": { + "id": { + "type": "string" + }, + "ac-ipv4-address": { + "type": "string" + }, + "ac-ipv4-prefix-length": { + "type": "integer" + }, + "sdp-peering": { + "$ref": "#/definitions/SdpPeering" + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "ConnectionGroups": { + "properties": { + "connection-group": { + "type": "array", + "items": { + "$ref": "#/definitions/ConnectionGroup" + } + } + }, + "type": "object" + }, + "ConnectionGroup": { + "properties": { + "id": { + "type": "string" + }, + "connectivity-type": { + "type": "string" + }, + "connectivity-construct": { + "type": "array", + "items": { + "$ref": "#/definitions/ConnectivityConstruct" + } + }, + "status": { + "type": "string" + } + }, + "type": "object" + }, + "ConnectivityConstruct": { + "properties": { + "id": { + "type": "integer" + }, + "a2a-sdp": { + "type": "array", + "items": { + "$ref": "#/definitions/A2ASdp" + } + } + }, + "type": "object" + }, + "A2ASdp": { + "properties": { + "sdp-id": { + "type": "string" + } + }, + "type": "object" + } + }, + "responses": { + "ParseError": { + "description": "When a mask can't be parsed" + }, + "MaskError": { + "description": "When any error occurs on mask" + } + } +} diff --git a/doc/bootstrap_phase.puml b/doc/bootstrap_phase.puml new file mode 100644 index 0000000..e9faa95 --- /dev/null +++ b/doc/bootstrap_phase.puml @@ -0,0 +1,56 @@ +@startuml Bootstrap_Phase +actor "TFS Controller" as TFS +participant "RestconfConsumerService" as RCS +participant "RestconfClient" as RC +participant "TerflowSDN\n(RESTCONF Server)" as TFSDN +participant "SloSleTemplateBootstrapService" as BOOTSTRAP +participant "CatalogClient" as CATALOG +participant "OpenSlice TMF API" as TMF + +TFS ->> BOOTSTRAP: ApplicationReady Event +activate BOOTSTRAP + +BOOTSTRAP ->> RCS: Retrieve templates from provider +activate RCS + +RCS ->> RC: GET /api/ns/v0/slice-service-templates +activate RC + +RC ->> TFSDN: RESTCONF GET request +activate TFSDN +TFSDN -->> RC: Return SloSleTemplate[] +deactivate TFSDN + +RC -->> RCS: SliceService[] with SloSleTemplate[] +deactivate RC + +RCS -->> BOOTSTRAP: List +deactivate RCS + +BOOTSTRAP ->> BOOTSTRAP: For each SloSleTemplate:\n1. Create LogicalResourceSpecification\n2. Generate example jsonRequest\n3. Add jsonRequest characteristic + +BOOTSTRAP ->> CATALOG: Register LogicalResourceSpecification +activate CATALOG + +CATALOG ->> TMF: POST to Resource Catalog API\nCategory: tfs.controllers.osl.etsi.org/v1alpha +activate TMF +TMF -->> CATALOG: ResourceSpecification created +deactivate TMF + +CATALOG -->> BOOTSTRAP: Success +deactivate CATALOG + +BOOTSTRAP ->> BOOTSTRAP: Create example SliceService\nfor testing + +BOOTSTRAP ->> RCS: Store SloSleTemplate in memory +RCS -->> BOOTSTRAP: Stored + +BOOTSTRAP -->> TFS: Bootstrap complete +deactivate BOOTSTRAP + +note over TFS + TFS Controller is now ready to receive + CREATE/UPDATE/DELETE messages + for network slice services +end note +@enduml diff --git a/doc/resource_creation_lifecycle.puml b/doc/resource_creation_lifecycle.puml new file mode 100644 index 0000000..df4adab --- /dev/null +++ b/doc/resource_creation_lifecycle.puml @@ -0,0 +1,67 @@ +@startuml Resource_Creation_Lifecycle +actor "OpenSlice OSOM" as OSOM +participant "ActiveMQ Broker" as QUEUE +participant "Apache Camel" as CAMEL +participant "ResourceRepoService" as RRS +participant "Rfc9543SliceService\nDeserializer" as DESER +participant "RestconfConsumerService" as RCS +participant "TerflowSDN\n(RESTCONF Server)" as TFSDN +participant "CatalogClient\n(TMF API)" as CATALOG + +OSOM ->> QUEUE: POST CREATE message\nQueue: CREATE/.../v1alpha/0.1.0\nBody: {resourceId, jsonRequest, ...} +activate QUEUE + +QUEUE ->> CAMEL: Route message +activate CAMEL + +CAMEL ->> RRS: processResourceCreate(message) +activate RRS + +RRS ->> RRS: Extract jsonRequest from\nresource characteristics +RRS ->> RRS: Validate jsonRequest + +RRS ->> DESER: mapper.readValue(jsonRequest) +activate DESER + +DESER ->> DESER: Map hyphenated fields to\nJava camelCase properties:\nservice-tags → serviceTags\nslo-sle-policy → sloSleTemplate\nconnection-groups → connectionGroups +DESER -->> RRS: SliceService object +deactivate DESER + +RRS ->> RCS: provisionSliceService(sliceService) +activate RCS + +RCS ->> TFSDN: PUT /api/ns/v0/slice-service/\n[SliceService JSON] +activate TFSDN +TFSDN -->> RCS: Success: SliceService with ID +deactivate TFSDN + +RCS -->> RRS: Provisioned SliceService +deactivate RCS + +RRS ->> RRS: Update resource status:\ninfoMessage = "Successfully\nprovisioned: {serviceId}"\nhealthStatus = "Healthy"\nresourceStatus = AVAILABLE + +RRS ->> CATALOG: Update resource in TMF API\nresourceId, status, infoMessage +activate CATALOG +CATALOG -->> RRS: Resource updated +deactivate CATALOG + +RRS -->> CAMEL: Completed +deactivate RRS + +CAMEL -->> QUEUE: Route complete +deactivate CAMEL +QUEUE -->> OSOM: ACK +deactivate QUEUE + +note over OSOM + Resource is now AVAILABLE in OpenSlice + and network slice is provisioned on TerflowSDN +end note + +alt On Failure + RRS ->> RRS: Catch exception + RRS ->> RRS: Set healthStatus = "Unhealthy"\nresourceStatus = UNKNOWN\ninfoMessage = error details + RRS ->> CATALOG: Update with failure status + RRS -->> CAMEL: Failed +end +@enduml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..27d1fc8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,372 @@ + + 4.0.0 + org.etsi.osl + org.etsi.osl.controllers.ietf.ns + 0.0.1-SNAPSHOT + org.etsi.osl.controllers.ietf.ns + org.etsi.osl.controllers.ietf.ns + + + + UTF-8 + UTF-8 + 3.2.2 + 1.18.28 + 2.1.0 + 1.5.3.Final + 17 + 4.0.0-RC2 + 2.8.11 + 2.0.0 + apache_v2 + 1.7.0 + 1.7.0 + 22.0.1 + 1.1.0-SNAPSHOT + + + + + gitlab-maven + https://labs.etsi.org/rep/api/v4/groups/260/-/packages/maven + + + + + gitlab-maven + ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/maven + + + gitlab-maven + ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/maven + + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot-version} + pom + import + + + + org.apache.camel.springboot + camel-spring-boot-dependencies + ${camel.version} + pom + import + + + + com.google.guava + guava + 32.0.0-jre + + + org.keycloak.bom + keycloak-adapter-bom + ${keycloak.version} + pom + import + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + com.jayway.jsonpath + json-path + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.security + spring-security-oauth2-client + + + org.springframework.security + spring-security-core + + + org.springframework.security + spring-security-web + + + org.springframework.security + spring-security-config + + + + + org.projectlombok + lombok + provided + ${lombok-version} + + + org.openapitools + jackson-databind-nullable + 0.2.6 + + + + org.keycloak + keycloak-spring-boot-starter + + + org.keycloak + keycloak-spring-security-adapter + + + + org.keycloak + keycloak-admin-client + ${keycloak.version} + + + + + org.etsi.osl + org.etsi.osl.model.tmf + ${org.etsi.osl.model.tmf.version} + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + org.springdoc + springdoc-openapi-ui + ${springdoc.openapiui.version} + + + org.springdoc + springdoc-openapi-security + ${springdoc.security.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + javax.annotation + javax.annotation-api + 1.3.2 + compile + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + org.jetbrains + annotations + 13.0 + compile + + + + + org.springframework.boot + spring-boot-starter-activemq + + + org.apache.activemq + activemq-amqp + test + + + org.apache.qpid + proton-j + + + + + org.messaginghub + pooled-jms + + + + + org.apache.camel.springboot + camel-spring-boot-starter + + + org.apache.activemq + activemq-pool + + + org.apache.camel + camel-activemq + + + org.apache.activemq + activemq-broker + + + + + org.apache.camel.springboot + camel-service-starter + + + + org.apache.camel.springboot + camel-http-starter + + + org.apache.camel + camel-jackson + + + org.apache.camel + camel-stream + + + + dk.brics.automaton + automaton + 1.11-8 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + -parameters + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok-version} + + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + + + org.codehaus.mojo + license-maven-plugin + ${maven-license-plugin.version} + + false + ========================LICENSE_START================================= + =========================LICENSE_END================================== + *.json + + + + generate-license-headers + + update-file-header + + process-sources + + ${license.licenseName} + + + + + download-licenses + + download-licenses + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/IETFNSGCSpringBoot.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/IETFNSGCSpringBoot.java new file mode 100644 index 0000000..b1403e2 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/IETFNSGCSpringBoot.java @@ -0,0 +1,60 @@ +package org.etsi.osl.controllers.ietf.ns; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ComponentScan; + + +/** + * For implementing the callback and events, it might be useful to check the DDD pattern: + * https://www.baeldung.com/spring-data-ddd + * + * + * @author ctranoris + * + */ +@SpringBootApplication +@EntityScan(basePackages = {"org.etsi.osl.controllers.ietf.ns","org.etsi.osl.controllers.ietf.ns.api"}) + +public class IETFNSGCSpringBoot implements CommandLineRunner { + + private static ApplicationContext applicationContext; + + private static final Logger logger = LoggerFactory.getLogger("org.etsi.osl.controllers.ietf.ns"); + + + @Override + public void run(String... arg0) throws Exception { + if (arg0.length > 0 && arg0[0].equals("exitcode")) { + throw new ExitException(); + } + } + + public static void main(String[] args) throws Exception { + + logger.info("=========== STARTING org.etsi.osl.controllers.ietf.ns =============================="); + applicationContext = new SpringApplication(IETFNSGCSpringBoot.class).run(args); + + + // for (String beanName : applicationContext.getBeanDefinitionNames()) { + // System.out.println(beanName); + // } + } + + class ExitException extends RuntimeException implements ExitCodeGenerator { + private static final long serialVersionUID = 1L; + + @Override + public int getExitCode() { + return 10; + } + + } + +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/CategoryConfigurationService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/CategoryConfigurationService.java new file mode 100644 index 0000000..ea9d8db --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/CategoryConfigurationService.java @@ -0,0 +1,131 @@ +package org.etsi.osl.controllers.ietf.ns.api; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Centralized service for managing category prefixes and related configurations. + * + * This service provides a single source of truth for all TMF category values used + * throughout the IETF NS controller application, ensuring consistency across: + * - EntityToLogicalResourceMapper + * - EntityToLogicalResourceSpecMapper + * - LogicalResourceToEntityMapper + * - SloSleTemplateBootstrapService + * - TMFResourceInventoryRepositoryImpl + * - And all other components that need category information + * + * Configuration is read from application.yaml: + * osl-ietf-ns-controller: + * category: ns.ietf.controllers.osl.etsi.org + * + */ +@Service +@Slf4j +@Getter +public class CategoryConfigurationService { + + /** + * Base category prefix from application configuration. + * Format: {domain}.{service}.osl.etsi.org + * Example: ns.ietf.controllers.osl.etsi.org + */ + @Value("${osl-ietf-ns-controller.category:ns.ietf.controllers.osl.etsi.org/v1alpha}") + private String categoryPrefix; + + /** + * API version from application configuration. + * Format: x.y.z or alphanumeric identifier + * Example: 0.1.0 or v1alpha + */ + @Value("${osl-ietf-ns-controller.version:0.1.0}") + private String version; + + /** + * Constructs the full category string for a given entity type. + * + * Format: {categoryPrefix}/{version}/{entityType} + * Example: ns.ietf.controllers.osl.etsi.org/0.1.0/SloSleTemplate + * + * @param entityType The entity type (e.g., "SloSleTemplate", "Network", "Device") + * @return Full category string + */ + public String getCategoryForEntity(String entityType) { + if (entityType == null || entityType.isEmpty()) { + throw new IllegalArgumentException("Entity type must not be null or empty"); + } + return String.format("%s/%s/%s", categoryPrefix, version, entityType); + } + + /** + * Gets the base category prefix with version suffix for specification templates. + * + * Format: {categoryPrefix}/{version} + * Example: ns.ietf.controllers.osl.etsi.org/0.1.0 + * + * Used by: + * - SloSleTemplateBootstrapService + * - EntityToLogicalResourceSpecMapper + * + * @return Category prefix with version suffix + */ + public String getCategoryForSpecifications() { + return String.format("%s", categoryPrefix); + } + + /** + * Gets the base category prefix without version suffix. + * + * Format: {categoryPrefix} + * Example: ns.ietf.controllers.osl.etsi.org + * + * Used by: + * - EntityToLogicalResourceMapper + * - LogicalResourceToEntityMapper + * - TMFResourceInventoryRepositoryImpl + * + * @return Base category prefix + */ + public String getCategoryPrefix() { + return categoryPrefix; + } + + /** + * Gets the category prefix with version suffix for resources. + * + * Format: {categoryPrefix}/{version} + * Example: ns.ietf.controllers.osl.etsi.org/0.1.0 + * + * Used by: + * - TMFResourceInventoryRepositoryImpl + * + * @return Category prefix with version suffix + */ + public String getCategoryPrefixWithVersion() { + return String.format("%s/%s", categoryPrefix, version); + } + + /** + * Logs the current configuration for diagnostic purposes. + * Call during application startup to verify category configuration. + */ + public void logConfiguration() { + log.info("=== IETF NS Controller Category Configuration ==="); + log.info("Category Prefix: {}", categoryPrefix); + log.info("Version: {}", version); + log.info("Category for Specifications: {}", getCategoryForSpecifications()); + log.info("Category Prefix with Version: {}", getCategoryPrefixWithVersion()); + log.info("Example - Entity Category: {}", getCategoryForEntity("SloSleTemplate")); + log.info("========================================="); + } + + @Override + public String toString() { + return "CategoryConfigurationService{" + + "categoryPrefix='" + categoryPrefix + '\'' + + ", version='" + version + '\'' + + '}'; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/PartnerRouteBuilder.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/PartnerRouteBuilder.java new file mode 100644 index 0000000..cbcbb57 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/PartnerRouteBuilder.java @@ -0,0 +1,91 @@ +package org.etsi.osl.controllers.ietf.ns.api; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.camel.LoggingLevel; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.dataformat.JsonLibrary; +import org.etsi.osl.controllers.ietf.ns.repository.impl.ResourceRepoService; +import org.etsi.osl.tmf.ri639.model.Resource; +import org.etsi.osl.tmf.ri639.model.ResourceCreate; +import org.etsi.osl.tmf.ri639.model.ResourceUpdate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + + +@Configuration +@Component +public class PartnerRouteBuilder extends RouteBuilder { + + private static final Logger logger = LoggerFactory.getLogger("org.etsi.osl.controllers.ietf.ns"); + + @Value("${spring.application.name}") + private String compname; + + + + @Autowired + private CategoryConfigurationService categoryConfig; + + @Autowired + ResourceRepoService resourceRepoService; + + @Override + public void configure() throws Exception { + + + String EVENT_CREATE = "jms:queue:CREATE/"+ categoryConfig.getCategoryPrefixWithVersion(); + + String EVENT_UPDATE = "jms:queue:UPDATE/"+ categoryConfig.getCategoryPrefixWithVersion(); + + String EVENT_DELETE = "jms:queue:DELETE/"+ categoryConfig.getCategoryPrefixWithVersion(); + + from(EVENT_CREATE) + .log(LoggingLevel.INFO, log, EVENT_CREATE + " message received!") + .to("log:DEBUG?showBody=true&showHeaders=true").unmarshal() + .json(JsonLibrary.Jackson, ResourceCreate.class, true) + .bean( resourceRepoService, "createResource( ${headers}, ${body} )") + .marshal().json( JsonLibrary.Jackson) + .convertBodyTo( String.class ); + + from(EVENT_UPDATE) + .log(LoggingLevel.INFO, log, EVENT_UPDATE + " message received!") + .to("log:DEBUG?showBody=true&showHeaders=true").unmarshal() + .json(JsonLibrary.Jackson, ResourceUpdate.class, true) + .bean( resourceRepoService, "updateResource( ${headers},${body} )") + .marshal().json( JsonLibrary.Jackson) + .convertBodyTo( String.class );; + + from(EVENT_DELETE) + .log(LoggingLevel.INFO, log, EVENT_DELETE + " message received!") + .to("log:DEBUG?showBody=true&showHeaders=true").unmarshal() + .json(JsonLibrary.Jackson, ResourceUpdate.class, true) + .bean( resourceRepoService, "deleteResource( ${headers}, ${body} )") + .marshal().json( JsonLibrary.Jackson) + .convertBodyTo( String.class );; + + + } + + static T toJsonObj(String content, Class valueType) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.readValue(content, valueType); + } + + static String toJsonString(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsString(object); + } + + +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/ResourceSpecificationTemplateRegistry.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/ResourceSpecificationTemplateRegistry.java new file mode 100644 index 0000000..c110720 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/ResourceSpecificationTemplateRegistry.java @@ -0,0 +1,84 @@ +package org.etsi.osl.controllers.ietf.ns.api; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Registry that stores resource specification template IDs created during bootstrap. + * These template IDs are used when creating actual resource instances. + * + * Usage: + * - Bootstrap process registers template IDs by entity type name + * - Services retrieve template IDs when creating resource instances + * - Template IDs reference the structure/schema definitions in TMF catalog + */ +@Component +@Slf4j +public class ResourceSpecificationTemplateRegistry { + + private final Map templateIds = new ConcurrentHashMap<>(); + + /** + * Register a template ID for an entity type + * + * @param entityTypeName Simple class name + * @param templateId UUID of the registered resource specification template + */ + public void registerTemplate(String entityTypeName, String templateId) { + log.info("Registering template ID for {}: {}", entityTypeName, templateId); + templateIds.put(entityTypeName, templateId); + } + + /** + * Get the template ID for an entity type + * + * @param entityTypeName Simple class name (e.g., "Datacenter", "EdgeSite") + * @return Optional containing the template ID if registered + */ + public Optional getTemplateId(String entityTypeName) { + return Optional.ofNullable(templateIds.get(entityTypeName)); + } + + /** + * Check if a template is registered for an entity type + * + * @param entityTypeName Simple class name + * @return true if template ID exists + */ + public boolean hasTemplate(String entityTypeName) { + return templateIds.containsKey(entityTypeName); + } + + /** + * Get all registered template IDs + * + * @return Immutable map of entity type names to template IDs + */ + public Map getAllTemplates() { + return Map.copyOf(templateIds); + } + + /** + * Clear all registered templates (useful for testing) + */ + public void clear() { + log.warn("Clearing all registered template IDs"); + templateIds.clear(); + } + + /** + * Get count of registered templates + */ + public int getTemplateCount() { + return templateIds.size(); + } + + public boolean getSloSleTemplateById(boolean b) { + // TODO Auto-generated method stub + return false; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java new file mode 100644 index 0000000..c6d9ea6 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java @@ -0,0 +1,1125 @@ +package org.etsi.osl.controllers.ietf.ns.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.ServiceStatus; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.ServiceTag; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.AvailabilityType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.MetricBound; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceIsolationType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceSecurityType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceSloMetricType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SlePolicy; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; +import org.etsi.osl.controllers.ietf.ns.api.restconf.RestconfClient; +import org.etsi.osl.controllers.ietf.ns.api.restconf.RestconfException; +import org.etsi.osl.controllers.ietf.ns.api.restconf.Rfc9543JsonConverter; +import org.etsi.osl.controllers.ietf.ns.mappers.EntityToLogicalResourceMapper; +import org.etsi.osl.controllers.ietf.ns.mappers.EntityToLogicalResourceSpecMapper; +import org.etsi.osl.controllers.ietf.ns.repository.impl.TMFResourceInventoryRepositoryImpl; +import org.etsi.osl.controllers.ietf.ns.repository.impl.TMFResourceSpecRepositoryImpl; +import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCreate; +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import java.util.UUID; + +/** + * Bootstrap component that registers SLO/SLE template resource specifications at startup. + * + * This service creates and registers LogicalResourceSpecification templates for + * different SLO/SLE tiers (Bronze, Silver, Gold examples) that define the structure and schema + * for each tier in the TMF catalog. + * + * + * @author ctranoris + */ +@Component +@Slf4j +public class SloSleTemplateBootstrapService implements CommandLineRunner { + + @Autowired + private TMFResourceSpecRepositoryImpl tmfRepository; + + @Autowired + private EntityToLogicalResourceSpecMapper mapper; + + @Autowired + private EntityToLogicalResourceMapper logicalResourceMapper; + + @Autowired + private ResourceSpecificationTemplateRegistry templateRegistry; + + @Autowired(required = false) + private RestconfClient restconfClient; + + @Autowired + private CategoryConfigurationService categoryConfig; + + + @Autowired + private TMFResourceInventoryRepositoryImpl tmfResourceinventory; + + + // Container to store provider templates for later slice service creation + private List providerTemplates = new ArrayList<>(); + + /** + * Executed at application startup via CommandLineRunner interface. + * Creates default templates, retrieves templates from RESTCONF provider, + * and registers all templates in the TMF catalog. + */ + @Override + public void run(String... args) throws Exception { + log.info("=== Starting SLO/SLE Template Resource Specification Bootstrap ==="); + + try { + // Create list to store all specifications (default + retrieved) + List allSpecifications = new ArrayList<>(); + // Separate list for slice services (LogicalResource) + List sliceServices = new ArrayList<>(); + + + // 1. Create default example templates + log.info("Step 1: Creating default SLO/SLE tier templates (Bronze, Silver, Gold)"); + List defaultSpecs = createSpecificationTemplates(); + allSpecifications.addAll(defaultSpecs); + log.info(" Created {} default templates", defaultSpecs.size()); + + // 2. Retrieve templates from RESTCONF provider + if (restconfClient != null) { + log.info("Step 2: Retrieving SLO/SLE templates from RESTCONF provider"); + List retrievedSpecs = retrieveTemplatesFromProvider(); + allSpecifications.addAll(retrievedSpecs); + log.info(" Retrieved {} templates from provider", retrievedSpecs.size()); + } else { + log.warn("Step 2: RESTCONF client not available - skipping provider template retrieval"); + } + + // 3. Register all default and retrieved specifications + log.info("Step 3: [NOT PERFORMED] Registering all SLO/SLE templates in TMF catalog"); + for (LogicalResourceSpecification spec : allSpecifications) { + //registerSpecification(spec); + } + + // 4. Create example slice services for provider templates + if (!providerTemplates.isEmpty()) { + log.info("Step 4: Creating example SliceServices for provider templates"); + List providerExampleServices = createExampleSliceServicesForProviderTemplates(); + log.info(" Created {} example slice services for provider templates", providerExampleServices.size()); + } else { + log.info("Step 4: No provider templates available - skipping example slice service creation"); + } + + + // Load and register backhaul slice requests + log.info("Step 5: Loading and registering RFC 9543 backhaul slice requests"); + loadAndRegisterLocalSlices(); + + log.info("=== SLO/SLE Template Bootstrap Complete: {} template IDs registered, {} slice services registered ===", + templateRegistry.getTemplateCount(), sliceServices.size()); + + // Log all registered template IDs + templateRegistry.getAllTemplates().forEach((templateName, templateId) -> + log.info(" {} -> {}", templateName, templateId)); + + } catch (Exception e) { + log.error("Error during SLO/SLE template bootstrap initialization", e); + // Don't throw - allow application to start even if bootstrap fails + } + } + + /** + * Create default template specifications for all SLO/SLE tiers + */ + private List createSpecificationTemplates() { + log.info("Creating SLO/SLE resource specification templates"); + + List specs = new ArrayList<>(); + + // Create templates for each tier + specs.add(createBronzeTemplateSpecification()); + specs.add(createSilverTemplateSpecification()); + specs.add(createGoldTemplateSpecification()); + + + return specs; + } + + + /** + * Retrieve RFC 9543 SLO/SLE templates directly from the RESTCONF provider. + * + * This method queries the RESTCONF provider for all SLO/SLE templates at the + * /restconf/data/ietf-network-slice-service:network-slice-services/slo-sle-templates + * endpoint and converts them to LogicalResourceSpecification for registration + * in the TMF catalog. + * + * Also stores the raw SloSleTemplate objects for later use in creating example + * slice services that reference these templates. + * + * @return List of LogicalResourceSpecification objects extracted from provider + */ + private List retrieveTemplatesFromProvider() { + List retrievedSpecs = new ArrayList<>(); + Set processedTemplateIds = new HashSet<>(); // Avoid duplicates + providerTemplates.clear(); // Clear any previous templates + + try { + log.debug("Querying RESTCONF provider for SLO/SLE templates at /slo-sle-templates endpoint"); + + // Get the templates response as JSON string + String templatesJson = getTemplatesFromProvider(); + + if (templatesJson == null || templatesJson.isEmpty()) { + log.info("No SLO/SLE templates retrieved from provider"); + return retrievedSpecs; + } + + // Parse the RFC 9543 formatted response using Rfc9543JsonConverter + List templates = Rfc9543JsonConverter.parseSloSleTemplates(templatesJson); + log.info("Retrieved {} SLO/SLE templates from provider", templates.size()); + + // Convert each template to LogicalResourceSpecification + for (SloSleTemplate template : templates) { + if (template != null && template.getId() != null) { + String templateId = template.getId(); + + // Process each template only once + if (!processedTemplateIds.contains(templateId)) { + processedTemplateIds.add(templateId); + log.debug("Processing SloSleTemplate from provider: {}", templateId); + + // Store the raw template for later slice service creation + providerTemplates.add(template); + + // Convert to LogicalResourceSpecification + LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); + + //Override Name, to show NSC templates as specnames... + spec.setName(template.getEntityName() ); + + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(categoryConfig.getVersion()); + spec.setDescription("SLO/SLE template '" + templateId + "' retrieved from RESTCONF provider"); + + retrievedSpecs.add(spec); + log.info("Extracted SloSleTemplate specification: {}", templateId); + } + } + } + + log.info("Successfully extracted {} unique SloSleTemplate specifications from provider", + retrievedSpecs.size()); + + } catch (RestconfException e) { + log.error("RESTCONF error while retrieving templates from provider: {} - {} ({})", + e.getMessage(), e.getErrorType(), e.getErrorTag(), e); + } catch (Exception e) { + log.error("Error retrieving templates from RESTCONF provider", e); + // Continue with bootstrap even if provider retrieval fails + } + + return retrievedSpecs; + } + + /** + * Retrieves raw JSON response for SLO/SLE templates from the RESTCONF provider. + * + * Makes a request via RestconfClient to the provider's + * /restconf/data/ietf-network-slice-service:network-slice-services/slo-sle-templates + * endpoint to retrieve RFC 9543 formatted templates with proper authentication. + * + * @return JSON string containing the RFC 9543 formatted templates response + * @throws RestconfException if the retrieval fails + */ + private String getTemplatesFromProvider() throws RestconfException { + try { + log.debug("Retrieving SLO/SLE templates from RESTCONF provider via RestconfClient"); + + // Use RestconfClient.getSloSleTemplates() which handles: + // - HTTP Basic Authentication (admin/admin123) + // - Proper RESTCONF headers (application/yang-data+json) + // - Error handling and retries + // - Logging at each stage + String templatesJson = restconfClient.getSloSleTemplates(); + + if (templatesJson == null || templatesJson.isEmpty() || "{}".equals(templatesJson)) { + log.debug("No templates retrieved from provider - provider may not have templates available"); + return null; + } + + log.debug("Successfully retrieved SLO/SLE templates from provider"); + return templatesJson; + + } catch (RestconfException e) { + log.warn("Failed to retrieve templates from RESTCONF provider: {}", e.getMessage()); + // Return null to allow bootstrap to continue with default templates + return null; + } catch (Exception e) { + log.error("Unexpected error retrieving templates from provider", e); + // Return null to allow bootstrap to continue with default templates + return null; + } + } + + + + + /** + * Register a specification using createOrUpdateResourceSpecByNameCategoryVersion + */ + private void registerSpecification(LogicalResourceSpecification spec) { + log.info("Registering specification: {} (category: {}, version: {})", + spec.getName(), spec.getCategory(), spec.getVersion()); + + try { + // Convert LogicalResourceSpecification to ResourceSpecificationCreate + ResourceSpecificationCreate createSpec = new ResourceSpecificationCreate(); + createSpec.setName(spec.getName()); + createSpec.setCategory(spec.getCategory()); + createSpec.setVersion(spec.getVersion()); + createSpec.setDescription(spec.getDescription()); + createSpec.setLifecycleStatus(spec.getLifecycleStatus()); + createSpec.setType("LogicalResourceSpecification"); + createSpec.setBaseType("ResourceSpecification"); + + // Copy characteristics and relationships (convert Set to List) + if (spec.getResourceSpecCharacteristic() != null) { + createSpec.setResourceSpecificationCharacteristic( + new ArrayList<>(spec.getResourceSpecCharacteristic())); + } + if (spec.getResourceSpecRelationship() != null) { + createSpec.setResourceSpecificationRelationship( + new ArrayList<>(spec.getResourceSpecRelationship())); + } + + // Register the specification + LogicalResourceSpecification registered = + tmfRepository.createOrUpdateResourceSpecByNameCategoryVersion(createSpec); + + if (registered != null && registered.getUuid() != null) { + log.info("Successfully registered specification: {} with ID: {}", + spec.getName(), registered.getUuid()); + + // Store the template ID in the registry for future use + templateRegistry.registerTemplate(spec.getName(), registered.getUuid()); + } else { + log.warn("Failed to register specification: {}", spec.getName()); + } + + } catch (Exception e) { + log.error("Error registering specification: {}", spec.getName(), e); + } + } + + /** + * Register a LogicalResource (slice service) in the TMF Resource Inventory. + * + * Slice services are registered as LogicalResource instances in the TMF639 + * Resource Inventory for use in resource management and monitoring. + * + * @param resource LogicalResource instance representing a slice service + */ + private void registerLogicalResource(LogicalResource resource) { + log.info("Registering LogicalResource: {} (category: {})", + resource.getName(), resource.getCategory()); + + try { + if (resource == null || resource.getName() == null) { + log.warn("Cannot register null or invalid LogicalResource"); + return; + } + + // In a real implementation, this would call the TMF Resource Inventory API + // For now, we log the resource for demonstration purposes + log.info("LogicalResource ready for inventory storage: name={}, uuid={}, status={}", + resource.getName(), + resource.getUuid(), + resource.getResourceStatus()); + + // TODO: Integrate with actual Resource Inventory Management service + // tmfRepository.createOrUpdateLogicalResource(resource); + tmfResourceinventory.createOrUpdate(resource); + + } catch (Exception e) { + log.error("Error registering LogicalResource: {}", resource.getName(), e); + } + } + + + /** + * Load a resource from classpath. + * + * @param resourceName Name of the resource file (e.g., "ietf_green_request.json") + * @return Resource content as String, or null if not found + */ + private String loadClasspathResource(String resourceName) { + try { + ClassPathResource resource = new ClassPathResource(resourceName); + if (!resource.exists()) { + log.warn("Classpath resource not found: {}", resourceName); + return null; + } + + try (InputStream inputStream = resource.getInputStream()) { + byte[] bytes = inputStream.readAllBytes(); + return new String(bytes, StandardCharsets.UTF_8); + } + } catch (IOException e) { + log.error("Error loading classpath resource: {}", resourceName, e); + return null; + } + } + + /** + * Parse RFC 9543 formatted SliceService JSON and return SliceService domain object. + * + * This method handles the real-world RFC 9543 network-slice-service format with + * SDP (Service Demarcation Points) and connection groups. + * + * @param sliceServiceJson RFC 9543 formatted JSON string + * @return SliceService domain object + * @throws Exception if JSON parsing fails + */ + public NetworkSliceServices parseAndConvertSliceService(String sliceServiceJson) throws Exception { + log.info("Parsing RFC 9543 SliceService JSON"); + + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(sliceServiceJson); + + // Navigate through RFC 9543 namespace wrapper + JsonNode nssNode = rootNode.get("ietf-network-slice-service:network-slice-services"); + if (nssNode == null) { + throw new IllegalArgumentException("Missing RFC 9543 namespace wrapper"); + } + + JsonNode sliceServicesNode = nssNode.get("slice-service"); + if (sliceServicesNode == null || !sliceServicesNode.isArray() || sliceServicesNode.size() == 0) { + throw new IllegalArgumentException("No slice-service array found"); + } + + // Parse all slice services from the array + List sliceServices = new ArrayList<>(); + + for (int i = 0; i < sliceServicesNode.size(); i++) { + JsonNode sliceServiceNode = sliceServicesNode.get(i); + + // Parse service identity + String serviceId = sliceServiceNode.get("id").asText(); + String serviceDescription = sliceServiceNode.has("description") ? + sliceServiceNode.get("description").asText() : "Network Slice Service"; + + log.info("Parsing slice service: {}", serviceId); + + // Create SliceService domain object + SliceService service = new SliceService(); + service.setId(serviceId); + service.setDescription(serviceDescription); + service.setTestOnly(false); + + // Parse SLO/SLE template reference + if (sliceServiceNode.has("slo-sle-policy")) { + JsonNode sloSlePolicyNode = sliceServiceNode.get("slo-sle-policy"); + if (sloSlePolicyNode.has("slo-sle-template")) { + String templateId = sloSlePolicyNode.get("slo-sle-template").asText(); + SloSleTemplate template = new SloSleTemplate(); + template.setId(templateId); + service.setSloSleTemplate(template); + log.debug("Service {} references template: {}", serviceId, templateId); + } + } + + // Parse service tags + if (sliceServiceNode.has("service-tags")) { + JsonNode tagsNode = sliceServiceNode.get("service-tags"); + if (tagsNode.has("tag-type") && tagsNode.get("tag-type").isArray()) { + for (JsonNode tagNode : tagsNode.get("tag-type")) { + String tagType = tagNode.get("tag-type").asText(); + if (tagNode.has("tag-type-value") && tagNode.get("tag-type-value").isArray()) { + for (JsonNode tagValueNode : tagNode.get("tag-type-value")) { + ServiceTag tag = new ServiceTag(); + tag.setValue(tagType + ":" + tagValueNode.asText()); + service.getServiceTags().add(tag); + } + } + } + } + } + + // Parse status + ServiceStatus status = new ServiceStatus(); + status.setAdminState("admin-up"); + status.setOperState("operational"); + service.setStatus(status); + + // Parse SDPs (Service Demarcation Points) + if (sliceServiceNode.has("sdps") && sliceServiceNode.get("sdps").has("sdp")) { + JsonNode sdpsNode = sliceServiceNode.get("sdps").get("sdp"); + if (sdpsNode.isArray()) { + log.debug("Found {} SDPs in slice service", sdpsNode.size()); + } + } + + // Parse connection groups + if (sliceServiceNode.has("connection-groups") && + sliceServiceNode.get("connection-groups").has("connection-group")) { + JsonNode connGroupsNode = sliceServiceNode.get("connection-groups").get("connection-group"); + if (connGroupsNode.isArray()) { + log.debug("Found {} connection groups in slice service", connGroupsNode.size()); + } + } + + // Add this service to the list + sliceServices.add(service); + log.info("Successfully parsed RFC 9543 SliceService: {}", serviceId); + } + + // Create NetworkSliceServices wrapper with all parsed services + NetworkSliceServices networkSliceServices = new NetworkSliceServices(); + networkSliceServices.setSliceServices(sliceServices); + + // Build and store the RFC 9543 formatted JSON request (slice-service array) + Map> sliceServiceWrapper = new HashMap<>(); + sliceServiceWrapper.put("slice-service", sliceServices); + String jsonRequest = mapper.writeValueAsString(sliceServiceWrapper); + networkSliceServices.setJsonRequest(jsonRequest); + + log.info("Successfully created NetworkSliceServices with {} service(s)", sliceServices.size()); + return networkSliceServices; + + } catch (IllegalArgumentException e) { + log.error("Invalid RFC 9543 SliceService JSON format: {}", e.getMessage()); + throw e; + } catch (Exception e) { + log.error("Error parsing RFC 9543 SliceService JSON", e); + throw e; + } + } + + + + /** + * Load and register backhaul slice requests from classpath. + * + * Loads both: + * 1. slice_request_backhaul_control.json - Control plane backhaul (Template A) + * 2. slice_request_backhaul_user.json - User plane backhaul (Template C) + * + * Both are registered as LogicalResourceSpecification in TMF catalog. + */ + public void loadAndRegisterLocalSlices() { + log.info("Loading and registering RFC 9543 backhaul slice requests"); + + + // Load and register GreenRequest + loadAndRegisterSliceFromFile("ietf_green_request.json", "Green-optimized network slice for energy-efficient transport. " + + "Maps 3GPP NetworkSlice1 to IETF RFC 9543 with energy consumption, efficiency, " + + "carbon emission, and renewable energy metrics."); + + // Load and register control plane backhaul + loadAndRegisterSliceFromFile("slice_request_backhaul_control.json", "Backhaul Control Plane"); + + // Load and register user plane backhaul + loadAndRegisterSliceFromFile("slice_request_backhaul_user.json", "Backhaul User Plane"); + } + + /** + * Load and register a single backhaul slice request. + * + * @param fileName Name of the JSON file (e.g., "slice_request_backhaul_control.json") + * @param description Description for the service + */ + private void loadAndRegisterSliceFromFile(String fileName, String description) { + try { + log.info("Loading backhaul slice request: {}", fileName); + + // Load JSON from classpath + String sliceJson = loadClasspathResource(fileName); + if (sliceJson == null || sliceJson.isEmpty()) { + log.warn("Backhaul slice request JSON not found or empty: {}", fileName); + return; + } + + // Handle array format (slice_request_backhaul_user.json is an array) + String serviceJson = sliceJson; + if (sliceJson.trim().startsWith("[")) { + // Extract first element from array + ObjectMapper mapper = new ObjectMapper(); + JsonNode arrayNode = mapper.readTree(sliceJson); + if (arrayNode.isArray() && arrayNode.size() > 0) { + serviceJson = mapper.writeValueAsString(arrayNode.get(0)); + } else { + log.warn("Backhaul slice request array is empty: {}", fileName); + return; + } + } + + // Parse JSON to NetworkSliceServices + NetworkSliceServices networkSliceServices = parseAndConvertSliceService(serviceJson); + log.info("Successfully parsed backhaul network slice services with {} service(s)", + networkSliceServices.getSliceServices().size()); + + // Convert NetworkSliceServices to LogicalResourceSpecification + LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(networkSliceServices); + + // Enhance specification with backhaul metadata + String templateId = networkSliceServices.getSliceServices().isEmpty() ? "unknown" : + (networkSliceServices.getSliceServices().get(0).getSloSleTemplate() != null ? + networkSliceServices.getSliceServices().get(0).getSloSleTemplate().getId() : "unknown"); + + spec.setName( spec.getName() + "_LocalTemplate_" + templateId ); + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(categoryConfig.getVersion()); + spec.setDescription(description + " - " + networkSliceServices.getEntityDescription() + + ". Maps 3GPP network slice to IETF RFC 9543 standard format."); + spec.setType("LogicalResourceSpecification"); + spec.setBaseType("ResourceSpecification"); + spec.setLifecycleStatus("Active"); + + // Register the specification + registerSpecification(spec); + + log.info("Successfully registered backhaul slice as LogicalResourceSpecification: {}", spec.getName()); + + } catch (Exception e) { + log.error("Error loading and registering backhaul slice: {}", fileName, e); + // Don't throw - allow bootstrap to continue + } + } + + /** + * Create and register example SliceService instances for each template retrieved from the RESTCONF provider. + * + * For each provider template, this method: + * 1. Creates a synthetic SliceService that references the template + * 2. Converts it to LogicalResource + * 3. Registers it in the TMF Resource Inventory + * + * This ensures that every template from the provider has at least one example + * slice service demonstrating its usage. + * + * @return List of LogicalResource instances created for provider templates + */ + public List createExampleSliceServicesForProviderTemplates() { + List createdServices = new ArrayList<>(); + + if (providerTemplates.isEmpty()) { + log.info("No provider templates available for example slice service creation"); + return createdServices; + } + + log.info("Creating example SliceService instances for {} provider templates", providerTemplates.size()); + + for (SloSleTemplate template : providerTemplates) { + try { + NetworkSliceServices networkSliceServices = createExampleSliceServiceForTemplate(template); + if (networkSliceServices != null) { + // Convert NetworkSliceServices to LogicalResourceSpecification + LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(networkSliceServices); + spec.setName( spec.getName() + "_fromTemplate_" + template.getEntityName()); + + if (spec != null) { + createdServices.add( spec ); + registerSpecification(spec); + } + } + } catch (Exception e) { + log.error("Error creating example slice service for template: {}", template.getId(), e); + // Continue with next template even if this one fails + } + } + + log.info("Successfully created and registered example slice SliceService for provider templates", + createdServices.size()); + + return createdServices; + } + + /** + * Create a synthetic example SliceService that references a specific template. + * + * This method generates a demo slice service with: + * - Unique ID (UUID-based) + * - Template reference pointing to the given template + * - Reasonable default configuration + * - Two SDPs with basic connectivity setup + * - One connection group for point-to-point connectivity + * + * @param template The SloSleTemplate to create an example service for + * @return NetworkSliceServices instance configured with the template + */ + private NetworkSliceServices createExampleSliceServiceForTemplate(SloSleTemplate template) { + try { + // Create unique service ID + String serviceId = "slice-service-" + UUID.randomUUID().toString(); + + SliceService service = new SliceService(); + service.setId(serviceId); + service.setDescription("Example network slice service using template '" + template.getId() + + "' from RESTCONF provider"); + service.setTestOnly(false); + + // Set template reference + service.setSloSleTemplate(template); + + // Set service status + ServiceStatus status = new ServiceStatus(); + status.setAdminState("admin-up"); + status.setOperState("operational"); + service.setStatus(status); + + // Add service tag + ServiceTag tag = new ServiceTag(); + tag.setValue("provider-template:" + template.getId()); + service.getServiceTags().add(tag); + + // Wrap in NetworkSliceServices + NetworkSliceServices networkSliceServices = new NetworkSliceServices(); + List services = new ArrayList<>(); + services.add(service); + networkSliceServices.setSliceServices(services); + + // Generate RFC 9543 formatted JSON with slice-service array + String exampleJson = buildExampleSliceServiceJson(serviceId, template.getId()); + networkSliceServices.setJsonRequest(exampleJson); + + log.debug("Created example NetworkSliceServices with service {} for template {}", serviceId, template.getId()); + return networkSliceServices; + + } catch (Exception e) { + log.error("Error creating example NetworkSliceServices for template: {}", template.getId(), e); + return null; + } + } + + /** + * Build a synthetic example slice service JSON structure. + * + * Creates a minimal but valid RFC 9543 slice-service JSON (without namespace wrapper) + * that demonstrates how to use a specific template. + * + * @param serviceId The service ID + * @param templateId The template ID to reference + * @return JSON string representing the slice service + */ + private String buildExampleSliceServiceJson(String serviceId, String templateId) { + try { + ObjectMapper mapper = new ObjectMapper(); + + // Build the slice service JSON object + com.fasterxml.jackson.databind.node.ObjectNode sliceService = + mapper.createObjectNode(); + + sliceService.put("id", serviceId); + sliceService.put("description", "Example network slice service using template '" + + templateId + "' from RESTCONF provider"); + + // Add SLO/SLE template reference + com.fasterxml.jackson.databind.node.ObjectNode sloSlePolicy = + mapper.createObjectNode(); + sloSlePolicy.put("slo-sle-template", templateId); + sliceService.set("slo-sle-policy", sloSlePolicy); + + // Add service tags + com.fasterxml.jackson.databind.node.ObjectNode serviceTags = + mapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode tagTypes = + mapper.createArrayNode(); + + com.fasterxml.jackson.databind.node.ObjectNode tagType = + mapper.createObjectNode(); + tagType.put("tag-type", "provider-template"); + com.fasterxml.jackson.databind.node.ArrayNode tagValues = + mapper.createArrayNode(); + tagValues.add(templateId); + tagType.set("tag-type-value", tagValues); + tagTypes.add(tagType); + + serviceTags.set("tag-type", tagTypes); + sliceService.set("service-tags", serviceTags); + + // Add status + com.fasterxml.jackson.databind.node.ObjectNode status = + mapper.createObjectNode(); + sliceService.set("status", status); + + // Add minimal SDPs (two example service demarcation points) + com.fasterxml.jackson.databind.node.ObjectNode sdps = + mapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode sdpArray = + mapper.createArrayNode(); + + // SDP 1 + com.fasterxml.jackson.databind.node.ObjectNode sdp1 = + mapper.createObjectNode(); + sdp1.put("node-id", "example-node-1"); + sdp1.put("sdp-ip-address", "10.0.1.1"); + com.fasterxml.jackson.databind.node.ObjectNode matchCriteria1 = + mapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode matchArray1 = + mapper.createArrayNode(); + com.fasterxml.jackson.databind.node.ObjectNode match1 = + mapper.createObjectNode(); + match1.put("index", 1); + match1.put("match-type", "VLAN"); + match1.put("value", "100"); + match1.put("target-connection-group-id", "example-connection"); + matchArray1.add(match1); + matchCriteria1.set("match-criterion", matchArray1); + sdp1.set("service-match-criteria", matchCriteria1); + sdpArray.add(sdp1); + + // SDP 2 + com.fasterxml.jackson.databind.node.ObjectNode sdp2 = + mapper.createObjectNode(); + sdp2.put("node-id", "example-node-2"); + sdp2.put("sdp-ip-address", "10.0.2.1"); + com.fasterxml.jackson.databind.node.ObjectNode matchCriteria2 = + mapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode matchArray2 = + mapper.createArrayNode(); + com.fasterxml.jackson.databind.node.ObjectNode match2 = + mapper.createObjectNode(); + match2.put("index", 1); + match2.put("match-type", "VLAN"); + match2.put("value", "100"); + match2.put("target-connection-group-id", "example-connection"); + matchArray2.add(match2); + matchCriteria2.set("match-criterion", matchArray2); + sdp2.set("service-match-criteria", matchCriteria2); + sdpArray.add(sdp2); + + sdps.set("sdp", sdpArray); + sliceService.set("sdps", sdps); + + // Add connection group + com.fasterxml.jackson.databind.node.ObjectNode connectionGroups = + mapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode connGroupArray = + mapper.createArrayNode(); + + com.fasterxml.jackson.databind.node.ObjectNode connGroup = + mapper.createObjectNode(); + connGroup.put("id", "example-connection"); + connGroup.put("connectivity-type", "ietf-vpn-common:any-to-any"); + com.fasterxml.jackson.databind.node.ArrayNode constructs = + mapper.createArrayNode(); + com.fasterxml.jackson.databind.node.ObjectNode construct = + mapper.createObjectNode(); + construct.put("id", 1); + com.fasterxml.jackson.databind.node.ArrayNode sdpIds = + mapper.createArrayNode(); + sdpIds.add(mapper.createObjectNode().put("sdp-id", "01")); + sdpIds.add(mapper.createObjectNode().put("sdp-id", "02")); + construct.set("a2a-sdp", sdpIds); + constructs.add(construct); + connGroup.set("connectivity-construct", constructs); + com.fasterxml.jackson.databind.node.ObjectNode connStatus = + mapper.createObjectNode(); + connGroup.set("status", connStatus); + connGroupArray.add(connGroup); + + connectionGroups.set("connection-group", connGroupArray); + sliceService.set("connection-groups", connectionGroups); + + // Wrap in RFC 9543 format with slice-service array + com.fasterxml.jackson.databind.node.ObjectNode wrapper = mapper.createObjectNode(); + com.fasterxml.jackson.databind.node.ArrayNode sliceServiceArray = mapper.createArrayNode(); + sliceServiceArray.add(sliceService); + wrapper.set("slice-service", sliceServiceArray); + + return mapper.writeValueAsString(wrapper); + + } catch (Exception e) { + log.error("Error building example slice service JSON for template: {}", templateId, e); + return null; + } + } + + /** + * Create Bronze tier template specification. + * + * Bronze characteristics: + * - Availability: 99% (43 minutes downtime per month) + * - Bandwidth: 100 Mbps minimum + * - Latency: 200ms maximum + * - Packet Loss: 0.1% maximum + * - MTU: 1500 bytes (standard Ethernet) + * - Isolation: Traffic isolation only + * - Use case: Cost-optimized, non-critical services + */ + private LogicalResourceSpecification createBronzeTemplateSpecification() { + SloSleTemplate template = new SloSleTemplate(); + template.setId("bronze-template"); + template.setDescription("Bronze-tier template - Best-effort service with basic guarantees"); + + // Create SLO Policy + SloPolicy sloPolicy = new SloPolicy(); + + // Set availability: 99% per month (43 minutes downtime) + AvailabilityType availability = new AvailabilityType(); + availability.setAvailabilityPercentage(99.0); + availability.setCommitmentPeriod("per-month"); + availability.setAllowedDowntimeMinutes(43L); + sloPolicy.setAvailability(availability); + + // Set MTU + sloPolicy.setMtu(1500L); + + // Add metric bounds + MetricBound bandwidthBound = new MetricBound( + ServiceSloMetricType.ONE_WAY_BANDWIDTH, + "Mbps", + 100L // 100 Mbps minimum + ); + bandwidthBound.setValueDescription("Minimum bandwidth guarantee"); + sloPolicy.addMetricBound(bandwidthBound); + + MetricBound latencyBound = new MetricBound( + ServiceSloMetricType.ONE_WAY_DELAY_MAXIMUM, + "ms", + 200L // 200 ms maximum + ); + latencyBound.setValueDescription("Maximum one-way latency"); + sloPolicy.addMetricBound(latencyBound); + + MetricBound packetLossBound = new MetricBound( + ServiceSloMetricType.ONE_WAY_PACKET_LOSS, + "%", + 1L // 0.1% maximum + ); + packetLossBound.setValueDescription("Maximum packet loss rate"); + sloPolicy.addMetricBound(packetLossBound); + + template.setSloPolicy(sloPolicy); + + // Create SLE Policy + SlePolicy slePolicy = new SlePolicy(); + slePolicy.addIsolationRequirement(ServiceIsolationType.TRAFFIC_ISOLATION); + slePolicy.setMaxOccupancyLevel((short) 50); // Max 50% of resources + template.setSlePolicy(slePolicy); + + LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); + + //Override Name, to show NSC templates as specnames... + spec.setName(template.getEntityName() ); + + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(categoryConfig.getVersion()); + spec.setDescription("Resource specification for Bronze-tier SLO/SLE templates. " + + "Bronze templates define best-effort network slice services suitable for cost-optimized, non-critical deployments with basic guarantees."); + + return spec; + } + + /** + * Create Silver tier template specification. + * + * Silver characteristics: + * - Availability: 99.9% (4 minutes downtime per month) + * - Bandwidth: 1 Gbps minimum + * - Latency: 100ms (50th percentile), 150ms (95th percentile) + * - Packet Loss: 0.01% maximum (99th percentile) + * - MTU: 1500 bytes (standard Ethernet) + * - Isolation: Traffic and logical isolation + * - Security: Authentication required + * - Use case: Standard service, most common deployments + */ + private LogicalResourceSpecification createSilverTemplateSpecification() { + SloSleTemplate template = new SloSleTemplate(); + template.setId("silver-template"); + template.setDescription("Silver-tier template - Standard service with strong guarantees"); + + // Create SLO Policy + SloPolicy sloPolicy = new SloPolicy(); + + // Set availability: 99.9% per month (4 minutes downtime) + AvailabilityType availability = new AvailabilityType(); + availability.setAvailabilityPercentage(99.9); + availability.setCommitmentPeriod("per-month"); + availability.setAllowedDowntimeMinutes(4L); + sloPolicy.setAvailability(availability); + + // Set MTU + sloPolicy.setMtu(1500L); + + // Add metric bounds + MetricBound silverBandwidth = new MetricBound( + ServiceSloMetricType.ONE_WAY_BANDWIDTH, + "Gbps", + 1000L // 1000 Mbps (1 Gbps) minimum + ); + silverBandwidth.setValueDescription("Minimum bandwidth guarantee"); + sloPolicy.addMetricBound(silverBandwidth); + + // One-way delay at 50th percentile (median) + sloPolicy.addMetricBound(new MetricBound( + ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, + "ms", + new BigDecimal("50.0"), + 100L // 100 ms at 50th percentile + )); + + // One-way delay at 95th percentile + sloPolicy.addMetricBound(new MetricBound( + ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, + "ms", + new BigDecimal("95.0"), + 150L // 150 ms at 95th percentile + )); + + // Packet loss at 99th percentile + sloPolicy.addMetricBound(new MetricBound( + ServiceSloMetricType.ONE_WAY_PACKET_LOSS, + "%", + new BigDecimal("99.0"), + 1L // 0.01% at 99th percentile + )); + + template.setSloPolicy(sloPolicy); + + // Create SLE Policy + SlePolicy slePolicy = new SlePolicy(); + slePolicy.addSecurityRequirement(ServiceSecurityType.AUTHENTICATION_REQUIRED); + slePolicy.addIsolationRequirement(ServiceIsolationType.TRAFFIC_ISOLATION); + slePolicy.addIsolationRequirement(ServiceIsolationType.LOGICAL_ISOLATION); + slePolicy.setMaxOccupancyLevel((short) 75); // Max 75% of resources + template.setSlePolicy(slePolicy); + + LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); + + //Override Name, to show NSC templates as specnames... + spec.setName(template.getEntityName() ); + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(categoryConfig.getVersion()); + spec.setDescription("Resource specification for Silver-tier SLO/SLE templates. " + + "Silver templates define standard network slice services suitable for most deployments with strong performance and security guarantees."); + + return spec; + } + + /** + * Create Gold tier template specification. + * + * Gold characteristics: + * - Availability: 99.99% (4 seconds downtime per month) + * - Bandwidth: 10 Gbps minimum + * - Latency: 30ms (50th percentile), 50ms (95th percentile), 80ms (99.9th percentile) + * - Packet Loss: 0.0001% maximum (99.99th percentile) + * - Delay Variation: 10ms maximum + * - MTU: 9000 bytes (jumbo frames for high performance) + * - Isolation: Full resource, dedicated resources + * - Security: Encryption, authentication, and integrity protection required + * - Use case: Mission-critical services, premium customers + */ + private LogicalResourceSpecification createGoldTemplateSpecification() { + SloSleTemplate template = new SloSleTemplate(); + template.setId("gold-template"); + template.setDescription("Gold-tier template - Premium service with highest guarantees"); + + // Create SLO Policy + SloPolicy sloPolicy = new SloPolicy(); + + // Set availability: 99.99% per month (4 seconds downtime) + AvailabilityType availability = new AvailabilityType(); + availability.setAvailabilityPercentage(99.99); + availability.setCommitmentPeriod("per-month"); + availability.setAllowedDowntimeMinutes(1L); + sloPolicy.setAvailability(availability); + + // Set MTU for high performance (jumbo frames) + sloPolicy.setMtu(9000L); + + // Add metric bounds + MetricBound goldBandwidth = new MetricBound( + ServiceSloMetricType.ONE_WAY_BANDWIDTH, + "Gbps", + 10000L // 10000 Mbps (10 Gbps) minimum + ); + goldBandwidth.setValueDescription("Minimum bandwidth guarantee"); + sloPolicy.addMetricBound(goldBandwidth); + + // One-way delay at 50th percentile (median) + sloPolicy.addMetricBound(new MetricBound( + ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, + "ms", + new BigDecimal("50.0"), + 30L // 30 ms at 50th percentile + )); + + // One-way delay at 95th percentile + sloPolicy.addMetricBound(new MetricBound( + ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, + "ms", + new BigDecimal("95.0"), + 50L // 50 ms at 95th percentile + )); + + // One-way delay at 99.9th percentile + sloPolicy.addMetricBound(new MetricBound( + ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, + "ms", + new BigDecimal("99.9"), + 80L // 80 ms at 99.9th percentile + )); + + // One-way delay variation (jitter) + MetricBound jitterBound = new MetricBound( + ServiceSloMetricType.ONE_WAY_DELAY_VARIATION_MAXIMUM, + "ms", + 10L // 10 ms maximum jitter + ); + jitterBound.setValueDescription("Maximum delay variation (jitter)"); + sloPolicy.addMetricBound(jitterBound); + + // Packet loss at 99.99th percentile + sloPolicy.addMetricBound(new MetricBound( + ServiceSloMetricType.ONE_WAY_PACKET_LOSS, + "%", + new BigDecimal("99.99"), + 1L // 0.0001% at 99.99th percentile + )); + + template.setSloPolicy(sloPolicy); + + // Create SLE Policy + SlePolicy slePolicy = new SlePolicy(); + slePolicy.addSecurityRequirement(ServiceSecurityType.ENCRYPTION_REQUIRED); + slePolicy.addSecurityRequirement(ServiceSecurityType.AUTHENTICATION_REQUIRED); + slePolicy.addSecurityRequirement(ServiceSecurityType.INTEGRITY_PROTECTION); + slePolicy.addIsolationRequirement(ServiceIsolationType.RESOURCE_ISOLATION); + slePolicy.addIsolationRequirement(ServiceIsolationType.DEDICATED_RESOURCES); + slePolicy.setMaxOccupancyLevel((short) 100); // Max 100% of resources (dedicated) + template.setSlePolicy(slePolicy); + + LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); + + //Override Name, to show NSC templates as specnames... + spec.setName(template.getEntityName() ); + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(categoryConfig.getVersion()); + spec.setDescription("Resource specification for Gold-tier SLO/SLE templates. " + + "Gold templates define premium network slice services suitable for mission-critical deployments with highest performance, security, and availability guarantees."); + + return spec; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/config/ActiveMQComponentConfig.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/config/ActiveMQComponentConfig.java new file mode 100644 index 0000000..54a7454 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/config/ActiveMQComponentConfig.java @@ -0,0 +1,22 @@ +package org.etsi.osl.controllers.ietf.ns.api.config; + +import org.apache.camel.component.activemq.ActiveMQComponent; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import jakarta.jms.ConnectionFactory; + +/** + * @author ctranoris + * + */ +@Configuration +public class ActiveMQComponentConfig { + + @Bean(name = "activemq") + public ActiveMQComponent createComponent(ConnectionFactory factory) { + ActiveMQComponent activeMQComponent = new ActiveMQComponent(); + activeMQComponent.setConnectionFactory(factory); + return activeMQComponent; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/AttachmentCircuit.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/AttachmentCircuit.java new file mode 100644 index 0000000..4022325 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/AttachmentCircuit.java @@ -0,0 +1,19 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents an attachment circuit for service demarcation points. + * Includes ingress and egress QoS policies. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AttachmentCircuit { + private String id; + private String description; + private QosPolicy ingressPolicy; + private QosPolicy egressPolicy; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectionGroup.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectionGroup.java new file mode 100644 index 0000000..87b7a90 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectionGroup.java @@ -0,0 +1,27 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a logical grouping of connectivity constructs. + * Defines the connectivity fabric per RFC 9543 with support for + * P2P (Point-to-Point), P2MP (Point-to-Multipoint), and A2A (Any-to-Any) patterns. + * Each group can apply a policy override relative to the SliceService-level SLO/SLE template. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ConnectionGroup { + private String id; + private ConnectivityType connectivityType; + + // 0-to-1 relationship: optional SLO/SLE policy override at the group level + private PolicyRef policyOverride; + + // 1-to-many relationship: contains multiple connectivity constructs + private List connectivityConstructs = new ArrayList<>(); +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityConstruct.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityConstruct.java new file mode 100644 index 0000000..06af7c8 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityConstruct.java @@ -0,0 +1,27 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a connectivity construct within a connection group. + * Defines a specific connectivity pattern (P2P, P2MP, A2A) and includes + * the SDP members that are part of this construct. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ConnectivityConstruct { + private String id; + private ConnectivityType type; + private ConstructStatus status; + + // 0-to-1 relationship: optional SLO/SLE policy override + private PolicyRef policyOverride; + + // Many-to-many relationship: members by reference + private List members = new ArrayList<>(); +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityType.java new file mode 100644 index 0000000..ad063be --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityType.java @@ -0,0 +1,13 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +/** + * Enumeration for connectivity types as per IETF Network Slice Service specification. + * P2P: Point-to-Point + * P2MP: Point-to-Multipoint + * A2A: Any-to-Any + */ +public enum ConnectivityType { + P2P, + P2MP, + A2A +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConstructStatus.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConstructStatus.java new file mode 100644 index 0000000..5bf1599 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConstructStatus.java @@ -0,0 +1,17 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents the status of a connectivity construct. + * Includes operational state and reason for the current state. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ConstructStatus { + private String state; + private String reason; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/MatchCriterion.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/MatchCriterion.java new file mode 100644 index 0000000..c064745 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/MatchCriterion.java @@ -0,0 +1,22 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a match criterion for classifying traffic in service demarcation points. + * Used to identify and match traffic based on various attributes such as interface, + * VLAN, IP prefix, or MPLS label. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MatchCriterion { + private String id; + private MatchType matchType; + private String ifName; + private Integer vlanId; + private String ipPrefix; + private Long mplsLabel; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/MatchType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/MatchType.java new file mode 100644 index 0000000..01e4dcf --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/MatchType.java @@ -0,0 +1,12 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +/** + * Enumeration for match criteria types as per IETF Network Slice Service specification. + */ +public enum MatchType { + mt_any, + mt_interface, + mt_vlan, + mt_ip_prefix, + mt_mpls_label +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java new file mode 100644 index 0000000..15eb7e5 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java @@ -0,0 +1,117 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceMappable; +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import org.etsi.osl.tmf.ri639.model.ResourceAdministrativeStateType; +import org.etsi.osl.tmf.ri639.model.ResourceOperationalStateType; +import org.etsi.osl.tmf.ri639.model.ResourceStatusType; + +/** + * Root container for IETF Network Slice Services. + * Represents the top-level container for managing network slice services and their + * associated SLO/SLE templates as per draft-ietf-teas-ietf-network-slice-nbi-yang-25. + * + * Implements LogicalResourceMappable to enable conversion to TMF LogicalResource instances. + * The jsonRequest field contains the RFC 9543 formatted JSON with slice-service array. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class NetworkSliceServices implements LogicalResourceMappable { + + /** + * RFC 9543 JSON request containing the slice-service array. + * Format: { "slice-service": [ {...}, {...} ] } + */ + private String jsonRequest; + + // 1-to-many relationship: contains multiple SLO/SLE templates + private List sloSleTemplates = new ArrayList<>(); + + // 1-to-many relationship: contains multiple slice services + private List sliceServices = new ArrayList<>(); + + // ========== LogicalResourceMappable Implementation ========== + + /** + * Get the unique identifier - uses the first slice service ID if available. + */ + @Override + public String getEntityId() { + if (sliceServices != null && !sliceServices.isEmpty()) { + return sliceServices.get(0).getId(); + } + return "unknown"; + } + + /** + * Get the display name - uses the first slice service ID if available. + */ + @Override + public String getEntityName() { + if (sliceServices != null && !sliceServices.isEmpty()) { + return sliceServices.get(0).getId(); + } + return "Network Slice Services"; + } + + /** + * Get the description - uses the first slice service description if available. + */ + @Override + public String getEntityDescription() { + if (sliceServices != null && !sliceServices.isEmpty() && sliceServices.get(0).getDescription() != null) { + return sliceServices.get(0).getDescription(); + } + return "IETF RFC 9543 Network Slice Services"; + } + + /** + * Check if this entity has status mapping. + */ + @Override + public boolean hasStatusMapping() { + return sliceServices != null && !sliceServices.isEmpty() && sliceServices.get(0).getStatus() != null; + } + + /** + * Map service status to TMF639 resource states - uses the first slice service status. + */ + @Override + public void mapStatusToResourceStates(LogicalResource resource) { + if (sliceServices != null && !sliceServices.isEmpty()) { + SliceService firstService = sliceServices.get(0); + if (firstService.getStatus() != null) { + ServiceStatus status = firstService.getStatus(); + + // Map administrative state + String adminState = status.getAdminState(); + if (adminState != null && adminState.equals("admin-up")) { + resource.setAdministrativeState(ResourceAdministrativeStateType.UNLOCKED); + } else if (adminState != null && adminState.equals("admin-down")) { + resource.setAdministrativeState(ResourceAdministrativeStateType.LOCKED); + } + + // Map operational state + String operState = status.getOperState(); + if (operState != null && operState.equals("operational")) { + resource.setOperationalState(ResourceOperationalStateType.ENABLE); + } else if (operState != null && operState.equals("non-operational")) { + resource.setOperationalState(ResourceOperationalStateType.DISABLE); + } + + // Set resource status (active if operational, disabled otherwise) + if ("operational".equals(operState)) { + resource.setResourceStatus(ResourceStatusType.AVAILABLE); + } else { + resource.setResourceStatus(ResourceStatusType.SUSPENDED); + } + } + } + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/PolicyRef.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/PolicyRef.java new file mode 100644 index 0000000..c3b1fb1 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/PolicyRef.java @@ -0,0 +1,17 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a reference to an SLO/SLE policy template. + * Used to override default SLO/SLE policies at ConnectionGroup or ConnectivityConstruct levels. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PolicyRef { + private String templateId; + private String note; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/QosPolicy.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/QosPolicy.java new file mode 100644 index 0000000..14498f9 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/QosPolicy.java @@ -0,0 +1,20 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents Quality of Service (QoS) policy parameters. + * Includes class selector, committed information rate (CIR), peak information rate (PIR), + * and peak burst size (PBS). + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class QosPolicy { + private String classSelector; + private String cir; // bandwidth + private String pir; // bandwidth + private String pbs; // size +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializer.java new file mode 100644 index 0000000..e92f8a4 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializer.java @@ -0,0 +1,283 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Custom Jackson deserializer for RFC 9543 formatted SliceService JSON. + * + * Maps RFC 9543 hyphenated field names to Java camelCase property names: + * - "service-tags" → serviceTags + * - "slo-sle-policy" → sloSleTemplate reference + * - "connection-groups" → connectionGroups + * - "sdp-peering" → sdpPeering + * - etc. + * + * This allows seamless parsing of RFC 9543 network slice service definitions + * into the SliceService domain model. + */ +public class Rfc9543SliceServiceDeserializer extends StdDeserializer { + + private static final Logger logger = LoggerFactory.getLogger(Rfc9543SliceServiceDeserializer.class); + + public Rfc9543SliceServiceDeserializer() { + super(SliceService.class); + } + + @Override + public SliceService deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + return parseSliceService(node); + } + + /** + * Parse RFC 9543 formatted JSON node into SliceService object. + * + * @param node JSON node representing a slice service + * @return SliceService object populated from the JSON + */ + private SliceService parseSliceService(JsonNode node) { + SliceService service = new SliceService(); + + // Parse basic properties + if (node.has("id")) { + service.setId(node.get("id").asText()); + } + + if (node.has("description")) { + service.setDescription(node.get("description").asText()); + } + + if (node.has("testOnly")) { + service.setTestOnly(node.get("testOnly").asBoolean(false)); + } + + // Parse service tags (RFC 9543: "service-tags") + if (node.has("service-tags")) { + List tags = parseServiceTags(node.get("service-tags")); + service.getServiceTags().addAll(tags); + logger.debug("Parsed {} service tags for slice service: {}", tags.size(), service.getId()); + } + + // Parse SLO/SLE template reference (RFC 9543: "slo-sle-policy") + if (node.has("slo-sle-policy")) { + JsonNode policyNode = node.get("slo-sle-policy"); + if (policyNode.has("slo-sle-template")) { + SloSleTemplate template = new SloSleTemplate(); + template.setId(policyNode.get("slo-sle-template").asText()); + service.setSloSleTemplate(template); + logger.debug("Parsed SLO/SLE template reference: {}", template.getId()); + } + } + + // Parse status + if (node.has("status")) { + ServiceStatus status = parseStatus(node.get("status")); + service.setStatus(status); + } + + // Parse SDPs (Service Demarcation Points) - RFC 9543: "sdps" + if (node.has("sdps")) { + List sdps = parseSdps(node.get("sdps")); + service.getSdps().addAll(sdps); + logger.debug("Parsed {} SDPs for slice service: {}", sdps.size(), service.getId()); + } + + // Parse connection groups - RFC 9543: "connection-groups" + if (node.has("connection-groups")) { + List connGroups = parseConnectionGroups(node.get("connection-groups")); + service.getConnectionGroups().addAll(connGroups); + logger.debug("Parsed {} connection groups for slice service: {}", connGroups.size(), + service.getId()); + } + + logger.debug("Successfully parsed RFC 9543 SliceService: {}", service.getId()); + return service; + } + + /** + * Parse service tags from RFC 9543 format. + * + * RFC 9543 structure: { "tag-type": [ { "tag-type": "service", "tag-type-value": ["L2"] } ] } + */ + private List parseServiceTags(JsonNode tagsNode) { + List tags = new ArrayList<>(); + + if (tagsNode.has("tag-type") && tagsNode.get("tag-type").isArray()) { + for (JsonNode tagTypeNode : tagsNode.get("tag-type")) { + String tagType = tagTypeNode.has("tag-type") ? tagTypeNode.get("tag-type").asText() : ""; + + if (tagTypeNode.has("tag-type-value") && tagTypeNode.get("tag-type-value").isArray()) { + for (JsonNode valueNode : tagTypeNode.get("tag-type-value")) { + String value = valueNode.asText(); + ServiceTag tag = new ServiceTag(); + if (!tagType.isEmpty()) { + tag.setValue(tagType + ":" + value); + } else { + tag.setValue(value); + } + tags.add(tag); + } + } + } + } + + return tags; + } + + /** + * Parse service status from JSON node. + */ + private ServiceStatus parseStatus(JsonNode statusNode) { + ServiceStatus status = new ServiceStatus(); + + if (statusNode.has("admin-state")) { + status.setAdminState(statusNode.get("admin-state").asText()); + } else { + status.setAdminState("admin-up"); // Default + } + + if (statusNode.has("oper-state")) { + status.setOperState(statusNode.get("oper-state").asText()); + } else { + status.setOperState("operational"); // Default + } + + return status; + } + + /** + * Parse SDPs (Service Demarcation Points) from RFC 9543 format. + */ + private List parseSdps(JsonNode sdpsNode) { + List sdps = new ArrayList<>(); + + if (sdpsNode.has("sdp") && sdpsNode.get("sdp").isArray()) { + for (JsonNode sdpNode : sdpsNode.get("sdp")) { + SDP sdp = parseSdp(sdpNode); + sdps.add(sdp); + } + } + + return sdps; + } + + /** + * Parse individual SDP from JSON node. + */ + private SDP parseSdp(JsonNode sdpNode) { + SDP sdp = new SDP(); + + if (sdpNode.has("id")) { + sdp.setId(sdpNode.get("id").asText()); + } + + if (sdpNode.has("node-id")) { + sdp.setNodeId(sdpNode.get("node-id").asText()); + } + + if (sdpNode.has("sdp-ip-address")) { + String ipAddress = sdpNode.get("sdp-ip-address").asText(); + sdp.getSdpIpAddress().add(ipAddress); + } + + if (sdpNode.has("geo-location")) { + sdp.setGeoLocation(sdpNode.get("geo-location").asText()); + } + + if (sdpNode.has("tp-ref")) { + sdp.setTpRef(sdpNode.get("tp-ref").asText()); + } + + // Parse service match criteria + if (sdpNode.has("service-match-criteria")) { + // For now, just log that we found it + logger.debug("Found service-match-criteria in SDP: {}", sdp.getId()); + } + + // Parse attachment circuits + if (sdpNode.has("attachment-circuits")) { + JsonNode circuitsNode = sdpNode.get("attachment-circuits"); + if (circuitsNode.has("attachment-circuit") && circuitsNode.get("attachment-circuit").isArray()) { + logger.debug("Found {} attachment circuits in SDP: {}", + circuitsNode.get("attachment-circuit").size(), sdp.getId()); + } + } + + return sdp; + } + + /** + * Parse connection groups from RFC 9543 format. + */ + private List parseConnectionGroups(JsonNode connGroupsNode) { + List connGroups = new ArrayList<>(); + + if (connGroupsNode.has("connection-group") && connGroupsNode.get("connection-group").isArray()) { + for (JsonNode connGroupNode : connGroupsNode.get("connection-group")) { + ConnectionGroup connGroup = parseConnectionGroup(connGroupNode); + connGroups.add(connGroup); + } + } + + return connGroups; + } + + /** + * Parse individual connection group from JSON node. + */ + private ConnectionGroup parseConnectionGroup(JsonNode connGroupNode) { + ConnectionGroup connGroup = new ConnectionGroup(); + + if (connGroupNode.has("id")) { + connGroup.setId(connGroupNode.get("id").asText()); + } + + if (connGroupNode.has("connectivity-type")) { + String typeStr = connGroupNode.get("connectivity-type").asText(); + connGroup.setConnectivityType(parseConnectivityType(typeStr)); + } + + // Parse connectivity constructs + if (connGroupNode.has("connectivity-construct") && + connGroupNode.get("connectivity-construct").isArray()) { + logger.debug("Found {} connectivity constructs in connection group: {}", + connGroupNode.get("connectivity-construct").size(), connGroup.getId()); + } + + return connGroup; + } + + /** + * Parse RFC 9543 connectivity type string to ConnectivityType enum. + * RFC 9543 uses strings like "ietf-vpn-common:any-to-any" + * Maps to P2P, P2MP, or A2A + */ + private ConnectivityType parseConnectivityType(String typeStr) { + if (typeStr == null || typeStr.isEmpty()) { + logger.debug("Empty connectivity type, defaulting to ANY_TO_ANY"); + return ConnectivityType.A2A; + } + + String normalized = typeStr.toLowerCase(); + + if (normalized.contains("point-to-point") || normalized.contains("p2p")) { + return ConnectivityType.P2P; + } else if (normalized.contains("point-to-multipoint") || normalized.contains("p2mp")) { + return ConnectivityType.P2MP; + } else if (normalized.contains("any-to-any") || normalized.contains("a2a")) { + return ConnectivityType.A2A; + } else { + logger.warn("Unknown connectivity type: {}, defaulting to ANY_TO_ANY", typeStr); + return ConnectivityType.A2A; + } + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SDP.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SDP.java new file mode 100644 index 0000000..ad5d1b1 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SDP.java @@ -0,0 +1,30 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Service Demarcation Point (SDP) - represents an edge point of a network slice service. + * SDPs are where customer traffic enters and exits the slice, and can be associated with + * RFC8345 topology termination points. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SDP { + private String id; + private String description; + private String nodeId; + private String tpRef; // leafref into topology TP + private List sdpIpAddress = new ArrayList<>(); // ip-address list + private String geoLocation; + + // 1-to-many relationship: SDP contains multiple match criteria + private List serviceMatchCriteria = new ArrayList<>(); + + // 0-to-1 relationship: optional attachment circuit + private AttachmentCircuit attachmentCircuit; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SdpMember.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SdpMember.java new file mode 100644 index 0000000..64f9def --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SdpMember.java @@ -0,0 +1,16 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a reference to an SDP that is a member of a connectivity construct. + * Used for logical membership relationships (P2P, P2MP, A2A) in connectivity constructs. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SdpMember { + private SDP sdpRef; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceStatus.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceStatus.java new file mode 100644 index 0000000..4d989be --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceStatus.java @@ -0,0 +1,19 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import java.time.OffsetDateTime; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents the status of a network slice service. + * Includes administrative state, operational state, and last change timestamp. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ServiceStatus { + private String adminState; + private String operState; + private OffsetDateTime lastChange; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceTag.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceTag.java new file mode 100644 index 0000000..78dcae3 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceTag.java @@ -0,0 +1,17 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a service tag for network slice services. + * Tags can be of different types (customer, service, opaque) and carry a value. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ServiceTag { + private ServiceTagType type; + private String value; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceTagType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceTagType.java new file mode 100644 index 0000000..0626f0b --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceTagType.java @@ -0,0 +1,10 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +/** + * Enumeration for service tag types as per IETF Network Slice Service specification. + */ +public enum ServiceTagType { + customer, + service, + opaque +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SliceService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SliceService.java new file mode 100644 index 0000000..1b7d94b --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SliceService.java @@ -0,0 +1,42 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a customer-facing Network Slice Service (NSS). + * Includes service configuration, status, and all connectivity and resource parameters. + * Supports optional test-only feasibility mode and service-level SLO/SLE template reference. + * + * Uses custom RFC 9543 deserializer to handle hyphenated field names in JSON: + * - "service-tags" → serviceTags + * - "slo-sle-policy" → sloSleTemplate + * - "connection-groups" → connectionGroups + * - etc. + */ +@JsonDeserialize(using = Rfc9543SliceServiceDeserializer.class) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SliceService { + private String id; + private String description; + private Boolean testOnly = false; + private ServiceStatus status; + + // 0-to-1 relationship: references an SLO/SLE template at the service level + private SloSleTemplate sloSleTemplate; + + // 0..* relationship: service tags for classification and management + private List serviceTags = new ArrayList<>(); + + // 1-to-many relationship: contains multiple SDPs (service demarcation points) + private List sdps = new ArrayList<>(); + + // 1-to-many relationship: contains multiple connection groups + private List connectionGroups = new ArrayList<>(); +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java new file mode 100644 index 0000000..c76a382 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java @@ -0,0 +1,156 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SlePolicy; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SliceTemplateRef; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; +import org.etsi.osl.controllers.ietf.ns.domain.common.ExcludeFromMapping; +import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceMappable; +import org.etsi.osl.controllers.ietf.ns.domain.common.RelatedManagedResourceReference; +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.Transient; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a reusable Service Level Objective (SLO) and Service Level Expectation (SLE) template. + * + * SLO/SLE templates encapsulate comprehensive service level definitions including: + * - SLO (Service Level Objectives): Performance metrics and targets + * - Availability percentage + * - Maximum Transmission Unit (MTU) + * - Performance metric bounds (latency, bandwidth, jitter, loss, etc.) + * + * - SLE (Service Level Expectations): Service assurance characteristics + * - Security requirements + * - Isolation requirements + * - Maximum occupancy level + * - Path constraints and diversity + * + * These templates can be referenced and reused by: + * - SliceService (service-level default policy) + * - ConnectionGroup (group-level policy) + * - ConnectivityConstruct (construct-level policy) + * + * Template composition is supported through SliceTemplateRef, allowing + * templates to reference other templates to build composite policies. + * + * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 + * /network-slice-services/slo-sle-templates/slo-sle-template + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Slf4j +public class SloSleTemplate implements LogicalResourceMappable, RelatedManagedResourceReference { + + /** + * Unique identifier for the SLO/SLE template. + * Must be unique within the network-slice-services container. + */ + private String id; + + /** + * Human-readable description of the template. + * Explains the purpose and use case of the template. + * Optional (0..1 cardinality). + */ + @ExcludeFromMapping(reason = "Mapped as LogicalResource.description") + private String description; + + /** + * Service Level Objectives (SLO) policy. + * Contains performance targets and constraints: + * - Availability percentage + * - MTU size + * - Performance metric bounds + * + * Mandatory containment (1..1 cardinality). + */ + private SloPolicy sloPolicy; + + /** + * Service Level Expectations (SLE) policy. + * Contains service assurance characteristics: + * - Security requirements + * - Isolation requirements + * - Maximum occupancy level + * - Path constraints + * + * Mandatory containment (1..1 cardinality). + */ + private SlePolicy slePolicy; + + + /** + * Reference to another SLO/SLE template for composition. + * Allows templates to build upon existing templates by reference. + * Optional (0..1 cardinality). + * + * Use case: Creating specialized templates by extending a base template. + * Example: A "premium-bandwidth" template might reference a "gold-template" + * and add additional bandwidth constraints. + */ + private SliceTemplateRef templateRef; + + /** + * ID of the related managed resource (e.g., a related device resource, managed component from another e.g. kubernetes controller) + * References another managed resource that this device is related to + * This field can be set by clients to establish relationships to other managed resources + */ + private String relatedManagedResourceId; + + /** + * The related managed resource fetched from resource inventory according to the relatedManagedResourceId + * References another managed resource that this device is related to + * This field is read-only in JSON to maintain consistency with service-layer management + * Not persisted to the database - populated by the service layer from the TMF Resource Inventory + */ + @ExcludeFromMapping(reason = "Service-managed relationship, not part of mapping") + @Transient + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private LogicalResource relatedManagedResource; + + + + + /** + * Creates a template with SLO and SLE policies. + * Convenience constructor for common template creation. + */ + public SloSleTemplate(String id, String description, SloPolicy sloPolicy, SlePolicy slePolicy) { + this.id = id; + this.description = description; + this.sloPolicy = sloPolicy; + this.slePolicy = slePolicy; + } + + /** + * Creates a template with reference to another template. + * Convenience constructor for composite template creation. + */ + public SloSleTemplate(String id, SliceTemplateRef templateRef) { + this.id = id; + this.templateRef = templateRef; + } + + @Override + public String getEntityId() { + return this.id; + } + + @Override + public String getEntityName() { + return this.id; //name is equal to id + } + + @Override + public String getEntityDescription() { + return this.description; + } + + + +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/AvailabilityType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/AvailabilityType.java new file mode 100644 index 0000000..1382695 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/AvailabilityType.java @@ -0,0 +1,35 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents availability constraints for a network slice service. + * + * Availability metrics express the percentage of time the service + * is expected to be operational and accessible. This includes both + * the service availability percentage and the commitment period. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AvailabilityType { + /** + * Availability percentage (0.0 to 100.0) + * Examples: 99.0, 99.9, 99.99, 99.999 + */ + private Double availabilityPercentage; + + /** + * Commitment period for the availability SLO + * Examples: "per-month", "per-year", "per-service-life" + */ + private String commitmentPeriod; + + /** + * Downtime allowed per period in minutes + * Calculated as (100 - availabilityPercentage) * periodLength + */ + private Long allowedDowntimeMinutes; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/Diversity.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/Diversity.java new file mode 100644 index 0000000..b0c7d06 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/Diversity.java @@ -0,0 +1,35 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents path diversity constraints for network slices. + * + * Diversity constraints ensure that connectivity constructs within a + * network slice service follow diverse paths through the network, providing + * protection against single points of failure (SPOF). + * + * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 and + * te-types:te-path-disjointness definitions. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Diversity { + + /** + * Type of path disjointness requirement. + * Defines the level of separation required between paths. + * + * Examples: + * - LINK: Paths must not share any link + * - NODE: Paths must not share any intermediate node + * - SRLG: Paths must not share any Shared Risk Link Group + * - LINK_AND_NODE: Paths must be both link-disjoint and node-disjoint + * + * Optional (0..1 cardinality). + */ + private TePathDisjointness diversityType; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java new file mode 100644 index 0000000..ba42fb6 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java @@ -0,0 +1,101 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +import java.math.BigDecimal; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a metric bound for Service Level Objectives (SLOs). + * + * A metric bound defines upper limits (or thresholds) for a specific + * performance metric. Each bound includes: + * - The metric type (e.g., latency, bandwidth, loss) + * - The measurement unit (e.g., ms, Mbps, %) + * - The upper limit value + * - Optional percentile value for percentile-based metrics + * + * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 MetricBound definition. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MetricBound { + + /** + * Type of SLO metric being bounded. + * Examples: ONE_WAY_DELAY_MAXIMUM, TWO_WAY_BANDWIDTH, ONE_WAY_PACKET_LOSS + */ + private ServiceSloMetricType metricType; + + /** + * Unit of measurement for the metric. + * Mandatory field per YANG specification. + * + * Examples: + * - For bandwidth: "bps", "Kbps", "Mbps", "Gbps" + * - For delay: "ms", "us", "ns" + * - For loss: "%" + */ + private String metricUnit; + + /** + * Optional human-readable description of the metric bound. + * Useful for documenting the purpose and context of the metric. + * Example: "Maximum one-way latency between customer sites" + */ + private String valueDescription; + + /** + * Percentile value for percentile-based metrics (0.0 to 100.0). + * Optional field with 3 decimal places precision. + * + * Examples: + * - 50.0 = median (50th percentile) + * - 95.0 = 95th percentile + * - 99.9 = 99.9th percentile + * + * Only applies to percentile metric types: + * - ONE_WAY_DELAY_PERCENTILE + * - TWO_WAY_DELAY_PERCENTILE + * - ONE_WAY_DELAY_VARIATION_PERCENTILE + * - TWO_WAY_DELAY_VARIATION_PERCENTILE + */ + private BigDecimal percentileValue; + + /** + * Upper bound limit for the metric. + * Value is uint64 (0 to 18,446,744,073,709,551,615). + * + * Value interpretation depends on metric type and unit: + * - 0 indicates unbounded (no upper limit) + * - Positive value is the maximum allowed value + * + * Examples: + * - For latency: bound=50 with metricUnit="ms" means max 50 milliseconds + * - For bandwidth: bound=1000 with metricUnit="Mbps" means at least 1000 Mbps + * - For loss: bound=0.1 with metricUnit="%" means max 0.1% loss + */ + private Long bound; + + /** + * Creates a metric bound with type, unit, and bound value. + * Convenience constructor for simple metrics. + */ + public MetricBound(ServiceSloMetricType metricType, String metricUnit, Long bound) { + this.metricType = metricType; + this.metricUnit = metricUnit; + this.bound = bound; + } + + /** + * Creates a percentile-based metric bound. + * Convenience constructor for percentile metrics. + */ + public MetricBound(ServiceSloMetricType metricType, String metricUnit, BigDecimal percentile, Long bound) { + this.metricType = metricType; + this.metricUnit = metricUnit; + this.percentileValue = percentile; + this.bound = bound; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/PathConstraints.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/PathConstraints.java new file mode 100644 index 0000000..7d600a9 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/PathConstraints.java @@ -0,0 +1,40 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents path constraints for connectivity constructs within a network slice service. + * + * Path constraints define how connectivity constructs should be realized in the + * network, including: + * - Path diversity requirements (disjointness) + * - Service functions to apply along the path + * - Topology constraints + * + * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 PathConstraints definition. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PathConstraints { + + /** + * Path diversity constraints ensuring that connectivity constructs + * follow diverse paths through the network. + * Optional containment (0..1 cardinality). + */ + private Diversity diversity; + + /** + * Service functions to apply along the connectivity construct paths. + * Optional containment (0..1 cardinality). + */ + private ServiceFunctions serviceFunctions; + + /** + * Additional path constraint parameters can be extended here + * as needed for specific implementations. + */ +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceFunctions.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceFunctions.java new file mode 100644 index 0000000..d226894 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceFunctions.java @@ -0,0 +1,56 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents service functions and capabilities for a network slice service. + * + * Service functions define additional services or capabilities that can be + * applied to the network slice, such as: + * - Deep packet inspection + * - Firewall functions + * - Load balancing + * - Traffic steering + * - NAT/PAT + * - QoS enforcement + * + * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 ServiceFunctions definition. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ServiceFunctions { + + /** + * List of service function identifiers or references. + * Each entry represents a specific service function capability + * that can be applied to the network slice. + * + * Examples: + * - "firewall" + * - "deep-packet-inspection" + * - "load-balancer" + * - "traffic-shaper" + * - "nat" + */ + private List functions = new ArrayList<>(); + + /** + * Ordered flag indicating whether service functions must be applied + * in a specific order (service function chaining). + * If true, functions are applied in the order they appear in the list. + */ + private Boolean ordered = false; + + /** + * Adds a service function to the list. + * Convenience method. + */ + public void addFunction(String function) { + this.functions.add(function); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceIsolationType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceIsolationType.java new file mode 100644 index 0000000..efc70d4 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceIsolationType.java @@ -0,0 +1,24 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +/** + * Enumeration of Service Level Expectation (SLE) isolation types. + * + * These types correspond to YANG identities in the + * draft-ietf-teas-ietf-network-slice-nbi-yang-25 specification and + * define isolation requirements that can be applied to network slice services. + * + * Isolation types ensure that network slices do not interfere with each other + * and meet specific separation requirements such as: + * - Physical path isolation + * - Logical isolation + * - Resource isolation + * - Traffic isolation + */ +public enum ServiceIsolationType { + PHYSICAL_ISOLATION, + LOGICAL_ISOLATION, + TRAFFIC_ISOLATION, + RESOURCE_ISOLATION, + DEDICATED_RESOURCES, + SHARED_RESOURCES_LIMITED +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSecurityType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSecurityType.java new file mode 100644 index 0000000..52195e3 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSecurityType.java @@ -0,0 +1,23 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +/** + * Enumeration of Service Level Expectation (SLE) security types. + * + * These types correspond to YANG identities in the + * draft-ietf-teas-ietf-network-slice-nbi-yang-25 specification and + * define security requirements that can be applied to network slice services. + * + * Security types may include: + * - Encryption requirements + * - Authentication mechanisms + * - Access control policies + * - Data protection standards + */ +public enum ServiceSecurityType { + ENCRYPTION_REQUIRED, + AUTHENTICATION_REQUIRED, + INTEGRITY_PROTECTION, + CONFIDENTIALITY_REQUIRED, + SECURE_ROUTING, + VPN_REQUIRED +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSloMetricType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSloMetricType.java new file mode 100644 index 0000000..8b07aa7 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSloMetricType.java @@ -0,0 +1,42 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +/** + * Enumeration of Service Level Objective (SLO) metric types. + * + * These metric types correspond to YANG identities in the + * draft-ietf-teas-ietf-network-slice-nbi-yang-25 specification and + * define the various performance metrics that can be monitored and + * constrained in network slice services. + * + * Organized by category: + * - Bandwidth metrics + * - Delay metrics (absolute) + * - Delay variation metrics + * - Packet loss metrics + */ +public enum ServiceSloMetricType { + // Bandwidth metrics + ONE_WAY_BANDWIDTH, + TWO_WAY_BANDWIDTH, + SHARED_BANDWIDTH, + + // Delay (absolute) metrics + ONE_WAY_DELAY_MAXIMUM, + TWO_WAY_DELAY_MAXIMUM, + + // Delay (percentile) metrics + ONE_WAY_DELAY_PERCENTILE, + TWO_WAY_DELAY_PERCENTILE, + + // Delay variation (absolute) metrics + ONE_WAY_DELAY_VARIATION_MAXIMUM, + TWO_WAY_DELAY_VARIATION_MAXIMUM, + + // Delay variation (percentile) metrics + ONE_WAY_DELAY_VARIATION_PERCENTILE, + TWO_WAY_DELAY_VARIATION_PERCENTILE, + + // Packet loss metrics + ONE_WAY_PACKET_LOSS, + TWO_WAY_PACKET_LOSS +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java new file mode 100644 index 0000000..5525f4b --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java @@ -0,0 +1,100 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents Service Level Expectations (SLE) policies for a network slice service. + * + * SLEs define the service assurance and operational characteristics that + * the network slice should maintain. Each SLE consists of: + * - Security requirements + * - Isolation requirements + * - Maximum occupancy level + * - Path constraints (diversity, service functions) + * + * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 SlePolicy definition. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SlePolicy { + + /** + * List of security requirements for the network slice. + * Contains one or more ServiceSecurityType entries. + * + * Examples: + * - ENCRYPTION_REQUIRED + * - AUTHENTICATION_REQUIRED + * - INTEGRITY_PROTECTION + * - VPN_REQUIRED + * + * Optional leaf-list (can be empty). + */ + private List security = new ArrayList<>(); + + /** + * List of isolation requirements for the network slice. + * Contains one or more ServiceIsolationType entries. + * Ensures the slice does not interfere with other slices. + * + * Examples: + * - PHYSICAL_ISOLATION + * - LOGICAL_ISOLATION + * - TRAFFIC_ISOLATION + * - RESOURCE_ISOLATION + * + * Optional leaf-list (can be empty). + */ + private List isolation = new ArrayList<>(); + + /** + * Maximum occupancy level as a percentage (1-100). + * Indicates the maximum percentage of network resources + * that this slice can consume. Helps prevent resource exhaustion. + * + * Range: 1 to 100 + * Optional (0..1 cardinality). + * + * Examples: + * - 25 = max 25% of network resources + * - 50 = max 50% of network resources + * - 100 = can use all available resources + */ + private Short maxOccupancyLevel; + + /** + * Path constraints including diversity and service function requirements. + * Optional containment (0..1 cardinality). + */ + private PathConstraints pathConstraints; + + /** + * Adds a security requirement to the SLE policy. + * Convenience method. + */ + public void addSecurityRequirement(ServiceSecurityType securityType) { + this.security.add(securityType); + } + + /** + * Adds an isolation requirement to the SLE policy. + * Convenience method. + */ + public void addIsolationRequirement(ServiceIsolationType isolationType) { + this.isolation.add(isolationType); + } + + /** + * Creates an SLE policy with security and isolation requirements. + * Convenience constructor. + */ + public SlePolicy(List security, List isolation) { + this.security = security; + this.isolation = isolation; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SliceTemplateRef.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SliceTemplateRef.java new file mode 100644 index 0000000..54ce1a0 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SliceTemplateRef.java @@ -0,0 +1,29 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a reference to another SLO/SLE template. + * + * This typedef allows templates to reference other templates, enabling + * template composition and reuse. A SliceTemplateRef contains a leafref + * to another SloSleTemplate by its ID. + * + * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 SliceTemplateRef definition. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SliceTemplateRef { + + /** + * Reference to another SLO/SLE template ID. + * This is a leafref pointing to /network-slice-services/ + * slo-sle-templates/slo-sle-template/id + * + * Allows creating composite templates that build upon existing templates. + */ + private String ref; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java new file mode 100644 index 0000000..360f6e3 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java @@ -0,0 +1,83 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents Service Level Objectives (SLO) policies for a network slice service. + * + * SLOs define the target performance metrics that a network slice service + * should achieve. Each SLO consists of: + * - Availability constraints (uptime percentage) + * - Maximum Transmission Unit (MTU) size + * - One or more metric bounds (latency, bandwidth, loss, etc.) + * + * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 SloPolicy definition. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SloPolicy { + + /** + * Availability constraint for the service. + * Specifies the expected uptime percentage and commitment period. + * Optional (0..1 cardinality). + */ + private AvailabilityType availability; + + /** + * Maximum Transmission Unit (MTU) size in bytes. + * Specifies the largest packet size that can be transmitted + * through the network slice service. + * + * Examples: + * - 1500 (Ethernet standard) + * - 1518 (Ethernet with VLAN) + * - 9000 (Jumbo frames) + * - 65535 (Maximum IP packet size) + * + * Optional (0..1 cardinality). + */ + private Long mtu; + + /** + * List of metric bounds defining SLO constraints. + * Contains one or more MetricBound entries, each defining + * a performance metric limit (latency, bandwidth, loss, etc.). + * + * Examples of metric bounds: + * - Maximum latency: 50 ms + * - Minimum bandwidth: 1000 Mbps + * - Maximum packet loss: 0.001% + */ + private List metricBounds = new ArrayList<>(); + + /** + * Adds a metric bound to the SLO policy. + * Convenience method for building SLO policies programmatically. + */ + public void addMetricBound(MetricBound bound) { + this.metricBounds.add(bound); + } + + /** + * Creates a basic SLO policy with availability only. + * Convenience constructor. + */ + public SloPolicy(AvailabilityType availability) { + this.availability = availability; + } + + /** + * Creates an SLO policy with availability and MTU. + * Convenience constructor. + */ + public SloPolicy(AvailabilityType availability, Long mtu) { + this.availability = availability; + this.mtu = mtu; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/TePathDisjointness.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/TePathDisjointness.java new file mode 100644 index 0000000..7c5a710 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/TePathDisjointness.java @@ -0,0 +1,26 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; + +/** + * Enumeration of Traffic Engineering (TE) path disjointness constraints. + * + * These types correspond to YANG identities and te-types:te-path-disjointness + * in the draft-ietf-teas-ietf-network-slice-nbi-yang-25 specification. + * + * Path disjointness requirements ensure that connectivity constructs + * within a network slice service follow diverse paths through the network, + * providing protection against single points of failure. + * + * Disjointness levels: + * - LINK: Connectivity constructs must not share any link + * - NODE: Connectivity constructs must not share any intermediate node + * - SRLG: Connectivity constructs must not share any Shared Risk Link Group + */ +public enum TePathDisjointness { + LINK, + NODE, + SRLG, + LINK_AND_NODE, + LINK_AND_SRLG, + NODE_AND_SRLG, + LINK_NODE_AND_SRLG +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClient.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClient.java new file mode 100644 index 0000000..000df3b --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClient.java @@ -0,0 +1,130 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf; + +import java.util.List; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; + +/** + * RESTCONF Client interface for communicating with Network Slice Service Provider. + * + * The IETF NS Controller acts as a Consumer (Network Slice Service Customer) that communicates + * with a Provider (Network Slice Controller) via RESTCONF (RFC 8040) to manage IETF + * Network Slice Services as defined in RFC 9543 and draft-ietf-teas-ietf-network-slice-nbi-yang-25. + * + * Operations supported: + * - CREATE: Provision a new network slice service + * - RETRIEVE: Fetch current service configuration and status + * - UPDATE: Modify existing service configuration + * - DELETE: Decommission a network slice service + * - LIST: Enumerate all provisioned services + * - FEASIBILITY_CHECK: Validate service request without provisioning resources + */ +public interface RestconfClient { + + /** + * Create new network slice services on the provider. + * + * HTTP Operation: POST /restconf/data/ietf-network-slice-service:network-slice-services + * + * The request body follows RFC 9543 format with slice-service as an array: + * { + * "slice-service": [ + * { + * "id": "slice-001", + * "slo-sle-policy": { "slo-sle-template": "Gold" }, + * "sdps": { ... }, + * "connection-groups": { ... } + * } + * ] + * } + * + * @param services List of network slice services to create + * @return List of created slice services with updated status + * @throws RestconfException if the request fails + */ + List createSliceService(List services) throws RestconfException; + + /** + * Retrieve a specific network slice service from the provider. + * + * HTTP Operation: GET /restconf/data/ietf-network-slice-service:network-slice-services/slice-service= + * + * @param serviceId The unique identifier of the slice service + * @return The slice service with current configuration and status + * @throws RestconfException if the request fails or service not found + */ + SliceService getSliceService(String serviceId) throws RestconfException; + + /** + * Update an existing network slice service configuration. + * + * HTTP Operation: PATCH /restconf/data/ietf-network-slice-service:network-slice-services/slice-service= + * + * @param serviceId The unique identifier of the slice service to update + * @param service The updated slice service configuration + * @return The updated slice service with new status + * @throws RestconfException if the request fails + */ + SliceService updateSliceService(String serviceId, SliceService service) throws RestconfException; + + /** + * Delete a network slice service from the provider. + * + * HTTP Operation: DELETE /restconf/data/ietf-network-slice-service:network-slice-services/slice-service= + * + * @param serviceId The unique identifier of the slice service to delete + * @throws RestconfException if the request fails + */ + void deleteSliceService(String serviceId) throws RestconfException; + + /** + * List all network slice services from the provider. + * + * HTTP Operation: GET /restconf/data/ietf-network-slice-service:network-slice-services/slice-service + * + * @return List of all provisioned slice services + * @throws RestconfException if the request fails + */ + List listSliceServices() throws RestconfException; + + /** + * Check feasibility of a network slice service request without provisioning resources. + * + * This operation uses the "test-only" mode as defined in RFC 9543, which allows + * validation of service requests before actual instantiation. Resources are not + * reserved, but the NSC computes the feasible connectivity constructs. + * + * HTTP Operation: PUT with test-only=true flag + * + * @param service The slice service with test-only flag set to true + * @return The service with computed connectivity constructs and feasibility status + * - admin-state: admin-up if feasible + * - admin-state: rejected if not feasible (with reason in status) + * @throws RestconfException if the request fails + */ + SliceService checkFeasibility(SliceService service) throws RestconfException; + + /** + * Get service status and monitoring information. + * + * HTTP Operation: GET /restconf/data/.../slice-service=/status + * + * @param serviceId The unique identifier of the slice service + * @return Current operational and administrative status + * @throws RestconfException if the request fails + */ + String getServiceStatus(String serviceId) throws RestconfException; + + /** + * Retrieve SLO/SLE templates from the provider. + * + * HTTP Operation: GET /restconf/data/ietf-network-slice-service:network-slice-services/slo-sle-templates + * + * Returns raw JSON response containing RFC 9543 formatted SLO/SLE templates. + * The response uses the RFC 9543 wrapped format with namespace container. + * + * @return JSON string containing RFC 9543 formatted SLO/SLE templates + * @throws RestconfException if the request fails + */ + String getSloSleTemplates() throws RestconfException; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java new file mode 100644 index 0000000..f6edb87 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java @@ -0,0 +1,536 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Implementation of RESTCONF Client for communicating with Network Slice Service Provider. + * + * This client handles HTTP/HTTPS communication with a provider's RESTCONF interface, + * implementing full lifecycle management of network slice services including: + * - Service provisioning (CREATE, RETRIEVE, UPDATE, DELETE) + * - Service discovery (LIST) + * - Feasibility validation (TEST-ONLY mode) + * - Error handling with retry logic + * + * Configuration: + * - restconf.provider-url: Base URL of the RESTCONF provider (e.g., https://nsc-provider:8443) + * - restconf.auth-method: Authentication method (basic, oauth2, mtls) + * - restconf.api-version: YANG model version (default: 2025-05-09) + * - restconf.timeout-ms: Connection timeout in milliseconds + */ +@Component +public class RestconfClientImpl implements RestconfClient { + + private static final Logger logger = LoggerFactory.getLogger("org.etsi.osl.controllers.ietf.ns"); + + private static final String YANG_MODULE = "ietf-network-slice-service"; + private static final String YANG_DATE = "2025-05-09"; + private static final String RESTCONF_PATH = "/restconf/data"; + + @Value("${restconf.provider-url:http://localhost:11880}") + private String providerUrl; + + @Value("${restconf.auth-method:basic}") + private String authMethod; + + @Value("${restconf.api-version:2025-05-09}") + private String apiVersion; + + @Value("${restconf.timeout-ms:10000}") + private long timeoutMs; + + @Value("${restconf.auth.username:admin}") + private String authUsername; + + @Value("${restconf.auth.password:admin123}") + private String authPassword; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Override + public List createSliceService(List services) throws RestconfException { + if (services == null || services.isEmpty()) { + throw new RestconfException("Services list must not be null or empty"); + } + + String uri = providerUrl + "/restconf/data/ietf-network-slice-service:network-slice-services"; + logger.info("Creating {} network slice service(s)", services.size()); + + try { + // Build RFC 9543 format: { "slice-service": [ {...}, {...} ] } + Map> requestBody = new HashMap<>(); + requestBody.put("slice-service", services); + + HttpEntity>> requestEntity = new HttpEntity<>(requestBody, buildHeaders()); + + // Use a custom response type to handle the RFC 9543 array response + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.POST, + requestEntity, + String.class + ); + + if (response.getStatusCode().is2xxSuccessful()) { + logger.info("Successfully created {} network slice service(s)", services.size()); + // Parse the response and return the created services + // For now, return the input services (they should be echoed back with status) + return services; + } else { + throw new RestconfException( + response.getStatusCode().value(), + "protocol", + "operation-failed", + "Failed to create network slice services" + ); + } + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + logger.error("Error creating network slice services", e); + throw new RestconfException("Failed to create network slice services: " + e.getMessage(), e); + } + } + + @Override + public SliceService getSliceService(String serviceId) throws RestconfException { + if (serviceId == null || serviceId.isEmpty()) { + throw new RestconfException("Service ID must not be null or empty"); + } + + String uri = buildServiceUri(serviceId); + logger.debug("Retrieving network slice service: {}", serviceId); + + try { + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.GET, + new HttpEntity<>(buildHeaders()), + SliceService.class + ); + + if (response.getStatusCode() == HttpStatus.OK) { + logger.debug("Successfully retrieved network slice service: {}", serviceId); + return response.getBody(); + } else { + throw new RestconfException( + response.getStatusCode().value(), + "protocol", + "operation-failed", + "Failed to retrieve network slice service" + ); + } + } catch (HttpClientErrorException.NotFound e) { + throw new RestconfException( + 404, + "application", + "data-missing", + "Network slice service '" + serviceId + "' not found" + ); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + logger.error("Error retrieving network slice service: {}", serviceId, e); + throw new RestconfException("Failed to retrieve network slice service: " + e.getMessage(), e); + } + } + + @Override + public SliceService updateSliceService(String serviceId, SliceService service) throws RestconfException { + if (serviceId == null || serviceId.isEmpty()) { + throw new RestconfException("Service ID must not be null or empty"); + } + if (service == null) { + throw new RestconfException("Service must not be null"); + } + + String uri = buildServiceUri(serviceId); + logger.info("Updating network slice service: {}", serviceId); + + try { + HttpEntity requestEntity = new HttpEntity<>(service, buildHeaders()); + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.PATCH, + requestEntity, + SliceService.class + ); + + if (response.getStatusCode().is2xxSuccessful()) { + logger.info("Successfully updated network slice service: {}", serviceId); + return response.getBody(); + } else { + throw new RestconfException( + response.getStatusCode().value(), + "protocol", + "operation-failed", + "Failed to update network slice service" + ); + } + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + logger.error("Error updating network slice service: {}", serviceId, e); + throw new RestconfException("Failed to update network slice service: " + e.getMessage(), e); + } + } + + @Override + public void deleteSliceService(String serviceId) throws RestconfException { + if (serviceId == null || serviceId.isEmpty()) { + throw new RestconfException("Service ID must not be null or empty"); + } + + String uri = buildServiceUri(serviceId); + logger.info("Deleting network slice service: {}", serviceId); + + try { + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.DELETE, + new HttpEntity<>(buildHeaders()), + Void.class + ); + + if (response.getStatusCode() == HttpStatus.NO_CONTENT || response.getStatusCode() == HttpStatus.OK) { + logger.info("Successfully deleted network slice service: {}", serviceId); + } else { + throw new RestconfException( + response.getStatusCode().value(), + "protocol", + "operation-failed", + "Failed to delete network slice service" + ); + } + } catch (HttpClientErrorException.NotFound e) { + throw new RestconfException( + 404, + "application", + "data-missing", + "Network slice service '" + serviceId + "' not found" + ); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + logger.error("Error deleting network slice service: {}", serviceId, e); + throw new RestconfException("Failed to delete network slice service: " + e.getMessage(), e); + } + } + + @Override + public List listSliceServices() throws RestconfException { + String uri = buildServicesListUri(); + logger.debug("Listing all network slice services"); + + try { + // Get response as string first to handle the wrapped container structure + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.GET, + new HttpEntity<>(buildHeaders()), + String.class + ); + + if (response.getStatusCode() == HttpStatus.OK) { + String responseBody = response.getBody(); + if (responseBody == null || responseBody.isEmpty()) { + logger.debug("Empty response from list slice services"); + return new ArrayList<>(); + } + + // Use Rfc9543JsonConverter to parse the RFC 9543 response + try { + List services = new Rfc9543JsonConverter() + .parseSliceServices(responseBody); + logger.debug("Successfully retrieved {} network slice services", services.size()); + return services; + } catch (Exception parseError) { + // Fallback: try to parse as direct array if it's not wrapped + logger.debug("RFC 9543 parsing failed, trying direct array deserialization", parseError); + try { + SliceService[] servicesArray = objectMapper.readValue(responseBody, SliceService[].class); + List services = Arrays.asList(servicesArray != null ? servicesArray : new SliceService[0]); + logger.debug("Successfully retrieved {} network slice services (direct array)", services.size()); + return services; + } catch (Exception fallbackError) { + logger.error("Failed to parse slice services response in both formats", fallbackError); + throw new RestconfException("Failed to parse network slice services response: " + fallbackError.getMessage(), fallbackError); + } + } + } else { + throw new RestconfException( + response.getStatusCode().value(), + "protocol", + "operation-failed", + "Failed to list network slice services" + ); + } + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + logger.error("Error listing network slice services", e); + throw new RestconfException("Failed to list network slice services: " + e.getMessage(), e); + } + } + + @Override + public SliceService checkFeasibility(SliceService service) throws RestconfException { + if (service == null || service.getId() == null) { + throw new RestconfException("Service and service ID must not be null"); + } + if (!Boolean.TRUE.equals(service.getTestOnly())) { + throw new RestconfException("Service must have test-only flag set to true for feasibility check"); + } + + String uri = buildServiceUri(service.getId()); + logger.info("Checking feasibility of network slice service: {}", service.getId()); + + try { + HttpEntity requestEntity = new HttpEntity<>(service, buildHeaders()); + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.PUT, + requestEntity, + SliceService.class + ); + + if (response.getStatusCode().is2xxSuccessful()) { + SliceService resultService = response.getBody(); + String adminState = resultService.getStatus() != null ? + resultService.getStatus().getAdminState() : "unknown"; + + if ("admin-up".equals(adminState)) { + logger.info("Feasibility check PASSED for service: {}", service.getId()); + } else if ("rejected".equals(adminState)) { + String reason = resultService.getStatus() != null ? + resultService.getStatus().getOperState() : "Unknown reason"; + logger.warn("Feasibility check FAILED for service: {} - Reason: {}", service.getId(), reason); + } else { + logger.info("Feasibility check status: {} for service: {}", adminState, service.getId()); + } + + return resultService; + } else { + throw new RestconfException( + response.getStatusCode().value(), + "protocol", + "operation-failed", + "Failed to check feasibility of network slice service" + ); + } + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + logger.error("Error checking feasibility of network slice service: {}", service.getId(), e); + throw new RestconfException("Failed to check feasibility: " + e.getMessage(), e); + } + } + + @Override + public String getServiceStatus(String serviceId) throws RestconfException { + if (serviceId == null || serviceId.isEmpty()) { + throw new RestconfException("Service ID must not be null or empty"); + } + + String uri = buildServiceStatusUri(serviceId); + logger.debug("Retrieving status of network slice service: {}", serviceId); + + try { + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.GET, + new HttpEntity<>(buildHeaders()), + String.class + ); + + if (response.getStatusCode() == HttpStatus.OK) { + logger.debug("Successfully retrieved status for service: {}", serviceId); + return response.getBody(); + } else { + throw new RestconfException( + response.getStatusCode().value(), + "protocol", + "operation-failed", + "Failed to retrieve service status" + ); + } + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + logger.error("Error retrieving service status: {}", serviceId, e); + throw new RestconfException("Failed to retrieve service status: " + e.getMessage(), e); + } + } + + @Override + public String getSloSleTemplates() throws RestconfException { + String uri = buildSloSleTemplatesUri(); + logger.debug("Retrieving SLO/SLE templates from provider"); + + try { + ResponseEntity response = restTemplate.exchange( + uri, + HttpMethod.GET, + new HttpEntity<>(buildHeaders()), + String.class + ); + + if (response.getStatusCode() == HttpStatus.OK) { + String responseBody = response.getBody(); + if (responseBody == null || responseBody.isEmpty()) { + logger.debug("Empty response from SLO/SLE templates endpoint"); + return "{}"; + } + logger.debug("Successfully retrieved SLO/SLE templates from provider"); + return responseBody; + } else { + throw new RestconfException( + response.getStatusCode().value(), + "protocol", + "operation-failed", + "Failed to retrieve SLO/SLE templates" + ); + } + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + logger.error("Error retrieving SLO/SLE templates", e); + throw new RestconfException("Failed to retrieve SLO/SLE templates: " + e.getMessage(), e); + } + } + + // Private helper methods + + /** + * Builds the URI for a specific slice service resource. + */ + private String buildServiceUriPost(String serviceId) { + String encodedId = encodeUrlComponent(serviceId); + return String.format("%s%s/%s:network-slice-services/slice-service", + providerUrl, RESTCONF_PATH, YANG_MODULE); + } + + /** + * Builds the URI for a specific slice service resource. + */ + private String buildServiceUri(String serviceId) { + String encodedId = encodeUrlComponent(serviceId); + return String.format("%s%s/%s:network-slice-services/slice-service=%s", + providerUrl, RESTCONF_PATH, YANG_MODULE, encodedId); + } + + /** + * Builds the URI for listing all slice services. + */ + private String buildServicesListUri() { + return String.format("%s%s/%s:network-slice-services/slice-service", + providerUrl, RESTCONF_PATH, YANG_MODULE); + } + + /** + * Builds the URI for retrieving service status. + */ + private String buildServiceStatusUri(String serviceId) { + String encodedId = encodeUrlComponent(serviceId); + return String.format("%s%s/%s:network-slice-services/slice-service=%s/status", + providerUrl, RESTCONF_PATH, YANG_MODULE, encodedId); + } + + /** + * Builds the URI for retrieving SLO/SLE templates. + */ + private String buildSloSleTemplatesUri() { + return String.format("%s%s/%s:network-slice-services/slo-sle-templates", + providerUrl, RESTCONF_PATH, YANG_MODULE); + } + + /** + * Encodes special characters in URL components. + */ + private String encodeUrlComponent(String component) { + return component.replaceAll(" ", "%20") + .replaceAll("/", "%2F") + .replaceAll(":", "%3A"); + } + + /** + * Builds HTTP headers for RESTCONF requests with HTTP Basic Authentication. + */ + private HttpHeaders buildHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + headers.set("Accept", "application/yang-data+json"); + headers.set("Content-Type", "application/yang-data+json"); + + // Add HTTP Basic Authentication if configured + if ("basic".equalsIgnoreCase(authMethod) && authUsername != null && !authUsername.isEmpty()) { + String credentials = authUsername + ":" + authPassword; + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + headers.set("Authorization", "Basic " + encodedCredentials); + logger.debug("HTTP Basic Authentication configured for user: {}", authUsername); + } + + return headers; + } + + /** + * Handles HTTP client errors and converts them to RestconfException. + */ + private RestconfException handleHttpError(HttpClientErrorException e) { + int status = e.getStatusCode().value(); + String message = e.getMessage(); + + String errorTag; + switch (status) { + case 400: + errorTag = "invalid-value"; + break; + case 401: + errorTag = "access-denied"; + break; + case 403: + errorTag = "access-denied"; + break; + case 404: + errorTag = "data-missing"; + break; + case 409: + errorTag = "data-exists"; + break; + default: + errorTag = "operation-failed"; + } + + return new RestconfException(status, "application", errorTag, message); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConfig.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConfig.java new file mode 100644 index 0000000..85fbfd6 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConfig.java @@ -0,0 +1,70 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf; + +import java.time.Duration; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +/** + * Configuration for RESTCONF Client. + * + * Provides Spring Bean configurations for: + * - RestTemplate with custom request factory and timeout settings + * - ObjectMapper for JSON serialization/deserialization + * - HTTP client request/response logging + */ +@Configuration +public class RestconfConfig { + + /** + * Creates a configured RestTemplate bean for RESTCONF communications. + * + * Features: + * - Connection timeout: 10 seconds (configurable) + * - Read timeout: 30 seconds (configurable) + * - Buffering request/response for logging + * - Support for JSON content type + */ + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder + .requestFactory(this::clientHttpRequestFactory) + .setConnectTimeout(Duration.ofSeconds(10)) + .setReadTimeout(Duration.ofSeconds(30)) + .build(); + } + + /** + * Configures the client HTTP request factory with buffering capability. + * Buffering allows request/response bodies to be read multiple times for logging. + */ + private ClientHttpRequestFactory clientHttpRequestFactory() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(10000); + factory.setReadTimeout(30000); + return new BufferingClientHttpRequestFactory(factory); + } + + /** + * Creates an ObjectMapper bean for JSON serialization/deserialization. + * + * Features: + * - Support for Java 8+ date/time types (OffsetDateTime, LocalDate, etc.) + * - Pretty printing disabled for production (reduces payload size) + * - Handles YANG data model types correctly + */ + @Bean + public ObjectMapper objectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return mapper; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java new file mode 100644 index 0000000..76558ac --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java @@ -0,0 +1,354 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf; + +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.ConnectionGroup; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.ConnectivityType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SDP; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.ServiceStatus; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * High-level service for managing Network Slice Services with the RESTCONF provider. + * + * This service provides business logic operations on top of the raw RESTCONF client, + * including: + * - Service lifecycle management (provision, modify, decommission) + * - Feasibility validation + * - Status monitoring + * - Error handling and logging + * + * This service acts as the bridge between the IETF NS Controller's internal operations + * (resource management, JMS message handling) and the external RESTCONF provider API. + */ +@Service +public class RestconfConsumerService { + + private static final Logger logger = LoggerFactory.getLogger("org.etsi.osl.controllers.ietf.ns"); + + @Autowired + private RestconfClient restconfClient; + + /** + * Provisions new network slice services on the provider. + * + * @param services List of slice service configurations to provision + * @return List of provisioned services with updated status + * @throws RestconfException if provisioning fails + */ + public List provisionSliceServices(List services) throws RestconfException { + logger.info("Provisioning {} network slice service(s)", services.size()); + + // Validate and set initial status for each service + for (SliceService service : services) { + if (service.getStatus() == null) { + service.setStatus(new ServiceStatus()); + } + service.getStatus().setAdminState("admin-up"); + service.getStatus().setLastChange(OffsetDateTime.now()); + } + + // Create on provider (sends array in RFC 9543 format) + List createdServices = restconfClient.createSliceService(services); + logger.info("Successfully provisioned {} service(s)", createdServices.size()); + + return createdServices; + } + + /** + * Provisions a single network slice service on the provider (convenience method). + * + * @param service The slice service configuration to provision + * @return The provisioned service with updated status + * @throws RestconfException if provisioning fails + */ + public SliceService provisionSliceService(SliceService service) throws RestconfException { + logger.info("Provisioning single network slice service: {}", service.getId()); + List services = new ArrayList<>(); + services.add(service); + List result = provisionSliceServices(services); + return result.isEmpty() ? null : result.get(0); + } + + /** + * Validates the feasibility of a network slice service request. + * + * This performs a "test-only" check on the provider to validate that the + * requested service can be realized without actually provisioning resources. + * + * @param service The slice service configuration to validate + * @return Result containing feasibility status and computed connectivity constructs + * @throws RestconfException if validation fails + */ + public SliceService validateFeasibility(SliceService service) throws RestconfException { + logger.info("Validating feasibility of network slice service: {}", service.getId()); + + // Validate service configuration + validateSliceServiceConfiguration(service); + + // Set test-only flag + service.setTestOnly(true); + + // Check feasibility on provider + SliceService feasibilityResult = restconfClient.checkFeasibility(service); + + String adminState = feasibilityResult.getStatus().getAdminState(); + if ("admin-up".equals(adminState)) { + logger.info("Feasibility validation PASSED for service: {}", service.getId()); + } else if ("rejected".equals(adminState)) { + logger.warn("Feasibility validation FAILED for service: {} - Status: {}", + service.getId(), feasibilityResult.getStatus().getOperState()); + } + + return feasibilityResult; + } + + /** + * Retrieves the current state of a provisioned network slice service. + * + * @param serviceId The unique identifier of the slice service + * @return Current service configuration and status + * @throws RestconfException if retrieval fails + */ + public SliceService getSliceService(String serviceId) throws RestconfException { + logger.debug("Retrieving network slice service: {}", serviceId); + return restconfClient.getSliceService(serviceId); + } + + /** + * Updates an existing network slice service configuration. + * + * Supports modifications to: + * - Service description + * - SLO/SLE policies + * - Connectivity constructs + * - Service tags + * + * @param serviceId The unique identifier of the slice service + * @param updatedService The updated service configuration + * @return The updated service from provider + * @throws RestconfException if update fails + */ + public SliceService updateSliceService(String serviceId, SliceService updatedService) throws RestconfException { + logger.info("Updating network slice service: {}", serviceId); + + // Get current service state + SliceService currentService = restconfClient.getSliceService(serviceId); + + // Preserve service ID + updatedService.setId(currentService.getId()); + + // Update on provider + SliceService result = restconfClient.updateSliceService(serviceId, updatedService); + logger.info("Successfully updated service: {}", serviceId); + + return result; + } + + /** + * Decommissions a network slice service. + * + * @param serviceId The unique identifier of the slice service to remove + * @throws RestconfException if decommissioning fails + */ + public void decommissionSliceService(String serviceId) throws RestconfException { + logger.info("Decommissioning network slice service: {}", serviceId); + + // Delete from provider + restconfClient.deleteSliceService(serviceId); + logger.info("Successfully decommissioned service: {}", serviceId); + } + + /** + * Lists all provisioned network slice services. + * + * @return List of all slice services + * @throws RestconfException if listing fails + */ + public List listAllSliceServices() throws RestconfException { + logger.debug("Listing all network slice services"); + return restconfClient.listSliceServices(); + } + + /** + * Retrieves the status of a network slice service. + * + * @param serviceId The unique identifier of the slice service + * @return Service status as JSON string + * @throws RestconfException if retrieval fails + */ + public String getServiceStatus(String serviceId) throws RestconfException { + logger.debug("Retrieving status of service: {}", serviceId); + return restconfClient.getServiceStatus(serviceId); + } + + /** + * Creates a new P2P (Point-to-Point) network slice service. + * + * Convenience method for common P2P service provisioning. + * + * @param serviceId Unique service identifier + * @param description Service description + * @param sdp1 First service demarcation point + * @param sdp2 Second service demarcation point + * @return The provisioned P2P service + * @throws RestconfException if provisioning fails + */ + public SliceService provisionP2pService(String serviceId, String description, SDP sdp1, SDP sdp2) throws RestconfException { + logger.info("Provisioning P2P network slice service: {}", serviceId); + + SliceService service = new SliceService(); + service.setId(serviceId); + service.setDescription(description); + service.setTestOnly(false); + + // Add SDPs + service.getSdps().add(sdp1); + service.getSdps().add(sdp2); + + // Create P2P connection group + ConnectionGroup cg = new ConnectionGroup(); + cg.setId("cg-" + serviceId); + cg.setConnectivityType(ConnectivityType.P2P); + service.getConnectionGroups().add(cg); + + return provisionSliceService(service); + } + + /** + * Creates a new A2A (Any-to-Any) network slice service. + * + * Convenience method for common A2A service provisioning. + * + * @param serviceId Unique service identifier + * @param description Service description + * @param sdps Service demarcation points + * @return The provisioned A2A service + * @throws RestconfException if provisioning fails + */ + public SliceService provisionA2aService(String serviceId, String description, List sdps) throws RestconfException { + logger.info("Provisioning A2A network slice service: {} with {} SDPs", serviceId, sdps.size()); + + if (sdps.size() < 2) { + throw new RestconfException("A2A service requires at least 2 SDPs"); + } + + SliceService service = new SliceService(); + service.setId(serviceId); + service.setDescription(description); + service.setTestOnly(false); + + // Add SDPs + service.getSdps().addAll(sdps); + + // Create A2A connection group + ConnectionGroup cg = new ConnectionGroup(); + cg.setId("cg-" + serviceId); + cg.setConnectivityType(ConnectivityType.A2A); + service.getConnectionGroups().add(cg); + + return provisionSliceService(service); + } + + /** + * Parse RFC 9543 compliant Network Slice Services response from provider. + * + * This method handles RFC 9543 YANG JSON responses with kebab-case property names + * and namespace prefixes, converting them to the existing domain model. + * + * @param rfc9543Json RFC 9543 JSON response string + * @return Parsed NetworkSliceServices object + * @throws RestconfException if parsing fails + */ + public NetworkSliceServices parseRfc9543NetworkSliceServices(String rfc9543Json) + throws RestconfException { + logger.debug("Parsing RFC 9543 Network Slice Services response"); + return Rfc9543JsonConverter.parseNetworkSliceServices(rfc9543Json); + } + + /** + * Parse RFC 9543 compliant SLO/SLE templates response from provider. + * + * @param rfc9543Json RFC 9543 templates JSON response string + * @return List of parsed SloSleTemplate objects + * @throws RestconfException if parsing fails + */ + public List parseRfc9543Templates(String rfc9543Json) throws RestconfException { + logger.debug("Parsing RFC 9543 SLO/SLE templates response"); + return Rfc9543JsonConverter.parseSloSleTemplates(rfc9543Json); + } + + /** + * Parse RFC 9543 compliant Slice Services response from provider. + * + * @param rfc9543Json RFC 9543 services JSON response string + * @return List of parsed SliceService objects + * @throws RestconfException if parsing fails + */ + public List parseRfc9543SliceServices(String rfc9543Json) + throws RestconfException { + logger.debug("Parsing RFC 9543 slice services response"); + return Rfc9543JsonConverter.parseSliceServices(rfc9543Json); + } + + /** + * Check if response is in RFC 9543 format. + * + * @param json Response JSON string + * @return true if RFC 9543 format, false otherwise + */ + public boolean isRfc9543Response(String json) { + return Rfc9543JsonConverter.isRfc9543Format(json); + } + + // Private validation methods + + /** + * Validates the network slice service configuration. + * + * @param service The service to validate + * @throws RestconfException if validation fails + */ + private void validateSliceServiceConfiguration(SliceService service) throws RestconfException { + if (service == null) { + throw new RestconfException("Service configuration is null"); + } + + if (service.getId() == null || service.getId().isEmpty()) { + throw new RestconfException("Service ID is required"); + } + + if (service.getSdps() == null || service.getSdps().isEmpty()) { + throw new RestconfException("At least one SDP is required"); + } + + if (service.getConnectionGroups() == null || service.getConnectionGroups().isEmpty()) { + throw new RestconfException("At least one connection group is required"); + } + + // Validate SDPs + for (SDP sdp : service.getSdps()) { + if (sdp.getId() == null || sdp.getId().isEmpty()) { + throw new RestconfException("SDP ID is required for all SDPs"); + } + } + + // Validate connectivity + for (ConnectionGroup cg : service.getConnectionGroups()) { + if (cg.getConnectivityType() == null) { + throw new RestconfException("Connectivity type is required for all connection groups"); + } + + if (cg.getConnectivityConstructs() == null || cg.getConnectivityConstructs().isEmpty()) { + throw new RestconfException("At least one connectivity construct is required per connection group"); + } + } + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfException.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfException.java new file mode 100644 index 0000000..c53c593 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfException.java @@ -0,0 +1,63 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf; + +/** + * Exception thrown when RESTCONF operations fail. + * + * This exception wraps HTTP errors, connection issues, and data model violations + * that occur during communication with the Network Slice Service Provider. + */ +public class RestconfException extends Exception { + + private static final long serialVersionUID = 1L; + + private int httpStatus; + private String errorType; + private String errorTag; + private String errorMessage; + + /** + * Constructs a RestconfException with a message. + */ + public RestconfException(String message) { + super(message); + } + + /** + * Constructs a RestconfException with a message and cause. + */ + public RestconfException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a RestconfException with detailed RESTCONF error information. + * + * @param httpStatus HTTP status code + * @param errorType YANG error-type (application, protocol, rpc) + * @param errorTag YANG error-tag (e.g., invalid-value, access-denied, data-missing) + * @param errorMessage Human-readable error message + */ + public RestconfException(int httpStatus, String errorType, String errorTag, String errorMessage) { + super(String.format("RESTCONF Error [%d]: %s/%s - %s", httpStatus, errorType, errorTag, errorMessage)); + this.httpStatus = httpStatus; + this.errorType = errorType; + this.errorTag = errorTag; + this.errorMessage = errorMessage; + } + + public int getHttpStatus() { + return httpStatus; + } + + public String getErrorType() { + return errorType; + } + + public String getErrorTag() { + return errorTag; + } + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/Rfc9543JsonConverter.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/Rfc9543JsonConverter.java new file mode 100644 index 0000000..d6d7db5 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/Rfc9543JsonConverter.java @@ -0,0 +1,260 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import java.util.ArrayList; +import java.util.List; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; +import org.etsi.osl.controllers.ietf.ns.api.restconf.deserializers.Rfc9543NetworkSliceServicesDeserializer; +import org.etsi.osl.controllers.ietf.ns.api.restconf.deserializers.Rfc9543SliceServiceDeserializer; +import org.etsi.osl.controllers.ietf.ns.api.restconf.deserializers.Rfc9543SloSleTemplateDeserializer; +import lombok.extern.slf4j.Slf4j; + +/** + * Utility class for converting RFC 9543 compliant YANG JSON responses to domain models. + * + * Provides convenient methods for parsing RFC 9543 RESTCONF responses with kebab-case + * property names and namespace prefixes into the existing domain model classes. + * + * This converter bridges the gap between RFC 9543 YANG JSON format and the existing + * domain model by using custom Jackson deserializers. + * + * Example Usage: + *
+ * String rfc9543Json = restTemplate.getForObject(url, String.class);
+ * NetworkSliceServices nss = Rfc9543JsonConverter.parseNetworkSliceServices(rfc9543Json);
+ * 
+ */ +@Slf4j +public class Rfc9543JsonConverter { + + private static final ObjectMapper mapper = createRfc9543ObjectMapper(); + + /** + * Create an ObjectMapper configured with RFC 9543 deserializers. + * + * @return Configured ObjectMapper + */ + private static ObjectMapper createRfc9543ObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + + // Register RFC 9543 deserializers + SimpleModule module = new SimpleModule("RFC9543Module"); + module.addDeserializer(NetworkSliceServices.class, + new Rfc9543NetworkSliceServicesDeserializer()); + module.addDeserializer(SloSleTemplate.class, + new Rfc9543SloSleTemplateDeserializer()); + module.addDeserializer(SliceService.class, + new Rfc9543SliceServiceDeserializer()); + + objectMapper.registerModule(module); + + log.debug("Created RFC9543 ObjectMapper with custom deserializers"); + return objectMapper; + } + + /** + * Parse RFC 9543 compliant Network Slice Services JSON response to domain model. + * + * Handles the complete namespace structure and converts kebab-case property names + * to the domain model with proper camelCase naming. + * + * @param json RFC 9543 JSON string + * @return Parsed NetworkSliceServices object + * @throws RestconfException if parsing fails + */ + public static NetworkSliceServices parseNetworkSliceServices(String json) + throws RestconfException { + log.debug("Parsing RFC 9543 Network Slice Services JSON"); + + try { + NetworkSliceServices nss = mapper.readValue(json, NetworkSliceServices.class); + log.info("Successfully parsed network slice services: {} templates, {} services", + nss.getSloSleTemplates() != null ? nss.getSloSleTemplates().size() : 0, + nss.getSliceServices() != null ? nss.getSliceServices().size() : 0); + return nss; + } catch (JsonProcessingException e) { + log.error("Failed to parse RFC 9543 Network Slice Services JSON", e); + throw new RestconfException("Failed to parse network slice services response", e); + } + } + + /** + * Parse RFC 9543 compliant SLO/SLE templates JSON response. + * + * Handles the templates container structure with RFC 9543 namespace wrapper and converts + * kebab-case property names to the domain model. + * + * Supports both wrapped format (with ietf-network-slice-service:network-slice-services) and + * bare format (direct slo-sle-templates). + * + * @param json RFC 9543 templates container JSON string + * @return List of parsed SloSleTemplate objects + * @throws RestconfException if parsing fails + */ + public static List parseSloSleTemplates(String json) throws RestconfException { + log.debug("Parsing RFC 9543 SLO/SLE templates JSON"); + + try { + JsonNode rootNode = mapper.readTree(json); + List templates = new ArrayList<>(); + + // Navigate to templates container + // Try RFC 9543 wrapped format first: ietf-network-slice-service:network-slice-services -> slo-sle-templates + JsonNode templatesContainer = rootNode.get("ietf-network-slice-service:network-slice-services"); + + if (templatesContainer != null) { + log.debug("Found RFC 9543 wrapped namespace container"); + templatesContainer = templatesContainer.get("slo-sle-templates"); + } else { + // Fallback to direct slo-sle-templates (for backward compatibility) + templatesContainer = rootNode.get("slo-sle-templates"); + if (templatesContainer != null) { + log.debug("Found direct slo-sle-templates container (unwrapped format)"); + } + } + + if (templatesContainer != null) { + JsonNode templatesList = templatesContainer.get("slo-sle-template"); + if (templatesList != null && templatesList.isArray()) { + log.debug("Found {} templates in response", templatesList.size()); + for (JsonNode templateNode : templatesList) { + try { + SloSleTemplate template = Rfc9543SloSleTemplateDeserializer + .parseTemplate(templateNode, mapper); + templates.add(template); + log.debug("Parsed template: {}", template.getId()); + } catch (Exception e) { + log.error("Error parsing individual template: {}", templateNode, e); + } + } + } else { + log.warn("No slo-sle-template array found in response"); + } + } else { + log.warn("No templates container found in RFC 9543 response"); + } + + log.info("Successfully parsed {} SLO/SLE templates", templates.size()); + return templates; + } catch (JsonProcessingException e) { + log.error("Failed to parse RFC 9543 templates JSON", e); + throw new RestconfException("Failed to parse templates response", e); + } + } + + /** + * Parse RFC 9543 compliant Slice Services JSON response. + * + * Handles the services list structure and converts kebab-case property names + * to the domain model. + * + * @param json RFC 9543 services list JSON string + * @return List of parsed SliceService objects + * @throws RestconfException if parsing fails + */ + public static List parseSliceServices(String json) throws RestconfException { + log.debug("Parsing RFC 9543 slice services JSON"); + + try { + JsonNode rootNode = mapper.readTree(json); + List services = new ArrayList<>(); + + JsonNode servicesList = rootNode.get("slice-service"); + if (servicesList != null && servicesList.isArray()) { + for (JsonNode serviceNode : servicesList) { + try { + SliceService service = Rfc9543SliceServiceDeserializer + .parseService(serviceNode, mapper); + services.add(service); + log.debug("Parsed service: {}", service.getId()); + } catch (Exception e) { + log.error("Error parsing individual service", e); + } + } + } + + log.info("Successfully parsed {} slice services", services.size()); + return services; + } catch (JsonProcessingException e) { + log.error("Failed to parse RFC 9543 services JSON", e); + throw new RestconfException("Failed to parse services response", e); + } + } + + /** + * Parse a single RFC 9543 compliant SLO/SLE template JSON object. + * + * @param json Single template JSON string + * @return Parsed SloSleTemplate object + * @throws RestconfException if parsing fails + */ + public static SloSleTemplate parseSingleTemplate(String json) throws RestconfException { + log.debug("Parsing single RFC 9543 SLO/SLE template JSON"); + + try { + SloSleTemplate template = mapper.readValue(json, SloSleTemplate.class); + log.info("Successfully parsed template: {}", template.getId()); + return template; + } catch (JsonProcessingException e) { + log.error("Failed to parse RFC 9543 template JSON", e); + throw new RestconfException("Failed to parse template response", e); + } + } + + /** + * Parse a single RFC 9543 compliant Slice Service JSON object. + * + * @param json Single service JSON string + * @return Parsed SliceService object + * @throws RestconfException if parsing fails + */ + public static SliceService parseSingleService(String json) throws RestconfException { + log.debug("Parsing single RFC 9543 slice service JSON"); + + try { + SliceService service = mapper.readValue(json, SliceService.class); + log.info("Successfully parsed service: {}", service.getId()); + return service; + } catch (JsonProcessingException e) { + log.error("Failed to parse RFC 9543 service JSON", e); + throw new RestconfException("Failed to parse service response", e); + } + } + + /** + * Check if JSON contains RFC 9543 namespace prefix. + * + * @param json JSON string to check + * @return true if contains RFC 9543 namespace, false otherwise + */ + public static boolean isRfc9543Format(String json) { + return json != null && json.contains("ietf-network-slice-service:network-slice-services"); + } + + /** + * Check if JSON contains RFC 9543 kebab-case properties. + * + * @param json JSON string to check + * @return true if contains kebab-case properties, false otherwise + */ + public static boolean hasKebabCaseProperties(String json) { + return json != null && (json.contains("\"slo-policy\"") || json.contains("\"metric-bound\"") + || json.contains("\"metric-type\"") || json.contains("\"sle-policy\"")); + } + + /** + * Get the ObjectMapper instance used by this converter. + * + * Useful if you need to use custom configuration for other operations. + * + * @return Configured ObjectMapper instance + */ + public static ObjectMapper getObjectMapper() { + return mapper; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543NetworkSliceServicesDeserializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543NetworkSliceServicesDeserializer.java new file mode 100644 index 0000000..3784128 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543NetworkSliceServicesDeserializer.java @@ -0,0 +1,154 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf.deserializers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; +import lombok.extern.slf4j.Slf4j; + +/** + * Jackson deserializer for RFC 9543 compliant Network Slice Services responses. + * + * Converts RFC 9543 YANG JSON format (with kebab-case property names and namespace prefix) + * to the existing domain model classes. + * + * RFC 9543 Response Structure: + * { + * "ietf-network-slice-service:network-slice-services": { + * "slo-sle-templates": { + * "slo-sle-template": [ ... ] + * }, + * "slice-service": [ ... ] + * } + * } + */ +@Slf4j +public class Rfc9543NetworkSliceServicesDeserializer extends JsonDeserializer { + + private static final String RFC9543_NAMESPACE = "ietf-network-slice-service:network-slice-services"; + private static final String TEMPLATES_CONTAINER = "slo-sle-templates"; + private static final String TEMPLATE_LIST = "slo-sle-template"; + private static final String SERVICES_LIST = "slice-service"; + + @Override + public NetworkSliceServices deserialize(JsonParser parser, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode rootNode = mapper.readTree(parser); + + log.debug("Deserializing RFC 9543 Network Slice Services response"); + + // Navigate to the RFC 9543 namespace root + JsonNode nssNode = rootNode.get(RFC9543_NAMESPACE); + if (nssNode == null) { + log.warn("RFC 9543 namespace not found. Attempting direct parse."); + nssNode = rootNode; + } + + NetworkSliceServices nss = new NetworkSliceServices(); + + // Parse SLO/SLE templates + List templates = parseSloSleTemplates(nssNode, mapper); + nss.setSloSleTemplates(templates); + log.debug("Parsed {} SLO/SLE templates", templates.size()); + + // Parse slice services + List services = parseSliceServices(nssNode, mapper); + nss.setSliceServices(services); + log.debug("Parsed {} slice services", services.size()); + + return nss; + } + + /** + * Parse RFC 9543 SLO/SLE templates from network slice services container. + * + * @param nssNode Root network-slice-services node + * @param mapper ObjectMapper for JSON conversion + * @return List of SloSleTemplate objects + */ + private List parseSloSleTemplates(JsonNode nssNode, ObjectMapper mapper) { + List templates = new ArrayList<>(); + + try { + JsonNode templatesContainer = nssNode.get(TEMPLATES_CONTAINER); + if (templatesContainer == null || templatesContainer.isNull()) { + log.debug("No SLO/SLE templates container found"); + return templates; + } + + JsonNode templatesList = templatesContainer.get(TEMPLATE_LIST); + if (templatesList == null || templatesList.isNull()) { + log.debug("No SLO/SLE templates list found"); + return templates; + } + + if (!templatesList.isArray()) { + log.warn("Templates is not an array"); + return templates; + } + + for (JsonNode templateNode : templatesList) { + try { + SloSleTemplate template = Rfc9543SloSleTemplateDeserializer.parseTemplate( + templateNode, mapper); + templates.add(template); + log.debug("Parsed SLO/SLE template: {}", template.getId()); + } catch (Exception e) { + log.error("Error parsing SLO/SLE template", e); + } + } + } catch (Exception e) { + log.error("Error parsing SLO/SLE templates container", e); + } + + return templates; + } + + /** + * Parse RFC 9543 slice services from network slice services container. + * + * @param nssNode Root network-slice-services node + * @param mapper ObjectMapper for JSON conversion + * @return List of SliceService objects + */ + private List parseSliceServices(JsonNode nssNode, ObjectMapper mapper) { + List services = new ArrayList<>(); + + try { + JsonNode servicesList = nssNode.get(SERVICES_LIST); + if (servicesList == null || servicesList.isNull()) { + log.debug("No slice-service list found"); + return services; + } + + if (!servicesList.isArray()) { + log.warn("Slice services is not an array"); + return services; + } + + for (JsonNode serviceNode : servicesList) { + try { + SliceService service = Rfc9543SliceServiceDeserializer.parseService( + serviceNode, mapper); + services.add(service); + log.debug("Parsed slice service: {}", service.getId()); + } catch (Exception e) { + log.error("Error parsing slice service", e); + } + } + } catch (Exception e) { + log.error("Error parsing slice services list", e); + } + + return services; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SliceServiceDeserializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SliceServiceDeserializer.java new file mode 100644 index 0000000..3735b50 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SliceServiceDeserializer.java @@ -0,0 +1,109 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf.deserializers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.time.OffsetDateTime; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.ServiceStatus; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import lombok.extern.slf4j.Slf4j; + +/** + * Jackson deserializer for RFC 9543 compliant Slice Service instances. + * + * Converts RFC 9543 YANG JSON format (kebab-case property names) + * to the existing SliceService domain model. + * + * RFC 9543 Slice Service Structure: + * { + * "id": "service-ecommerce-001", + * "description": "E-Commerce Platform Network Slice Service", + * "test-only": false, + * "status": "active" + * } + */ +@Slf4j +public class Rfc9543SliceServiceDeserializer extends JsonDeserializer { + + private static final String ID = "id"; + private static final String DESCRIPTION = "description"; + private static final String TEST_ONLY = "test-only"; + private static final String STATUS = "status"; + private static final String SERVICE_TAGS = "service-tags"; + private static final String TAG = "tag"; + + @Override + public SliceService deserialize(JsonParser parser, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode node = mapper.readTree(parser); + return parseService(node, mapper); + } + + /** + * Static method for parsing RFC 9543 slice service from JsonNode. + * Allows reuse by other deserializers. + * + * @param node JsonNode containing service data + * @param mapper ObjectMapper for JSON conversion + * @return Parsed SliceService object + */ + public static SliceService parseService(JsonNode node, ObjectMapper mapper) { + log.debug("Parsing RFC 9543 slice service"); + + SliceService service = new SliceService(); + + // Parse basic fields + if (node.has(ID)) { + service.setId(node.get(ID).asText()); + } + + if (node.has(DESCRIPTION)) { + service.setDescription(node.get(DESCRIPTION).asText()); + } + + // Parse test-only flag + if (node.has(TEST_ONLY)) { + service.setTestOnly(node.get(TEST_ONLY).asBoolean()); + } + + // Parse status + if (node.has(STATUS)) { + String statusStr = node.get(STATUS).asText(); + ServiceStatus status = parseStatus(statusStr); + service.setStatus(status); + } + + log.debug("Parsed slice service: id={}, description={}, testOnly={}, status={}", + service.getId(), service.getDescription(), service.getTestOnly(), + service.getStatus() != null ? service.getStatus().getOperState() : null); + + return service; + } + + /** + * Parse RFC 9543 status string to ServiceStatus object. + * + * @param statusStr Status string (e.g., "active", "pending", "terminated") + * @return ServiceStatus object + */ + private static ServiceStatus parseStatus(String statusStr) { + log.debug("Parsing service status: {}", statusStr); + ServiceStatus status = new ServiceStatus(); + + // Set operational state from status string + status.setOperState(statusStr); + + // Set administrative state to match operational state by default + status.setAdminState(statusStr); + + // Set last change to current time + status.setLastChange(OffsetDateTime.now()); + + return status; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SloSleTemplateDeserializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SloSleTemplateDeserializer.java new file mode 100644 index 0000000..dcac61a --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SloSleTemplateDeserializer.java @@ -0,0 +1,350 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf.deserializers; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.MetricBound; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceIsolationType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceSecurityType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceSloMetricType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SlePolicy; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; +import lombok.extern.slf4j.Slf4j; + +/** + * Jackson deserializer for RFC 9543 compliant SLO/SLE templates. + * + * Converts RFC 9543 YANG JSON format (kebab-case property names) + * to the existing SloSleTemplate domain model. + * + * RFC 9543 Template Structure: + * { + * "id": "PLATINUM-template", + * "description": "Two-way bandwidth: 1 Gbps, 95th percentile latency 50ms", + * "slo-policy": { + * "metric-bound": [ + * { + * "metric-type": "two-way-bandwidth", + * "metric-unit": "Gbps", + * "bound": "1" + * } + * ] + * }, + * "sle-policy": { + * "isolation": ["traffic-isolation"] + * } + * } + */ +@Slf4j +public class Rfc9543SloSleTemplateDeserializer extends JsonDeserializer { + + private static final String ID = "id"; + private static final String DESCRIPTION = "description"; + private static final String SLO_POLICY = "slo-policy"; + private static final String SLE_POLICY = "sle-policy"; + private static final String METRIC_BOUND = "metric-bound"; + private static final String ISOLATION = "isolation"; + private static final String PATH_CONSTRAINTS = "path-constraints"; + private static final String SECURITY = "security"; + + // Metric bound properties + private static final String METRIC_TYPE = "metric-type"; + private static final String METRIC_UNIT = "metric-unit"; + private static final String BOUND = "bound"; + private static final String PERCENTILE_VALUE = "percentile-value"; + + @Override + public SloSleTemplate deserialize(JsonParser parser, DeserializationContext ctxt) + throws IOException, JsonProcessingException { + ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + JsonNode node = mapper.readTree(parser); + return parseTemplate(node, mapper); + } + + /** + * Static method for parsing RFC 9543 template from JsonNode. + * Allows reuse by other deserializers. + * + * @param node JsonNode containing template data + * @param mapper ObjectMapper for JSON conversion + * @return Parsed SloSleTemplate object + */ + public static SloSleTemplate parseTemplate(JsonNode node, ObjectMapper mapper) { + log.debug("Parsing RFC 9543 SLO/SLE template"); + + SloSleTemplate template = new SloSleTemplate(); + + // Parse basic fields + if (node.has(ID)) { + template.setId(node.get(ID).asText()); + } + if (node.has(DESCRIPTION)) { + template.setDescription(node.get(DESCRIPTION).asText()); + } + + // Parse SLO policy + if (node.has(SLO_POLICY)) { + SloPolicy sloPolicy = parseSloPolicy(node.get(SLO_POLICY)); + template.setSloPolicy(sloPolicy); + } + + // Parse SLE policy + if (node.has(SLE_POLICY)) { + SlePolicy slePolicy = parseSlePolicy(node.get(SLE_POLICY)); + template.setSlePolicy(slePolicy); + } + + log.debug("Parsed template: id={}, slo={}, sle={}", template.getId(), + template.getSloPolicy() != null, template.getSlePolicy() != null); + + return template; + } + + /** + * Parse RFC 9543 SLO policy from YANG JSON. + * + * @param sloNode SLO policy node + * @return Parsed SloPolicy object + */ + private static SloPolicy parseSloPolicy(JsonNode sloNode) { + log.debug("Parsing SLO policy"); + SloPolicy policy = new SloPolicy(); + + // Parse metric bounds + List metricBounds = new ArrayList<>(); + if (sloNode.has(METRIC_BOUND)) { + JsonNode boundsNode = sloNode.get(METRIC_BOUND); + if (boundsNode.isArray()) { + for (JsonNode boundNode : boundsNode) { + MetricBound bound = parseMetricBound(boundNode); + if (bound != null) { + metricBounds.add(bound); + } + } + } + } + policy.setMetricBounds(metricBounds); + log.debug("Parsed {} metric bounds", metricBounds.size()); + + return policy; + } + + /** + * Parse RFC 9543 metric bound from YANG JSON. + * + * @param boundNode Metric bound node + * @return Parsed MetricBound object + */ + private static MetricBound parseMetricBound(JsonNode boundNode) { + log.debug("Parsing metric bound"); + MetricBound bound = new MetricBound(); + + // Parse metric type (convert from RFC 9543 string to enum) + if (boundNode.has(METRIC_TYPE)) { + String metricTypeStr = boundNode.get(METRIC_TYPE).asText(); + ServiceSloMetricType metricType = convertRfc9543MetricType(metricTypeStr); + if (metricType != null) { + bound.setMetricType(metricType); + } else { + log.warn("Unknown metric type: {}", metricTypeStr); + return null; + } + } + + // Parse metric unit + if (boundNode.has(METRIC_UNIT)) { + bound.setMetricUnit(boundNode.get(METRIC_UNIT).asText()); + } + + // Parse bound value + if (boundNode.has(BOUND)) { + String boundStr = boundNode.get(BOUND).asText(); + try { + bound.setBound(Long.parseLong(boundStr)); + } catch (NumberFormatException e) { + log.warn("Invalid bound value: {}", boundStr); + bound.setBound(0L); + } + } + + // Parse percentile value (optional, only for percentile-based metrics) + if (boundNode.has(PERCENTILE_VALUE)) { + String percentileStr = boundNode.get(PERCENTILE_VALUE).asText(); + try { + bound.setPercentileValue(new BigDecimal(percentileStr)); + } catch (NumberFormatException e) { + log.warn("Invalid percentile value: {}", percentileStr); + } + } + + log.debug("Parsed metric bound: type={}, unit={}, bound={}, percentile={}", + bound.getMetricType(), bound.getMetricUnit(), bound.getBound(), + bound.getPercentileValue()); + + return bound; + } + + /** + * Parse RFC 9543 SLE policy from YANG JSON. + * + * @param sleNode SLE policy node + * @return Parsed SlePolicy object + */ + private static SlePolicy parseSlePolicy(JsonNode sleNode) { + log.debug("Parsing SLE policy"); + SlePolicy policy = new SlePolicy(); + + // Parse isolation requirements + List isolations = new ArrayList<>(); + if (sleNode.has(ISOLATION)) { + JsonNode isolationNode = sleNode.get(ISOLATION); + if (isolationNode.isArray()) { + for (JsonNode isoNode : isolationNode) { + String isoStr = isoNode.asText(); + ServiceIsolationType isoType = convertRfc9543IsolationType(isoStr); + if (isoType != null) { + isolations.add(isoType); + } + } + } + } + policy.setIsolation(isolations); + log.debug("Parsed {} isolation requirements", isolations.size()); + + // Parse security requirements + List securities = new ArrayList<>(); + if (sleNode.has(SECURITY)) { + JsonNode securityNode = sleNode.get(SECURITY); + if (securityNode.isArray()) { + for (JsonNode secNode : securityNode) { + String secStr = secNode.asText(); + ServiceSecurityType secType = convertRfc9543SecurityType(secStr); + if (secType != null) { + securities.add(secType); + } + } + } + } + policy.setSecurity(securities); + log.debug("Parsed {} security requirements", securities.size()); + + // Note: Path constraints are handled by PathConstraints class + // which is more complex and requires further mapping + // Skipping path constraints in this basic deserializer + + return policy; + } + + /** + * Convert RFC 9543 metric type string to ServiceSloMetricType enum. + * + * @param rfc9543Type RFC 9543 metric type string (kebab-case) + * @return ServiceSloMetricType enum or null if not recognized + */ + private static ServiceSloMetricType convertRfc9543MetricType(String rfc9543Type) { + switch (rfc9543Type.toLowerCase()) { + // Bandwidth metrics + case "one-way-bandwidth": + return ServiceSloMetricType.ONE_WAY_BANDWIDTH; + case "two-way-bandwidth": + return ServiceSloMetricType.TWO_WAY_BANDWIDTH; + case "shared-bandwidth": + return ServiceSloMetricType.SHARED_BANDWIDTH; + + // Delay metrics - maximum (absolute) + case "one-way-delay-maximum": + return ServiceSloMetricType.ONE_WAY_DELAY_MAXIMUM; + case "two-way-delay-maximum": + return ServiceSloMetricType.TWO_WAY_DELAY_MAXIMUM; + + // Delay metrics - percentile + case "one-way-delay-percentile": + return ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE; + case "two-way-delay-percentile": + return ServiceSloMetricType.TWO_WAY_DELAY_PERCENTILE; + + // Jitter metrics - maximum (absolute) + case "one-way-jitter-maximum": + case "one-way-delay-variation-maximum": + return ServiceSloMetricType.ONE_WAY_DELAY_VARIATION_MAXIMUM; + case "two-way-jitter-maximum": + case "two-way-delay-variation-maximum": + return ServiceSloMetricType.TWO_WAY_DELAY_VARIATION_MAXIMUM; + + // Jitter metrics - percentile + case "one-way-jitter-percentile": + case "one-way-delay-variation-percentile": + return ServiceSloMetricType.ONE_WAY_DELAY_VARIATION_PERCENTILE; + case "two-way-jitter-percentile": + case "two-way-delay-variation-percentile": + return ServiceSloMetricType.TWO_WAY_DELAY_VARIATION_PERCENTILE; + + // Packet loss metrics + case "one-way-packet-loss": + return ServiceSloMetricType.ONE_WAY_PACKET_LOSS; + case "two-way-packet-loss": + return ServiceSloMetricType.TWO_WAY_PACKET_LOSS; + + default: + return null; + } + } + + /** + * Convert RFC 9543 isolation type string to ServiceIsolationType enum. + * + * @param rfc9543Type RFC 9543 isolation type string + * @return ServiceIsolationType enum or null if not recognized + */ + private static ServiceIsolationType convertRfc9543IsolationType(String rfc9543Type) { + switch (rfc9543Type.toLowerCase()) { + case "traffic-isolation": + return ServiceIsolationType.TRAFFIC_ISOLATION; + case "physical-isolation": + return ServiceIsolationType.PHYSICAL_ISOLATION; + case "logical-isolation": + return ServiceIsolationType.LOGICAL_ISOLATION; + case "resource-isolation": + return ServiceIsolationType.RESOURCE_ISOLATION; + case "dedicated-resources": + return ServiceIsolationType.DEDICATED_RESOURCES; + case "shared-resources-limited": + return ServiceIsolationType.SHARED_RESOURCES_LIMITED; + default: + return null; + } + } + + /** + * Convert RFC 9543 security type string to ServiceSecurityType enum. + * + * @param rfc9543Type RFC 9543 security type string + * @return ServiceSecurityType enum or null if not recognized + */ + private static ServiceSecurityType convertRfc9543SecurityType(String rfc9543Type) { + switch (rfc9543Type.toLowerCase()) { + case "encryption-required": + return ServiceSecurityType.ENCRYPTION_REQUIRED; + case "authentication-required": + return ServiceSecurityType.AUTHENTICATION_REQUIRED; + case "integrity-protection": + return ServiceSecurityType.INTEGRITY_PROTECTION; + case "confidentiality-required": + return ServiceSecurityType.CONFIDENTIALITY_REQUIRED; + case "secure-routing": + return ServiceSecurityType.SECURE_ROUTING; + case "vpn-required": + return ServiceSecurityType.VPN_REQUIRED; + default: + return null; + } + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/RestconfServerDemo.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/RestconfServerDemo.java new file mode 100644 index 0000000..5a96290 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/RestconfServerDemo.java @@ -0,0 +1,43 @@ +package org.etsi.osl.controllers.ietf.ns.demo; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.ComponentScan; +import lombok.extern.slf4j.Slf4j; + +/** + * Simple demo RESTCONF server that serves example network-slice-services. + * + * This server: + * - Listens on port 11880 + * - Provides RESTCONF endpoints for network slice services + * - Serves example SLO/SLE templates + * - Demonstrates RFC 8040 RESTCONF protocol compliance + * + * Usage: + * 1. Run this application + * 2. Access endpoints at http://localhost:11880/restconf/data/... + * + * Example RESTCONF operations: + * - GET http://localhost:11880/restconf/data/ietf-network-slice-service:network-slice-services/slo-sle-templates + * - GET http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services + * - GET http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services + * - POST http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services + * + * @author ctranoris + */ +@SpringBootApplication +@ComponentScan(basePackages = { + "org.etsi.osl.controllers.ietf.ns.demo", + +}) +@Slf4j +public class RestconfServerDemo { + + public static void main(String[] args) { + log.info("Starting RESTCONF Server Demo on port 11880..."); + new SpringApplicationBuilder(RestconfServerDemo.class) + .profiles("demo") + .run(args); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543DemoService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543DemoService.java new file mode 100644 index 0000000..82db05d --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543DemoService.java @@ -0,0 +1,223 @@ +package org.etsi.osl.controllers.ietf.ns.demo; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; + +/** + * RFC 9543 compliant demo service generator. + * + * Generates example IETF Network Slice Service templates and services + * according to draft-ietf-teas-ietf-network-slice-nbi-yang-25. + * + * This service creates demo data in the exact format specified in the RFC. + */ +@Service +@Slf4j +public class Rfc9543DemoService { + + /** + * Create RFC 9543 compliant network slice services response. + * + * @return Response with proper YANG structure + */ + public Rfc9543NetworkSliceServicesResponse createDemoServices() { + log.info("Generating RFC 9543 compliant network slice services..."); + + Rfc9543NetworkSliceServicesResponse response = new Rfc9543NetworkSliceServicesResponse(); + + // Create main container + Rfc9543NetworkSliceServicesResponse.NetworkSliceServices nss = + new Rfc9543NetworkSliceServicesResponse.NetworkSliceServices(); + + // Create templates container with proper templates + Rfc9543NetworkSliceServicesResponse.SloSleTemplatesContainer templatesContainer = + new Rfc9543NetworkSliceServicesResponse.SloSleTemplatesContainer(); + + templatesContainer.setSloSleTemplate(createRfc9543Templates()); + nss.setSloSleTemplates(templatesContainer); + + // Create slice services + nss.setSliceServices(createRfc9543SliceServices()); + + response.setNetworkSliceServices(nss); + + log.info("Created {} templates and {} services", + nss.getSloSleTemplates().getSloSleTemplate().size(), + nss.getSliceServices().size()); + + return response; + } + + /** + * Create RFC 9543 compliant SLO/SLE templates. + * Based on Figure 6 from draft-ietf-teas-ietf-network-slice-nbi-yang-25. + */ + private List createRfc9543Templates() { + List templates = new ArrayList<>(); + + // PLATINUM Template + templates.add(createPlatinumTemplate()); + + // GOLD Template + templates.add(createGoldTemplate()); + + // SILVER Template (additional example) + templates.add(createSilverTemplate()); + + return templates; + } + + /** + * PLATINUM Template: High performance, low latency + * Two-way bandwidth: 1 Gbps, 95th percentile latency 50ms + */ + private Rfc9543SloSleTemplate createPlatinumTemplate() { + Rfc9543SloSleTemplate template = new Rfc9543SloSleTemplate(); + template.setId("DEMO-PLATINUM-template"); + template.setDescription("Two-way bandwidth: 1 Gbps, 95th percentile latency 50ms"); + + // SLO Policy + Rfc9543SloSleTemplate.SloPolicy sloPolicy = new Rfc9543SloSleTemplate.SloPolicy(); + + // Metric 1: Two-way bandwidth + Rfc9543SloSleTemplate.MetricBound bandwidthBound = + new Rfc9543SloSleTemplate.MetricBound(); + bandwidthBound.setMetricType("two-way-bandwidth"); + bandwidthBound.setMetricUnit("Gbps"); + bandwidthBound.setBound("1"); + sloPolicy.getMetricBound().add(bandwidthBound); + + // Metric 2: Two-way delay percentile (95th percentile) + Rfc9543SloSleTemplate.MetricBound delayBound = + new Rfc9543SloSleTemplate.MetricBound(); + delayBound.setMetricType("two-way-delay-percentile"); + delayBound.setMetricUnit("milliseconds"); + delayBound.setPercentileValue("95.000"); + delayBound.setBound("50"); + sloPolicy.getMetricBound().add(delayBound); + + template.setSloPolicy(sloPolicy); + + // SLE Policy + Rfc9543SloSleTemplate.SlePolicy slePolicy = new Rfc9543SloSleTemplate.SlePolicy(); + slePolicy.getIsolation().add("traffic-isolation"); + template.setSlePolicy(slePolicy); + + return template; + } + + /** + * GOLD Template: High performance with guaranteed latency + * Two-way bandwidth: 1 Gbps, maximum latency 100ms + */ + private Rfc9543SloSleTemplate createGoldTemplate() { + Rfc9543SloSleTemplate template = new Rfc9543SloSleTemplate(); + template.setId("DEMO-GOLD-template"); + template.setDescription("Two-way bandwidth: 1 Gbps, maximum latency 100ms"); + + // SLO Policy + Rfc9543SloSleTemplate.SloPolicy sloPolicy = new Rfc9543SloSleTemplate.SloPolicy(); + + // Metric 1: Two-way bandwidth + Rfc9543SloSleTemplate.MetricBound bandwidthBound = + new Rfc9543SloSleTemplate.MetricBound(); + bandwidthBound.setMetricType("two-way-bandwidth"); + bandwidthBound.setMetricUnit("Gbps"); + bandwidthBound.setBound("1"); + sloPolicy.getMetricBound().add(bandwidthBound); + + // Metric 2: Two-way delay maximum + Rfc9543SloSleTemplate.MetricBound delayBound = + new Rfc9543SloSleTemplate.MetricBound(); + delayBound.setMetricType("two-way-delay-maximum"); + delayBound.setMetricUnit("milliseconds"); + delayBound.setBound("100"); + sloPolicy.getMetricBound().add(delayBound); + + template.setSloPolicy(sloPolicy); + + // SLE Policy + Rfc9543SloSleTemplate.SlePolicy slePolicy = new Rfc9543SloSleTemplate.SlePolicy(); + slePolicy.getIsolation().add("traffic-isolation"); + template.setSlePolicy(slePolicy); + + return template; + } + + /** + * SILVER Template: Standard performance + * Two-way bandwidth: 500 Mbps, 99th percentile latency 100ms + */ + private Rfc9543SloSleTemplate createSilverTemplate() { + Rfc9543SloSleTemplate template = new Rfc9543SloSleTemplate(); + template.setId("DEMO-SILVER-template"); + template.setDescription("Two-way bandwidth: 500 Mbps, 99th percentile latency 100ms"); + + // SLO Policy + Rfc9543SloSleTemplate.SloPolicy sloPolicy = new Rfc9543SloSleTemplate.SloPolicy(); + + // Metric 1: Two-way bandwidth + Rfc9543SloSleTemplate.MetricBound bandwidthBound = + new Rfc9543SloSleTemplate.MetricBound(); + bandwidthBound.setMetricType("two-way-bandwidth"); + bandwidthBound.setMetricUnit("Mbps"); + bandwidthBound.setBound("500"); + sloPolicy.getMetricBound().add(bandwidthBound); + + // Metric 2: Two-way delay percentile (99th percentile) + Rfc9543SloSleTemplate.MetricBound delayBound = + new Rfc9543SloSleTemplate.MetricBound(); + delayBound.setMetricType("two-way-delay-percentile"); + delayBound.setMetricUnit("milliseconds"); + delayBound.setPercentileValue("99.000"); + delayBound.setBound("100"); + sloPolicy.getMetricBound().add(delayBound); + + template.setSloPolicy(sloPolicy); + + // SLE Policy + Rfc9543SloSleTemplate.SlePolicy slePolicy = new Rfc9543SloSleTemplate.SlePolicy(); + slePolicy.getIsolation().add("traffic-isolation"); + template.setSlePolicy(slePolicy); + + return template; + } + + /** + * Create RFC 9543 compliant slice services. + */ + private List createRfc9543SliceServices() { + List services = new ArrayList<>(); + + // Service 1: E-Commerce + Rfc9543NetworkSliceServicesResponse.SliceService ecommerce = + new Rfc9543NetworkSliceServicesResponse.SliceService(); + ecommerce.setId("service-ecommerce-001"); + ecommerce.setDescription("E-Commerce Platform Network Slice Service"); + ecommerce.setTestOnly(false); + ecommerce.setStatus("active"); + services.add(ecommerce); + + // Service 2: Video Streaming + Rfc9543NetworkSliceServicesResponse.SliceService video = + new Rfc9543NetworkSliceServicesResponse.SliceService(); + video.setId("service-video-001"); + video.setDescription("Video Streaming CDN Network Slice Service"); + video.setTestOnly(false); + video.setStatus("active"); + services.add(video); + + // Service 3: Enterprise WAN + Rfc9543NetworkSliceServicesResponse.SliceService enterprise = + new Rfc9543NetworkSliceServicesResponse.SliceService(); + enterprise.setId("service-enterprise-wan-001"); + enterprise.setDescription("Enterprise WAN Network Slice Service"); + enterprise.setTestOnly(false); + enterprise.setStatus("active"); + services.add(enterprise); + + return services; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543NetworkSliceServicesResponse.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543NetworkSliceServicesResponse.java new file mode 100644 index 0000000..b650059 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543NetworkSliceServicesResponse.java @@ -0,0 +1,136 @@ +package org.etsi.osl.controllers.ietf.ns.demo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * RFC 9543 compliant response wrapper for network-slice-services. + * + * This class represents the complete RESTCONF response structure as defined + * in draft-ietf-teas-ietf-network-slice-nbi-yang-25. + * + * The response follows the YANG container hierarchy: + * { + * "ietf-network-slice-service:network-slice-services": { + * "slo-sle-templates": { + * "slo-sle-template": [ ... ] + * }, + * "slice-service": [ ... ] + * } + * } + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Rfc9543NetworkSliceServicesResponse { + + /** + * YANG container for network slice services with RFC 9543 namespace. + * This is the root container for all network slice service data. + */ + @JsonProperty("ietf-network-slice-service:network-slice-services") + private NetworkSliceServices networkSliceServices; + + /** + * Network slice services container with templates and services. + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class NetworkSliceServices { + /** + * Container for SLO/SLE template definitions. + * These are reusable service level templates. + */ + @JsonProperty("slo-sle-templates") + private SloSleTemplatesContainer sloSleTemplates; + + /** + * Container for slice service instances. + * These are actual service instances that may reference templates. + */ + @JsonProperty("slice-service") + private List sliceServices = new ArrayList<>(); + } + + /** + * Container for SLO/SLE templates. + * Holds a list of reusable template definitions. + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SloSleTemplatesContainer { + /** + * List of SLO/SLE template definitions. + * Each template defines a set of service level requirements. + */ + @JsonProperty("slo-sle-template") + private List sloSleTemplate = new ArrayList<>(); + } + + /** + * Network Slice Service instance. + * + * Represents a customer-requested network slice service with specific + * connectivity and SLO/SLE requirements. + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SliceService { + /** + * Unique identifier for the slice service. + * Example: "service-ecommerce-001" + */ + @JsonProperty("id") + private String id; + + /** + * Human-readable description of the slice service. + * Example: "E-Commerce Platform Network Slice Service" + */ + @JsonProperty("description") + private String description; + + /** + * Service tags for classification and management. + * Optional list of tags. + */ + @JsonProperty("service-tags") + private ServiceTags serviceTags; + + /** + * Test-only flag indicating this is a feasibility check. + * If present and set to true, the service is not provisioned. + * Optional field. + */ + @JsonProperty("test-only") + private Boolean testOnly; + + /** + * Status of the slice service. + * Example: "active", "pending", "terminated" + */ + @JsonProperty("status") + private String status; + } + + /** + * Service tags container for classification. + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ServiceTags { + /** + * List of tag values for service categorization. + */ + @JsonProperty("tag") + private List tags = new ArrayList<>(); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543RestController.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543RestController.java new file mode 100644 index 0000000..1d30752 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543RestController.java @@ -0,0 +1,119 @@ +package org.etsi.osl.controllers.ietf.ns.demo; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import lombok.extern.slf4j.Slf4j; + +/** + * RFC 9543 compliant RESTCONF controller. + * + * Implements the exact response format specified in + * draft-ietf-teas-ietf-network-slice-nbi-yang-25. + * + * Endpoints: + * - GET /restconf/data/ietf-network-slice-service:network-slice-services + */ +@RestController +@RequestMapping("/restconf/data") +@Slf4j +public class Rfc9543RestController { + + @Autowired + private Rfc9543DemoService demoService; + + private Rfc9543NetworkSliceServicesResponse cachedResponse; + + /** + * GET ietf-network-slice-service:network-slice-services + * + * Returns RFC 9543 compliant network slice services with YANG structure: + * { + * "ietf-network-slice-service:network-slice-services": { + * "slo-sle-templates": { + * "slo-sle-template": [ ... ] + * }, + * "slice-service": [ ... ] + * } + * } + */ + @GetMapping("/ietf-network-slice-service:network-slice-services") + public ResponseEntity getNetworkSliceServices() { + log.info("GET /ietf-network-slice-service:network-slice-services"); + + if (cachedResponse == null) { + log.debug("Initializing RFC 9543 demo data..."); + cachedResponse = demoService.createDemoServices(); + } + + return ResponseEntity.ok(cachedResponse); + } + + /** + * GET ietf-network-slice-service:network-slice-services/slo-sle-templates + * + * Returns the SLO/SLE templates wrapped in RFC 9543 format with proper YANG structure: + * { + * "ietf-network-slice-service:network-slice-services": { + * "slo-sle-templates": { + * "slo-sle-template": [ ... ] + * } + * } + * } + */ + @GetMapping("/ietf-network-slice-service:network-slice-services/slo-sle-templates") + public ResponseEntity getSloSleTemplates() { + log.info("GET /ietf-network-slice-service:network-slice-services/slo-sle-templates"); + + if (cachedResponse == null) { + cachedResponse = demoService.createDemoServices(); + } + + // Create a response with only templates (no slice-services) + Rfc9543NetworkSliceServicesResponse templatesOnlyResponse = new Rfc9543NetworkSliceServicesResponse(); + Rfc9543NetworkSliceServicesResponse.NetworkSliceServices nss = new Rfc9543NetworkSliceServicesResponse.NetworkSliceServices(); + nss.setSloSleTemplates(cachedResponse.getNetworkSliceServices().getSloSleTemplates()); + // Don't set slice-services - templates endpoint only returns templates + templatesOnlyResponse.setNetworkSliceServices(nss); + + return ResponseEntity.ok(templatesOnlyResponse); + } + + /** + * GET ietf-network-slice-service:network-slice-services/slice-service + * + * Returns just the slice services. + */ + @GetMapping("/ietf-network-slice-service:network-slice-services/slice-service") + public ResponseEntity getSliceServices() { + log.info("GET /ietf-network-slice-service:network-slice-services/slice-service"); + + if (cachedResponse == null) { + cachedResponse = demoService.createDemoServices(); + } + + // Return as a container with "slice-service" key to be YANG compliant + SliceServicesContainer container = new SliceServicesContainer(); + container.setSliceService(cachedResponse.getNetworkSliceServices().getSliceServices()); + + return ResponseEntity.ok(container); + } + + /** + * Wrapper for slice services to maintain YANG structure. + */ + private static class SliceServicesContainer { + @com.fasterxml.jackson.annotation.JsonProperty("slice-service") + private java.util.List sliceService; + + public java.util.List getSliceService() { + return sliceService; + } + + public void setSliceService(java.util.List sliceService) { + this.sliceService = sliceService; + } + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543SloSleTemplate.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543SloSleTemplate.java new file mode 100644 index 0000000..3fcb71a --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543SloSleTemplate.java @@ -0,0 +1,173 @@ +package org.etsi.osl.controllers.ietf.ns.demo; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * RFC 9543 compliant SLO/SLE Template structure for RESTCONF JSON responses. + * + * This class represents the YANG data model for slo-sle-template as defined in + * draft-ietf-teas-ietf-network-slice-nbi-yang-25. + * + * YANG Structure: + * { + * "ietf-network-slice-service:network-slice-services": { + * "slo-sle-templates": { + * "slo-sle-template": [ + * { + * "id": "...", + * "description": "...", + * "slo-policy": { + * "metric-bound": [ ... ] + * }, + * "sle-policy": { + * "isolation": [ ... ] + * } + * } + * ] + * } + * } + * } + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Rfc9543SloSleTemplate { + + /** + * Unique identifier for the SLO/SLE template. + * Example: "PLATINUM-template", "GOLD-template" + */ + @JsonProperty("id") + private String id; + + /** + * Human-readable description of the template. + * Example: "Two-way bandwidth: 1 Gbps, 95th percentile latency 50ms" + */ + @JsonProperty("description") + private String description; + + /** + * Service Level Objectives policy containing metric bounds. + */ + @JsonProperty("slo-policy") + private SloPolicy sloPolicy; + + /** + * Service Level Expectations policy containing isolation and path constraints. + */ + @JsonProperty("sle-policy") + private SlePolicy slePolicy; + + /** + * Service Level Objectives container. + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SloPolicy { + /** + * List of metric bounds defining performance targets. + */ + @JsonProperty("metric-bound") + private List metricBound = new ArrayList<>(); + } + + /** + * Service Level Expectations container. + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SlePolicy { + /** + * List of isolation requirements. + * Example: ["traffic-isolation"] + */ + @JsonProperty("isolation") + private List isolation = new ArrayList<>(); + + /** + * List of path constraint requirements. + * Optional field. + */ + @JsonProperty("path-constraints") + private List pathConstraints = new ArrayList<>(); + } + + /** + * Individual metric bound defining a performance metric constraint. + * + * YANG Structure: + * { + * "metric-type": "two-way-bandwidth", + * "metric-unit": "Gbps", + * "bound": "1" + * } + * + * For percentile-based metrics: + * { + * "metric-type": "two-way-delay-percentile", + * "metric-unit": "milliseconds", + * "percentile-value": "95.000", + * "bound": "50" + * } + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class MetricBound { + /** + * Type of metric being constrained. + * + * Supported metric types (from RFC 9543): + * - two-way-bandwidth: Guaranteed minimum bandwidth (both directions) + * - two-way-delay-maximum: Maximum one-way delay + * - two-way-delay-percentile: Percentile-based delay + * - two-way-jitter-maximum: Maximum delay variation + * - two-way-jitter-percentile: Percentile-based jitter + * - two-way-packet-loss: Packet loss percentage + * + * Example: "two-way-bandwidth", "two-way-delay-percentile" + */ + @JsonProperty("metric-type") + private String metricType; + + /** + * Unit of measurement for the metric. + * + * Examples: + * - Bandwidth: "bps", "Kbps", "Mbps", "Gbps" + * - Delay/Jitter: "milliseconds", "microseconds", "nanoseconds" + * - Loss: "percentage" + */ + @JsonProperty("metric-unit") + private String metricUnit; + + /** + * Upper bound value for the metric. + * + * This is the maximum allowed value for the metric. + * Example: "1" (when metric-unit is Gbps), "50" (when metric-unit is milliseconds) + */ + @JsonProperty("bound") + private String bound; + + /** + * Percentile value for percentile-based metrics (0.0 to 100.0). + * + * Only present for percentile metrics like: + * - two-way-delay-percentile + * - two-way-jitter-percentile + * + * Example: "95.000" for 95th percentile + */ + @JsonProperty("percentile-value") + private String percentileValue; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/SecurityConfig.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/SecurityConfig.java new file mode 100644 index 0000000..100e78a --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/SecurityConfig.java @@ -0,0 +1,103 @@ +package org.etsi.osl.controllers.ietf.ns.demo; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import lombok.extern.slf4j.Slf4j; + +/** + * Spring Security Configuration for Demo RESTCONF Server. + * + * Provides HTTP Basic Authentication for all RESTCONF endpoints. + * + * Default credentials: + * - Username: admin + * - Password: admin123 + * + * Additional users: + * - Username: user + * - Password: user123 + * + * To authenticate with curl: + * curl -u admin:admin123 http://localhost:11880/restconf/data/... + * + * Or with header: + * curl -H "Authorization: Basic YWRtaW46YWRtaW4xMjM=" http://localhost:11880/restconf/data/... + */ +@Configuration +@EnableWebSecurity +@Slf4j +public class SecurityConfig { + + /** + * Configure HTTP security with Basic Authentication. + * All endpoints under /restconf require authentication. + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + log.info("Configuring Spring Security with HTTP Basic Authentication"); + + http + // Require authentication for all /restconf endpoints + .authorizeHttpRequests(authz -> authz + .requestMatchers("/restconf/**").authenticated() + .requestMatchers("/actuator/health").permitAll() // Allow health check without auth + .anyRequest().authenticated() + ) + // Enable HTTP Basic Authentication + .httpBasic(basic -> { + log.debug("HTTP Basic Authentication enabled"); + }) + // Disable CSRF for demo (enable for production) + .csrf(csrf -> csrf.disable()); + + return http.build(); + } + + /** + * Define in-memory users for demo purposes. + * + * Users: + * 1. admin / admin123 - Full access + * 2. user / user123 - Read-only access (can be implemented with role-based security) + */ + @Bean + public UserDetailsService userDetailsService() { + log.info("Creating in-memory user details service for authentication"); + + // Admin user with full access + UserDetails admin = User.builder() + .username("admin") + .password(passwordEncoder().encode("admin123")) + .roles("ADMIN", "USER") + .build(); + + // Regular user + UserDetails user = User.builder() + .username("user") + .password(passwordEncoder().encode("user123")) + .roles("USER") + .build(); + + log.info("Created users: admin (ADMIN, USER), user (USER)"); + + return new InMemoryUserDetailsManager(admin, user); + } + + /** + * Password encoder using BCrypt. + * Required for secure password storage. + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/ExcludeFromMapping.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/ExcludeFromMapping.java new file mode 100644 index 0000000..9b0c110 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/ExcludeFromMapping.java @@ -0,0 +1,46 @@ +package org.etsi.osl.controllers.ietf.ns.domain.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a field to be excluded from EntityToLogicalResourceMapper field processing. + * + * Use this annotation to mark fields that: + * - Are already mapped to top-level LogicalResource properties (id, name, description) + * - Are internal implementation details (timestamps if not needed, foreign keys) + * - Are bidirectional relationship back-references + * - Should not be included in characteristics or relationships + * + * Example: + *
+ * @ExcludeFromMapping(reason = "Mapped as LogicalResource.uuid")
+ * private String id;
+ *
+ * @ExcludeFromMapping(reason = "Mapped separately as physical device relationships")
+ * @OneToMany
+ * private List physicalDevices;
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ExcludeFromMapping { + + /** + * Reason why this field is excluded from mapping. + * Used for documentation and debugging. + * + * @return Reason description + */ + String reason() default "Field excluded from mapping"; + + /** + * Whether to log when this field is skipped. + * Useful for debugging field processing logic. + * + * @return true to log, false to silently skip + */ + boolean logIfSkipped() default false; +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceMappable.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceMappable.java new file mode 100644 index 0000000..78daa0e --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceMappable.java @@ -0,0 +1,239 @@ +package org.etsi.osl.controllers.ietf.ns.domain.common; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.etsi.osl.tmf.ri639.model.LogicalResource; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; + +/** + * Interface for entities that can be mapped to/from TMF639 LogicalResource instances. + * Provides a contract for both forward (entity → resource) and reverse (resource → entity) mapping operations. + * + * Implementations should: + * 1. Provide entity ID, name, and description via abstract methods + * 2. Define a VERSION constant for schema versioning + * 3. Be annotated with @ExcludeFromMapping for fields not meant for mapping + * 4. Override getFieldMappings() to declare characteristic-to-property mappings + * 5. Override hasRelationships() and getRelationshipFields() if entity has relationships + * + * Usage: + *
+ * // Forward mapping: entity → resource
+ * LogicalResourceMappable entity = ...;
+ * LogicalResource resource = forwardMapper.toLogicalResource(entity);
+ *
+ * // Reverse mapping: resource → entity
+ * LogicalResource resource = ...;
+ * MyEntity entity = reverseMapper.fromLogicalResource(resource, MyEntity.class);
+ * 
+ */ +public interface LogicalResourceMappable { + + /** + * Get the entity type name for resource category. + * Default implementation returns the class simple name. + * Can be overridden for custom naming. + * + * Examples: "Datacenter", "EdgeSite", "ComputeDevice", "Region" + * + * @return Entity type name + */ + default String getEntityTypeName() { + return this.getClass().getSimpleName(); + } + + /** + * Get the unique identifier for this entity. + * Used as LogicalResource.uuid. + * + * @return Entity ID (UUID string) + */ + String getEntityId(); + + /** + * Get the display name for this entity. + * Used as LogicalResource.name. + * + * @return Entity name + */ + String getEntityName(); + + /** + * Get the description for this entity. + * Used as LogicalResource.description. + * Can return null or empty string - mapper handles default descriptions. + * + * @return Entity description or null + */ + String getEntityDescription(); + + /** + * Get the version constant for this entity type. + * Used in resource specification reference versioning. + * + * Default implementation uses reflection to retrieve the VERSION field. + * Can be overridden for performance optimization. + * + * @return Version string (e.g., "0.0.1") + */ + default String getVersion() { + try { + Class clazz = this.getClass(); + // For subclasses, check superclass hierarchy + while (clazz != null && !clazz.equals(Object.class)) { + try { + Field versionField = clazz.getDeclaredField("VERSION"); + if (versionField != null) { + versionField.setAccessible(true); + Object version = versionField.get(null); + return version != null ? version.toString() : "0.0.1"; + } + } catch (NoSuchFieldException e) { + // Try superclass + clazz = clazz.getSuperclass(); + } + } + } catch (Exception e) { + // Silently fall back to default + } + return "0.0.1"; + } + + /** + * Check if this entity type should have status mapping. + * Override in entities with status fields to enable status mapping. + * + * @return true if entity has status that should be mapped + */ + default boolean hasStatusMapping() { + return false; + } + + /** + * Check if this entity has child physical devices. + * Override in Site subclasses to enable device relationship mapping. + * + * @return true if entity can have deployed physical devices + */ + default boolean hasPhysicalDevices() { + return false; + } + + /** + * Map entity status to TMF639 resource states. + * Override in entities that have status fields (Site, Region, PhysicalDevice). + * Default implementation does nothing - only entities with status should override. + * + * Implementations should set TMF resource state fields: + * - administrativeState + * - operationalState + * - resourceStatus + * - usageState + * + * @param resource LogicalResource to populate with status-based states + */ + @JsonIgnore + default void mapStatusToResourceStates(LogicalResource resource) { + // Default: no-op. Override in entities with status. + } + + /** + * Map entity administrative state to TMF639 resource administrative state. + * Override in PhysicalDevice class for device-specific administrative state mapping. + * Default implementation does nothing - only PhysicalDevice should override. + * + * Implementations should set TMF administrativeState field based on entity's administrative state. + * + * @param resource LogicalResource to populate with administrative state + */ + @JsonIgnore + default void mapAdministrativeStateToResource(LogicalResource resource) { + // Default: no-op. Override in PhysicalDevice. + } + + /** + * Map fields from a TMF639 LogicalResource back to this entity instance. + * Override in entities to populate fields from a LogicalResource during reverse mapping. + * Default implementation does nothing - only entities with reverse mapping should override. + * + * Implementations should extract characteristics and relationships from the resource + * and populate the corresponding entity fields. + * + * Called during deserialization: LogicalResource → Entity + * + * @param resource LogicalResource containing characteristics and relationships + * @param characteristicMap Map of characteristic names to values (helper for field extraction) + */ + @JsonIgnore + default void mapFromLogicalResource(LogicalResource resource, java.util.Map characteristicMap) { + // Default: no-op. Override in entities with reverse mapping. + } + + /** + * Map TMF639 resource states back to entity status/state fields. + * Override in entities that have status fields (Site, Region, PhysicalDevice). + * Default implementation does nothing - only entities with status should override. + * + * Implementations should extract TMF resource state fields and apply them to the entity: + * - administrativeState → entity administrative state if applicable + * - operationalState → compute entity status + * - resourceStatus → compute entity status + * - usageState → compute entity status + * + * @param resource LogicalResource containing state fields + */ + @JsonIgnore + default void applyResourceStates(LogicalResource resource) { + // Default: no-op. Override in entities with status. + } + + // ========== Reverse Mapping Support (LogicalResource → Entity) ========== + + /** + * Get field mappings for reverse mapping (LogicalResource characteristics → entity properties). + * Maps TMF characteristic field names to entity property names and types. + * + * Format: Maps characteristic field names (e.g., "rackCount", "totalPowerCapacity") to entity property names. + * + * + * Override in concrete entity classes to provide type-specific field mappings. + * Subclasses should call super.getFieldMappings() and add their own fields. + * + * @return Map of characteristic field names to entity property names (empty by default) + */ + @JsonIgnore + default Map getFieldMappings() { + return Map.of(); + } + + /** + * Check if this entity has relationships that need extraction from LogicalResource. + * Override in entities that have entity references + + * + * @return true if entity has relationships to extract from LogicalResource + */ + @JsonIgnore + default boolean hasRelationships() { + return false; + } + + /** + * Get the relationship field names for this entity. + * Used during reverse mapping to extract and populate entity references from LogicalResource. + * + * Format: List of relationship field names as defined in entity class. + + * + * Only override if hasRelationships() returns true. + * Should return the relationship field names as they appear in the entity class. + * + * @return List of relationship field names (empty by default) + */ + @JsonIgnore + default List getRelationshipFields() { + return List.of(); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/RelatedManagedResourceReference.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/RelatedManagedResourceReference.java new file mode 100644 index 0000000..22e85d2 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/RelatedManagedResourceReference.java @@ -0,0 +1,67 @@ +package org.etsi.osl.controllers.ietf.ns.domain.common; + +import org.etsi.osl.tmf.ri639.model.LogicalResource; + +/** + * Interface for entities that can have related managed resources. + * Provides a contract for entities that need to track relationships to other managed resources + * from the TMF Resource Inventory. + * + * Implementations should: + * 1. Maintain a relatedManagedResourceId field (writable by clients) + * 2. Maintain a relatedManagedResource field (read-only, service-managed) + * 3. Implement getter/setter methods for both fields + * + * Usage: + *
+ * // Client sets the ID
+ * device.setRelatedManagedResourceId("resource-uuid-123");
+ *
+ * // Service layer fetches and populates the resource
+ * mapper.setRelatedManagedResource(device);
+ *
+ * // Later retrieval includes the populated resource
+ * LogicalResource relatedResource = device.getRelatedManagedResource();
+ * 
+ * + * Examples of implementing classes: + * - PhysicalDevice - Links devices to related managed resources (clusters, controllers, etc.) + * - ComputeDomain - Links to related resource that describes the compute domain + * - Future: Any entity that needs to reference other managed resources + */ +public interface RelatedManagedResourceReference { + + /** + * Get the ID of the related managed resource. + * This is the writable field that clients can set to establish relationships. + * + * @return Related managed resource ID (UUID string), or null if not set + */ + String getRelatedManagedResourceId(); + + /** + * Set the ID of the related managed resource. + * Clients can use this to establish relationships to other managed resources. + * + * @param relatedManagedResourceId Related managed resource ID (UUID string) + */ + void setRelatedManagedResourceId(String relatedManagedResourceId); + + /** + * Get the related managed resource fetched from the inventory. + * This field is read-only and service-managed - it contains the actual LogicalResource + * object fetched from the TMF Resource Inventory based on relatedManagedResourceId. + * + * @return LogicalResource object, or null if not yet fetched/set + */ + LogicalResource getRelatedManagedResource(); + + /** + * Set the related managed resource fetched from the inventory. + * This is called by the service layer after fetching the resource from inventory. + * Clients cannot set this field directly (it's marked read-only in JSON). + * + * @param relatedManagedResource LogicalResource object fetched from inventory + */ + void setRelatedManagedResource(LogicalResource relatedManagedResource); +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceMapper.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceMapper.java new file mode 100644 index 0000000..8e36eb9 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceMapper.java @@ -0,0 +1,462 @@ +package org.etsi.osl.controllers.ietf.ns.mappers; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.util.Collection; +import org.etsi.osl.controllers.ietf.ns.api.CategoryConfigurationService; +import org.etsi.osl.controllers.ietf.ns.api.ResourceSpecificationTemplateRegistry; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; +import org.etsi.osl.controllers.ietf.ns.domain.common.ExcludeFromMapping; +import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceMappable; +import org.etsi.osl.tmf.common.model.Any; +import org.etsi.osl.tmf.common.model.service.ResourceRef; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationRef; +import org.etsi.osl.tmf.ri639.model.Characteristic; +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import org.etsi.osl.tmf.ri639.model.ResourceRelationship; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import jakarta.persistence.Transient; +import lombok.extern.slf4j.Slf4j; + +/** + * Mapper utility to transform domain entities to TMF639 LogicalResource instances + * for persistence in Resource Inventory. + * + * This refactored version uses a single generic method for all LogicalResourceMappable + * entities, reducing code duplication and improving maintainability. + * + * Strategy: + * - Use entity name as LogicalResource name + * - Use category from CategoryConfigurationService with entity type suffix + * - Reference the template ID from ResourceSpecificationTemplateRegistry + * - Convert all simple fields to Characteristic + * - Convert entity references to ResourceRelationship + * - Map entity status (if present) to TMF resource states + */ +@Component +@Slf4j +public class EntityToLogicalResourceMapper { + + private static final String RELATIONSHIP_TYPE_DEPENDENCY = "dependency"; + + @Autowired + private CategoryConfigurationService categoryConfig; + + @Autowired + private ResourceSpecificationTemplateRegistry templateRegistry; + + /** + * Generic mapping method for any LogicalResourceMappable entity. + * This single method replaces all the previous overloaded toLogicalResource() methods. + * + * @param entity The entity to map (must implement LogicalResourceMappable) + * @return LogicalResource instance ready for persistence + * @throws RuntimeException if entity type template not found in registry + * @throws IllegalArgumentException if entity is null + */ + public LogicalResource toLogicalResource(LogicalResourceMappable entity) { + if (entity == null) { + throw new IllegalArgumentException("Entity cannot be null"); + } + + log.debug("Mapping {} to LogicalResource", entity.getEntityTypeName()); + + LogicalResource resource = new LogicalResource(); + String entityType = entity.getEntityTypeName(); + + // Map basic attributes + resource.setName(entity.getEntityName()); + resource.setCategory(categoryConfig.getCategoryForEntity(entityType)); + resource.setDescription(entity.getEntityDescription()); + + // Set ID if entity has one + if (entity.getEntityId() != null) { + resource.setUuid(entity.getEntityId()); + } + + // Reference the resource specification template + setResourceSpecificationReference(resource, entityType, entity.getVersion()); + + // Handle status mapping via entity methods + if (entity.hasStatusMapping()) { + entity.mapStatusToResourceStates(resource); + } + + // Convert all fields to characteristics and relationships + convertFieldsToCharacteristicsAndRelationships(entity, resource); + + + + return resource; + } + + /** + * Set the resource specification reference to the template registered during bootstrap + */ + private void setResourceSpecificationReference(LogicalResource resource, + String entityType, + String version) { + String templateId = templateRegistry.getTemplateId(entityType) + .orElseThrow(() -> new RuntimeException( + entityType + " template not registered. Bootstrap may have failed.")); + + ResourceSpecificationRef specRef = new ResourceSpecificationRef(); + specRef.setId(templateId); + specRef.setName(entityType); + specRef.setVersion(version); + specRef.setReferredType("LogicalResourceSpecification"); + + resource.setResourceSpecification(specRef); + log.debug("Set resourceSpecification reference to template ID: {}", templateId); + } + /** + * Check if a class is a value object (embedded type) + */ + private boolean isValueObject(Class type) { + return false; + } + + + /** + * Convert all fields of an entity to characteristics and relationships + * Uses @ExcludeFromMapping annotation to skip fields + */ + private void convertFieldsToCharacteristicsAndRelationships(Object entity, LogicalResource resource) { + Class clazz = entity.getClass(); + + // Process fields from the entity class and its superclasses + while (clazz != null && !clazz.equals(Object.class)) { + for (Field field : clazz.getDeclaredFields()) { + try { + // Check if field is excluded from mapping + if (field.isAnnotationPresent(ExcludeFromMapping.class)) { + ExcludeFromMapping exclusion = field.getAnnotation(ExcludeFromMapping.class); + if (exclusion.logIfSkipped()) { + log.debug("Skipping field {} - {}", field.getName(), exclusion.reason()); + } + continue; + } + + field.setAccessible(true); + Object value = field.get(entity); + + // Skip static fields and @Transient fields + if (java.lang.reflect.Modifier.isStatic(field.getModifiers()) + || field.isAnnotationPresent(Transient.class)) { + continue; + } + + String fieldName = field.getName(); + Class fieldType = field.getType(); + + // Handle relatedManagedResourceId as a ResourceRelationship instead of a characteristic + if ("relatedManagedResourceId".equals(fieldName) && value != null && value instanceof String) { + addManagedResourceRelationship(resource, (String) value); + continue; + } + + // Handle entity relationships + if (isEntityType(fieldType)) { + if (value != null) { + addRelationship(resource, fieldName, fieldType, value); + } + } + // Handle collections - pass the field to check generic type + else if (Collection.class.isAssignableFrom(fieldType)) { + handleCollectionField(resource, fieldName, field, value); + } + // Handle value objects (embedded types) + else if (isValueObject(fieldType)) { + addValueObjectCharacteristics(resource, fieldName, value); + } + // Handle simple types as characteristics + else { + addCharacteristic(resource, fieldName, value, fieldType); + } + + } catch (IllegalAccessException e) { + log.warn("Could not access field {} in class {}", field.getName(), clazz.getSimpleName(), e); + } + } + clazz = clazz.getSuperclass(); + } + } + + /** + * Check if a class is an entity type + */ + private boolean isEntityType(Class type) { + return SloSleTemplate.class.isAssignableFrom(type); + } + + + /** + * Handle collection fields - distinguish between simple types and entities + */ + private void handleCollectionField(LogicalResource resource, String fieldName, Field field, Object value) { + Collection collection = (Collection) value; + + // Determine if collection holds entities by examining generic type parameter + String entityTypeName = getCollectionEntityType(field); + + if (collection.isEmpty()) { + if (entityTypeName != null) { + log.debug("Empty collection of {} entities for field: {} - no relationships to add", + entityTypeName, fieldName); + } + addArrayCharacteristic(resource, fieldName, "[]"); + return; + } + + if (entityTypeName != null) { + // Collection of entities → Create a relationship for each entity + log.debug("Handling collection of {} {} entities for field: {}", + collection.size(), entityTypeName, fieldName); + for (Object element : collection) { + addRelationship(resource, fieldName, element.getClass(), element); + } + } else { + // Collection of simple types → Single ARRAY characteristic + log.debug("Handling collection of {} simple types for field: {}", collection.size(), fieldName); + addArrayCharacteristic(resource, fieldName, convertCollectionToString(collection)); + } + } + + /** + * Get the entity type name from a collection field using reflection + */ + private String getCollectionEntityType(Field field) { + try { + java.lang.reflect.Type genericType = field.getGenericType(); + + if (genericType instanceof java.lang.reflect.ParameterizedType) { + java.lang.reflect.ParameterizedType paramType = (java.lang.reflect.ParameterizedType) genericType; + java.lang.reflect.Type[] typeArgs = paramType.getActualTypeArguments(); + + if (typeArgs.length > 0 && typeArgs[0] instanceof Class) { + Class elementType = (Class) typeArgs[0]; + + if (isEntityType(elementType)) { + return elementType.getSimpleName(); + } + } + } + } catch (Exception e) { + log.warn("Could not determine generic type for field {}: {}", field.getName(), e.getMessage()); + } + + return null; + } + + /** + * Add an ARRAY characteristic for collections + */ + private void addArrayCharacteristic(LogicalResource resource, String fieldName, String arrayValue) { + Characteristic characteristic = new Characteristic(); + characteristic.setName(fieldName); + characteristic.setValueType("ARRAY"); + + Any anyValue = new Any(); + anyValue.setValue(arrayValue); + anyValue.setAlias(fieldName); + characteristic.setValue(anyValue); + + resource.addResourceCharacteristicItem(characteristic); + log.debug("Added ARRAY characteristic: {} = {}", fieldName, arrayValue); + } + + /** + * Convert collection to string representation + */ + private String convertCollectionToString(Collection collection) { + if (collection == null || collection.isEmpty()) { + return "[]"; + } + + StringBuilder sb = new StringBuilder("["); + boolean first = true; + for (Object item : collection) { + if (!first) { + sb.append(", "); + } + if (item instanceof String) { + sb.append("\"").append(item).append("\""); + } else { + sb.append(item); + } + first = false; + } + sb.append("]"); + return sb.toString(); + } + + /** + * Add a characteristic for a simple field + */ + private void addCharacteristic(LogicalResource resource, String fieldName, Object value, Class type) { + Characteristic characteristic = new Characteristic(); + characteristic.setName(fieldName); + characteristic.setValueType(getValueType(type)); + + Any anyValue = new Any(); + anyValue.setValue(convertValueToString(value)); + anyValue.setAlias(fieldName); + characteristic.setValue(anyValue); + + resource.addResourceCharacteristicItem(characteristic); + log.debug("Added characteristic: {} = {}", fieldName, value); + } + + /** + * Add characteristics for value object fields + */ + private void addValueObjectCharacteristics(LogicalResource resource, String fieldName, Object valueObject) { + if (valueObject == null) { + return; + } + + Class voClass = valueObject.getClass(); + + for (Field voField : voClass.getDeclaredFields()) { + try { + voField.setAccessible(true); + Object voValue = voField.get(valueObject); + + if (voValue != null && !java.lang.reflect.Modifier.isStatic(voField.getModifiers())) { + String nestedFieldName = fieldName + "." + voField.getName(); + addCharacteristic(resource, nestedFieldName, voValue, voField.getType()); + } + } catch (IllegalAccessException e) { + log.warn("Could not access field {} in value object {}", voField.getName(), voClass.getSimpleName(), e); + } + } + } + + /** + * Add a ResourceRelationship for an entity reference + */ + private void addRelationship(LogicalResource resource, String fieldName, Class relatedClass, Object relatedEntity) { + ResourceRelationship relationship = new ResourceRelationship(); + + // Get the ID of the related entity + String relatedId = extractEntityId(relatedEntity); + + // Get the actual entity type name (use actual object class for polymorphic types) + String entityType = relatedEntity.getClass().getSimpleName(); + + ResourceRef resourceRef = new ResourceRef(); + resourceRef.setId(relatedId); + resourceRef.setName(fieldName); + resourceRef.setReferredType(entityType); + + if (relatedId != null) { + resourceRef.setHref("/api/topology/" + entityType.toLowerCase() + "s/" + relatedId); + } + + relationship.setResource(resourceRef); + relationship.setRelationshipType(RELATIONSHIP_TYPE_DEPENDENCY); + + resource.addResourceRelationshipItem(relationship); + log.debug("Added ResourceRelationship: field={}, referredType={}, id={}", fieldName, entityType, relatedId); + } + + /** + * Add a ResourceRelationship for a related managed resource reference + */ + private void addManagedResourceRelationship(LogicalResource resource, String relatedManagedResourceId) { + if (relatedManagedResourceId == null || relatedManagedResourceId.isEmpty()) { + return; + } + + ResourceRelationship relationship = new ResourceRelationship(); + + ResourceRef resourceRef = new ResourceRef(); + resourceRef.setId(relatedManagedResourceId); + resourceRef.setName("relatedManagedResource"); + resourceRef.setReferredType("LogicalResource"); + resourceRef.setHref("/api/inventory/resources/" + relatedManagedResourceId); + + relationship.setResource(resourceRef); + relationship.setRelationshipType(RELATIONSHIP_TYPE_DEPENDENCY); + + resource.addResourceRelationshipItem(relationship); + log.debug("Added managed resource relationship: id={}, referredType=LogicalResource", relatedManagedResourceId); + } + + /** + * Extract ID from an entity using reflection + */ + private String extractEntityId(Object entity) { + try { + Field idField = findIdField(entity.getClass()); + if (idField != null) { + idField.setAccessible(true); + Object id = idField.get(entity); + return id != null ? id.toString() : null; + } + } catch (IllegalAccessException e) { + log.warn("Could not extract ID from entity {}", entity.getClass().getSimpleName(), e); + } + return null; + } + + /** + * Find the ID field in a class hierarchy + */ + private Field findIdField(Class clazz) { + while (clazz != null && !clazz.equals(Object.class)) { + for (Field field : clazz.getDeclaredFields()) { + if ("id".equals(field.getName()) || "siteId".equals(field.getName())) { + return field; + } + } + clazz = clazz.getSuperclass(); + } + return null; + } + + /** + * Get TMF value type from Java type + */ + private String getValueType(Class type) { + if (type.equals(String.class)) { + return "TEXT"; + } else if (type.equals(Integer.class) || type.equals(int.class) || + type.equals(Long.class) || type.equals(long.class)) { + return "NUMBER"; + } else if (type.equals(Boolean.class) || type.equals(boolean.class)) { + return "BOOLEAN"; + } else if (type.equals(Double.class) || type.equals(double.class) || + type.equals(Float.class) || type.equals(float.class)) { + return "NUMBER"; + } else if (type.isEnum()) { + return "TEXT"; + } else if (LocalDateTime.class.isAssignableFrom(type)) { + return "DATETIME"; + } else if (Collection.class.isAssignableFrom(type)) { + return "ARRAY"; + } else { + return "TEXT"; + } + } + + /** + * Convert value to string representation + */ + private String convertValueToString(Object value) { + if (value == null) { + return ""; + } + + if (value instanceof Collection) { + return convertCollectionToString((Collection) value); + } else if (value instanceof LocalDateTime) { + return value.toString(); + } else if (value.getClass().isEnum()) { + return ((Enum) value).name(); + } else { + return value.toString(); + } + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java new file mode 100644 index 0000000..3194c49 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java @@ -0,0 +1,453 @@ +package org.etsi.osl.controllers.ietf.ns.mappers; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Set; +import org.etsi.osl.controllers.ietf.ns.api.CategoryConfigurationService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.AvailabilityType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.MetricBound; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.PathConstraints; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SlePolicy; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; +import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceMappable; +import org.etsi.osl.tmf.common.model.Any; +import org.etsi.osl.tmf.pm628.model.AdministrativeState; +import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCharacteristic; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCharacteristicValue; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationRelationship; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; + +/** + * Mapper utility to transform domain topology entities to TMF LogicalResourceSpecification + * Strategy: + * - Use class name as LogicalResourceSpecification name + * - Use category from CategoryConfigurationService + * - Use class VERSION constant as version + * - Convert all simple fields to ResourceSpecificationCharacteristic + * - Convert entity references to ResourceSpecificationRelationship + */ +@Component +@Slf4j +public class EntityToLogicalResourceSpecMapper { + + private static final String RELATIONSHIP_TYPE_DEPENDENCY = "dependency"; + + @Autowired + private CategoryConfigurationService categoryConfig; + + // ========== Type Registries ========== + + /** + * Registry of all entity types (for example, topology sites, physical devices, etc) + * Used for efficient type checking without long chains of instanceof + */ + private static final Set> ENTITY_TYPES = Set.of( + SloSleTemplate.class, + MetricBound.class + ); + + /** + * Registry of all value object types (embedded types) + * Used for identifying and processing value objects + */ + private static final Set> VALUE_OBJECT_TYPES = Set.of( + SloPolicy.class, + SlePolicy.class, + AvailabilityType.class, + PathConstraints.class + ); + + /** + * Transform an entity to LogicalResourceSpecification (generic method). + * Works with all entity types: Site subclasses, Region, RANSite, PhysicalDevice and other subclasses. + * + * @param Entity type extending LogicalResourceMappable + * @param entity Entity to convert + * @return LogicalResourceSpecification representing the entity + */ + public + LogicalResourceSpecification toLogicalResourceSpec(T entity) { + log.debug("Mapping {} to LogicalResourceSpecification", entity.getEntityTypeName()); + + LogicalResourceSpecification spec = new LogicalResourceSpecification(); + + // Set basic attributes using interface methods + spec.setName(entity.getEntityTypeName() ); + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(entity.getVersion()); + // Use the raw description field value (may be null), not the interface method which provides defaults + + spec.setDescription( entity.getEntityDescription()); + spec.setLifecycleStatus("Active"); + + // Set ID if entity has one + String entityId = entity.getEntityId(); + if (entityId != null) { + spec.setUuid(entityId); + } + + // Convert all fields to characteristics and relationships + convertFieldsToCharacteristicsAndRelationships(entity, spec); + + + return spec; + } + + /** + * Convert all fields of an entity to characteristics and relationships + */ + private void convertFieldsToCharacteristicsAndRelationships(Object entity, LogicalResourceSpecification spec) { + Class clazz = entity.getClass(); + + // Process fields from the entity class and its superclasses + while (clazz != null && !clazz.equals(Object.class)) { + for (Field field : clazz.getDeclaredFields()) { + try { + field.setAccessible(true); + Object value = field.get(entity); + + // Skip static fields, VERSION, and lifecycle fields + // These are metadata that shouldn't be in the specification + if (java.lang.reflect.Modifier.isStatic(field.getModifiers()) + || "VERSION".equals(field.getName()) + || "id".equals(field.getName()) + || "siteId".equals(field.getName()) + || "name".equals(field.getName()) + || "description".equals(field.getName()) + || "createdAt".equals(field.getName()) + || "lastModified".equals(field.getName())) { + continue; + } + + String fieldName = field.getName(); + Class fieldType = field.getType(); + + // Handle entity relationships + if (isEntityType(fieldType)) { + //addRelationship(spec, fieldName, fieldType, value); + addCharacteristic(spec, fieldName, value, fieldType); + } + // Handle collections - pass the field to check generic type + else if (Collection.class.isAssignableFrom(fieldType)) { + handleCollectionField(spec, fieldName, field, value); + } + // Handle value objects (embedded types) + else if (isValueObject(fieldType)) { + addValueObjectCharacteristics(spec, fieldName, value); + } + // Handle simple types as characteristics + else { + addCharacteristic(spec, fieldName, value, fieldType); + } + + } catch (IllegalAccessException e) { + log.warn("Could not access field {} in class {}", field.getName(), clazz.getSimpleName(), e); + } + } + clazz = clazz.getSuperclass(); + } + } + + /** + * Check if a class is an entity type using registry + */ + private boolean isEntityType(Class type) { + return ENTITY_TYPES.stream().anyMatch(t -> t.isAssignableFrom(type)); + } + + /** + * Check if a class is a value object (embedded type) using registry + */ + private boolean isValueObject(Class type) { + return VALUE_OBJECT_TYPES.contains(type); + } + + /** + * Handle collection fields - distinguish between simple types and entities + * - Collections of simple types (List, List, etc.) → ARRAY characteristic + * - Collections of entities (List, etc.) → Multiple relationships + * + * Uses reflection to check the generic type parameter, so it works even for empty collections + */ + private void handleCollectionField(LogicalResourceSpecification spec, String fieldName, Field field, Object value) { + Collection collection = (Collection) value; + + // Determine if collection holds entities by examining generic type parameter + // Returns entity type name (e.g., "EdgeSite") or null if not an entity collection + String entityTypeName = getCollectionEntityType(field); + + if (collection.isEmpty()) { + if (entityTypeName != null) { + // Empty collection of entities → No relationships to add, just log + log.debug("Empty collection of {} entities for field: {} - no relationships to add", + entityTypeName, fieldName); + addArrayCharacteristic(spec, fieldName, "[]"); + } else { + // Empty collection of simple types → Add empty ARRAY characteristic + addArrayCharacteristic(spec, fieldName, "[]"); + log.debug("Added empty ARRAY characteristic for field: {}", fieldName); + } + return; + } + + if (entityTypeName != null) { + // Collection of entities → Create a relationship for each entity + log.debug("Handling collection of {} {} entities for field: {}", + collection.size(), entityTypeName, fieldName); + for (Object element : collection) { + addRelationship(spec, fieldName, element.getClass(), element); + } + } else { + // Collection of simple types (String, Integer, etc.) → Single ARRAY characteristic + log.debug("Handling collection of {} simple types for field: {}", collection.size(), fieldName); + addArrayCharacteristic(spec, fieldName, convertCollectionToString(collection)); + } + } + + /** + * Get the entity type name from a collection field using reflection on generic type + * + * @param field The field to examine + * @return The entity type name (e.g., "EdgeSite", "Datacenter") if it's a collection of entities, + * or null if it's a collection of simple types + */ + private String getCollectionEntityType(Field field) { + try { + // Get the generic type of the field + java.lang.reflect.Type genericType = field.getGenericType(); + + if (genericType instanceof java.lang.reflect.ParameterizedType) { + java.lang.reflect.ParameterizedType paramType = (java.lang.reflect.ParameterizedType) genericType; + java.lang.reflect.Type[] typeArgs = paramType.getActualTypeArguments(); + + if (typeArgs.length > 0 && typeArgs[0] instanceof Class) { + Class elementType = (Class) typeArgs[0]; + + if (isEntityType(elementType)) { + String entityTypeName = elementType.getSimpleName(); + log.debug("Field {} has entity generic type: {}", field.getName(), entityTypeName); + return entityTypeName; + } else { + log.debug("Field {} has simple type generic type: {}", + field.getName(), elementType.getSimpleName()); + return null; + } + } + } + } catch (Exception e) { + log.warn("Could not determine generic type for field {}: {}", field.getName(), e.getMessage()); + } + + // Default to null (treat as simple type collection) + return null; + } + + /** + * Add an ARRAY characteristic for collections of simple types + */ + private void addArrayCharacteristic(LogicalResourceSpecification spec, String fieldName, String arrayValue) { + ResourceSpecificationCharacteristic characteristic = new ResourceSpecificationCharacteristic(); + characteristic.setName(fieldName); + characteristic.setValueType("ARRAY"); + characteristic.setConfigurable(false); + + // Create characteristic value + ResourceSpecificationCharacteristicValue charValue = new ResourceSpecificationCharacteristicValue(); + charValue.setIsDefault(true); + charValue.setValueType("ARRAY"); + + // Set the array value + Any anyValue = new Any(); + anyValue.setValue(arrayValue); + anyValue.setAlias(fieldName); + charValue.setValue(anyValue); + + characteristic.addResourceSpecCharacteristicValueItem(charValue); + spec.addResourceSpecCharacteristicItem(characteristic); + + log.debug("Added ARRAY characteristic: {} = {}", fieldName, arrayValue); + } + + /** + * Convert collection to string representation (JSON-like format) + */ + private String convertCollectionToString(Collection collection) { + if (collection == null || collection.isEmpty()) { + return "[]"; + } + + StringBuilder sb = new StringBuilder("["); + boolean first = true; + for (Object item : collection) { + if (!first) { + sb.append(", "); + } + if (item instanceof String) { + sb.append("\"").append(item).append("\""); + } else { + sb.append(item); + } + first = false; + } + sb.append("]"); + return sb.toString(); + } + + /** + * Add a characteristic for a simple field + */ + private void addCharacteristic(LogicalResourceSpecification spec, String fieldName, Object value, Class type) { + ResourceSpecificationCharacteristic characteristic = new ResourceSpecificationCharacteristic(); + characteristic.setName(fieldName); + characteristic.setValueType(getValueType(type)); + characteristic.setConfigurable(false); + + // Create characteristic value + ResourceSpecificationCharacteristicValue charValue = new ResourceSpecificationCharacteristicValue(); + charValue.setIsDefault(true); + charValue.setValueType(getValueType(type)); + + // Set the value + Any anyValue = new Any(); + anyValue.setValue(convertValueToString(value)); + anyValue.setAlias(fieldName); + charValue.setValue(anyValue); + + characteristic.addResourceSpecCharacteristicValueItem(charValue); + spec.addResourceSpecCharacteristicItem(characteristic); + + log.debug("Added characteristic: {} = {}", fieldName, value); + } + + /** + * Add characteristics for value object fields + */ + private void addValueObjectCharacteristics(LogicalResourceSpecification spec, String fieldName, Object valueObject) { + Class voClass = valueObject.getClass(); + + for (Field voField : voClass.getDeclaredFields()) { + try { + voField.setAccessible(true); + Object voValue = voField.get(valueObject); + + if (voValue != null && !java.lang.reflect.Modifier.isStatic(voField.getModifiers())) { + // Use nested naming: e.g., "coordinates.lat", "coordinates.lng" + String nestedFieldName = fieldName + "." + voField.getName(); + addCharacteristic(spec, nestedFieldName, voValue, voField.getType()); + } + } catch (IllegalAccessException e) { + log.warn("Could not access field {} in value object {}", voField.getName(), voClass.getSimpleName(), e); + } + } + } + + + + /** + * Add a relationship for an entity field + */ + private void addRelationship(LogicalResourceSpecification spec, String fieldName, Class relatedClass, Object relatedEntity) { + ResourceSpecificationRelationship relationship = new ResourceSpecificationRelationship(); + + // Get the ID of the related entity + String relatedId = extractEntityId(relatedEntity); + relationship.setId(relatedId); + relationship.setName(relatedClass.getSimpleName()); + relationship.setRelationshipType(RELATIONSHIP_TYPE_DEPENDENCY); + relationship.setRole(fieldName); + + // Set href if we have an ID + if (relatedId != null) { + relationship.setHref("/api/topology/" + relatedClass.getSimpleName().toLowerCase() + "s/" + relatedId); + } + + spec.addResourceSpecRelationshipItem(relationship); + + log.debug("Added relationship: {} -> {} (id: {})", fieldName, relatedClass.getSimpleName(), relatedId); + } + + /** + * Extract ID from an entity using reflection + */ + private String extractEntityId(Object entity) { + try { + Field idField = findIdField(entity.getClass()); + if (idField != null) { + idField.setAccessible(true); + Object id = idField.get(entity); + return id != null ? id.toString() : null; + } + } catch (IllegalAccessException e) { + log.warn("Could not extract ID from entity {}", entity.getClass().getSimpleName(), e); + } + return null; + } + + /** + * Find the ID field in a class hierarchy + */ + private Field findIdField(Class clazz) { + while (clazz != null && !clazz.equals(Object.class)) { + for (Field field : clazz.getDeclaredFields()) { + if ("id".equals(field.getName())) { + return field; + } + } + clazz = clazz.getSuperclass(); + } + return null; + } + + + /** + * Get TMF value type from Java type + */ + private String getValueType(Class type) { + if (type.equals(String.class)) { + return "TEXT"; + } else if (type.equals(Integer.class) || type.equals(int.class) || + type.equals(Long.class) || type.equals(long.class)) { + return "NUMBER"; + } else if (type.equals(Boolean.class) || type.equals(boolean.class)) { + return "BOOLEAN"; + } else if (type.equals(Double.class) || type.equals(double.class) || + type.equals(Float.class) || type.equals(float.class)) { + return "NUMBER"; + } else if (type.isEnum()) { + return "TEXT"; + } else if (LocalDateTime.class.isAssignableFrom(type)) { + return "DATETIME"; + } else if (Collection.class.isAssignableFrom(type)) { + return "ARRAY"; + } else { + return "TEXT"; + } + } + + /** + * Convert value to string representation + */ + private String convertValueToString(Object value) { + if (value == null) { + return ""; + } + + if (value instanceof Collection) { + // Convert collection to JSON-like string + Collection collection = (Collection) value; + return collection.toString(); + } else if (value instanceof LocalDateTime) { + return value.toString(); + } else if (value.getClass().isEnum()) { + return ((Enum) value).name(); + } else { + return value.toString(); + } + } + +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/LogicalResourceToEntityMapper.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/LogicalResourceToEntityMapper.java new file mode 100644 index 0000000..d5ee3d0 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/LogicalResourceToEntityMapper.java @@ -0,0 +1,521 @@ +package org.etsi.osl.controllers.ietf.ns.mappers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.etsi.osl.controllers.ietf.ns.api.CategoryConfigurationService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; +import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceMappable; +import org.etsi.osl.controllers.ietf.ns.domain.common.RelatedManagedResourceReference; +import org.etsi.osl.controllers.ietf.ns.repository.TMFResourceInventoryRepository; +import org.etsi.osl.tmf.common.model.Any; +import org.etsi.osl.tmf.ri639.model.Characteristic; +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; + +/** + * Mapper utility to transform TMF639 LogicalResource instances back to domain entities. + * + * Strategy: + * - Use registry pattern for entity type dispatch (replaces switch statement) + * - Generic method using LogicalResourceMappable interface for field mapping + * - Extract characteristics and map to entity fields via field mappings + * - Handle value objects (Coordinates, Capacity, Contact) + * - Map TMF resource states back to SiteStatus/RegionStatus + * - Use category from CategoryConfigurationService for consistent category format + * + * Changes from legacy version: + * - Removed 11 individual converter methods (toDatacenter, toEdgeSite, etc.) + * - Replaced with single generic fromLogicalResource(LogicalResource, Class) method + * - Uses entity's getFieldMappings() interface method for field-to-characteristic mappings + * - Registry pattern replaces 18-case switch statement + * - Code reduction: ~791 lines → ~520 lines (34% reduction) + */ +@Component +@Slf4j +public class LogicalResourceToEntityMapper { + + @Autowired + private CategoryConfigurationService categoryConfig; + + @Autowired(required = false) + private TMFResourceInventoryRepository tmfInventoryRepository; + + // Registry mapping entity type names to their classes + private static final Map> ENTITY_REGISTRY = + Map.ofEntries( + Map.entry("SloSleTemplate", SloSleTemplate.class) + ); + + /** + * Convert LogicalResource to appropriate entity type based on category. + * Uses registry to dispatch to correct entity class. + * + * @param resource LogicalResource to convert + * @return Entity instance of the appropriate type (Site, Region, or PhysicalDevice) + * @throws IllegalArgumentException if category is null or unknown entity type + */ + public Object fromLogicalResource(LogicalResource resource) { + String category = resource.getCategory(); + if (category == null) { + throw new IllegalArgumentException("LogicalResource category is null"); + } + + String entityType = extractEntityType(category); + log.debug("Mapping LogicalResource to {} entity", entityType); + + Class entityClass = ENTITY_REGISTRY.get(entityType); + if (entityClass == null) { + throw new IllegalArgumentException("Unknown entity type: " + entityType); + } + + return fromLogicalResource(resource, entityClass); + } + + /** + * Generic conversion method for any LogicalResourceMappable entity. + * Replaces 11 individual converter methods (toDatacenter, toEdgeSite, toComputeDevice, etc.) + * + * @param Entity type extending LogicalResourceMappable + * @param resource LogicalResource to convert + * @param entityClass Target entity class to instantiate + * @return Entity instance with all fields mapped from characteristics + */ + public T fromLogicalResource(LogicalResource resource, Class entityClass) { + try { + T entity = entityClass.getDeclaredConstructor().newInstance(); + log.debug("Mapping LogicalResource to {}", entityClass.getSimpleName()); + + // Get characteristics map for field lookups + Map charMap = getCharacteristicsMap(resource); + + // Let entity populate its own basic fields from LogicalResource + entity.mapFromLogicalResource(resource, charMap); + + // Apply field mappings from entity's getFieldMappings() + applyFieldMappings(entity, charMap); + + // Handle relationships if entity declares them + if (entity.hasRelationships()) { + applyRelationships(entity, resource, charMap); + } + // Handle related managed resources for any entity implementing RelatedManagedResource + if (entity instanceof RelatedManagedResourceReference relatedResource) { + // Extract relatedManagedResourceId from ResourceRelationship + String relatedResourceId = TransformationUtils.extractRelationshipId(resource, "relatedManagedResource"); + if (relatedResourceId != null) { + relatedResource.setRelatedManagedResourceId(relatedResourceId); + } + // Fetch and populate relatedManagedResource from inventory + setRelatedManagedResource(entity, relatedResource); + } + + log.debug("Successfully mapped {} to {}", resource.getUuid(), entityClass.getSimpleName()); + return entity; + + } catch (Exception e) { + throw new RuntimeException("Failed to convert LogicalResource to " + entityClass.getSimpleName() + ": " + e.getMessage(), e); + } + } + + + + + /** + * Apply field mappings declared in entity's getFieldMappings() method + */ + private void applyFieldMappings(T entity, Map charMap) { + if (!charMap.isEmpty()) { + for (Map.Entry fieldMapping : entity.getFieldMappings().entrySet()) { + String charName = fieldMapping.getKey(); + String fieldName = fieldMapping.getValue(); + String charValue = charMap.get(charName); + + if (charValue != null && !charValue.isEmpty()) { + try { + applyFieldValue(entity, fieldName, charValue); + } catch (Exception e) { + log.warn("Could not apply field mapping {} = {}: {}", fieldName, charValue, e.getMessage()); + } + } + } + } + } + + /** + * Apply a characteristic value to an entity field using reflection + */ + @SuppressWarnings("unchecked") + private void applyFieldValue(T entity, String fieldName, String charValue) { + try { + // Use helper method to find field in class hierarchy + java.lang.reflect.Field field = findFieldInHierarchy(entity.getClass(), fieldName); + if (field == null) { + log.trace("Field {} not found in class hierarchy of {}", fieldName, entity.getClass().getSimpleName()); + return; + } + field.setAccessible(true); + Class fieldType = field.getType(); + + Object value = null; + if (fieldType == String.class) { + value = charValue; + } else if (fieldType == Integer.class) { + value = Integer.parseInt(charValue); + } else if (fieldType == Double.class) { + value = Double.parseDouble(charValue); + } else if (fieldType == Boolean.class) { + value = Boolean.parseBoolean(charValue); + } else if (fieldType == List.class) { + value = parseArrayCharacteristic(charValue); + } else if (fieldType.isEnum()) { + try { + value = Enum.valueOf((Class) fieldType, charValue); + } catch (IllegalArgumentException e) { + log.warn("Could not parse enum value {} for field {}", charValue, fieldName); + } + } + + if (value != null) { + field.set(entity, value); + } + } catch (IllegalArgumentException | IllegalAccessException e) { + log.warn("Error setting field {} on {}: {}", fieldName, entity.getClass().getSimpleName(), e.getMessage()); + } + } + + /** + * Apply relationships to entity based on getRelationshipFields() + */ + private void applyRelationships(T entity, LogicalResource resource, Map charMap) { + for (String relationshipField : entity.getRelationshipFields()) { + try { + applyRelationshipField(entity, relationshipField, resource); + } catch (Exception e) { + log.warn("Could not apply relationship field {}: {}", relationshipField, e.getMessage()); + } + } + } + + /** + * Apply a single relationship field value + */ + private void applyRelationshipField(T entity, String relationshipField, LogicalResource resource) { + try { + // Use helper method to find field in class hierarchy + java.lang.reflect.Field field = findFieldInHierarchy(entity.getClass(), relationshipField); + if (field == null) { + log.trace("Relationship field {} not found in class hierarchy of {}", relationshipField, entity.getClass().getSimpleName()); + return; + } + field.setAccessible(true); + + // Check if this is a collection relationship (List) or single entity relationship + if (java.util.Collection.class.isAssignableFrom(field.getType())) { + // Handle collection relationships (e.g., physicalDevices) + List devices = extractCollectionRelationships(resource, field.getType()); + if (!devices.isEmpty()) { + field.set(entity, devices); + log.debug("Set relationship collection {} with {} items", relationshipField, devices.size()); + } + } else { + // Handle single entity relationships (e.g., datacenter, region) + String relationshipId = TransformationUtils.extractRelationshipId(resource, relationshipField); + if (relationshipId != null) { + Object refEntity = instantiateRelatedEntity(field.getType(), relationshipId); + if (refEntity != null) { + field.set(entity, refEntity); + log.debug("Set relationship {} to {}", relationshipField, relationshipId); + } else { + log.warn("Could not instantiate related entity for field {} with id {}", relationshipField, relationshipId); + } + } else { + log.trace("No relationship found for field {}", relationshipField); + } + } + } catch (IllegalArgumentException | IllegalAccessException e) { + log.warn("Error setting relationship field {}: {}", relationshipField, e.getMessage()); + } + } + + /** + * Find a field in the class hierarchy (searches superclasses) + */ + private java.lang.reflect.Field findFieldInHierarchy(Class clazz, String fieldName) { + Class currentClass = clazz; + while (currentClass != null) { + try { + return currentClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + currentClass = currentClass.getSuperclass(); + } + } + return null; + } + + /** + * Instantiate a related entity with just the ID set + */ + private Object instantiateRelatedEntity(Class entityClass, String id) { + try { + Object instance = entityClass.getDeclaredConstructor().newInstance(); + // Find id field in class hierarchy + java.lang.reflect.Field idField = findFieldInHierarchy(entityClass, "id"); + if (idField != null) { + idField.setAccessible(true); + idField.set(instance, id); + } + return instance; + } catch (Exception e) { + log.warn("Could not instantiate {} with id {}", entityClass.getSimpleName(), id); + return null; + } + } + + /** + * Extract entity type from category string + */ + private String extractEntityType(String category) { + String prefix = categoryConfig.getCategoryPrefix(); + if (category.startsWith(prefix + "/")) { + return category.substring(prefix.length() + 1); + } + throw new IllegalArgumentException("Invalid category format: " + category + " (expected prefix: " + prefix + ")"); + } + + /** + * Convert characteristic list to Map for easy field lookups + */ + private Map getCharacteristicsMap(LogicalResource resource) { + Map charMap = new HashMap<>(); + + if (resource.getResourceCharacteristic() != null) { + for (Characteristic characteristic : resource.getResourceCharacteristic()) { + String name = characteristic.getName(); + String value = extractCharacteristicValue(characteristic); + if (name != null && value != null) { + charMap.put(name, value); + } + } + } + + return charMap; + } + + /** + * Extract the actual value from a Characteristic object + */ + private String extractCharacteristicValue(Characteristic characteristic) { + if (characteristic == null) { + return null; + } + + if (characteristic.getValue() != null) { + Object value = characteristic.getValue(); + + // Handle TMF Any object (has getValue() method) + if (value instanceof Any) { + Any anyValue = (Any) value; + if (anyValue.getValue() != null) { + return String.valueOf(anyValue.getValue()); + } + return null; + } + + if (value instanceof Map) { + Map valueMap = (Map) value; + if (valueMap.containsKey("value")) { + return String.valueOf(valueMap.get("value")); + } + // If it's a map but no "value" key, return toString representation + return valueMap.toString(); + } + + return String.valueOf(value); + } + + return characteristic.getValueType(); + } + + /** + * Extract object from nested characteristics (e.g., capacity, coordinates, contact) + */ + private Map extractValueObject(Map charMap, String prefix) { + Map result = new HashMap<>(); + String prefixWithDot = prefix + "."; + + for (Map.Entry entry : charMap.entrySet()) { + if (entry.getKey().startsWith(prefixWithDot)) { + String fieldName = entry.getKey().substring(prefixWithDot.length()); + result.put(fieldName, entry.getValue()); + } + } + + return result.isEmpty() ? null : result; + } + + /** + * Parse array characteristic (e.g., "[\"item1\", \"item2\"]") + */ + private List parseArrayCharacteristic(String arrayStr) { + if (arrayStr == null || arrayStr.isEmpty()) { + return new ArrayList<>(); + } + + // Simple parsing for array strings like ["item1", "item2"] + arrayStr = arrayStr.trim(); + if (arrayStr.startsWith("[") && arrayStr.endsWith("]")) { + arrayStr = arrayStr.substring(1, arrayStr.length() - 1); + if (arrayStr.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + String[] items = arrayStr.split(","); + for (String item : items) { + String trimmed = item.trim(); + // Remove quotes if present + if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) { + trimmed = trimmed.substring(1, trimmed.length() - 1); + } + if (!trimmed.isEmpty()) { + result.add(trimmed); + } + } + return result; + } + + return new ArrayList<>(); + } + + /** + * Get Integer value from characteristic map + */ + private Integer getIntegerValue(Map charMap, String fieldName) { + String value = charMap.get(fieldName); + if (value != null && !value.isEmpty()) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + log.warn("Could not parse integer value for {}: {}", fieldName, value); + } + } + return null; + } + + /** + * Get Long value from characteristic map + */ + private Long getLongValue(Map charMap, String fieldName) { + String value = charMap.get(fieldName); + if (value != null && !value.isEmpty()) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + log.warn("Could not parse long value for {}: {}", fieldName, value); + } + } + return null; + } + + /** + * Get Double value from characteristic map + */ + private Double getDoubleValue(Map charMap, String fieldName) { + String value = charMap.get(fieldName); + if (value != null && !value.isEmpty()) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + log.warn("Could not parse double value for {}: {}", fieldName, value); + } + } + return null; + } + + /** + * Get Boolean value from characteristic map + */ + private Boolean getBooleanValue(Map charMap, String fieldName) { + String value = charMap.get(fieldName); + if (value != null && !value.isEmpty()) { + return Boolean.parseBoolean(value); + } + return null; + } + + + + /** + * Extract all related entities from collection relationships + * Used for collection references (e.g., Site.physicalDevices) + * + * @param resource LogicalResource containing relationships + * @param collectionType The collection field type (not used but kept for API consistency) + * @return List of instantiated related entities + */ + private List extractCollectionRelationships(LogicalResource resource, Class collectionType) { + List result = new ArrayList<>(); + + if (resource.getResourceRelationship() == null || resource.getResourceRelationship().isEmpty()) { + return result; + } + + for (org.etsi.osl.tmf.ri639.model.ResourceRelationship rel : resource.getResourceRelationship()) { + if (rel.getResource() != null && rel.getResource().getReferredType() != null) { + String referredType = rel.getResource().getReferredType(); + String id = rel.getResource().getId(); + + // Map referredType to entity class + Class entityClass = ENTITY_REGISTRY.get(referredType); + if (entityClass != null) { + Object entity = instantiateRelatedEntity(entityClass, id); + if (entity != null) { + result.add(entity); + log.debug("Added {} with id {} to collection", referredType, id); + } + } else { + log.warn("Unknown referred type: {}", referredType); + } + } + } + return result; + } + + /** + * Set the relatedManagedResource on a RelatedManagedResourceReference by fetching it from the Resource Inventory + * Uses the relatedManagedResourceId to look up the actual LogicalResource object + * Public method for entity access during reverse mapping + * + * @param relatedManagedResourceReference The RelatedManagedResourceReference to populate + */ + + + + private void setRelatedManagedResource(T entity, + RelatedManagedResourceReference relatedManagedResourceReference) { + if (relatedManagedResourceReference == null || relatedManagedResourceReference.getRelatedManagedResourceId() == null) { + return; + } + + if (tmfInventoryRepository == null) { + log.warn("TMFResourceInventoryRepository not available, cannot fetch related managed resource"); + return; + } + + try { + java.util.Optional relatedResource = tmfInventoryRepository.findById(relatedManagedResourceReference.getRelatedManagedResourceId()); + if (relatedResource.isPresent()) { + relatedManagedResourceReference.setRelatedManagedResource(relatedResource.get()); + log.debug("Set relatedManagedResource for device {}: {}", entity.getEntityId(), relatedManagedResourceReference.getRelatedManagedResourceId()); + } else { + log.warn("Related managed resource not found: {}", relatedManagedResourceReference.getRelatedManagedResourceId()); + } + } catch (Exception e) { + log.error("Error fetching related managed resource for device {}: {}", entity.getEntityId(), e.getMessage()); + } + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/TransformationUtils.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/TransformationUtils.java new file mode 100644 index 0000000..c58b276 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/TransformationUtils.java @@ -0,0 +1,33 @@ +package org.etsi.osl.controllers.ietf.ns.mappers; + +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TransformationUtils { + + /** + * Extract a single entity ID from ResourceRelationship by field name + * Used for single entity references (e.g., Site.region, EdgeSite.datacenter) + * Public method for entity access during reverse mapping + * + * @param resource LogicalResource containing relationships + * @param fieldName The field name to match (e.g., "region", "datacenter") + * @return The ID of the related entity, or null if not found + */ + public static String extractRelationshipId(LogicalResource resource, String fieldName) { + if (resource.getResourceRelationship() == null || resource.getResourceRelationship().isEmpty()) { + return null; + } + + for (org.etsi.osl.tmf.ri639.model.ResourceRelationship rel : resource.getResourceRelationship()) { + if (rel.getResource() != null && fieldName.equals(rel.getResource().getName())) { + String id = rel.getResource().getId(); + String referredType = rel.getResource().getReferredType(); + log.debug("Found relationship: field={}, referredType={}, id={}", fieldName, referredType, id); + return id; + } + } + return null; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/SloSleTemplateRepository.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/SloSleTemplateRepository.java new file mode 100644 index 0000000..b5b3ab4 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/SloSleTemplateRepository.java @@ -0,0 +1,93 @@ +package org.etsi.osl.controllers.ietf.ns.repository; + +import java.util.List; +import java.util.Optional; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; + +/** + * Repository interface for SloSleTemplate persistence and retrieval. + * + * This repository provides CRUD operations and query methods for managing + * SLO/SLE templates. Templates are the reusable service level definitions + * that can be referenced by network slice services. + * + * The repository abstracts the storage mechanism (database, cache, API) + * and provides a consistent interface for template management. + */ +public interface SloSleTemplateRepository { + + /** + * Creates a new SLO/SLE template. + * + * @param template The template to create + * @return The created template with any generated fields populated + * @throws IllegalArgumentException if the template ID already exists + */ + SloSleTemplate create(SloSleTemplate template); + + /** + * Retrieves a template by its ID. + * + * @param id The template ID + * @return An Optional containing the template if found, empty otherwise + */ + Optional findById(String id); + + /** + * Retrieves all templates. + * + * @return List of all templates, empty if none exist + */ + List findAll(); + + /** + * Finds templates by description pattern. + * Useful for searching templates by partial description. + * + * @param descriptionPattern Pattern to match (SQL LIKE or regex depending on implementation) + * @return List of matching templates + */ + List findByDescriptionContaining(String descriptionPattern); + + /** + * Updates an existing template. + * + * @param id The ID of the template to update + * @param template The updated template data + * @return The updated template + * @throws java.util.NoSuchElementException if the template does not exist + */ + SloSleTemplate update(String id, SloSleTemplate template); + + /** + * Deletes a template by its ID. + * + * @param id The ID of the template to delete + * @return true if the template was deleted, false if it didn't exist + */ + boolean delete(String id); + + /** + * Checks if a template with the given ID exists. + * + * @param id The template ID + * @return true if the template exists, false otherwise + */ + boolean exists(String id); + + /** + * Gets the count of all templates. + * + * @return The number of templates + */ + long count(); + + /** + * Retrieves templates that reference another template. + * Useful for finding derived templates or template compositions. + * + * @param referencedTemplateId The ID of the referenced template + * @return List of templates that reference the given template + */ + List findByTemplateRef(String referencedTemplateId); +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/TMFResourceInventoryRepository.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/TMFResourceInventoryRepository.java new file mode 100644 index 0000000..5b240a8 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/TMFResourceInventoryRepository.java @@ -0,0 +1,77 @@ +package org.etsi.osl.controllers.ietf.ns.repository; + +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * Repository interface for TMF639 LogicalResource entities + * Handles persistence of resource instances via Camel/ActiveMQ to external TMF Resource Inventory service + */ +@Repository +public interface TMFResourceInventoryRepository { + + /** + * Find all logical resources + * @return list of all logical resources + */ + List findAll(); + + /** + * Find logical resources by category + * @param category resource category (e.g., "ns.ietf.controllers.osl.etsi.org/0.0.1/categ") + * @return list of logical resources in the category + */ + List findByCategory(String category); + + /** + * Find logical resource by ID + * @param id resource ID + * @return optional logical resource + */ + Optional findById(String id); + + /** + * Save logical resource instance + * @param resource resource instance to save + * @return saved resource instance + */ + LogicalResource save(LogicalResource resource); + + /** + * Update logical resource instance + * @param id resource ID + * @param resource updated resource data + * @return updated resource instance + */ + LogicalResource update(String id, LogicalResource resource); + + /** + * Create or update logical resource instance + * @param resource resource to create or update + * @return created or updated resource instance + */ + LogicalResource createOrUpdate(LogicalResource resource); + + /** + * Delete logical resource by ID + * @param id resource ID + */ + void deleteById(String id); + + /** + * Check if logical resource exists + * @param id resource ID + * @return true if exists + */ + boolean existsById(String id); + + /** + * Search logical resources by name or category + * @param text tosearch resource (e.g., "Datacenter" or "%Datacenter") + * @return list of logical resources in the category + */ + List searchByText(String text); +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/TMFResourceSpecRepository.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/TMFResourceSpecRepository.java new file mode 100644 index 0000000..47ca4a0 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/TMFResourceSpecRepository.java @@ -0,0 +1,56 @@ +package org.etsi.osl.controllers.ietf.ns.repository; + +import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * Repository interface for TMF LogicalResourceSpecification entities + * Handles persistence via Camel/ActiveMQ to external TMF service + */ +@Repository +public interface TMFResourceSpecRepository { + + /** + * Find all resource specifications + * @return list of all resource specifications + */ + List findAll(); + + /** + * Find resource specification by ID + * @param id resource spec ID + * @return optional resource specification + */ + Optional findById(String id); + + /** + * Save resource specification + * @param resourceSpec resource spec to save + * @return saved resource specification + */ + LogicalResourceSpecification save(LogicalResourceSpecification resourceSpec); + + /** + * Update resource specification + * @param id resource spec ID + * @param resourceSpec updated resource spec data + * @return updated resource specification + */ + LogicalResourceSpecification update(String id, LogicalResourceSpecification resourceSpec); + + /** + * Delete resource specification by ID + * @param id resource spec ID + */ + void deleteById(String id); + + /** + * Check if resource specification exists + * @param id resource spec ID + * @return true if exists + */ + boolean existsById(String id); +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java new file mode 100644 index 0000000..f98a840 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java @@ -0,0 +1,217 @@ +package org.etsi.osl.controllers.ietf.ns.repository.impl; + +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.etsi.osl.controllers.ietf.ns.api.ResourceSpecificationTemplateRegistry; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.etsi.osl.controllers.ietf.ns.api.restconf.RestconfConsumerService; +import org.etsi.osl.tmf.common.model.EValueType; +import org.etsi.osl.tmf.ri639.model.Characteristic; +import org.etsi.osl.tmf.ri639.model.Resource; +import org.etsi.osl.tmf.ri639.model.ResourceCreate; +import org.etsi.osl.tmf.ri639.model.ResourceStatusType; +import org.etsi.osl.tmf.ri639.model.ResourceUpdate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ResourceRepoService { + + private static final Logger logger = LoggerFactory.getLogger("org.etsi.osl.example.gc"); + + @Autowired + private TMFResourceSpecRepositoryImpl tmfRepository; + + @Autowired + private ResourceSpecificationTemplateRegistry templateRegistry; + + @Autowired + SimpleResourceMapper simpleResourceMapper; + + @Autowired(required = false) + private RestconfConsumerService restconfConsumerService; + + public Resource createResource( Map headers, ResourceCreate resourceRequested) { + + String resourceid = ""; + + if ( headers.get("org.etsi.osl.serviceId") !=null ) { + + } + if ( headers.get("org.etsi.osl.resourceId") !=null ) { //the resource to update back + resourceid = (String) headers.get("org.etsi.osl.resourceId") ; + } + if ( headers.get("org.etsi.osl.serviceOrderId") !=null ) { + + } + + ResourceUpdate resourceUpdate = simpleResourceMapper.resourceCreateToResourceUpdate(resourceRequested); + + if ( templateRegistry.getTemplateId (resourceRequested.getResourceSpecification().getName() ).isPresent() ) { + resourceUpdate = applySliceRequest(resourceUpdate, resourceid); + } else { + return null; + } + + //send it to TMF API + Resource res = tmfRepository.updateResourceById( resourceid, resourceUpdate); + + return res; + } + + private ResourceUpdate applySliceRequest(ResourceUpdate resourceUpdate, String resourceid) { + + String jsonRequest = ""; + + // Extract jsonRequest from resource characteristics + for (Characteristic c : resourceUpdate.getResourceCharacteristic()) { + if (c.getName().equalsIgnoreCase("jsonRequest")) { + jsonRequest = c.getValue().getValue(); + } + } + + String infoMessage = "Created"; + String healthStatus = "Healthy"; + ResourceStatusType resourceStatus = ResourceStatusType.AVAILABLE; + + // Parse and send slice request via RESTCONF if consumer service is available + if (jsonRequest != null && !jsonRequest.isEmpty()) { + try { + logger.info("Parsing slice request JSON for resource: {}", resourceid); + + // Parse JSON string to NetworkSliceServices object (RFC 9543 format with slice-service array) + ObjectMapper mapper = new ObjectMapper(); + // Configure mapper to ignore unknown properties (for RFC 9543 field name mappings) + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + // Parse the RFC 9543 format: { "slice-service": [ {...} ] } + com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonRequest); + com.fasterxml.jackson.databind.JsonNode sliceServiceNode = rootNode.get("slice-service"); + + if (sliceServiceNode == null || !sliceServiceNode.isArray()) { + throw new IllegalArgumentException("Expected 'slice-service' array in JSON request"); + } + + // Parse array of SliceService objects + java.util.List sliceServices = new java.util.ArrayList<>(); + for (com.fasterxml.jackson.databind.JsonNode serviceNode : sliceServiceNode) { + SliceService service = mapper.treeToValue(serviceNode, SliceService.class); + sliceServices.add(service); + } + + logger.info("Successfully parsed {} SliceService(s)", sliceServices.size()); + + // Send to RESTCONF provider if consumer service is available + if (restconfConsumerService != null) { + logger.info("Sending {} slice service request(s) to RESTCONF provider", sliceServices.size()); + + // Provision the slice services via RESTCONF (sends as array) + java.util.List provisionedServices = restconfConsumerService.provisionSliceServices(sliceServices); + + if (provisionedServices != null && !provisionedServices.isEmpty()) { + logger.info("Successfully provisioned {} slice service(s) via RESTCONF", provisionedServices.size()); + StringBuilder serviceIds = new StringBuilder(); + for (SliceService svc : provisionedServices) { + if (serviceIds.length() > 0) serviceIds.append(", "); + serviceIds.append(svc.getId()); + } + infoMessage = "Successfully created and provisioned via RESTCONF: " + serviceIds.toString(); + healthStatus = "Healthy"; + resourceStatus = ResourceStatusType.AVAILABLE; + } else { + logger.warn("Failed to provision slice services via RESTCONF - received null or empty response"); + infoMessage = "Failed to provision via RESTCONF: null or empty response"; + healthStatus = "Degraded"; + resourceStatus = ResourceStatusType.SUSPENDED; + } + } else { + logger.warn("RESTCONF consumer service not available - slice services not sent to provider"); + infoMessage = "Parsed successfully but RESTCONF consumer not available"; + healthStatus = "Degraded"; + resourceStatus = ResourceStatusType.SUSPENDED; + } + + } catch (IllegalArgumentException e) { + logger.error("Invalid slice service JSON format: {}", e.getMessage()); + infoMessage = "Failed: Invalid JSON format - " + e.getMessage(); + healthStatus = "Unhealthy"; + resourceStatus = ResourceStatusType.SUSPENDED; + } catch (Exception e) { + logger.error("Error processing slice request", e); + infoMessage = "Failed: " + e.getMessage(); + healthStatus = "Unhealthy"; + resourceStatus = ResourceStatusType.SUSPENDED; + } + } else { + logger.warn("Empty jsonRequest for resource: {}", resourceid); + infoMessage = "Failed: Empty or missing jsonRequest"; + healthStatus = "Unhealthy"; + resourceStatus = ResourceStatusType.SUSPENDED; + } + + // Update resource with status + resourceUpdate.addResourceCharacteristicItemShort("status.infoMessage", infoMessage + " [" + new Date() + "]", EValueType.TEXT.getValue()); + resourceUpdate.addResourceCharacteristicItemShort("status.Health", healthStatus, EValueType.TEXT.getValue()); + resourceUpdate.addResourceCharacteristicItemShort("status.jsonRequest", jsonRequest, EValueType.TEXT.getValue()); + resourceUpdate.addResourceCharacteristicItemShort("status.UUID", UUID.randomUUID().toString(), EValueType.TEXT.getValue()); + + resourceUpdate.setResourceStatus(resourceStatus); + return resourceUpdate; + } + + public Resource updateResource( Map headers, ResourceUpdate r) { + String resourceid = ""; + + if ( headers.get("org.etsi.osl.serviceId") !=null ) { + + } + if ( headers.get("org.etsi.osl.resourceId") !=null ) { //the resource to update back + resourceid = (String) headers.get("org.etsi.osl.resourceId") ; + } + if ( headers.get("org.etsi.osl.serviceOrderId") !=null ) { + + } + + ResourceUpdate resourceUpdate = r; + + resourceUpdate.addResourceCharacteristicItemShort("status.infoMessage", "Updated " + new Date() , EValueType.TEXT.getValue()); + + + Resource res = tmfRepository.updateResourceById( resourceid, resourceUpdate); + + return res; + } + + public Resource deleteResource( Map headers, ResourceUpdate r) { + String resourceid = ""; + + if ( headers.get("org.etsi.osl.serviceId") !=null ) { + + } + if ( headers.get("org.etsi.osl.resourceId") !=null ) { //the resource to update back + resourceid = (String) headers.get("org.etsi.osl.resourceId") ; + } + if ( headers.get("org.etsi.osl.serviceOrderId") !=null ) { + + } + + + ResourceUpdate resourceUpdate = r; + resourceUpdate.setResourceStatus(ResourceStatusType.UNKNOWN); + + resourceUpdate.addResourceCharacteristicItemShort("status.Health", "deleted", EValueType.TEXT.getValue()); + + Resource res = tmfRepository.updateResourceById( resourceid, resourceUpdate); + + return res; + } + + + + + +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/SimpleResourceMapper.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/SimpleResourceMapper.java new file mode 100644 index 0000000..6a9f4a0 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/SimpleResourceMapper.java @@ -0,0 +1,14 @@ +package org.etsi.osl.controllers.ietf.ns.repository.impl; + +import org.etsi.osl.tmf.ri639.model.Resource; +import org.etsi.osl.tmf.ri639.model.ResourceCreate; +import org.etsi.osl.tmf.ri639.model.ResourceUpdate; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface SimpleResourceMapper { + + ResourceUpdate resourceToResourceUpdate(Resource source); + ResourceUpdate resourceCreateToResourceUpdate(ResourceCreate source); + +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/TMFResourceInventoryRepositoryImpl.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/TMFResourceInventoryRepositoryImpl.java new file mode 100644 index 0000000..c1c455e --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/TMFResourceInventoryRepositoryImpl.java @@ -0,0 +1,268 @@ +package org.etsi.osl.controllers.ietf.ns.repository.impl; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.camel.ProducerTemplate; +import org.etsi.osl.controllers.ietf.ns.api.CategoryConfigurationService; +import org.etsi.osl.controllers.ietf.ns.repository.TMFResourceInventoryRepository; +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Repository; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Implementation of TMFResourceInventoryRepository using Apache Camel and ActiveMQ + * Communicates with external TMF Resource Inventory Management service (TMF639) + */ +@Repository +@Slf4j +public class TMFResourceInventoryRepositoryImpl implements TMFResourceInventoryRepository { + + @Autowired(required = false) + private ProducerTemplate producerTemplate; + + @Value("${CATALOG_GET_RESOURCE_BY_ID}") + private String endpointGetById; + + @Value("${CATALOG_ADD_RESOURCE}") + private String endpointAdd; + + @Value("${CATALOG_UPD_RESOURCE}") + private String endpointUpdate; + + @Value("${CATALOG_UPDADD_RESOURCE}") + private String endpointCreateOrUpdate; + + @Value("${CATALOG_GET_RESOURCES_BY_CATEGORY}") + private String endpointGetByCategory; + + + @Value("${CATALOG_SEARCH_RESOURCES}") + private String endpointSearchResources; + + @Autowired + private CategoryConfigurationService categoryConfig; + + // Known entity types for findAll() + private static final String[] ENTITY_TYPES = { + "SloSleTemplate" + }; + + @Override + public List findAll() { + log.info("Finding all logical resources across all categories"); + + List allResources = new ArrayList<>(); + + for (String entityType : ENTITY_TYPES) { + String category = categoryConfig.getCategoryForSpecifications() + "/" + entityType; + List resources = findByCategory(category); + allResources.addAll(resources); + } + + log.info("Retrieved {} total logical resources from TMF Resource Inventory", allResources.size()); + return allResources; + } + + @Override + public List findByCategory(String category) { + log.info("Finding logical resources by category: {}", category); + + if (producerTemplate != null) { + try { + Object response = producerTemplate.requestBodyAndHeader(endpointGetByCategory, "", "category", category); + + if (response instanceof String) { + List resources = toJsonList((String) response, LogicalResource.class); + log.info("Retrieved {} logical resources from category {}", resources.size(), category); + return resources; + } else if (response instanceof List) { + @SuppressWarnings("unchecked") + List resources = (List) response; + log.info("Retrieved {} logical resources from category {}", resources.size(), category); + return resources; + } + } catch (Exception e) { + log.error("Error retrieving logical resources by category {} from TMF Resource Inventory", category, e); + } + } else { + log.warn("ProducerTemplate not available"); + } + + return new ArrayList<>(); + } + + + @Override + public List searchByText(String text) { + log.info("Search logical resources by text: {}", text); + + if (producerTemplate != null) { + try { + Object response = producerTemplate.requestBodyAndHeader(endpointSearchResources, "", "text", text); + + if (response instanceof String) { + List resources = toJsonList((String) response, LogicalResource.class); + log.info("Retrieved {} logical resources from text {}", resources.size(), text); + return resources; + } else if (response instanceof List) { + @SuppressWarnings("unchecked") + List resources = (List) response; + log.info("Retrieved {} logical resources from text {}", resources.size(), text); + return resources; + } + } catch (Exception e) { + log.error("Error retrieving logical resources by text {} from TMF Resource Inventory", text, e); + } + } else { + log.warn("ProducerTemplate not available"); + } + + return new ArrayList<>(); + } + + @Override + public Optional findById(String id) { + log.info("Finding logical resource by id: {}", id); + + if (producerTemplate != null) { + try { + Object response = producerTemplate.requestBody(endpointGetById, id); + + if (response instanceof String) { + LogicalResource resource = toJsonObj((String) response, LogicalResource.class); + return Optional.ofNullable(resource); + } else if (response instanceof LogicalResource) { + return Optional.of((LogicalResource) response); + } + } catch (Exception e) { + log.error("Error retrieving logical resource {} from TMF Resource Inventory", id, e); + } + } else { + log.warn("ProducerTemplate not available"); + } + + return Optional.empty(); + } + + @Override + public LogicalResource save(LogicalResource resource) { + log.info("Saving logical resource: {}", resource.getName()); + + if (producerTemplate != null) { + try { + String payload = toJsonString(resource); + Object response = producerTemplate.requestBody(endpointAdd, payload); + + if (response instanceof String) { + return toJsonObj((String) response, LogicalResource.class); + } else if (response instanceof LogicalResource) { + return (LogicalResource) response; + } + } catch (Exception e) { + log.error("Error saving logical resource to TMF Resource Inventory", e); + throw new RuntimeException("Failed to save logical resource", e); + } + } + + log.warn("ProducerTemplate not available, returning input object"); + return resource; + } + + @Override + public LogicalResource update(String id, LogicalResource resource) { + log.info("Updating logical resource: {}", id); + + if (producerTemplate != null) { + try { + String payload = toJsonString(resource); + Map headers = new HashMap<>(); + headers.put("resourceId", id); + headers.put("triggerServiceActionQueue", false); + Object response = producerTemplate.requestBodyAndHeaders(endpointUpdate, payload, headers ); + + if (response instanceof String) { + return toJsonObj((String) response, LogicalResource.class); + } else if (response instanceof LogicalResource) { + return (LogicalResource) response; + } + } catch (Exception e) { + log.error("Error updating logical resource {} in TMF Resource Inventory", id, e); + throw new RuntimeException("Failed to update logical resource", e); + } + } + + log.warn("ProducerTemplate not available, returning input object"); + return resource; + } + + @Override + public LogicalResource createOrUpdate(LogicalResource resource) { + log.info("Creating or updating logical resource: {}", resource.getName()); + + if (producerTemplate != null) { + try { + String payload = toJsonString(resource); + Object response = producerTemplate.requestBody(endpointCreateOrUpdate, payload); + + if (response instanceof String) { + LogicalResource result = toJsonObj((String) response, LogicalResource.class); + log.info("Successfully created/updated logical resource with ID: {}", result.getUuid()); + return result; + } else if (response instanceof LogicalResource) { + LogicalResource result = (LogicalResource) response; + log.info("Successfully created/updated logical resource with ID: {}", result.getUuid()); + return result; + } + } catch (Exception e) { + log.error("Error creating/updating logical resource in TMF Resource Inventory", e); + throw new RuntimeException("Failed to create or update logical resource", e); + } + } + + log.warn("ProducerTemplate not available, returning input object"); + return resource; + } + + @Override + public void deleteById(String id) { + log.info("Deleting logical resource: {}", id); + + // Note: There's no DELETE endpoint configured in application.yml for resources + // Resource deletion is typically handled through lifecycle state changes + log.warn("Direct delete not supported - use lifecycle state changes instead"); + throw new UnsupportedOperationException("Resource deletion not supported via this repository"); + } + + @Override + public boolean existsById(String id) { + return findById(id).isPresent(); + } + + private T toJsonObj(String content, Class valueType) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.readValue(content, valueType); + } + + private List toJsonList(String content, Class valueType) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.readValue(content, mapper.getTypeFactory().constructCollectionType(List.class, valueType)); + } + + private String toJsonString(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsString(object); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/TMFResourceSpecRepositoryImpl.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/TMFResourceSpecRepositoryImpl.java new file mode 100644 index 0000000..06464b7 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/TMFResourceSpecRepositoryImpl.java @@ -0,0 +1,292 @@ +package org.etsi.osl.controllers.ietf.ns.repository.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.camel.ProducerTemplate; +import org.etsi.osl.controllers.ietf.ns.repository.TMFResourceSpecRepository; +import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecification; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCreate; +import org.etsi.osl.tmf.ri639.model.LogicalResource; +import org.etsi.osl.tmf.ri639.model.Resource; +import org.etsi.osl.tmf.ri639.model.ResourceUpdate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Repository; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Implementation of TMFResourceSpecRepository using Apache Camel and ActiveMQ + * Communicates with external TMF Resource Catalog Management service + */ +@Repository +@Slf4j +public class TMFResourceSpecRepositoryImpl implements TMFResourceSpecRepository { + + @Autowired(required = false) + private ProducerTemplate producerTemplate; + + @Value("${CATALOG_GET_RESOURCESPECS}") + private String endpointGetAll; + + + @Value("${CATALOG_ADD_RESOURCESPEC}") + private String endpointSave; + + @Value("${CATALOG_UPD_RESOURCESPEC}") + private String endpointUpdate; + + @Value("${CATALOG_DELETE_RESOURCESPEC}") + private String endpointDelete; + + @Value("${CATALOG_UPDADD_RESOURCESPEC}") + private String CATALOG_UPDADD_RESOURCESPEC = ""; + + @Value("${CATALOG_GET_RESOURCESPEC_BY_NAME_CATEGORY}") + private String CATALOG_GET_RESOURCESPEC_BY_NAME_CATEGORY = ""; + + @Value("${CATALOG_GET_RESOURCESPEC_BY_ID}") + private String CATALOG_GET_RESOURCESPEC_BY_ID = ""; + + @Value("${CATALOG_UPDADD_RESOURCE}") + private String CATALOG_UPDADD_RESOURCE = ""; + + @Value("${CATALOG_UPD_RESOURCE}") + private String CATALOG_UPD_RESOURCE = ""; + + @Value("${CATALOG_GET_RESOURCE_BY_ID}") + private String CATALOG_GET_RESOURCE_BY_ID = ""; + + + @Autowired + private ProducerTemplate template; + + @Override + public List findAll() { + log.info("Finding all resource specifications via TMF service"); + + if (producerTemplate != null) { + try { + Object result = producerTemplate.requestBody(endpointGetAll, (Object) null); + if (result instanceof List) { + return (List) result; + } + } catch (Exception e) { + log.error("Error retrieving resource specifications from TMF service", e); + } + } + + log.warn("ProducerTemplate not available or error occurred, returning empty list"); + return new ArrayList<>(); + } + + @Override + public Optional findById(String id) { + log.info("Finding resource specification by id: {}", id); + + if (producerTemplate != null) { + try { + LogicalResourceSpecification result = producerTemplate.requestBody( + CATALOG_GET_RESOURCESPEC_BY_ID, id, LogicalResourceSpecification.class); + return Optional.ofNullable(result); + } catch (Exception e) { + log.error("Error retrieving resource specification {} from TMF service", id, e); + } + } + + return Optional.empty(); + } + + @Override + public LogicalResourceSpecification save(LogicalResourceSpecification resourceSpec) { + log.info("Saving resource specification: {}", resourceSpec.getName()); + + if (producerTemplate != null) { + try { + return producerTemplate.requestBody( + endpointSave, resourceSpec, LogicalResourceSpecification.class); + } catch (Exception e) { + log.error("Error saving resource specification to TMF service", e); + throw new RuntimeException("Failed to save resource specification", e); + } + } + + log.warn("ProducerTemplate not available, returning input object"); + return resourceSpec; + } + + @Override + public LogicalResourceSpecification update(String id, LogicalResourceSpecification resourceSpec) { + log.info("Updating resource specification: {}", id); + + resourceSpec.setUuid(id); + + if (producerTemplate != null) { + try { + return producerTemplate.requestBody( + endpointUpdate, resourceSpec, LogicalResourceSpecification.class); + } catch (Exception e) { + log.error("Error updating resource specification {} in TMF service", id, e); + throw new RuntimeException("Failed to update resource specification", e); + } + } + + log.warn("ProducerTemplate not available, returning input object"); + return resourceSpec; + } + + @Override + public void deleteById(String id) { + log.info("Deleting resource specification: {}", id); + + if (producerTemplate != null) { + try { + producerTemplate.requestBody(endpointDelete, id); + } catch (Exception e) { + log.error("Error deleting resource specification {} from TMF service", id, e); + throw new RuntimeException("Failed to delete resource specification", e); + } + } else { + log.warn("ProducerTemplate not available, delete operation not executed"); + } + } + + @Override + public boolean existsById(String id) { + return findById(id).isPresent(); + } + + /** + * get service spec by id from model via bus + * @param id + * @return + * @throws IOException + */ + public ResourceSpecification retrieveResourceSpecByNameCategoryVersion(String aName, String aCategory, String aVersion) { + log.info("will retrieve Resource Specification aName=" + aName ); + + try { + Map map = new HashMap<>(); + map.put( "aname", aName); + map.put( "acategory", aCategory); + map.put( "aversion", aVersion); + Object response = + producerTemplate.requestBodyAndHeaders( CATALOG_GET_RESOURCESPEC_BY_NAME_CATEGORY, null, map); + + if ( !(response instanceof String)) { + log.error("Resource Specification object is wrong."); + return null; + } + LogicalResourceSpecification sor = toJsonObj( (String)response, LogicalResourceSpecification.class); + //log.debug("retrieveSpec response is: " + response); + return sor; + + }catch (Exception e) { + log.error("Cannot retrieve Resource Specification details from catalog. " + e.toString()); + } + return null; + } + + /** + * get service spec by id from model via bus + * @param id + * @return + * @throws IOException + */ + public ResourceSpecification retrieveResourceSpec(String specid) { + log.info("will retrieve Resource Specification id=" + specid ); + + try { + Object response = producerTemplate. + requestBody( CATALOG_GET_RESOURCESPEC_BY_ID, specid); + + if ( !(response instanceof String)) { + log.error("Resource Specification object is wrong."); + return null; + } + LogicalResourceSpecification sor = toJsonObj( (String)response, LogicalResourceSpecification.class); + //log.debug("retrieveSpec response is: " + response); + return sor; + + }catch (Exception e) { + log.error("Cannot retrieve Resource Specification details from catalog. " + e.toString()); + } + return null; + } + + + public LogicalResourceSpecification createOrUpdateResourceSpecByNameCategoryVersion( ResourceSpecificationCreate s) { + log.info("will createOrUpdateResourceSpecByNameCategoryVersion " ); + try { + Map map = new HashMap<>(); + map.put("aname", s.getName()); + map.put("aversion", s.getVersion()); + map.put("acategory", s.getCategory()); + + Object response = producerTemplate.requestBodyAndHeaders( CATALOG_UPDADD_RESOURCESPEC, toJsonString(s), map); + + if ( !(response instanceof String)) { + log.error("ResourceSpecification object is wrong."); + } + + LogicalResourceSpecification rs = toJsonObj( (String)response, LogicalResourceSpecification.class); + return rs; + + + }catch (Exception e) { + log.error("Cannot create ResourceSpecification"); + e.printStackTrace(); + } + return null; + + } + + + public Resource updateResourceById(String oslResourceId, ResourceUpdate rs) { + + + log.debug("will update Resource : " + oslResourceId ); + try { + Map map = new HashMap<>(); + map.put("resourceId", oslResourceId ); + map.put("triggerServiceActionQueue", false ); + + Object response = template.requestBodyAndHeaders( CATALOG_UPD_RESOURCE, toJsonString(rs), map); + + if ( !(response instanceof String)) { + log.error("Service Instance object is wrong."); + } + + LogicalResource resourceInstance = toJsonObj( (String)response, LogicalResource.class); + //logger.debug("createService response is: " + response); + return resourceInstance; + + + }catch (Exception e) { + e.printStackTrace(); + log.error("Cannot update Service: " + oslResourceId + ": " + e.toString()); + } + return null; + } + + + private T toJsonObj(String content, Class valueType) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.readValue( content, valueType); + } + + private String toJsonString(Object object) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsString(object); + } + + +} diff --git a/src/main/resources/application-demo.yaml b/src/main/resources/application-demo.yaml new file mode 100644 index 0000000..c0ad70c --- /dev/null +++ b/src/main/resources/application-demo.yaml @@ -0,0 +1,75 @@ +############################################################################### +# Demo RESTCONF Server Configuration +# Port: 11880 +# Purpose: Serves example network-slice-services for testing and integration +############################################################################### + +server: + port: 11880 + servlet: + context-path: / + +spring: + application: + name: restconf-server-demo + + # Disable unnecessary auto-configurations for demo + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration + - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration + +# IETF NS Controller Configuration +osl-ietf-ns-controller: + category: ns.ietf.controllers.osl.etsi.org + version: 0.1.0 + +# Logging Configuration +logging: + level: + root: INFO + org.etsi.osl.controllers.ietf.ns: DEBUG + org.springframework.web: INFO + pattern: + console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" + +# Management endpoints (for health checks, etc.) +management: + endpoints: + web: + exposure: + include: health,info + endpoint: + health: + show-details: always + +############################################################################### +# Demo Server Information +############################################################################### +# After starting this server, you can access the following endpoints: +# +# 1. Get root network slice services container: +# GET http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services +# +# 2. Get all SLO/SLE templates: +# GET http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slo-sle-templates +# +# 3. Get all slice services: +# GET http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services +# +# 4. Get specific slice service: +# GET http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services/{id} +# +# 5. Create new slice service: +# POST http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services +# Body: SliceService JSON object +# +# 6. Update slice service: +# PUT http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services/{id} +# Body: SliceService JSON object +# +# 7. Delete slice service: +# DELETE http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services/{id} +# +############################################################################### diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..c3b9498 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,119 @@ +osl-ietf-ns-controller: + category: ns.ietf.controllers.osl.etsi.org/v1alpha + version: 0.1.0 + +server: + port: 15601 + +spring: + config: + activate: + on-profile: "default" + application: + name: IETFNSController + description: "This is a IETF NS controller ns.ietf.controllers.osl.etsi.org" + servlet: + multipart.max-file-size: 10MB + multipart.max-request-size: 10MB + activemq: + brokerUrl: tcp://localhost:61616?jms.watchTopicAdvisories=false + user: artemis + password: artemis + pool: + enabled: true + max-connections: 100 + packages: + trust-all: true + autoconfigure.exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + +logging: + level: + root: INFO + org.etsi.osl.gsma.ewbi.*: DEBUG + org.etsi.osl.controllers.ietf.ns: DEBUG + org.springframework: INFO + org.apache.camel: INFO + com.zaxxer.hikari: INFO + pattern: + console: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" + file: "%d %p %c{1.} [%t] %m%n" + + + + +oauthsign: + key: "EK97Y7Y9WPGG1MEG" + +# RESTCONF Client Configuration +# Configure connection to Network Slice Service Provider +restconf: + # Base URL of the RESTCONF provider (e.g., https://nsc-provider:8443) + provider-url: http://localhost:11880 + + # Authentication method: basic, oauth2, mtls, none + # basic = HTTP Basic Authentication (default) + auth-method: basic + + # Authentication credentials for HTTP Basic Auth + # Default: admin/admin123 (matches demo server) + auth: + username: admin + password: admin123 + + # YANG model version for ietf-network-slice-service + api-version: "2025-05-09" + + # Connection timeout in milliseconds + timeout-ms: 10000 + +#QUEUE MESSAGES +CATALOG_GET_SERVICEORDERS: "jms:queue:CATALOG.GET.SERVICEORDERS" +CATALOG_GET_SERVICEORDER_BY_ID: "jms:queue:CATALOG.GET.SERVICEORDER_BY_ID" +CATALOG_ADD_SERVICEORDER: "jms:queue:CATALOG.ADD.SERVICEORDER" +CATALOG_UPD_SERVICEORDER_BY_ID: "jms:queue:CATALOG.UPD.SERVICEORDER_BY_ID" +CATALOG_GET_SERVICESPEC_BY_ID: "jms:queue:CATALOG.GET.SERVICESPEC_BY_ID" +CATALOG_ADD_SERVICESPEC: "jms:queue:CATALOG.ADD.SERVICESPEC" +CATALOG_UPD_SERVICESPEC: "jms:queue:CATALOG.UPD.SERVICESPEC" +CATALOG_UPDADD_SERVICESPEC: "jms:queue:CATALOG.UPDADD.SERVICESPEC" + + +CATALOG_GET_INITIAL_SERVICEORDERS_IDS: "jms:queue:CATALOG.GET.INITIAL_SERVICEORDERS" +CATALOG_GET_SERVICEORDER_IDS_BY_STATE: "jms:queue:CATALOG.GET.ACKNOWLEDGED_SERVICEORDERS" +CATALOG_ADD_SERVICE: "jms:queue:CATALOG.ADD.SERVICE" +CATALOG_UPD_SERVICE: "jms:queue:CATALOG.UPD.SERVICE" +CATALOG_GET_SERVICE_BY_ID: "jms:queue:CATALOG.GET.SERVICE" +CATALOG_GET_SERVICE_BY_ORDERID: "jms:queue:CATALOG.GET.SERVICE_BY_ORDERID" +CATALOG_SERVICE_QUEUE_ITEMS_GET: "jms:queue:CATALOG.SERVICEQUEUEITEMS.GET" +CATALOG_SERVICE_QUEUE_ITEM_UPD: "jms:queue:CATALOG.SERVICEQUEUEITEM.UPDATE" +CATALOG_SERVICE_QUEUE_ITEM_DELETE: "jms:queue:CATALOG.SERVICEQUEUEITEM.DELETE" +CATALOG_SERVICES_TO_TERMINATE: "jms:queue:CATALOG.GET.SERVICETOTERMINATE" + +CATALOG_GET_EXTERNAL_SERVICE_PARTNERS: "jms:queue:CATALOG.GET.EXTERNALSERVICEPARTNERS" +CATALOG_UPD_EXTERNAL_SERVICESPEC: "jms:queue:CATALOG.UPD.EXTERNAL_SERVICESPEC" + + +#RESOURCES MESSAGES +CATALOG_ADD_RESOURCE: "jms:queue:CATALOG.ADD.RESOURCE" +CATALOG_UPD_RESOURCE: "jms:queue:CATALOG.UPD.RESOURCE" +CATALOG_UPDADD_RESOURCE: "jms:queue:CATALOG.UPDADD.RESOURCE" +CATALOG_GET_RESOURCE_BY_ID: "jms:queue:CATALOG.GET.RESOURCE" +CATALOG_GET_RESOURCES_BY_CATEGORY: "jms:queue:CATALOG.GET.RESOURCE_BY_CATEGORY" +CATALOG_SEARCH_RESOURCES: "jms:queue:CATALOG.SEARCH.RESOURCE" +CATALOG_ADD_RESOURCESPEC: "jms:queue:CATALOG.ADD.RESOURCESPEC" +CATALOG_UPD_RESOURCESPEC: "jms:queue:CATALOG.UPD.RESOURCESPEC" +CATALOG_UPDADD_RESOURCESPEC: "jms:queue:CATALOG.UPDADD.RESOURCESPEC" +CATALOG_GET_RESOURCESPEC_BY_ID: "jms:queue:CATALOG.GET.RESOURCESPEC_BY_ID" +CATALOG_GET_RESOURCESPEC_BY_NAME_CATEGORY: "jms:queue:CATALOG.GET.RESOURCESPEC_BY_NAME_CATEGORY" +CATALOG_GET_RESOURCESPECS: "jms:queue:CATALOG.GET.RESOURCESPECS" +CATALOG_DELETE_RESOURCESPEC: "jms:queue:CATALOG.DELETE.RESOURCESPEC" + +#PARTNER MESSAGES +CATALOG_GET_PARTNER_ORGANIZATON_BY_ID: "jms:queue:CATALOG.GET.PARTNER_ORGANIZATION_BY_ID" +CATALOG_UPDATE_PARTNER_ORGANIZATION: "jms:queue:CATALOG.UPD.PARTNER_ORGANIZATION" +CATALOG_SERVICES_OF_PARTNERS: "jms:queue:CATALOG.GET.SERVICESOFPARTNERS" +CATALOG_RESOURCES_OF_PARTNERS: "jms:queue:CATALOG.GET.SERVICESOFPARTNERS" +EVENT_ORGANIZATION_CREATE: "jms:topic:EVENT.ORGANIZATION.CREATE" +EVENT_ORGANIZATION_CHANGED: "jms:topic:EVENT.ORGANIZATION.CHANGE" +--- + + diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..74229cd --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,11 @@ + ___ ____ _ _ + / _ \ _ __ ___ _ __ / ___|| (_) ___ ___ + | | | | '_ \ / _ \ '_ \\___ \| | |/ __/ _ \ + | |_| | |_) | __/ | | |___) | | | (_| __/ + \___/| .__/ \___|_| |_|____/|_|_|\___\___| + |_| + __ __________________ + / / __ __ / __/_ __/ __/ _/ + / _ \/ // / / _/ / / _\ \_/ / + /_.__/\_, / /___/ /_/ /___/___/ + /___/ \ No newline at end of file diff --git a/src/main/resources/ietf_green_request.json b/src/main/resources/ietf_green_request.json new file mode 100644 index 0000000..9430f28 --- /dev/null +++ b/src/main/resources/ietf_green_request.json @@ -0,0 +1,172 @@ +{ + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "B", + "description": "", + "slo-policy": { + "metric-bound": [ + { + "metric-type": "energy_consumption", + "metric-unit": "kWh", + "bound": 20200 + }, + { + "metric-type": "energy_efficiency", + "metric-unit": "Wats/bps", + "bound": 6 + }, + { + "metric-type": "carbon_emission", + "metric-unit": "grams of CO2 per kWh", + "bound": 750 + }, + { + "metric-type": "renewable_energy_usage", + "metric-unit": "rate", + "bound": 0.5 + } + ] + }, + "sle-policy": { + "security": "", + "isolation": "", + "path-constraints": { + "service-functions": "", + "diversity": { + "diversity": { + "diversity-type": "" + } + } + } + } + } + ] + }, + "slice-service": [ + { + "id": "slice-service-88a585f7-a432-4312-8774-6210fb0b2342", + "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L2" + ] + } + ] + }, + "slo-sle-policy": { + "slo-sle-template": "B" + }, + "status": {}, + "sdps": { + "sdp": [ + { + "id": "CU-N32", + "geo-location": "", + "node-id": "A", + "sdp-ip-address": "10.60.11.3", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "101", + "target-connection-group-id": "A_B" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "100", + "ac-ipv4-address": "10.60.11.3", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "4.4.4.4" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + }, + { + "id": "UPF-N32", + "geo-location": "", + "node-id": "B", + "sdp-ip-address": "10.60.10.6", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "101", + "target-connection-group-id": "A_B" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "200", + "ac-ipv4-address": "10.60.10.6", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "5.5.5.5" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "A_B", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": 1, + "a2a-sdp": [ + { + "sdp-id": "CU-N32" + }, + { + "sdp-id": "UPF-N32" + } + ] + } + ], + "status": {} + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/src/main/resources/slice_request_backhaul_control.json b/src/main/resources/slice_request_backhaul_control.json new file mode 100644 index 0000000..f215078 --- /dev/null +++ b/src/main/resources/slice_request_backhaul_control.json @@ -0,0 +1,162 @@ +{ + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "A", + "description": "", + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-bandwidth", + "metric-unit": "kbps", + "bound": 2000 + }, + { + "metric-type": "one-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": 5 + } + ] + }, + "sle-policy": { + "security": "", + "isolation": "", + "path-constraints": { + "service-functions": "", + "diversity": { + "diversity": { + "diversity-type": "" + } + } + } + } + } + ] + }, + "slice-service": [ + { + "id": "slice-service-11327140-7361-41b3-aa45-e84a7fb40be9", + "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", + "service-tags": { + "tag-type": [ + { + "tag-type": "service", + "tag-type-value": [ + "L2" + ] + } + ] + }, + "slo-sle-policy": { + "slo-sle-template": "A" + }, + "status": {}, + "sdps": { + "sdp": [ + { + "id": "", + "geo-location": "", + "node-id": "CU-N2", + "sdp-ip-address": "10.60.11.3", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "100", + "target-connection-group-id": "CU-N2_AMF-N2" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "100", + "ac-ipv4-address": "10.60.11.3", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "1.1.1.1" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + }, + { + "id": "", + "geo-location": "", + "node-id": "AMF-N2", + "sdp-ip-address": "10.60.60.105", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "100", + "target-connection-group-id": "CU-N2_AMF-N2" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "200", + "ac-ipv4-address": "10.60.60.105", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "3.3.3.3" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "CU-N2_AMF-N2", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": 1, + "a2a-sdp": [ + { + "sdp-id": "01" + }, + { + "sdp-id": "02" + } + ] + } + ], + "status": {} + } + ] + } + } + ] + } + } \ No newline at end of file diff --git a/src/main/resources/slice_request_backhaul_user.json b/src/main/resources/slice_request_backhaul_user.json new file mode 100644 index 0000000..efd1666 --- /dev/null +++ b/src/main/resources/slice_request_backhaul_user.json @@ -0,0 +1,164 @@ +[ + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "C", + "description": "", + "slo-policy": { + "metric-bound": [ + { + "metric-type": "one-way-bandwidth", + "metric-unit": "kbps", + "bound": 100 + }, + { + "metric-type": "one-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": 10 + } + ] + }, + "sle-policy": { + "security": "", + "isolation": "", + "path-constraints": { + "service-functions": "", + "diversity": { + "diversity": { + "diversity-type": "" + } + } + } + } + } + ] + }, + "slice-service": [ + { + "id": "slice-service-181e303a-a051-42e5-b2f2-4060732c631f", + "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", + "service-tags": { + "tag-type": [ + { + "tag-type": "", + "tag-type-value": [ + "" + ] + } + ] + }, + "slo-sle-policy": { + "slo-sle-template": "C" + }, + "status": {}, + "sdps": { + "sdp": [ + { + "id": "", + "geo-location": "", + "node-id": "CU-N31", + "sdp-ip-address": "10.60.11.3", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "102", + "target-connection-group-id": "CU-N31_UPF-N31" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "100", + "ac-ipv4-address": "10.60.11.3", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "4.4.4.4" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + }, + { + "id": "", + "geo-location": "", + "node-id": "UPF-N31", + "sdp-ip-address": "10.60.60.106", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": "102", + "target-connection-group-id": "CU-N31_UPF-N31" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "200", + "ac-ipv4-address": "10.60.60.106", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "5.5.5.5" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "CU-N31_UPF-N31", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": 1, + "a2a-sdp": [ + { + "sdp-id": "01" + }, + { + "sdp-id": "02" + } + ] + } + ], + "status": {} + } + ] + } + } + ] + } + } +] \ No newline at end of file diff --git a/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializerTest.java b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializerTest.java new file mode 100644 index 0000000..122ea42 --- /dev/null +++ b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializerTest.java @@ -0,0 +1,249 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.ConnectionGroup; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.ConnectivityType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SDP; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +/** + * Unit tests for RFC 9543 SliceService deserialization. + * + * Tests the custom deserializer's ability to parse RFC 9543 formatted JSON + * with hyphenated field names into SliceService domain objects. + */ +@DisplayName("RFC 9543 SliceService Deserialization Tests") +class Rfc9543SliceServiceDeserializerTest { + + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + @DisplayName("Should parse RFC 9543 SliceService JSON with service-tags") + void testParseSliceServiceWithServiceTags() throws Exception { + // Sample RFC 9543 JSON from green request + String json = "{\"id\":\"slice-service-88a585f7-a432-4312-8774-6210fb0b2342\"," + + "\"description\":\"Transport network slice mapped with 3GPP slice NetworkSlice1\"," + + "\"service-tags\":{\"tag-type\":[{\"tag-type\":\"service\",\"tag-type-value\":[\"L2\"]}]}," + + "\"slo-sle-policy\":{\"slo-sle-template\":\"B\"}," + + "\"status\":{}," + + "\"sdps\":{\"sdp\":[]}," + + "\"connection-groups\":{\"connection-group\":[]}}"; + + // Act + SliceService sliceService = objectMapper.readValue(json, SliceService.class); + + // Assert + assertNotNull(sliceService, "SliceService should not be null"); + assertEquals("slice-service-88a585f7-a432-4312-8774-6210fb0b2342", sliceService.getId()); + assertEquals("Transport network slice mapped with 3GPP slice NetworkSlice1", + sliceService.getDescription()); + + // Verify service tags were parsed + assertEquals(1, sliceService.getServiceTags().size(), + "Should have exactly one service tag"); + assertTrue(sliceService.getServiceTags().get(0).getValue().contains("L2"), + "Service tag should contain 'L2'"); + + // Verify SLO/SLE template reference + assertNotNull(sliceService.getSloSleTemplate(), "SLO/SLE template should not be null"); + assertEquals("B", sliceService.getSloSleTemplate().getId()); + } + + @Test + @DisplayName("Should parse RFC 9543 SliceService JSON with SDPs") + void testParseSliceServiceWithSdps() throws Exception { + String json = "{" + + "\"id\":\"slice-service-test-123\"," + + "\"description\":\"Test slice service\"," + + "\"slo-sle-policy\":{\"slo-sle-template\":\"A\"}," + + "\"status\":{}," + + "\"sdps\":{" + + " \"sdp\":[" + + " {" + + " \"id\":\"CU-N32\"," + + " \"node-id\":\"A\"," + + " \"sdp-ip-address\":\"10.60.11.3\"," + + " \"geo-location\":\"\"," + + " \"tp-ref\":\"\"," + + " \"service-match-criteria\":{\"match-criterion\":[]}," + + " \"incoming-qos-policy\":\"\"," + + " \"outgoing-qos-policy\":\"\"," + + " \"sdp-peering\":{\"peer-sap-id\":\"\",\"protocols\":\"\"}," + + " \"ac-svc-ref\":[]," + + " \"attachment-circuits\":{\"attachment-circuit\":[]}," + + " \"status\":{}," + + " \"sdp-monitoring\":\"\"" + + " }" + + " ]" + + "}," + + "\"connection-groups\":{\"connection-group\":[]}}"; + + // Act + SliceService sliceService = objectMapper.readValue(json, SliceService.class); + + // Assert + assertNotNull(sliceService); + assertEquals("slice-service-test-123", sliceService.getId()); + assertEquals(1, sliceService.getSdps().size(), "Should have one SDP"); + + SDP sdp = sliceService.getSdps().get(0); + assertEquals("CU-N32", sdp.getId()); + assertEquals("A", sdp.getNodeId()); + assertEquals(1, sdp.getSdpIpAddress().size()); + assertEquals("10.60.11.3", sdp.getSdpIpAddress().get(0)); + } + + @Test + @DisplayName("Should parse RFC 9543 SliceService JSON with connection groups") + void testParseSliceServiceWithConnectionGroups() throws Exception { + String json = "{" + + "\"id\":\"slice-service-conn-test\"," + + "\"description\":\"Test with connection groups\"," + + "\"slo-sle-policy\":{\"slo-sle-template\":\"C\"}," + + "\"status\":{}," + + "\"sdps\":{\"sdp\":[]}," + + "\"connection-groups\":{" + + " \"connection-group\":[" + + " {" + + " \"id\":\"A_B\"," + + " \"connectivity-type\":\"ietf-vpn-common:any-to-any\"," + + " \"connectivity-construct\":[]," + + " \"status\":{}" + + " }" + + " ]" + + "}}"; + + // Act + SliceService sliceService = objectMapper.readValue(json, SliceService.class); + + // Assert + assertNotNull(sliceService); + assertEquals(1, sliceService.getConnectionGroups().size(), + "Should have one connection group"); + + ConnectionGroup connGroup = sliceService.getConnectionGroups().get(0); + assertEquals("A_B", connGroup.getId()); + assertEquals(ConnectivityType.A2A, connGroup.getConnectivityType()); + } + + @Test + @DisplayName("Should handle missing optional fields gracefully") + void testParseSliceServiceWithMinimalData() throws Exception { + String json = "{" + + "\"id\":\"minimal-service\"," + + "\"description\":\"Minimal service\"}"; + + // Act + SliceService sliceService = objectMapper.readValue(json, SliceService.class); + + // Assert + assertNotNull(sliceService); + assertEquals("minimal-service", sliceService.getId()); + assertEquals("Minimal service", sliceService.getDescription()); + assertEquals(0, sliceService.getServiceTags().size()); + assertEquals(0, sliceService.getSdps().size()); + assertEquals(0, sliceService.getConnectionGroups().size()); + } + + @Test + @DisplayName("Should parse complete green request JSON") + void testParseCompleteGreenRequestJson() throws Exception { + // Full green request JSON from the ietf_green_request.json file + String json = "{" + + "\"id\":\"slice-service-88a585f7-a432-4312-8774-6210fb0b2342\"," + + "\"description\":\"Transport network slice mapped with 3GPP slice NetworkSlice1\"," + + "\"service-tags\":{\"tag-type\":[{\"tag-type\":\"service\",\"tag-type-value\":[\"L2\"]}]}," + + "\"slo-sle-policy\":{\"slo-sle-template\":\"B\"}," + + "\"status\":{}," + + "\"sdps\":{" + + " \"sdp\":[" + + " {" + + " \"id\":\"CU-N32\"," + + " \"geo-location\":\"\"," + + " \"node-id\":\"A\"," + + " \"sdp-ip-address\":\"10.60.11.3\"," + + " \"tp-ref\":\"\"," + + " \"service-match-criteria\":{\"match-criterion\":[{\"index\":1,\"match-type\":\"VLAN\",\"value\":\"101\",\"target-connection-group-id\":\"A_B\"}]}," + + " \"incoming-qos-policy\":\"\"," + + " \"outgoing-qos-policy\":\"\"," + + " \"sdp-peering\":{\"peer-sap-id\":\"\",\"protocols\":\"\"}," + + " \"ac-svc-ref\":[]," + + " \"attachment-circuits\":{\"attachment-circuit\":[{\"id\":\"100\",\"ac-ipv4-address\":\"10.60.11.3\",\"ac-ipv4-prefix-length\":0,\"sdp-peering\":{\"peer-sap-id\":\"4.4.4.4\"},\"status\":{}}]}," + + " \"status\":{}," + + " \"sdp-monitoring\":\"\"" + + " }," + + " {" + + " \"id\":\"UPF-N32\"," + + " \"geo-location\":\"\"," + + " \"node-id\":\"B\"," + + " \"sdp-ip-address\":\"10.60.10.6\"," + + " \"tp-ref\":\"\"," + + " \"service-match-criteria\":{\"match-criterion\":[{\"index\":1,\"match-type\":\"VLAN\",\"value\":\"101\",\"target-connection-group-id\":\"A_B\"}]}," + + " \"incoming-qos-policy\":\"\"," + + " \"outgoing-qos-policy\":\"\"," + + " \"sdp-peering\":{\"peer-sap-id\":\"\",\"protocols\":\"\"}," + + " \"ac-svc-ref\":[]," + + " \"attachment-circuits\":{\"attachment-circuit\":[{\"id\":\"200\",\"ac-ipv4-address\":\"10.60.10.6\",\"ac-ipv4-prefix-length\":0,\"sdp-peering\":{\"peer-sap-id\":\"5.5.5.5\"},\"status\":{}}]}," + + " \"status\":{}," + + " \"sdp-monitoring\":\"\"" + + " }" + + " ]" + + "}," + + "\"connection-groups\":{" + + " \"connection-group\":[" + + " {" + + " \"id\":\"A_B\"," + + " \"connectivity-type\":\"ietf-vpn-common:any-to-any\"," + + " \"connectivity-construct\":[{\"id\":1,\"a2a-sdp\":[{\"sdp-id\":\"01\"},{\"sdp-id\":\"02\"}]}]," + + " \"status\":{}" + + " }" + + " ]" + + "}}"; + + // Act + SliceService sliceService = objectMapper.readValue(json, SliceService.class); + + // Assert - comprehensive validation + assertNotNull(sliceService); + assertEquals("slice-service-88a585f7-a432-4312-8774-6210fb0b2342", sliceService.getId()); + + // Validate service tags + assertEquals(1, sliceService.getServiceTags().size()); + assertEquals("service:L2", sliceService.getServiceTags().get(0).getValue()); + + // Validate template reference + assertNotNull(sliceService.getSloSleTemplate()); + assertEquals("B", sliceService.getSloSleTemplate().getId()); + + // Validate SDPs + assertEquals(2, sliceService.getSdps().size()); + SDP sdp1 = sliceService.getSdps().get(0); + assertEquals("CU-N32", sdp1.getId()); + assertEquals("A", sdp1.getNodeId()); + assertEquals(1, sdp1.getSdpIpAddress().size()); + assertEquals("10.60.11.3", sdp1.getSdpIpAddress().get(0)); + + SDP sdp2 = sliceService.getSdps().get(1); + assertEquals("UPF-N32", sdp2.getId()); + assertEquals("B", sdp2.getNodeId()); + assertEquals(1, sdp2.getSdpIpAddress().size()); + assertEquals("10.60.10.6", sdp2.getSdpIpAddress().get(0)); + + // Validate connection groups + assertEquals(1, sliceService.getConnectionGroups().size()); + ConnectionGroup connGroup = sliceService.getConnectionGroups().get(0); + assertEquals("A_B", connGroup.getId()); + assertEquals(ConnectivityType.A2A, connGroup.getConnectivityType()); + } +} diff --git a/src/test/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImplTest.java b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImplTest.java new file mode 100644 index 0000000..ee86e6a --- /dev/null +++ b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImplTest.java @@ -0,0 +1,444 @@ +package org.etsi.osl.controllers.ietf.ns.api.restconf; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import org.etsi.osl.controllers.ietf.ns.api.restconf.RestconfClientImpl; +import org.etsi.osl.controllers.ietf.ns.api.restconf.RestconfException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +/** + * Unit tests for RestconfClientImpl.getSloSleTemplates() method. + * + * Tests cover: + * - Successful template retrieval + * - Empty response handling + * - HTTP error handling (401, 404, 500) + * - Network errors (timeout, connection refused) + * - Null/empty response body handling + * - Authentication header inclusion + */ +@ExtendWith(MockitoExtension.class) +@DisplayName("RestconfClientImpl - getSloSleTemplates()") +class RestconfClientImplTest { + + private RestconfClientImpl restconfClient; + + @Mock + private RestTemplate restTemplate; + + private static final String PROVIDER_URL = "http://localhost:11880"; + private static final String TEMPLATES_URI = PROVIDER_URL + "/restconf/data/ietf-network-slice-service:network-slice-services/slo-sle-templates"; + private static final String VALID_RESPONSE = """ + { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "GOLD-template", + "description": "Premium service level", + "slo-policy": { + "availability": { + "availability-type": "single-percent-five-nines" + } + } + } + ] + } + } + """; + + @BeforeEach + void setUp() { + restconfClient = new RestconfClientImpl(); + ReflectionTestUtils.setField(restconfClient, "restTemplate", restTemplate); + ReflectionTestUtils.setField(restconfClient, "providerUrl", PROVIDER_URL); + ReflectionTestUtils.setField(restconfClient, "authMethod", "basic"); + ReflectionTestUtils.setField(restconfClient, "authUsername", "admin"); + ReflectionTestUtils.setField(restconfClient, "authPassword", "admin123"); + } + + @Test + @DisplayName("Should retrieve templates successfully with valid response") + void testGetSloSleTemplates_Success() throws RestconfException { + // Arrange + ResponseEntity responseEntity = new ResponseEntity<>(VALID_RESPONSE, HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + String result = restconfClient.getSloSleTemplates(); + + // Assert + assertNotNull(result); + assertEquals(VALID_RESPONSE, result); + assertTrue(result.contains("slo-sle-templates")); + assertTrue(result.contains("GOLD-template")); + + // Verify RestTemplate was called with GET method + verify(restTemplate, times(1)).exchange( + contains("slo-sle-templates"), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + @DisplayName("Should return empty object when provider returns empty response") + void testGetSloSleTemplates_EmptyResponse() throws RestconfException { + // Arrange + ResponseEntity responseEntity = new ResponseEntity<>("", HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + String result = restconfClient.getSloSleTemplates(); + + // Assert + assertNotNull(result); + assertEquals("{}", result); + } + + @Test + @DisplayName("Should return empty object when provider returns null body") + void testGetSloSleTemplates_NullBody() throws RestconfException { + // Arrange + ResponseEntity responseEntity = new ResponseEntity<>(null, HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + String result = restconfClient.getSloSleTemplates(); + + // Assert + assertNotNull(result); + assertEquals("{}", result); + } + + @Test + @DisplayName("Should throw RestconfException on 401 Unauthorized") + void testGetSloSleTemplates_401Unauthorized() { + // Arrange + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenThrow(new HttpClientErrorException(HttpStatus.UNAUTHORIZED, "Unauthorized")); + + // Act & Assert + RestconfException exception = assertThrows(RestconfException.class, () -> { + restconfClient.getSloSleTemplates(); + }); + + assertNotNull(exception); + } + + @Test + @DisplayName("Should throw RestconfException on 404 Not Found") + void testGetSloSleTemplates_404NotFound() { + // Arrange + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenThrow(new HttpClientErrorException(HttpStatus.NOT_FOUND, "Not Found")); + + // Act & Assert + RestconfException exception = assertThrows(RestconfException.class, () -> { + restconfClient.getSloSleTemplates(); + }); + + assertNotNull(exception); + } + + @Test + @DisplayName("Should throw RestconfException on 500 Server Error") + void testGetSloSleTemplates_500ServerError() { + // Arrange + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenThrow(new HttpClientErrorException(HttpStatus.INTERNAL_SERVER_ERROR, "Server Error")); + + // Act & Assert + RestconfException exception = assertThrows(RestconfException.class, () -> { + restconfClient.getSloSleTemplates(); + }); + + assertNotNull(exception); + } + + @Test + @DisplayName("Should throw RestconfException on network timeout") + void testGetSloSleTemplates_NetworkTimeout() { + // Arrange + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenThrow(new RuntimeException("Connection timeout")); + + // Act & Assert + RestconfException exception = assertThrows(RestconfException.class, () -> { + restconfClient.getSloSleTemplates(); + }); + + assertTrue(exception.getMessage().contains("Failed to retrieve SLO/SLE templates")); + } + + @Test + @DisplayName("Should throw RestconfException on connection refused") + void testGetSloSleTemplates_ConnectionRefused() { + // Arrange + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenThrow(new RuntimeException("Connection refused")); + + // Act & Assert + RestconfException exception = assertThrows(RestconfException.class, () -> { + restconfClient.getSloSleTemplates(); + }); + + assertTrue(exception.getMessage().contains("Failed to retrieve SLO/SLE templates")); + } + + @Test + @DisplayName("Should construct correct URI with provider URL and templates endpoint") + void testGetSloSleTemplates_URIConstruction() throws RestconfException { + // Arrange + ResponseEntity responseEntity = new ResponseEntity<>(VALID_RESPONSE, HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + restconfClient.getSloSleTemplates(); + + // Assert - Verify the correct URI was used + verify(restTemplate).exchange( + contains("slo-sle-templates"), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + ); + } + + @Test + @DisplayName("Should handle very large template response") + void testGetSloSleTemplates_LargeResponse() throws RestconfException { + // Arrange - Create a large response with multiple templates + StringBuilder largeResponse = new StringBuilder(); + largeResponse.append("{\"slo-sle-templates\": {\"slo-sle-template\": ["); + for (int i = 0; i < 100; i++) { + if (i > 0) largeResponse.append(","); + largeResponse.append("{\"id\": \"template-").append(i).append("\"}"); + } + largeResponse.append("]}}"); + + ResponseEntity responseEntity = new ResponseEntity<>(largeResponse.toString(), HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + String result = restconfClient.getSloSleTemplates(); + + // Assert + assertNotNull(result); + assertTrue(result.contains("template-0")); + assertTrue(result.contains("template-99")); + } + + @Test + @DisplayName("Should handle RFC 9543 wrapped template response") + void testGetSloSleTemplates_RFC9543Response() throws RestconfException { + // Arrange - RFC 9543 format with namespace and kebab-case properties + String rfc9543Response = """ + { + "ietf-network-slice-service:slo-sle-templates": { + "slo-sle-template": [ + { + "id": "template-1", + "slo-policy": { + "availability": { + "availability-type": "single-percent-five-nines" + }, + "mtu": 1500 + }, + "sle-policy": { + "service-security-type": "public", + "service-isolation-type": "logical" + } + } + ] + } + } + """; + + ResponseEntity responseEntity = new ResponseEntity<>(rfc9543Response, HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + String result = restconfClient.getSloSleTemplates(); + + // Assert + assertNotNull(result); + assertTrue(result.contains("slo-sle-template")); + assertTrue(result.contains("availability-type")); + assertTrue(result.contains("mtu")); + } + + @Test + @DisplayName("Should include authentication header in request") + void testGetSloSleTemplates_IncludesAuthHeader() throws RestconfException { + // Arrange + ResponseEntity responseEntity = new ResponseEntity<>(VALID_RESPONSE, HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + restconfClient.getSloSleTemplates(); + + // Assert - Verify HttpEntity was created (which should contain headers with auth) + verify(restTemplate).exchange( + anyString(), + eq(HttpMethod.GET), + argThat(httpEntity -> { + if (httpEntity == null) return false; + // Verify headers are included + return httpEntity.getHeaders() != null; + }), + eq(String.class) + ); + } + + @Test + @DisplayName("Should return HTTP 400 Bad Request with meaningful error") + void testGetSloSleTemplates_400BadRequest() { + // Arrange + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Invalid query")); + + // Act & Assert + RestconfException exception = assertThrows(RestconfException.class, () -> { + restconfClient.getSloSleTemplates(); + }); + + assertNotNull(exception); + } + + @Test + @DisplayName("Should handle multiline JSON response") + void testGetSloSleTemplates_MultilineJSON() throws RestconfException { + // Arrange - Response with multiple lines and nested structure + String multilineResponse = """ + { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "template-1", + "description": "Multi-line + description", + "slo-policy": { + "availability": { + "availability-type": "single-percent-five-nines", + "mtu": 1500 + } + } + } + ] + } + } + """; + + ResponseEntity responseEntity = new ResponseEntity<>(multilineResponse, HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + String result = restconfClient.getSloSleTemplates(); + + // Assert + assertNotNull(result); + assertTrue(result.contains("slo-sle-templates")); + } + + @Test + @DisplayName("Should call exchange method exactly once per request") + void testGetSloSleTemplates_SingleExchangeCall() throws RestconfException { + // Arrange + ResponseEntity responseEntity = new ResponseEntity<>(VALID_RESPONSE, HttpStatus.OK); + when(restTemplate.exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + )).thenReturn(responseEntity); + + // Act + restconfClient.getSloSleTemplates(); + + // Assert + verify(restTemplate, times(1)).exchange( + anyString(), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(String.class) + ); + } +} -- GitLab From 503279c81698016291e3d5ba5bc40aea9ccb84b0 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Wed, 13 May 2026 23:56:06 +0300 Subject: [PATCH 02/12] logicalresource specs created and restconf api updated --- doc/bootstrap_phase.puml | 12 +- .../api/SloSleTemplateBootstrapService.java | 857 ++++-------------- .../ns/api/domain/model/SloSleTemplate.java | 3 +- .../ietf/ns/api/restconf/RestconfClient.java | 279 ++++-- .../ns/api/restconf/RestconfClientImpl.java | 789 ++++++++-------- .../api/restconf/RestconfConsumerService.java | 25 +- .../common/LogicalResourceSpecMappable.java | 191 ++++ .../EntityToLogicalResourceSpecMapper.java | 92 +- .../LogicalResourceToEntityMapper.java | 2 +- src/main/resources/ietf_green_request.json | 172 ---- .../slice_request_backhaul_control.json | 162 ---- .../slice_request_backhaul_user.json | 164 ---- 12 files changed, 1130 insertions(+), 1618 deletions(-) create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceSpecMappable.java delete mode 100644 src/main/resources/ietf_green_request.json delete mode 100644 src/main/resources/slice_request_backhaul_control.json delete mode 100644 src/main/resources/slice_request_backhaul_user.json diff --git a/doc/bootstrap_phase.puml b/doc/bootstrap_phase.puml index e9faa95..0bf1457 100644 --- a/doc/bootstrap_phase.puml +++ b/doc/bootstrap_phase.puml @@ -1,5 +1,5 @@ @startuml Bootstrap_Phase -actor "TFS Controller" as TFS +actor "IETF NS Controller" as IETFNS participant "RestconfConsumerService" as RCS participant "RestconfClient" as RC participant "TerflowSDN\n(RESTCONF Server)" as TFSDN @@ -7,7 +7,7 @@ participant "SloSleTemplateBootstrapService" as BOOTSTRAP participant "CatalogClient" as CATALOG participant "OpenSlice TMF API" as TMF -TFS ->> BOOTSTRAP: ApplicationReady Event +IETFNS ->> BOOTSTRAP: ApplicationReady Event activate BOOTSTRAP BOOTSTRAP ->> RCS: Retrieve templates from provider @@ -32,7 +32,7 @@ BOOTSTRAP ->> BOOTSTRAP: For each SloSleTemplate:\n1. Create LogicalResourceSpec BOOTSTRAP ->> CATALOG: Register LogicalResourceSpecification activate CATALOG -CATALOG ->> TMF: POST to Resource Catalog API\nCategory: tfs.controllers.osl.etsi.org/v1alpha +CATALOG ->> TMF: POST to Resource Catalog API\nCategory: ns.ietf.controllers.osl.etsi.org/v1alpha activate TMF TMF -->> CATALOG: ResourceSpecification created deactivate TMF @@ -45,11 +45,11 @@ BOOTSTRAP ->> BOOTSTRAP: Create example SliceService\nfor testing BOOTSTRAP ->> RCS: Store SloSleTemplate in memory RCS -->> BOOTSTRAP: Stored -BOOTSTRAP -->> TFS: Bootstrap complete +BOOTSTRAP -->> IETFNS: Bootstrap complete deactivate BOOTSTRAP -note over TFS - TFS Controller is now ready to receive +note over IETFNS + IETF NS Controller is now ready to receive CREATE/UPDATE/DELETE messages for network slice services end note diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java index c6d9ea6..94cb846 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java @@ -33,7 +33,10 @@ import org.etsi.osl.controllers.ietf.ns.mappers.EntityToLogicalResourceMapper; import org.etsi.osl.controllers.ietf.ns.mappers.EntityToLogicalResourceSpecMapper; import org.etsi.osl.controllers.ietf.ns.repository.impl.TMFResourceInventoryRepositoryImpl; import org.etsi.osl.controllers.ietf.ns.repository.impl.TMFResourceSpecRepositoryImpl; +import org.etsi.osl.tmf.common.model.Any; import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCharacteristic; +import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCharacteristicValue; import org.etsi.osl.tmf.rcm634.model.ResourceSpecificationCreate; import org.etsi.osl.tmf.ri639.model.LogicalResource; import org.springframework.beans.factory.annotation.Autowired; @@ -45,7 +48,7 @@ import java.util.UUID; * Bootstrap component that registers SLO/SLE template resource specifications at startup. * * This service creates and registers LogicalResourceSpecification templates for - * different SLO/SLE tiers (Bronze, Silver, Gold examples) that define the structure and schema + * different SLO/SLE tiers that define the structure and schema * for each tier in the TMF catalog. * * @@ -92,20 +95,12 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { try { // Create list to store all specifications (default + retrieved) - List allSpecifications = new ArrayList<>(); - // Separate list for slice services (LogicalResource) - List sliceServices = new ArrayList<>(); - - - // 1. Create default example templates - log.info("Step 1: Creating default SLO/SLE tier templates (Bronze, Silver, Gold)"); - List defaultSpecs = createSpecificationTemplates(); - allSpecifications.addAll(defaultSpecs); - log.info(" Created {} default templates", defaultSpecs.size()); - - // 2. Retrieve templates from RESTCONF provider + List allSpecifications = createSpecificationTemplates(); + + + // Retrieve templates from RESTCONF provider if (restconfClient != null) { - log.info("Step 2: Retrieving SLO/SLE templates from RESTCONF provider"); + log.info("Step 1: Retrieving SLO/SLE templates from RESTCONF provider"); List retrievedSpecs = retrieveTemplatesFromProvider(); allSpecifications.addAll(retrievedSpecs); log.info(" Retrieved {} templates from provider", retrievedSpecs.size()); @@ -114,27 +109,13 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { } // 3. Register all default and retrieved specifications - log.info("Step 3: [NOT PERFORMED] Registering all SLO/SLE templates in TMF catalog"); + log.info("Step 3: Registering all SLO/SLE templates in TMF catalog"); for (LogicalResourceSpecification spec : allSpecifications) { - //registerSpecification(spec); - } - - // 4. Create example slice services for provider templates - if (!providerTemplates.isEmpty()) { - log.info("Step 4: Creating example SliceServices for provider templates"); - List providerExampleServices = createExampleSliceServicesForProviderTemplates(); - log.info(" Created {} example slice services for provider templates", providerExampleServices.size()); - } else { - log.info("Step 4: No provider templates available - skipping example slice service creation"); + registerSpecification(spec); } - - // Load and register backhaul slice requests - log.info("Step 5: Loading and registering RFC 9543 backhaul slice requests"); - loadAndRegisterLocalSlices(); - - log.info("=== SLO/SLE Template Bootstrap Complete: {} template IDs registered, {} slice services registered ===", - templateRegistry.getTemplateCount(), sliceServices.size()); + log.info("=== SLO/SLE Template Bootstrap Complete: {} template IDs registered ===", + templateRegistry.getTemplateCount()); // Log all registered template IDs templateRegistry.getAllTemplates().forEach((templateName, templateId) -> @@ -147,22 +128,188 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { } /** - * Create default template specifications for all SLO/SLE tiers + * Create template specifications for all entity types */ private List createSpecificationTemplates() { - log.info("Creating SLO/SLE resource specification templates"); - + log.info("Creating resource specification templates for all entity types"); List specs = new ArrayList<>(); + specs.add(createSloSleSpecification()); + specs.add(createSliceServiceSpecification()); + specs.add(createNetworkSliceServicesSpecification()); + return specs; + } + - // Create templates for each tier - specs.add(createBronzeTemplateSpecification()); - specs.add(createSilverTemplateSpecification()); - specs.add(createGoldTemplateSpecification()); - - return specs; + private LogicalResourceSpecification createSloSleSpecification() { + log.info("Creating default SloSleTemplate resource specification"); + + // Build a representative SloSleTemplate whose fields will be reflected + // as ResourceSpecificationCharacteristic entries by the mapper + SloSleTemplate template = new SloSleTemplate(); + template.setId("SloSleTemplate"); + template.setDescription( + "IETF RFC 9543 SLO/SLE template defining service level objectives and expectations"); + + // SloPolicy — representative defaults covering the three main metric families + SloPolicy sloPolicy = new SloPolicy(); + sloPolicy.setAvailability(new AvailabilityType(99.9, "per-month", 43L)); + sloPolicy.setMtu(1500L); + sloPolicy.addMetricBound( + new MetricBound(ServiceSloMetricType.ONE_WAY_DELAY_MAXIMUM, "ms", 50L)); + sloPolicy.addMetricBound( + new MetricBound(ServiceSloMetricType.TWO_WAY_BANDWIDTH, "Mbps", 1000L)); + sloPolicy.addMetricBound( + new MetricBound(ServiceSloMetricType.ONE_WAY_PACKET_LOSS, "%", 1L)); + template.setSloPolicy(sloPolicy); + + // SlePolicy — representative defaults for security and isolation requirements + SlePolicy slePolicy = new SlePolicy(); + slePolicy.addSecurityRequirement(ServiceSecurityType.ENCRYPTION_REQUIRED); + slePolicy.addIsolationRequirement(ServiceIsolationType.LOGICAL_ISOLATION); + slePolicy.setMaxOccupancyLevel((short) 100); + template.setSlePolicy(slePolicy); + + // Convert fields to ResourceSpecificationCharacteristic entries via mapper + LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); + + spec.setName("IETFSloSleTemplateSpec"); + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(categoryConfig.getVersion()); + spec.setDescription( + "IETF RFC 9543 SLO/SLE template specification for network slice service level objectives and expectations"); + + // Carry the full template as a JSON blob so consumers can round-trip + // the original RFC 9543 structure without re-assembling it from individual characteristics + ResourceSpecificationCharacteristic jsonCharacteristic = new ResourceSpecificationCharacteristic(); + jsonCharacteristic.setName("SloSleTemplateAsJson"); + jsonCharacteristic.setValueType("TEXT"); + jsonCharacteristic.setConfigurable(true); + ResourceSpecificationCharacteristicValue jsonCharValue = new ResourceSpecificationCharacteristicValue(); + jsonCharValue.setIsDefault(true); + jsonCharValue.setValueType("TEXT"); + Any jsonAny = new Any(); + jsonAny.setValue(""); + jsonAny.setAlias("SloSleTemplateAsJson"); + jsonCharValue.setValue(jsonAny); + jsonCharacteristic.addResourceSpecCharacteristicValueItem(jsonCharValue); + spec.addResourceSpecCharacteristicItem(jsonCharacteristic); + + log.info("Created SloSleTemplate specification: name={}, category={}, version={}", + spec.getName(), spec.getCategory(), spec.getVersion()); + return spec; } + /** + * Creates a LogicalResourceSpecification for an IETF RFC 9543 Network Slice Service. + * + * The specification captures the structural schema of a SliceService — its identity, + * operational status, SLO/SLE template reference, SDPs, connection groups and service + * tags — as TMF ResourceSpecificationCharacteristic entries. + * A configurable {@code SliceServiceAsJson} TEXT characteristic is also added so that + * consumers can store and round-trip the full RFC 9543 JSON payload without having to + * reassemble it from individual characteristics. + * + * @return LogicalResourceSpecification representing a Network Slice Service schema + */ + private LogicalResourceSpecification createSliceServiceSpecification() { + log.info("Creating NetworkSliceService resource specification"); + + LogicalResourceSpecification spec = new LogicalResourceSpecification(); + spec.setName("IETFSliceServiceSpec"); + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(categoryConfig.getVersion()); + spec.setDescription( + "IETF RFC 9543 Network Slice Service specification defining connectivity endpoints, " + + "SLO/SLE policy reference, and operational status"); + spec.setLifecycleStatus("Active"); + + // Core identity + addSpecCharacteristic(spec, "id", "TEXT", "", true); + addSpecCharacteristic(spec, "description", "TEXT", "", true); + addSpecCharacteristic(spec, "testOnly", "BOOLEAN", "false", true); + + // Operational status (RFC 9543 admin/oper states) + addSpecCharacteristic(spec, "status.adminState", "TEXT", "admin-up", true); + addSpecCharacteristic(spec, "status.operState", "TEXT", "operational", true); + + // SLO/SLE template reference (stores the referenced SloSleTemplate ID) + addSpecCharacteristic(spec, "sloSleTemplate", "TEXT", "", false); + + // Structural collections + addSpecCharacteristic(spec, "serviceTags", "ARRAY", "[]", true); + addSpecCharacteristic(spec, "sdps", "ARRAY", "[]", true); + addSpecCharacteristic(spec, "connectionGroups", "ARRAY", "[]", true); + + // Full RFC 9543 JSON blob — configurable at instantiation time + addSpecCharacteristic(spec, "SliceServiceAsJson", "TEXT", "", true); + + log.info("Created NetworkSliceService specification: name={}, category={}, version={}", + spec.getName(), spec.getCategory(), spec.getVersion()); + return spec; + } + + /** + * Creates a LogicalResourceSpecification for the IETF RFC 9543 NetworkSliceServices container. + * + * This specification carries the entire RFC 9543 data model as two JSON array + * characteristics, allowing consumers to read and write the full container content + * without navigating individual nested characteristics: + *
    + *
  • {@code SloSleTemplatesAsJsonArray} – JSON array of all slo-sle-template entries
  • + *
  • {@code SliceServicesAsJsonArray} – JSON array of all slice-service entries
  • + *
+ * + * @return LogicalResourceSpecification representing the NetworkSliceServices container schema + */ + private LogicalResourceSpecification createNetworkSliceServicesSpecification() { + log.info("Creating NetworkSliceServices resource specification"); + + LogicalResourceSpecification spec = new LogicalResourceSpecification(); + spec.setName("IETFNetworkSliceServicesSpec"); + spec.setCategory(categoryConfig.getCategoryForSpecifications()); + spec.setVersion(categoryConfig.getVersion()); + spec.setDescription( + "IETF RFC 9543 top-level network-slice-services container specification " + + "holding SLO/SLE templates and slice services as JSON arrays"); + spec.setLifecycleStatus("Active"); + + addSpecCharacteristic(spec, "SloSleTemplatesAsJsonArray", "TEXT", "[]", true); + addSpecCharacteristic(spec, "SliceServicesAsJsonArray", "TEXT", "[]", true); + + log.info("Created NetworkSliceServices specification: name={}, category={}, version={}", + spec.getName(), spec.getCategory(), spec.getVersion()); + return spec; + } + + /** + * Adds a single ResourceSpecificationCharacteristic to the given spec. + * + * @param spec target specification + * @param name characteristic name + * @param valueType TMF value type (TEXT, BOOLEAN, NUMBER, ARRAY, …) + * @param defaultValue string representation of the default value + * @param configurable whether the characteristic is writable by consumers + */ + private void addSpecCharacteristic(LogicalResourceSpecification spec, + String name, String valueType, String defaultValue, boolean configurable) { + ResourceSpecificationCharacteristic characteristic = new ResourceSpecificationCharacteristic(); + characteristic.setName(name); + characteristic.setValueType(valueType); + characteristic.setConfigurable(configurable); + + ResourceSpecificationCharacteristicValue charValue = new ResourceSpecificationCharacteristicValue(); + charValue.setIsDefault(true); + charValue.setValueType(valueType); + + Any anyValue = new Any(); + anyValue.setValue(defaultValue); + anyValue.setAlias(name); + charValue.setValue(anyValue); + + characteristic.addResourceSpecCharacteristicValueItem(charValue); + spec.addResourceSpecCharacteristicItem(characteristic); + } /** * Retrieve RFC 9543 SLO/SLE templates directly from the RESTCONF provider. @@ -365,29 +512,6 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { } - /** - * Load a resource from classpath. - * - * @param resourceName Name of the resource file (e.g., "ietf_green_request.json") - * @return Resource content as String, or null if not found - */ - private String loadClasspathResource(String resourceName) { - try { - ClassPathResource resource = new ClassPathResource(resourceName); - if (!resource.exists()) { - log.warn("Classpath resource not found: {}", resourceName); - return null; - } - - try (InputStream inputStream = resource.getInputStream()) { - byte[] bytes = inputStream.readAllBytes(); - return new String(bytes, StandardCharsets.UTF_8); - } - } catch (IOException e) { - log.error("Error loading classpath resource: {}", resourceName, e); - return null; - } - } /** * Parse RFC 9543 formatted SliceService JSON and return SliceService domain object. @@ -517,609 +641,4 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { - /** - * Load and register backhaul slice requests from classpath. - * - * Loads both: - * 1. slice_request_backhaul_control.json - Control plane backhaul (Template A) - * 2. slice_request_backhaul_user.json - User plane backhaul (Template C) - * - * Both are registered as LogicalResourceSpecification in TMF catalog. - */ - public void loadAndRegisterLocalSlices() { - log.info("Loading and registering RFC 9543 backhaul slice requests"); - - - // Load and register GreenRequest - loadAndRegisterSliceFromFile("ietf_green_request.json", "Green-optimized network slice for energy-efficient transport. " + - "Maps 3GPP NetworkSlice1 to IETF RFC 9543 with energy consumption, efficiency, " + - "carbon emission, and renewable energy metrics."); - - // Load and register control plane backhaul - loadAndRegisterSliceFromFile("slice_request_backhaul_control.json", "Backhaul Control Plane"); - - // Load and register user plane backhaul - loadAndRegisterSliceFromFile("slice_request_backhaul_user.json", "Backhaul User Plane"); - } - - /** - * Load and register a single backhaul slice request. - * - * @param fileName Name of the JSON file (e.g., "slice_request_backhaul_control.json") - * @param description Description for the service - */ - private void loadAndRegisterSliceFromFile(String fileName, String description) { - try { - log.info("Loading backhaul slice request: {}", fileName); - - // Load JSON from classpath - String sliceJson = loadClasspathResource(fileName); - if (sliceJson == null || sliceJson.isEmpty()) { - log.warn("Backhaul slice request JSON not found or empty: {}", fileName); - return; - } - - // Handle array format (slice_request_backhaul_user.json is an array) - String serviceJson = sliceJson; - if (sliceJson.trim().startsWith("[")) { - // Extract first element from array - ObjectMapper mapper = new ObjectMapper(); - JsonNode arrayNode = mapper.readTree(sliceJson); - if (arrayNode.isArray() && arrayNode.size() > 0) { - serviceJson = mapper.writeValueAsString(arrayNode.get(0)); - } else { - log.warn("Backhaul slice request array is empty: {}", fileName); - return; - } - } - - // Parse JSON to NetworkSliceServices - NetworkSliceServices networkSliceServices = parseAndConvertSliceService(serviceJson); - log.info("Successfully parsed backhaul network slice services with {} service(s)", - networkSliceServices.getSliceServices().size()); - - // Convert NetworkSliceServices to LogicalResourceSpecification - LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(networkSliceServices); - - // Enhance specification with backhaul metadata - String templateId = networkSliceServices.getSliceServices().isEmpty() ? "unknown" : - (networkSliceServices.getSliceServices().get(0).getSloSleTemplate() != null ? - networkSliceServices.getSliceServices().get(0).getSloSleTemplate().getId() : "unknown"); - - spec.setName( spec.getName() + "_LocalTemplate_" + templateId ); - spec.setCategory(categoryConfig.getCategoryForSpecifications()); - spec.setVersion(categoryConfig.getVersion()); - spec.setDescription(description + " - " + networkSliceServices.getEntityDescription() + - ". Maps 3GPP network slice to IETF RFC 9543 standard format."); - spec.setType("LogicalResourceSpecification"); - spec.setBaseType("ResourceSpecification"); - spec.setLifecycleStatus("Active"); - - // Register the specification - registerSpecification(spec); - - log.info("Successfully registered backhaul slice as LogicalResourceSpecification: {}", spec.getName()); - - } catch (Exception e) { - log.error("Error loading and registering backhaul slice: {}", fileName, e); - // Don't throw - allow bootstrap to continue - } - } - - /** - * Create and register example SliceService instances for each template retrieved from the RESTCONF provider. - * - * For each provider template, this method: - * 1. Creates a synthetic SliceService that references the template - * 2. Converts it to LogicalResource - * 3. Registers it in the TMF Resource Inventory - * - * This ensures that every template from the provider has at least one example - * slice service demonstrating its usage. - * - * @return List of LogicalResource instances created for provider templates - */ - public List createExampleSliceServicesForProviderTemplates() { - List createdServices = new ArrayList<>(); - - if (providerTemplates.isEmpty()) { - log.info("No provider templates available for example slice service creation"); - return createdServices; - } - - log.info("Creating example SliceService instances for {} provider templates", providerTemplates.size()); - - for (SloSleTemplate template : providerTemplates) { - try { - NetworkSliceServices networkSliceServices = createExampleSliceServiceForTemplate(template); - if (networkSliceServices != null) { - // Convert NetworkSliceServices to LogicalResourceSpecification - LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(networkSliceServices); - spec.setName( spec.getName() + "_fromTemplate_" + template.getEntityName()); - - if (spec != null) { - createdServices.add( spec ); - registerSpecification(spec); - } - } - } catch (Exception e) { - log.error("Error creating example slice service for template: {}", template.getId(), e); - // Continue with next template even if this one fails - } - } - - log.info("Successfully created and registered example slice SliceService for provider templates", - createdServices.size()); - - return createdServices; - } - - /** - * Create a synthetic example SliceService that references a specific template. - * - * This method generates a demo slice service with: - * - Unique ID (UUID-based) - * - Template reference pointing to the given template - * - Reasonable default configuration - * - Two SDPs with basic connectivity setup - * - One connection group for point-to-point connectivity - * - * @param template The SloSleTemplate to create an example service for - * @return NetworkSliceServices instance configured with the template - */ - private NetworkSliceServices createExampleSliceServiceForTemplate(SloSleTemplate template) { - try { - // Create unique service ID - String serviceId = "slice-service-" + UUID.randomUUID().toString(); - - SliceService service = new SliceService(); - service.setId(serviceId); - service.setDescription("Example network slice service using template '" + template.getId() + - "' from RESTCONF provider"); - service.setTestOnly(false); - - // Set template reference - service.setSloSleTemplate(template); - - // Set service status - ServiceStatus status = new ServiceStatus(); - status.setAdminState("admin-up"); - status.setOperState("operational"); - service.setStatus(status); - - // Add service tag - ServiceTag tag = new ServiceTag(); - tag.setValue("provider-template:" + template.getId()); - service.getServiceTags().add(tag); - - // Wrap in NetworkSliceServices - NetworkSliceServices networkSliceServices = new NetworkSliceServices(); - List services = new ArrayList<>(); - services.add(service); - networkSliceServices.setSliceServices(services); - - // Generate RFC 9543 formatted JSON with slice-service array - String exampleJson = buildExampleSliceServiceJson(serviceId, template.getId()); - networkSliceServices.setJsonRequest(exampleJson); - - log.debug("Created example NetworkSliceServices with service {} for template {}", serviceId, template.getId()); - return networkSliceServices; - - } catch (Exception e) { - log.error("Error creating example NetworkSliceServices for template: {}", template.getId(), e); - return null; - } - } - - /** - * Build a synthetic example slice service JSON structure. - * - * Creates a minimal but valid RFC 9543 slice-service JSON (without namespace wrapper) - * that demonstrates how to use a specific template. - * - * @param serviceId The service ID - * @param templateId The template ID to reference - * @return JSON string representing the slice service - */ - private String buildExampleSliceServiceJson(String serviceId, String templateId) { - try { - ObjectMapper mapper = new ObjectMapper(); - - // Build the slice service JSON object - com.fasterxml.jackson.databind.node.ObjectNode sliceService = - mapper.createObjectNode(); - - sliceService.put("id", serviceId); - sliceService.put("description", "Example network slice service using template '" + - templateId + "' from RESTCONF provider"); - - // Add SLO/SLE template reference - com.fasterxml.jackson.databind.node.ObjectNode sloSlePolicy = - mapper.createObjectNode(); - sloSlePolicy.put("slo-sle-template", templateId); - sliceService.set("slo-sle-policy", sloSlePolicy); - - // Add service tags - com.fasterxml.jackson.databind.node.ObjectNode serviceTags = - mapper.createObjectNode(); - com.fasterxml.jackson.databind.node.ArrayNode tagTypes = - mapper.createArrayNode(); - - com.fasterxml.jackson.databind.node.ObjectNode tagType = - mapper.createObjectNode(); - tagType.put("tag-type", "provider-template"); - com.fasterxml.jackson.databind.node.ArrayNode tagValues = - mapper.createArrayNode(); - tagValues.add(templateId); - tagType.set("tag-type-value", tagValues); - tagTypes.add(tagType); - - serviceTags.set("tag-type", tagTypes); - sliceService.set("service-tags", serviceTags); - - // Add status - com.fasterxml.jackson.databind.node.ObjectNode status = - mapper.createObjectNode(); - sliceService.set("status", status); - - // Add minimal SDPs (two example service demarcation points) - com.fasterxml.jackson.databind.node.ObjectNode sdps = - mapper.createObjectNode(); - com.fasterxml.jackson.databind.node.ArrayNode sdpArray = - mapper.createArrayNode(); - - // SDP 1 - com.fasterxml.jackson.databind.node.ObjectNode sdp1 = - mapper.createObjectNode(); - sdp1.put("node-id", "example-node-1"); - sdp1.put("sdp-ip-address", "10.0.1.1"); - com.fasterxml.jackson.databind.node.ObjectNode matchCriteria1 = - mapper.createObjectNode(); - com.fasterxml.jackson.databind.node.ArrayNode matchArray1 = - mapper.createArrayNode(); - com.fasterxml.jackson.databind.node.ObjectNode match1 = - mapper.createObjectNode(); - match1.put("index", 1); - match1.put("match-type", "VLAN"); - match1.put("value", "100"); - match1.put("target-connection-group-id", "example-connection"); - matchArray1.add(match1); - matchCriteria1.set("match-criterion", matchArray1); - sdp1.set("service-match-criteria", matchCriteria1); - sdpArray.add(sdp1); - - // SDP 2 - com.fasterxml.jackson.databind.node.ObjectNode sdp2 = - mapper.createObjectNode(); - sdp2.put("node-id", "example-node-2"); - sdp2.put("sdp-ip-address", "10.0.2.1"); - com.fasterxml.jackson.databind.node.ObjectNode matchCriteria2 = - mapper.createObjectNode(); - com.fasterxml.jackson.databind.node.ArrayNode matchArray2 = - mapper.createArrayNode(); - com.fasterxml.jackson.databind.node.ObjectNode match2 = - mapper.createObjectNode(); - match2.put("index", 1); - match2.put("match-type", "VLAN"); - match2.put("value", "100"); - match2.put("target-connection-group-id", "example-connection"); - matchArray2.add(match2); - matchCriteria2.set("match-criterion", matchArray2); - sdp2.set("service-match-criteria", matchCriteria2); - sdpArray.add(sdp2); - - sdps.set("sdp", sdpArray); - sliceService.set("sdps", sdps); - - // Add connection group - com.fasterxml.jackson.databind.node.ObjectNode connectionGroups = - mapper.createObjectNode(); - com.fasterxml.jackson.databind.node.ArrayNode connGroupArray = - mapper.createArrayNode(); - - com.fasterxml.jackson.databind.node.ObjectNode connGroup = - mapper.createObjectNode(); - connGroup.put("id", "example-connection"); - connGroup.put("connectivity-type", "ietf-vpn-common:any-to-any"); - com.fasterxml.jackson.databind.node.ArrayNode constructs = - mapper.createArrayNode(); - com.fasterxml.jackson.databind.node.ObjectNode construct = - mapper.createObjectNode(); - construct.put("id", 1); - com.fasterxml.jackson.databind.node.ArrayNode sdpIds = - mapper.createArrayNode(); - sdpIds.add(mapper.createObjectNode().put("sdp-id", "01")); - sdpIds.add(mapper.createObjectNode().put("sdp-id", "02")); - construct.set("a2a-sdp", sdpIds); - constructs.add(construct); - connGroup.set("connectivity-construct", constructs); - com.fasterxml.jackson.databind.node.ObjectNode connStatus = - mapper.createObjectNode(); - connGroup.set("status", connStatus); - connGroupArray.add(connGroup); - - connectionGroups.set("connection-group", connGroupArray); - sliceService.set("connection-groups", connectionGroups); - - // Wrap in RFC 9543 format with slice-service array - com.fasterxml.jackson.databind.node.ObjectNode wrapper = mapper.createObjectNode(); - com.fasterxml.jackson.databind.node.ArrayNode sliceServiceArray = mapper.createArrayNode(); - sliceServiceArray.add(sliceService); - wrapper.set("slice-service", sliceServiceArray); - - return mapper.writeValueAsString(wrapper); - - } catch (Exception e) { - log.error("Error building example slice service JSON for template: {}", templateId, e); - return null; - } - } - - /** - * Create Bronze tier template specification. - * - * Bronze characteristics: - * - Availability: 99% (43 minutes downtime per month) - * - Bandwidth: 100 Mbps minimum - * - Latency: 200ms maximum - * - Packet Loss: 0.1% maximum - * - MTU: 1500 bytes (standard Ethernet) - * - Isolation: Traffic isolation only - * - Use case: Cost-optimized, non-critical services - */ - private LogicalResourceSpecification createBronzeTemplateSpecification() { - SloSleTemplate template = new SloSleTemplate(); - template.setId("bronze-template"); - template.setDescription("Bronze-tier template - Best-effort service with basic guarantees"); - - // Create SLO Policy - SloPolicy sloPolicy = new SloPolicy(); - - // Set availability: 99% per month (43 minutes downtime) - AvailabilityType availability = new AvailabilityType(); - availability.setAvailabilityPercentage(99.0); - availability.setCommitmentPeriod("per-month"); - availability.setAllowedDowntimeMinutes(43L); - sloPolicy.setAvailability(availability); - - // Set MTU - sloPolicy.setMtu(1500L); - - // Add metric bounds - MetricBound bandwidthBound = new MetricBound( - ServiceSloMetricType.ONE_WAY_BANDWIDTH, - "Mbps", - 100L // 100 Mbps minimum - ); - bandwidthBound.setValueDescription("Minimum bandwidth guarantee"); - sloPolicy.addMetricBound(bandwidthBound); - - MetricBound latencyBound = new MetricBound( - ServiceSloMetricType.ONE_WAY_DELAY_MAXIMUM, - "ms", - 200L // 200 ms maximum - ); - latencyBound.setValueDescription("Maximum one-way latency"); - sloPolicy.addMetricBound(latencyBound); - - MetricBound packetLossBound = new MetricBound( - ServiceSloMetricType.ONE_WAY_PACKET_LOSS, - "%", - 1L // 0.1% maximum - ); - packetLossBound.setValueDescription("Maximum packet loss rate"); - sloPolicy.addMetricBound(packetLossBound); - - template.setSloPolicy(sloPolicy); - - // Create SLE Policy - SlePolicy slePolicy = new SlePolicy(); - slePolicy.addIsolationRequirement(ServiceIsolationType.TRAFFIC_ISOLATION); - slePolicy.setMaxOccupancyLevel((short) 50); // Max 50% of resources - template.setSlePolicy(slePolicy); - - LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); - - //Override Name, to show NSC templates as specnames... - spec.setName(template.getEntityName() ); - - spec.setCategory(categoryConfig.getCategoryForSpecifications()); - spec.setVersion(categoryConfig.getVersion()); - spec.setDescription("Resource specification for Bronze-tier SLO/SLE templates. " + - "Bronze templates define best-effort network slice services suitable for cost-optimized, non-critical deployments with basic guarantees."); - - return spec; - } - - /** - * Create Silver tier template specification. - * - * Silver characteristics: - * - Availability: 99.9% (4 minutes downtime per month) - * - Bandwidth: 1 Gbps minimum - * - Latency: 100ms (50th percentile), 150ms (95th percentile) - * - Packet Loss: 0.01% maximum (99th percentile) - * - MTU: 1500 bytes (standard Ethernet) - * - Isolation: Traffic and logical isolation - * - Security: Authentication required - * - Use case: Standard service, most common deployments - */ - private LogicalResourceSpecification createSilverTemplateSpecification() { - SloSleTemplate template = new SloSleTemplate(); - template.setId("silver-template"); - template.setDescription("Silver-tier template - Standard service with strong guarantees"); - - // Create SLO Policy - SloPolicy sloPolicy = new SloPolicy(); - - // Set availability: 99.9% per month (4 minutes downtime) - AvailabilityType availability = new AvailabilityType(); - availability.setAvailabilityPercentage(99.9); - availability.setCommitmentPeriod("per-month"); - availability.setAllowedDowntimeMinutes(4L); - sloPolicy.setAvailability(availability); - - // Set MTU - sloPolicy.setMtu(1500L); - - // Add metric bounds - MetricBound silverBandwidth = new MetricBound( - ServiceSloMetricType.ONE_WAY_BANDWIDTH, - "Gbps", - 1000L // 1000 Mbps (1 Gbps) minimum - ); - silverBandwidth.setValueDescription("Minimum bandwidth guarantee"); - sloPolicy.addMetricBound(silverBandwidth); - - // One-way delay at 50th percentile (median) - sloPolicy.addMetricBound(new MetricBound( - ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, - "ms", - new BigDecimal("50.0"), - 100L // 100 ms at 50th percentile - )); - - // One-way delay at 95th percentile - sloPolicy.addMetricBound(new MetricBound( - ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, - "ms", - new BigDecimal("95.0"), - 150L // 150 ms at 95th percentile - )); - - // Packet loss at 99th percentile - sloPolicy.addMetricBound(new MetricBound( - ServiceSloMetricType.ONE_WAY_PACKET_LOSS, - "%", - new BigDecimal("99.0"), - 1L // 0.01% at 99th percentile - )); - - template.setSloPolicy(sloPolicy); - - // Create SLE Policy - SlePolicy slePolicy = new SlePolicy(); - slePolicy.addSecurityRequirement(ServiceSecurityType.AUTHENTICATION_REQUIRED); - slePolicy.addIsolationRequirement(ServiceIsolationType.TRAFFIC_ISOLATION); - slePolicy.addIsolationRequirement(ServiceIsolationType.LOGICAL_ISOLATION); - slePolicy.setMaxOccupancyLevel((short) 75); // Max 75% of resources - template.setSlePolicy(slePolicy); - - LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); - - //Override Name, to show NSC templates as specnames... - spec.setName(template.getEntityName() ); - spec.setCategory(categoryConfig.getCategoryForSpecifications()); - spec.setVersion(categoryConfig.getVersion()); - spec.setDescription("Resource specification for Silver-tier SLO/SLE templates. " + - "Silver templates define standard network slice services suitable for most deployments with strong performance and security guarantees."); - - return spec; - } - - /** - * Create Gold tier template specification. - * - * Gold characteristics: - * - Availability: 99.99% (4 seconds downtime per month) - * - Bandwidth: 10 Gbps minimum - * - Latency: 30ms (50th percentile), 50ms (95th percentile), 80ms (99.9th percentile) - * - Packet Loss: 0.0001% maximum (99.99th percentile) - * - Delay Variation: 10ms maximum - * - MTU: 9000 bytes (jumbo frames for high performance) - * - Isolation: Full resource, dedicated resources - * - Security: Encryption, authentication, and integrity protection required - * - Use case: Mission-critical services, premium customers - */ - private LogicalResourceSpecification createGoldTemplateSpecification() { - SloSleTemplate template = new SloSleTemplate(); - template.setId("gold-template"); - template.setDescription("Gold-tier template - Premium service with highest guarantees"); - - // Create SLO Policy - SloPolicy sloPolicy = new SloPolicy(); - - // Set availability: 99.99% per month (4 seconds downtime) - AvailabilityType availability = new AvailabilityType(); - availability.setAvailabilityPercentage(99.99); - availability.setCommitmentPeriod("per-month"); - availability.setAllowedDowntimeMinutes(1L); - sloPolicy.setAvailability(availability); - - // Set MTU for high performance (jumbo frames) - sloPolicy.setMtu(9000L); - - // Add metric bounds - MetricBound goldBandwidth = new MetricBound( - ServiceSloMetricType.ONE_WAY_BANDWIDTH, - "Gbps", - 10000L // 10000 Mbps (10 Gbps) minimum - ); - goldBandwidth.setValueDescription("Minimum bandwidth guarantee"); - sloPolicy.addMetricBound(goldBandwidth); - - // One-way delay at 50th percentile (median) - sloPolicy.addMetricBound(new MetricBound( - ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, - "ms", - new BigDecimal("50.0"), - 30L // 30 ms at 50th percentile - )); - - // One-way delay at 95th percentile - sloPolicy.addMetricBound(new MetricBound( - ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, - "ms", - new BigDecimal("95.0"), - 50L // 50 ms at 95th percentile - )); - - // One-way delay at 99.9th percentile - sloPolicy.addMetricBound(new MetricBound( - ServiceSloMetricType.ONE_WAY_DELAY_PERCENTILE, - "ms", - new BigDecimal("99.9"), - 80L // 80 ms at 99.9th percentile - )); - - // One-way delay variation (jitter) - MetricBound jitterBound = new MetricBound( - ServiceSloMetricType.ONE_WAY_DELAY_VARIATION_MAXIMUM, - "ms", - 10L // 10 ms maximum jitter - ); - jitterBound.setValueDescription("Maximum delay variation (jitter)"); - sloPolicy.addMetricBound(jitterBound); - - // Packet loss at 99.99th percentile - sloPolicy.addMetricBound(new MetricBound( - ServiceSloMetricType.ONE_WAY_PACKET_LOSS, - "%", - new BigDecimal("99.99"), - 1L // 0.0001% at 99.99th percentile - )); - - template.setSloPolicy(sloPolicy); - - // Create SLE Policy - SlePolicy slePolicy = new SlePolicy(); - slePolicy.addSecurityRequirement(ServiceSecurityType.ENCRYPTION_REQUIRED); - slePolicy.addSecurityRequirement(ServiceSecurityType.AUTHENTICATION_REQUIRED); - slePolicy.addSecurityRequirement(ServiceSecurityType.INTEGRITY_PROTECTION); - slePolicy.addIsolationRequirement(ServiceIsolationType.RESOURCE_ISOLATION); - slePolicy.addIsolationRequirement(ServiceIsolationType.DEDICATED_RESOURCES); - slePolicy.setMaxOccupancyLevel((short) 100); // Max 100% of resources (dedicated) - template.setSlePolicy(slePolicy); - - LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); - - //Override Name, to show NSC templates as specnames... - spec.setName(template.getEntityName() ); - spec.setCategory(categoryConfig.getCategoryForSpecifications()); - spec.setVersion(categoryConfig.getVersion()); - spec.setDescription("Resource specification for Gold-tier SLO/SLE templates. " + - "Gold templates define premium network slice services suitable for mission-critical deployments with highest performance, security, and availability guarantees."); - - return spec; - } } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java index c76a382..3edbacc 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java @@ -5,6 +5,7 @@ import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SliceTemplateRe import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; import org.etsi.osl.controllers.ietf.ns.domain.common.ExcludeFromMapping; import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceMappable; +import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceSpecMappable; import org.etsi.osl.controllers.ietf.ns.domain.common.RelatedManagedResourceReference; import org.etsi.osl.tmf.ri639.model.LogicalResource; import com.fasterxml.jackson.annotation.JsonProperty; @@ -44,7 +45,7 @@ import lombok.extern.slf4j.Slf4j; @NoArgsConstructor @AllArgsConstructor @Slf4j -public class SloSleTemplate implements LogicalResourceMappable, RelatedManagedResourceReference { +public class SloSleTemplate implements LogicalResourceSpecMappable, RelatedManagedResourceReference { /** * Unique identifier for the SLO/SLE template. diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClient.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClient.java index 000df3b..21c23a0 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClient.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClient.java @@ -2,129 +2,280 @@ package org.etsi.osl.controllers.ietf.ns.api.restconf; import java.util.List; import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SDP; import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; /** - * RESTCONF Client interface for communicating with Network Slice Service Provider. + * RESTCONF Client interface for communicating with a Network Slice Service Provider. * * The IETF NS Controller acts as a Consumer (Network Slice Service Customer) that communicates * with a Provider (Network Slice Controller) via RESTCONF (RFC 8040) to manage IETF * Network Slice Services as defined in RFC 9543 and draft-ietf-teas-ietf-network-slice-nbi-yang-25. * - * Operations supported: - * - CREATE: Provision a new network slice service - * - RETRIEVE: Fetch current service configuration and status - * - UPDATE: Modify existing service configuration - * - DELETE: Decommission a network slice service - * - LIST: Enumerate all provisioned services - * - FEASIBILITY_CHECK: Validate service request without provisioning resources + * Operations are grouped by resource: + * 1. Container – /restconf/data/ietf-network-slice-service:network-slice-services + * 2. SliceService – .../slice-service and .../slice-service={id} + * 3. SDP – .../slice-service={id}/sdps and .../sdps/sdp={sdpId} + * 4. SLO/SLE Template – .../slo-sle-templates/slo-sle-template and ...={id} */ public interface RestconfClient { + // ========================================================================= + // 1. Container – /restconf/data/ietf-network-slice-service:network-slice-services + // ========================================================================= + /** - * Create new network slice services on the provider. + * Create the network-slice-services container with all nested services and templates. + * + * HTTP: POST /restconf/data/ietf-network-slice-service:network-slice-services * - * HTTP Operation: POST /restconf/data/ietf-network-slice-service:network-slice-services + * @param networkSliceServices container with slice services and SLO/SLE templates + * @return the created container + * @throws RestconfException if the request fails + */ + NetworkSliceServices createNetworkSliceServices(NetworkSliceServices networkSliceServices) + throws RestconfException; + + /** + * Replace the entire network-slice-services container. + * + * HTTP: PUT /restconf/data/ietf-network-slice-service:network-slice-services + * + * @param networkSliceServices updated container + * @return the updated container + * @throws RestconfException if the request fails or nothing exists to update + */ + NetworkSliceServices updateNetworkSliceServices(NetworkSliceServices networkSliceServices) + throws RestconfException; + + /** + * Delete the entire network-slice-services container (all slices and templates). * - * The request body follows RFC 9543 format with slice-service as an array: - * { - * "slice-service": [ - * { - * "id": "slice-001", - * "slo-sle-policy": { "slo-sle-template": "Gold" }, - * "sdps": { ... }, - * "connection-groups": { ... } - * } - * ] - * } + * HTTP: DELETE /restconf/data/ietf-network-slice-service:network-slice-services * - * @param services List of network slice services to create - * @return List of created slice services with updated status * @throws RestconfException if the request fails */ - List createSliceService(List services) throws RestconfException; + void deleteAllNetworkSliceServices() throws RestconfException; /** - * Retrieve a specific network slice service from the provider. + * Retrieve the entire network-slice-services container. * - * HTTP Operation: GET /restconf/data/ietf-network-slice-service:network-slice-services/slice-service= + * HTTP: GET /restconf/data/ietf-network-slice-service:network-slice-services * - * @param serviceId The unique identifier of the slice service - * @return The slice service with current configuration and status - * @throws RestconfException if the request fails or service not found + * @return the full container with all slice services and templates + * @throws RestconfException if the request fails or nothing is found */ - SliceService getSliceService(String serviceId) throws RestconfException; + NetworkSliceServices getAllNetworkSliceServices() throws RestconfException; + + // ========================================================================= + // 2. SliceService – .../slice-service and .../slice-service={id} + // ========================================================================= + + /** + * Create a new network slice service. + * + * HTTP: POST /restconf/data/ietf-network-slice-service:network-slice-services/slice-service + * + * @param service the slice service to create + * @return the created slice service + * @throws RestconfException if the request fails or the service already exists (409) + */ + SliceService createSliceService(SliceService service) throws RestconfException; /** - * Update an existing network slice service configuration. + * List all provisioned network slice services. * - * HTTP Operation: PATCH /restconf/data/ietf-network-slice-service:network-slice-services/slice-service= + * HTTP: GET /restconf/data/ietf-network-slice-service:network-slice-services/slice-service + * + * @return list of all slice services + * @throws RestconfException if the request fails or none are found + */ + List listSliceServices() throws RestconfException; + + /** + * Delete all network slice services. + * + * HTTP: DELETE /restconf/data/ietf-network-slice-service:network-slice-services/slice-service * - * @param serviceId The unique identifier of the slice service to update - * @param service The updated slice service configuration - * @return The updated slice service with new status * @throws RestconfException if the request fails */ + void deleteAllSliceServices() throws RestconfException; + + /** + * Retrieve a specific network slice service. + * + * HTTP: GET /restconf/data/ietf-network-slice-service:network-slice-services/slice-service={id} + * + * @param serviceId the unique identifier of the slice service + * @return the slice service with current configuration and status + * @throws RestconfException if the request fails or the service is not found + */ + SliceService getSliceService(String serviceId) throws RestconfException; + + /** + * Update an existing network slice service. + * + * HTTP: PUT /restconf/data/ietf-network-slice-service:network-slice-services/slice-service={id} + * + * @param serviceId the unique identifier of the slice service to update + * @param service the updated slice service configuration + * @return the updated slice service + * @throws RestconfException if the request fails or the service is not found + */ SliceService updateSliceService(String serviceId, SliceService service) throws RestconfException; /** - * Delete a network slice service from the provider. + * Delete a specific network slice service. * - * HTTP Operation: DELETE /restconf/data/ietf-network-slice-service:network-slice-services/slice-service= + * HTTP: DELETE /restconf/data/ietf-network-slice-service:network-slice-services/slice-service={id} * - * @param serviceId The unique identifier of the slice service to delete - * @throws RestconfException if the request fails + * @param serviceId the unique identifier of the slice service to delete + * @throws RestconfException if the request fails or the service is not found */ void deleteSliceService(String serviceId) throws RestconfException; + // ========================================================================= + // 3. SDP – .../slice-service={id}/sdps and .../sdps/sdp={sdpId} + // ========================================================================= + /** - * List all network slice services from the provider. + * Add a new SDP to an existing slice service. * - * HTTP Operation: GET /restconf/data/ietf-network-slice-service:network-slice-services/slice-service + * HTTP: POST /restconf/data/.../slice-service={serviceId}/sdps * - * @return List of all provisioned slice services - * @throws RestconfException if the request fails + * @param serviceId the slice service that will own the SDP + * @param sdp the SDP to create + * @return the created SDP + * @throws RestconfException if the request fails or the SDP already exists (409) */ - List listSliceServices() throws RestconfException; + SDP createSdp(String serviceId, SDP sdp) throws RestconfException; /** - * Check feasibility of a network slice service request without provisioning resources. + * List all SDPs of a slice service. * - * This operation uses the "test-only" mode as defined in RFC 9543, which allows - * validation of service requests before actual instantiation. Resources are not - * reserved, but the NSC computes the feasible connectivity constructs. + * HTTP: GET /restconf/data/.../slice-service={serviceId}/sdps * - * HTTP Operation: PUT with test-only=true flag - * - * @param service The slice service with test-only flag set to true - * @return The service with computed connectivity constructs and feasibility status - * - admin-state: admin-up if feasible - * - admin-state: rejected if not feasible (with reason in status) + * @param serviceId the slice service whose SDPs to retrieve + * @return list of SDPs * @throws RestconfException if the request fails */ - SliceService checkFeasibility(SliceService service) throws RestconfException; + List listSdps(String serviceId) throws RestconfException; /** - * Get service status and monitoring information. + * Delete all SDPs of a slice service. * - * HTTP Operation: GET /restconf/data/.../slice-service=/status + * HTTP: DELETE /restconf/data/.../slice-service={serviceId}/sdps * - * @param serviceId The unique identifier of the slice service - * @return Current operational and administrative status + * @param serviceId the slice service whose SDPs to delete * @throws RestconfException if the request fails */ - String getServiceStatus(String serviceId) throws RestconfException; + void deleteAllSdps(String serviceId) throws RestconfException; + + /** + * Retrieve a specific SDP of a slice service. + * + * HTTP: GET /restconf/data/.../slice-service={serviceId}/sdps/sdp={sdpId} + * + * @param serviceId the slice service identifier + * @param sdpId the SDP identifier + * @return the SDP + * @throws RestconfException if the request fails or the SDP is not found + */ + SDP getSdp(String serviceId, String sdpId) throws RestconfException; /** - * Retrieve SLO/SLE templates from the provider. + * Update a specific SDP of a slice service. * - * HTTP Operation: GET /restconf/data/ietf-network-slice-service:network-slice-services/slo-sle-templates + * HTTP: PUT /restconf/data/.../slice-service={serviceId}/sdps/sdp={sdpId} * - * Returns raw JSON response containing RFC 9543 formatted SLO/SLE templates. - * The response uses the RFC 9543 wrapped format with namespace container. + * @param serviceId the slice service identifier + * @param sdpId the SDP identifier to update + * @param sdp the updated SDP configuration + * @return the updated SDP + * @throws RestconfException if the request fails or the SDP is not found + */ + SDP updateSdp(String serviceId, String sdpId, SDP sdp) throws RestconfException; + + /** + * Delete a specific SDP from a slice service. + * + * HTTP: DELETE /restconf/data/.../slice-service={serviceId}/sdps/sdp={sdpId} + * + * @param serviceId the slice service identifier + * @param sdpId the SDP identifier to delete + * @throws RestconfException if the request fails or the SDP is not found + */ + void deleteSdp(String serviceId, String sdpId) throws RestconfException; + + // ========================================================================= + // 4. SLO/SLE Template – .../slo-sle-templates/slo-sle-template and ...={id} + // ========================================================================= + + /** + * Create a new SLO/SLE template. + * + * HTTP: POST /restconf/data/.../slo-sle-templates/slo-sle-template + * + * @param template the SLO/SLE template to create + * @return the created template + * @throws RestconfException if the request fails or the template already exists (409) + */ + SloSleTemplate createSloSleTemplate(SloSleTemplate template) throws RestconfException; + + /** + * List all SLO/SLE templates. + * + * HTTP: GET /restconf/data/.../slo-sle-templates/slo-sle-template + * + * Returns raw JSON so callers can use {@link org.etsi.osl.controllers.ietf.ns.api.restconf.Rfc9543JsonConverter} + * to parse the RFC 9543 namespace-wrapped response. * * @return JSON string containing RFC 9543 formatted SLO/SLE templates * @throws RestconfException if the request fails */ String getSloSleTemplates() throws RestconfException; + + /** + * Delete all SLO/SLE templates. + * + * HTTP: DELETE /restconf/data/.../slo-sle-templates/slo-sle-template + * + * @throws RestconfException if the request fails + */ + void deleteAllSloSleTemplates() throws RestconfException; + + /** + * Retrieve a specific SLO/SLE template by ID. + * + * HTTP: GET /restconf/data/.../slo-sle-templates/slo-sle-template={id} + * + * @param templateId the unique identifier of the template + * @return the SLO/SLE template + * @throws RestconfException if the request fails or the template is not found + */ + SloSleTemplate getSloSleTemplate(String templateId) throws RestconfException; + + /** + * Update an existing SLO/SLE template. + * + * HTTP: PUT /restconf/data/.../slo-sle-templates/slo-sle-template={id} + * + * @param templateId the unique identifier of the template to update + * @param template the updated template configuration + * @return the updated template + * @throws RestconfException if the request fails or the template is not found + */ + SloSleTemplate updateSloSleTemplate(String templateId, SloSleTemplate template) + throws RestconfException; + + /** + * Delete a specific SLO/SLE template. + * + * HTTP: DELETE /restconf/data/.../slo-sle-templates/slo-sle-template={id} + * + * @param templateId the unique identifier of the template to delete + * @throws RestconfException if the request fails or the template is not found + */ + void deleteSloSleTemplate(String templateId) throws RestconfException; } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java index f6edb87..cdddd82 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java @@ -3,11 +3,11 @@ package org.etsi.osl.controllers.ietf.ns.api.restconf; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SDP; import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -24,29 +24,27 @@ import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.databind.ObjectMapper; /** - * Implementation of RESTCONF Client for communicating with Network Slice Service Provider. + * Implementation of {@link RestconfClient} that communicates with a Network Slice Service Provider + * over HTTP/HTTPS using Spring RestTemplate. * - * This client handles HTTP/HTTPS communication with a provider's RESTCONF interface, - * implementing full lifecycle management of network slice services including: - * - Service provisioning (CREATE, RETRIEVE, UPDATE, DELETE) - * - Service discovery (LIST) - * - Feasibility validation (TEST-ONLY mode) - * - Error handling with retry logic + * All paths follow the swagger-documented RESTCONF resource tree rooted at: + * /restconf/data/ietf-network-slice-service:network-slice-services * - * Configuration: - * - restconf.provider-url: Base URL of the RESTCONF provider (e.g., https://nsc-provider:8443) - * - restconf.auth-method: Authentication method (basic, oauth2, mtls) - * - restconf.api-version: YANG model version (default: 2025-05-09) - * - restconf.timeout-ms: Connection timeout in milliseconds + * Configuration properties (application.yml): + * restconf.provider-url – base URL of the RESTCONF provider (default: http://localhost:11880) + * restconf.auth-method – authentication method: basic | none (default: basic) + * restconf.auth.username – HTTP Basic username (default: admin) + * restconf.auth.password – HTTP Basic password (default: admin123) + * restconf.timeout-ms – connection timeout in ms (default: 10000) */ @Component public class RestconfClientImpl implements RestconfClient { private static final Logger logger = LoggerFactory.getLogger("org.etsi.osl.controllers.ietf.ns"); - private static final String YANG_MODULE = "ietf-network-slice-service"; - private static final String YANG_DATE = "2025-05-09"; - private static final String RESTCONF_PATH = "/restconf/data"; + private static final String YANG_MODULE = "ietf-network-slice-service"; + private static final String RESTCONF_BASE = "/restconf/data"; + private static final String NSS_PATH = RESTCONF_BASE + "/" + YANG_MODULE + ":network-slice-services"; @Value("${restconf.provider-url:http://localhost:11880}") private String providerUrl; @@ -54,12 +52,6 @@ public class RestconfClientImpl implements RestconfClient { @Value("${restconf.auth-method:basic}") private String authMethod; - @Value("${restconf.api-version:2025-05-09}") - private String apiVersion; - - @Value("${restconf.timeout-ms:10000}") - private long timeoutMs; - @Value("${restconf.auth.username:admin}") private String authUsername; @@ -72,465 +64,532 @@ public class RestconfClientImpl implements RestconfClient { @Autowired private ObjectMapper objectMapper; + // ========================================================================= + // 1. Container + // ========================================================================= + @Override - public List createSliceService(List services) throws RestconfException { - if (services == null || services.isEmpty()) { - throw new RestconfException("Services list must not be null or empty"); + public NetworkSliceServices createNetworkSliceServices(NetworkSliceServices networkSliceServices) + throws RestconfException { + String uri = nssUri(); + logger.info("Creating network-slice-services container"); + try { + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.POST, + new HttpEntity<>(networkSliceServices, buildHeaders()), + NetworkSliceServices.class); + assertSuccess(resp, "create network-slice-services container"); + return resp.getBody(); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + throw wrap("create network-slice-services container", e); } + } - String uri = providerUrl + "/restconf/data/ietf-network-slice-service:network-slice-services"; - logger.info("Creating {} network slice service(s)", services.size()); + @Override + public NetworkSliceServices updateNetworkSliceServices(NetworkSliceServices networkSliceServices) + throws RestconfException { + String uri = nssUri(); + logger.info("Updating network-slice-services container"); + try { + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.PUT, + new HttpEntity<>(networkSliceServices, buildHeaders()), + NetworkSliceServices.class); + assertSuccess(resp, "update network-slice-services container"); + return resp.getBody(); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + throw wrap("update network-slice-services container", e); + } + } + @Override + public void deleteAllNetworkSliceServices() throws RestconfException { + String uri = nssUri(); + logger.info("Deleting entire network-slice-services container"); try { - // Build RFC 9543 format: { "slice-service": [ {...}, {...} ] } - Map> requestBody = new HashMap<>(); - requestBody.put("slice-service", services); - - HttpEntity>> requestEntity = new HttpEntity<>(requestBody, buildHeaders()); - - // Use a custom response type to handle the RFC 9543 array response - ResponseEntity response = restTemplate.exchange( - uri, - HttpMethod.POST, - requestEntity, - String.class - ); - - if (response.getStatusCode().is2xxSuccessful()) { - logger.info("Successfully created {} network slice service(s)", services.size()); - // Parse the response and return the created services - // For now, return the input services (they should be echoed back with status) - return services; - } else { - throw new RestconfException( - response.getStatusCode().value(), - "protocol", - "operation-failed", - "Failed to create network slice services" - ); - } + restTemplate.exchange(uri, HttpMethod.DELETE, + new HttpEntity<>(buildHeaders()), Void.class); } catch (HttpClientErrorException e) { throw handleHttpError(e); } catch (Exception e) { - logger.error("Error creating network slice services", e); - throw new RestconfException("Failed to create network slice services: " + e.getMessage(), e); + throw wrap("delete network-slice-services container", e); } } @Override - public SliceService getSliceService(String serviceId) throws RestconfException { - if (serviceId == null || serviceId.isEmpty()) { - throw new RestconfException("Service ID must not be null or empty"); + public NetworkSliceServices getAllNetworkSliceServices() throws RestconfException { + String uri = nssUri(); + logger.debug("Retrieving entire network-slice-services container"); + try { + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.GET, + new HttpEntity<>(buildHeaders()), + NetworkSliceServices.class); + assertSuccess(resp, "get network-slice-services"); + return resp.getBody(); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + throw wrap("get network-slice-services", e); } + } - String uri = buildServiceUri(serviceId); - logger.debug("Retrieving network slice service: {}", serviceId); + // ========================================================================= + // 2. SliceService + // ========================================================================= + @Override + public SliceService createSliceService(SliceService service) throws RestconfException { + String uri = sliceServiceListUri(); + logger.info("Creating network slice service: {}", service != null ? service.getId() : "null"); try { - ResponseEntity response = restTemplate.exchange( - uri, - HttpMethod.GET, + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.POST, + new HttpEntity<>(service, buildHeaders()), + SliceService.class); + assertSuccess(resp, "create slice service"); + return resp.getBody(); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + throw wrap("create slice service", e); + } + } + + @Override + public List listSliceServices() throws RestconfException { + String uri = sliceServiceListUri(); + logger.debug("Listing all network slice services"); + try { + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.GET, new HttpEntity<>(buildHeaders()), - SliceService.class - ); - - if (response.getStatusCode() == HttpStatus.OK) { - logger.debug("Successfully retrieved network slice service: {}", serviceId); - return response.getBody(); - } else { - throw new RestconfException( - response.getStatusCode().value(), - "protocol", - "operation-failed", - "Failed to retrieve network slice service" - ); + String.class); + + if (resp.getStatusCode() == HttpStatus.NOT_FOUND) { + return new ArrayList<>(); + } + assertSuccess(resp, "list slice services"); + + String body = resp.getBody(); + if (body == null || body.isBlank()) { + return new ArrayList<>(); + } + try { + return new Rfc9543JsonConverter().parseSliceServices(body); + } catch (Exception parseErr) { + logger.debug("RFC 9543 parse failed, falling back to direct array", parseErr); + SliceService[] arr = objectMapper.readValue(body, SliceService[].class); + return Arrays.asList(arr != null ? arr : new SliceService[0]); } - } catch (HttpClientErrorException.NotFound e) { - throw new RestconfException( - 404, - "application", - "data-missing", - "Network slice service '" + serviceId + "' not found" - ); } catch (HttpClientErrorException e) { throw handleHttpError(e); + } catch (RestconfException e) { + throw e; } catch (Exception e) { - logger.error("Error retrieving network slice service: {}", serviceId, e); - throw new RestconfException("Failed to retrieve network slice service: " + e.getMessage(), e); + throw wrap("list slice services", e); } } @Override - public SliceService updateSliceService(String serviceId, SliceService service) throws RestconfException { - if (serviceId == null || serviceId.isEmpty()) { - throw new RestconfException("Service ID must not be null or empty"); + public void deleteAllSliceServices() throws RestconfException { + String uri = sliceServiceListUri(); + logger.info("Deleting all network slice services"); + try { + restTemplate.exchange(uri, HttpMethod.DELETE, + new HttpEntity<>(buildHeaders()), Void.class); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + throw wrap("delete all slice services", e); } - if (service == null) { - throw new RestconfException("Service must not be null"); + } + + @Override + public SliceService getSliceService(String serviceId) throws RestconfException { + String uri = sliceServiceUri(serviceId); + logger.debug("Retrieving network slice service: {}", serviceId); + try { + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.GET, + new HttpEntity<>(buildHeaders()), + SliceService.class); + assertSuccess(resp, "get slice service " + serviceId); + return resp.getBody(); + } catch (HttpClientErrorException.NotFound e) { + throw notFound("slice service", serviceId); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + throw wrap("get slice service " + serviceId, e); } + } - String uri = buildServiceUri(serviceId); + @Override + public SliceService updateSliceService(String serviceId, SliceService service) + throws RestconfException { + String uri = sliceServiceUri(serviceId); logger.info("Updating network slice service: {}", serviceId); - try { - HttpEntity requestEntity = new HttpEntity<>(service, buildHeaders()); - ResponseEntity response = restTemplate.exchange( - uri, - HttpMethod.PATCH, - requestEntity, - SliceService.class - ); - - if (response.getStatusCode().is2xxSuccessful()) { - logger.info("Successfully updated network slice service: {}", serviceId); - return response.getBody(); - } else { - throw new RestconfException( - response.getStatusCode().value(), - "protocol", - "operation-failed", - "Failed to update network slice service" - ); - } + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.PUT, + new HttpEntity<>(service, buildHeaders()), + SliceService.class); + assertSuccess(resp, "update slice service " + serviceId); + return resp.getBody(); } catch (HttpClientErrorException e) { throw handleHttpError(e); + } catch (RestconfException e) { + throw e; } catch (Exception e) { - logger.error("Error updating network slice service: {}", serviceId, e); - throw new RestconfException("Failed to update network slice service: " + e.getMessage(), e); + throw wrap("update slice service " + serviceId, e); } } @Override public void deleteSliceService(String serviceId) throws RestconfException { - if (serviceId == null || serviceId.isEmpty()) { - throw new RestconfException("Service ID must not be null or empty"); + String uri = sliceServiceUri(serviceId); + logger.info("Deleting network slice service: {}", serviceId); + try { + restTemplate.exchange(uri, HttpMethod.DELETE, + new HttpEntity<>(buildHeaders()), Void.class); + } catch (HttpClientErrorException.NotFound e) { + throw notFound("slice service", serviceId); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + throw wrap("delete slice service " + serviceId, e); } + } - String uri = buildServiceUri(serviceId); - logger.info("Deleting network slice service: {}", serviceId); + // ========================================================================= + // 3. SDP + // ========================================================================= + @Override + public SDP createSdp(String serviceId, SDP sdp) throws RestconfException { + String uri = sdpListUri(serviceId); + logger.info("Creating SDP in slice service: {}", serviceId); try { - ResponseEntity response = restTemplate.exchange( - uri, - HttpMethod.DELETE, + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.POST, + new HttpEntity<>(sdp, buildHeaders()), + SDP.class); + assertSuccess(resp, "create SDP in " + serviceId); + return resp.getBody(); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + throw wrap("create SDP in " + serviceId, e); + } + } + + @Override + public List listSdps(String serviceId) throws RestconfException { + String uri = sdpListUri(serviceId); + logger.debug("Listing SDPs for slice service: {}", serviceId); + try { + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.GET, new HttpEntity<>(buildHeaders()), - Void.class - ); - - if (response.getStatusCode() == HttpStatus.NO_CONTENT || response.getStatusCode() == HttpStatus.OK) { - logger.info("Successfully deleted network slice service: {}", serviceId); - } else { - throw new RestconfException( - response.getStatusCode().value(), - "protocol", - "operation-failed", - "Failed to delete network slice service" - ); - } - } catch (HttpClientErrorException.NotFound e) { - throw new RestconfException( - 404, - "application", - "data-missing", - "Network slice service '" + serviceId + "' not found" - ); + SDP[].class); + assertSuccess(resp, "list SDPs for " + serviceId); + SDP[] body = resp.getBody(); + return Arrays.asList(body != null ? body : new SDP[0]); } catch (HttpClientErrorException e) { throw handleHttpError(e); + } catch (RestconfException e) { + throw e; } catch (Exception e) { - logger.error("Error deleting network slice service: {}", serviceId, e); - throw new RestconfException("Failed to delete network slice service: " + e.getMessage(), e); + throw wrap("list SDPs for " + serviceId, e); } } @Override - public List listSliceServices() throws RestconfException { - String uri = buildServicesListUri(); - logger.debug("Listing all network slice services"); + public void deleteAllSdps(String serviceId) throws RestconfException { + String uri = sdpListUri(serviceId); + logger.info("Deleting all SDPs for slice service: {}", serviceId); + try { + restTemplate.exchange(uri, HttpMethod.DELETE, + new HttpEntity<>(buildHeaders()), Void.class); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + throw wrap("delete all SDPs for " + serviceId, e); + } + } + @Override + public SDP getSdp(String serviceId, String sdpId) throws RestconfException { + String uri = sdpUri(serviceId, sdpId); + logger.debug("Retrieving SDP {} from slice service: {}", sdpId, serviceId); try { - // Get response as string first to handle the wrapped container structure - ResponseEntity response = restTemplate.exchange( - uri, - HttpMethod.GET, + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.GET, new HttpEntity<>(buildHeaders()), - String.class - ); - - if (response.getStatusCode() == HttpStatus.OK) { - String responseBody = response.getBody(); - if (responseBody == null || responseBody.isEmpty()) { - logger.debug("Empty response from list slice services"); - return new ArrayList<>(); - } - - // Use Rfc9543JsonConverter to parse the RFC 9543 response - try { - List services = new Rfc9543JsonConverter() - .parseSliceServices(responseBody); - logger.debug("Successfully retrieved {} network slice services", services.size()); - return services; - } catch (Exception parseError) { - // Fallback: try to parse as direct array if it's not wrapped - logger.debug("RFC 9543 parsing failed, trying direct array deserialization", parseError); - try { - SliceService[] servicesArray = objectMapper.readValue(responseBody, SliceService[].class); - List services = Arrays.asList(servicesArray != null ? servicesArray : new SliceService[0]); - logger.debug("Successfully retrieved {} network slice services (direct array)", services.size()); - return services; - } catch (Exception fallbackError) { - logger.error("Failed to parse slice services response in both formats", fallbackError); - throw new RestconfException("Failed to parse network slice services response: " + fallbackError.getMessage(), fallbackError); - } - } - } else { - throw new RestconfException( - response.getStatusCode().value(), - "protocol", - "operation-failed", - "Failed to list network slice services" - ); - } + SDP.class); + assertSuccess(resp, "get SDP " + sdpId); + return resp.getBody(); + } catch (HttpClientErrorException.NotFound e) { + throw notFound("SDP", sdpId); } catch (HttpClientErrorException e) { throw handleHttpError(e); } catch (RestconfException e) { throw e; } catch (Exception e) { - logger.error("Error listing network slice services", e); - throw new RestconfException("Failed to list network slice services: " + e.getMessage(), e); + throw wrap("get SDP " + sdpId, e); } } @Override - public SliceService checkFeasibility(SliceService service) throws RestconfException { - if (service == null || service.getId() == null) { - throw new RestconfException("Service and service ID must not be null"); + public SDP updateSdp(String serviceId, String sdpId, SDP sdp) throws RestconfException { + String uri = sdpUri(serviceId, sdpId); + logger.info("Updating SDP {} in slice service: {}", sdpId, serviceId); + try { + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.PUT, + new HttpEntity<>(sdp, buildHeaders()), + SDP.class); + assertSuccess(resp, "update SDP " + sdpId); + return resp.getBody(); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + throw wrap("update SDP " + sdpId, e); } - if (!Boolean.TRUE.equals(service.getTestOnly())) { - throw new RestconfException("Service must have test-only flag set to true for feasibility check"); + } + + @Override + public void deleteSdp(String serviceId, String sdpId) throws RestconfException { + String uri = sdpUri(serviceId, sdpId); + logger.info("Deleting SDP {} from slice service: {}", sdpId, serviceId); + try { + restTemplate.exchange(uri, HttpMethod.DELETE, + new HttpEntity<>(buildHeaders()), Void.class); + } catch (HttpClientErrorException.NotFound e) { + throw notFound("SDP", sdpId); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + throw wrap("delete SDP " + sdpId, e); } + } - String uri = buildServiceUri(service.getId()); - logger.info("Checking feasibility of network slice service: {}", service.getId()); + // ========================================================================= + // 4. SLO/SLE Template + // ========================================================================= + @Override + public SloSleTemplate createSloSleTemplate(SloSleTemplate template) throws RestconfException { + String uri = templateListUri(); + logger.info("Creating SLO/SLE template: {}", template != null ? template.getId() : "null"); try { - HttpEntity requestEntity = new HttpEntity<>(service, buildHeaders()); - ResponseEntity response = restTemplate.exchange( - uri, - HttpMethod.PUT, - requestEntity, - SliceService.class - ); - - if (response.getStatusCode().is2xxSuccessful()) { - SliceService resultService = response.getBody(); - String adminState = resultService.getStatus() != null ? - resultService.getStatus().getAdminState() : "unknown"; - - if ("admin-up".equals(adminState)) { - logger.info("Feasibility check PASSED for service: {}", service.getId()); - } else if ("rejected".equals(adminState)) { - String reason = resultService.getStatus() != null ? - resultService.getStatus().getOperState() : "Unknown reason"; - logger.warn("Feasibility check FAILED for service: {} - Reason: {}", service.getId(), reason); - } else { - logger.info("Feasibility check status: {} for service: {}", adminState, service.getId()); - } - - return resultService; - } else { - throw new RestconfException( - response.getStatusCode().value(), - "protocol", - "operation-failed", - "Failed to check feasibility of network slice service" - ); - } + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.POST, + new HttpEntity<>(template, buildHeaders()), + SloSleTemplate.class); + assertSuccess(resp, "create SLO/SLE template"); + return resp.getBody(); } catch (HttpClientErrorException e) { throw handleHttpError(e); + } catch (RestconfException e) { + throw e; } catch (Exception e) { - logger.error("Error checking feasibility of network slice service: {}", service.getId(), e); - throw new RestconfException("Failed to check feasibility: " + e.getMessage(), e); + throw wrap("create SLO/SLE template", e); } } @Override - public String getServiceStatus(String serviceId) throws RestconfException { - if (serviceId == null || serviceId.isEmpty()) { - throw new RestconfException("Service ID must not be null or empty"); + public String getSloSleTemplates() throws RestconfException { + String uri = templateListUri(); + logger.debug("Retrieving all SLO/SLE templates"); + try { + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.GET, + new HttpEntity<>(buildHeaders()), + String.class); + assertSuccess(resp, "get SLO/SLE templates"); + String body = resp.getBody(); + return (body == null || body.isBlank()) ? "{}" : body; + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (RestconfException e) { + throw e; + } catch (Exception e) { + throw wrap("get SLO/SLE templates", e); } + } - String uri = buildServiceStatusUri(serviceId); - logger.debug("Retrieving status of network slice service: {}", serviceId); + @Override + public void deleteAllSloSleTemplates() throws RestconfException { + String uri = templateListUri(); + logger.info("Deleting all SLO/SLE templates"); + try { + restTemplate.exchange(uri, HttpMethod.DELETE, + new HttpEntity<>(buildHeaders()), Void.class); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + throw wrap("delete all SLO/SLE templates", e); + } + } + @Override + public SloSleTemplate getSloSleTemplate(String templateId) throws RestconfException { + String uri = templateUri(templateId); + logger.debug("Retrieving SLO/SLE template: {}", templateId); try { - ResponseEntity response = restTemplate.exchange( - uri, - HttpMethod.GET, + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.GET, new HttpEntity<>(buildHeaders()), - String.class - ); - - if (response.getStatusCode() == HttpStatus.OK) { - logger.debug("Successfully retrieved status for service: {}", serviceId); - return response.getBody(); - } else { - throw new RestconfException( - response.getStatusCode().value(), - "protocol", - "operation-failed", - "Failed to retrieve service status" - ); - } + SloSleTemplate.class); + assertSuccess(resp, "get SLO/SLE template " + templateId); + return resp.getBody(); + } catch (HttpClientErrorException.NotFound e) { + throw notFound("SLO/SLE template", templateId); } catch (HttpClientErrorException e) { throw handleHttpError(e); + } catch (RestconfException e) { + throw e; } catch (Exception e) { - logger.error("Error retrieving service status: {}", serviceId, e); - throw new RestconfException("Failed to retrieve service status: " + e.getMessage(), e); + throw wrap("get SLO/SLE template " + templateId, e); } } @Override - public String getSloSleTemplates() throws RestconfException { - String uri = buildSloSleTemplatesUri(); - logger.debug("Retrieving SLO/SLE templates from provider"); - + public SloSleTemplate updateSloSleTemplate(String templateId, SloSleTemplate template) + throws RestconfException { + String uri = templateUri(templateId); + logger.info("Updating SLO/SLE template: {}", templateId); try { - ResponseEntity response = restTemplate.exchange( - uri, - HttpMethod.GET, - new HttpEntity<>(buildHeaders()), - String.class - ); - - if (response.getStatusCode() == HttpStatus.OK) { - String responseBody = response.getBody(); - if (responseBody == null || responseBody.isEmpty()) { - logger.debug("Empty response from SLO/SLE templates endpoint"); - return "{}"; - } - logger.debug("Successfully retrieved SLO/SLE templates from provider"); - return responseBody; - } else { - throw new RestconfException( - response.getStatusCode().value(), - "protocol", - "operation-failed", - "Failed to retrieve SLO/SLE templates" - ); - } + ResponseEntity resp = restTemplate.exchange( + uri, HttpMethod.PUT, + new HttpEntity<>(template, buildHeaders()), + SloSleTemplate.class); + assertSuccess(resp, "update SLO/SLE template " + templateId); + return resp.getBody(); } catch (HttpClientErrorException e) { throw handleHttpError(e); } catch (RestconfException e) { throw e; } catch (Exception e) { - logger.error("Error retrieving SLO/SLE templates", e); - throw new RestconfException("Failed to retrieve SLO/SLE templates: " + e.getMessage(), e); + throw wrap("update SLO/SLE template " + templateId, e); } } - // Private helper methods - - /** - * Builds the URI for a specific slice service resource. - */ - private String buildServiceUriPost(String serviceId) { - String encodedId = encodeUrlComponent(serviceId); - return String.format("%s%s/%s:network-slice-services/slice-service", - providerUrl, RESTCONF_PATH, YANG_MODULE); + @Override + public void deleteSloSleTemplate(String templateId) throws RestconfException { + String uri = templateUri(templateId); + logger.info("Deleting SLO/SLE template: {}", templateId); + try { + restTemplate.exchange(uri, HttpMethod.DELETE, + new HttpEntity<>(buildHeaders()), Void.class); + } catch (HttpClientErrorException.NotFound e) { + throw notFound("SLO/SLE template", templateId); + } catch (HttpClientErrorException e) { + throw handleHttpError(e); + } catch (Exception e) { + throw wrap("delete SLO/SLE template " + templateId, e); + } + } + + // ========================================================================= + // URI helpers + // ========================================================================= + + private String nssUri() { + return providerUrl + NSS_PATH; } - /** - * Builds the URI for a specific slice service resource. - */ - private String buildServiceUri(String serviceId) { - String encodedId = encodeUrlComponent(serviceId); - return String.format("%s%s/%s:network-slice-services/slice-service=%s", - providerUrl, RESTCONF_PATH, YANG_MODULE, encodedId); + private String sliceServiceListUri() { + return nssUri() + "/slice-service"; } - /** - * Builds the URI for listing all slice services. - */ - private String buildServicesListUri() { - return String.format("%s%s/%s:network-slice-services/slice-service", - providerUrl, RESTCONF_PATH, YANG_MODULE); + private String sliceServiceUri(String serviceId) { + return nssUri() + "/slice-service=" + encode(serviceId); } - /** - * Builds the URI for retrieving service status. - */ - private String buildServiceStatusUri(String serviceId) { - String encodedId = encodeUrlComponent(serviceId); - return String.format("%s%s/%s:network-slice-services/slice-service=%s/status", - providerUrl, RESTCONF_PATH, YANG_MODULE, encodedId); + private String sdpListUri(String serviceId) { + return sliceServiceUri(serviceId) + "/sdps"; } - /** - * Builds the URI for retrieving SLO/SLE templates. - */ - private String buildSloSleTemplatesUri() { - return String.format("%s%s/%s:network-slice-services/slo-sle-templates", - providerUrl, RESTCONF_PATH, YANG_MODULE); + private String sdpUri(String serviceId, String sdpId) { + return sdpListUri(serviceId) + "/sdp=" + encode(sdpId); } - /** - * Encodes special characters in URL components. - */ - private String encodeUrlComponent(String component) { - return component.replaceAll(" ", "%20") - .replaceAll("/", "%2F") - .replaceAll(":", "%3A"); + private String templateListUri() { + return nssUri() + "/slo-sle-templates/slo-sle-template"; } - /** - * Builds HTTP headers for RESTCONF requests with HTTP Basic Authentication. - */ + private String templateUri(String templateId) { + return nssUri() + "/slo-sle-templates/slo-sle-template=" + encode(templateId); + } + + private String encode(String value) { + return value.replaceAll(" ", "%20").replaceAll("/", "%2F").replaceAll(":", "%3A"); + } + + // ========================================================================= + // HTTP helpers + // ========================================================================= + private HttpHeaders buildHeaders() { HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); headers.set("Accept", "application/yang-data+json"); headers.set("Content-Type", "application/yang-data+json"); - - // Add HTTP Basic Authentication if configured if ("basic".equalsIgnoreCase(authMethod) && authUsername != null && !authUsername.isEmpty()) { - String credentials = authUsername + ":" + authPassword; - String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes()); - headers.set("Authorization", "Basic " + encodedCredentials); - logger.debug("HTTP Basic Authentication configured for user: {}", authUsername); + String creds = Base64.getEncoder().encodeToString( + (authUsername + ":" + authPassword).getBytes()); + headers.set("Authorization", "Basic " + creds); } - return headers; } - /** - * Handles HTTP client errors and converts them to RestconfException. - */ - private RestconfException handleHttpError(HttpClientErrorException e) { - int status = e.getStatusCode().value(); - String message = e.getMessage(); - - String errorTag; - switch (status) { - case 400: - errorTag = "invalid-value"; - break; - case 401: - errorTag = "access-denied"; - break; - case 403: - errorTag = "access-denied"; - break; - case 404: - errorTag = "data-missing"; - break; - case 409: - errorTag = "data-exists"; - break; - default: - errorTag = "operation-failed"; + private void assertSuccess(ResponseEntity resp, String operation) throws RestconfException { + if (!resp.getStatusCode().is2xxSuccessful()) { + throw new RestconfException( + resp.getStatusCode().value(), "protocol", "operation-failed", + "Failed to " + operation); } + } - return new RestconfException(status, "application", errorTag, message); + private RestconfException notFound(String resource, String id) { + return new RestconfException(404, "application", "data-missing", + resource + " '" + id + "' not found"); + } + + private RestconfException wrap(String operation, Exception e) { + logger.error("Error during: {}", operation, e); + return new RestconfException("Failed to " + operation + ": " + e.getMessage(), e); + } + + private RestconfException handleHttpError(HttpClientErrorException e) { + int status = e.getStatusCode().value(); + String tag = switch (status) { + case 400 -> "invalid-value"; + case 401, 403 -> "access-denied"; + case 404 -> "data-missing"; + case 409 -> "data-exists"; + default -> "operation-failed"; + }; + return new RestconfException(status, "application", tag, e.getMessage()); } } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java index 76558ac..c3d9fef 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java @@ -1,5 +1,6 @@ package org.etsi.osl.controllers.ietf.ns.api.restconf; +import com.fasterxml.jackson.databind.ObjectMapper; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; @@ -36,6 +37,9 @@ public class RestconfConsumerService { @Autowired private RestconfClient restconfClient; + @Autowired + private ObjectMapper objectMapper; + /** * Provisions new network slice services on the provider. * @@ -55,8 +59,12 @@ public class RestconfConsumerService { service.getStatus().setLastChange(OffsetDateTime.now()); } - // Create on provider (sends array in RFC 9543 format) - List createdServices = restconfClient.createSliceService(services); + // POST /slice-service accepts one service at a time per the swagger spec + List createdServices = new ArrayList<>(); + for (SliceService service : services) { + SliceService created = restconfClient.createSliceService(service); + createdServices.add(created != null ? created : service); + } logger.info("Successfully provisioned {} service(s)", createdServices.size()); return createdServices; @@ -93,11 +101,11 @@ public class RestconfConsumerService { // Validate service configuration validateSliceServiceConfiguration(service); - // Set test-only flag + // Set test-only flag — RFC 9543 §4.4: same POST /slice-service path, + // provider validates without reserving resources when test-only=true service.setTestOnly(true); - // Check feasibility on provider - SliceService feasibilityResult = restconfClient.checkFeasibility(service); + SliceService feasibilityResult = restconfClient.createSliceService(service); String adminState = feasibilityResult.getStatus().getAdminState(); if ("admin-up".equals(adminState)) { @@ -186,7 +194,12 @@ public class RestconfConsumerService { */ public String getServiceStatus(String serviceId) throws RestconfException { logger.debug("Retrieving status of service: {}", serviceId); - return restconfClient.getServiceStatus(serviceId); + SliceService service = restconfClient.getSliceService(serviceId); + try { + return objectMapper.writeValueAsString(service.getStatus()); + } catch (Exception e) { + throw new RestconfException("Failed to serialize service status: " + e.getMessage(), e); + } } /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceSpecMappable.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceSpecMappable.java new file mode 100644 index 0000000..7948473 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceSpecMappable.java @@ -0,0 +1,191 @@ +package org.etsi.osl.controllers.ietf.ns.domain.common; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; + +/** + * Interface for entities that can be mapped to/from TMF634 LogicalResourceSpecification instances. + * Provides a contract for both forward (entity → resourceSpec) and reverse (resourceSpec → entity) mapping operations. + * + * This interface is for CATALOG/DESIGN-TIME entities (controllers, operator definitions, etc.) + * that define types and specifications, NOT runtime instances. + * + * Contrast with {@link LogicalResourceMappable} which is for runtime/inventory entities (TMF639). + * + * Implementations should: + * 1. Provide entity ID, name, and description via abstract methods + * 2. Define a VERSION constant for schema versioning + * 3. Be annotated with @ExcludeFromEntityToLogicalResourceCharacteristicsMapping for fields not meant for mapping + * 4. Override getFieldMappings() to declare characteristic-to-property mappings + * 5. Override hasRelationships() and getRelationshipFields() if entity has relationships + * + * Usage: + *
+ * // Forward mapping: entity → resourceSpec
+ * LogicalResourceSpecMappable entity = ...;
+ * LogicalResourceSpecification spec = forwardMapper.toLogicalResourceSpec(entity);
+ *
+ * // Reverse mapping: resourceSpec → entity
+ * LogicalResourceSpecification spec = ...;
+ * MyEntity entity = reverseMapper.fromLogicalResourceSpec(spec, MyEntity.class);
+ * 
+ */ +public interface LogicalResourceSpecMappable { + + /** + * Get the entity type name for resource specification category. + * Default implementation returns the class simple name. + * Can be overridden for custom naming. + * + * Examples: "GenericController", "KubernetesOperator", "ControllerDescriptor" + * + * @return Entity type name + */ + default String getEntityTypeName() { + return this.getClass().getSimpleName(); + } + + /** + * Get the unique identifier for this entity. + * Used as LogicalResourceSpecification.uuid. + * + * @return Entity ID (UUID string) + */ + String getEntityId(); + + /** + * Get the display name for this entity. + * Used as LogicalResourceSpecification.name. + * + * @return Entity name + */ + String getEntityName(); + + /** + * Get the description for this entity. + * Used as LogicalResourceSpecification.description. + * Can return null or empty string - mapper handles default descriptions. + * + * @return Entity description or null + */ + String getEntityDescription(); + + /** + * Get the version constant for this entity type. + * Used in resource specification versioning. + * + * Default implementation uses reflection to retrieve the VERSION field. + * Can be overridden for performance optimization. + * + * @return Version string (e.g., "0.0.1") + */ + default String getVersion() { + try { + Class clazz = this.getClass(); + // For subclasses, check superclass hierarchy + while (clazz != null && !clazz.equals(Object.class)) { + try { + Field versionField = clazz.getDeclaredField("VERSION"); + if (versionField != null) { + versionField.setAccessible(true); + Object version = versionField.get(null); + return version != null ? version.toString() : "0.0.1"; + } + } catch (NoSuchFieldException e) { + // Try superclass + clazz = clazz.getSuperclass(); + } + } + } catch (Exception e) { + // Silently fall back to default + } + return "0.0.1"; + } + + // ========== Reverse Mapping Support (LogicalResourceSpecification → Entity) ========== + + /** + * Map fields from a TMF634 LogicalResourceSpecification back to this entity instance. + * Override in entities to populate fields from a LogicalResourceSpecification during reverse mapping. + * Default implementation does nothing - only entities with reverse mapping should override. + * + * Implementations should extract characteristics and relationships from the specification + * and populate the corresponding entity fields. + * + * Called during deserialization: LogicalResourceSpecification → Entity + * + * @param spec LogicalResourceSpecification containing characteristics and relationships + * @param characteristicMap Map of characteristic names to values (helper for field extraction) + */ + @JsonIgnore + default void mapFromLogicalResourceSpec(LogicalResourceSpecification spec, Map characteristicMap) { + // Default: no-op. Override in entities with reverse mapping. + } + + /** + * Get field mappings for reverse mapping (LogicalResourceSpecification characteristics → entity properties). + * Maps TMF characteristic field names to entity property names and types. + * + * Format: Maps characteristic field names (e.g., "apiGroup", "apiVersion", "kind") to entity property names. + * + * Example for KubernetesOperator: + *
+     * {
+     *   "apiGroup" → "apiGroup",
+     *   "apiVersion" → "apiVersion",
+     *   "kind" → "kind",
+     *   "plural" → "plural",
+     *   "singular" → "singular",
+     *   ...
+     * }
+     * 
+ * + * Override in concrete entity classes to provide type-specific field mappings. + * Subclasses should call super.getFieldMappings() and add their own fields. + * + * @return Map of characteristic field names to entity property names (empty by default) + */ + @JsonIgnore + default Map getFieldMappings() { + return Map.of(); + } + + /** + * Check if this entity has relationships that need extraction from LogicalResourceSpecification. + * Override in entities that have entity references (e.g., GenericController has kubernetesOperators). + * + * Examples: + * - GenericController: has "kubernetesOperators" relationship + * - KubernetesOperator: has "controller" relationship + * + * @return true if entity has relationships to extract from LogicalResourceSpecification + */ + @JsonIgnore + default boolean hasRelationships() { + return false; + } + + /** + * Get the relationship field names for this entity. + * Used during reverse mapping to extract and populate entity references from LogicalResourceSpecification. + * + * Format: List of relationship field names as defined in entity class. + * + * Examples: + * - GenericController: List.of("kubernetesOperators") + * - KubernetesOperator: List.of("controller") + * + * Only override if hasRelationships() returns true. + * Should return the relationship field names as they appear in the entity class. + * + * @return List of relationship field names (empty by default) + */ + @JsonIgnore + default List getRelationshipFields() { + return List.of(); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java index 3194c49..746049a 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java @@ -12,6 +12,7 @@ import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.PathConstraints import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SlePolicy; import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceMappable; +import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceSpecMappable; import org.etsi.osl.tmf.common.model.Any; import org.etsi.osl.tmf.pm628.model.AdministrativeState; import org.etsi.osl.tmf.rcm634.model.LogicalResourceSpecification; @@ -64,29 +65,34 @@ public class EntityToLogicalResourceSpecMapper { /** * Transform an entity to LogicalResourceSpecification (generic method). - * Works with all entity types: Site subclasses, Region, RANSite, PhysicalDevice and other subclasses. * * @param Entity type extending LogicalResourceMappable * @param entity Entity to convert * @return LogicalResourceSpecification representing the entity */ - public - LogicalResourceSpecification toLogicalResourceSpec(T entity) { - log.debug("Mapping {} to LogicalResourceSpecification", entity.getEntityTypeName()); + public LogicalResourceSpecification toLogicalResourceSpec(Object entity) { + // Validate that entity implements one of the mappable interfaces + if (!(entity instanceof LogicalResourceMappable) && !(entity instanceof LogicalResourceSpecMappable)) { + throw new IllegalArgumentException("Entity must implement LogicalResourceMappable or LogicalResourceSpecMappable"); + } + + String entityTypeName = getEntityTypeName(entity); + String entityName = getEntityName(entity); + log.debug("Mapping {} to LogicalResourceSpecification", entityName); LogicalResourceSpecification spec = new LogicalResourceSpecification(); // Set basic attributes using interface methods - spec.setName(entity.getEntityTypeName() ); + spec.setName(entityName); spec.setCategory(categoryConfig.getCategoryForSpecifications()); - spec.setVersion(entity.getVersion()); + spec.setVersion(getVersion(entity)); // Use the raw description field value (may be null), not the interface method which provides defaults - spec.setDescription( entity.getEntityDescription()); + spec.setDescription( getEntityDescription( entity)); spec.setLifecycleStatus("Active"); // Set ID if entity has one - String entityId = entity.getEntityId(); + String entityId = getEntityId(entity); if (entityId != null) { spec.setUuid(entityId); } @@ -97,6 +103,12 @@ public class EntityToLogicalResourceSpecMapper { return spec; } + + + + + + /** * Convert all fields of an entity to characteristics and relationships @@ -450,4 +462,68 @@ public class EntityToLogicalResourceSpecMapper { } } + // ========== Helper Methods for Common Interface Methods ========== + + /** + * Get entity type name from either interface + */ + private String getEntityTypeName(Object entity) { + if (entity instanceof LogicalResourceMappable) { + return ((LogicalResourceMappable) entity).getEntityTypeName(); + } else if (entity instanceof LogicalResourceSpecMappable) { + return ((LogicalResourceSpecMappable) entity).getEntityTypeName(); + } + return entity.getClass().getSimpleName(); + } + + /** + * Get entity ID from either interface + */ + private String getEntityId(Object entity) { + if (entity instanceof LogicalResourceMappable) { + return ((LogicalResourceMappable) entity).getEntityId(); + } else if (entity instanceof LogicalResourceSpecMappable) { + return ((LogicalResourceSpecMappable) entity).getEntityId(); + } + return null; + } + + /** + * Get entity description from either interface + */ + private String getEntityDescription(Object entity) { + if (entity instanceof LogicalResourceMappable) { + return ((LogicalResourceMappable) entity).getEntityDescription(); + } else if (entity instanceof LogicalResourceSpecMappable) { + return ((LogicalResourceSpecMappable) entity).getEntityDescription(); + } + return ""; + } + + /** + * Get version from either interface + */ + private String getVersion(Object entity) { + if (entity instanceof LogicalResourceMappable) { + return ((LogicalResourceMappable) entity).getVersion(); + } else if (entity instanceof LogicalResourceSpecMappable) { + return ((LogicalResourceSpecMappable) entity).getVersion(); + } + return "0.0.1"; + } + + /** + * Get entity name from either interface + * Returns the actual name field value from the entity + */ + private String getEntityName(Object entity) { + if (entity instanceof LogicalResourceMappable) { + return ((LogicalResourceMappable) entity).getEntityName(); + } else if (entity instanceof LogicalResourceSpecMappable) { + return ((LogicalResourceSpecMappable) entity).getEntityName(); + } + return ""; + } + } + diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/LogicalResourceToEntityMapper.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/LogicalResourceToEntityMapper.java index d5ee3d0..007f088 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/LogicalResourceToEntityMapper.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/LogicalResourceToEntityMapper.java @@ -47,7 +47,7 @@ public class LogicalResourceToEntityMapper { // Registry mapping entity type names to their classes private static final Map> ENTITY_REGISTRY = Map.ofEntries( - Map.entry("SloSleTemplate", SloSleTemplate.class) + //Map.entry("SloSleTemplate", SloSleTemplate.class) ); /** diff --git a/src/main/resources/ietf_green_request.json b/src/main/resources/ietf_green_request.json deleted file mode 100644 index 9430f28..0000000 --- a/src/main/resources/ietf_green_request.json +++ /dev/null @@ -1,172 +0,0 @@ -{ - "ietf-network-slice-service:network-slice-services": { - "slo-sle-templates": { - "slo-sle-template": [ - { - "id": "B", - "description": "", - "slo-policy": { - "metric-bound": [ - { - "metric-type": "energy_consumption", - "metric-unit": "kWh", - "bound": 20200 - }, - { - "metric-type": "energy_efficiency", - "metric-unit": "Wats/bps", - "bound": 6 - }, - { - "metric-type": "carbon_emission", - "metric-unit": "grams of CO2 per kWh", - "bound": 750 - }, - { - "metric-type": "renewable_energy_usage", - "metric-unit": "rate", - "bound": 0.5 - } - ] - }, - "sle-policy": { - "security": "", - "isolation": "", - "path-constraints": { - "service-functions": "", - "diversity": { - "diversity": { - "diversity-type": "" - } - } - } - } - } - ] - }, - "slice-service": [ - { - "id": "slice-service-88a585f7-a432-4312-8774-6210fb0b2342", - "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", - "service-tags": { - "tag-type": [ - { - "tag-type": "service", - "tag-type-value": [ - "L2" - ] - } - ] - }, - "slo-sle-policy": { - "slo-sle-template": "B" - }, - "status": {}, - "sdps": { - "sdp": [ - { - "id": "CU-N32", - "geo-location": "", - "node-id": "A", - "sdp-ip-address": "10.60.11.3", - "tp-ref": "", - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": "VLAN", - "value": "101", - "target-connection-group-id": "A_B" - } - ] - }, - "incoming-qos-policy": "", - "outgoing-qos-policy": "", - "sdp-peering": { - "peer-sap-id": "", - "protocols": "" - }, - "ac-svc-ref": [], - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "100", - "ac-ipv4-address": "10.60.11.3", - "ac-ipv4-prefix-length": 0, - "sdp-peering": { - "peer-sap-id": "4.4.4.4" - }, - "status": {} - } - ] - }, - "status": {}, - "sdp-monitoring": "" - }, - { - "id": "UPF-N32", - "geo-location": "", - "node-id": "B", - "sdp-ip-address": "10.60.10.6", - "tp-ref": "", - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": "VLAN", - "value": "101", - "target-connection-group-id": "A_B" - } - ] - }, - "incoming-qos-policy": "", - "outgoing-qos-policy": "", - "sdp-peering": { - "peer-sap-id": "", - "protocols": "" - }, - "ac-svc-ref": [], - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "200", - "ac-ipv4-address": "10.60.10.6", - "ac-ipv4-prefix-length": 0, - "sdp-peering": { - "peer-sap-id": "5.5.5.5" - }, - "status": {} - } - ] - }, - "status": {}, - "sdp-monitoring": "" - } - ] - }, - "connection-groups": { - "connection-group": [ - { - "id": "A_B", - "connectivity-type": "ietf-vpn-common:any-to-any", - "connectivity-construct": [ - { - "id": 1, - "a2a-sdp": [ - { - "sdp-id": "CU-N32" - }, - { - "sdp-id": "UPF-N32" - } - ] - } - ], - "status": {} - } - ] - } - } - ] - } -} \ No newline at end of file diff --git a/src/main/resources/slice_request_backhaul_control.json b/src/main/resources/slice_request_backhaul_control.json deleted file mode 100644 index f215078..0000000 --- a/src/main/resources/slice_request_backhaul_control.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "ietf-network-slice-service:network-slice-services": { - "slo-sle-templates": { - "slo-sle-template": [ - { - "id": "A", - "description": "", - "slo-policy": { - "metric-bound": [ - { - "metric-type": "one-way-bandwidth", - "metric-unit": "kbps", - "bound": 2000 - }, - { - "metric-type": "one-way-delay-maximum", - "metric-unit": "milliseconds", - "bound": 5 - } - ] - }, - "sle-policy": { - "security": "", - "isolation": "", - "path-constraints": { - "service-functions": "", - "diversity": { - "diversity": { - "diversity-type": "" - } - } - } - } - } - ] - }, - "slice-service": [ - { - "id": "slice-service-11327140-7361-41b3-aa45-e84a7fb40be9", - "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", - "service-tags": { - "tag-type": [ - { - "tag-type": "service", - "tag-type-value": [ - "L2" - ] - } - ] - }, - "slo-sle-policy": { - "slo-sle-template": "A" - }, - "status": {}, - "sdps": { - "sdp": [ - { - "id": "", - "geo-location": "", - "node-id": "CU-N2", - "sdp-ip-address": "10.60.11.3", - "tp-ref": "", - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": "VLAN", - "value": "100", - "target-connection-group-id": "CU-N2_AMF-N2" - } - ] - }, - "incoming-qos-policy": "", - "outgoing-qos-policy": "", - "sdp-peering": { - "peer-sap-id": "", - "protocols": "" - }, - "ac-svc-ref": [], - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "100", - "ac-ipv4-address": "10.60.11.3", - "ac-ipv4-prefix-length": 0, - "sdp-peering": { - "peer-sap-id": "1.1.1.1" - }, - "status": {} - } - ] - }, - "status": {}, - "sdp-monitoring": "" - }, - { - "id": "", - "geo-location": "", - "node-id": "AMF-N2", - "sdp-ip-address": "10.60.60.105", - "tp-ref": "", - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": "VLAN", - "value": "100", - "target-connection-group-id": "CU-N2_AMF-N2" - } - ] - }, - "incoming-qos-policy": "", - "outgoing-qos-policy": "", - "sdp-peering": { - "peer-sap-id": "", - "protocols": "" - }, - "ac-svc-ref": [], - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "200", - "ac-ipv4-address": "10.60.60.105", - "ac-ipv4-prefix-length": 0, - "sdp-peering": { - "peer-sap-id": "3.3.3.3" - }, - "status": {} - } - ] - }, - "status": {}, - "sdp-monitoring": "" - } - ] - }, - "connection-groups": { - "connection-group": [ - { - "id": "CU-N2_AMF-N2", - "connectivity-type": "ietf-vpn-common:any-to-any", - "connectivity-construct": [ - { - "id": 1, - "a2a-sdp": [ - { - "sdp-id": "01" - }, - { - "sdp-id": "02" - } - ] - } - ], - "status": {} - } - ] - } - } - ] - } - } \ No newline at end of file diff --git a/src/main/resources/slice_request_backhaul_user.json b/src/main/resources/slice_request_backhaul_user.json deleted file mode 100644 index efd1666..0000000 --- a/src/main/resources/slice_request_backhaul_user.json +++ /dev/null @@ -1,164 +0,0 @@ -[ - { - "ietf-network-slice-service:network-slice-services": { - "slo-sle-templates": { - "slo-sle-template": [ - { - "id": "C", - "description": "", - "slo-policy": { - "metric-bound": [ - { - "metric-type": "one-way-bandwidth", - "metric-unit": "kbps", - "bound": 100 - }, - { - "metric-type": "one-way-delay-maximum", - "metric-unit": "milliseconds", - "bound": 10 - } - ] - }, - "sle-policy": { - "security": "", - "isolation": "", - "path-constraints": { - "service-functions": "", - "diversity": { - "diversity": { - "diversity-type": "" - } - } - } - } - } - ] - }, - "slice-service": [ - { - "id": "slice-service-181e303a-a051-42e5-b2f2-4060732c631f", - "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", - "service-tags": { - "tag-type": [ - { - "tag-type": "", - "tag-type-value": [ - "" - ] - } - ] - }, - "slo-sle-policy": { - "slo-sle-template": "C" - }, - "status": {}, - "sdps": { - "sdp": [ - { - "id": "", - "geo-location": "", - "node-id": "CU-N31", - "sdp-ip-address": "10.60.11.3", - "tp-ref": "", - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": "VLAN", - "value": "102", - "target-connection-group-id": "CU-N31_UPF-N31" - } - ] - }, - "incoming-qos-policy": "", - "outgoing-qos-policy": "", - "sdp-peering": { - "peer-sap-id": "", - "protocols": "" - }, - "ac-svc-ref": [], - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "100", - "ac-ipv4-address": "10.60.11.3", - "ac-ipv4-prefix-length": 0, - "sdp-peering": { - "peer-sap-id": "4.4.4.4" - }, - "status": {} - } - ] - }, - "status": {}, - "sdp-monitoring": "" - }, - { - "id": "", - "geo-location": "", - "node-id": "UPF-N31", - "sdp-ip-address": "10.60.60.106", - "tp-ref": "", - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": "VLAN", - "value": "102", - "target-connection-group-id": "CU-N31_UPF-N31" - } - ] - }, - "incoming-qos-policy": "", - "outgoing-qos-policy": "", - "sdp-peering": { - "peer-sap-id": "", - "protocols": "" - }, - "ac-svc-ref": [], - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "200", - "ac-ipv4-address": "10.60.60.106", - "ac-ipv4-prefix-length": 0, - "sdp-peering": { - "peer-sap-id": "5.5.5.5" - }, - "status": {} - } - ] - }, - "status": {}, - "sdp-monitoring": "" - } - ] - }, - "connection-groups": { - "connection-group": [ - { - "id": "CU-N31_UPF-N31", - "connectivity-type": "ietf-vpn-common:any-to-any", - "connectivity-construct": [ - { - "id": 1, - "a2a-sdp": [ - { - "sdp-id": "01" - }, - { - "sdp-id": "02" - } - ] - } - ], - "status": {} - } - ] - } - } - ] - } - } -] \ No newline at end of file -- GitLab From 1ff3a05a88381c32c38094779a0b6015d166d12b Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 14 May 2026 00:53:19 +0300 Subject: [PATCH 03/12] fix specs and bootstrap --- ...ResourceSpecificationTemplateRegistry.java | 9 + .../api/SloSleTemplateBootstrapService.java | 8 +- .../domain/model/NetworkSliceServices.java | 5 - .../ns/api/restconf/RestconfClientImpl.java | 11 +- .../api/restconf/RestconfConsumerService.java | 27 ++ .../ns/api/restconf/Rfc9543JsonConverter.java | 9 +- .../EntityToLogicalResourceSpecMapper.java | 2 +- .../repository/impl/ResourceRepoService.java | 419 ++++++++++-------- 8 files changed, 302 insertions(+), 188 deletions(-) diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/ResourceSpecificationTemplateRegistry.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/ResourceSpecificationTemplateRegistry.java index c110720..637b64f 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/ResourceSpecificationTemplateRegistry.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/ResourceSpecificationTemplateRegistry.java @@ -20,6 +20,15 @@ import java.util.concurrent.ConcurrentHashMap; @Slf4j public class ResourceSpecificationTemplateRegistry { + /** Spec name for the IETFSloSleTemplateSpec resource specification. */ + public static final String SPEC_SLO_SLE_TEMPLATE = "IETFSloSleTemplateSpec"; + + /** Spec name for the IETFSliceServiceSpec resource specification. */ + public static final String SPEC_SLICE_SERVICE = "IETFSliceServiceSpec"; + + /** Spec name for the IETFNetworkSliceServicesSpec resource specification. */ + public static final String SPEC_NETWORK_SLICE_SERVICES = "IETFNetworkSliceServicesSpec"; + private final Map templateIds = new ConcurrentHashMap<>(); /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java index 94cb846..278b53c 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java @@ -173,7 +173,7 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { // Convert fields to ResourceSpecificationCharacteristic entries via mapper LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); - spec.setName("IETFSloSleTemplateSpec"); + spec.setName(ResourceSpecificationTemplateRegistry.SPEC_SLO_SLE_TEMPLATE); spec.setCategory(categoryConfig.getCategoryForSpecifications()); spec.setVersion(categoryConfig.getVersion()); spec.setDescription( @@ -216,7 +216,7 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { log.info("Creating NetworkSliceService resource specification"); LogicalResourceSpecification spec = new LogicalResourceSpecification(); - spec.setName("IETFSliceServiceSpec"); + spec.setName(ResourceSpecificationTemplateRegistry.SPEC_SLICE_SERVICE); spec.setCategory(categoryConfig.getCategoryForSpecifications()); spec.setVersion(categoryConfig.getVersion()); spec.setDescription( @@ -266,7 +266,7 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { log.info("Creating NetworkSliceServices resource specification"); LogicalResourceSpecification spec = new LogicalResourceSpecification(); - spec.setName("IETFNetworkSliceServicesSpec"); + spec.setName(ResourceSpecificationTemplateRegistry.SPEC_NETWORK_SLICE_SERVICES); spec.setCategory(categoryConfig.getCategoryForSpecifications()); spec.setVersion(categoryConfig.getVersion()); spec.setDescription( @@ -624,8 +624,6 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { // Build and store the RFC 9543 formatted JSON request (slice-service array) Map> sliceServiceWrapper = new HashMap<>(); sliceServiceWrapper.put("slice-service", sliceServices); - String jsonRequest = mapper.writeValueAsString(sliceServiceWrapper); - networkSliceServices.setJsonRequest(jsonRequest); log.info("Successfully created NetworkSliceServices with {} service(s)", sliceServices.size()); return networkSliceServices; diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java index 15eb7e5..c329796 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java @@ -24,11 +24,6 @@ import org.etsi.osl.tmf.ri639.model.ResourceStatusType; @AllArgsConstructor public class NetworkSliceServices implements LogicalResourceMappable { - /** - * RFC 9543 JSON request containing the slice-service array. - * Format: { "slice-service": [ {...}, {...} ] } - */ - private String jsonRequest; // 1-to-many relationship: contains multiple SLO/SLE templates private List sloSleTemplates = new ArrayList<>(); diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java index cdddd82..c55ade7 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java @@ -72,8 +72,15 @@ public class RestconfClientImpl implements RestconfClient { public NetworkSliceServices createNetworkSliceServices(NetworkSliceServices networkSliceServices) throws RestconfException { String uri = nssUri(); - logger.info("Creating network-slice-services container"); + logger.info("Creating network-slice-services container at {}", uri); try { + // Serialize payload for debugging — visible when log level is DEBUG + if (logger.isDebugEnabled()) { + String payloadJson = objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(networkSliceServices); + logger.debug("POST {} — request payload:\n{}", uri, payloadJson); + } + ResponseEntity resp = restTemplate.exchange( uri, HttpMethod.POST, new HttpEntity<>(networkSliceServices, buildHeaders()), @@ -81,6 +88,8 @@ public class RestconfClientImpl implements RestconfClient { assertSuccess(resp, "create network-slice-services container"); return resp.getBody(); } catch (HttpClientErrorException e) { + logger.error("POST {} — HTTP {} response body:\n{}", uri, + e.getStatusCode().value(), e.getResponseBodyAsString()); throw handleHttpError(e); } catch (RestconfException e) { throw e; diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java index c3d9fef..68cf1ac 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java @@ -174,6 +174,33 @@ public class RestconfConsumerService { logger.info("Successfully decommissioned service: {}", serviceId); } + /** + * Provisions the entire RFC 9543 network-slice-services container on the provider. + * + * @param networkSliceServices container with slo-sle-templates and slice-services + * @return the provisioned container as returned by the provider + * @throws RestconfException if provisioning fails + */ + public NetworkSliceServices provisionNetworkSliceServices(NetworkSliceServices networkSliceServices) + throws RestconfException { + logger.info("Provisioning NetworkSliceServices container ({} template(s), {} service(s))", + networkSliceServices.getSloSleTemplates().size(), + networkSliceServices.getSliceServices().size()); + return restconfClient.createNetworkSliceServices(networkSliceServices); + } + + /** + * Provisions a single SLO/SLE template on the provider. + * + * @param template the SLO/SLE template to create + * @return the created template as returned by the provider + * @throws RestconfException if provisioning fails + */ + public SloSleTemplate provisionSloSleTemplate(SloSleTemplate template) throws RestconfException { + logger.info("Provisioning SLO/SLE template: {}", template.getId()); + return restconfClient.createSloSleTemplate(template); + } + /** * Lists all provisioned network slice services. * diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/Rfc9543JsonConverter.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/Rfc9543JsonConverter.java index d6d7db5..18d41c3 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/Rfc9543JsonConverter.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/Rfc9543JsonConverter.java @@ -105,14 +105,17 @@ public class Rfc9543JsonConverter { // Navigate to templates container // Try RFC 9543 wrapped format first: ietf-network-slice-service:network-slice-services -> slo-sle-templates - JsonNode templatesContainer = rootNode.get("ietf-network-slice-service:network-slice-services"); + JsonNode templatesContainer = rootNode.get("network-slice-services"); if (templatesContainer != null) { log.debug("Found RFC 9543 wrapped namespace container"); templatesContainer = templatesContainer.get("slo-sle-templates"); } else { - // Fallback to direct slo-sle-templates (for backward compatibility) - templatesContainer = rootNode.get("slo-sle-templates"); + // Fallback to direct ietf-network-slice-service: + templatesContainer = rootNode.get("ietf-network-slice-service:network-slice-services"); + if (templatesContainer != null) { + templatesContainer = rootNode.get("slo-sle-templates"); + } if (templatesContainer != null) { log.debug("Found direct slo-sle-templates container (unwrapped format)"); } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java index 746049a..68c50ff 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/mappers/EntityToLogicalResourceSpecMapper.java @@ -149,7 +149,7 @@ public class EntityToLogicalResourceSpecMapper { handleCollectionField(spec, fieldName, field, value); } // Handle value objects (embedded types) - else if (isValueObject(fieldType)) { + else if (isValueObject(fieldType) && value!=null) { addValueObjectCharacteristics(spec, fieldName, value); } // Handle simple types as characteristics diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java index f98a840..1a6da8b 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java @@ -1,11 +1,17 @@ package org.etsi.osl.controllers.ietf.ns.repository.impl; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.UUID; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.etsi.osl.controllers.ietf.ns.api.ResourceSpecificationTemplateRegistry; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.NetworkSliceServices; import org.etsi.osl.controllers.ietf.ns.api.domain.model.SliceService; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.SloSleTemplate; import org.etsi.osl.controllers.ietf.ns.api.restconf.RestconfConsumerService; import org.etsi.osl.tmf.common.model.EValueType; import org.etsi.osl.tmf.ri639.model.Characteristic; @@ -21,197 +27,264 @@ import org.springframework.stereotype.Service; @Service public class ResourceRepoService { - private static final Logger logger = LoggerFactory.getLogger("org.etsi.osl.example.gc"); - - @Autowired - private TMFResourceSpecRepositoryImpl tmfRepository; - - @Autowired - private ResourceSpecificationTemplateRegistry templateRegistry; - - @Autowired - SimpleResourceMapper simpleResourceMapper; - - @Autowired(required = false) - private RestconfConsumerService restconfConsumerService; - - public Resource createResource( Map headers, ResourceCreate resourceRequested) { - - String resourceid = ""; - - if ( headers.get("org.etsi.osl.serviceId") !=null ) { - - } - if ( headers.get("org.etsi.osl.resourceId") !=null ) { //the resource to update back - resourceid = (String) headers.get("org.etsi.osl.resourceId") ; - } - if ( headers.get("org.etsi.osl.serviceOrderId") !=null ) { - - } - - ResourceUpdate resourceUpdate = simpleResourceMapper.resourceCreateToResourceUpdate(resourceRequested); - - if ( templateRegistry.getTemplateId (resourceRequested.getResourceSpecification().getName() ).isPresent() ) { - resourceUpdate = applySliceRequest(resourceUpdate, resourceid); - } else { - return null; + private static final Logger logger = LoggerFactory.getLogger("org.etsi.osl.controllers.ietf.ns"); + + @Autowired + private TMFResourceSpecRepositoryImpl tmfRepository; + + @Autowired + private ResourceSpecificationTemplateRegistry templateRegistry; + + @Autowired + SimpleResourceMapper simpleResourceMapper; + + @Autowired(required = false) + private RestconfConsumerService restconfConsumerService; + + // ========================================================================= + // Public entry points + // ========================================================================= + + public Resource createResource(Map headers, ResourceCreate resourceRequested) { + + String resourceid = extractResourceId(headers); + + ResourceUpdate resourceUpdate = simpleResourceMapper.resourceCreateToResourceUpdate(resourceRequested); + + String specName = resourceRequested.getResourceSpecification().getName(); + + if (!templateRegistry.getTemplateId(specName).isPresent()) { + logger.warn("No registered template found for spec name '{}' — ignoring resource create", specName); + return null; + } + + resourceUpdate = switch (specName) { + case ResourceSpecificationTemplateRegistry.SPEC_NETWORK_SLICE_SERVICES -> applyNetworkSliceServicesRequest(resourceUpdate, resourceid); + case ResourceSpecificationTemplateRegistry.SPEC_SLO_SLE_TEMPLATE -> applySloSleTemplateRequest(resourceUpdate, resourceid); + case ResourceSpecificationTemplateRegistry.SPEC_SLICE_SERVICE -> applySliceServiceRequest(resourceUpdate, resourceid); + default -> { + logger.warn("Spec '{}' is registered but has no dedicated handler — skipping", specName); + yield resourceUpdate; + } + }; + + return tmfRepository.updateResourceById(resourceid, resourceUpdate); + } + + public Resource updateResource(Map headers, ResourceUpdate r) { + String resourceid = extractResourceId(headers); + r.addResourceCharacteristicItemShort( + "status.infoMessage", "Updated " + new Date(), EValueType.TEXT.getValue()); + return tmfRepository.updateResourceById(resourceid, r); } - - //send it to TMF API - Resource res = tmfRepository.updateResourceById( resourceid, resourceUpdate); - - return res; - } - - private ResourceUpdate applySliceRequest(ResourceUpdate resourceUpdate, String resourceid) { - - String jsonRequest = ""; - - // Extract jsonRequest from resource characteristics - for (Characteristic c : resourceUpdate.getResourceCharacteristic()) { - if (c.getName().equalsIgnoreCase("jsonRequest")) { - jsonRequest = c.getValue().getValue(); - } + + public Resource deleteResource(Map headers, ResourceUpdate r) { + String resourceid = extractResourceId(headers); + r.setResourceStatus(ResourceStatusType.UNKNOWN); + r.addResourceCharacteristicItemShort("status.Health", "deleted", EValueType.TEXT.getValue()); + return tmfRepository.updateResourceById(resourceid, r); } - String infoMessage = "Created"; - String healthStatus = "Healthy"; - ResourceStatusType resourceStatus = ResourceStatusType.AVAILABLE; + // ========================================================================= + // Spec-specific handlers + // ========================================================================= + + /** + * Handles IETFNetworkSliceServicesSpec resources. + * + * Extracts the {@code SloSleTemplatesAsJsonArray} and {@code SliceServicesAsJsonArray} + * characteristics, builds a {@link NetworkSliceServices} container and provisions it + * on the RESTCONF provider in a single call. + */ + private ResourceUpdate applyNetworkSliceServicesRequest(ResourceUpdate resourceUpdate, String resourceid) { + String sloSleTemplatesJson = ""; + String sliceServicesJson = ""; + + for (Characteristic c : resourceUpdate.getResourceCharacteristic()) { + if ("SloSleTemplatesAsJsonArray".equalsIgnoreCase(c.getName())) { + sloSleTemplatesJson = c.getValue().getValue(); + } else if ("SliceServicesAsJsonArray".equalsIgnoreCase(c.getName())) { + sliceServicesJson = c.getValue().getValue(); + } + } + + String infoMessage = "Created"; + String healthStatus = "Healthy"; + ResourceStatusType resourceStatus = ResourceStatusType.AVAILABLE; + + try { + ObjectMapper mapper = lenientMapper(); - // Parse and send slice request via RESTCONF if consumer service is available - if (jsonRequest != null && !jsonRequest.isEmpty()) { - try { - logger.info("Parsing slice request JSON for resource: {}", resourceid); + List templates = parseSloSleTemplates(mapper, sloSleTemplatesJson); + List services = parseSliceServices(mapper, sliceServicesJson); - // Parse JSON string to NetworkSliceServices object (RFC 9543 format with slice-service array) - ObjectMapper mapper = new ObjectMapper(); - // Configure mapper to ignore unknown properties (for RFC 9543 field name mappings) - mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + NetworkSliceServices nss = new NetworkSliceServices(); + nss.setSloSleTemplates(templates); + nss.setSliceServices(services); - // Parse the RFC 9543 format: { "slice-service": [ {...} ] } - com.fasterxml.jackson.databind.JsonNode rootNode = mapper.readTree(jsonRequest); - com.fasterxml.jackson.databind.JsonNode sliceServiceNode = rootNode.get("slice-service"); + logger.info("Provisioning NetworkSliceServices for resource {} ({} template(s), {} service(s))", + resourceid, templates.size(), services.size()); - if (sliceServiceNode == null || !sliceServiceNode.isArray()) { - throw new IllegalArgumentException("Expected 'slice-service' array in JSON request"); + if (restconfConsumerService != null) { + NetworkSliceServices provisioned = restconfConsumerService.provisionNetworkSliceServices(nss); + infoMessage = "Successfully provisioned NetworkSliceServices via RESTCONF [" + + templates.size() + " template(s), " + services.size() + " service(s)]"; + } else { + logger.warn("RESTCONF consumer not available — NetworkSliceServices not sent to provider"); + infoMessage = "Parsed successfully but RESTCONF consumer not available"; + healthStatus = "Degraded"; + resourceStatus = ResourceStatusType.SUSPENDED; + } + + } catch (Exception e) { + logger.error("Error processing IETFNetworkSliceServicesSpec for resource {}", resourceid, e); + infoMessage = "Failed: " + e.getMessage(); + healthStatus = "Unhealthy"; + resourceStatus = ResourceStatusType.SUSPENDED; } - // Parse array of SliceService objects - java.util.List sliceServices = new java.util.ArrayList<>(); - for (com.fasterxml.jackson.databind.JsonNode serviceNode : sliceServiceNode) { - SliceService service = mapper.treeToValue(serviceNode, SliceService.class); - sliceServices.add(service); + return applyStatus(resourceUpdate, infoMessage, healthStatus, resourceStatus); + } + + /** + * Handles IETFSloSleTemplateSpec resources. + * + * Extracts the {@code SloSleTemplateAsJson} characteristic, parses it into a + * {@link SloSleTemplate} and provisions it on the RESTCONF provider. + */ + private ResourceUpdate applySloSleTemplateRequest(ResourceUpdate resourceUpdate, String resourceid) { + String templateJson = ""; + + for (Characteristic c : resourceUpdate.getResourceCharacteristic()) { + if ("SloSleTemplateAsJson".equalsIgnoreCase(c.getName())) { + templateJson = c.getValue().getValue(); + break; + } } - logger.info("Successfully parsed {} SliceService(s)", sliceServices.size()); + String infoMessage = "Created"; + String healthStatus = "Healthy"; + ResourceStatusType resourceStatus = ResourceStatusType.AVAILABLE; - // Send to RESTCONF provider if consumer service is available - if (restconfConsumerService != null) { - logger.info("Sending {} slice service request(s) to RESTCONF provider", sliceServices.size()); + if (templateJson == null || templateJson.isBlank()) { + return applyStatus(resourceUpdate, "Failed: SloSleTemplateAsJson characteristic is empty", + "Unhealthy", ResourceStatusType.SUSPENDED); + } - // Provision the slice services via RESTCONF (sends as array) - java.util.List provisionedServices = restconfConsumerService.provisionSliceServices(sliceServices); + try { + SloSleTemplate template = lenientMapper().readValue(templateJson, SloSleTemplate.class); + logger.info("Provisioning SLO/SLE template '{}' for resource {}", template.getId(), resourceid); - if (provisionedServices != null && !provisionedServices.isEmpty()) { - logger.info("Successfully provisioned {} slice service(s) via RESTCONF", provisionedServices.size()); - StringBuilder serviceIds = new StringBuilder(); - for (SliceService svc : provisionedServices) { - if (serviceIds.length() > 0) serviceIds.append(", "); - serviceIds.append(svc.getId()); + if (restconfConsumerService != null) { + restconfConsumerService.provisionSloSleTemplate(template); + infoMessage = "Successfully provisioned SLO/SLE template '" + template.getId() + "' via RESTCONF"; + } else { + logger.warn("RESTCONF consumer not available — SLO/SLE template not sent to provider"); + infoMessage = "Parsed successfully but RESTCONF consumer not available"; + healthStatus = "Degraded"; + resourceStatus = ResourceStatusType.SUSPENDED; } - infoMessage = "Successfully created and provisioned via RESTCONF: " + serviceIds.toString(); - healthStatus = "Healthy"; - resourceStatus = ResourceStatusType.AVAILABLE; - } else { - logger.warn("Failed to provision slice services via RESTCONF - received null or empty response"); - infoMessage = "Failed to provision via RESTCONF: null or empty response"; - healthStatus = "Degraded"; + + } catch (Exception e) { + logger.error("Error processing IETFSloSleTemplateSpec for resource {}", resourceid, e); + infoMessage = "Failed: " + e.getMessage(); + healthStatus = "Unhealthy"; resourceStatus = ResourceStatusType.SUSPENDED; - } - } else { - logger.warn("RESTCONF consumer service not available - slice services not sent to provider"); - infoMessage = "Parsed successfully but RESTCONF consumer not available"; - healthStatus = "Degraded"; - resourceStatus = ResourceStatusType.SUSPENDED; } - } catch (IllegalArgumentException e) { - logger.error("Invalid slice service JSON format: {}", e.getMessage()); - infoMessage = "Failed: Invalid JSON format - " + e.getMessage(); - healthStatus = "Unhealthy"; - resourceStatus = ResourceStatusType.SUSPENDED; - } catch (Exception e) { - logger.error("Error processing slice request", e); - infoMessage = "Failed: " + e.getMessage(); - healthStatus = "Unhealthy"; - resourceStatus = ResourceStatusType.SUSPENDED; - } - } else { - logger.warn("Empty jsonRequest for resource: {}", resourceid); - infoMessage = "Failed: Empty or missing jsonRequest"; - healthStatus = "Unhealthy"; - resourceStatus = ResourceStatusType.SUSPENDED; + return applyStatus(resourceUpdate, infoMessage, healthStatus, resourceStatus); } - // Update resource with status - resourceUpdate.addResourceCharacteristicItemShort("status.infoMessage", infoMessage + " [" + new Date() + "]", EValueType.TEXT.getValue()); - resourceUpdate.addResourceCharacteristicItemShort("status.Health", healthStatus, EValueType.TEXT.getValue()); - resourceUpdate.addResourceCharacteristicItemShort("status.jsonRequest", jsonRequest, EValueType.TEXT.getValue()); - resourceUpdate.addResourceCharacteristicItemShort("status.UUID", UUID.randomUUID().toString(), EValueType.TEXT.getValue()); - - resourceUpdate.setResourceStatus(resourceStatus); - return resourceUpdate; - } - - public Resource updateResource( Map headers, ResourceUpdate r) { - String resourceid = ""; - - if ( headers.get("org.etsi.osl.serviceId") !=null ) { - - } - if ( headers.get("org.etsi.osl.resourceId") !=null ) { //the resource to update back - resourceid = (String) headers.get("org.etsi.osl.resourceId") ; - } - if ( headers.get("org.etsi.osl.serviceOrderId") !=null ) { - - } - - ResourceUpdate resourceUpdate = r; - - resourceUpdate.addResourceCharacteristicItemShort("status.infoMessage", "Updated " + new Date() , EValueType.TEXT.getValue()); - - - Resource res = tmfRepository.updateResourceById( resourceid, resourceUpdate); - - return res; - } - - public Resource deleteResource( Map headers, ResourceUpdate r) { - String resourceid = ""; - - if ( headers.get("org.etsi.osl.serviceId") !=null ) { - - } - if ( headers.get("org.etsi.osl.resourceId") !=null ) { //the resource to update back - resourceid = (String) headers.get("org.etsi.osl.resourceId") ; - } - if ( headers.get("org.etsi.osl.serviceOrderId") !=null ) { - - } - - - ResourceUpdate resourceUpdate = r; - resourceUpdate.setResourceStatus(ResourceStatusType.UNKNOWN); - - resourceUpdate.addResourceCharacteristicItemShort("status.Health", "deleted", EValueType.TEXT.getValue()); - - Resource res = tmfRepository.updateResourceById( resourceid, resourceUpdate); - - return res; - } - - - - - + /** + * Handles IETFSliceServiceSpec resources. + * + * Extracts the {@code SliceServiceAsJson} characteristic, parses it into a + * {@link SliceService} and provisions it on the RESTCONF provider. + */ + private ResourceUpdate applySliceServiceRequest(ResourceUpdate resourceUpdate, String resourceid) { + String serviceJson = ""; + + for (Characteristic c : resourceUpdate.getResourceCharacteristic()) { + if ("SliceServiceAsJson".equalsIgnoreCase(c.getName())) { + serviceJson = c.getValue().getValue(); + break; + } + } + + String infoMessage = "Created"; + String healthStatus = "Healthy"; + ResourceStatusType resourceStatus = ResourceStatusType.AVAILABLE; + + if (serviceJson == null || serviceJson.isBlank()) { + return applyStatus(resourceUpdate, "Failed: SliceServiceAsJson characteristic is empty", + "Unhealthy", ResourceStatusType.SUSPENDED); + } + + try { + SliceService service = lenientMapper().readValue(serviceJson, SliceService.class); + logger.info("Provisioning SliceService '{}' for resource {}", service.getId(), resourceid); + + if (restconfConsumerService != null) { + restconfConsumerService.provisionSliceService(service); + infoMessage = "Successfully provisioned SliceService '" + service.getId() + "' via RESTCONF"; + } else { + logger.warn("RESTCONF consumer not available — SliceService not sent to provider"); + infoMessage = "Parsed successfully but RESTCONF consumer not available"; + healthStatus = "Degraded"; + resourceStatus = ResourceStatusType.SUSPENDED; + } + + } catch (Exception e) { + logger.error("Error processing IETFSliceServiceSpec for resource {}", resourceid, e); + infoMessage = "Failed: " + e.getMessage(); + healthStatus = "Unhealthy"; + resourceStatus = ResourceStatusType.SUSPENDED; + } + + return applyStatus(resourceUpdate, infoMessage, healthStatus, resourceStatus); + } + + // ========================================================================= + // Helpers + // ========================================================================= + + private String extractResourceId(Map headers) { + Object id = headers.get("org.etsi.osl.resourceId"); + return id != null ? (String) id : ""; + } + + private ObjectMapper lenientMapper() { + return new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + private List parseSloSleTemplates(ObjectMapper mapper, String json) + throws Exception { + if (json == null || json.isBlank() || "[]".equals(json.trim())) { + return new ArrayList<>(); + } + SloSleTemplate[] arr = mapper.readValue(json, SloSleTemplate[].class); + return Arrays.asList(arr != null ? arr : new SloSleTemplate[0]); + } + + private List parseSliceServices(ObjectMapper mapper, String json) + throws Exception { + if (json == null || json.isBlank() || "[]".equals(json.trim())) { + return new ArrayList<>(); + } + SliceService[] arr = mapper.readValue(json, SliceService[].class); + return Arrays.asList(arr != null ? arr : new SliceService[0]); + } + + private ResourceUpdate applyStatus(ResourceUpdate resourceUpdate, + String infoMessage, String healthStatus, ResourceStatusType resourceStatus) { + resourceUpdate.addResourceCharacteristicItemShort( + "status.infoMessage", infoMessage + " [" + new Date() + "]", EValueType.TEXT.getValue()); + resourceUpdate.addResourceCharacteristicItemShort( + "status.Health", healthStatus, EValueType.TEXT.getValue()); + resourceUpdate.addResourceCharacteristicItemShort( + "status.UUID", UUID.randomUUID().toString(), EValueType.TEXT.getValue()); + resourceUpdate.setResourceStatus(resourceStatus); + return resourceUpdate; + } } -- GitLab From 2fd1407447dd17e95b1eb265b1130c903220bff9 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 14 May 2026 01:17:17 +0300 Subject: [PATCH 04/12] fix parsing --- .../ns/api/domain/model/SloSleTemplate.java | 10 ++++-- .../model/slo_sle/AvailabilityType.java | 4 +++ .../api/domain/model/slo_sle/MetricBound.java | 5 +++ .../model/slo_sle/ServiceIsolationType.java | 33 ++++++++++++------- .../model/slo_sle/ServiceSecurityType.java | 32 ++++++++++++------ .../model/slo_sle/ServiceSloMetricType.java | 30 +++++++++++++---- .../api/domain/model/slo_sle/SlePolicy.java | 3 ++ .../api/domain/model/slo_sle/SloPolicy.java | 2 ++ .../common/LogicalResourceSpecMappable.java | 2 ++ .../repository/impl/ResourceRepoService.java | 12 +++++-- src/main/resources/application.yml | 4 +-- 11 files changed, 103 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java index 3edbacc..68a6dd6 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java @@ -4,10 +4,11 @@ import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SlePolicy; import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SliceTemplateRef; import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; import org.etsi.osl.controllers.ietf.ns.domain.common.ExcludeFromMapping; -import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceMappable; import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceSpecMappable; import org.etsi.osl.controllers.ietf.ns.domain.common.RelatedManagedResourceReference; import org.etsi.osl.tmf.ri639.model.LogicalResource; +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.Transient; import lombok.AllArgsConstructor; @@ -70,6 +71,7 @@ public class SloSleTemplate implements LogicalResourceSpecMappable, RelatedManag * * Mandatory containment (1..1 cardinality). */ + @JsonAlias({"slo-policy"}) private SloPolicy sloPolicy; /** @@ -82,6 +84,7 @@ public class SloSleTemplate implements LogicalResourceSpecMappable, RelatedManag * * Mandatory containment (1..1 cardinality). */ + @JsonAlias({"sle-policy"}) private SlePolicy slePolicy; @@ -138,16 +141,19 @@ public class SloSleTemplate implements LogicalResourceSpecMappable, RelatedManag } @Override + @JsonIgnore public String getEntityId() { return this.id; } @Override + @JsonIgnore public String getEntityName() { - return this.id; //name is equal to id + return this.id; } @Override + @JsonIgnore public String getEntityDescription() { return this.description; } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/AvailabilityType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/AvailabilityType.java index 1382695..ceb1e81 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/AvailabilityType.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/AvailabilityType.java @@ -1,5 +1,6 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -19,17 +20,20 @@ public class AvailabilityType { * Availability percentage (0.0 to 100.0) * Examples: 99.0, 99.9, 99.99, 99.999 */ + @JsonAlias({"availability-percentage"}) private Double availabilityPercentage; /** * Commitment period for the availability SLO * Examples: "per-month", "per-year", "per-service-life" */ + @JsonAlias({"commitment-period"}) private String commitmentPeriod; /** * Downtime allowed per period in minutes * Calculated as (100 - availabilityPercentage) * periodLength */ + @JsonAlias({"allowed-downtime-minutes"}) private Long allowedDowntimeMinutes; } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java index ba42fb6..0ed5e8f 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java @@ -1,6 +1,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; import java.math.BigDecimal; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -26,6 +27,7 @@ public class MetricBound { * Type of SLO metric being bounded. * Examples: ONE_WAY_DELAY_MAXIMUM, TWO_WAY_BANDWIDTH, ONE_WAY_PACKET_LOSS */ + @JsonAlias({"metric-type"}) private ServiceSloMetricType metricType; /** @@ -37,6 +39,7 @@ public class MetricBound { * - For delay: "ms", "us", "ns" * - For loss: "%" */ + @JsonAlias({"metric-unit"}) private String metricUnit; /** @@ -44,6 +47,7 @@ public class MetricBound { * Useful for documenting the purpose and context of the metric. * Example: "Maximum one-way latency between customer sites" */ + @JsonAlias({"value-description"}) private String valueDescription; /** @@ -61,6 +65,7 @@ public class MetricBound { * - ONE_WAY_DELAY_VARIATION_PERCENTILE * - TWO_WAY_DELAY_VARIATION_PERCENTILE */ + @JsonAlias({"percentile-value"}) private BigDecimal percentileValue; /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceIsolationType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceIsolationType.java index efc70d4..58def6a 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceIsolationType.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceIsolationType.java @@ -1,18 +1,13 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + /** * Enumeration of Service Level Expectation (SLE) isolation types. * - * These types correspond to YANG identities in the - * draft-ietf-teas-ietf-network-slice-nbi-yang-25 specification and - * define isolation requirements that can be applied to network slice services. - * - * Isolation types ensure that network slices do not interfere with each other - * and meet specific separation requirements such as: - * - Physical path isolation - * - Logical isolation - * - Resource isolation - * - Traffic isolation + * Accepts both RFC 9543 kebab-case (e.g., "physical-isolation") and + * UPPER_SNAKE_CASE (e.g., "PHYSICAL_ISOLATION") during deserialization. */ public enum ServiceIsolationType { PHYSICAL_ISOLATION, @@ -20,5 +15,21 @@ public enum ServiceIsolationType { TRAFFIC_ISOLATION, RESOURCE_ISOLATION, DEDICATED_RESOURCES, - SHARED_RESOURCES_LIMITED + SHARED_RESOURCES_LIMITED; + + @JsonCreator + public static ServiceIsolationType fromValue(String value) { + if (value == null || value.isBlank()) return null; + String normalized = value.toUpperCase().replace('-', '_'); + try { + return ServiceIsolationType.valueOf(normalized); + } catch (IllegalArgumentException e) { + return null; + } + } + + @JsonValue + public String toValue() { + return name().toLowerCase().replace('_', '-'); + } } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSecurityType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSecurityType.java index 52195e3..387eb4d 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSecurityType.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSecurityType.java @@ -1,17 +1,13 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + /** * Enumeration of Service Level Expectation (SLE) security types. * - * These types correspond to YANG identities in the - * draft-ietf-teas-ietf-network-slice-nbi-yang-25 specification and - * define security requirements that can be applied to network slice services. - * - * Security types may include: - * - Encryption requirements - * - Authentication mechanisms - * - Access control policies - * - Data protection standards + * Accepts both RFC 9543 kebab-case (e.g., "encryption-required") and + * UPPER_SNAKE_CASE (e.g., "ENCRYPTION_REQUIRED") during deserialization. */ public enum ServiceSecurityType { ENCRYPTION_REQUIRED, @@ -19,5 +15,21 @@ public enum ServiceSecurityType { INTEGRITY_PROTECTION, CONFIDENTIALITY_REQUIRED, SECURE_ROUTING, - VPN_REQUIRED + VPN_REQUIRED; + + @JsonCreator + public static ServiceSecurityType fromValue(String value) { + if (value == null || value.isBlank()) return null; + String normalized = value.toUpperCase().replace('-', '_'); + try { + return ServiceSecurityType.valueOf(normalized); + } catch (IllegalArgumentException e) { + return null; + } + } + + @JsonValue + public String toValue() { + return name().toLowerCase().replace('_', '-'); + } } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSloMetricType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSloMetricType.java index 8b07aa7..3b2d3af 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSloMetricType.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/ServiceSloMetricType.java @@ -1,5 +1,8 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + /** * Enumeration of Service Level Objective (SLO) metric types. * @@ -8,11 +11,8 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; * define the various performance metrics that can be monitored and * constrained in network slice services. * - * Organized by category: - * - Bandwidth metrics - * - Delay metrics (absolute) - * - Delay variation metrics - * - Packet loss metrics + * Accepts both RFC 9543 kebab-case (e.g., "two-way-bandwidth") and + * UPPER_SNAKE_CASE (e.g., "TWO_WAY_BANDWIDTH") during deserialization. */ public enum ServiceSloMetricType { // Bandwidth metrics @@ -38,5 +38,23 @@ public enum ServiceSloMetricType { // Packet loss metrics ONE_WAY_PACKET_LOSS, - TWO_WAY_PACKET_LOSS + TWO_WAY_PACKET_LOSS; + + /** Deserialize from kebab-case RFC 9543 names or standard UPPER_SNAKE_CASE. */ + @JsonCreator + public static ServiceSloMetricType fromValue(String value) { + if (value == null || value.isBlank()) return null; + String normalized = value.toUpperCase().replace('-', '_'); + try { + return ServiceSloMetricType.valueOf(normalized); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** Serialize as lowercase kebab-case to stay RFC 9543 compliant. */ + @JsonValue + public String toValue() { + return name().toLowerCase().replace('_', '-'); + } } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java index 5525f4b..d42c786 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java @@ -2,6 +2,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -65,12 +66,14 @@ public class SlePolicy { * - 50 = max 50% of network resources * - 100 = can use all available resources */ + @JsonAlias({"max-occupancy-level"}) private Short maxOccupancyLevel; /** * Path constraints including diversity and service function requirements. * Optional containment (0..1 cardinality). */ + @JsonAlias({"path-constraints"}) private PathConstraints pathConstraints; /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java index 360f6e3..ae1a631 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java @@ -2,6 +2,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonAlias; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -54,6 +55,7 @@ public class SloPolicy { * - Minimum bandwidth: 1000 Mbps * - Maximum packet loss: 0.001% */ + @JsonAlias({"metric-bound"}) private List metricBounds = new ArrayList<>(); /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceSpecMappable.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceSpecMappable.java index 7948473..51cd907 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceSpecMappable.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/domain/common/LogicalResourceSpecMappable.java @@ -45,6 +45,7 @@ public interface LogicalResourceSpecMappable { * * @return Entity type name */ + @JsonIgnore default String getEntityTypeName() { return this.getClass().getSimpleName(); } @@ -83,6 +84,7 @@ public interface LogicalResourceSpecMappable { * * @return Version string (e.g., "0.0.1") */ + @JsonIgnore default String getVersion() { try { Class clazz = this.getClass(); diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java index 1a6da8b..fef1cc5 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java @@ -6,6 +6,8 @@ import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.etsi.osl.controllers.ietf.ns.api.ResourceSpecificationTemplateRegistry; @@ -114,7 +116,7 @@ public class ResourceRepoService { try { ObjectMapper mapper = lenientMapper(); - + List templates = parseSloSleTemplates(mapper, sloSleTemplatesJson); List services = parseSliceServices(mapper, sliceServicesJson); @@ -254,8 +256,12 @@ public class ResourceRepoService { } private ObjectMapper lenientMapper() { - return new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + ObjectMapper mapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + // Skip null elements in collections (handles empty-string enum values that resolve to null) + mapper.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP)); + return mapper; } private List parseSloSleTemplates(ObjectMapper mapper, String json) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c3b9498..da653a5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -48,7 +48,7 @@ oauthsign: # Configure connection to Network Slice Service Provider restconf: # Base URL of the RESTCONF provider (e.g., https://nsc-provider:8443) - provider-url: http://localhost:11880 + provider-url: http://192.168.185.165:8085 # Authentication method: basic, oauth2, mtls, none # basic = HTTP Basic Authentication (default) @@ -58,7 +58,7 @@ restconf: # Default: admin/admin123 (matches demo server) auth: username: admin - password: admin123 + password: admin # YANG model version for ietf-network-slice-service api-version: "2025-05-09" -- GitLab From cd90e81e9fbae8bd718358d6e04d71a21e9df1d6 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 14 May 2026 02:08:59 +0300 Subject: [PATCH 05/12] successful creation of resource --- .../api/SloSleTemplateBootstrapService.java | 129 +----- .../domain/model/ConnectivityConstruct.java | 19 +- .../ns/api/domain/model/ConnectivityType.java | 28 +- .../domain/model/NetworkSliceServices.java | 22 +- .../Rfc9543SliceServiceDeserializer.java | 387 +++++++----------- .../model/Rfc9543SliceServiceSerializer.java | 134 ++++++ .../ns/api/domain/model/ServiceStatus.java | 8 +- .../ns/api/domain/model/SliceService.java | 2 + .../ns/api/domain/model/SloSleTemplate.java | 14 +- .../api/domain/model/slo_sle/MetricBound.java | 30 +- .../api/domain/model/slo_sle/SlePolicy.java | 6 +- .../api/domain/model/slo_sle/SloPolicy.java | 4 +- .../ns/api/restconf/RestconfClientImpl.java | 59 ++- .../api/restconf/RestconfConsumerService.java | 13 +- .../Rfc9543SliceServiceDeserializer.java | 6 +- .../common/LogicalResourceMappable.java | 4 + 16 files changed, 399 insertions(+), 466 deletions(-) create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceSerializer.java diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java index 278b53c..32a5e00 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java @@ -230,8 +230,8 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { addSpecCharacteristic(spec, "testOnly", "BOOLEAN", "false", true); // Operational status (RFC 9543 admin/oper states) - addSpecCharacteristic(spec, "status.adminState", "TEXT", "admin-up", true); - addSpecCharacteristic(spec, "status.operState", "TEXT", "operational", true); + addSpecCharacteristic(spec, "status.adminStatus", "TEXT", "admin-up", true); + addSpecCharacteristic(spec, "status.operStatus", "TEXT", "operational", true); // SLO/SLE template reference (stores the referenced SloSleTemplate ID) addSpecCharacteristic(spec, "sloSleTemplate", "TEXT", "", false); @@ -512,131 +512,6 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { } - - /** - * Parse RFC 9543 formatted SliceService JSON and return SliceService domain object. - * - * This method handles the real-world RFC 9543 network-slice-service format with - * SDP (Service Demarcation Points) and connection groups. - * - * @param sliceServiceJson RFC 9543 formatted JSON string - * @return SliceService domain object - * @throws Exception if JSON parsing fails - */ - public NetworkSliceServices parseAndConvertSliceService(String sliceServiceJson) throws Exception { - log.info("Parsing RFC 9543 SliceService JSON"); - - try { - ObjectMapper mapper = new ObjectMapper(); - JsonNode rootNode = mapper.readTree(sliceServiceJson); - - // Navigate through RFC 9543 namespace wrapper - JsonNode nssNode = rootNode.get("ietf-network-slice-service:network-slice-services"); - if (nssNode == null) { - throw new IllegalArgumentException("Missing RFC 9543 namespace wrapper"); - } - - JsonNode sliceServicesNode = nssNode.get("slice-service"); - if (sliceServicesNode == null || !sliceServicesNode.isArray() || sliceServicesNode.size() == 0) { - throw new IllegalArgumentException("No slice-service array found"); - } - - // Parse all slice services from the array - List sliceServices = new ArrayList<>(); - - for (int i = 0; i < sliceServicesNode.size(); i++) { - JsonNode sliceServiceNode = sliceServicesNode.get(i); - - // Parse service identity - String serviceId = sliceServiceNode.get("id").asText(); - String serviceDescription = sliceServiceNode.has("description") ? - sliceServiceNode.get("description").asText() : "Network Slice Service"; - - log.info("Parsing slice service: {}", serviceId); - - // Create SliceService domain object - SliceService service = new SliceService(); - service.setId(serviceId); - service.setDescription(serviceDescription); - service.setTestOnly(false); - - // Parse SLO/SLE template reference - if (sliceServiceNode.has("slo-sle-policy")) { - JsonNode sloSlePolicyNode = sliceServiceNode.get("slo-sle-policy"); - if (sloSlePolicyNode.has("slo-sle-template")) { - String templateId = sloSlePolicyNode.get("slo-sle-template").asText(); - SloSleTemplate template = new SloSleTemplate(); - template.setId(templateId); - service.setSloSleTemplate(template); - log.debug("Service {} references template: {}", serviceId, templateId); - } - } - - // Parse service tags - if (sliceServiceNode.has("service-tags")) { - JsonNode tagsNode = sliceServiceNode.get("service-tags"); - if (tagsNode.has("tag-type") && tagsNode.get("tag-type").isArray()) { - for (JsonNode tagNode : tagsNode.get("tag-type")) { - String tagType = tagNode.get("tag-type").asText(); - if (tagNode.has("tag-type-value") && tagNode.get("tag-type-value").isArray()) { - for (JsonNode tagValueNode : tagNode.get("tag-type-value")) { - ServiceTag tag = new ServiceTag(); - tag.setValue(tagType + ":" + tagValueNode.asText()); - service.getServiceTags().add(tag); - } - } - } - } - } - - // Parse status - ServiceStatus status = new ServiceStatus(); - status.setAdminState("admin-up"); - status.setOperState("operational"); - service.setStatus(status); - - // Parse SDPs (Service Demarcation Points) - if (sliceServiceNode.has("sdps") && sliceServiceNode.get("sdps").has("sdp")) { - JsonNode sdpsNode = sliceServiceNode.get("sdps").get("sdp"); - if (sdpsNode.isArray()) { - log.debug("Found {} SDPs in slice service", sdpsNode.size()); - } - } - - // Parse connection groups - if (sliceServiceNode.has("connection-groups") && - sliceServiceNode.get("connection-groups").has("connection-group")) { - JsonNode connGroupsNode = sliceServiceNode.get("connection-groups").get("connection-group"); - if (connGroupsNode.isArray()) { - log.debug("Found {} connection groups in slice service", connGroupsNode.size()); - } - } - - // Add this service to the list - sliceServices.add(service); - log.info("Successfully parsed RFC 9543 SliceService: {}", serviceId); - } - - // Create NetworkSliceServices wrapper with all parsed services - NetworkSliceServices networkSliceServices = new NetworkSliceServices(); - networkSliceServices.setSliceServices(sliceServices); - - // Build and store the RFC 9543 formatted JSON request (slice-service array) - Map> sliceServiceWrapper = new HashMap<>(); - sliceServiceWrapper.put("slice-service", sliceServices); - - log.info("Successfully created NetworkSliceServices with {} service(s)", sliceServices.size()); - return networkSliceServices; - - } catch (IllegalArgumentException e) { - log.error("Invalid RFC 9543 SliceService JSON format: {}", e.getMessage()); - throw e; - } catch (Exception e) { - log.error("Error parsing RFC 9543 SliceService JSON", e); - throw e; - } - } - } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityConstruct.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityConstruct.java index 06af7c8..d50ecd1 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityConstruct.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityConstruct.java @@ -2,26 +2,33 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * Represents a connectivity construct within a connection group. - * Defines a specific connectivity pattern (P2P, P2MP, A2A) and includes - * the SDP members that are part of this construct. + * Supports P2P (p2p-sender-sdp / p2p-receiver-sdp), P2MP, and A2A patterns. */ @Data @NoArgsConstructor @AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) public class ConnectivityConstruct { private String id; + + /** P2P sender SDP reference (RFC 9543: p2p-sender-sdp). */ + @JsonProperty("p2p-sender-sdp") + private String p2pSenderSdp; + + /** P2P receiver SDP reference (RFC 9543: p2p-receiver-sdp). */ + @JsonProperty("p2p-receiver-sdp") + private String p2pReceiverSdp; + private ConnectivityType type; private ConstructStatus status; - - // 0-to-1 relationship: optional SLO/SLE policy override private PolicyRef policyOverride; - - // Many-to-many relationship: members by reference private List members = new ArrayList<>(); } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityType.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityType.java index ad063be..f3dec27 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityType.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ConnectivityType.java @@ -1,13 +1,33 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + /** * Enumeration for connectivity types as per IETF Network Slice Service specification. - * P2P: Point-to-Point - * P2MP: Point-to-Multipoint - * A2A: Any-to-Any + * Accepts RFC 9543 kebab-case strings (e.g., "point-to-point") and UPPER_SNAKE_CASE. */ public enum ConnectivityType { P2P, P2MP, - A2A + A2A; + + @JsonCreator + public static ConnectivityType fromValue(String value) { + if (value == null || value.isBlank()) return null; + String lower = value.toLowerCase(); + if (lower.contains("point-to-point") || lower.equals("p2p")) return P2P; + if (lower.contains("point-to-multipoint") || lower.equals("p2mp")) return P2MP; + if (lower.contains("any-to-any") || lower.equals("a2a")) return A2A; + try { return valueOf(value.toUpperCase()); } catch (IllegalArgumentException e) { return null; } + } + + @JsonValue + public String toValue() { + return switch (this) { + case P2P -> "point-to-point"; + case P2MP -> "point-to-multipoint"; + case A2A -> "any-to-any"; + }; + } } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java index c329796..d36e9a8 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/NetworkSliceServices.java @@ -2,6 +2,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -37,6 +38,7 @@ public class NetworkSliceServices implements LogicalResourceMappable { * Get the unique identifier - uses the first slice service ID if available. */ @Override + @JsonIgnore public String getEntityId() { if (sliceServices != null && !sliceServices.isEmpty()) { return sliceServices.get(0).getId(); @@ -44,10 +46,8 @@ public class NetworkSliceServices implements LogicalResourceMappable { return "unknown"; } - /** - * Get the display name - uses the first slice service ID if available. - */ @Override + @JsonIgnore public String getEntityName() { if (sliceServices != null && !sliceServices.isEmpty()) { return sliceServices.get(0).getId(); @@ -55,10 +55,8 @@ public class NetworkSliceServices implements LogicalResourceMappable { return "Network Slice Services"; } - /** - * Get the description - uses the first slice service description if available. - */ @Override + @JsonIgnore public String getEntityDescription() { if (sliceServices != null && !sliceServices.isEmpty() && sliceServices.get(0).getDescription() != null) { return sliceServices.get(0).getDescription(); @@ -66,18 +64,14 @@ public class NetworkSliceServices implements LogicalResourceMappable { return "IETF RFC 9543 Network Slice Services"; } - /** - * Check if this entity has status mapping. - */ @Override + @JsonIgnore public boolean hasStatusMapping() { return sliceServices != null && !sliceServices.isEmpty() && sliceServices.get(0).getStatus() != null; } - /** - * Map service status to TMF639 resource states - uses the first slice service status. - */ @Override + @JsonIgnore public void mapStatusToResourceStates(LogicalResource resource) { if (sliceServices != null && !sliceServices.isEmpty()) { SliceService firstService = sliceServices.get(0); @@ -85,7 +79,7 @@ public class NetworkSliceServices implements LogicalResourceMappable { ServiceStatus status = firstService.getStatus(); // Map administrative state - String adminState = status.getAdminState(); + String adminState = status.getAdminStatus(); if (adminState != null && adminState.equals("admin-up")) { resource.setAdministrativeState(ResourceAdministrativeStateType.UNLOCKED); } else if (adminState != null && adminState.equals("admin-down")) { @@ -93,7 +87,7 @@ public class NetworkSliceServices implements LogicalResourceMappable { } // Map operational state - String operState = status.getOperState(); + String operState = status.getOperStatus(); if (operState != null && operState.equals("operational")) { resource.setOperationalState(ResourceOperationalStateType.ENABLE); } else if (operState != null && operState.equals("non-operational")) { diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializer.java index e92f8a4..8bfbb90 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializer.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializer.java @@ -14,270 +14,161 @@ import org.slf4j.LoggerFactory; * Custom Jackson deserializer for RFC 9543 formatted SliceService JSON. * * Maps RFC 9543 hyphenated field names to Java camelCase property names: - * - "service-tags" → serviceTags - * - "slo-sle-policy" → sloSleTemplate reference - * - "connection-groups" → connectionGroups - * - "sdp-peering" → sdpPeering - * - etc. - * - * This allows seamless parsing of RFC 9543 network slice service definitions - * into the SliceService domain model. + * - "service-tags" → serviceTags + * - "slo-sle-template" → sloSleTemplate (direct string id or nested object) + * - "connection-groups" → connectionGroups + * - "connectivity-construct" → connectivityConstructs + * - "p2p-sender-sdp" → p2pSenderSdp in ConnectivityConstruct + * - "p2p-receiver-sdp" → p2pReceiverSdp in ConnectivityConstruct */ public class Rfc9543SliceServiceDeserializer extends StdDeserializer { - private static final Logger logger = LoggerFactory.getLogger(Rfc9543SliceServiceDeserializer.class); - - public Rfc9543SliceServiceDeserializer() { - super(SliceService.class); - } - - @Override - public SliceService deserialize(JsonParser jp, DeserializationContext ctxt) - throws IOException { - JsonNode node = jp.getCodec().readTree(jp); - return parseSliceService(node); - } - - /** - * Parse RFC 9543 formatted JSON node into SliceService object. - * - * @param node JSON node representing a slice service - * @return SliceService object populated from the JSON - */ - private SliceService parseSliceService(JsonNode node) { - SliceService service = new SliceService(); - - // Parse basic properties - if (node.has("id")) { - service.setId(node.get("id").asText()); - } - - if (node.has("description")) { - service.setDescription(node.get("description").asText()); - } - - if (node.has("testOnly")) { - service.setTestOnly(node.get("testOnly").asBoolean(false)); - } + private static final Logger logger = LoggerFactory.getLogger(Rfc9543SliceServiceDeserializer.class); - // Parse service tags (RFC 9543: "service-tags") - if (node.has("service-tags")) { - List tags = parseServiceTags(node.get("service-tags")); - service.getServiceTags().addAll(tags); - logger.debug("Parsed {} service tags for slice service: {}", tags.size(), service.getId()); + public Rfc9543SliceServiceDeserializer() { + super(SliceService.class); } - // Parse SLO/SLE template reference (RFC 9543: "slo-sle-policy") - if (node.has("slo-sle-policy")) { - JsonNode policyNode = node.get("slo-sle-policy"); - if (policyNode.has("slo-sle-template")) { - SloSleTemplate template = new SloSleTemplate(); - template.setId(policyNode.get("slo-sle-template").asText()); - service.setSloSleTemplate(template); - logger.debug("Parsed SLO/SLE template reference: {}", template.getId()); - } + @Override + public SliceService deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + return parseSliceService(node); } - // Parse status - if (node.has("status")) { - ServiceStatus status = parseStatus(node.get("status")); - service.setStatus(status); - } + private SliceService parseSliceService(JsonNode node) { + SliceService service = new SliceService(); - // Parse SDPs (Service Demarcation Points) - RFC 9543: "sdps" - if (node.has("sdps")) { - List sdps = parseSdps(node.get("sdps")); - service.getSdps().addAll(sdps); - logger.debug("Parsed {} SDPs for slice service: {}", sdps.size(), service.getId()); - } - - // Parse connection groups - RFC 9543: "connection-groups" - if (node.has("connection-groups")) { - List connGroups = parseConnectionGroups(node.get("connection-groups")); - service.getConnectionGroups().addAll(connGroups); - logger.debug("Parsed {} connection groups for slice service: {}", connGroups.size(), - service.getId()); - } + if (node.has("id")) service.setId(node.get("id").asText()); + if (node.has("description")) service.setDescription(node.get("description").asText()); + if (node.has("test-only")) service.setTestOnly(node.get("test-only").asBoolean(false)); + if (node.has("testOnly")) service.setTestOnly(node.get("testOnly").asBoolean(false)); - logger.debug("Successfully parsed RFC 9543 SliceService: {}", service.getId()); - return service; - } - - /** - * Parse service tags from RFC 9543 format. - * - * RFC 9543 structure: { "tag-type": [ { "tag-type": "service", "tag-type-value": ["L2"] } ] } - */ - private List parseServiceTags(JsonNode tagsNode) { - List tags = new ArrayList<>(); - - if (tagsNode.has("tag-type") && tagsNode.get("tag-type").isArray()) { - for (JsonNode tagTypeNode : tagsNode.get("tag-type")) { - String tagType = tagTypeNode.has("tag-type") ? tagTypeNode.get("tag-type").asText() : ""; - - if (tagTypeNode.has("tag-type-value") && tagTypeNode.get("tag-type-value").isArray()) { - for (JsonNode valueNode : tagTypeNode.get("tag-type-value")) { - String value = valueNode.asText(); - ServiceTag tag = new ServiceTag(); - if (!tagType.isEmpty()) { - tag.setValue(tagType + ":" + value); - } else { - tag.setValue(value); + // SLO/SLE template: "slo-sle-template": "silver" (direct string id) + if (node.has("slo-sle-template") && node.get("slo-sle-template").isTextual()) { + SloSleTemplate t = new SloSleTemplate(); + t.setId(node.get("slo-sle-template").asText()); + service.setSloSleTemplate(t); + } + // SLO/SLE template nested under slo-sle-policy container + if (node.has("slo-sle-policy")) { + JsonNode policy = node.get("slo-sle-policy"); + if (policy.has("slo-sle-template")) { + SloSleTemplate t = new SloSleTemplate(); + t.setId(policy.get("slo-sle-template").asText()); + service.setSloSleTemplate(t); } - tags.add(tag); - } } - } - } - - return tags; - } - - /** - * Parse service status from JSON node. - */ - private ServiceStatus parseStatus(JsonNode statusNode) { - ServiceStatus status = new ServiceStatus(); - - if (statusNode.has("admin-state")) { - status.setAdminState(statusNode.get("admin-state").asText()); - } else { - status.setAdminState("admin-up"); // Default - } - - if (statusNode.has("oper-state")) { - status.setOperState(statusNode.get("oper-state").asText()); - } else { - status.setOperState("operational"); // Default - } - - return status; - } - - /** - * Parse SDPs (Service Demarcation Points) from RFC 9543 format. - */ - private List parseSdps(JsonNode sdpsNode) { - List sdps = new ArrayList<>(); - - if (sdpsNode.has("sdp") && sdpsNode.get("sdp").isArray()) { - for (JsonNode sdpNode : sdpsNode.get("sdp")) { - SDP sdp = parseSdp(sdpNode); - sdps.add(sdp); - } - } - - return sdps; - } - - /** - * Parse individual SDP from JSON node. - */ - private SDP parseSdp(JsonNode sdpNode) { - SDP sdp = new SDP(); - - if (sdpNode.has("id")) { - sdp.setId(sdpNode.get("id").asText()); - } - - if (sdpNode.has("node-id")) { - sdp.setNodeId(sdpNode.get("node-id").asText()); - } - if (sdpNode.has("sdp-ip-address")) { - String ipAddress = sdpNode.get("sdp-ip-address").asText(); - sdp.getSdpIpAddress().add(ipAddress); - } - - if (sdpNode.has("geo-location")) { - sdp.setGeoLocation(sdpNode.get("geo-location").asText()); - } - - if (sdpNode.has("tp-ref")) { - sdp.setTpRef(sdpNode.get("tp-ref").asText()); - } - - // Parse service match criteria - if (sdpNode.has("service-match-criteria")) { - // For now, just log that we found it - logger.debug("Found service-match-criteria in SDP: {}", sdp.getId()); - } - - // Parse attachment circuits - if (sdpNode.has("attachment-circuits")) { - JsonNode circuitsNode = sdpNode.get("attachment-circuits"); - if (circuitsNode.has("attachment-circuit") && circuitsNode.get("attachment-circuit").isArray()) { - logger.debug("Found {} attachment circuits in SDP: {}", - circuitsNode.get("attachment-circuit").size(), sdp.getId()); - } - } - - return sdp; - } - - /** - * Parse connection groups from RFC 9543 format. - */ - private List parseConnectionGroups(JsonNode connGroupsNode) { - List connGroups = new ArrayList<>(); - - if (connGroupsNode.has("connection-group") && connGroupsNode.get("connection-group").isArray()) { - for (JsonNode connGroupNode : connGroupsNode.get("connection-group")) { - ConnectionGroup connGroup = parseConnectionGroup(connGroupNode); - connGroups.add(connGroup); - } - } - - return connGroups; - } - - /** - * Parse individual connection group from JSON node. - */ - private ConnectionGroup parseConnectionGroup(JsonNode connGroupNode) { - ConnectionGroup connGroup = new ConnectionGroup(); - - if (connGroupNode.has("id")) { - connGroup.setId(connGroupNode.get("id").asText()); - } - - if (connGroupNode.has("connectivity-type")) { - String typeStr = connGroupNode.get("connectivity-type").asText(); - connGroup.setConnectivityType(parseConnectivityType(typeStr)); - } - - // Parse connectivity constructs - if (connGroupNode.has("connectivity-construct") && - connGroupNode.get("connectivity-construct").isArray()) { - logger.debug("Found {} connectivity constructs in connection group: {}", - connGroupNode.get("connectivity-construct").size(), connGroup.getId()); - } + // Service tags + if (node.has("service-tags")) { + service.getServiceTags().addAll(parseServiceTags(node.get("service-tags"))); + } - return connGroup; - } + // Status + if (node.has("status")) { + service.setStatus(parseStatus(node.get("status"))); + } - /** - * Parse RFC 9543 connectivity type string to ConnectivityType enum. - * RFC 9543 uses strings like "ietf-vpn-common:any-to-any" - * Maps to P2P, P2MP, or A2A - */ - private ConnectivityType parseConnectivityType(String typeStr) { - if (typeStr == null || typeStr.isEmpty()) { - logger.debug("Empty connectivity type, defaulting to ANY_TO_ANY"); - return ConnectivityType.A2A; - } + // SDPs — RFC 9543: { "sdp": [...] } + if (node.has("sdps")) { + JsonNode sdpsNode = node.get("sdps"); + if (sdpsNode.has("sdp") && sdpsNode.get("sdp").isArray()) { + for (JsonNode sdpNode : sdpsNode.get("sdp")) { + service.getSdps().add(parseSdp(sdpNode)); + } + } else if (sdpsNode.isArray()) { + for (JsonNode sdpNode : sdpsNode) { + service.getSdps().add(parseSdp(sdpNode)); + } + } + } - String normalized = typeStr.toLowerCase(); + // Connection groups — RFC 9543: { "connection-group": [...] } + if (node.has("connection-groups")) { + JsonNode cgRoot = node.get("connection-groups"); + if (cgRoot.has("connection-group") && cgRoot.get("connection-group").isArray()) { + for (JsonNode cgNode : cgRoot.get("connection-group")) { + service.getConnectionGroups().add(parseConnectionGroup(cgNode)); + } + } else if (cgRoot.isArray()) { + for (JsonNode cgNode : cgRoot) { + service.getConnectionGroups().add(parseConnectionGroup(cgNode)); + } + } + } - if (normalized.contains("point-to-point") || normalized.contains("p2p")) { - return ConnectivityType.P2P; - } else if (normalized.contains("point-to-multipoint") || normalized.contains("p2mp")) { - return ConnectivityType.P2MP; - } else if (normalized.contains("any-to-any") || normalized.contains("a2a")) { - return ConnectivityType.A2A; - } else { - logger.warn("Unknown connectivity type: {}, defaulting to ANY_TO_ANY", typeStr); - return ConnectivityType.A2A; + logger.debug("Parsed RFC 9543 SliceService: {}", service.getId()); + return service; + } + + /** + * Parses service-tags from RFC 9543 format: + * { "tag-type": [ { "tag-type": "service", "tag-type-value": ["L3"] } ] } + */ + private List parseServiceTags(JsonNode tagsNode) { + List tags = new ArrayList<>(); + if (!tagsNode.has("tag-type") || !tagsNode.get("tag-type").isArray()) return tags; + + for (JsonNode tagTypeNode : tagsNode.get("tag-type")) { + String typeStr = tagTypeNode.has("tag-type") ? tagTypeNode.get("tag-type").asText() : ""; + ServiceTagType tagType = null; + try { tagType = ServiceTagType.valueOf(typeStr); } catch (Exception ignored) {} + + if (tagTypeNode.has("tag-type-value") && tagTypeNode.get("tag-type-value").isArray()) { + for (JsonNode valueNode : tagTypeNode.get("tag-type-value")) { + ServiceTag tag = new ServiceTag(); + tag.setType(tagType); + tag.setValue(valueNode.asText()); + tags.add(tag); + } + } + } + return tags; + } + + private ServiceStatus parseStatus(JsonNode statusNode) { + ServiceStatus status = new ServiceStatus(); + if (statusNode.has("admin-status")) + status.setAdminStatus( statusNode.get("admin-status").asText() ); + if ( statusNode.has("oper-status") ) + status.setOperStatus( statusNode.get("oper-status").asText() ); + return status; + } + + private SDP parseSdp(JsonNode sdpNode) { + SDP sdp = new SDP(); + if (sdpNode.has("id")) sdp.setId(sdpNode.get("id").asText()); + if (sdpNode.has("node-id")) sdp.setNodeId(sdpNode.get("node-id").asText()); + if (sdpNode.has("tp-ref")) sdp.setTpRef(sdpNode.get("tp-ref").asText()); + if (sdpNode.has("geo-location") && !sdpNode.get("geo-location").isObject()) + sdp.setGeoLocation(sdpNode.get("geo-location").asText()); + if (sdpNode.has("sdp-ip-address")) { + JsonNode ipNode = sdpNode.get("sdp-ip-address"); + if (ipNode.isArray()) { + for (JsonNode ip : ipNode) sdp.getSdpIpAddress().add(ip.asText()); + } else if (!ipNode.isObject()) { + sdp.getSdpIpAddress().add(ipNode.asText()); + } + } + return sdp; + } + + private ConnectionGroup parseConnectionGroup(JsonNode node) { + ConnectionGroup cg = new ConnectionGroup(); + if (node.has("id")) cg.setId(node.get("id").asText()); + if (node.has("connectivity-type")) cg.setConnectivityType( + ConnectivityType.fromValue(node.get("connectivity-type").asText())); + + // connectivity-construct list + if (node.has("connectivity-construct") && node.get("connectivity-construct").isArray()) { + for (JsonNode ccNode : node.get("connectivity-construct")) { + ConnectivityConstruct cc = new ConnectivityConstruct(); + if (ccNode.has("id")) cc.setId(ccNode.get("id").asText()); + if (ccNode.has("p2p-sender-sdp")) cc.setP2pSenderSdp(ccNode.get("p2p-sender-sdp").asText()); + if (ccNode.has("p2p-receiver-sdp")) cc.setP2pReceiverSdp(ccNode.get("p2p-receiver-sdp").asText()); + cg.getConnectivityConstructs().add(cc); + } + } + return cg; } - } } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceSerializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceSerializer.java new file mode 100644 index 0000000..1cab11d --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceSerializer.java @@ -0,0 +1,134 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Custom Jackson serializer for RFC 9543 formatted SliceService JSON. + * + * Produces the standard RFC 9543 wire format with proper nested containers: + * - "sdps": { "sdp": [...] } + * - "connection-groups": { "connection-group": [...] } + * - "service-tags": { "tag-type": [ { "tag-type": "...", "tag-type-value": [...] } ] } + * - "slo-sle-template": "" (string reference, not object) + */ +public class Rfc9543SliceServiceSerializer extends StdSerializer { + + private static final Logger logger = LoggerFactory.getLogger(Rfc9543SliceServiceSerializer.class); + + public Rfc9543SliceServiceSerializer() { + super(SliceService.class); + } + + @Override + public void serialize(SliceService s, JsonGenerator gen, SerializerProvider provider) + throws IOException { + gen.writeStartObject(); + + writeIfNotNull(gen, "id", s.getId()); + writeIfNotNull(gen, "description", s.getDescription()); + + if (Boolean.TRUE.equals(s.getTestOnly())) { + gen.writeBooleanField("test-only", true); + } + + // slo-sle-template as a plain string id reference + if (s.getSloSleTemplate() != null && s.getSloSleTemplate().getId() != null) { + gen.writeStringField("slo-sle-template", s.getSloSleTemplate().getId()); + } + + // service-tags: { "tag-type": [ { "tag-type": "...", "tag-type-value": [...] } ] } + if (s.getServiceTags() != null && !s.getServiceTags().isEmpty()) { + gen.writeFieldName("service-tags"); + gen.writeStartObject(); + gen.writeFieldName("tag-type"); + gen.writeStartArray(); + for (ServiceTag tag : s.getServiceTags()) { + gen.writeStartObject(); + if (tag.getType() != null) gen.writeStringField("tag-type", tag.getType().name()); + gen.writeFieldName("tag-type-value"); + gen.writeStartArray(); + if (tag.getValue() != null) gen.writeString(tag.getValue()); + gen.writeEndArray(); + gen.writeEndObject(); + } + gen.writeEndArray(); + gen.writeEndObject(); + } + + // status: { "admin-status": "...", "oper-status": "..." } + if (s.getStatus() != null) { + ServiceStatus st = s.getStatus(); + gen.writeFieldName("status"); + gen.writeStartObject(); + writeIfNotNull(gen, "admin-status", st.getAdminStatus()); + writeIfNotNull(gen, "oper-status", st.getOperStatus()); + gen.writeEndObject(); + } + + // sdps: { "sdp": [ { "id": "...", "node-id": "...", ... } ] } + if (s.getSdps() != null && !s.getSdps().isEmpty()) { + gen.writeFieldName("sdps"); + gen.writeStartObject(); + gen.writeFieldName("sdp"); + gen.writeStartArray(); + for (SDP sdp : s.getSdps()) { + gen.writeStartObject(); + writeIfNotNull(gen, "id", sdp.getId()); + writeIfNotNull(gen, "node-id", sdp.getNodeId()); + writeIfNotNull(gen, "tp-ref", sdp.getTpRef()); + writeIfNotNull(gen, "geo-location", sdp.getGeoLocation()); + if (sdp.getSdpIpAddress() != null && !sdp.getSdpIpAddress().isEmpty()) { + gen.writeFieldName("sdp-ip-address"); + gen.writeStartArray(); + for (String ip : sdp.getSdpIpAddress()) gen.writeString(ip); + gen.writeEndArray(); + } + gen.writeEndObject(); + } + gen.writeEndArray(); + gen.writeEndObject(); + } + + // connection-groups: { "connection-group": [ { "id": "...", "connectivity-type": "...", "connectivity-construct": [...] } ] } + if (s.getConnectionGroups() != null && !s.getConnectionGroups().isEmpty()) { + gen.writeFieldName("connection-groups"); + gen.writeStartObject(); + gen.writeFieldName("connection-group"); + gen.writeStartArray(); + for (ConnectionGroup cg : s.getConnectionGroups()) { + gen.writeStartObject(); + writeIfNotNull(gen, "id", cg.getId()); + if (cg.getConnectivityType() != null) { + gen.writeStringField("connectivity-type", cg.getConnectivityType().toValue()); + } + if (cg.getConnectivityConstructs() != null && !cg.getConnectivityConstructs().isEmpty()) { + gen.writeFieldName("connectivity-construct"); + gen.writeStartArray(); + for (ConnectivityConstruct cc : cg.getConnectivityConstructs()) { + gen.writeStartObject(); + writeIfNotNull(gen, "id", cc.getId()); + writeIfNotNull(gen, "p2p-sender-sdp", cc.getP2pSenderSdp()); + writeIfNotNull(gen, "p2p-receiver-sdp", cc.getP2pReceiverSdp()); + gen.writeEndObject(); + } + gen.writeEndArray(); + } + gen.writeEndObject(); + } + gen.writeEndArray(); + gen.writeEndObject(); + } + + gen.writeEndObject(); + logger.debug("Serialized RFC 9543 SliceService: {}", s.getId()); + } + + private void writeIfNotNull(JsonGenerator gen, String field, String value) throws IOException { + if (value != null) gen.writeStringField(field, value); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceStatus.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceStatus.java index 4d989be..f333796 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceStatus.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/ServiceStatus.java @@ -1,6 +1,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model; import java.time.OffsetDateTime; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -13,7 +14,10 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @AllArgsConstructor public class ServiceStatus { - private String adminState; - private String operState; + @JsonProperty("admin-status") + private String adminStatus; + @JsonProperty("oper-status") + private String operStatus; + @JsonProperty("last-change") private OffsetDateTime lastChange; } diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SliceService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SliceService.java index 1b7d94b..fe27113 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SliceService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SliceService.java @@ -1,6 +1,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.util.ArrayList; import java.util.List; import lombok.AllArgsConstructor; @@ -19,6 +20,7 @@ import lombok.NoArgsConstructor; * - etc. */ @JsonDeserialize(using = Rfc9543SliceServiceDeserializer.class) +@JsonSerialize(using = Rfc9543SliceServiceSerializer.class) @Data @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java index 68a6dd6..ed1832a 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java @@ -7,7 +7,6 @@ import org.etsi.osl.controllers.ietf.ns.domain.common.ExcludeFromMapping; import org.etsi.osl.controllers.ietf.ns.domain.common.LogicalResourceSpecMappable; import org.etsi.osl.controllers.ietf.ns.domain.common.RelatedManagedResourceReference; import org.etsi.osl.tmf.ri639.model.LogicalResource; -import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.persistence.Transient; @@ -71,7 +70,7 @@ public class SloSleTemplate implements LogicalResourceSpecMappable, RelatedManag * * Mandatory containment (1..1 cardinality). */ - @JsonAlias({"slo-policy"}) + @JsonProperty("slo-policy") private SloPolicy sloPolicy; /** @@ -84,7 +83,7 @@ public class SloSleTemplate implements LogicalResourceSpecMappable, RelatedManag * * Mandatory containment (1..1 cardinality). */ - @JsonAlias({"sle-policy"}) + @JsonProperty("sle-policy") private SlePolicy slePolicy; @@ -104,17 +103,12 @@ public class SloSleTemplate implements LogicalResourceSpecMappable, RelatedManag * References another managed resource that this device is related to * This field can be set by clients to establish relationships to other managed resources */ + @JsonIgnore private String relatedManagedResourceId; - /** - * The related managed resource fetched from resource inventory according to the relatedManagedResourceId - * References another managed resource that this device is related to - * This field is read-only in JSON to maintain consistency with service-layer management - * Not persisted to the database - populated by the service layer from the TMF Resource Inventory - */ @ExcludeFromMapping(reason = "Service-managed relationship, not part of mapping") @Transient - @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @JsonIgnore private LogicalResource relatedManagedResource; diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java index 0ed5e8f..9243f2a 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/MetricBound.java @@ -1,7 +1,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; import java.math.BigDecimal; -import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -27,45 +27,25 @@ public class MetricBound { * Type of SLO metric being bounded. * Examples: ONE_WAY_DELAY_MAXIMUM, TWO_WAY_BANDWIDTH, ONE_WAY_PACKET_LOSS */ - @JsonAlias({"metric-type"}) + @JsonProperty("metric-type") private ServiceSloMetricType metricType; /** * Unit of measurement for the metric. - * Mandatory field per YANG specification. - * - * Examples: - * - For bandwidth: "bps", "Kbps", "Mbps", "Gbps" - * - For delay: "ms", "us", "ns" - * - For loss: "%" */ - @JsonAlias({"metric-unit"}) + @JsonProperty("metric-unit") private String metricUnit; /** * Optional human-readable description of the metric bound. - * Useful for documenting the purpose and context of the metric. - * Example: "Maximum one-way latency between customer sites" */ - @JsonAlias({"value-description"}) + @JsonProperty("value-description") private String valueDescription; /** * Percentile value for percentile-based metrics (0.0 to 100.0). - * Optional field with 3 decimal places precision. - * - * Examples: - * - 50.0 = median (50th percentile) - * - 95.0 = 95th percentile - * - 99.9 = 99.9th percentile - * - * Only applies to percentile metric types: - * - ONE_WAY_DELAY_PERCENTILE - * - TWO_WAY_DELAY_PERCENTILE - * - ONE_WAY_DELAY_VARIATION_PERCENTILE - * - TWO_WAY_DELAY_VARIATION_PERCENTILE */ - @JsonAlias({"percentile-value"}) + @JsonProperty("percentile-value") private BigDecimal percentileValue; /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java index d42c786..1e82650 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java @@ -2,7 +2,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -66,14 +66,14 @@ public class SlePolicy { * - 50 = max 50% of network resources * - 100 = can use all available resources */ - @JsonAlias({"max-occupancy-level"}) + @JsonProperty("max-occupancy-level") private Short maxOccupancyLevel; /** * Path constraints including diversity and service function requirements. * Optional containment (0..1 cardinality). */ - @JsonAlias({"path-constraints"}) + @JsonProperty("path-constraints") private PathConstraints pathConstraints; /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java index ae1a631..09c5cd7 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SloPolicy.java @@ -2,7 +2,7 @@ package org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle; import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -55,7 +55,7 @@ public class SloPolicy { * - Minimum bandwidth: 1000 Mbps * - Maximum packet loss: 0.001% */ - @JsonAlias({"metric-bound"}) + @JsonProperty("metric-bound") private List metricBounds = new ArrayList<>(); /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java index c55ade7..cc2053b 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImpl.java @@ -22,6 +22,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; /** * Implementation of {@link RestconfClient} that communicates with a Network Slice Service Provider @@ -74,19 +76,13 @@ public class RestconfClientImpl implements RestconfClient { String uri = nssUri(); logger.info("Creating network-slice-services container at {}", uri); try { - // Serialize payload for debugging — visible when log level is DEBUG - if (logger.isDebugEnabled()) { - String payloadJson = objectMapper.writerWithDefaultPrettyPrinter() - .writeValueAsString(networkSliceServices); - logger.debug("POST {} — request payload:\n{}", uri, payloadJson); - } + String payloadJson = buildRfc9543Payload(networkSliceServices); + logger.debug("POST {} — request payload:\n{}", uri, payloadJson); - ResponseEntity resp = restTemplate.exchange( - uri, HttpMethod.POST, - new HttpEntity<>(networkSliceServices, buildHeaders()), - NetworkSliceServices.class); + HttpEntity request = new HttpEntity<>(payloadJson, buildHeaders()); + ResponseEntity resp = restTemplate.exchange(uri, HttpMethod.POST, request, String.class); assertSuccess(resp, "create network-slice-services container"); - return resp.getBody(); + return networkSliceServices; } catch (HttpClientErrorException e) { logger.error("POST {} — HTTP {} response body:\n{}", uri, e.getStatusCode().value(), e.getResponseBodyAsString()); @@ -520,6 +516,47 @@ public class RestconfClientImpl implements RestconfClient { } } + // ========================================================================= + // RFC 9543 payload builder + // ========================================================================= + + /** + * Builds the RFC 9543 RESTCONF JSON payload for a NetworkSliceServices container. + * + * Produces the standard RFC 9543 wire format: + *
+     * {
+     *   "ietf-network-slice-service:network-slice-services": {
+     *     "slo-sle-templates": {
+     *       "slo-sle-template": [ ... ]
+     *     },
+     *     "slice-service": [ ... ]
+     *   }
+     * }
+     * 
+ */ + private String buildRfc9543Payload(NetworkSliceServices nss) throws Exception { + ObjectNode root = objectMapper.createObjectNode(); + ObjectNode container = root.putObject("ietf-network-slice-service:network-slice-services"); + + if (nss.getSloSleTemplates() != null && !nss.getSloSleTemplates().isEmpty()) { + ObjectNode templatesObj = container.putObject("slo-sle-templates"); + ArrayNode templateArr = templatesObj.putArray("slo-sle-template"); + for (SloSleTemplate t : nss.getSloSleTemplates()) { + templateArr.add(objectMapper.valueToTree(t)); + } + } + + if (nss.getSliceServices() != null && !nss.getSliceServices().isEmpty()) { + ArrayNode serviceArr = container.putArray("slice-service"); + for (SliceService s : nss.getSliceServices()) { + serviceArr.add(objectMapper.valueToTree(s)); + } + } + + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(root); + } + // ========================================================================= // URI helpers // ========================================================================= diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java index 68cf1ac..6e09d48 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java @@ -50,15 +50,6 @@ public class RestconfConsumerService { public List provisionSliceServices(List services) throws RestconfException { logger.info("Provisioning {} network slice service(s)", services.size()); - // Validate and set initial status for each service - for (SliceService service : services) { - if (service.getStatus() == null) { - service.setStatus(new ServiceStatus()); - } - service.getStatus().setAdminState("admin-up"); - service.getStatus().setLastChange(OffsetDateTime.now()); - } - // POST /slice-service accepts one service at a time per the swagger spec List createdServices = new ArrayList<>(); for (SliceService service : services) { @@ -107,12 +98,12 @@ public class RestconfConsumerService { SliceService feasibilityResult = restconfClient.createSliceService(service); - String adminState = feasibilityResult.getStatus().getAdminState(); + String adminState = feasibilityResult.getStatus().getAdminStatus(); if ("admin-up".equals(adminState)) { logger.info("Feasibility validation PASSED for service: {}", service.getId()); } else if ("rejected".equals(adminState)) { logger.warn("Feasibility validation FAILED for service: {} - Status: {}", - service.getId(), feasibilityResult.getStatus().getOperState()); + service.getId(), feasibilityResult.getStatus().getOperStatus()); } return feasibilityResult; diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SliceServiceDeserializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SliceServiceDeserializer.java index 3735b50..f3729d4 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SliceServiceDeserializer.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/deserializers/Rfc9543SliceServiceDeserializer.java @@ -80,7 +80,7 @@ public class Rfc9543SliceServiceDeserializer extends JsonDeserializer clazz = this.getClass(); @@ -107,6 +109,7 @@ public interface LogicalResourceMappable { * * @return true if entity has status that should be mapped */ + @JsonIgnore default boolean hasStatusMapping() { return false; } @@ -117,6 +120,7 @@ public interface LogicalResourceMappable { * * @return true if entity can have deployed physical devices */ + @JsonIgnore default boolean hasPhysicalDevices() { return false; } -- GitLab From 6eac3b006e7923f769e11b7027cd61beaf85d763 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 14 May 2026 02:18:46 +0300 Subject: [PATCH 06/12] fixed readme --- README.md | 463 +++++++++++++++++++++++---------------- doc/bootstrap_phase.puml | 90 ++++---- 2 files changed, 325 insertions(+), 228 deletions(-) diff --git a/README.md b/README.md index 08c9356..72fcf7d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Overview The **IETF NS Controller** is a Spring Boot microservice implementing the customer part of the **IETF RFC 9543 Network Slice Service** for Teraflow SDN. This microservice acts as an **OSL Resource Controller** that bridges -OpenSlice microservices (like TMF API, Service Orchestrator (OSOM)) and Network Slice Service Providers (e.g., TerflowSDN) through a **message-driven architecture** using Apache Camel and ActiveMQ. +OpenSlice microservices (like TMF API, Service Orchestrator (OSOM)) and Network Slice Service Providers (e.g., TerflowSDN) through a **message-driven architecture** using Apache Camel and ActiveMQ. **WARNING: This artifact is still under testing and not fully functional** @@ -26,7 +26,7 @@ OpenSlice microservices (like TMF API, Service Orchestrator (OSOM)) and Network │ - Manages resource specifications & instances │ └──────────────────────┬──────────────────────────────────────┘ │ - RESTCONF Protocol (RFC 9543) + RESTCONF Protocol (RFC 8040 / RFC 9543) │ ▼ ┌─────────────────────────────────────────────────────────────┐ @@ -37,12 +37,41 @@ OpenSlice microservices (like TMF API, Service Orchestrator (OSOM)) and Network ## Key Responsibilities -1. **Resource Controller Registration** - Automatically registers as a Resource Controller of resource specifications under category (ns.ietf.controllers.osl.etsi.org/v1alpha) with OpenSlice on startup +1. **Resource Controller Registration** - Automatically registers three fixed resource specifications under category `ns.ietf.controllers.osl.etsi.org/v1alpha` in the OpenSlice TMF catalog on startup 2. **Message-Driven Operations** - Listens on ActiveMQ queues for CREATE/UPDATE/DELETE requests -3. **Model Transformation** - Converts between IETF RFC 9543 Network Slice models and TMF resource specifications -4. **RESTCONF Provisioning** - Provisions network slices to RFC 9543-compliant Network Slice Service Providers -5. **Catalog Management** - Manages resource specifications and instances in the OpenSlice TMF catalog -6. **SLO/SLE Template System** - Handles Service Level Objectives and Expectations based on RFC 9543 +3. **Spec-Based Dispatch** - Routes incoming resource operations to different handlers based on the resource spec name (`IETFSloSleTemplateSpec`, `IETFSliceServiceSpec`, `IETFNetworkSliceServicesSpec`) +4. **Model Transformation** - Converts between IETF RFC 9543 Network Slice models and TMF resource specifications using MapStruct and custom Jackson serializers/deserializers +5. **RESTCONF Provisioning** - Provisions network slices to RFC 9543-compliant Network Slice Service Providers via RFC 8040 RESTCONF +6. **Catalog Management** - Manages resource specifications and instances in the OpenSlice TMF catalog + +## Resource Specification Types + +Three fixed `LogicalResourceSpecification` entries are registered in the TMF catalog at startup, each under category `ns.ietf.controllers.osl.etsi.org/v1alpha`: + +| Spec Name | Constant | Description | +|-----------|----------|-------------| +| `IETFSloSleTemplateSpec` | `SPEC_SLO_SLE_TEMPLATE` | Defines the structure of an RFC 9543 SLO/SLE template (service level objectives and expectations) | +| `IETFSliceServiceSpec` | `SPEC_SLICE_SERVICE` | Defines the structure of a single RFC 9543 Network Slice Service including SDPs, connection groups and SLO/SLE reference | +| `IETFNetworkSliceServicesSpec` | `SPEC_NETWORK_SLICE_SERVICES` | Top-level container carrying SLO/SLE templates and slice services as JSON arrays | + +### Characteristics per Spec + +**IETFSloSleTemplateSpec** characteristics (derived from `SloSleTemplate` fields via MapStruct): +- Per-field characteristics from `SloPolicy` (availability, MTU, metric bounds) and `SlePolicy` (security, isolation, path constraints) +- `SloSleTemplateAsJson` (TEXT, configurable) — full RFC 9543 SLO/SLE template JSON blob + +**IETFSliceServiceSpec** characteristics: +- `id`, `description`, `testOnly` +- `status.adminStatus`, `status.operStatus` +- `sloSleTemplate` (TEXT, stores the referenced template ID) +- `serviceTags`, `sdps`, `connectionGroups` (ARRAY) +- `SliceServiceAsJson` (TEXT, configurable) — full RFC 9543 slice service JSON blob + +**IETFNetworkSliceServicesSpec** characteristics: +- `SloSleTemplatesAsJsonArray` (TEXT) — JSON array of all `slo-sle-template` entries +- `SliceServicesAsJsonArray` (TEXT) — JSON array of all `slice-service` entries + +The UUID of each registered spec is stored in `ResourceSpecificationTemplateRegistry` and used when dispatching incoming resource operations. ## Message-Driven Architecture @@ -66,11 +95,12 @@ For this controller: ### Message Flow 1. **OpenSlice OSOM** sends a resource request to the controller's CREATE queue -2. **Apache Camel route** intercepts the message and triggers `ResourceRepoService` -3. **ResourceRepoService** extracts the RFC 9543 JSON payload from the message -4. **RFC 9543 Deserializer** parses the JSON to `SliceService` domain objects -5. **RESTCONF Consumer** provisions the slice service to the Network Slice Service Provider -6. **Response** is sent back via reply queues or catalog updates +2. **Apache Camel route** (`PartnerRouteBuilder`) intercepts the message and triggers `ResourceRepoService` +3. **ResourceRepoService** reads the spec name from the resource's `resourceSpecification.name` +4. **Spec-based dispatch:** routes to `applyIETFSloSleTemplateSpec`, `applyIETFSliceServiceSpec`, or `applyIETFNetworkSliceServicesSpec` depending on the spec name +5. **RFC 9543 deserializer** parses the JSON characteristic payload using `Rfc9543SliceServiceDeserializer` (custom Jackson `StdDeserializer`) +6. **RESTCONF client** (`RestconfClientImpl`) provisions the slice service to the Network Slice Service Provider using the RFC 9543 wire format +7. **Response** is sent back via catalog update (resource status set to `AVAILABLE`, `TERMINATED`, or `UNKNOWN`) ### Message Headers @@ -82,167 +112,245 @@ Important metadata is included in message headers: | `org.etsi.osl.serviceOrderId` | UUID of the related service order | | `org.etsi.osl.serviceId` | UUID of the related service | -**Note:** it updates the resource status in the TMF API back to OpenSlice (e.g., `AVAILABLE`, `TERMINATED`, `UNKNOWN`) to reflect provisioning success/failure. - -## RFC 9543 Integration +## Bootstrap Phase -### IETF Network Slice Service Framework - -This controller implements the IETF RFC 9543 Network Slice Service specification: - -- **Network Slice Customer:** OpenSlice OSOM (service orchestrator requesting network slices) -- **Network Slice Controller:** IETF NS Controller (resource controller managing slice lifecycle) -- **Network Slice Service Provider:** RFC 9543-compliant systems (e.g., TerflowSDN) - -### Supported Features - -- **Slice Service Management** - CREATE/UPDATE/DELETE network slice services -- **SLO/SLE Templates** - Service Level Objective/Expectation policies - -### Field Mapping - -The controller automatically maps RFC 9543 hyphenated JSON field names to Java camelCase properties: - -| RFC 9543 JSON | Java Property | Type | -|---|---|---| -| `service-tags` | `serviceTags` | `List` | -| `slo-sle-policy` | `sloSleTemplate` | `SloSleTemplate` | -| `connection-groups` | `connectionGroups` | `List` | -| `sdp-ip-address` | `sdpIpAddress` | `List` | - -## OpenSlice Integration +On startup, `SloSleTemplateBootstrapService` (implements `CommandLineRunner`) executes a 3-step sequence: -### Resource Specification Registration +**Step 1 — Create fixed resource specification templates** -On startup, the `SloSleTemplateBootstrapService` performs the following steps: +Three specifications are built in memory: +- `createSloSleSpecification()` → `IETFSloSleTemplateSpec` + `SloSleTemplateAsJson` characteristic +- `createSliceServiceSpecification()` → `IETFSliceServiceSpec` + `SliceServiceAsJson` characteristic +- `createNetworkSliceServicesSpecification()` → `IETFNetworkSliceServicesSpec` + `SloSleTemplatesAsJsonArray` + `SliceServicesAsJsonArray` characteristics -1. **Retrieves SLO/SLE templates** from the RESTCONF provider -2. **Registers each template** as a `LogicalResourceSpecification` in the OpenSlice catalog **Category:** `ns.ietf.controllers.osl.etsi.org/v1alpha`. Each template contains a **jsonRequest** (for testing/reference) that is used as an example -3. These `LogicalResourceSpecification`s can be used for service orders. The **jsonRequest** is important since it is the payload that the IETF NS Controller will POST to NSC (**WARNING: This needs testing**) +**Step 2 — Retrieve dynamic templates from RESTCONF Provider (optional)** -**Bootstrap Sequence Diagram:** +If a `RestconfClient` bean is configured, the bootstrap calls: +``` +GET /restconf/data/ietf-network-slice-service:network-slice-services/slo-sle-templates +Accept: application/yang-data+json +``` +The JSON response is parsed with `Rfc9543JsonConverter.parseSloSleTemplates()` and each resulting `SloSleTemplate` is converted to an additional `LogicalResourceSpecification` via `EntityToLogicalResourceSpecMapper`. -See: [`doc/bootstrap_phase.puml`](doc/bootstrap_phase.puml) -![`doc/bootstrap_phase.puml`](//www.plantuml.com/plantuml/png/VLJ1Zjem4BtdAqPxQWz0jxr5rMhPR5XLeWjAqbEfAYiPWbl7JiTEwFvzFS8A66ttG33XlUStRyQ-jqwG6pe53yOuwqZqFxS7OJ4HjJC4DzMgXCneHqOff1iG5lohfFSiMSjUQ0St1LfN6xttE3jqI2NIA6jaC1JP_y1AedO14qCgfBqon_BnUQVV5NbPPPld5R0gqljWmVyPaqfbIeKLThqI3gTgBhqyR3PLMHNBRSpCX1FAj1U6icMrN6-UOjYcHrqghmNLrnKijryOokiauP1cTtVd3L9Ozht72YUDBb0qBv2FNfrJbQDmUE4bcPQimO6bKA0ZYIE22_LOs9Ffe2SpoWRfhCFv9luHk2ayvHKiH2yNsYx63mlBZcVsbC9iahiKO3xJJwccC2NEKeH_1hHduo7wYfym2vkotu4qn3tu_YDfCEQTjfhAv3Znr26FXgDqXxSqF2dKUXNsMbhtEIRUnRmoZZbYhmo1nrvldUxqxHmoGbPOczPtKnLepK3USu-rt8V-xlJ7EJoXHc8a_XMUZ_3B-iwVmjlfJtDODitbEwWFWhn19EzTrjVsmWHoigq78BtfOEhEDBb9MB0OpsWAsqrPWJCG1132sFCaxxJ_WQsXbnJStixhwS3RkR5gZixAkQ5sCTuArH_4UJyUV_-1MMJwLPDGSvJO-CP4zCdgTLVBeQxHsajyWBxHpD2lq8LemdPwRmWS-hnr--gnAvqXl1e3a1ewx7mshunUe3IHMZXAwVbAXTgm6uTJBV4De8r3C2CIynA0-D85QK6R8n3V8zn2mXhbY1wO5VcooXVVje_yzPYlyog73gqLBTe4W0lMw8w6VeFbA23S13P1tG3lczpxrT2fv1y0) +If no RESTCONF client is available, this step is skipped with a warning. -### jsonRequest and SliceService Model +**Step 3 — Register all specifications in the TMF catalog** -The **jsonRequest** is a critical characteristic that carries the RFC 9543 Network Slice Service specification in JSON format during the service order: +Each specification (fixed + any retrieved from provider) is posted to the TMF Catalog API: +``` +POST /resourceCatalogManagement/v4/resourceSpecification +``` +The returned UUID is stored in `ResourceSpecificationTemplateRegistry` under the spec name key. -**What is jsonRequest?** -- A JSON string containing a complete RFC 9543 **SliceService** definition -- Stored in the `LogicalResourceSpecification` as a resource characteristic -- Generated automatically during bootstrap with example values for each SLO/SLE template -- Passed through the message queue when OSOM creates a resource instance +**Bootstrap sequence diagram:** [`doc/bootstrap_phase.puml`](doc/bootstrap_phase.puml) -**SliceService Structure** (RFC 9543 Format) +## RFC 9543 JSON Wire Format -The jsonRequest contains a SliceService object with: +### Namespace Wrapper -| Field | Description | Example | -|-------|-------------|---------| -| `id` | Unique service identifier | `slice-service-001` | -| `description` | Service description | `Example network slice with SLO/SLE template B` | -| `service-tags` | Tags for service classification | `["tag1", "tag2"]` | -| `slo-sle-policy` | Reference to SLO/SLE template | `{"id": "template-B"}` | -| `connection-groups` | Connectivity requirements | Array of connection group definitions | -| `sdp` | Service Demarcation Points | Array with node-id and sdp-ip-address | +All RESTCONF payloads sent to the Network Slice Service Provider use the RFC 9543 namespace-qualified root: -**Example jsonRequest:** ```json { - "id": "example-slice-001", - "description": "Example network slice service using template B", - "service-tags": ["production", "voice"], - "slo-sle-policy": { - "id": "B", - "availabilityLevel": "four-nines", - "mtuSize": 1500 - }, - "connection-groups": [ - { - "id": "A_B", - "connectivity-type": "ietf-vpn-common:any-to-any", - "sdp-directions": ["A", "B"] - } - ], - "sdp": [ - { - "id": "CU-N32", - "node-id": "A", - "sdp-ip-address": ["10.60.11.3"] + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ { "id": "gold", ... } ] }, - { - "id": "UPF-N32", - "node-id": "B", - "sdp-ip-address": ["10.60.10.6"] - } - ] + "slice-service": [ + { "id": "service-001", ... } + ] + } +} +``` + +This wrapper is built in `RestconfClientImpl.buildRfc9543Payload(NetworkSliceServices)` using Jackson `ObjectNode`/`ArrayNode` directly, independent of domain model serialization. + +### SliceService Wire Format + +`SliceService` objects are serialized/deserialized with a matched pair of custom Jackson handlers: + +- **`Rfc9543SliceServiceSerializer`** (`StdSerializer`) — annotated on `SliceService` via `@JsonSerialize`. Produces RFC 9543 nested structure: + - `"sdps": { "sdp": [...] }` + - `"connection-groups": { "connection-group": [...] }` + - `"service-tags": { "tag-type": [...] }` + - `"slo-sle-template": ""` (plain string reference, not object) + - `"status": { "admin-status": "...", "oper-status": "..." }` + +- **`Rfc9543SliceServiceDeserializer`** (`StdDeserializer`) — annotated on `SliceService` via `@JsonDeserialize`. Handles: + - Direct `"slo-sle-template": "silver"` string reference at service level + - Nested `"slo-sle-policy": { "slo-sle-template": "..." }` variant + - `"sdps"` as `{"sdp": [...]}` or flat array + - `"connection-groups"` as `{"connection-group": [...]}` or flat array + - `"service-tags": { "tag-type": [{ "tag-type": "...", "tag-type-value": [...] }] }` + +### Field Name Mapping + +All RFC 9543 hyphenated JSON fields are mapped to Java camelCase using `@JsonProperty`: + +| RFC 9543 JSON field | Java property | Class | +|---------------------|---------------|-------| +| `slo-sle-template` | `sloSleTemplate` | `SliceService` (via deserializer) | +| `connection-groups` | `connectionGroups` | `SliceService` (via deserializer) | +| `sdp-ip-address` | `sdpIpAddress` | `SDP` | +| `p2p-sender-sdp` | `p2pSenderSdp` | `ConnectivityConstruct` | +| `p2p-receiver-sdp` | `p2pReceiverSdp` | `ConnectivityConstruct` | +| `slo-policy` | `sloPolicy` | `SloSleTemplate` | +| `sle-policy` | `slePolicy` | `SloSleTemplate` | +| `metric-bound` | `metricBounds` | `SloPolicy` | +| `metric-type` | `metricType` | `MetricBound` | +| `metric-unit` | `metricUnit` | `MetricBound` | +| `value-description` | `valueDescription` | `MetricBound` | +| `percentile-value` | `percentileValue` | `MetricBound` | +| `max-occupancy-level` | `maxOccupancyLevel` | `SlePolicy` | +| `path-constraints` | `pathConstraints` | `SlePolicy` | +| `admin-status` | `adminStatus` | `ServiceStatus` | +| `oper-status` | `operStatus` | `ServiceStatus` | + +### Enum Handling + +All RFC 9543 enums use `@JsonCreator` / `@JsonValue` for automatic kebab-case ↔ UPPER_SNAKE_CASE conversion: + +```java +// Deserialization: "two-way-bandwidth" → TWO_WAY_BANDWIDTH +@JsonCreator +public static ServiceSloMetricType fromValue(String value) { + String normalized = value.toUpperCase().replace('-', '_'); + return ServiceSloMetricType.valueOf(normalized); // returns null on blank/unknown } + +// Serialization: TWO_WAY_BANDWIDTH → "two-way-bandwidth" +@JsonValue +public String toValue() { return name().toLowerCase().replace('_', '-'); } ``` -**How jsonRequest is Used:** +Enums with this pattern: `ServiceSloMetricType`, `ServiceSecurityType`, `ServiceIsolationType`, `ConnectivityType`. -1. **Bootstrap Phase** - `SloSleTemplateBootstrapService` generates example jsonRequest for each template -2. **Resource Registration** - jsonRequest is stored as a characteristic in the `LogicalResourceSpecification` -3. **Service Ordering** - When OSOM creates a resource instance using the resource spec, it passes the jsonRequest -4. **Resource Creation** - The jsonRequest is included in the CREATE message to the IETF NS Controller's queue -5. **Deserialization** - The `ResourceRepoService` extracts jsonRequest and deserializes it to a `SliceService` object using `Rfc9543SliceServiceDeserializer` -6. **RESTCONF Provisioning** - The `RestconfConsumerService` uses the SliceService object to provision the network slice via RESTCONF to the Network Slice Service Provider (TerflowSDN) +The Jackson `ObjectMapper` used for parsing characteristics is configured with: +```java +ObjectMapper mapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); +mapper.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP)); +``` +This tolerates unknown fields, single values where arrays are expected, and empty-string enum values in collection fields. -**Field Name Mapping:** +## Project Structure -The custom `Rfc9543SliceServiceDeserializer` automatically handles the conversion from RFC 9543's hyphenated field names to Java camelCase properties: +``` +src/main/java/org/etsi/osl/controllers/ietf/ns/ +├── IETFNSGCSpringBoot.java (Application entry point) +├── api/ +│ ├── SloSleTemplateBootstrapService.java (3-step bootstrap on startup) +│ ├── ResourceRepoService.java (Message handler & spec-based dispatch) +│ ├── ResourceSpecificationTemplateRegistry.java (Stores spec name → UUID mappings) +│ ├── CategoryConfigurationService.java (Category/version config) +│ ├── PartnerRouteBuilder.java (Camel EIP routes) +│ ├── config/ +│ │ ├── CatalogClient.java (TMF Catalog API integration) +│ │ ├── ActiveMQComponentConfig.java (Message broker setup) +│ │ └── RestconfConfig.java (RESTCONF client configuration) +│ ├── restconf/ +│ │ ├── RestconfClient.java (RESTCONF interface: CRUD + feasibility) +│ │ ├── RestconfClientImpl.java (HTTP/HTTPS via Spring RestTemplate) +│ │ ├── RestconfConsumerService.java (High-level business logic) +│ │ ├── Rfc9543JsonConverter.java (Parses RFC 9543 RESTCONF responses) +│ │ └── RestconfException.java (Structured YANG error handling) +│ └── domain/model/ +│ ├── NetworkSliceServices.java (Top-level RFC 9543 container) +│ ├── SliceService.java (@JsonSerialize/@JsonDeserialize pair) +│ ├── SloSleTemplate.java (Implements LogicalResourceMappable) +│ ├── SDP.java (Service Demarcation Point) +│ ├── ConnectionGroup.java (Connectivity group) +│ ├── ConnectivityConstruct.java (P2P/P2MP/A2A endpoints) +│ ├── ServiceStatus.java (admin-status / oper-status) +│ ├── ServiceTag.java (service-tags entry) +│ ├── ConnectivityType.java (Enum: P2P, P2MP, A2A) +│ ├── Rfc9543SliceServiceSerializer.java (StdSerializer for RFC 9543 wire format) +│ ├── Rfc9543SliceServiceDeserializer.java (StdDeserializer for RFC 9543 wire format) +│ └── slo_sle/ +│ ├── SloPolicy.java (Service Level Objectives) +│ ├── SlePolicy.java (Service Level Expectations) +│ ├── MetricBound.java (Performance metric constraint) +│ ├── AvailabilityType.java (Availability percentage + periods) +│ ├── PathConstraints.java (Path diversity/disjointness) +│ ├── ServiceSloMetricType.java (14 metric type enum, @JsonCreator/@JsonValue) +│ ├── ServiceSecurityType.java (Security requirement enum) +│ └── ServiceIsolationType.java (Isolation requirement enum) +├── domain/common/ +│ ├── LogicalResourceMappable.java (Interface for TMF LogicalResource mapping) +│ ├── LogicalResourceSpecMappable.java (Interface for TMF Spec mapping) +│ ├── RelatedManagedResourceReference.java (Interface for resource relationships) +│ └── ExcludeFromMapping.java (Annotation to exclude from MapStruct) +├── mappers/ +│ ├── EntityToLogicalResourceSpecMapper.java (MapStruct: domain → ResourceSpec) +│ └── LogicalResourceToEntityMapper.java (MapStruct: ResourceSpec/Resource → domain) +└── repository/impl/ + ├── TMFResourceSpecRepositoryImpl.java (TMF Catalog API client) + └── TMFResourceInventoryRepositoryImpl.java (TMF Inventory API client) + +doc/ +├── bootstrap_phase.puml (Bootstrap sequence diagram) +├── resource_creation_lifecycle.puml (Resource lifecycle diagram) +└── NSC/ + ├── RESTCONF_CONSUMER_GUIDE.md + ├── RESTCONF_IMPLEMENTATION.md + ├── SLO_SLE_TEMPLATE_GUIDE.md + ├── SLO_SLE_REFACTORING_SUMMARY.md + ├── draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt + └── examples/ (RFC 9543 JSON test payloads) +``` -- `service-tags` → `serviceTags` (List) -- `slo-sle-policy` → `sloSleTemplate` (SloSleTemplate) -- `connection-groups` → `connectionGroups` (List) -- `sdp-ip-address` → `sdpIpAddress` (List) -- `connectivity-type` → `connectivityType` (ConnectivityType enum: P2P, P2MP, A2A) +## OpenSlice Integration ### Resource Lifecycle When OpenSlice OSOM creates a resource using a registered resource specification: -1. OSOM sends a `CREATE` message to the controller's queue, i.e. `CREATE/ns.ietf.controllers.osl.etsi.org/v1alpha/0.1.0` -2. Controller parses the resource request from the message -3. Controller provisions the network slice to the RFC 9543 provider (TeraflowSDN - TFS) -4. Controller updates the resource status in OpenSlice: - - **Success:** Status = `AVAILABLE`, Message includes provisioned service ID - - **Failure:** Status = `UNKNOWN`, Health = `Unhealthy`, Message describes error +1. OSOM sends a `CREATE` message to `CREATE/ns.ietf.controllers.osl.etsi.org/v1alpha/0.1.0` +2. Controller reads the spec name from the resource's `resourceSpecification.name` +3. Handler parses the JSON characteristic payload and deserializes to domain objects +4. Controller calls `RestconfClientImpl` to provision the slice via RESTCONF to TerflowSDN +5. Controller updates the resource status in OpenSlice: + - **Success:** Status = `AVAILABLE`, administrative state = `UNLOCKED` + - **Failure:** Status = `UNKNOWN`, health = `Unhealthy`, message describes error -**Resource Creation Sequence Diagram:** +**Resource creation sequence diagram:** [`doc/resource_creation_lifecycle.puml`](doc/resource_creation_lifecycle.puml) -See: [`doc/resource_creation_lifecycle.puml`](doc/resource_creation_lifecycle.puml) -![`doc/resource_creation_lifecycle.puml`](//www.plantuml.com/plantuml/png/TLN1afj64Btp5LqwhQjMqhgI7AAgkAnbMKVsIkm2SWuXXmcq5kpH39aPj5PIlkO3yedvaZJ3I8DmvGA2UQ_xTVUZzLORfaoxvm4hZ5GmYHiKAQyBX3YDrnDWslu86eBLHTqmOM2oB4nzmFKJt2hv6PL5FMsZRJI2DQoe44AsHsunuN8TFLnY4jIoLGKcsCWKrO4oMsYIf2FWhlZnX--_izeSC1Ttg57LZDT_EXLtKHebtsGmXHIxK0hTxb5TP0b7MJ9KEovVqhjDBgwIACt2U7CF7GNLAql9xkdmaXGomxXy3dcDmX0lMz_3yd7rojMSBH_YTq7GjH6cRzxqdLh0Ovnc42RHCejWZrgpPohegSKM5-xrV3QRpG-l6MygDh-PlPxTvE9MbiS5_ALSsrRbDNpIKYJuHulQN0DHlWQicmypw8OIs9lDRIUmW4Is1azPFRoVJs1l5avJM42ZP478qwH2XOIzSkHNdjsDBA2BPqPVZABZeKBOARdFnKa_51Nh8AXgJGtLb_oFDDcIGWy3-1HsrWlOiwP1DIDL9U5Rl1g0lJhdZC3UXlGrQw0wDXKAGfMmgv6N6epCnNjsO51qvWsPnguDbCVKg1UvqBs9feOzQ_Ztxt_0lSZecKwvd6gEqqQIMu_zEPSfnmpt3QSykIYa44ZQf9W-AzasVHJ_zfrzUP9xxiZG_Y1__qJWeCHHaRw9OJ--Gwqfv91xfGSFmnc6tGp3dcwjJaRTPZ1dJJqNUdwucLlcrDKxpyMl5vhUf_7cDn-l_rhi5QPdIMbRT8uQbVDIcmfMTxuUH-y5-HuydWh2yYB0a4YCaHUtZNNIsNZQCDDsewp5JgvxCyDFC77QTYtdnrmqFXTzTQxeLVbraeiA6JTLQiDqn9_jyt7YpNq-MtvShXwMjm-Hr-JMmhUwQW5kV-HYy9KxlTSmaElRxA85xZveLdAAszeyvR193kr43TP3ACccE48JwBmQ_1fpgwHmZ271OjtkiOnk0ovW6NuOejoA4f9-omCgYmh02wyr2FdYUzXrwFmlHK0cIX1eNgJw3EJDdZ6O6qW1Vgq7sC4bAGreN10Bk6SrRrKNwS8l8JD51VYbmCPGdF5nYWR63jYAwlyziDrysCI_RCP-GwMaWX8DgT0-oSWVxkFOMQdEkmuz79oj1Skknk0jNTez_my0) +### jsonRequest and SliceService Model -## Project Structure +The `SliceServiceAsJson` characteristic carries the full RFC 9543 Network Slice Service JSON. The correct wire format uses RFC 9543 hyphenated names and nested containers: +```json +{ + "id": "example-slice-001", + "description": "Example network slice with SLO/SLE template silver", + "slo-sle-template": "silver", + "service-tags": { + "tag-type": [ + { "tag-type": "service", "tag-type-value": ["L3"] } + ] + }, + "sdps": { + "sdp": [ + { "id": "CU-N32", "node-id": "A", "sdp-ip-address": ["10.60.11.3"] }, + { "id": "UPF-N32", "node-id": "B", "sdp-ip-address": ["10.60.10.6"] } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "cg-001", + "connectivity-type": "point-to-point", + "connectivity-construct": [ + { "id": "cc-001", "p2p-sender-sdp": "CU-N32", "p2p-receiver-sdp": "UPF-N32" } + ] + } + ] + } +} ``` -src/main/java/org/etsi/osl/controllers/ietf/ns/ -├── IETFNSGCSpringBoot.java (Application entry point) -├── api/ -│ ├── SloSleTemplateBootstrapService (Registration & example creation) -│ ├── ResourceRepoService (Message handler & provisioning) -│ ├── PartnerRouteBuilder (Camel EIP routes) -│ ├── config/ -│ │ ├── CatalogClient (TMF Catalog API) -│ │ ├── ActiveMQComponentConfig (Message broker setup) -│ │ └── RestconfConfig (RESTCONF client) -│ ├── restconf/ -│ │ ├── RestconfClient (RESTCONF interface) -│ │ ├── RestconfClientImpl (HTTP implementation) -│ │ ├── RestconfConsumerService (Business logic) -│ │ └── RestconfException (Error handling) -│ └── domain/model/ -│ ├── SliceService.java (RFC 9543 slice service) -│ ├── SloSleTemplate.java (SLO/SLE policies) -│ ├── Rfc9543SliceServiceDeserializer (JSON field mapping) -│ └── ... (connection groups, SDPs, metrics) -└── repository/ - ├── SloSleTemplateRepository (Template persistence) - └── impl/InMemorySloSleTemplateRepository (In-memory storage) -``` + +The `SloSleTemplatesAsJsonArray` characteristic on `IETFNetworkSliceServicesSpec` carries an array of SLO/SLE template objects in the same hyphenated RFC 9543 format. ## Getting Started @@ -250,33 +358,31 @@ src/main/java/org/etsi/osl/controllers/ietf/ns/ - Java 17+ - Maven 3.6+ -- ActiveMQ/Artemis running on `tcp://localhost:61616` (default: artemis/artemis) +- ActiveMQ/Artemis running on `tcp://localhost:61616` (default credentials: artemis/artemis) - OpenSlice OSOM with TMF Catalog API -- RFC 9543 Network Slice Service Provider (RESTCONF endpoint) +- RFC 9543 Network Slice Service Provider (RESTCONF endpoint, optional) ### Build and Run ```bash -# Clean build and run tests -mvn clean test - # Build the application mvn clean package -# Run locally +# Run locally (auto-starts server) mvn spring-boot:run # Run packaged JAR java -jar target/org.etsi.osl.controllers.ietf.ns-0.0.1-SNAPSHOT.jar + +# Build without tests +mvn clean package -DskipTests ``` ### Docker Deployment ```bash -# Build Docker image docker build -t ns-ietf-controller:latest . -# Run container docker run -p 8080:8080 \ -e SPRING_ACTIVEMQ_BROKERURL=tcp://activemq:61616 \ -e SPRING_APPLICATION_NAME=ns-ietf-controller \ @@ -287,58 +393,39 @@ docker run -p 8080:8080 \ All runtime settings are configured in `application.yml`: -- **Server port:** 0 (OS assigns available port, see startup logs - in general it is not used) +- **Server port:** 0 (OS assigns available port — check startup logs) - **ActiveMQ broker URL:** `tcp://localhost:61616` -- **Queue names:** 21 configured endpoints (see `application.yml` for full list) -- **RESTCONF provider URL:** Configure in `RestconfConfig` +- **Queue names:** 21 configured endpoints (see `application.yml`) +- **RESTCONF provider URL:** Configure in `RestconfConfig` (if omitted, Step 2 of bootstrap is skipped) +- **Category:** `ns.ietf.controllers.osl.etsi.org/v1alpha` - **OAuth signing key:** For resource update signatures -## Testing - - -### Unit Tests - -Comprehensive unit tests validate: -- RFC 9543 JSON deserialization -- Field name mapping (hyphenated → camelCase) -- SLO/SLE template parsing -- Connection group configuration -- SDP IP address handling +## Technology Stack -Run tests: -```bash -mvn clean test -``` +- **Framework:** Spring Boot 3.2.2, Java 17 +- **Messaging:** Apache Camel 4.0.0-RC2, Apache ActiveMQ (JMS) +- **RESTCONF:** Custom implementation per RFC 8040, targeting RFC 9543 data model +- **Object Mapping:** MapStruct 1.5.3.Final (requires annotation processing) +- **JSON Processing:** Jackson with `@JsonProperty`, `@JsonCreator`/`@JsonValue` on enums, and custom `StdSerializer`/`StdDeserializer` for `SliceService` +- **Security:** Spring Security, OAuth2 resource server, Keycloak 22.0.1 +- **Utilities:** Lombok 1.18.28, Guava 32.0.0 ## Documentation -Key documentation and diagrams in `doc/NSC/`: - **Architecture Diagrams:** -- **bootstrap_phase.puml** - Bootstrap sequence showing template retrieval and registration -- **resource_creation_lifecycle.puml** - Resource creation workflow from OSOM to RESTCONF provisioning +- [`doc/bootstrap_phase.puml`](doc/bootstrap_phase.puml) — 3-step bootstrap sequence +- [`doc/resource_creation_lifecycle.puml`](doc/resource_creation_lifecycle.puml) — resource lifecycle from OSOM to RESTCONF -**Implementation Guides:** -- **RESTCONF_CONSUMER_GUIDE.md** - RFC 9543 overview and RESTCONF operations -- **RESTCONF_IMPLEMENTATION.md** - Implementation patterns with examples -- **SLO_SLE_TEMPLATE_GUIDE.md** - Complete API reference -- **SLO_SLE_REFACTORING_SUMMARY.md** - Refactoring details +**Implementation Guides (doc/NSC/):** +- `RESTCONF_CONSUMER_GUIDE.md` — RFC 9543 overview and RESTCONF operations +- `RESTCONF_IMPLEMENTATION.md` — Implementation patterns with examples +- `SLO_SLE_TEMPLATE_GUIDE.md` — SLO/SLE API reference +- `SLO_SLE_REFACTORING_SUMMARY.md` — Refactoring history and class hierarchy **Test Resources:** -- **examples/** - JSON test requests and payloads -- **draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt** - RFC 9543 specification - -## Technology Stack - -- **Framework:** Spring Boot 3.2.2 -- **Messaging:** Apache Camel 4.0.0-RC2, Apache ActiveMQ (JMS) -- **RESTCONF:** Custom implementation per RFC 9543 -- **Object Mapping:** MapStruct 1.5.3.Final -- **JSON Processing:** Jackson 2.8.11 with custom deserializers -- **Security:** Spring Security, OAuth2, Keycloak 22.0.1 -- **Utilities:** Lombok 1.18.28, Guava 32.0.0 +- `doc/NSC/examples/` — RFC 9543 JSON test payloads +- `doc/NSC/draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt` — RFC 9543 specification ## License This project is part of the OpenSlice OSL ETSI SDG, licensed under Apache 2.0 - diff --git a/doc/bootstrap_phase.puml b/doc/bootstrap_phase.puml index 0bf1457..60d4e93 100644 --- a/doc/bootstrap_phase.puml +++ b/doc/bootstrap_phase.puml @@ -1,56 +1,66 @@ @startuml Bootstrap_Phase -actor "IETF NS Controller" as IETFNS -participant "RestconfConsumerService" as RCS -participant "RestconfClient" as RC -participant "TerflowSDN\n(RESTCONF Server)" as TFSDN +actor "IETF NS Controller\n(Spring Boot)" as IETFNS participant "SloSleTemplateBootstrapService" as BOOTSTRAP -participant "CatalogClient" as CATALOG -participant "OpenSlice TMF API" as TMF +participant "ResourceSpecificationTemplateRegistry" as REGISTRY +participant "RestconfClient" as RC +participant "RESTCONF Provider\n(e.g. TerflowSDN)" as PROVIDER +participant "TMFResourceSpecRepositoryImpl" as REPO +participant "OpenSlice TMF Catalog API" as TMF -IETFNS ->> BOOTSTRAP: ApplicationReady Event +IETFNS ->> BOOTSTRAP: ApplicationReady Event\n(CommandLineRunner.run) activate BOOTSTRAP -BOOTSTRAP ->> RCS: Retrieve templates from provider -activate RCS - -RCS ->> RC: GET /api/ns/v0/slice-service-templates -activate RC - -RC ->> TFSDN: RESTCONF GET request -activate TFSDN -TFSDN -->> RC: Return SloSleTemplate[] -deactivate TFSDN - -RC -->> RCS: SliceService[] with SloSleTemplate[] -deactivate RC - -RCS -->> BOOTSTRAP: List -deactivate RCS - -BOOTSTRAP ->> BOOTSTRAP: For each SloSleTemplate:\n1. Create LogicalResourceSpecification\n2. Generate example jsonRequest\n3. Add jsonRequest characteristic +== Step 1: Create Fixed Resource Specification Templates == -BOOTSTRAP ->> CATALOG: Register LogicalResourceSpecification -activate CATALOG +BOOTSTRAP ->> BOOTSTRAP: createSloSleSpecification()\n→ IETFSloSleTemplateSpec\n + SloSleTemplateAsJson (TEXT) +BOOTSTRAP ->> BOOTSTRAP: createSliceServiceSpecification()\n→ IETFSliceServiceSpec\n + SliceServiceAsJson (TEXT) +BOOTSTRAP ->> BOOTSTRAP: createNetworkSliceServicesSpecification()\n→ IETFNetworkSliceServicesSpec\n + SloSleTemplatesAsJsonArray (TEXT)\n + SliceServicesAsJsonArray (TEXT) -CATALOG ->> TMF: POST to Resource Catalog API\nCategory: ns.ietf.controllers.osl.etsi.org/v1alpha -activate TMF -TMF -->> CATALOG: ResourceSpecification created -deactivate TMF +== Step 2: Retrieve Dynamic Templates from RESTCONF Provider (optional) == -CATALOG -->> BOOTSTRAP: Success -deactivate CATALOG +alt RESTCONF client available + BOOTSTRAP ->> RC: getSloSleTemplates() + activate RC + RC ->> PROVIDER: GET /restconf/data/ietf-network-slice-service:\nnetwork-slice-services/slo-sle-templates\nAccept: application/yang-data+json + activate PROVIDER + PROVIDER -->> RC: JSON (RFC 9543 slo-sle-template list) + deactivate PROVIDER + RC -->> BOOTSTRAP: JSON string + deactivate RC + BOOTSTRAP ->> BOOTSTRAP: Rfc9543JsonConverter.parseSloSleTemplates(json)\n→ List + BOOTSTRAP ->> BOOTSTRAP: mapper.toLogicalResourceSpec(template)\nfor each retrieved template +else RESTCONF client not available + BOOTSTRAP ->> BOOTSTRAP: Skip provider retrieval (warn) +end -BOOTSTRAP ->> BOOTSTRAP: Create example SliceService\nfor testing +== Step 3: Register All Specifications in TMF Catalog == -BOOTSTRAP ->> RCS: Store SloSleTemplate in memory -RCS -->> BOOTSTRAP: Stored +loop for each LogicalResourceSpecification + BOOTSTRAP ->> REPO: createOrUpdateResourceSpecByNameCategoryVersion(spec) + activate REPO + REPO ->> TMF: POST /resourceCatalogManagement/v4/resourceSpecification\nCategory: ns.ietf.controllers.osl.etsi.org/v1alpha + activate TMF + TMF -->> REPO: LogicalResourceSpecification { uuid } + deactivate TMF + REPO -->> BOOTSTRAP: registered spec with UUID + deactivate REPO + BOOTSTRAP ->> REGISTRY: registerTemplate(spec.name, spec.uuid) + activate REGISTRY + REGISTRY -->> BOOTSTRAP: stored + deactivate REGISTRY +end -BOOTSTRAP -->> IETFNS: Bootstrap complete +BOOTSTRAP -->> IETFNS: Bootstrap complete\n(N template IDs registered) deactivate BOOTSTRAP note over IETFNS - IETF NS Controller is now ready to receive - CREATE/UPDATE/DELETE messages - for network slice services + Controller is now ready to receive + CREATE / UPDATE / DELETE messages + on ActiveMQ queues: + CREATE/ns.ietf.controllers.osl.etsi.org/v1alpha/0.1.0 + Spec names mapped in ResourceSpecificationTemplateRegistry: + IETFSloSleTemplateSpec → uuid-1 + IETFSliceServiceSpec → uuid-2 + IETFNetworkSliceServicesSpec → uuid-3 end note @enduml -- GitLab From e841ec7aad5ae4fdb4dde410824ab7ef33af7ed7 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 14 May 2026 14:41:04 +0300 Subject: [PATCH 07/12] support SloSle temaplates --- README.md | 51 +++-- .../api/SloSleTemplateBootstrapService.java | 2 +- .../Rfc9543SloSleTemplateDeserializer.java | 64 +++++++ .../Rfc9543SloSleTemplateSerializer.java | 59 ++++++ .../ns/api/domain/model/SloSleTemplate.java | 4 + .../api/domain/model/slo_sle/SlePolicy.java | 1 + .../api/restconf/RestconfConsumerService.java | 25 ++- .../repository/impl/ResourceRepoService.java | 179 +++++++++++++++++- 8 files changed, 363 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SloSleTemplateDeserializer.java create mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SloSleTemplateSerializer.java diff --git a/README.md b/README.md index 72fcf7d..2f7a572 100644 --- a/README.md +++ b/README.md @@ -94,14 +94,24 @@ For this controller: ### Message Flow +**CREATE:** 1. **OpenSlice OSOM** sends a resource request to the controller's CREATE queue 2. **Apache Camel route** (`PartnerRouteBuilder`) intercepts the message and triggers `ResourceRepoService` 3. **ResourceRepoService** reads the spec name from the resource's `resourceSpecification.name` -4. **Spec-based dispatch:** routes to `applyIETFSloSleTemplateSpec`, `applyIETFSliceServiceSpec`, or `applyIETFNetworkSliceServicesSpec` depending on the spec name -5. **RFC 9543 deserializer** parses the JSON characteristic payload using `Rfc9543SliceServiceDeserializer` (custom Jackson `StdDeserializer`) -6. **RESTCONF client** (`RestconfClientImpl`) provisions the slice service to the Network Slice Service Provider using the RFC 9543 wire format +4. **Spec-based dispatch:** routes to `applySloSleTemplateRequest`, `applySliceServiceRequest`, or `applyNetworkSliceServicesRequest` depending on the spec name +5. **RFC 9543 deserializer** parses the JSON characteristic payload (`Rfc9543SloSleTemplateDeserializer` or `Rfc9543SliceServiceDeserializer`) +6. **RESTCONF client** (`RestconfClientImpl`) provisions the resource to the Network Slice Service Provider 7. **Response** is sent back via catalog update (resource status set to `AVAILABLE`, `TERMINATED`, or `UNKNOWN`) +**DELETE:** +1. **OpenSlice OSOM** sends a resource request to the controller's DELETE queue +2. **Apache Camel route** triggers `ResourceRepoService.deleteResource()` +3. **Spec detection from characteristics:** since `ResourceUpdate` carries no spec name, the handler inspects which characteristics are present in the resource: + - `SloSleTemplateAsJson` present → parses template ID → `RestconfConsumerService.decommissionSloSleTemplate(id)` + - `SliceServiceAsJson` present → parses service ID → `RestconfConsumerService.decommissionSliceService(id)` + - `SloSleTemplatesAsJsonArray` / `SliceServicesAsJsonArray` present → `RestconfConsumerService.decommissionNetworkSliceServices()` +4. **Response:** status set to `TERMINATED` + Healthy on success, `UNKNOWN` + Unhealthy on RESTCONF failure, `TERMINATED` + Degraded when no RESTCONF consumer is configured + ### Message Headers Important metadata is included in message headers: @@ -165,6 +175,14 @@ All RESTCONF payloads sent to the Network Slice Service Provider use the RFC 954 This wrapper is built in `RestconfClientImpl.buildRfc9543Payload(NetworkSliceServices)` using Jackson `ObjectNode`/`ArrayNode` directly, independent of domain model serialization. +### SloSleTemplate Wire Format + +`SloSleTemplate` objects are serialized/deserialized with a matched pair of custom Jackson handlers annotated directly on the class: + +- **`Rfc9543SloSleTemplateSerializer`** (`StdSerializer`) — writes only the known RFC 9543 top-level fields (`id`, `description`, `slo-policy`, `sle-policy`, `template-ref`), preventing interface default methods from `LogicalResourceSpecMappable` / `RelatedManagedResourceReference` from leaking into the output. Nested `SloPolicy` and `SlePolicy` are delegated to Jackson's default serialization, which honours their `@JsonProperty` annotations. + +- **`Rfc9543SloSleTemplateDeserializer`** (`StdDeserializer`) — reads `"slo-policy"` and `"sle-policy"` keys explicitly and delegates nested object parsing to Jackson via `codec.treeToValue()`, so `SloPolicy` gets its `"metric-bound"` mapping, `SlePolicy` gets its `"max-occupancy-level"` and `"isolation-requirement"` mappings, etc. + ### SliceService Wire Format `SliceService` objects are serialized/deserialized with a matched pair of custom Jackson handlers: @@ -202,6 +220,7 @@ All RFC 9543 hyphenated JSON fields are mapped to Java camelCase using `@JsonPro | `value-description` | `valueDescription` | `MetricBound` | | `percentile-value` | `percentileValue` | `MetricBound` | | `max-occupancy-level` | `maxOccupancyLevel` | `SlePolicy` | +| `isolation-requirement` | `isolation` | `SlePolicy` | | `path-constraints` | `pathConstraints` | `SlePolicy` | | `admin-status` | `adminStatus` | `ServiceStatus` | | `oper-status` | `operStatus` | `ServiceStatus` | @@ -258,15 +277,17 @@ src/main/java/org/etsi/osl/controllers/ietf/ns/ │ └── domain/model/ │ ├── NetworkSliceServices.java (Top-level RFC 9543 container) │ ├── SliceService.java (@JsonSerialize/@JsonDeserialize pair) -│ ├── SloSleTemplate.java (Implements LogicalResourceMappable) -│ ├── SDP.java (Service Demarcation Point) -│ ├── ConnectionGroup.java (Connectivity group) -│ ├── ConnectivityConstruct.java (P2P/P2MP/A2A endpoints) -│ ├── ServiceStatus.java (admin-status / oper-status) -│ ├── ServiceTag.java (service-tags entry) -│ ├── ConnectivityType.java (Enum: P2P, P2MP, A2A) -│ ├── Rfc9543SliceServiceSerializer.java (StdSerializer for RFC 9543 wire format) -│ ├── Rfc9543SliceServiceDeserializer.java (StdDeserializer for RFC 9543 wire format) +│ ├── SloSleTemplate.java (@JsonSerialize/@JsonDeserialize pair) +│ ├── SDP.java (Service Demarcation Point) +│ ├── ConnectionGroup.java (Connectivity group) +│ ├── ConnectivityConstruct.java (P2P/P2MP/A2A endpoints) +│ ├── ServiceStatus.java (admin-status / oper-status) +│ ├── ServiceTag.java (service-tags entry) +│ ├── ConnectivityType.java (Enum: P2P, P2MP, A2A) +│ ├── Rfc9543SloSleTemplateSerializer.java (StdSerializer for SloSleTemplate) +│ ├── Rfc9543SloSleTemplateDeserializer.java (StdDeserializer for SloSleTemplate) +│ ├── Rfc9543SliceServiceSerializer.java (StdSerializer for SliceService) +│ ├── Rfc9543SliceServiceDeserializer.java (StdDeserializer for SliceService) │ └── slo_sle/ │ ├── SloPolicy.java (Service Level Objectives) │ ├── SlePolicy.java (Service Level Expectations) @@ -311,8 +332,10 @@ When OpenSlice OSOM creates a resource using a registered resource specification 3. Handler parses the JSON characteristic payload and deserializes to domain objects 4. Controller calls `RestconfClientImpl` to provision the slice via RESTCONF to TerflowSDN 5. Controller updates the resource status in OpenSlice: - - **Success:** Status = `AVAILABLE`, administrative state = `UNLOCKED` - - **Failure:** Status = `UNKNOWN`, health = `Unhealthy`, message describes error + - **CREATE success:** Status = `AVAILABLE`, administrative state = `UNLOCKED` + - **CREATE failure:** Status = `UNKNOWN`, health = `Unhealthy`, message describes error + - **DELETE success:** Status = `TERMINATED`, health = `Healthy` + - **DELETE failure:** Status = `UNKNOWN`, health = `Unhealthy`, message describes error **Resource creation sequence diagram:** [`doc/resource_creation_lifecycle.puml`](doc/resource_creation_lifecycle.puml) diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java index 32a5e00..5554016 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/SloSleTemplateBootstrapService.java @@ -361,7 +361,7 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(template); //Override Name, to show NSC templates as specnames... - spec.setName(template.getEntityName() ); + spec.setName( ResourceSpecificationTemplateRegistry.SPEC_SLO_SLE_TEMPLATE +"_" + template.getEntityName() ); spec.setCategory(categoryConfig.getCategoryForSpecifications()); spec.setVersion(categoryConfig.getVersion()); diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SloSleTemplateDeserializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SloSleTemplateDeserializer.java new file mode 100644 index 0000000..1d101ae --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SloSleTemplateDeserializer.java @@ -0,0 +1,64 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import java.io.IOException; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SlePolicy; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SliceTemplateRef; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Custom Jackson deserializer for RFC 9543 formatted SloSleTemplate JSON. + * + * Maps RFC 9543 hyphenated top-level field names: + * - "slo-policy" → sloPolicy + * - "sle-policy" → slePolicy + * - "template-ref" → templateRef + * + * Nested SloPolicy and SlePolicy parsing is delegated back to Jackson, + * which honours the @JsonProperty annotations on those classes + * (e.g. "metric-bound" → metricBounds, "max-occupancy-level" → maxOccupancyLevel, + * "isolation-requirement" → isolation). + */ +public class Rfc9543SloSleTemplateDeserializer extends StdDeserializer { + + private static final Logger logger = LoggerFactory.getLogger(Rfc9543SloSleTemplateDeserializer.class); + + public Rfc9543SloSleTemplateDeserializer() { + super(SloSleTemplate.class); + } + + @Override + public SloSleTemplate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + ObjectMapper codec = (ObjectMapper) jp.getCodec(); + return parseTemplate(node, codec); + } + + private SloSleTemplate parseTemplate(JsonNode node, ObjectMapper codec) throws IOException { + SloSleTemplate t = new SloSleTemplate(); + + if (node.has("id")) t.setId(node.get("id").asText()); + if (node.has("description")) t.setDescription(node.get("description").asText()); + + if (node.has("slo-policy")) { + t.setSloPolicy(codec.treeToValue(node.get("slo-policy"), SloPolicy.class)); + } + + if (node.has("sle-policy")) { + t.setSlePolicy(codec.treeToValue(node.get("sle-policy"), SlePolicy.class)); + } + + if (node.has("template-ref")) { + t.setTemplateRef(codec.treeToValue(node.get("template-ref"), SliceTemplateRef.class)); + } + + logger.debug("Parsed RFC 9543 SloSleTemplate: {}", t.getId()); + return t; + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SloSleTemplateSerializer.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SloSleTemplateSerializer.java new file mode 100644 index 0000000..f37bc96 --- /dev/null +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SloSleTemplateSerializer.java @@ -0,0 +1,59 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Custom Jackson serializer for RFC 9543 formatted SloSleTemplate JSON. + * + * Writes only the known RFC 9543 wire fields (id, description, slo-policy, + * sle-policy, template-ref), preventing interface default methods from + * LogicalResourceSpecMappable / RelatedManagedResourceReference from + * appearing as unwanted JSON properties. + * + * Nested SloPolicy and SlePolicy objects are delegated to Jackson's default + * serialization, which honours the @JsonProperty annotations on those classes. + */ +public class Rfc9543SloSleTemplateSerializer extends StdSerializer { + + private static final Logger logger = LoggerFactory.getLogger(Rfc9543SloSleTemplateSerializer.class); + + public Rfc9543SloSleTemplateSerializer() { + super(SloSleTemplate.class); + } + + @Override + public void serialize(SloSleTemplate t, JsonGenerator gen, SerializerProvider provider) + throws IOException { + gen.writeStartObject(); + + writeIfNotNull(gen, "id", t.getId()); + writeIfNotNull(gen, "description", t.getDescription()); + + if (t.getSloPolicy() != null) { + gen.writeFieldName("slo-policy"); + provider.defaultSerializeValue(t.getSloPolicy(), gen); + } + + if (t.getSlePolicy() != null) { + gen.writeFieldName("sle-policy"); + provider.defaultSerializeValue(t.getSlePolicy(), gen); + } + + if (t.getTemplateRef() != null) { + gen.writeFieldName("template-ref"); + provider.defaultSerializeValue(t.getTemplateRef(), gen); + } + + gen.writeEndObject(); + logger.debug("Serialized RFC 9543 SloSleTemplate: {}", t.getId()); + } + + private void writeIfNotNull(JsonGenerator gen, String field, String value) throws IOException { + if (value != null) gen.writeStringField(field, value); + } +} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java index ed1832a..d240e79 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/SloSleTemplate.java @@ -9,6 +9,8 @@ import org.etsi.osl.controllers.ietf.ns.domain.common.RelatedManagedResourceRefe import org.etsi.osl.tmf.ri639.model.LogicalResource; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import jakarta.persistence.Transient; import lombok.AllArgsConstructor; import lombok.Data; @@ -41,6 +43,8 @@ import lombok.extern.slf4j.Slf4j; * Based on draft-ietf-teas-ietf-network-slice-nbi-yang-25 * /network-slice-services/slo-sle-templates/slo-sle-template */ +@JsonDeserialize(using = Rfc9543SloSleTemplateDeserializer.class) +@JsonSerialize(using = Rfc9543SloSleTemplateSerializer.class) @Data @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java index 1e82650..c7a32e1 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/slo_sle/SlePolicy.java @@ -51,6 +51,7 @@ public class SlePolicy { * * Optional leaf-list (can be empty). */ + @JsonProperty("isolation-requirement") private List isolation = new ArrayList<>(); /** diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java index 6e09d48..d292f79 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfConsumerService.java @@ -159,12 +159,33 @@ public class RestconfConsumerService { */ public void decommissionSliceService(String serviceId) throws RestconfException { logger.info("Decommissioning network slice service: {}", serviceId); - - // Delete from provider restconfClient.deleteSliceService(serviceId); logger.info("Successfully decommissioned service: {}", serviceId); } + /** + * Decommissions a single SLO/SLE template. + * + * @param templateId The unique identifier of the template to remove + * @throws RestconfException if decommissioning fails + */ + public void decommissionSloSleTemplate(String templateId) throws RestconfException { + logger.info("Decommissioning SLO/SLE template: {}", templateId); + restconfClient.deleteSloSleTemplate(templateId); + logger.info("Successfully decommissioned SLO/SLE template: {}", templateId); + } + + /** + * Decommissions the entire network-slice-services container (all slices and templates). + * + * @throws RestconfException if decommissioning fails + */ + public void decommissionNetworkSliceServices() throws RestconfException { + logger.info("Decommissioning entire network-slice-services container"); + restconfClient.deleteAllNetworkSliceServices(); + logger.info("Successfully decommissioned network-slice-services container"); + } + /** * Provisions the entire RFC 9543 network-slice-services container on the provider. * diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java index fef1cc5..624ca82 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/repository/impl/ResourceRepoService.java @@ -82,11 +82,136 @@ public class ResourceRepoService { public Resource deleteResource(Map headers, ResourceUpdate r) { String resourceid = extractResourceId(headers); - r.setResourceStatus(ResourceStatusType.UNKNOWN); - r.addResourceCharacteristicItemShort("status.Health", "deleted", EValueType.TEXT.getValue()); + + // Detect spec type from which characteristics are present and decommission via RESTCONF + r = applyDeleteBySpec(r, resourceid); + return tmfRepository.updateResourceById(resourceid, r); } + /** + * Identifies the spec type from the characteristics carried in the ResourceUpdate, + * issues the corresponding RESTCONF delete, and stamps the result status. + * + * Spec detection order: + * 1. SloSleTemplateAsJson → IETFSloSleTemplateSpec + * 2. SliceServiceAsJson → IETFSliceServiceSpec + * 3. SloSleTemplatesAsJsonArray / SliceServicesAsJsonArray → IETFNetworkSliceServicesSpec + */ + private ResourceUpdate applyDeleteBySpec(ResourceUpdate resourceUpdate, String resourceid) { + String templateJson = null; + String serviceJson = null; + boolean hasNssChars = false; + + for (Characteristic c : resourceUpdate.getResourceCharacteristic()) { + String name = c.getName(); + if ("SloSleTemplateAsJson".equalsIgnoreCase(name)) { + templateJson = c.getValue().getValue(); + } else if ("SliceServiceAsJson".equalsIgnoreCase(name)) { + serviceJson = c.getValue().getValue(); + } else if ("SloSleTemplatesAsJsonArray".equalsIgnoreCase(name) + || "SliceServicesAsJsonArray".equalsIgnoreCase(name)) { + hasNssChars = true; + } + } + + if (templateJson != null && !templateJson.isBlank()) { + return deleteSleSloTemplateViaRestconf(resourceUpdate, resourceid, templateJson); + } + if (serviceJson != null && !serviceJson.isBlank()) { + return deleteSliceServiceViaRestconf(resourceUpdate, resourceid, serviceJson); + } + if (hasNssChars) { + return deleteNetworkSliceServicesViaRestconf(resourceUpdate, resourceid); + } + + // No recognisable spec — just mark terminated locally + logger.warn("Delete for resource {} — no recognised spec characteristics; marking terminated without RESTCONF call", resourceid); + return applyStatus(resourceUpdate, "Deleted (no RESTCONF target identified)", "Healthy", ResourceStatusType.UNKNOWN); + } + + private ResourceUpdate deleteSleSloTemplateViaRestconf(ResourceUpdate resourceUpdate, + String resourceid, String templateJson) { + try { + ObjectMapper mapper = lenientMapper(); + SloSleTemplate template = parseSloSleTemplate(mapper, templateJson); + + if (template == null || template.getId() == null) { + return applyStatus(resourceUpdate, + "Failed: could not parse SloSleTemplate ID for RESTCONF delete", + "Unhealthy", ResourceStatusType.UNKNOWN); + } + + logger.info("Deleting SLO/SLE template '{}' for resource {}", template.getId(), resourceid); + + if (restconfConsumerService != null) { + restconfConsumerService.decommissionSloSleTemplate(template.getId()); + return applyStatus(resourceUpdate, + "Successfully deleted SLO/SLE template '" + template.getId() + "' via RESTCONF", + "Healthy", ResourceStatusType.UNKNOWN); + } else { + logger.warn("RESTCONF consumer not available — SLO/SLE template not deleted from provider"); + return applyStatus(resourceUpdate, + "Marked deleted locally; RESTCONF consumer not available", + "Degraded", ResourceStatusType.UNKNOWN); + } + } catch (Exception e) { + logger.error("Error deleting IETFSloSleTemplateSpec for resource {}", resourceid, e); + return applyStatus(resourceUpdate, "Failed: " + e.getMessage(), "Unhealthy", ResourceStatusType.UNKNOWN); + } + } + + private ResourceUpdate deleteSliceServiceViaRestconf(ResourceUpdate resourceUpdate, + String resourceid, String serviceJson) { + try { + ObjectMapper mapper = lenientMapper(); + SliceService service = parseSliceService(mapper, serviceJson); + + if (service == null || service.getId() == null) { + return applyStatus(resourceUpdate, + "Failed: could not parse SliceService ID for RESTCONF delete", + "Unhealthy", ResourceStatusType.UNKNOWN); + } + + logger.info("Deleting SliceService '{}' for resource {}", service.getId(), resourceid); + + if (restconfConsumerService != null) { + restconfConsumerService.decommissionSliceService(service.getId()); + return applyStatus(resourceUpdate, + "Successfully deleted SliceService '" + service.getId() + "' via RESTCONF", + "Healthy", ResourceStatusType.UNKNOWN); + } else { + logger.warn("RESTCONF consumer not available — SliceService not deleted from provider"); + return applyStatus(resourceUpdate, + "Marked deleted locally; RESTCONF consumer not available", + "Degraded", ResourceStatusType.UNKNOWN); + } + } catch (Exception e) { + logger.error("Error deleting IETFSliceServiceSpec for resource {}", resourceid, e); + return applyStatus(resourceUpdate, "Failed: " + e.getMessage(), "Unhealthy", ResourceStatusType.UNKNOWN); + } + } + + private ResourceUpdate deleteNetworkSliceServicesViaRestconf(ResourceUpdate resourceUpdate, String resourceid) { + logger.info("Deleting entire NetworkSliceServices container for resource {}", resourceid); + try { + if (restconfConsumerService != null) { + restconfConsumerService.decommissionNetworkSliceServices(); + return applyStatus(resourceUpdate, + "Successfully deleted NetworkSliceServices container via RESTCONF", + "Healthy", ResourceStatusType.UNKNOWN); + } else { + logger.warn("RESTCONF consumer not available — NetworkSliceServices not deleted from provider"); + return applyStatus(resourceUpdate, + "Marked deleted locally; RESTCONF consumer not available", + "Degraded", ResourceStatusType.UNKNOWN); + } + } catch (Exception e) { + logger.error("Error deleting IETFNetworkSliceServicesSpec for resource {}", resourceid, e); + return applyStatus(resourceUpdate, "Failed: " + e.getMessage(), "Unhealthy", ResourceStatusType.UNKNOWN); + } + } + // ========================================================================= // Spec-specific handlers // ========================================================================= @@ -174,7 +299,15 @@ public class ResourceRepoService { } try { - SloSleTemplate template = lenientMapper().readValue(templateJson, SloSleTemplate.class); + ObjectMapper mapper = lenientMapper(); + SloSleTemplate template = parseSloSleTemplate(mapper, templateJson); + + if (template == null || template.getId() == null) { + return applyStatus(resourceUpdate, + "Failed: could not parse a valid SloSleTemplate from SloSleTemplateAsJson", + "Unhealthy", ResourceStatusType.SUSPENDED); + } + logger.info("Provisioning SLO/SLE template '{}' for resource {}", template.getId(), resourceid); if (restconfConsumerService != null) { @@ -201,7 +334,8 @@ public class ResourceRepoService { * Handles IETFSliceServiceSpec resources. * * Extracts the {@code SliceServiceAsJson} characteristic, parses it into a - * {@link SliceService} and provisions it on the RESTCONF provider. + * {@link SliceService} using the RFC 9543 deserializer, and provisions it + * on the RESTCONF provider. */ private ResourceUpdate applySliceServiceRequest(ResourceUpdate resourceUpdate, String resourceid) { String serviceJson = ""; @@ -223,7 +357,15 @@ public class ResourceRepoService { } try { - SliceService service = lenientMapper().readValue(serviceJson, SliceService.class); + ObjectMapper mapper = lenientMapper(); + SliceService service = parseSliceService(mapper, serviceJson); + + if (service == null || service.getId() == null) { + return applyStatus(resourceUpdate, + "Failed: could not parse a valid SliceService from SliceServiceAsJson", + "Unhealthy", ResourceStatusType.SUSPENDED); + } + logger.info("Provisioning SliceService '{}' for resource {}", service.getId(), resourceid); if (restconfConsumerService != null) { @@ -264,6 +406,19 @@ public class ResourceRepoService { return mapper; } + /** + * Parses a single SloSleTemplate from RFC 9543 JSON. + * Handles both a single-object payload ({...}) and a single-element array ([{...}]). + */ + private SloSleTemplate parseSloSleTemplate(ObjectMapper mapper, String json) throws Exception { + if (json == null || json.isBlank()) return null; + if (json.trim().startsWith("[")) { + SloSleTemplate[] arr = mapper.readValue(json, SloSleTemplate[].class); + return (arr != null && arr.length > 0) ? arr[0] : null; + } + return mapper.readValue(json, SloSleTemplate.class); + } + private List parseSloSleTemplates(ObjectMapper mapper, String json) throws Exception { if (json == null || json.isBlank() || "[]".equals(json.trim())) { @@ -273,6 +428,20 @@ public class ResourceRepoService { return Arrays.asList(arr != null ? arr : new SloSleTemplate[0]); } + /** + * Parses a single SliceService from RFC 9543 JSON. + * Handles both a single-object payload ({...}) and a single-element array ([{...}]). + * Uses Rfc9543SliceServiceDeserializer wired via @JsonDeserialize on SliceService. + */ + private SliceService parseSliceService(ObjectMapper mapper, String json) throws Exception { + if (json == null || json.isBlank()) return null; + if (json.trim().startsWith("[")) { + SliceService[] arr = mapper.readValue(json, SliceService[].class); + return (arr != null && arr.length > 0) ? arr[0] : null; + } + return mapper.readValue(json, SliceService.class); + } + private List parseSliceServices(ObjectMapper mapper, String json) throws Exception { if (json == null || json.isBlank() || "[]".equals(json.trim())) { -- GitLab From 3da43d349d3f10bbaec199d09919ad2b9c9422a9 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 14 May 2026 16:59:29 +0300 Subject: [PATCH 08/12] updated readme and added spme tests --- README.md | 16 +- .../ietf/ns/IETFNSGCSpringBoot.java | 3 +- .../ietf/ns/demo/RestconfServerDemo.java | 43 -- .../ietf/ns/demo/Rfc9543DemoService.java | 223 ------- .../Rfc9543NetworkSliceServicesResponse.java | 136 ---- .../ietf/ns/demo/Rfc9543RestController.java | 119 ---- .../ietf/ns/demo/Rfc9543SloSleTemplate.java | 173 ----- .../ietf/ns/demo/SecurityConfig.java | 103 --- ...c9543SerializationDeserializationTest.java | 611 ++++++++++++++++++ .../Rfc9543SliceServiceDeserializerTest.java | 2 +- .../api/restconf/RestconfClientImplTest.java | 4 +- 11 files changed, 619 insertions(+), 814 deletions(-) delete mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/RestconfServerDemo.java delete mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543DemoService.java delete mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543NetworkSliceServicesResponse.java delete mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543RestController.java delete mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543SloSleTemplate.java delete mode 100644 src/main/java/org/etsi/osl/controllers/ietf/ns/demo/SecurityConfig.java create mode 100644 src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SerializationDeserializationTest.java diff --git a/README.md b/README.md index 2f7a572..beb39dc 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,8 @@ ## Overview -The **IETF NS Controller** is a Spring Boot microservice implementing the customer part of the **IETF RFC 9543 Network Slice Service** for Teraflow SDN. This microservice acts as an **OSL Resource Controller** that bridges -OpenSlice microservices (like TMF API, Service Orchestrator (OSOM)) and Network Slice Service Providers (e.g., TerflowSDN) through a **message-driven architecture** using Apache Camel and ActiveMQ. - -**WARNING: This artifact is still under testing and not fully functional** +The **IETF NS Controller** is a Spring Boot microservice implementing the customer part of the **IETF RFC 9543 Network Slice Service** for controllers who support the IETF RFC 9543 like Teraflow SDN. This microservice acts as an **OSL Resource Controller** that bridges +OpenSlice microservices (like TMF API, Service Orchestrator (OSOM)) and Network Slice Service Providers (e.g., TeraflowSDN) through a **message-driven architecture** using Apache Camel and ActiveMQ. ### Architectural approach @@ -30,7 +28,7 @@ OpenSlice microservices (like TMF API, Service Orchestrator (OSOM)) and Network │ ▼ ┌─────────────────────────────────────────────────────────────┐ -│ Network Slice Service Provider (e.g. TerflowSDN) │ +│ Network Slice Service Provider (e.g. TeraflowSDN) │ │ (RESTCONF Server implementing RFC 9543) │ └─────────────────────────────────────────────────────────────┘ ``` @@ -330,7 +328,7 @@ When OpenSlice OSOM creates a resource using a registered resource specification 1. OSOM sends a `CREATE` message to `CREATE/ns.ietf.controllers.osl.etsi.org/v1alpha/0.1.0` 2. Controller reads the spec name from the resource's `resourceSpecification.name` 3. Handler parses the JSON characteristic payload and deserializes to domain objects -4. Controller calls `RestconfClientImpl` to provision the slice via RESTCONF to TerflowSDN +4. Controller calls `RestconfClientImpl` to provision the slice via RESTCONF to TeraflowSDN 5. Controller updates the resource status in OpenSlice: - **CREATE success:** Status = `AVAILABLE`, administrative state = `UNLOCKED` - **CREATE failure:** Status = `UNKNOWN`, health = `Unhealthy`, message describes error @@ -439,12 +437,6 @@ All runtime settings are configured in `application.yml`: - [`doc/bootstrap_phase.puml`](doc/bootstrap_phase.puml) — 3-step bootstrap sequence - [`doc/resource_creation_lifecycle.puml`](doc/resource_creation_lifecycle.puml) — resource lifecycle from OSOM to RESTCONF -**Implementation Guides (doc/NSC/):** -- `RESTCONF_CONSUMER_GUIDE.md` — RFC 9543 overview and RESTCONF operations -- `RESTCONF_IMPLEMENTATION.md` — Implementation patterns with examples -- `SLO_SLE_TEMPLATE_GUIDE.md` — SLO/SLE API reference -- `SLO_SLE_REFACTORING_SUMMARY.md` — Refactoring history and class hierarchy - **Test Resources:** - `doc/NSC/examples/` — RFC 9543 JSON test payloads - `doc/NSC/draft-ietf-teas-ietf-network-slice-nbi-yang-25.txt` — RFC 9543 specification diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/IETFNSGCSpringBoot.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/IETFNSGCSpringBoot.java index b1403e2..e23d6fa 100644 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/IETFNSGCSpringBoot.java +++ b/src/main/java/org/etsi/osl/controllers/ietf/ns/IETFNSGCSpringBoot.java @@ -12,8 +12,7 @@ import org.springframework.context.annotation.ComponentScan; /** - * For implementing the callback and events, it might be useful to check the DDD pattern: - * https://www.baeldung.com/spring-data-ddd + * * * @author ctranoris diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/RestconfServerDemo.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/RestconfServerDemo.java deleted file mode 100644 index 5a96290..0000000 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/RestconfServerDemo.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.etsi.osl.controllers.ietf.ns.demo; - -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.annotation.ComponentScan; -import lombok.extern.slf4j.Slf4j; - -/** - * Simple demo RESTCONF server that serves example network-slice-services. - * - * This server: - * - Listens on port 11880 - * - Provides RESTCONF endpoints for network slice services - * - Serves example SLO/SLE templates - * - Demonstrates RFC 8040 RESTCONF protocol compliance - * - * Usage: - * 1. Run this application - * 2. Access endpoints at http://localhost:11880/restconf/data/... - * - * Example RESTCONF operations: - * - GET http://localhost:11880/restconf/data/ietf-network-slice-service:network-slice-services/slo-sle-templates - * - GET http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services - * - GET http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services - * - POST http://localhost:11880/restconf/data/ietf-network-slice:network-slice-services/slice-services - * - * @author ctranoris - */ -@SpringBootApplication -@ComponentScan(basePackages = { - "org.etsi.osl.controllers.ietf.ns.demo", - -}) -@Slf4j -public class RestconfServerDemo { - - public static void main(String[] args) { - log.info("Starting RESTCONF Server Demo on port 11880..."); - new SpringApplicationBuilder(RestconfServerDemo.class) - .profiles("demo") - .run(args); - } -} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543DemoService.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543DemoService.java deleted file mode 100644 index 82db05d..0000000 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543DemoService.java +++ /dev/null @@ -1,223 +0,0 @@ -package org.etsi.osl.controllers.ietf.ns.demo; - -import java.util.ArrayList; -import java.util.List; -import org.springframework.stereotype.Service; -import lombok.extern.slf4j.Slf4j; - -/** - * RFC 9543 compliant demo service generator. - * - * Generates example IETF Network Slice Service templates and services - * according to draft-ietf-teas-ietf-network-slice-nbi-yang-25. - * - * This service creates demo data in the exact format specified in the RFC. - */ -@Service -@Slf4j -public class Rfc9543DemoService { - - /** - * Create RFC 9543 compliant network slice services response. - * - * @return Response with proper YANG structure - */ - public Rfc9543NetworkSliceServicesResponse createDemoServices() { - log.info("Generating RFC 9543 compliant network slice services..."); - - Rfc9543NetworkSliceServicesResponse response = new Rfc9543NetworkSliceServicesResponse(); - - // Create main container - Rfc9543NetworkSliceServicesResponse.NetworkSliceServices nss = - new Rfc9543NetworkSliceServicesResponse.NetworkSliceServices(); - - // Create templates container with proper templates - Rfc9543NetworkSliceServicesResponse.SloSleTemplatesContainer templatesContainer = - new Rfc9543NetworkSliceServicesResponse.SloSleTemplatesContainer(); - - templatesContainer.setSloSleTemplate(createRfc9543Templates()); - nss.setSloSleTemplates(templatesContainer); - - // Create slice services - nss.setSliceServices(createRfc9543SliceServices()); - - response.setNetworkSliceServices(nss); - - log.info("Created {} templates and {} services", - nss.getSloSleTemplates().getSloSleTemplate().size(), - nss.getSliceServices().size()); - - return response; - } - - /** - * Create RFC 9543 compliant SLO/SLE templates. - * Based on Figure 6 from draft-ietf-teas-ietf-network-slice-nbi-yang-25. - */ - private List createRfc9543Templates() { - List templates = new ArrayList<>(); - - // PLATINUM Template - templates.add(createPlatinumTemplate()); - - // GOLD Template - templates.add(createGoldTemplate()); - - // SILVER Template (additional example) - templates.add(createSilverTemplate()); - - return templates; - } - - /** - * PLATINUM Template: High performance, low latency - * Two-way bandwidth: 1 Gbps, 95th percentile latency 50ms - */ - private Rfc9543SloSleTemplate createPlatinumTemplate() { - Rfc9543SloSleTemplate template = new Rfc9543SloSleTemplate(); - template.setId("DEMO-PLATINUM-template"); - template.setDescription("Two-way bandwidth: 1 Gbps, 95th percentile latency 50ms"); - - // SLO Policy - Rfc9543SloSleTemplate.SloPolicy sloPolicy = new Rfc9543SloSleTemplate.SloPolicy(); - - // Metric 1: Two-way bandwidth - Rfc9543SloSleTemplate.MetricBound bandwidthBound = - new Rfc9543SloSleTemplate.MetricBound(); - bandwidthBound.setMetricType("two-way-bandwidth"); - bandwidthBound.setMetricUnit("Gbps"); - bandwidthBound.setBound("1"); - sloPolicy.getMetricBound().add(bandwidthBound); - - // Metric 2: Two-way delay percentile (95th percentile) - Rfc9543SloSleTemplate.MetricBound delayBound = - new Rfc9543SloSleTemplate.MetricBound(); - delayBound.setMetricType("two-way-delay-percentile"); - delayBound.setMetricUnit("milliseconds"); - delayBound.setPercentileValue("95.000"); - delayBound.setBound("50"); - sloPolicy.getMetricBound().add(delayBound); - - template.setSloPolicy(sloPolicy); - - // SLE Policy - Rfc9543SloSleTemplate.SlePolicy slePolicy = new Rfc9543SloSleTemplate.SlePolicy(); - slePolicy.getIsolation().add("traffic-isolation"); - template.setSlePolicy(slePolicy); - - return template; - } - - /** - * GOLD Template: High performance with guaranteed latency - * Two-way bandwidth: 1 Gbps, maximum latency 100ms - */ - private Rfc9543SloSleTemplate createGoldTemplate() { - Rfc9543SloSleTemplate template = new Rfc9543SloSleTemplate(); - template.setId("DEMO-GOLD-template"); - template.setDescription("Two-way bandwidth: 1 Gbps, maximum latency 100ms"); - - // SLO Policy - Rfc9543SloSleTemplate.SloPolicy sloPolicy = new Rfc9543SloSleTemplate.SloPolicy(); - - // Metric 1: Two-way bandwidth - Rfc9543SloSleTemplate.MetricBound bandwidthBound = - new Rfc9543SloSleTemplate.MetricBound(); - bandwidthBound.setMetricType("two-way-bandwidth"); - bandwidthBound.setMetricUnit("Gbps"); - bandwidthBound.setBound("1"); - sloPolicy.getMetricBound().add(bandwidthBound); - - // Metric 2: Two-way delay maximum - Rfc9543SloSleTemplate.MetricBound delayBound = - new Rfc9543SloSleTemplate.MetricBound(); - delayBound.setMetricType("two-way-delay-maximum"); - delayBound.setMetricUnit("milliseconds"); - delayBound.setBound("100"); - sloPolicy.getMetricBound().add(delayBound); - - template.setSloPolicy(sloPolicy); - - // SLE Policy - Rfc9543SloSleTemplate.SlePolicy slePolicy = new Rfc9543SloSleTemplate.SlePolicy(); - slePolicy.getIsolation().add("traffic-isolation"); - template.setSlePolicy(slePolicy); - - return template; - } - - /** - * SILVER Template: Standard performance - * Two-way bandwidth: 500 Mbps, 99th percentile latency 100ms - */ - private Rfc9543SloSleTemplate createSilverTemplate() { - Rfc9543SloSleTemplate template = new Rfc9543SloSleTemplate(); - template.setId("DEMO-SILVER-template"); - template.setDescription("Two-way bandwidth: 500 Mbps, 99th percentile latency 100ms"); - - // SLO Policy - Rfc9543SloSleTemplate.SloPolicy sloPolicy = new Rfc9543SloSleTemplate.SloPolicy(); - - // Metric 1: Two-way bandwidth - Rfc9543SloSleTemplate.MetricBound bandwidthBound = - new Rfc9543SloSleTemplate.MetricBound(); - bandwidthBound.setMetricType("two-way-bandwidth"); - bandwidthBound.setMetricUnit("Mbps"); - bandwidthBound.setBound("500"); - sloPolicy.getMetricBound().add(bandwidthBound); - - // Metric 2: Two-way delay percentile (99th percentile) - Rfc9543SloSleTemplate.MetricBound delayBound = - new Rfc9543SloSleTemplate.MetricBound(); - delayBound.setMetricType("two-way-delay-percentile"); - delayBound.setMetricUnit("milliseconds"); - delayBound.setPercentileValue("99.000"); - delayBound.setBound("100"); - sloPolicy.getMetricBound().add(delayBound); - - template.setSloPolicy(sloPolicy); - - // SLE Policy - Rfc9543SloSleTemplate.SlePolicy slePolicy = new Rfc9543SloSleTemplate.SlePolicy(); - slePolicy.getIsolation().add("traffic-isolation"); - template.setSlePolicy(slePolicy); - - return template; - } - - /** - * Create RFC 9543 compliant slice services. - */ - private List createRfc9543SliceServices() { - List services = new ArrayList<>(); - - // Service 1: E-Commerce - Rfc9543NetworkSliceServicesResponse.SliceService ecommerce = - new Rfc9543NetworkSliceServicesResponse.SliceService(); - ecommerce.setId("service-ecommerce-001"); - ecommerce.setDescription("E-Commerce Platform Network Slice Service"); - ecommerce.setTestOnly(false); - ecommerce.setStatus("active"); - services.add(ecommerce); - - // Service 2: Video Streaming - Rfc9543NetworkSliceServicesResponse.SliceService video = - new Rfc9543NetworkSliceServicesResponse.SliceService(); - video.setId("service-video-001"); - video.setDescription("Video Streaming CDN Network Slice Service"); - video.setTestOnly(false); - video.setStatus("active"); - services.add(video); - - // Service 3: Enterprise WAN - Rfc9543NetworkSliceServicesResponse.SliceService enterprise = - new Rfc9543NetworkSliceServicesResponse.SliceService(); - enterprise.setId("service-enterprise-wan-001"); - enterprise.setDescription("Enterprise WAN Network Slice Service"); - enterprise.setTestOnly(false); - enterprise.setStatus("active"); - services.add(enterprise); - - return services; - } -} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543NetworkSliceServicesResponse.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543NetworkSliceServicesResponse.java deleted file mode 100644 index b650059..0000000 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543NetworkSliceServicesResponse.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.etsi.osl.controllers.ietf.ns.demo; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.ArrayList; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * RFC 9543 compliant response wrapper for network-slice-services. - * - * This class represents the complete RESTCONF response structure as defined - * in draft-ietf-teas-ietf-network-slice-nbi-yang-25. - * - * The response follows the YANG container hierarchy: - * { - * "ietf-network-slice-service:network-slice-services": { - * "slo-sle-templates": { - * "slo-sle-template": [ ... ] - * }, - * "slice-service": [ ... ] - * } - * } - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class Rfc9543NetworkSliceServicesResponse { - - /** - * YANG container for network slice services with RFC 9543 namespace. - * This is the root container for all network slice service data. - */ - @JsonProperty("ietf-network-slice-service:network-slice-services") - private NetworkSliceServices networkSliceServices; - - /** - * Network slice services container with templates and services. - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class NetworkSliceServices { - /** - * Container for SLO/SLE template definitions. - * These are reusable service level templates. - */ - @JsonProperty("slo-sle-templates") - private SloSleTemplatesContainer sloSleTemplates; - - /** - * Container for slice service instances. - * These are actual service instances that may reference templates. - */ - @JsonProperty("slice-service") - private List sliceServices = new ArrayList<>(); - } - - /** - * Container for SLO/SLE templates. - * Holds a list of reusable template definitions. - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class SloSleTemplatesContainer { - /** - * List of SLO/SLE template definitions. - * Each template defines a set of service level requirements. - */ - @JsonProperty("slo-sle-template") - private List sloSleTemplate = new ArrayList<>(); - } - - /** - * Network Slice Service instance. - * - * Represents a customer-requested network slice service with specific - * connectivity and SLO/SLE requirements. - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class SliceService { - /** - * Unique identifier for the slice service. - * Example: "service-ecommerce-001" - */ - @JsonProperty("id") - private String id; - - /** - * Human-readable description of the slice service. - * Example: "E-Commerce Platform Network Slice Service" - */ - @JsonProperty("description") - private String description; - - /** - * Service tags for classification and management. - * Optional list of tags. - */ - @JsonProperty("service-tags") - private ServiceTags serviceTags; - - /** - * Test-only flag indicating this is a feasibility check. - * If present and set to true, the service is not provisioned. - * Optional field. - */ - @JsonProperty("test-only") - private Boolean testOnly; - - /** - * Status of the slice service. - * Example: "active", "pending", "terminated" - */ - @JsonProperty("status") - private String status; - } - - /** - * Service tags container for classification. - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class ServiceTags { - /** - * List of tag values for service categorization. - */ - @JsonProperty("tag") - private List tags = new ArrayList<>(); - } -} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543RestController.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543RestController.java deleted file mode 100644 index 1d30752..0000000 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543RestController.java +++ /dev/null @@ -1,119 +0,0 @@ -package org.etsi.osl.controllers.ietf.ns.demo; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import lombok.extern.slf4j.Slf4j; - -/** - * RFC 9543 compliant RESTCONF controller. - * - * Implements the exact response format specified in - * draft-ietf-teas-ietf-network-slice-nbi-yang-25. - * - * Endpoints: - * - GET /restconf/data/ietf-network-slice-service:network-slice-services - */ -@RestController -@RequestMapping("/restconf/data") -@Slf4j -public class Rfc9543RestController { - - @Autowired - private Rfc9543DemoService demoService; - - private Rfc9543NetworkSliceServicesResponse cachedResponse; - - /** - * GET ietf-network-slice-service:network-slice-services - * - * Returns RFC 9543 compliant network slice services with YANG structure: - * { - * "ietf-network-slice-service:network-slice-services": { - * "slo-sle-templates": { - * "slo-sle-template": [ ... ] - * }, - * "slice-service": [ ... ] - * } - * } - */ - @GetMapping("/ietf-network-slice-service:network-slice-services") - public ResponseEntity getNetworkSliceServices() { - log.info("GET /ietf-network-slice-service:network-slice-services"); - - if (cachedResponse == null) { - log.debug("Initializing RFC 9543 demo data..."); - cachedResponse = demoService.createDemoServices(); - } - - return ResponseEntity.ok(cachedResponse); - } - - /** - * GET ietf-network-slice-service:network-slice-services/slo-sle-templates - * - * Returns the SLO/SLE templates wrapped in RFC 9543 format with proper YANG structure: - * { - * "ietf-network-slice-service:network-slice-services": { - * "slo-sle-templates": { - * "slo-sle-template": [ ... ] - * } - * } - * } - */ - @GetMapping("/ietf-network-slice-service:network-slice-services/slo-sle-templates") - public ResponseEntity getSloSleTemplates() { - log.info("GET /ietf-network-slice-service:network-slice-services/slo-sle-templates"); - - if (cachedResponse == null) { - cachedResponse = demoService.createDemoServices(); - } - - // Create a response with only templates (no slice-services) - Rfc9543NetworkSliceServicesResponse templatesOnlyResponse = new Rfc9543NetworkSliceServicesResponse(); - Rfc9543NetworkSliceServicesResponse.NetworkSliceServices nss = new Rfc9543NetworkSliceServicesResponse.NetworkSliceServices(); - nss.setSloSleTemplates(cachedResponse.getNetworkSliceServices().getSloSleTemplates()); - // Don't set slice-services - templates endpoint only returns templates - templatesOnlyResponse.setNetworkSliceServices(nss); - - return ResponseEntity.ok(templatesOnlyResponse); - } - - /** - * GET ietf-network-slice-service:network-slice-services/slice-service - * - * Returns just the slice services. - */ - @GetMapping("/ietf-network-slice-service:network-slice-services/slice-service") - public ResponseEntity getSliceServices() { - log.info("GET /ietf-network-slice-service:network-slice-services/slice-service"); - - if (cachedResponse == null) { - cachedResponse = demoService.createDemoServices(); - } - - // Return as a container with "slice-service" key to be YANG compliant - SliceServicesContainer container = new SliceServicesContainer(); - container.setSliceService(cachedResponse.getNetworkSliceServices().getSliceServices()); - - return ResponseEntity.ok(container); - } - - /** - * Wrapper for slice services to maintain YANG structure. - */ - private static class SliceServicesContainer { - @com.fasterxml.jackson.annotation.JsonProperty("slice-service") - private java.util.List sliceService; - - public java.util.List getSliceService() { - return sliceService; - } - - public void setSliceService(java.util.List sliceService) { - this.sliceService = sliceService; - } - } -} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543SloSleTemplate.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543SloSleTemplate.java deleted file mode 100644 index 3fcb71a..0000000 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/Rfc9543SloSleTemplate.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.etsi.osl.controllers.ietf.ns.demo; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.ArrayList; -import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * RFC 9543 compliant SLO/SLE Template structure for RESTCONF JSON responses. - * - * This class represents the YANG data model for slo-sle-template as defined in - * draft-ietf-teas-ietf-network-slice-nbi-yang-25. - * - * YANG Structure: - * { - * "ietf-network-slice-service:network-slice-services": { - * "slo-sle-templates": { - * "slo-sle-template": [ - * { - * "id": "...", - * "description": "...", - * "slo-policy": { - * "metric-bound": [ ... ] - * }, - * "sle-policy": { - * "isolation": [ ... ] - * } - * } - * ] - * } - * } - * } - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -public class Rfc9543SloSleTemplate { - - /** - * Unique identifier for the SLO/SLE template. - * Example: "PLATINUM-template", "GOLD-template" - */ - @JsonProperty("id") - private String id; - - /** - * Human-readable description of the template. - * Example: "Two-way bandwidth: 1 Gbps, 95th percentile latency 50ms" - */ - @JsonProperty("description") - private String description; - - /** - * Service Level Objectives policy containing metric bounds. - */ - @JsonProperty("slo-policy") - private SloPolicy sloPolicy; - - /** - * Service Level Expectations policy containing isolation and path constraints. - */ - @JsonProperty("sle-policy") - private SlePolicy slePolicy; - - /** - * Service Level Objectives container. - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class SloPolicy { - /** - * List of metric bounds defining performance targets. - */ - @JsonProperty("metric-bound") - private List metricBound = new ArrayList<>(); - } - - /** - * Service Level Expectations container. - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class SlePolicy { - /** - * List of isolation requirements. - * Example: ["traffic-isolation"] - */ - @JsonProperty("isolation") - private List isolation = new ArrayList<>(); - - /** - * List of path constraint requirements. - * Optional field. - */ - @JsonProperty("path-constraints") - private List pathConstraints = new ArrayList<>(); - } - - /** - * Individual metric bound defining a performance metric constraint. - * - * YANG Structure: - * { - * "metric-type": "two-way-bandwidth", - * "metric-unit": "Gbps", - * "bound": "1" - * } - * - * For percentile-based metrics: - * { - * "metric-type": "two-way-delay-percentile", - * "metric-unit": "milliseconds", - * "percentile-value": "95.000", - * "bound": "50" - * } - */ - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class MetricBound { - /** - * Type of metric being constrained. - * - * Supported metric types (from RFC 9543): - * - two-way-bandwidth: Guaranteed minimum bandwidth (both directions) - * - two-way-delay-maximum: Maximum one-way delay - * - two-way-delay-percentile: Percentile-based delay - * - two-way-jitter-maximum: Maximum delay variation - * - two-way-jitter-percentile: Percentile-based jitter - * - two-way-packet-loss: Packet loss percentage - * - * Example: "two-way-bandwidth", "two-way-delay-percentile" - */ - @JsonProperty("metric-type") - private String metricType; - - /** - * Unit of measurement for the metric. - * - * Examples: - * - Bandwidth: "bps", "Kbps", "Mbps", "Gbps" - * - Delay/Jitter: "milliseconds", "microseconds", "nanoseconds" - * - Loss: "percentage" - */ - @JsonProperty("metric-unit") - private String metricUnit; - - /** - * Upper bound value for the metric. - * - * This is the maximum allowed value for the metric. - * Example: "1" (when metric-unit is Gbps), "50" (when metric-unit is milliseconds) - */ - @JsonProperty("bound") - private String bound; - - /** - * Percentile value for percentile-based metrics (0.0 to 100.0). - * - * Only present for percentile metrics like: - * - two-way-delay-percentile - * - two-way-jitter-percentile - * - * Example: "95.000" for 95th percentile - */ - @JsonProperty("percentile-value") - private String percentileValue; - } -} diff --git a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/SecurityConfig.java b/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/SecurityConfig.java deleted file mode 100644 index 100e78a..0000000 --- a/src/main/java/org/etsi/osl/controllers/ietf/ns/demo/SecurityConfig.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.etsi.osl.controllers.ietf.ns.demo; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; -import org.springframework.security.web.SecurityFilterChain; -import lombok.extern.slf4j.Slf4j; - -/** - * Spring Security Configuration for Demo RESTCONF Server. - * - * Provides HTTP Basic Authentication for all RESTCONF endpoints. - * - * Default credentials: - * - Username: admin - * - Password: admin123 - * - * Additional users: - * - Username: user - * - Password: user123 - * - * To authenticate with curl: - * curl -u admin:admin123 http://localhost:11880/restconf/data/... - * - * Or with header: - * curl -H "Authorization: Basic YWRtaW46YWRtaW4xMjM=" http://localhost:11880/restconf/data/... - */ -@Configuration -@EnableWebSecurity -@Slf4j -public class SecurityConfig { - - /** - * Configure HTTP security with Basic Authentication. - * All endpoints under /restconf require authentication. - */ - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - log.info("Configuring Spring Security with HTTP Basic Authentication"); - - http - // Require authentication for all /restconf endpoints - .authorizeHttpRequests(authz -> authz - .requestMatchers("/restconf/**").authenticated() - .requestMatchers("/actuator/health").permitAll() // Allow health check without auth - .anyRequest().authenticated() - ) - // Enable HTTP Basic Authentication - .httpBasic(basic -> { - log.debug("HTTP Basic Authentication enabled"); - }) - // Disable CSRF for demo (enable for production) - .csrf(csrf -> csrf.disable()); - - return http.build(); - } - - /** - * Define in-memory users for demo purposes. - * - * Users: - * 1. admin / admin123 - Full access - * 2. user / user123 - Read-only access (can be implemented with role-based security) - */ - @Bean - public UserDetailsService userDetailsService() { - log.info("Creating in-memory user details service for authentication"); - - // Admin user with full access - UserDetails admin = User.builder() - .username("admin") - .password(passwordEncoder().encode("admin123")) - .roles("ADMIN", "USER") - .build(); - - // Regular user - UserDetails user = User.builder() - .username("user") - .password(passwordEncoder().encode("user123")) - .roles("USER") - .build(); - - log.info("Created users: admin (ADMIN, USER), user (USER)"); - - return new InMemoryUserDetailsManager(admin, user); - } - - /** - * Password encoder using BCrypt. - * Required for secure password storage. - */ - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } -} diff --git a/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SerializationDeserializationTest.java b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SerializationDeserializationTest.java new file mode 100644 index 0000000..730f1a3 --- /dev/null +++ b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SerializationDeserializationTest.java @@ -0,0 +1,611 @@ +package org.etsi.osl.controllers.ietf.ns.api.domain.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.AvailabilityType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.MetricBound; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceIsolationType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceSecurityType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.ServiceSloMetricType; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SlePolicy; +import org.etsi.osl.controllers.ietf.ns.api.domain.model.slo_sle.SloPolicy; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Verifies RFC 9543 JSON serialization and deserialization for + * SloSleTemplate, SliceService, and NetworkSliceServices. + * + * The ObjectMapper used here mirrors the lenientMapper() in ResourceRepoService. + */ +@DisplayName("RFC 9543 Serialization / Deserialization Tests") +class Rfc9543SerializationDeserializationTest { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + mapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + mapper.setDefaultSetterInfo(JsonSetter.Value.forContentNulls(Nulls.SKIP)); + } + + // ========================================================================= + // SloSleTemplate + // ========================================================================= + + @Nested + @DisplayName("SloSleTemplate") + class SloSleTemplateTests { + + @Test + @DisplayName("Serializes top-level fields with RFC 9543 names: slo-policy, sle-policy") + void serializesRfc9543TopLevelFieldNames() throws Exception { + SloSleTemplate t = buildGoldTemplate(); + + JsonNode node = toJsonNode(t); + + assertEquals("gold", node.get("id").asText()); + assertTrue(node.has("slo-policy"), "slo-policy must be present"); + assertTrue(node.has("sle-policy"), "sle-policy must be present"); + assertFalse(node.has("sloPolicy"), "camelCase sloPolicy must NOT appear"); + assertFalse(node.has("slePolicy"), "camelCase slePolicy must NOT appear"); + } + + @Test + @DisplayName("Serializes no interface-method fields (entityId, entityName, version, …)") + void serializesNoUnwantedInterfaceFields() throws Exception { + JsonNode node = toJsonNode(buildGoldTemplate()); + + assertFalse(node.has("entityId"), "entityId must not appear"); + assertFalse(node.has("entityName"), "entityName must not appear"); + assertFalse(node.has("entityDescription"), "entityDescription must not appear"); + assertFalse(node.has("entityTypeName"), "entityTypeName must not appear"); + assertFalse(node.has("version"), "version must not appear"); + assertFalse(node.has("relatedManagedResourceId"), "relatedManagedResourceId must not appear"); + assertFalse(node.has("relatedManagedResource"), "relatedManagedResource must not appear"); + } + + @Test + @DisplayName("SloPolicy serializes metric-bound array with kebab-case metric-type") + void serializesSloPolicyMetricBoundArray() throws Exception { + JsonNode sloPolicy = toJsonNode(buildGoldTemplate()).get("slo-policy"); + + assertTrue(sloPolicy.has("metric-bound"), "metric-bound must be present"); + JsonNode bounds = sloPolicy.get("metric-bound"); + assertTrue(bounds.isArray()); + assertEquals(2, bounds.size()); + + JsonNode first = bounds.get(0); + assertTrue(first.has("metric-type"), "metric-type must be present"); + assertEquals("one-way-delay-maximum", first.get("metric-type").asText(), + "ServiceSloMetricType.@JsonValue must produce kebab-case"); + assertTrue(first.has("metric-unit")); + assertEquals("ms", first.get("metric-unit").asText()); + assertEquals(50L, first.get("bound").asLong()); + } + + @Test + @DisplayName("SlePolicy serializes isolation-requirement (not isolation) field name") + void serializesSlePolicyIsolationRequirementFieldName() throws Exception { + JsonNode slePolicy = toJsonNode(buildGoldTemplate()).get("sle-policy"); + + assertTrue(slePolicy.has("isolation-requirement"), + "isolation-requirement must be present"); + assertFalse(slePolicy.has("isolation"), + "raw isolation field must NOT appear"); + JsonNode isoArr = slePolicy.get("isolation-requirement"); + assertTrue(isoArr.isArray()); + assertEquals("logical-isolation", isoArr.get(0).asText()); + } + + @Test + @DisplayName("SlePolicy serializes max-occupancy-level field name") + void serializesSlePolicyMaxOccupancyLevel() throws Exception { + JsonNode slePolicy = toJsonNode(buildGoldTemplate()).get("sle-policy"); + + assertTrue(slePolicy.has("max-occupancy-level")); + assertEquals(80, slePolicy.get("max-occupancy-level").asInt()); + } + + @Test + @DisplayName("Deserializes slo-policy and sle-policy from RFC 9543 JSON") + void deserializesFromRfc9543Json() throws Exception { + String json = "{" + + "\"id\":\"silver\"," + + "\"description\":\"Silver tier template\"," + + "\"slo-policy\":{" + + " \"mtu\":1500," + + " \"metric-bound\":[" + + " {\"metric-type\":\"two-way-bandwidth\",\"metric-unit\":\"Mbps\",\"bound\":500}" + + " ]" + + "}," + + "\"sle-policy\":{" + + " \"security\":[\"encryption-required\"]," + + " \"isolation-requirement\":[\"logical-isolation\"]," + + " \"max-occupancy-level\":70" + + "}" + + "}"; + + SloSleTemplate t = mapper.readValue(json, SloSleTemplate.class); + + assertEquals("silver", t.getId()); + assertEquals("Silver tier template", t.getDescription()); + + assertNotNull(t.getSloPolicy(), "sloPolicy must not be null"); + assertEquals(1500L, t.getSloPolicy().getMtu()); + assertEquals(1, t.getSloPolicy().getMetricBounds().size()); + MetricBound bound = t.getSloPolicy().getMetricBounds().get(0); + assertEquals(ServiceSloMetricType.TWO_WAY_BANDWIDTH, bound.getMetricType()); + assertEquals("Mbps", bound.getMetricUnit()); + assertEquals(500L, bound.getBound()); + + assertNotNull(t.getSlePolicy(), "slePolicy must not be null"); + assertTrue(t.getSlePolicy().getSecurity().contains(ServiceSecurityType.ENCRYPTION_REQUIRED)); + assertTrue(t.getSlePolicy().getIsolation().contains(ServiceIsolationType.LOGICAL_ISOLATION)); + assertEquals((short) 70, t.getSlePolicy().getMaxOccupancyLevel()); + } + + @Test + @DisplayName("Enum ServiceSloMetricType deserializes kebab-case and serializes back to kebab-case") + void enumRoundTripsKebabCase() throws Exception { + String json = "{\"metric-type\":\"two-way-bandwidth\",\"metric-unit\":\"Mbps\",\"bound\":1000}"; + + MetricBound bound = mapper.readValue(json, MetricBound.class); + assertEquals(ServiceSloMetricType.TWO_WAY_BANDWIDTH, bound.getMetricType()); + + String serialized = mapper.writeValueAsString(bound); + JsonNode node = mapper.readTree(serialized); + assertEquals("two-way-bandwidth", node.get("metric-type").asText()); + } + + @Test + @DisplayName("Round-trips: serialize → deserialize preserves id, mtu, metric bounds, isolation") + void roundTrip() throws Exception { + SloSleTemplate original = buildGoldTemplate(); + String json = mapper.writeValueAsString(original); + SloSleTemplate parsed = mapper.readValue(json, SloSleTemplate.class); + + assertEquals(original.getId(), parsed.getId()); + assertEquals(original.getDescription(), parsed.getDescription()); + + assertNotNull(parsed.getSloPolicy()); + assertEquals(original.getSloPolicy().getMtu(), parsed.getSloPolicy().getMtu()); + assertEquals(original.getSloPolicy().getMetricBounds().size(), + parsed.getSloPolicy().getMetricBounds().size()); + assertEquals( + original.getSloPolicy().getMetricBounds().get(0).getMetricType(), + parsed.getSloPolicy().getMetricBounds().get(0).getMetricType()); + + assertNotNull(parsed.getSlePolicy()); + assertEquals( + original.getSlePolicy().getMaxOccupancyLevel(), + parsed.getSlePolicy().getMaxOccupancyLevel()); + assertTrue(parsed.getSlePolicy().getIsolation() + .containsAll(original.getSlePolicy().getIsolation())); + assertTrue(parsed.getSlePolicy().getSecurity() + .containsAll(original.getSlePolicy().getSecurity())); + } + + @Test + @DisplayName("Minimal JSON with only id deserializes without error") + void minimalJsonDeserializes() throws Exception { + String json = "{\"id\":\"basic\"}"; + SloSleTemplate t = mapper.readValue(json, SloSleTemplate.class); + + assertEquals("basic", t.getId()); + assertNull(t.getSloPolicy()); + assertNull(t.getSlePolicy()); + } + } + + // ========================================================================= + // SliceService + // ========================================================================= + + @Nested + @DisplayName("SliceService") + class SliceServiceTests { + + @Test + @DisplayName("Serializes sdps as {\"sdp\": [...]} nested container") + void serializesSdpsAsNestedContainer() throws Exception { + JsonNode node = toJsonNode(buildP2pSliceService()); + + assertTrue(node.has("sdps"), "sdps must be present"); + JsonNode sdps = node.get("sdps"); + assertTrue(sdps.has("sdp"), "sdps.sdp array must be present"); + assertTrue(sdps.get("sdp").isArray()); + assertEquals(2, sdps.get("sdp").size()); + } + + @Test + @DisplayName("Serializes SDPs with node-id and sdp-ip-address kebab-case field names") + void serializesSdpFieldsAsKebabCase() throws Exception { + JsonNode firstSdp = toJsonNode(buildP2pSliceService()) + .get("sdps").get("sdp").get(0); + + assertTrue(firstSdp.has("node-id"), "node-id must be present"); + assertTrue(firstSdp.has("sdp-ip-address") || firstSdp.has("id"), + "at least id must be present"); + assertFalse(firstSdp.has("nodeId"), "camelCase nodeId must NOT appear"); + } + + @Test + @DisplayName("Serializes connection-groups as {\"connection-group\": [...]} nested container") + void serializesConnectionGroupsAsNestedContainer() throws Exception { + JsonNode node = toJsonNode(buildP2pSliceService()); + + assertTrue(node.has("connection-groups"), "connection-groups must be present"); + JsonNode cg = node.get("connection-groups"); + assertTrue(cg.has("connection-group"), "connection-group array must be present"); + assertTrue(cg.get("connection-group").isArray()); + assertEquals(1, cg.get("connection-group").size()); + } + + @Test + @DisplayName("Serializes connectivity-type as kebab-case RFC 9543 value") + void serializesConnectivityTypeAsKebabCase() throws Exception { + String type = toJsonNode(buildP2pSliceService()) + .get("connection-groups") + .get("connection-group").get(0) + .get("connectivity-type").asText(); + + assertEquals("point-to-point", type); + } + + @Test + @DisplayName("Serializes connectivity-construct with p2p-sender-sdp / p2p-receiver-sdp") + void serializesP2pConnectivityConstructFields() throws Exception { + JsonNode cc = toJsonNode(buildP2pSliceService()) + .get("connection-groups") + .get("connection-group").get(0) + .get("connectivity-construct").get(0); + + assertTrue(cc.has("p2p-sender-sdp"), "p2p-sender-sdp must be present"); + assertTrue(cc.has("p2p-receiver-sdp"), "p2p-receiver-sdp must be present"); + assertEquals("sdp-A", cc.get("p2p-sender-sdp").asText()); + assertEquals("sdp-B", cc.get("p2p-receiver-sdp").asText()); + } + + @Test + @DisplayName("Serializes slo-sle-template as plain string reference (not nested object)") + void serializesSloSleTemplateAsStringReference() throws Exception { + JsonNode node = toJsonNode(buildP2pSliceService()); + + assertTrue(node.has("slo-sle-template"), "slo-sle-template must be present"); + assertTrue(node.get("slo-sle-template").isTextual(), + "slo-sle-template must be a plain string"); + assertEquals("silver", node.get("slo-sle-template").asText()); + } + + @Test + @DisplayName("Serializes service-tags as {\"tag-type\": [...]} nested structure") + void serializesServiceTagsAsNestedStructure() throws Exception { + JsonNode node = toJsonNode(buildP2pSliceService()); + + assertTrue(node.has("service-tags"), "service-tags must be present"); + JsonNode tags = node.get("service-tags"); + assertTrue(tags.has("tag-type"), "tag-type array must be present"); + assertTrue(tags.get("tag-type").isArray()); + } + + @Test + @DisplayName("Deserializes direct \"slo-sle-template\": \"silver\" string reference") + void deserializesDirectSloSleTemplateRef() throws Exception { + String json = "{" + + "\"id\":\"svc-001\"," + + "\"slo-sle-template\":\"silver\"," + + "\"sdps\":{\"sdp\":[]}," + + "\"connection-groups\":{\"connection-group\":[]}" + + "}"; + + SliceService svc = mapper.readValue(json, SliceService.class); + + assertEquals("svc-001", svc.getId()); + assertNotNull(svc.getSloSleTemplate(), "sloSleTemplate must not be null"); + assertEquals("silver", svc.getSloSleTemplate().getId()); + } + + @Test + @DisplayName("Deserializes nested slo-sle-policy.slo-sle-template reference") + void deserializesNestedSloSlePolicyRef() throws Exception { + String json = "{" + + "\"id\":\"svc-002\"," + + "\"slo-sle-policy\":{\"slo-sle-template\":\"gold\"}," + + "\"sdps\":{\"sdp\":[]}," + + "\"connection-groups\":{\"connection-group\":[]}" + + "}"; + + SliceService svc = mapper.readValue(json, SliceService.class); + + assertNotNull(svc.getSloSleTemplate()); + assertEquals("gold", svc.getSloSleTemplate().getId()); + } + + @Test + @DisplayName("Deserializes p2p connectivity-construct with p2p-sender-sdp and p2p-receiver-sdp") + void deserializesP2pConnectivityConstruct() throws Exception { + String json = "{" + + "\"id\":\"svc-p2p\"," + + "\"slo-sle-template\":\"bronze\"," + + "\"sdps\":{\"sdp\":[" + + " {\"id\":\"sdp-A\",\"node-id\":\"nodeA\",\"sdp-ip-address\":[\"10.0.0.1\"]}," + + " {\"id\":\"sdp-B\",\"node-id\":\"nodeB\",\"sdp-ip-address\":[\"10.0.0.2\"]}" + + "]}," + + "\"connection-groups\":{\"connection-group\":[" + + " {\"id\":\"cg-1\",\"connectivity-type\":\"point-to-point\"," + + " \"connectivity-construct\":[{" + + " \"id\":\"cc-1\"," + + " \"p2p-sender-sdp\":\"sdp-A\"," + + " \"p2p-receiver-sdp\":\"sdp-B\"" + + " }]}" + + "]}" + + "}"; + + SliceService svc = mapper.readValue(json, SliceService.class); + + assertEquals(1, svc.getConnectionGroups().size()); + ConnectionGroup cg = svc.getConnectionGroups().get(0); + assertEquals(ConnectivityType.P2P, cg.getConnectivityType()); + assertEquals(1, cg.getConnectivityConstructs().size()); + ConnectivityConstruct cc = cg.getConnectivityConstructs().get(0); + assertEquals("sdp-A", cc.getP2pSenderSdp()); + assertEquals("sdp-B", cc.getP2pReceiverSdp()); + } + + @Test + @DisplayName("Deserializes SDPs with node-id and sdp-ip-address") + void deserializesSdpFields() throws Exception { + String json = "{" + + "\"id\":\"svc-sdp\"," + + "\"sdps\":{\"sdp\":[" + + " {\"id\":\"CU-N32\",\"node-id\":\"nodeA\",\"sdp-ip-address\":[\"10.60.11.3\"]}" + + "]}," + + "\"connection-groups\":{\"connection-group\":[]}" + + "}"; + + SliceService svc = mapper.readValue(json, SliceService.class); + + assertEquals(1, svc.getSdps().size()); + SDP sdp = svc.getSdps().get(0); + assertEquals("CU-N32", sdp.getId()); + assertEquals("nodeA", sdp.getNodeId()); + assertEquals(1, sdp.getSdpIpAddress().size()); + assertEquals("10.60.11.3", sdp.getSdpIpAddress().get(0)); + } + + @Test + @DisplayName("Deserializes ConnectivityType any-to-any as A2A") + void deserializesConnectivityTypeAnyToAny() throws Exception { + String json = "{" + + "\"id\":\"svc-a2a\"," + + "\"sdps\":{\"sdp\":[]}," + + "\"connection-groups\":{\"connection-group\":[" + + " {\"id\":\"cg\",\"connectivity-type\":\"any-to-any\"," + + " \"connectivity-construct\":[]}" + + "]}" + + "}"; + + SliceService svc = mapper.readValue(json, SliceService.class); + + assertEquals(ConnectivityType.A2A, + svc.getConnectionGroups().get(0).getConnectivityType()); + } + + @Test + @DisplayName("Round-trips: serialize → deserialize preserves all key fields") + void roundTrip() throws Exception { + SliceService original = buildP2pSliceService(); + String json = mapper.writeValueAsString(original); + SliceService parsed = mapper.readValue(json, SliceService.class); + + assertEquals(original.getId(), parsed.getId()); + assertEquals(original.getDescription(), parsed.getDescription()); + assertEquals(original.getSloSleTemplate().getId(), + parsed.getSloSleTemplate().getId()); + + assertEquals(2, parsed.getSdps().size()); + assertEquals("sdp-A", parsed.getSdps().get(0).getId()); + assertEquals("nodeA", parsed.getSdps().get(0).getNodeId()); + + assertEquals(1, parsed.getConnectionGroups().size()); + ConnectionGroup cg = parsed.getConnectionGroups().get(0); + assertEquals(ConnectivityType.P2P, cg.getConnectivityType()); + assertEquals(1, cg.getConnectivityConstructs().size()); + assertEquals("sdp-A", cg.getConnectivityConstructs().get(0).getP2pSenderSdp()); + assertEquals("sdp-B", cg.getConnectivityConstructs().get(0).getP2pReceiverSdp()); + } + } + + // ========================================================================= + // NetworkSliceServices + // ========================================================================= + + @Nested + @DisplayName("NetworkSliceServices") + class NetworkSliceServicesTests { + + @Test + @DisplayName("Serializes both lists without interface-method fields") + void serializesListsWithoutInterfaceFields() throws Exception { + JsonNode node = toJsonNode(buildNetworkSliceServices()); + + assertTrue(node.has("sloSleTemplates"), "sloSleTemplates list must be present"); + assertTrue(node.has("sliceServices"), "sliceServices list must be present"); + + assertFalse(node.has("entityId"), "entityId must not appear"); + assertFalse(node.has("entityName"), "entityName must not appear"); + assertFalse(node.has("entityDescription"), "entityDescription must not appear"); + assertFalse(node.has("hasStatusMapping"), "hasStatusMapping must not appear"); + } + + @Test + @DisplayName("SloSleTemplate inside container uses RFC 9543 field names") + void templateInsideContainerUsesRfc9543Names() throws Exception { + JsonNode template = toJsonNode(buildNetworkSliceServices()) + .get("sloSleTemplates").get(0); + + assertTrue(template.has("slo-policy"), "slo-policy must be present"); + assertTrue(template.has("sle-policy"), "sle-policy must be present"); + assertFalse(template.has("entityId"), "entityId must not appear inside template"); + } + + @Test + @DisplayName("SliceService inside container uses RFC 9543 nested containers") + void serviceInsideContainerUsesRfc9543Nesting() throws Exception { + JsonNode svc = toJsonNode(buildNetworkSliceServices()) + .get("sliceServices").get(0); + + assertTrue(svc.has("sdps"), "sdps must be present"); + assertTrue(svc.get("sdps").has("sdp"), "sdps.sdp must be present"); + assertTrue(svc.has("connection-groups"), "connection-groups must be present"); + assertTrue(svc.get("connection-groups").has("connection-group"), + "connection-groups.connection-group must be present"); + assertTrue(svc.has("slo-sle-template"), "slo-sle-template must be present"); + assertTrue(svc.get("slo-sle-template").isTextual(), + "slo-sle-template must be a plain string reference"); + } + + @Test + @DisplayName("Round-trips: serialize → deserialize preserves both list sizes and IDs") + void roundTrip() throws Exception { + NetworkSliceServices original = buildNetworkSliceServices(); + String json = mapper.writeValueAsString(original); + NetworkSliceServices parsed = mapper.readValue(json, NetworkSliceServices.class); + + assertEquals(1, parsed.getSloSleTemplates().size()); + assertEquals(original.getSloSleTemplates().get(0).getId(), + parsed.getSloSleTemplates().get(0).getId()); + + assertEquals(1, parsed.getSliceServices().size()); + assertEquals(original.getSliceServices().get(0).getId(), + parsed.getSliceServices().get(0).getId()); + } + + @Test + @DisplayName("Parses array of SloSleTemplates from JSON array string") + void parsesTemplatesArrayFromJson() throws Exception { + String json = "[" + + "{\"id\":\"gold\",\"slo-policy\":{\"mtu\":9000}," + + " \"sle-policy\":{\"max-occupancy-level\":80}}," + + "{\"id\":\"silver\",\"slo-policy\":{\"mtu\":1500}," + + " \"sle-policy\":{\"max-occupancy-level\":50}}" + + "]"; + + SloSleTemplate[] templates = mapper.readValue(json, SloSleTemplate[].class); + + assertEquals(2, templates.length); + assertEquals("gold", templates[0].getId()); + assertEquals("silver", templates[1].getId()); + assertEquals(9000L, templates[0].getSloPolicy().getMtu()); + assertEquals(1500L, templates[1].getSloPolicy().getMtu()); + } + + @Test + @DisplayName("Parses array of SliceServices from JSON array string") + void parsesServicesArrayFromJson() throws Exception { + String json = "[" + + "{\"id\":\"svc-1\",\"slo-sle-template\":\"gold\"," + + " \"sdps\":{\"sdp\":[]},\"connection-groups\":{\"connection-group\":[]}}," + + "{\"id\":\"svc-2\",\"slo-sle-template\":\"silver\"," + + " \"sdps\":{\"sdp\":[]},\"connection-groups\":{\"connection-group\":[]}}" + + "]"; + + SliceService[] services = mapper.readValue(json, SliceService[].class); + + assertEquals(2, services.length); + assertEquals("svc-1", services[0].getId()); + assertEquals("svc-2", services[1].getId()); + assertEquals("gold", services[0].getSloSleTemplate().getId()); + assertEquals("silver", services[1].getSloSleTemplate().getId()); + } + } + + // ========================================================================= + // Object builders + // ========================================================================= + + private SloSleTemplate buildGoldTemplate() { + SloPolicy sloPolicy = new SloPolicy(); + sloPolicy.setMtu(1500L); + sloPolicy.setAvailability(new AvailabilityType(99.9, "per-month", 43L)); + sloPolicy.addMetricBound( + new MetricBound(ServiceSloMetricType.ONE_WAY_DELAY_MAXIMUM, "ms", 50L)); + sloPolicy.addMetricBound( + new MetricBound(ServiceSloMetricType.TWO_WAY_BANDWIDTH, "Mbps", 1000L)); + + SlePolicy slePolicy = new SlePolicy(); + slePolicy.addSecurityRequirement(ServiceSecurityType.ENCRYPTION_REQUIRED); + slePolicy.addIsolationRequirement(ServiceIsolationType.LOGICAL_ISOLATION); + slePolicy.setMaxOccupancyLevel((short) 80); + + SloSleTemplate t = new SloSleTemplate(); + t.setId("gold"); + t.setDescription("Gold tier SLO/SLE template"); + t.setSloPolicy(sloPolicy); + t.setSlePolicy(slePolicy); + return t; + } + + private SliceService buildP2pSliceService() { + SDP sdpA = new SDP(); + sdpA.setId("sdp-A"); + sdpA.setNodeId("nodeA"); + sdpA.getSdpIpAddress().add("10.0.0.1"); + + SDP sdpB = new SDP(); + sdpB.setId("sdp-B"); + sdpB.setNodeId("nodeB"); + sdpB.getSdpIpAddress().add("10.0.0.2"); + + ConnectivityConstruct cc = new ConnectivityConstruct(); + cc.setId("cc-1"); + cc.setP2pSenderSdp("sdp-A"); + cc.setP2pReceiverSdp("sdp-B"); + + ConnectionGroup cg = new ConnectionGroup(); + cg.setId("cg-1"); + cg.setConnectivityType(ConnectivityType.P2P); + cg.getConnectivityConstructs().add(cc); + + ServiceTag tag = new ServiceTag(); + tag.setValue("L3"); + + SloSleTemplate templateRef = new SloSleTemplate(); + templateRef.setId("silver"); + + SliceService svc = new SliceService(); + svc.setId("svc-p2p-001"); + svc.setDescription("P2P test slice service"); + svc.setSloSleTemplate(templateRef); + svc.getSdps().add(sdpA); + svc.getSdps().add(sdpB); + svc.getConnectionGroups().add(cg); + svc.getServiceTags().add(tag); + return svc; + } + + private NetworkSliceServices buildNetworkSliceServices() { + NetworkSliceServices nss = new NetworkSliceServices(); + nss.getSloSleTemplates().add(buildGoldTemplate()); + nss.getSliceServices().add(buildP2pSliceService()); + return nss; + } + + private JsonNode toJsonNode(Object obj) throws Exception { + return mapper.readTree(mapper.writeValueAsString(obj)); + } +} diff --git a/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializerTest.java b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializerTest.java index 122ea42..fe44a29 100644 --- a/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializerTest.java +++ b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/domain/model/Rfc9543SliceServiceDeserializerTest.java @@ -220,7 +220,7 @@ class Rfc9543SliceServiceDeserializerTest { // Validate service tags assertEquals(1, sliceService.getServiceTags().size()); - assertEquals("service:L2", sliceService.getServiceTags().get(0).getValue()); + assertEquals("L2", sliceService.getServiceTags().get(0).getValue()); // Validate template reference assertNotNull(sliceService.getSloSleTemplate()); diff --git a/src/test/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImplTest.java b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImplTest.java index ee86e6a..51daab3 100644 --- a/src/test/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImplTest.java +++ b/src/test/java/org/etsi/osl/controllers/ietf/ns/api/restconf/RestconfClientImplTest.java @@ -212,7 +212,7 @@ class RestconfClientImplTest { restconfClient.getSloSleTemplates(); }); - assertTrue(exception.getMessage().contains("Failed to retrieve SLO/SLE templates")); + assertTrue(exception.getMessage().contains("Failed to get SLO/SLE templates")); } @Test @@ -231,7 +231,7 @@ class RestconfClientImplTest { restconfClient.getSloSleTemplates(); }); - assertTrue(exception.getMessage().contains("Failed to retrieve SLO/SLE templates")); + assertTrue(exception.getMessage().contains("Failed to get SLO/SLE templates")); } @Test -- GitLab From 07217f3f8164e20938f1e5dd8341306b21493e4b Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Thu, 23 Jul 2026 01:46:38 +0300 Subject: [PATCH 09/12] fix for #2: Adding deployment artifacts and gitlab CIs --- .gitlab-ci.yml | 41 +++++++++++++++++++++++++++++++++++++++++ Dockerfile | 21 +++++++++++---------- README.md | 2 +- ci_settings.xml | 17 +++++++++++++++++ compose.yml | 31 +++++++++++++++++++++++++++++++ pom.xml | 4 ++-- 6 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 ci_settings.xml create mode 100644 compose.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e5ec948 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,41 @@ +include: + - project: osl/code/org.etsi.osl.main + ref: main + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_NAME == "main"' + + - project: osl/code/org.etsi.osl.main + ref: develop + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_NAME == "develop"' + + - project: osl/code/org.etsi.osl.main + ref: $CI_COMMIT_REF_NAME + file: + - ci-templates/default.yml + - ci-templates/build.yml + rules: + - if: '$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "develop"' + + - project: osl/code/org.etsi.osl.main + ref: develop + file: + - ci-templates/default.yml + - ci-templates/build_unprotected.yml + rules: + - if: '$CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "develop" && $CI_COMMIT_REF_PROTECTED == "false"' + +maven_build: + extends: .maven_build + +docker_build: + extends: .docker_build + needs: + - maven_build + diff --git a/Dockerfile b/Dockerfile index 50d98a5..c3faee1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,18 @@ # Multi-stage build for TFS Controller # Stage 1: Build the application -FROM maven:3.9.6-eclipse-temurin-17 AS builder +# FROM maven:3.9.6-eclipse-temurin-17 AS builder -WORKDIR /build +# WORKDIR /build -# Copy pom.xml and download dependencies -COPY pom.xml . -RUN mvn dependency:go-offline -B +# # Copy pom.xml and download dependencies +# COPY pom.xml . +# RUN mvn dependency:go-offline -B -# Copy source code -COPY src ./src +# # Copy source code +# COPY src ./src -# Build the application -RUN mvn clean package -DskipTests +# # Build the application +# RUN mvn clean package -DskipTests # Stage 2: Runtime image FROM eclipse-temurin:17-jdk-alpine @@ -20,7 +20,8 @@ FROM eclipse-temurin:17-jdk-alpine WORKDIR /app # Copy the built JAR from builder stage -COPY --from=builder /build/target/org.etsi.osl.controllers.ietf.ns-0.0.1-SNAPSHOT.jar app.jar +# COPY --from=builder /build/target/org.etsi.osl.controllers.ietf.ns-0.0.9-SNAPSHOT.jar app.jar +COPY target/org.etsi.osl.controllers.ietf.ns-0.0.9-SNAPSHOT.jar app.jar # Run the application ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/README.md b/README.md index beb39dc..b96d407 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ mvn clean package mvn spring-boot:run # Run packaged JAR -java -jar target/org.etsi.osl.controllers.ietf.ns-0.0.1-SNAPSHOT.jar +java -jar target/org.etsi.osl.controllers.ietf.ns-0.0.9-SNAPSHOT.jar # Build without tests mvn clean package -DskipTests diff --git a/ci_settings.xml b/ci_settings.xml new file mode 100644 index 0000000..eade238 --- /dev/null +++ b/ci_settings.xml @@ -0,0 +1,17 @@ + + + + gitlab-maven + + + + Job-Token + ${CI_JOB_TOKEN} + + + + + + + diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..1cdf127 --- /dev/null +++ b/compose.yml @@ -0,0 +1,31 @@ +services: + osl-ietf-ns-controller: + build: + context: . + dockerfile: Dockerfile + image: labs.etsi.org:5050/osl/code/addons/org.etsi.controllers.ietf.ns:develop + container_name: osl-ietf-ns-controller + restart: always + profiles: ["dev", "prod"] + environment: + SPRING_ACTIVEMQ_BROKERURL: tcp://anartemis:61616?jms.watchTopicAdvisories=false + SPRING_ACTIVEMQ_USER: artemis + SPRING_ACTIVEMQ_PASSWORD: artemis + RESTCONF_PROVIDERURL: "http://nscontroller:8085" + RESTCONF_AUTHMETHOD: "basic" + RESTCONF_AUTH_USERNAME: "admin" + RESTCONF_AUTH_PASSWORD: "admin" + RESTCONF_APIVERSION: "2025-05-09" + LOGGING_LEVEL_ORG_SPRINGFRAMEWORK: INFO + logging: + driver: "json-file" + options: + max-size: "250m" + max-file: "2" + networks: + - compose_back + +networks: + compose_back: + external: true + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 27d1fc8..3c52ffc 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.etsi.osl org.etsi.osl.controllers.ietf.ns - 0.0.1-SNAPSHOT + 0.0.9-SNAPSHOT org.etsi.osl.controllers.ietf.ns org.etsi.osl.controllers.ietf.ns @@ -24,7 +24,7 @@ 1.7.0 1.7.0 22.0.1 - 1.1.0-SNAPSHOT + 1.4.0 -- GitLab From 104b31d501fb01d7bae3e52838915eae6ce9a7b7 Mon Sep 17 00:00:00 2001 From: Kostis Trantzas Date: Thu, 23 Jul 2026 02:05:06 +0300 Subject: [PATCH 10/12] Adding correct OSL image name --- compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index 1cdf127..5df6452 100644 --- a/compose.yml +++ b/compose.yml @@ -3,7 +3,7 @@ services: build: context: . dockerfile: Dockerfile - image: labs.etsi.org:5050/osl/code/addons/org.etsi.controllers.ietf.ns:develop + image: labs.etsi.org:5050/osl/code/addons/org.etsi.osl.controllers.ietf.ns:develop container_name: osl-ietf-ns-controller restart: always profiles: ["dev", "prod"] -- GitLab From 897adc6a7bbc5e66c8feb891165eb980a06cc4f2 Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 23 Jul 2026 13:46:26 +0300 Subject: [PATCH 11/12] add capability to build fat jar exe --- Dockerfile | 2 +- pom.xml | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 50d98a5..1453932 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ FROM eclipse-temurin:17-jdk-alpine WORKDIR /app # Copy the built JAR from builder stage -COPY --from=builder /build/target/org.etsi.osl.controllers.ietf.ns-0.0.1-SNAPSHOT.jar app.jar +COPY --from=builder /build/target/org.etsi.osl.controllers.ietf.ns-0.0.1-SNAPSHOT-exec.jar app.jar # Run the application ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/pom.xml b/pom.xml index 27d1fc8..19e7316 100644 --- a/pom.xml +++ b/pom.xml @@ -367,6 +367,28 @@ + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot-version} + + + + repackage + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot-version} + + exec + + + \ No newline at end of file -- GitLab From 3dea5cdd151f932cbea914be23204bcaa85c7c3d Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 23 Jul 2026 14:29:02 +0300 Subject: [PATCH 12/12] fox Dockerfile --- Dockerfile | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1453932..a975b49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,18 @@ # Multi-stage build for TFS Controller # Stage 1: Build the application -FROM maven:3.9.6-eclipse-temurin-17 AS builder +# FROM maven:3.9.6-eclipse-temurin-17 AS builder -WORKDIR /build +# WORKDIR /build -# Copy pom.xml and download dependencies -COPY pom.xml . -RUN mvn dependency:go-offline -B +# # Copy pom.xml and download dependencies +# COPY pom.xml . +# RUN mvn dependency:go-offline -B -# Copy source code -COPY src ./src +# # Copy source code +# COPY src ./src -# Build the application -RUN mvn clean package -DskipTests +# # Build the application +# RUN mvn clean package -DskipTests # Stage 2: Runtime image FROM eclipse-temurin:17-jdk-alpine @@ -20,7 +20,8 @@ FROM eclipse-temurin:17-jdk-alpine WORKDIR /app # Copy the built JAR from builder stage -COPY --from=builder /build/target/org.etsi.osl.controllers.ietf.ns-0.0.1-SNAPSHOT-exec.jar app.jar +# COPY --from=builder /build/target/org.etsi.osl.controllers.ietf.ns-0.0.9-SNAPSHOT.jar app.jar +COPY target/org.etsi.osl.controllers.ietf.ns-0.0.9-SNAPSHOT-exec.jar app.jar # Run the application -ENTRYPOINT ["java", "-jar", "app.jar"] +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file -- GitLab