Commit da274119 authored by Christos Tranoris's avatar Christos Tranoris
Browse files

simulation support

parent e5f5bba8
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -10,7 +10,9 @@ import org.etsi.osl.domain.model.kubernetes.KubernetesConfigMap;
import org.etsi.osl.domain.model.kubernetes.KubernetesSecret;
import org.etsi.osl.domain.model.kubernetes.KubernetesService;
import org.etsi.osl.tmf.rcm634.model.ResourceSpecification;
import org.etsi.osl.tmf.ri639.model.ResourceAdministrativeStateType;
import org.etsi.osl.tmf.ri639.model.ResourceCreate;
import org.etsi.osl.tmf.ri639.model.ResourceOperationalStateType;
import org.etsi.osl.tmf.ri639.model.ResourceStatusType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -183,6 +185,10 @@ public class NamespaceWatcher {
    ResourceCreate rs = this.KubernetesSecret2OpensliceResource( resource ).toResourceCreate();
    if (toDelete) {
      rs.setResourceStatus(ResourceStatusType.SUSPENDED);
      // The object is gone from the cluster: say so in the X.731 attributes the supported service
      // actually reads, not only in the inventory status.
      rs.setOperationalState(ResourceOperationalStateType.DISABLE);
      rs.setAdministrativeState(ResourceAdministrativeStateType.SHUTDOWN);
    }
    catalogClient.createOrUpdateResourceByNameCategoryVersion( rs );  
    
@@ -330,6 +336,10 @@ public class NamespaceWatcher {
    ResourceCreate rs = this.KubernetesService2OpensliceResource( resource ).toResourceCreate();
    if (toDelete) {
      rs.setResourceStatus(ResourceStatusType.SUSPENDED);
      // The object is gone from the cluster: say so in the X.731 attributes the supported service
      // actually reads, not only in the inventory status.
      rs.setOperationalState(ResourceOperationalStateType.DISABLE);
      rs.setAdministrativeState(ResourceAdministrativeStateType.SHUTDOWN);
    }
    catalogClient.createOrUpdateResourceByNameCategoryVersion( rs );  
    
@@ -500,6 +510,10 @@ public class NamespaceWatcher {
    ResourceCreate rs = this.KubernetesConfigMap2OpensliceResource( resource ).toResourceCreate();
    if (toDelete) {
      rs.setResourceStatus(ResourceStatusType.SUSPENDED);
      // The object is gone from the cluster: say so in the X.731 attributes the supported service
      // actually reads, not only in the inventory status.
      rs.setOperationalState(ResourceOperationalStateType.DISABLE);
      rs.setAdministrativeState(ResourceAdministrativeStateType.SHUTDOWN);
    }
    catalogClient.createOrUpdateResourceByNameCategoryVersion( rs );  
    
+62 −0
Original line number Diff line number Diff line
package org.etsi.osl.cridge.simulation;

import org.apache.camel.LoggingLevel;
import org.apache.camel.builder.RouteBuilder;
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.stereotype.Service;

/**
 * Consumers for OSOM's tentative-planning "dry-run" queues - the {@code /PLANNING}-suffixed
 * counterparts of {@link org.etsi.osl.cridge.CRRouteBuilder}'s three production routes. See
 * {@link SimulatedKubernetesClientResource} for what answering them actually does: nothing real,
 * ever.
 *
 * <p>This is intentionally a separate {@code RouteBuilder}/bean pair from the production one.
 * {@link org.etsi.osl.cridge.CRRouteBuilder} and {@link org.etsi.osl.cridge.KubernetesClientResource}
 * are not modified by this class - production traffic, queues and behaviour are unchanged, and a bug
 * here cannot regress them.
 */
@Service
public class CRPlanningRouteBuilder extends RouteBuilder {

	@Value("${CRD_DEPLOY_CR_REQ_PLANNING}")
	private String CRD_DEPLOY_CR_REQ_PLANNING = "";

	@Value("${CRD_PATCH_CR_REQ_PLANNING}")
	private String CRD_PATCH_CR_REQ_PLANNING = "";

	@Value("${CRD_DELETE_CR_REQ_PLANNING}")
	private String CRD_DELETE_CR_REQ_PLANNING = "";

	@Autowired
	private SimulatedKubernetesClientResource simulatedKubernetesClientResource;

	private static final Logger logger = LoggerFactory.getLogger( CRPlanningRouteBuilder.class.getSimpleName());

	@Override
	public void configure() throws Exception {

		from( CRD_DEPLOY_CR_REQ_PLANNING )
		.log(LoggingLevel.INFO, log, CRD_DEPLOY_CR_REQ_PLANNING + " [SIMULATION] message received!")
		.to("log:DEBUG?showBody=true&showHeaders=true")
		.bean( simulatedKubernetesClientResource, "deployCR(${headers}, ${body})")
		.convertBodyTo( String.class );

		from( CRD_PATCH_CR_REQ_PLANNING )
		.log(LoggingLevel.INFO, log, CRD_PATCH_CR_REQ_PLANNING + " [SIMULATION] message received!")
		.to("log:DEBUG?showBody=true&showHeaders=true")
		.bean( simulatedKubernetesClientResource, "patchCR(${headers}, ${body})")
		.convertBodyTo( String.class );

		from( CRD_DELETE_CR_REQ_PLANNING )
		.log(LoggingLevel.INFO, log, CRD_DELETE_CR_REQ_PLANNING + " [SIMULATION] message received!")
		.to("log:DEBUG?showBody=true&showHeaders=true")
		.bean( simulatedKubernetesClientResource, "deleteCR(${headers}, ${body})")
		.convertBodyTo( String.class );

	}

}
+229 −0
Original line number Diff line number Diff line
package org.etsi.osl.cridge.simulation;

import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.camel.ProducerTemplate;
import org.etsi.osl.tmf.common.model.EValueType;
import org.etsi.osl.tmf.ri639.model.ResourceAdministrativeStateType;
import org.etsi.osl.tmf.ri639.model.ResourceCreate;
import org.etsi.osl.tmf.ri639.model.ResourceOperationalStateType;
import org.etsi.osl.tmf.ri639.model.ResourceStatusType;
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.stereotype.Component;

/**
 * Sends the async "planning resource" callback that OSOM's tentative-planning framework expects
 * from a simulator adapter: a request-reply call to {@code PLANNING.RESOURCE.CREATEORUPDATE} that
 * actually populates the plan (name, status, cost/duration/energy estimates). The synchronous reply
 * a simulated request receives on the {@code /PLANNING} queue is only a liveness/acceptance signal -
 * this is where the real answer goes.
 *
 * <p>Header names mirror {@code org.etsi.osl.osom.tentative.PlanningResourceCallbackHandler}'s
 * constants exactly (that class, in the OSOM repo, is authoritative if this drifts). See OSOM's
 * docs/20260805_0123_writing-a-simulator-adapter.md, section 5, for the full contract.
 */
@Component
public class PlanningCallbackClient {

  private static final Logger logger = LoggerFactory.getLogger( "org.etsi.osl.cridge" );

  public static final String HDR_SERVICE_ORDER_ID = "org.etsi.osl.serviceOrderId";
  public static final String HDR_SERVICE_ID = "org.etsi.osl.serviceId";
  public static final String HDR_RESOURCE_ID = "org.etsi.osl.resourceId";
  public static final String HDR_RESOURCE_STATUS = "org.etsi.osl.resourceStatus";
  public static final String HDR_OPERATIONAL_STATE = "org.etsi.osl.operationalState";
  public static final String HDR_ADMINISTRATIVE_STATE = "org.etsi.osl.administrativeState";
  public static final String HDR_EST_CREATION_DURATION = "org.etsi.osl.estimate.creation.durationSeconds";
  public static final String HDR_EST_CREATION_ENERGY = "org.etsi.osl.estimate.creation.energy";
  public static final String HDR_EST_CREATION_COST = "org.etsi.osl.estimate.creation.cost";
  public static final String HDR_EST_OPERATION_DURATION = "org.etsi.osl.estimate.operation.durationSeconds";
  public static final String HDR_EST_OPERATION_ENERGY = "org.etsi.osl.estimate.operation.energy";
  public static final String HDR_EST_OPERATION_COST = "org.etsi.osl.estimate.operation.cost";
  public static final String HDR_EST_TEARDOWN_DURATION = "org.etsi.osl.estimate.teardown.durationSeconds";
  public static final String HDR_EST_TEARDOWN_ENERGY = "org.etsi.osl.estimate.teardown.energy";
  public static final String HDR_EST_TEARDOWN_COST = "org.etsi.osl.estimate.teardown.cost";

  @Value("${PLANNING_RESOURCE_CREATEORUPDATE}")
  private String PLANNING_RESOURCE_CREATEORUPDATE = "";

  @Autowired
  private ProducerTemplate template;

  /**
   * Reports one resource callback. Never throws - a lost callback must not surface as a failure of
   * the request/reply path that has already answered OSOM synchronously; it just leaves the plan
   * showing this resource as it last knew it (or, on the very first callback, not at all).
   *
   * <p>Any estimate component left {@code null} is omitted from the message entirely rather than
   * sent as zero - per the OSOM contract, an omitted component means "not measured" and an explicit
   * zero means "measured as zero"; the two are not interchangeable.
   *
   * @param resourceId    OSOM's placeholder id for this resource (echoed so the callback updates,
   *                      rather than duplicates, the row OSOM already created)
   * @param resourceName  the name CRIDGE would have assigned the real custom resource
   */
  public void report(String serviceOrderId, String serviceId, String resourceId, String resourceName,
      ResourceStatusType status,
      Double creationDurationSeconds, Double creationEnergy, Double creationCost,
      Double operationDurationSeconds, Double operationEnergy, Double operationCost,
      Double teardownDurationSeconds, Double teardownEnergy, Double teardownCost) {
    send(serviceOrderId, serviceId, resourceId, resourceName, status,
        creationDurationSeconds, creationEnergy, creationCost,
        operationDurationSeconds, operationEnergy, operationCost,
        teardownDurationSeconds, teardownEnergy, teardownCost,
        null, null, null, null);
  }

  /**
   * Reports a resource that a simulated deployment would produce as a <em>side effect</em> of the
   * resource the request was actually about - e.g. the Secrets/ConfigMap that end up in the namespace
   * an ArgoCD K8aaS {@code Application} deploys into.
   *
   * <p>Fixed by construction, because that is what such a resource always is:
   * <ul>
   *   <li>no {@code resourceId} - it must create a genuinely new {@code ExpectedResource} in the plan
   *       rather than update the row of the resource that caused it;</li>
   *   <li>status {@code AVAILABLE} - it only ever appears, for real, once the thing that produces it
   *       is up;</li>
   *   <li>no estimates - a mirrored Secret/ConfigMap is a passive artifact, not separately billable
   *       (production's {@code KubernetesSecret.toResourceCreate()} carries none either).</li>
   * </ul>
   *
   * <p>{@code category}, {@code description} and {@code resourceVersion} mirror the corresponding
   * fields production's own domain-model classes set ({@code KubernetesSecret}/
   * {@code KubernetesConfigMap} in {@code org.etsi.osl.model.k8s}), and {@code characteristics} is a
   * flat name-&gt;value map attached to the {@link ResourceCreate} body as TEXT
   * {@code resourceCharacteristic} entries - so the plan shows a resource shaped like the real catalog
   * entry it stands in for rather than a bare name. Blank values are dropped, never sent as empty
   * characteristics.
   *
   * <p>{@code resourceSpecification} is deliberately <b>not</b> set: production resolves it to a
   * catalog-assigned ResourceSpecification uuid, and this class never talks to the catalog. An
   * invented uuid would be worse than an absent one.
   */
  public void reportSideEffectResource(String serviceOrderId, String serviceId, String resourceName,
      String category, String description, String resourceVersion,
      Map<String, String> characteristics) {
    send(serviceOrderId, serviceId, null, resourceName, ResourceStatusType.AVAILABLE,
        null, null, null,
        null, null, null,
        null, null, null,
        category, description, resourceVersion, characteristics);
  }

  private void send(String serviceOrderId, String serviceId, String resourceId, String resourceName,
      ResourceStatusType status,
      Double creationDurationSeconds, Double creationEnergy, Double creationCost,
      Double operationDurationSeconds, Double operationEnergy, Double operationCost,
      Double teardownDurationSeconds, Double teardownEnergy, Double teardownCost,
      String category, String description, String resourceVersion,
      Map<String, String> characteristics) {

    if (serviceOrderId == null || serviceOrderId.isBlank()) {
      logger.warn("Skipping planning resource callback for resourceId={}: no {} to report against.",
          resourceId, HDR_SERVICE_ORDER_ID);
      return;
    }

    try {
      Map<String, Object> headers = new HashMap<>();
      headers.put(HDR_SERVICE_ORDER_ID, serviceOrderId);
      putIfPresent(headers, HDR_SERVICE_ID, serviceId);
      putIfPresent(headers, HDR_RESOURCE_ID, resourceId);
      headers.put(HDR_RESOURCE_STATUS, status.name());
      // The health the simulated resource declares about itself. This — not the status above — is
      // what the tentative service derives its own state from, so the simulated deployment only
      // brings a service up when it reports ENABLE/UNLOCKED.
      putIfPresent(headers, HDR_OPERATIONAL_STATE, operationalStateFor(status));
      putIfPresent(headers, HDR_ADMINISTRATIVE_STATE, administrativeStateFor(status));
      putIfPresent(headers, HDR_EST_CREATION_DURATION, creationDurationSeconds);
      putIfPresent(headers, HDR_EST_CREATION_ENERGY, creationEnergy);
      putIfPresent(headers, HDR_EST_CREATION_COST, creationCost);
      putIfPresent(headers, HDR_EST_OPERATION_DURATION, operationDurationSeconds);
      putIfPresent(headers, HDR_EST_OPERATION_ENERGY, operationEnergy);
      putIfPresent(headers, HDR_EST_OPERATION_COST, operationCost);
      putIfPresent(headers, HDR_EST_TEARDOWN_DURATION, teardownDurationSeconds);
      putIfPresent(headers, HDR_EST_TEARDOWN_ENERGY, teardownEnergy);
      putIfPresent(headers, HDR_EST_TEARDOWN_COST, teardownCost);

      ResourceCreate rc = new ResourceCreate();
      rc.setName(resourceName);
      rc.setCategory(category);
      rc.setDescription(description);
      rc.setResourceVersion(resourceVersion);
      if (characteristics != null) {
        characteristics.forEach((name, value) -> {
          if (value != null && !value.isBlank()) {
            rc.addResourceCharacteristicItemShort(name, value, EValueType.TEXT.getValue());
          }
        });
      }

      logger.debug("Planning resource callback: order={} resourceId={} status={}",
          serviceOrderId, resourceId, status);
      template.requestBodyAndHeaders(PLANNING_RESOURCE_CREATEORUPDATE, toJsonString(rc), headers);

    } catch (Exception e) {
      logger.error("Planning resource callback failed for order={} resourceId={}: {}",
          serviceOrderId, resourceId, e.toString());
    }
  }

  /**
   * The health a simulated resource in this state declares about itself. Mirrors
   * {@code KubernetesCRV1.applyHealth} so a simulated deployment reads exactly like a real one:
   * only ENABLE + UNLOCKED lets the service derived from this resource reach ACTIVE, and only
   * SHUTDOWN tears it down. A status we say nothing about leaves both null, which reads as PENDING.
   */
  private static ResourceOperationalStateType operationalStateFor(ResourceStatusType status) {
    if (status == null) {
      return null;
    }
    switch (status) {
    case AVAILABLE:
      return ResourceOperationalStateType.ENABLE;
    case STANDBY:
    case ALARM:
    case SUSPENDED:
      return ResourceOperationalStateType.DISABLE;
    default:
      // RESERVED, UNKNOWN: submitted but not yet observed as up.
      return null;
    }
  }

  private static ResourceAdministrativeStateType administrativeStateFor(ResourceStatusType status) {
    if (status == null) {
      return null;
    }
    switch (status) {
    case AVAILABLE:
    case ALARM:
      return ResourceAdministrativeStateType.UNLOCKED;
    case STANDBY:
      return ResourceAdministrativeStateType.LOCKED;
    case SUSPENDED:
      return ResourceAdministrativeStateType.SHUTDOWN;
    default:
      return null;
    }
  }

  private static void putIfPresent(Map<String, Object> headers, String key, Object value) {
    if (value != null) {
      headers.put(key, value.toString());
    }
  }

  private String toJsonString(Object object) throws com.fasterxml.jackson.core.JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper();
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    return mapper.writeValueAsString(object);
  }

}
+480 −0

File added.

Preview size limit exceeded, changes collapsed.

+35 −1
Original line number Diff line number Diff line
@@ -46,3 +46,37 @@ CATALOG_GET_RESOURCESPEC_BY_NAME_CATEGORY: "jms:queue:CATALOG.GET.RESOURCESPEC_B
CRD_DEPLOY_CR_REQ: "jms:queue:CRD.DEPLOY.CR_REQ"
CRD_DELETE_CR_REQ: "jms:queue:CRD.DELETE.CR_REQ"
CRD_PATCH_CR_REQ: "jms:queue:CRD.PATCH.CR_REQ"

#CRD ACTIONS - OSOM tentative-planning simulation ("dry-run") queues.
#These are consumed only by org.etsi.osl.cridge.simulation, never by the production
#routes/beans above. See OSOM docs/20260812_1220_planning-queue-for-fixed-queue-adapters.md:
#the /PLANNING suffix, not any header, is the safety boundary that keeps a dry-run off real
#infrastructure.
CRD_DEPLOY_CR_REQ_PLANNING: "jms:queue:CRD.DEPLOY.CR_REQ/PLANNING"
CRD_PATCH_CR_REQ_PLANNING: "jms:queue:CRD.PATCH.CR_REQ/PLANNING"
CRD_DELETE_CR_REQ_PLANNING: "jms:queue:CRD.DELETE.CR_REQ/PLANNING"
PLANNING_RESOURCE_CREATEORUPDATE: "jms:queue:PLANNING.RESOURCE.CREATEORUPDATE"

#Tunables for the simulated planning callbacks (org.etsi.osl.cridge.simulation). The estimate figures
#below are static, clearly-labelled placeholders, not measured data. default-operation-duration-seconds
#is only a fallback: OSOM sends an org.etsi.osl.operationDurationSeconds header on every planning send
#(see OSOM docs/20260812_1545_operation-window-for-all-planning-sends.md), and that header - not this
#default - sizes the operation-phase estimate whenever it's present. This default (mirrors OSOM's own
#tentative.planning.default-operation-duration-seconds default of one day) only applies if a caller
#omits the header.
cridge:
  simulation:
    simulated-deploy-delay-ms: 1500
    default-operation-duration-seconds: 86400
    estimate:
      creation:
        duration-seconds: 30
        energy: 5
        cost: 2
      operation:
        energy: 0.01
        cost: 0.0001
      teardown:
        duration-seconds: 15
        energy: 2
        cost: 1
 No newline at end of file
Loading