Commit 0b6c2642 authored by Christos Tranoris's avatar Christos Tranoris
Browse files

adding callback health not status

parent f3654501
Loading
Loading
Loading
Loading
Loading
+26 −58
Original line number Diff line number Diff line
@@ -6,10 +6,9 @@ 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.Resource;
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.etsi.osl.tmf.ri639.model.ResourceHealth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -19,7 +18,7 @@ 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
 * actually populates the plan (name, ITU-T X.731 health, 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.
 *
@@ -35,7 +34,6 @@ public class PlanningCallbackClient {
  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";
@@ -66,13 +64,17 @@ public class PlanningCallbackClient {
   * @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
   * @param health        the ITU-T X.731 health verdict the simulated resource declares about itself
   *                      ({@code PENDING} while deploying, {@code UP} once available, {@code GONE}
   *                      once torn down) - this, not any inventory status, is what the tentative
   *                      service derives its own state from
   */
  public void report(String serviceOrderId, String serviceId, String resourceId, String resourceName,
      ResourceStatusType status,
      ResourceHealth health,
      Double creationDurationSeconds, Double creationEnergy, Double creationCost,
      Double operationDurationSeconds, Double operationEnergy, Double operationCost,
      Double teardownDurationSeconds, Double teardownEnergy, Double teardownCost) {
    send(serviceOrderId, serviceId, resourceId, resourceName, status,
    send(serviceOrderId, serviceId, resourceId, resourceName, health,
        creationDurationSeconds, creationEnergy, creationCost,
        operationDurationSeconds, operationEnergy, operationCost,
        teardownDurationSeconds, teardownEnergy, teardownCost,
@@ -88,7 +90,7 @@ public class PlanningCallbackClient {
   * <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
   *   <li>health {@code UP} - 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>
@@ -109,7 +111,7 @@ public class PlanningCallbackClient {
  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,
    send(serviceOrderId, serviceId, null, resourceName, ResourceHealth.UP,
        null, null, null,
        null, null, null,
        null, null, null,
@@ -117,7 +119,7 @@ public class PlanningCallbackClient {
  }

  private void send(String serviceOrderId, String serviceId, String resourceId, String resourceName,
      ResourceStatusType status,
      ResourceHealth health,
      Double creationDurationSeconds, Double creationEnergy, Double creationCost,
      Double operationDurationSeconds, Double operationEnergy, Double operationCost,
      Double teardownDurationSeconds, Double teardownEnergy, Double teardownCost,
@@ -135,12 +137,18 @@ public class PlanningCallbackClient {
      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));
      // The ITU-T X.731 health the simulated resource declares about itself - the only thing the
      // tentative service derives its own state from. CRIDGE never asserts an inventory
      // resourceStatus here, exactly as CatalogClient.withoutResourceStatus() keeps it off every
      // real catalog write. Spelt out via the model's canonical inverse so it stays in lock-step
      // with Resource.deriveHealth(); PENDING (or null) leaves both attributes unset, which reads
      // as "not up yet".
      Resource healthProbe = new Resource();
      if (health != null) {
        healthProbe.applyHealth(health);
      }
      putIfPresent(headers, HDR_OPERATIONAL_STATE, healthProbe.getOperationalState());
      putIfPresent(headers, HDR_ADMINISTRATIVE_STATE, healthProbe.getAdministrativeState());
      putIfPresent(headers, HDR_EST_CREATION_DURATION, creationDurationSeconds);
      putIfPresent(headers, HDR_EST_CREATION_ENERGY, creationEnergy);
      putIfPresent(headers, HDR_EST_CREATION_COST, creationCost);
@@ -164,8 +172,8 @@ public class PlanningCallbackClient {
        });
      }

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

    } catch (Exception e) {
@@ -174,46 +182,6 @@ public class PlanningCallbackClient {
    }
  }

  /**
   * The health a simulated resource in this state declares about itself. Mirrors
   * {@code KubernetesCRV1.applyState} 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());
+23 −254

File changed.

Preview size limit exceeded, changes collapsed.

+32 −24
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.camel.ProducerTemplate;
import org.etsi.osl.tmf.ri639.model.ResourceStatusType;
import org.etsi.osl.tmf.ri639.model.ResourceHealth;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -26,8 +26,12 @@ import org.springframework.test.util.ReflectionTestUtils;
 * the {@code PLANNING.RESOURCE.CREATEORUPDATE} request matches OSOM's
 * {@code PlanningResourceCallbackHandler} contract (section 5/6/7.1 of
 * docs/20260805_0123_writing-a-simulator-adapter.md) - required
 * {@code org.etsi.osl.serviceOrderId}, echoed {@code org.etsi.osl.resourceId}, and that unmeasured
 * estimate components are omitted rather than sent as zero.
 * {@code org.etsi.osl.serviceOrderId}, echoed {@code org.etsi.osl.resourceId}, the ITU-T X.731
 * health headers, and that unmeasured estimate components are omitted rather than sent as zero.
 *
 * <p>CRIDGE never asserts an inventory {@code org.etsi.osl.resourceStatus} on a planning callback
 * (same rule the real catalog path follows via {@code CatalogClient.withoutResourceStatus()}), so
 * every case here also checks that header is absent.
 */
class PlanningCallbackClientTest {

@@ -47,9 +51,9 @@ class PlanningCallbackClientTest {

  @Test
  @SuppressWarnings("unchecked")
  void report_reserved_setsRequiredAndEchoedHeaders_andOmitsUnmeasuredEstimates() {
  void report_pending_setsRequiredAndEchoedHeaders_andOmitsUnmeasuredEstimates() {
    client.report("orderid-xxx-xxx-xxx", "sid-xxx-xxx-xxx", "rid-12345678-xxxx", "crrid-1234",
        ResourceStatusType.RESERVED,
        ResourceHealth.PENDING,
        30d, 5d, 2d,
        null, null, null,
        null, null, null);
@@ -61,7 +65,7 @@ class PlanningCallbackClientTest {
    Map<String, Object> headers = headersCaptor.getValue();
    assertEquals("orderid-xxx-xxx-xxx", headers.get("org.etsi.osl.serviceOrderId"));
    assertEquals("rid-12345678-xxxx", headers.get("org.etsi.osl.resourceId"));
    assertEquals("RESERVED", headers.get("org.etsi.osl.resourceStatus"));
    assertFalse(headers.containsKey("org.etsi.osl.resourceStatus"));
    assertEquals("30.0", headers.get("org.etsi.osl.estimate.creation.durationSeconds"));
    assertEquals("5.0", headers.get("org.etsi.osl.estimate.creation.energy"));
    assertEquals("2.0", headers.get("org.etsi.osl.estimate.creation.cost"));
@@ -76,9 +80,9 @@ class PlanningCallbackClientTest {
  }

  @Test
  void report_available_afterOperationEstimate_reportsOperationHeadersOnly() {
  void report_up_afterOperationEstimate_reportsOperationHeadersOnly() {
    client.report("orderid-xxx-xxx-xxx", "sid-xxx-xxx-xxx", "rid-12345678-xxxx", "crrid-1234",
        ResourceStatusType.AVAILABLE,
        ResourceHealth.UP,
        null, null, null,
        86400d, 40d, 16d,
        null, null, null);
@@ -88,15 +92,15 @@ class PlanningCallbackClientTest {
    verify(template).requestBodyAndHeaders(anyString(), anyString(), headersCaptor.capture());

    Map<String, Object> headers = headersCaptor.getValue();
    assertEquals("AVAILABLE", headers.get("org.etsi.osl.resourceStatus"));
    assertFalse(headers.containsKey("org.etsi.osl.resourceStatus"));
    assertEquals("86400.0", headers.get("org.etsi.osl.estimate.operation.durationSeconds"));
    assertFalse(headers.containsKey("org.etsi.osl.estimate.creation.durationSeconds"));
  }

  @Test
  void report_unknown_forTeardown_reportsTeardownHeadersOnly() {
  void report_gone_forTeardown_reportsTeardownHeadersOnly() {
    client.report("orderid-xxx-xxx-xxx", "sid-xxx-xxx-xxx", "rid-12345678-xxxx", "crrid12345",
        ResourceStatusType.UNKNOWN,
        ResourceHealth.GONE,
        null, null, null,
        null, null, null,
        15d, 2d, 1d);
@@ -106,7 +110,7 @@ class PlanningCallbackClientTest {
    verify(template).requestBodyAndHeaders(anyString(), anyString(), headersCaptor.capture());

    Map<String, Object> headers = headersCaptor.getValue();
    assertEquals("UNKNOWN", headers.get("org.etsi.osl.resourceStatus"));
    assertFalse(headers.containsKey("org.etsi.osl.resourceStatus"));
    assertEquals("15.0", headers.get("org.etsi.osl.estimate.teardown.durationSeconds"));
    assertEquals("1.0", headers.get("org.etsi.osl.estimate.teardown.cost"));
  }
@@ -130,9 +134,12 @@ class PlanningCallbackClientTest {
        bodyCaptor.capture(), headersCaptor.capture());

    // No resourceId: OSOM must create a distinct new ExpectedResource for it, not update the row of
    // the resource whose deployment produced it. And nothing billable is claimed for it.
    // the resource whose deployment produced it. It declares health UP (a side effect is only ever
    // seen once the thing that produces it is up), no inventory status, and nothing billable.
    Map<String, Object> headers = headersCaptor.getValue();
    assertEquals("AVAILABLE", headers.get("org.etsi.osl.resourceStatus"));
    assertFalse(headers.containsKey("org.etsi.osl.resourceStatus"));
    assertEquals("enable", headers.get("org.etsi.osl.operationalState"));
    assertEquals("unlocked", headers.get("org.etsi.osl.administrativeState"));
    assertFalse(headers.containsKey("org.etsi.osl.resourceId"));
    assertFalse(headers.containsKey("org.etsi.osl.estimate.creation.durationSeconds"));
    assertFalse(headers.containsKey("org.etsi.osl.estimate.operation.durationSeconds"));
@@ -156,7 +163,7 @@ class PlanningCallbackClientTest {
  @Test
  void report_withoutServiceOrderId_isSkipped_neverCallsTemplate() {
    client.report(null, "sid-xxx-xxx-xxx", "rid-12345678-xxxx", "crrid12345",
        ResourceStatusType.RESERVED,
        ResourceHealth.PENDING,
        30d, 5d, 2d,
        null, null, null,
        null, null, null);
@@ -166,14 +173,14 @@ class PlanningCallbackClientTest {

  /**
   * The health headers are the ones that actually move the tentative service: OSOM derives service
   * state from them and not from the inventory status. A resource that has only been submitted
   * declares no health at all, which OSOM reads as "not up yet".
   * state from them and not from any inventory status. A resource that has only been submitted
   * declares no health at all ({@code PENDING}), which OSOM reads as "not up yet".
   */
  @Test
  @SuppressWarnings("unchecked")
  void report_reserved_declaresNoHealth_soTheServiceStaysReserved() {
  void report_pending_declaresNoHealth_soTheServiceStaysReserved() {
    client.report("orderid-xxx-xxx-xxx", "sid-xxx-xxx-xxx", "rid-12345678-xxxx", "crrid-1234",
        ResourceStatusType.RESERVED,
        ResourceHealth.PENDING,
        30d, 5d, 2d, null, null, null, null, null, null);

    ArgumentCaptor<Map<String, Object>> headersCaptor = ArgumentCaptor.forClass(Map.class);
@@ -182,13 +189,14 @@ class PlanningCallbackClientTest {
    Map<String, Object> headers = headersCaptor.getValue();
    assertFalse(headers.containsKey("org.etsi.osl.operationalState"));
    assertFalse(headers.containsKey("org.etsi.osl.administrativeState"));
    assertFalse(headers.containsKey("org.etsi.osl.resourceStatus"));
  }

  @Test
  @SuppressWarnings("unchecked")
  void report_available_declaresTheResourceUp() {
  void report_up_declaresTheResourceUp() {
    client.report("orderid-xxx-xxx-xxx", "sid-xxx-xxx-xxx", "rid-12345678-xxxx", "crrid-1234",
        ResourceStatusType.AVAILABLE,
        ResourceHealth.UP,
        null, null, null, 86400d, 40d, 16d, null, null, null);

    ArgumentCaptor<Map<String, Object>> headersCaptor = ArgumentCaptor.forClass(Map.class);
@@ -199,12 +207,12 @@ class PlanningCallbackClientTest {
    assertEquals("unlocked", headers.get("org.etsi.osl.administrativeState"));
  }

  /** Teardown: SHUTDOWN is the tombstone that drives the tentative service to TERMINATED. */
  /** Teardown: {@code GONE} spells out as DISABLE/SHUTDOWN, which drives the tentative service to TERMINATED. */
  @Test
  @SuppressWarnings("unchecked")
  void report_suspended_declaresTheResourceGone() {
  void report_gone_declaresTheResourceGone() {
    client.report("orderid-xxx-xxx-xxx", "sid-xxx-xxx-xxx", "rid-12345678-xxxx", "crrid12345",
        ResourceStatusType.SUSPENDED,
        ResourceHealth.GONE,
        null, null, null, null, null, null, 15d, 2d, 1d);

    ArgumentCaptor<Map<String, Object>> headersCaptor = ArgumentCaptor.forClass(Map.class);
+248 −0

File added.

Preview size limit exceeded, changes collapsed.

+22 −159

File changed.

Preview size limit exceeded, changes collapsed.