From 5868c88405a5057923095e84b225adc23feef84b Mon Sep 17 00:00:00 2001 From: "Claudio D. Gasparini" Date: Mon, 3 May 2021 17:42:22 +0200 Subject: [PATCH] Soutbound PM Rest Client Issue-ID: OAM-207 Signed-off-by: Claudio D. Gasparini Change-Id: I77363efda6f4d228409d64d330b2b640c67efb69 --- ves-nf-oam-adopter/pom.xml | 1 + .../ves-nf-oam-adopter-artifacts/pom.xml | 5 + .../ves-nf-oam-adopter-parent/pom.xml | 4 +- .../pm/rest/manager/VesEventNotifierMock.java | 2 +- .../ves-nf-oam-adopter-pm-sb-rest-client/pom.xml | 79 +++++++++ .../pm/sb/rest/client/DefaultHttpRestClient.java | 157 ++++++++++++++++++ .../DownloadPerformanceManagementFilesHandler.java | 67 ++++++++ .../sb/rest/client/http/OffSetTimeZoneHandler.java | 67 ++++++++ .../pm/sb/rest/client/http/TokenHandler.java | 53 ++++++ .../pm/sb/rest/client/pojos/TimeZoneResponse.java | 34 ++++ .../pm/sb/rest/client/pojos/TokenResponse.java | 29 ++++ .../properties/PmEndpointsUrlsProperties.java | 23 +++ .../sb/rest/client/DefaultHttpRestClientTest.java | 180 +++++++++++++++++++++ .../oam/adopter/pm/sb/rest/client/JsonUtils.java | 13 ++ .../src/test/resources/application.yaml | 4 + .../resources/json/timeZoneOffsetResponse.json | 3 + .../src/test/resources/json/tokenResponse.json | 3 + .../org.mockito.plugins.MockMaker | 1 + 18 files changed, 723 insertions(+), 2 deletions(-) create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/pom.xml create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClient.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/DownloadPerformanceManagementFilesHandler.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/OffSetTimeZoneHandler.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/TokenHandler.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TimeZoneResponse.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TokenResponse.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/properties/PmEndpointsUrlsProperties.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClientTest.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/JsonUtils.java create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/application.yaml create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/timeZoneOffsetResponse.json create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/tokenResponse.json create mode 100644 ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/ves-nf-oam-adopter/pom.xml b/ves-nf-oam-adopter/pom.xml index c116ba1..7b455cb 100644 --- a/ves-nf-oam-adopter/pom.xml +++ b/ves-nf-oam-adopter/pom.xml @@ -51,5 +51,6 @@ ves-nf-oam-adopter-event-notifier ves-nf-oam-adopter-snmp-manager ves-nf-oam-adopter-pm-manager + ves-nf-oam-adopter-pm-sb-rest-client \ No newline at end of file diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-artifacts/pom.xml b/ves-nf-oam-adopter/ves-nf-oam-adopter-artifacts/pom.xml index eac13ad..7269b55 100644 --- a/ves-nf-oam-adopter/ves-nf-oam-adopter-artifacts/pom.xml +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-artifacts/pom.xml @@ -71,6 +71,11 @@ ves-nf-oam-adopter-pm-manager ${project.version} + + ${project.groupId} + ves-nf-oam-adopter-pm-sb-rest-client + ${project.version} + \ No newline at end of file diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-parent/pom.xml b/ves-nf-oam-adopter/ves-nf-oam-adopter-parent/pom.xml index aa697e4..393a11b 100644 --- a/ves-nf-oam-adopter/ves-nf-oam-adopter-parent/pom.xml +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-parent/pom.xml @@ -46,7 +46,9 @@ ../ves-nf-oam-adopter-snmp-manager/target/site/jacoco-ut/jacoco.xml, ../ves-nf-oam-adopter-snmp-manager/target/site/jacoco-aggregate/jacoco.xml, ../ves-nf-oam-adopter-pm-manager/target/site/jacoco-ut/jacoco.xml, - ../ves-nf-oam-adopter-pm-manager/target/site/jacoco-aggregate/jacoco.xml + ../ves-nf-oam-adopter-pm-manager/target/site/jacoco-aggregate/jacoco.xml, + ../ves-nf-oam-adopter-pm-sb-rest-client/target/site/jacoco-ut/jacoco.xml, + ../ves-nf-oam-adopter-pm-sb-rest-client/target/site/jacoco-aggregate/jacoco.xml 3.8.0.2131 diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-manager/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/rest/manager/VesEventNotifierMock.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-manager/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/rest/manager/VesEventNotifierMock.java index e6249ff..ff79438 100644 --- a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-manager/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/rest/manager/VesEventNotifierMock.java +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-manager/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/rest/manager/VesEventNotifierMock.java @@ -35,7 +35,7 @@ final class VesEventNotifierMock implements VesEventNotifier { private final List event = new ArrayList<>(); @Override - public Completable notifyEvents(final CommonEventFormat302ONAP event) { + public synchronized Completable notifyEvents(final CommonEventFormat302ONAP event) { this.event.add(event); return Completable.complete(); } diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/pom.xml b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/pom.xml new file mode 100644 index 0000000..3dcd893 --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/pom.xml @@ -0,0 +1,79 @@ + + + + 4.0.0 + + + org.o-ran-sc.oam + ves-nf-oam-adopter-parent + 1.0.0-SNAPSHOT + ../ves-nf-oam-adopter-parent/pom.xml + + + ves-nf-oam-adopter-pm-sb-rest-client + + + + ${project.groupId} + ves-nf-oam-adopter-pm-manager + + + org.apache.httpcomponents.client5 + httpclient5 + + + com.google.code.gson + gson + + + com.google.guava + guava + + + io.reactivex.rxjava3 + rxjava + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + test + + + \ No newline at end of file diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClient.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClient.java new file mode 100644 index 0000000..b1d6008 --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClient.java @@ -0,0 +1,157 @@ +/* + * ============LICENSE_START======================================================= + * O-RAN-SC + * ================================================================================ + * Copyright © 2021 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client; + +import static org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http.DownloadPerformanceManagementFilesHandler.readPerformanceManagementFiles; +import static org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http.OffSetTimeZoneHandler.readTimeZone; +import static org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http.TokenHandler.returnToken; +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import io.reactivex.rxjava3.core.Single; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipInputStream; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequests; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.http.ConnectionClosedException; +import org.apache.hc.core5.http.HttpHeaders; +import org.checkerframework.checker.lock.qual.GuardedBy; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.api.HttpRestClient; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.PerformanceManagementException; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.TokenGenerationException; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.ZoneIdException; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter; +import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.properties.PmEndpointsUrlsProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public final class DefaultHttpRestClient implements HttpRestClient { + private static final Logger LOG = LoggerFactory.getLogger(DefaultHttpRestClient.class); + + public static final String HTTPS = "https://"; + public static final String BEARER = "Bearer "; + private static final DateTimeFormatter OFFSET_FORMATTER = DateTimeFormatter.ofPattern("xxx"); + private final CloseableHttpAsyncClient client; + @GuardedBy("this") + private final LoadingCache sessionCache = + CacheBuilder.newBuilder().refreshAfterWrite(59, TimeUnit.MINUTES).build(new CacheLoader<>() { + @Override + public String load(final Adapter adapter) throws ExecutionException, InterruptedException { + try { + return returnToken(DefaultHttpRestClient.this.client, DefaultHttpRestClient.this.tokenEndpoint, + adapter); + } catch (final Exception error) { + LOG.error("Failed to read time zone", error); + throw error; + } + } + }); + + @GuardedBy("this") + private final LoadingCache zoneIdCache = + CacheBuilder.newBuilder().build(new CacheLoader<>() { + @Override + public ZoneId load(final Adapter adapter) { + return readTimeZone(DefaultHttpRestClient.this, timeZoneEndpoint, adapter) + .doOnError(error -> LOG.error("Failed to read time zone", error)) + .blockingGet(); + } + }); + private final String pmFilesEndpoint; + private final String timeZoneEndpoint; + private final String tokenEndpoint; + + /** + * Default constructor. + */ + @Autowired + public DefaultHttpRestClient(final CloseableHttpAsyncClient httpAsyncClient, + final PmEndpointsUrlsProperties properties) { + this.client = httpAsyncClient; + this.pmFilesEndpoint = properties.getRanPmEndpoint(); + this.timeZoneEndpoint = properties.getRanTimeZoneOffsetEndpoint(); + this.tokenEndpoint = properties.getRanTokenEndpoint(); + } + + + @Override + public synchronized Single readFiles(final Adapter adapter) { + return readPerformanceManagementFiles(this, pmFilesEndpoint, adapter); + } + + @Override + public Single getTimeZone(final Adapter adapter) { + try { + final ZoneId zoneId = zoneIdCache.get(adapter); + LOG.info("Adapter {} has offset {}", adapter.getHostIpAddress(), + OFFSET_FORMATTER.format(zoneId.getRules().getOffset(Instant.now()))); + return Single.just(zoneId); + } catch (final Exception e) { + final Throwable cause = e.getCause(); + if (cause instanceof PerformanceManagementException) { + return Single.error(cause); + } + return Single.error(new ZoneIdException("Failed to get Zone ID for " + adapter.getHostIpAddress(), cause)); + } + } + + /** + * Execute GET request on adapter endpoint. + * @param adapter destiny + * @param url endpoint + * @return response + */ + public Single get(final Adapter adapter, final String url) { + return getToken(adapter).flatMap(token -> { + final SimpleHttpRequest request = + SimpleHttpRequests.get(HTTPS + adapter.getHostIpAddress() + url); + request.addHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE); + request.addHeader(HttpHeaders.AUTHORIZATION, BEARER + token); + return Single.fromFuture(client.execute(request, null)) + .doOnSubscribe(result -> LOG.trace("GET Request started {} ...", request.toString())) + .doOnSuccess(result -> LOG.trace("GET Request finished {}", request)); + }); + } + + private Single getToken(final Adapter adapter) { + try { + final String token = sessionCache.get(adapter); + return Single.just(token); + } catch (final Exception e) { + if (e.getCause() instanceof TokenGenerationException) { + return Single.error(e.getCause()); + } else if (e.getCause() instanceof ConnectionClosedException) { + return Single.error(e.getCause()); + } + return Single.error(e); + } + } +} diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/DownloadPerformanceManagementFilesHandler.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/DownloadPerformanceManagementFilesHandler.java new file mode 100644 index 0000000..77957cb --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/DownloadPerformanceManagementFilesHandler.java @@ -0,0 +1,67 @@ +/* + * ============LICENSE_START======================================================= + * O-RAN-SC + * ================================================================================ + * Copyright © 2021 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http; + +import io.reactivex.rxjava3.core.Single; +import java.io.ByteArrayInputStream; +import java.util.zip.ZipInputStream; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.hc.client5.http.async.methods.SimpleBody; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.message.StatusLine; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.PerformanceManagementException; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter; +import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.DefaultHttpRestClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class DownloadPerformanceManagementFilesHandler { + + private static final Logger LOG = LoggerFactory.getLogger(DownloadPerformanceManagementFilesHandler.class); + + /** + * Reads and return ZIP CSV PM Files from device. + */ + public static Single readPerformanceManagementFiles(final DefaultHttpRestClient httpSession, + final String pmFilesEndpoint, final Adapter adapter) { + LOG.info("Download PM files from RAN {}", adapter.getHostIpAddress()); + return httpSession.get(adapter, pmFilesEndpoint) + .flatMap(response -> DownloadPerformanceManagementFilesHandler + .validateGetZipFile(adapter, response)) + .map(entity -> new ZipInputStream(new ByteArrayInputStream(entity.getBodyBytes()))); + } + + private static Single validateGetZipFile(final Adapter adapter, final SimpleHttpResponse response) { + final String statusLine = new StatusLine(response).toString(); + final ContentType contentType = response.getContentType(); + final SimpleBody entity = response.getBody(); + if (response.getCode() == HttpStatus.SC_OK && entity != null) { + if (ContentType.APPLICATION_OCTET_STREAM.getMimeType().equals(contentType.getMimeType())) { + return Single.just(entity); + } + } + return Single.error(new PerformanceManagementException( + "Download files from " + adapter.getHostIpAddress() + " failed: " + statusLine)); + } +} diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/OffSetTimeZoneHandler.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/OffSetTimeZoneHandler.java new file mode 100644 index 0000000..2bf8aeb --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/OffSetTimeZoneHandler.java @@ -0,0 +1,67 @@ +/* + * ============LICENSE_START======================================================= + * O-RAN-SC + * ================================================================================ + * Copyright © 2021 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http; + +import com.google.gson.Gson; +import io.reactivex.rxjava3.core.Single; +import java.time.ZoneId; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.PerformanceManagementEmptyOutputException; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.PerformanceManagementException; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter; +import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.DefaultHttpRestClient; +import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.pojos.TimeZoneResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class OffSetTimeZoneHandler { + + private static final Logger LOG = LoggerFactory.getLogger(OffSetTimeZoneHandler.class); + + private static final Gson GSON = new Gson(); + + /** + * Returns time zone of the device. + */ + public static Single readTimeZone(final DefaultHttpRestClient httpRestClient, final String offsetEndpoint, + final Adapter adapter) { + LOG.debug("Read Time Zone from adapter {}", adapter.getHostIpAddress()); + return httpRestClient.get(adapter, offsetEndpoint) + .map(response -> validateGet(response, adapter)) + .map(ZoneId::of); + } + + private static String validateGet(final SimpleHttpResponse response, final Adapter adapter) { + if (response.getCode() != HttpStatus.SC_OK) { + throw new PerformanceManagementException( + "Get Zone offset failed for " + adapter.getHostIpAddress() + " Code: " + response.getCode()); + } + final String output = response.getBody().getBodyText(); + if (output.isEmpty()) { + throw new PerformanceManagementEmptyOutputException( + "Get Zone offset failed for " + adapter.getHostIpAddress() + " . Empty output received"); + } + return GSON.fromJson(output, TimeZoneResponse.class).getOffset(); + } +} diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/TokenHandler.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/TokenHandler.java new file mode 100644 index 0000000..26c62b5 --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/http/TokenHandler.java @@ -0,0 +1,53 @@ +package org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.http; + +import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; + +import com.google.gson.Gson; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.ExecutionException; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequests; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.message.StatusLine; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.exceptions.TokenGenerationException; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter; +import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.pojos.TokenResponse; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TokenHandler { + + public static final Gson GSON = new Gson(); + public static final String HTTPS = "https://"; + + /** + * Request Token under defined endpoint. + */ + public static synchronized String returnToken(final CloseableHttpAsyncClient client, final String tokenEndpoint, + final Adapter adapter) throws ExecutionException, InterruptedException { + final String host = HTTPS + adapter.getHostIpAddress(); + final SimpleHttpRequest request = SimpleHttpRequests.post(host + tokenEndpoint); + final String basicAuth = Base64.getEncoder().encodeToString( + (adapter.getUsername() + ":" + adapter.getPassword()).getBytes(StandardCharsets.UTF_8)); + request.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + basicAuth); + request.addHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE); + request.addHeader(HttpHeaders.ACCEPT, APPLICATION_JSON_VALUE); + + final SimpleHttpResponse response = client.execute(request, null).get(); + final String statusLine = new StatusLine(response).toString(); + if (response.getCode() != HttpStatus.SC_OK) { + throw new TokenGenerationException("Failed to obtain a token for host " + host + ": " + statusLine); + } + final String output = response.getBody().getBodyText(); + if (output.isEmpty()) { + throw new TokenGenerationException( + "Failed to obtain a token for host " + host + ", empty output: " + statusLine); + } + return GSON.fromJson(output, TokenResponse.class).getToken(); + } +} diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TimeZoneResponse.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TimeZoneResponse.java new file mode 100644 index 0000000..853951d --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TimeZoneResponse.java @@ -0,0 +1,34 @@ +/* + * ============LICENSE_START======================================================= + * O-RAN-SC + * ================================================================================ + * Copyright © 2021 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.pojos; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@RequiredArgsConstructor +@Getter +@Setter +public class TimeZoneResponse { + @SerializedName(value = "offset") + private @NonNull final String offset; +} diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TokenResponse.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TokenResponse.java new file mode 100644 index 0000000..195922f --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/pojos/TokenResponse.java @@ -0,0 +1,29 @@ +/* + * ============LICENSE_START======================================================= + * O-RAN-SC + * ================================================================================ + * Copyright © 2021 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.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.pojos; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class TokenResponse { + private String token; +} diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/properties/PmEndpointsUrlsProperties.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/properties/PmEndpointsUrlsProperties.java new file mode 100644 index 0000000..65a6826 --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/main/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/properties/PmEndpointsUrlsProperties.java @@ -0,0 +1,23 @@ +package org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.properties; + +import javax.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Component +@ConfigurationProperties(prefix = "pm-rest-manager") +@Data +@NoArgsConstructor +@Validated +public class PmEndpointsUrlsProperties { + + @NotEmpty + private String ranTokenEndpoint; + @NotEmpty + private String ranPmEndpoint; + @NotEmpty + private String ranTimeZoneOffsetEndpoint; +} diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClientTest.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClientTest.java new file mode 100644 index 0000000..f9c115a --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/DefaultHttpRestClientTest.java @@ -0,0 +1,180 @@ +package org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +import io.reactivex.rxjava3.observers.TestObserver; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.time.ZoneId; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.zip.ZipInputStream; +import org.apache.hc.client5.http.async.methods.SimpleHttpRequest; +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; +import org.apache.hc.core5.concurrent.FutureCallback; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.mockito.stubbing.Answer; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.api.HttpRestClient; +import org.o.ran.oam.nf.oam.adopter.pm.rest.manager.pojos.Adapter; +import org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client.properties.PmEndpointsUrlsProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.MethodMode; + +@SpringBootTest(classes = {DefaultHttpRestClient.class, PmEndpointsUrlsProperties.class}) +@EnableConfigurationProperties +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class DefaultHttpRestClientTest { + + private static final Adapter ADAPTER = + Adapter.builder().hostIpAddress("150.62.25.26").username("admin").password("secretPassword").build(); + @Autowired + public HttpRestClient restClient; + + @MockBean + CloseableHttpAsyncClient client; + + @Test + @DirtiesContext(methodMode = MethodMode.BEFORE_METHOD) + public void testGetFailedToken() { + final SimpleHttpResponse response = + SimpleHttpResponse.create(HttpStatus.SC_UNAUTHORIZED, "error", ContentType.APPLICATION_JSON); + + when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class))) + .thenAnswer((Answer>) invocation -> { + final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0]; + if ("/auth/token".equals(req.getPath())) { + return CompletableFuture.completedFuture(response); + } + throw new IllegalStateException("Unexpected value: " + req.getPath()); + }); + + + final TestObserver observer = restClient.getTimeZone(ADAPTER).test(); + observer.assertError(throwable -> throwable.getMessage() + .equals("Failed to obtain a token for host https://150.62.25.26: HTTP/1.1 401 Unauthorized")); + } + + @Test + public void testReadFiles() throws IOException { + final String tokenJson = JsonUtils.readJson("/json/tokenResponse.json"); + final SimpleHttpResponse response = + SimpleHttpResponse.create(HttpStatus.SC_OK, tokenJson, ContentType.APPLICATION_JSON); + + final SimpleHttpResponse zipResponse = + SimpleHttpResponse.create(HttpStatus.SC_OK, "mockZip", ContentType.APPLICATION_OCTET_STREAM); + + when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class))) + .thenAnswer((Answer>) invocation -> { + final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0]; + switch (req.getPath()) { + case "/auth/token": + return CompletableFuture.completedFuture(response); + case "/pm/files": + return CompletableFuture.completedFuture(zipResponse); + default: + throw new IllegalStateException("Unexpected value: " + req.getPath()); + } + }); + + + final TestObserver observer = restClient.readFiles(ADAPTER).test(); + final ZipInputStream expected = new ZipInputStream(new ByteArrayInputStream("mockZip".getBytes())); + observer.assertValue(zip -> Arrays.equals(zip.readAllBytes(), expected.readAllBytes())); + } + + @Test + public void testReadFilesResponseFail() throws IOException { + final String tokenJson = JsonUtils.readJson("/json/tokenResponse.json"); + final SimpleHttpResponse response = + SimpleHttpResponse.create(HttpStatus.SC_OK, tokenJson, ContentType.APPLICATION_JSON); + + final SimpleHttpResponse zipResponse = SimpleHttpResponse.create(HttpStatus.SC_BAD_REQUEST, "mockZip", + ContentType.APPLICATION_OCTET_STREAM); + + when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class))) + .thenAnswer((Answer>) invocation -> { + final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0]; + switch (req.getPath()) { + case "/auth/token": + return CompletableFuture.completedFuture(response); + case "/pm/files": + return CompletableFuture.completedFuture(zipResponse); + default: + throw new IllegalStateException("Unexpected value: " + req.getPath()); + } + }); + + + final TestObserver observer = restClient.readFiles(ADAPTER).test(); + observer.assertError(throwable -> throwable.getMessage() + .equals("Download files from 150.62.25.26 failed: HTTP/1.1 400 Bad Request")); + } + + @Test + public void testGetTimeOffset() throws IOException { + final String tokenJson = JsonUtils.readJson("/json/tokenResponse.json"); + final SimpleHttpResponse response = + SimpleHttpResponse.create(HttpStatus.SC_OK, tokenJson, ContentType.APPLICATION_JSON); + + final String timeZoneOFfsetResponseJson = JsonUtils.readJson("/json/timeZoneOffsetResponse.json"); + final SimpleHttpResponse timeOffsetResponse = + SimpleHttpResponse.create(HttpStatus.SC_OK, timeZoneOFfsetResponseJson, ContentType.APPLICATION_JSON); + + when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class))) + .thenAnswer((Answer>) invocation -> { + final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0]; + switch (req.getPath()) { + case "/auth/token": + return CompletableFuture.completedFuture(response); + case "/system/timeZone": + return CompletableFuture.completedFuture(timeOffsetResponse); + default: + throw new IllegalStateException("Unexpected value: " + req.getPath()); + } + }); + + + final TestObserver observer = restClient.getTimeZone(ADAPTER).test(); + observer.assertValues(ZoneId.of("+02:00")); + } + + @Test + public void testGetTimeOffsetFail() throws IOException { + final String tokenJson = JsonUtils.readJson("/json/tokenResponse.json"); + final SimpleHttpResponse response = + SimpleHttpResponse.create(HttpStatus.SC_OK, tokenJson, ContentType.APPLICATION_JSON); + + final SimpleHttpResponse timeOffsetResponse = + SimpleHttpResponse.create(HttpStatus.SC_OK, "", ContentType.APPLICATION_JSON); + + when(client.execute(any(SimpleHttpRequest.class), nullable(FutureCallback.class))) + .thenAnswer((Answer>) invocation -> { + final SimpleHttpRequest req = (SimpleHttpRequest) invocation.getArguments()[0]; + switch (req.getPath()) { + case "/auth/token": + return CompletableFuture.completedFuture(response); + case "/system/timeZone": + return CompletableFuture.completedFuture(timeOffsetResponse); + default: + throw new IllegalStateException("Unexpected value: " + req.getPath()); + } + }); + + + final TestObserver observer = restClient.getTimeZone(ADAPTER).test(); + observer.assertError(throwable -> throwable.getMessage() + .equals("Get Zone offset failed for 150.62.25.26 . Empty output received")); + } +} \ No newline at end of file diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/JsonUtils.java b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/JsonUtils.java new file mode 100644 index 0000000..60bf0ef --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/java/org/o/ran/oam/nf/oam/adopter/pm/sb/rest/client/JsonUtils.java @@ -0,0 +1,13 @@ +package org.o.ran.oam.nf.oam.adopter.pm.sb.rest.client; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import lombok.experimental.UtilityClass; +import org.apache.commons.io.IOUtils; + +@UtilityClass +final class JsonUtils { + static String readJson(final String url) throws IOException { + return IOUtils.toString(JsonUtils.class.getResourceAsStream(url), StandardCharsets.UTF_8); + } +} diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/application.yaml b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/application.yaml new file mode 100644 index 0000000..f81c8ff --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/application.yaml @@ -0,0 +1,4 @@ +pm-rest-manager: + ran-token-endpoint: /auth/token + ran-pm-endpoint: /pm/files + ran-time-zone-offset-endpoint: /system/timeZone \ No newline at end of file diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/timeZoneOffsetResponse.json b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/timeZoneOffsetResponse.json new file mode 100644 index 0000000..c80769c --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/timeZoneOffsetResponse.json @@ -0,0 +1,3 @@ +{ + "offset": "+02:00" +} \ No newline at end of file diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/tokenResponse.json b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/tokenResponse.json new file mode 100644 index 0000000..2269aa0 --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/json/tokenResponse.json @@ -0,0 +1,3 @@ +{ + "token": "someRandomToken" +} \ No newline at end of file diff --git a/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/ves-nf-oam-adopter/ves-nf-oam-adopter-pm-sb-rest-client/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file -- 2.16.6