diff options
author | Jim Hahn <jrh3@att.com> | 2020-03-03 21:02:13 -0500 |
---|---|---|
committer | Jim Hahn <jrh3@att.com> | 2020-03-05 15:42:01 -0500 |
commit | 03248c4de4197dac33c156e6a7a6538c9943305c (patch) | |
tree | 3224aa774d09649ed83d91a9a36f9bd969e701cf /models-interactions/model-actors/actor.so/src/main | |
parent | 7f1903bae3069d5e14b4c322c09c1317d90114b6 (diff) |
Add SO VF Module Delete Operation
Redesigned the SO Operation classes; moved some code from the subclass
to the superclass so it could be reused by the VF Module Delete Operation.
JerseyClient does not support DELETE with a request body, so had to
implement a delete() method using java11 HttpClient.
Fix some issues found while testing with drools-apps.
Added "delete" operation to SO simulator.
Issue-ID: POLICY-2371
Signed-off-by: Jim Hahn <jrh3@att.com>
Change-Id: I269fe13cf90c295ec2bbac92bc5a59b3820ea265
Diffstat (limited to 'models-interactions/model-actors/actor.so/src/main')
5 files changed, 525 insertions, 22 deletions
diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/RestManagerResponse.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/RestManagerResponse.java new file mode 100644 index 000000000..1b49ab579 --- /dev/null +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/RestManagerResponse.java @@ -0,0 +1,199 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.controlloop.actor.so; + +import java.lang.annotation.Annotation; +import java.net.URI; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import javax.ws.rs.core.EntityTag; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.Link.Builder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import lombok.Getter; +import org.onap.policy.common.utils.coder.Coder; +import org.onap.policy.common.utils.coder.CoderException; + +/** + * RestManager Response suitable for use with subclasses of HttpOperation. Only a couple + * of methods are implemented; the rest throw {@link UnsupportedOperationException}. + */ +public class RestManagerResponse extends Response { + // TODO move to actorServices + + @Getter + private final int status; + + private final String body; + private final Coder coder; + + /** + * Constructs the object. + * + * @param status HTTP response status code + * @param body response body + * @param coder coder to decode the entity body + */ + public RestManagerResponse(int status, String body, Coder coder) { + this.status = status; + this.body = body; + this.coder = coder; + } + + @Override + public void close() { + // do nothing + } + + @Override + public <T> T readEntity(Class<T> entityType) { + if (entityType == String.class) { + return entityType.cast(body); + } + + try { + return coder.decode(body, entityType); + } catch (CoderException e) { + throw new IllegalArgumentException("cannot decode response", e); + } + } + + @Override + public <T> T readEntity(GenericType<T> entityType) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> T readEntity(Class<T> entityType, Annotation[] annotations) { + throw new UnsupportedOperationException(); + } + + @Override + public <T> T readEntity(GenericType<T> entityType, Annotation[] annotations) { + throw new UnsupportedOperationException(); + } + + @Override + public StatusType getStatusInfo() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getEntity() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasEntity() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean bufferEntity() { + throw new UnsupportedOperationException(); + } + + @Override + public MediaType getMediaType() { + throw new UnsupportedOperationException(); + } + + @Override + public Locale getLanguage() { + throw new UnsupportedOperationException(); + } + + @Override + public int getLength() { + throw new UnsupportedOperationException(); + } + + @Override + public Set<String> getAllowedMethods() { + throw new UnsupportedOperationException(); + } + + @Override + public Map<String, NewCookie> getCookies() { + throw new UnsupportedOperationException(); + } + + @Override + public EntityTag getEntityTag() { + throw new UnsupportedOperationException(); + } + + @Override + public Date getDate() { + throw new UnsupportedOperationException(); + } + + @Override + public Date getLastModified() { + throw new UnsupportedOperationException(); + } + + @Override + public URI getLocation() { + throw new UnsupportedOperationException(); + } + + @Override + public Set<Link> getLinks() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasLink(String relation) { + throw new UnsupportedOperationException(); + } + + @Override + public Link getLink(String relation) { + throw new UnsupportedOperationException(); + } + + @Override + public Builder getLinkBuilder(String relation) { + throw new UnsupportedOperationException(); + } + + @Override + public MultivaluedMap<String, Object> getMetadata() { + throw new UnsupportedOperationException(); + } + + @Override + public MultivaluedMap<String, String> getStringHeaders() { + throw new UnsupportedOperationException(); + } + + @Override + public String getHeaderString(String name) { + throw new UnsupportedOperationException(); + } +} diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java index 9c9e6dc62..e4b9c14fe 100644 --- a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java @@ -99,6 +99,7 @@ public class SoActorServiceProvider extends HttpActor<SoActorParams> { super(NAME, SoActorParams.class); addOperator(new SoOperator(NAME, VfModuleCreate.NAME, VfModuleCreate::new)); + addOperator(new SoOperator(NAME, VfModuleDelete.NAME, VfModuleDelete::new)); } // TODO old code: remove lines down to **HERE** diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java index 41ecd07a0..1ca6c734c 100644 --- a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java @@ -21,12 +21,14 @@ package org.onap.policy.controlloop.actor.so; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import lombok.Getter; import org.onap.aai.domain.yang.CloudRegion; @@ -46,6 +48,7 @@ import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOp import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpConfig; import org.onap.policy.controlloop.policy.PolicyResult; import org.onap.policy.controlloop.policy.Target; +import org.onap.policy.so.SoCloudConfiguration; import org.onap.policy.so.SoModelInfo; import org.onap.policy.so.SoRequest; import org.onap.policy.so.SoRequestInfo; @@ -63,6 +66,7 @@ public abstract class SoOperation extends HttpOperation<SoResponse> { private static final Logger logger = LoggerFactory.getLogger(SoOperation.class); private static final Coder coder = new StandardCoder(); + public static final String PAYLOAD_KEY_VF_COUNT = "vfCount"; public static final String FAILED = "FAILED"; public static final String COMPLETE = "COMPLETE"; public static final int SO_RESPONSE_CODE = 999; @@ -208,7 +212,8 @@ public abstract class SoOperation extends HttpOperation<SoResponse> { // still incomplete // need a request ID with which to query - if (response.getRequestReferences() == null || response.getRequestReferences().getRequestId() == null) { + if (response == null || response.getRequestReferences() == null + || response.getRequestReferences().getRequestId() == null) { throw new IllegalArgumentException("missing request ID in response"); } @@ -371,6 +376,31 @@ public abstract class SoOperation extends HttpOperation<SoResponse> { } } + /** + * Construct cloudConfiguration for the SO requestDetails. Overridden for custom + * query. + * + * @param tenantItem tenant item from A&AI named-query response + * @return SO cloud configuration + */ + protected SoCloudConfiguration constructCloudConfigurationCq(Tenant tenantItem, CloudRegion cloudRegionItem) { + SoCloudConfiguration cloudConfiguration = new SoCloudConfiguration(); + cloudConfiguration.setTenantId(tenantItem.getTenantId()); + cloudConfiguration.setLcpCloudRegionId(cloudRegionItem.getCloudRegionId()); + return cloudConfiguration; + } + + /** + * Create simple HTTP headers for unauthenticated requests to SO. + * + * @return the HTTP headers + */ + protected Map<String, Object> createSimpleHeaders() { + Map<String, Object> headers = new HashMap<>(); + headers.put("Accept", MediaType.APPLICATION_JSON); + return headers; + } + /* * These methods extract data from the Custom Query and throw an * IllegalArgumentException if the desired data item is not found. diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java index 4c35f9abe..077c8578b 100644 --- a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java @@ -36,7 +36,6 @@ import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpConfig; -import org.onap.policy.so.SoCloudConfiguration; import org.onap.policy.so.SoModelInfo; import org.onap.policy.so.SoOperationType; import org.onap.policy.so.SoRelatedInstance; @@ -50,13 +49,11 @@ import org.onap.policy.so.SoRequestParameters; * response and stores it in the context. It also passes the count+1 to the guard. Once * the "create" completes successfully, it bumps the VF count that's stored in the * context. - * <p/> - * Note: currently, this only supports storing the count for a single target VF. */ public class VfModuleCreate extends SoOperation { public static final String NAME = "VF Module Create"; - public static final String PAYLOAD_KEY_VF_COUNT = "vfCount"; + private static final String PATH_PREFIX = "/"; /** * Constructs the object. @@ -72,7 +69,7 @@ public class VfModuleCreate extends SoOperation { } /** - * Ensures that A&AI customer query has been performed, and then runs the guard. + * Ensures that A&AI custom query has been performed, and then runs the guard. */ @Override @SuppressWarnings("unchecked") @@ -115,7 +112,9 @@ public class VfModuleCreate extends SoOperation { logMessage(EventType.OUT, CommInfrastructure.REST, url, request); - return handleResponse(outcome, url, callback -> getClient().post(callback, path, entity, null)); + Map<String, Object> headers = createSimpleHeaders(); + + return handleResponse(outcome, url, callback -> getClient().post(callback, path, entity, headers)); } /** @@ -206,23 +205,9 @@ public class VfModuleCreate extends SoOperation { buildConfigurationParameters().ifPresent(request.getRequestDetails()::setConfigurationParameters); // compute the path - String path = "/serviceInstances/" + vnfServiceItem.getServiceInstanceId() + "/vnfs/" + vnfItem.getVnfId() + String path = PATH_PREFIX + vnfServiceItem.getServiceInstanceId() + "/vnfs/" + vnfItem.getVnfId() + "/vfModules/scaleOut"; return Pair.of(path, request); } - - /** - * Construct cloudConfiguration for the SO requestDetails. Overridden for custom - * query. - * - * @param tenantItem tenant item from A&AI named-query response - * @return SO cloud configuration - */ - private SoCloudConfiguration constructCloudConfigurationCq(Tenant tenantItem, CloudRegion cloudRegionItem) { - SoCloudConfiguration cloudConfiguration = new SoCloudConfiguration(); - cloudConfiguration.setTenantId(tenantItem.getTenantId()); - cloudConfiguration.setLcpCloudRegionId(cloudRegionItem.getCloudRegionId()); - return cloudConfiguration; - } } diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleDelete.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleDelete.java new file mode 100644 index 000000000..5134d58da --- /dev/null +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleDelete.java @@ -0,0 +1,288 @@ +/*- + * ============LICENSE_START======================================================= + * ONAP + * ================================================================================ + * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.policy.controlloop.actor.so; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import javax.ws.rs.client.InvocationCallback; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.onap.aai.domain.yang.CloudRegion; +import org.onap.aai.domain.yang.GenericVnf; +import org.onap.aai.domain.yang.ServiceInstance; +import org.onap.aai.domain.yang.Tenant; +import org.onap.policy.aai.AaiConstants; +import org.onap.policy.aai.AaiCqResponse; +import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure; +import org.onap.policy.common.endpoints.http.client.HttpClient; +import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType; +import org.onap.policy.common.utils.coder.CoderException; +import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome; +import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams; +import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpConfig; +import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture; +import org.onap.policy.so.SoModelInfo; +import org.onap.policy.so.SoOperationType; +import org.onap.policy.so.SoRequest; +import org.onap.policy.so.SoRequestDetails; + +/** + * Operation to delete a VF Module. This gets the VF count from the A&AI Custom Query + * response and stores it in the context. It also passes the count-1 to the guard. Once + * the "delete" completes successfully, it decrements the VF count that's stored in the + * context. + */ +public class VfModuleDelete extends SoOperation { + public static final String NAME = "VF Module Delete"; + + private static final String PATH_PREFIX = "/"; + + /** + * Constructs the object. + * + * @param params operation parameters + * @param config configuration for this operation + */ + public VfModuleDelete(ControlLoopOperationParams params, HttpConfig config) { + super(params, config); + + // ensure we have the necessary parameters + validateTarget(); + } + + /** + * Ensures that A&AI custom query has been performed, and then runs the guard. + */ + @Override + @SuppressWarnings("unchecked") + protected CompletableFuture<OperationOutcome> startPreprocessorAsync() { + + // need the VF count + ControlLoopOperationParams cqParams = params.toBuilder().actor(AaiConstants.ACTOR_NAME) + .operation(AaiCqResponse.OPERATION).payload(null).retry(null).timeoutSec(null).build(); + + // run Custom Query, extract the VF count, and then run the Guard + + // @formatter:off + return sequence(() -> params.getContext().obtain(AaiCqResponse.CONTEXT_KEY, cqParams), + this::obtainVfCount, this::startGuardAsync); + // @formatter:on + } + + @Override + protected Map<String, Object> makeGuardPayload() { + Map<String, Object> payload = super.makeGuardPayload(); + + // run guard with the proposed vf count + payload.put(PAYLOAD_KEY_VF_COUNT, getVfCount() - 1); + + return payload; + } + + @Override + protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) { + + // starting a whole new attempt - reset the count + resetGetCount(); + + Pair<String, SoRequest> pair = makeRequest(); + SoRequest request = pair.getRight(); + String url = getPath() + pair.getLeft(); + + logMessage(EventType.OUT, CommInfrastructure.REST, url, request); + + Map<String, Object> headers = createSimpleHeaders(); + + // @formatter:off + return handleResponse(outcome, url, + callback -> delete(url, headers, MediaType.APPLICATION_JSON, request, callback)); + // @formatter:on + } + + /** + * Issues an HTTP "DELETE" request, containing a request body, using the java built-in + * HttpClient, as the JerseyClient does not support it. This will add the content-type + * and authorization headers, so they should not be included within "headers". + * + * @param <Q> request type + * @param uri URI suffix, to be appended to the URI prefix + * @param headers headers to be included + * @param contentType content type of the request + * @param request request to be posted + * @param callback response callbacks + * @return a future to await the response. Note: it's untested whether canceling this + * future will actually cancel the underlying HTTP request + */ + protected <Q> CompletableFuture<Response> delete(String uri, Map<String, Object> headers, String contentType, + Q request, InvocationCallback<Response> callback) { + // TODO move to HttpOperation + + // make sure we can encode it before going any further + final String body = encodeRequest(request); + + final String url = getClient().getBaseUrl() + uri; + + Builder builder = HttpRequest.newBuilder(URI.create(url)); + builder = builder.header("Content-type", contentType); + builder = addAuthHeader(builder); + + for (Entry<String, Object> header : headers.entrySet()) { + builder = builder.header(header.getKey(), header.getValue().toString()); + } + + PipelineControllerFuture<Response> controller = new PipelineControllerFuture<>(); + + HttpRequest req = builder.method("DELETE", BodyPublishers.ofString(body)).build(); + + CompletableFuture<HttpResponse<String>> future = makeHttpClient().sendAsync(req, BodyHandlers.ofString()); + + // propagate "cancel" to the future + controller.add(future); + + future.thenApply(response -> new RestManagerResponse(response.statusCode(), response.body(), makeCoder())) + .whenComplete((resp, thrown) -> { + if (thrown != null) { + callback.failed(thrown); + controller.completeExceptionally(thrown); + } else { + callback.completed(resp); + controller.complete(resp); + } + }); + + return controller; + } + + /** + * Encodes a request. + * + * @param <Q> request type + * @param request request to be encoded + * @return the encoded request + */ + protected <Q> String encodeRequest(Q request) { + // TODO move to HttpOperation + try { + if (request instanceof String) { + return request.toString(); + } else { + return makeCoder().encode(request); + } + } catch (CoderException e) { + throw new IllegalArgumentException("cannot encode request", e); + } + } + + /** + * Adds the authorization header to the HTTP request, if configured. + * + * @param builder request builder to which the header should be added + * @return the builder + */ + protected Builder addAuthHeader(Builder builder) { + // TODO move to HttpOperation + final HttpClient client = getClient(); + String username = client.getUserName(); + if (StringUtils.isBlank(username)) { + return builder; + } + + String password = client.getPassword(); + if (password == null) { + password = ""; + } + + String encoded = username + ":" + password; + encoded = Base64.getEncoder().encodeToString(encoded.getBytes(StandardCharsets.UTF_8)); + return builder.header("Authorization", "Basic " + encoded); + } + + /** + * Decrements the VF count that's stored in the context. + */ + @Override + protected void successfulCompletion() { + setVfCount(getVfCount() - 1); + } + + /** + * Makes a request. + * + * @return a pair containing the request URL and the new request + */ + protected Pair<String, SoRequest> makeRequest() { + final AaiCqResponse aaiCqResponse = params.getContext().getProperty(AaiCqResponse.CONTEXT_KEY); + final SoModelInfo soModelInfo = prepareSoModelInfo(); + final GenericVnf vnfItem = getVnfItem(aaiCqResponse, soModelInfo); + final ServiceInstance vnfServiceItem = getServiceInstance(aaiCqResponse); + final Tenant tenantItem = getDefaultTenant(aaiCqResponse); + final CloudRegion cloudRegionItem = getDefaultCloudRegion(aaiCqResponse); + + SoRequest request = new SoRequest(); + request.setOperationType(SoOperationType.DELETE_VF_MODULE); + + // + // + // Do NOT send SO the requestId, they do not support this field + // + SoRequestDetails details = new SoRequestDetails(); + request.setRequestDetails(details); + details.setRelatedInstanceList(null); + details.setConfigurationParameters(null); + + // cloudConfiguration + details.setCloudConfiguration(constructCloudConfigurationCq(tenantItem, cloudRegionItem)); + + // modelInfo + details.setModelInfo(soModelInfo); + + // requestInfo + details.setRequestInfo(constructRequestInfo()); + + /* + * TODO the legacy SO code always passes null for the last argument, though it + * should be passing the vfModuleInstanceId + */ + + // compute the path + String path = PATH_PREFIX + vnfServiceItem.getServiceInstanceId() + "/vnfs/" + vnfItem.getVnfId() + + "/vfModules/null"; + + return Pair.of(path, request); + } + + // these may be overridden by junit tests + + protected java.net.http.HttpClient makeHttpClient() { + return java.net.http.HttpClient.newHttpClient(); + } +} |