From ec4e19028f8aff64984859811a67ad2a68a15b8f Mon Sep 17 00:00:00 2001 From: Christos Tranoris Date: Thu, 13 Nov 2025 16:26:33 +0200 Subject: [PATCH] change the slice-service request to an array --- .../api/SloSleTemplateBootstrapService.java | 131 +++++++++++------- .../domain/model/NetworkSliceServices.java | 95 ++++++++++++- .../tfs/api/domain/model/SliceService.java | 80 +---------- .../tfs/api/restconf/RestconfClient.java | 26 +++- .../tfs/api/restconf/RestconfClientImpl.java | 36 +++-- .../api/restconf/RestconfConsumerService.java | 50 ++++--- .../repository/impl/ResourceRepoService.java | 46 ++++-- 7 files changed, 284 insertions(+), 180 deletions(-) diff --git a/src/main/java/org/etsi/osl/controllers/tfs/api/SloSleTemplateBootstrapService.java b/src/main/java/org/etsi/osl/controllers/tfs/api/SloSleTemplateBootstrapService.java index 4e182e3..b6e4c77 100644 --- a/src/main/java/org/etsi/osl/controllers/tfs/api/SloSleTemplateBootstrapService.java +++ b/src/main/java/org/etsi/osl/controllers/tfs/api/SloSleTemplateBootstrapService.java @@ -7,11 +7,14 @@ 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.tfs.api.domain.model.NetworkSliceServices; import org.etsi.osl.controllers.tfs.api.domain.model.ServiceStatus; import org.etsi.osl.controllers.tfs.api.domain.model.ServiceTag; import org.etsi.osl.controllers.tfs.api.domain.model.SliceService; @@ -396,7 +399,7 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { * @return SliceService domain object * @throws Exception if JSON parsing fails */ - public SliceService parseAndConvertSliceService(String sliceServiceJson) throws Exception { + public NetworkSliceServices parseAndConvertSliceService(String sliceServiceJson) throws Exception { log.info("Parsing RFC 9543 SliceService JSON"); try { @@ -414,23 +417,24 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { throw new IllegalArgumentException("No slice-service array found"); } - // Get the first slice service from the array - JsonNode sliceServiceNode = sliceServicesNode.get(0); + // Parse all slice services from the array + List sliceServices = new ArrayList<>(); - // Parse service identity - String serviceId = sliceServiceNode.get("id").asText(); - String serviceDescription = sliceServiceNode.has("description") ? - sliceServiceNode.get("description").asText() : "Network Slice Service"; + for (int i = 0; i < sliceServicesNode.size(); i++) { + JsonNode sliceServiceNode = sliceServicesNode.get(i); - log.info("Parsing slice service: {}", serviceId); + // Parse service identity + String serviceId = sliceServiceNode.get("id").asText(); + String serviceDescription = sliceServiceNode.has("description") ? + sliceServiceNode.get("description").asText() : "Network Slice Service"; - // Create SliceService domain object - SliceService service = new SliceService(); - service.setId(serviceId); - service.setDescription(serviceDescription); - service.setTestOnly(false); - // Store just the slice service JSON (not the wrapped namespace) - service.setJsonRequest(mapper.writeValueAsString(sliceServiceNode)); + 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")) { @@ -475,17 +479,32 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { } } - // 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()); + // 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); } - log.info("Successfully parsed RFC 9543 SliceService: {}", serviceId); - return service; + // 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()); @@ -554,18 +573,23 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { } } - // Parse JSON to SliceService - SliceService sliceService = parseAndConvertSliceService(serviceJson); - log.info("Successfully parsed backhaul slice service: {}", sliceService.getId()); + // Parse JSON to NetworkSliceServices + NetworkSliceServices networkSliceServices = parseAndConvertSliceService(serviceJson); + log.info("Successfully parsed backhaul network slice services with {} service(s)", + networkSliceServices.getSliceServices().size()); - // Convert SliceService to LogicalResourceSpecification - LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(sliceService); + // Convert NetworkSliceServices to LogicalResourceSpecification + LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(networkSliceServices); // Enhance specification with backhaul metadata - spec.setName( spec.getName() + "_LocalTemplate_" + sliceService.getSloSleTemplate().getId() ); + 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 + " - " + sliceService.getDescription() + + spec.setDescription(description + " - " + networkSliceServices.getEntityDescription() + ". Maps 3GPP network slice to IETF RFC 9543 standard format."); spec.setType("LogicalResourceSpecification"); spec.setBaseType("ResourceSpecification"); @@ -607,14 +631,13 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { for (SloSleTemplate template : providerTemplates) { try { - SliceService exampleService = createExampleSliceServiceForTemplate(template); - if (exampleService != null) { - // Convert to LogicalResource - //LogicalResource resource = logicalResourceMapper.toLogicalResource(exampleService); - LogicalResourceSpecification spec = mapper.toLogicalResourceSpec(exampleService); - spec.setName( spec.getName() + "_fromTemplate_" + template.getEntityName()); - - if (spec != null) { + 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); } @@ -642,9 +665,9 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { * - One connection group for point-to-point connectivity * * @param template The SloSleTemplate to create an example service for - * @return SliceService instance configured with the template + * @return NetworkSliceServices instance configured with the template */ - private SliceService createExampleSliceServiceForTemplate(SloSleTemplate template) { + private NetworkSliceServices createExampleSliceServiceForTemplate(SloSleTemplate template) { try { // Create unique service ID String serviceId = "slice-service-" + UUID.randomUUID().toString(); @@ -655,10 +678,6 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { "' from RESTCONF provider"); service.setTestOnly(false); - // Generate synthetic example slice service JSON - String exampleJson = buildExampleSliceServiceJson(serviceId, template.getId()); - service.setJsonRequest(exampleJson); - // Set template reference service.setSloSleTemplate(template); @@ -673,11 +692,21 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { tag.setValue("provider-template:" + template.getId()); service.getServiceTags().add(tag); - log.debug("Created example SliceService {} for template {}", serviceId, template.getId()); - return service; + // 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 SliceService for template: {}", template.getId(), e); + log.error("Error creating example NetworkSliceServices for template: {}", template.getId(), e); return null; } } @@ -812,7 +841,13 @@ public class SloSleTemplateBootstrapService implements CommandLineRunner { connectionGroups.set("connection-group", connGroupArray); sliceService.set("connection-groups", connectionGroups); - return mapper.writeValueAsString(sliceService); + // 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); diff --git a/src/main/java/org/etsi/osl/controllers/tfs/api/domain/model/NetworkSliceServices.java b/src/main/java/org/etsi/osl/controllers/tfs/api/domain/model/NetworkSliceServices.java index 1787d29..ca233af 100644 --- a/src/main/java/org/etsi/osl/controllers/tfs/api/domain/model/NetworkSliceServices.java +++ b/src/main/java/org/etsi/osl/controllers/tfs/api/domain/model/NetworkSliceServices.java @@ -5,20 +5,113 @@ import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import org.etsi.osl.controllers.tfs.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 { +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/tfs/api/domain/model/SliceService.java b/src/main/java/org/etsi/osl/controllers/tfs/api/domain/model/SliceService.java index 639de7a..9429e47 100644 --- a/src/main/java/org/etsi/osl/controllers/tfs/api/domain/model/SliceService.java +++ b/src/main/java/org/etsi/osl/controllers/tfs/api/domain/model/SliceService.java @@ -6,19 +6,12 @@ import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.etsi.osl.controllers.tfs.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; /** * 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. * - * Implements LogicalResourceMappable to enable conversion to TMF LogicalResource instances. - * * Uses custom RFC 9543 deserializer to handle hyphenated field names in JSON: * - "service-tags" → serviceTags * - "slo-sle-policy" → sloSleTemplate @@ -29,16 +22,11 @@ import org.etsi.osl.tmf.ri639.model.ResourceStatusType; @Data @NoArgsConstructor @AllArgsConstructor -public class SliceService implements LogicalResourceMappable { +public class SliceService { private String id; private String description; private Boolean testOnly = false; private ServiceStatus status; - - /** - * thia is for now the Slice Service request that will go down to the consumer as is - */ - private String jsonRequest; // 0-to-1 relationship: references an SLO/SLE template at the service level private SloSleTemplate sloSleTemplate; @@ -51,70 +39,4 @@ public class SliceService implements LogicalResourceMappable { // 1-to-many relationship: contains multiple connection groups private List connectionGroups = new ArrayList<>(); - - // ========== LogicalResourceMappable Implementation ========== - - /** - * Get the unique identifier for this slice service. - */ - @Override - public String getEntityId() { - return this.id; - } - - /** - * Get the display name for this slice service. - */ - @Override - public String getEntityName() { - return this.id; - } - - /** - * Get the description for this slice service. - */ - @Override - public String getEntityDescription() { - return this.description; - } - - /** - * Check if this entity has status mapping. - * SliceService has a status field, so this should return true. - */ - @Override - public boolean hasStatusMapping() { - return this.status != null; - } - - /** - * Map service status to TMF639 resource states. - */ - @Override - public void mapStatusToResourceStates(LogicalResource resource) { - if (this.status != null) { - // Map administrative state - String adminState = this.status.getAdminState(); - if (adminState != null && adminState.equals("admin-up")) { - resource.setAdministrativeState( ResourceAdministrativeStateType.UNLOCKED ); - } else if (adminState != null && adminState.equals("admin-down")) { - resource.setAdministrativeState( ResourceAdministrativeStateType.UNLOCKED); - } - - // Map operational state - String operState = this.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/tfs/api/restconf/RestconfClient.java b/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfClient.java index 633c347..dfbdb9d 100644 --- a/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfClient.java +++ b/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfClient.java @@ -22,15 +22,27 @@ import org.etsi.osl.controllers.tfs.api.domain.model.SliceService; public interface RestconfClient { /** - * Create a new network slice service on the provider. - * - * HTTP Operation: PUT /restconf/data/ietf-network-slice-service:network-slice-services/slice-service= - * - * @param service The network slice service to create - * @return The created slice service with updated status + * 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 */ - SliceService createSliceService(SliceService service) throws RestconfException; + List createSliceService(List services) throws RestconfException; /** * Retrieve a specific network slice service from the provider. diff --git a/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfClientImpl.java b/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfClientImpl.java index 3ab6cf7..14e60a9 100644 --- a/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfClientImpl.java +++ b/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfClientImpl.java @@ -3,7 +3,9 @@ package org.etsi.osl.controllers.tfs.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.tfs.api.domain.model.NetworkSliceServices; import org.etsi.osl.controllers.tfs.api.domain.model.SliceService; import org.slf4j.Logger; @@ -71,39 +73,47 @@ public class RestconfClientImpl implements RestconfClient { private ObjectMapper objectMapper; @Override - public SliceService createSliceService(SliceService service) throws RestconfException { - if (service == null || service.getId() == null) { - throw new RestconfException("Service and service ID must not be null"); + 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 = buildServiceUriPost(service.getId()); - logger.info("Creating network slice service: {}", service.getId()); + String uri = providerUrl + "/restconf/data/ietf-network-slice-service:network-slice-services"; + logger.info("Creating {} network slice service(s)", services.size()); try { - HttpEntity requestEntity = new HttpEntity<>(service, buildHeaders()); - ResponseEntity response = restTemplate.exchange( + // 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, - SliceService.class + String.class ); if (response.getStatusCode().is2xxSuccessful()) { - logger.info("Successfully created network slice service: {}", service.getId()); - return response.getBody(); + 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 service" + "Failed to create network slice services" ); } } catch (HttpClientErrorException e) { throw handleHttpError(e); } catch (Exception e) { - logger.error("Error creating network slice service: {}", service.getId(), e); - throw new RestconfException("Failed to create network slice service: " + e.getMessage(), e); + logger.error("Error creating network slice services", e); + throw new RestconfException("Failed to create network slice services: " + e.getMessage(), e); } } diff --git a/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfConsumerService.java b/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfConsumerService.java index af4bb12..7537232 100644 --- a/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfConsumerService.java +++ b/src/main/java/org/etsi/osl/controllers/tfs/api/restconf/RestconfConsumerService.java @@ -1,6 +1,7 @@ package org.etsi.osl.controllers.tfs.api.restconf; import java.time.OffsetDateTime; +import java.util.ArrayList; import java.util.List; import org.etsi.osl.controllers.tfs.api.domain.model.ConnectionGroup; import org.etsi.osl.controllers.tfs.api.domain.model.ConnectivityType; @@ -36,31 +37,44 @@ public class RestconfConsumerService { private RestconfClient restconfClient; /** - * Provisions a new network slice service on the provider. + * Provisions new network slice services on the provider. * - * @param service The slice service configuration to provision - * @return The provisioned service with updated status + * @param services List of slice service configurations to provision + * @return List of provisioned services with updated status * @throws RestconfException if provisioning fails */ - public SliceService provisionSliceService(SliceService service) throws RestconfException { - logger.info("Provisioning network slice service: {}", service.getId()); - - // Validate service configuration - //validateSliceServiceConfiguration(service); + public List provisionSliceServices(List services) throws RestconfException { + logger.info("Provisioning {} network slice service(s)", services.size()); - // Set initial service status - if (service.getStatus() == null) { - service.setStatus(new ServiceStatus()); + // 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()); } - service.getStatus().setAdminState("admin-up"); - service.getStatus().setLastChange(OffsetDateTime.now()); - // Create on provider - SliceService createdService = restconfClient.createSliceService(service); - logger.info("Successfully provisioned service {} with status: {}", - service.getId(), createdService.getStatus().getOperState()); + // Create on provider (sends array in RFC 9543 format) + List createdServices = restconfClient.createSliceService(services); + logger.info("Successfully provisioned {} service(s)", createdServices.size()); + + return createdServices; + } - return createdService; + /** + * 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); } /** diff --git a/src/main/java/org/etsi/osl/controllers/tfs/repository/impl/ResourceRepoService.java b/src/main/java/org/etsi/osl/controllers/tfs/repository/impl/ResourceRepoService.java index 962dfe7..c9ff5c8 100644 --- a/src/main/java/org/etsi/osl/controllers/tfs/repository/impl/ResourceRepoService.java +++ b/src/main/java/org/etsi/osl/controllers/tfs/repository/impl/ResourceRepoService.java @@ -83,35 +83,53 @@ public class ResourceRepoService { try { logger.info("Parsing slice request JSON for resource: {}", resourceid); - // Parse JSON string to SliceService object + // 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); - SliceService sliceService = mapper.readValue(jsonRequest, SliceService.class); + // 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("Successfully parsed SliceService: {}", sliceService.getId()); + if (sliceServiceNode == null || !sliceServiceNode.isArray()) { + throw new IllegalArgumentException("Expected 'slice-service' array in JSON request"); + } - // Send to RESTCONF provider if consumer service is available - if (restconfConsumerService != null) { - logger.info("Sending slice service request to RESTCONF provider: {}", sliceService.getId()); + // 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); + } - // Provision the slice service via RESTCONF - SliceService provisionedService = restconfConsumerService.provisionSliceService(sliceService); + logger.info("Successfully parsed {} SliceService(s)", sliceServices.size()); - if (provisionedService != null && provisionedService.getId() != null) { - logger.info("Successfully provisioned slice service via RESTCONF: {}", provisionedService.getId()); - infoMessage = "Successfully created and provisioned via RESTCONF: " + provisionedService.getId(); + // 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 service via RESTCONF - received null response"); - infoMessage = "Failed to provision via RESTCONF: null response"; + 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 service not sent to provider"); + 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; -- GitLab