From 4140b0716234e7540d57b8a746944018fa591d69 Mon Sep 17 00:00:00 2001 From: prathamesh morde Date: Mon, 1 Apr 2019 23:19:42 -0400 Subject: [PATCH] CDS-SDC Listener application Things done -Implementation of GRPC client to store CBA archive using CDS Backend -Unit test coverage. -Tested locally. -Minor bug fix, performance improvement, more logging and make use of BluePrintProcessorHandler api in ListenerServiceImpl class. -Extract csar artifact from IDistributionClientResult and store it into local file(disk). Change-Id: I08b0de017396bb76d5bc13ddb9e7b430990e8df0 Issue-ID: CCSDK-1184 Signed-off-by: prathamesh morde --- ms/cds-sdc-listener/application/pom.xml | 45 +++++++ .../CdsSdcListenerNotificationCallback.java | 41 ++++++- .../CdsSdcListenerAuthClientInterceptor.java | 46 +++++++ .../{ => client}/CdsSdcListenerClient.java | 7 +- .../{ => dto}/CdsSdcListenerDto.java | 4 +- .../handler/BluePrintProcesssorHandler.java | 57 +++++++++ .../cdssdclistener/service/ListenerService.java | 15 ++- .../service/ListenerServiceImpl.java | 136 +++++++++++++++++++-- .../application/src/main/resources/application.yml | 6 +- .../cdssdclistener/CdsSdcListenerClientTest.java | 2 + .../handler/BluePrintProcessorHandlerTest.java | 106 ++++++++++++++++ .../service/ListenerServiceImplTest.java | 47 ++++++- .../application/src/test/resources/testcba.zip | Bin 0 -> 15123 bytes 13 files changed, 487 insertions(+), 25 deletions(-) create mode 100644 ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/client/CdsSdcListenerAuthClientInterceptor.java rename ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/{ => client}/CdsSdcListenerClient.java (89%) rename ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/{ => dto}/CdsSdcListenerDto.java (86%) create mode 100644 ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/handler/BluePrintProcesssorHandler.java create mode 100644 ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/handler/BluePrintProcessorHandlerTest.java create mode 100644 ms/cds-sdc-listener/application/src/test/resources/testcba.zip diff --git a/ms/cds-sdc-listener/application/pom.xml b/ms/cds-sdc-listener/application/pom.xml index 899d173ef..c2ec8b98c 100644 --- a/ms/cds-sdc-listener/application/pom.xml +++ b/ms/cds-sdc-listener/application/pom.xml @@ -20,6 +20,11 @@ cds-sdc-listener-application CDS-SDC Listener Application + + 1.17.1 + 3.6.1 + + @@ -51,6 +56,46 @@ test + + + io.grpc + grpc-netty + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + io.grpc + grpc-testing + ${grpc.version} + test + + + org.onap.ccsdk.cds.components + proto-definition + 0.4.2-SNAPSHOT + + + + + org.onap.sdc.sdc-distribution-client + sdc-distribution-client + 1.3.0 + + diff --git a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerNotificationCallback.java b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerNotificationCallback.java index aaab8d81f..e2aae9654 100644 --- a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerNotificationCallback.java +++ b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerNotificationCallback.java @@ -9,7 +9,13 @@ package org.onap.ccsdk.cds.cdssdclistener; import static org.onap.sdc.utils.DistributionActionResultEnum.SUCCESS; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; import java.util.Objects; +import java.util.Optional; +import org.onap.ccsdk.cds.cdssdclistener.dto.CdsSdcListenerDto; import org.onap.ccsdk.cds.cdssdclistener.service.ListenerServiceImpl; import org.onap.sdc.api.IDistributionClient; import org.onap.sdc.api.consumer.INotificationCallback; @@ -19,9 +25,14 @@ import org.onap.sdc.api.results.IDistributionClientDownloadResult; 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.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.ComponentScan; import org.springframework.stereotype.Component; +@ConfigurationProperties("listenerservice") @Component +@ComponentScan("org.onap.ccsdk.cds.cdssdclistener.dto") public class CdsSdcListenerNotificationCallback implements INotificationCallback { @Autowired @@ -30,6 +41,9 @@ public class CdsSdcListenerNotificationCallback implements INotificationCallback @Autowired private ListenerServiceImpl listenerService; + @Value("${listenerservice.config.archivePath}") + private String pathToStoreArchives; + private static final Logger LOGGER = LoggerFactory.getLogger(CdsSdcListenerNotificationCallback.class); @Override @@ -45,7 +59,7 @@ public class CdsSdcListenerNotificationCallback implements INotificationCallback } /** - * Download the TOSCA CSAR artifact. + * Download the TOSCA CSAR artifact and process it. * * @param info - Artifact information * @param distributionClient - SDC distribution client @@ -53,6 +67,7 @@ public class CdsSdcListenerNotificationCallback implements INotificationCallback private void downloadCsarArtifacts(IArtifactInfo info, IDistributionClient distributionClient) { final String url = info.getArtifactURL(); final String id = info.getArtifactUUID(); + if (Objects.equals(info.getArtifactType(), CdsSdcListenerConfiguration.TOSCA_CSAR)) { LOGGER.info("Trying to download the artifact from : {} and UUID is {} ", url, id); @@ -63,8 +78,30 @@ public class CdsSdcListenerNotificationCallback implements INotificationCallback LOGGER.error("Failed to download the artifact from : {} due to {} ", url, result.getDistributionActionResult()); } else { - // TODO Store the CSAR into CSARArchive path and extract the Blueprint using ListenerServiceImpl.extractBluePrint + LOGGER.info("Trying to write CSAR artifact to file with URL {} and UUID {}", url, id); + processCsarArtifact(result); } } } + + public void processCsarArtifact(IDistributionClientDownloadResult result) { + Path cbaArchivePath = Paths.get(pathToStoreArchives, "cba-archive"); + Path csarArchivePath = Paths.get(pathToStoreArchives, "csar-archive"); + + // extract and store the CSAR archive into local disk. + listenerService.extractCsarAndStore(result, csarArchivePath.toString()); + + Optional> csarFiles = listenerService.getFilesFromDisk(csarArchivePath); + + if (csarFiles.isPresent()) { + + //Extract CBA archive from CSAR package and and store it into CDS Database. + csarFiles.get() + .forEach(file -> listenerService.extractBluePrint(file.getAbsolutePath(), cbaArchivePath.toString())); + + listenerService.saveBluePrintToCdsDatabase(cbaArchivePath); + } else { + LOGGER.error("The CSAR file is not present at this location {}", csarArchivePath); + } + } } diff --git a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/client/CdsSdcListenerAuthClientInterceptor.java b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/client/CdsSdcListenerAuthClientInterceptor.java new file mode 100644 index 000000000..528fbe2f3 --- /dev/null +++ b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/client/CdsSdcListenerAuthClientInterceptor.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 Bell Canada. All rights reserved. + * + * NOTICE: All the intellectual and technical concepts contained herein are + * proprietary to Bell Canada and are protected by trade secret or copyright law. + * Unauthorized copying of this file, via any medium is strictly prohibited. + */ + +package org.onap.ccsdk.cds.cdssdclistener.client; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.MethodDescriptor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * To provide authentication with GRPC server. + */ +@ConfigurationProperties("listenerservice") +@Component +public class CdsSdcListenerAuthClientInterceptor implements ClientInterceptor { + + @Value("${listenerservice.config.authHeader}") + private String basicAuth; + + @Override + public ClientCall interceptCall(MethodDescriptor methodDescriptor, + CallOptions callOptions, Channel channel) { + Key authHeader = Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + return new ForwardingClientCall.SimpleForwardingClientCall( + channel.newCall(methodDescriptor, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + headers.put(authHeader, basicAuth); + super.start(responseListener, headers); + } + }; + } +} diff --git a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerClient.java b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/client/CdsSdcListenerClient.java similarity index 89% rename from ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerClient.java rename to ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/client/CdsSdcListenerClient.java index 76295bacb..6f888dd0b 100644 --- a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerClient.java +++ b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/client/CdsSdcListenerClient.java @@ -5,9 +5,12 @@ * proprietary to Bell Canada and are protected by trade secret or copyright law. * Unauthorized copying of this file, via any medium is strictly prohibited. */ -package org.onap.ccsdk.cds.cdssdclistener; +package org.onap.ccsdk.cds.cdssdclistener.client; import java.util.Optional; +import org.onap.ccsdk.cds.cdssdclistener.CdsSdcListenerConfiguration; +import org.onap.ccsdk.cds.cdssdclistener.dto.CdsSdcListenerDto; +import org.onap.ccsdk.cds.cdssdclistener.CdsSdcListenerNotificationCallback; import org.onap.ccsdk.cds.cdssdclistener.exceptions.CdsSdcListenerException; import org.onap.sdc.api.IDistributionClient; import org.onap.sdc.api.results.IDistributionClientResult; @@ -17,10 +20,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @Component +@ComponentScan("org.onap.ccsdk.cds.cdssdclistener.dto") public class CdsSdcListenerClient { private static Logger LOG = LoggerFactory.getLogger(CdsSdcListenerClient.class); diff --git a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerDto.java b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/dto/CdsSdcListenerDto.java similarity index 86% rename from ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerDto.java rename to ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/dto/CdsSdcListenerDto.java index 7d154da42..41039eb28 100644 --- a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerDto.java +++ b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/dto/CdsSdcListenerDto.java @@ -5,10 +5,12 @@ * proprietary to Bell Canada and are protected by trade secret or copyright law. * Unauthorized copying of this file, via any medium is strictly prohibited. */ -package org.onap.ccsdk.cds.cdssdclistener; +package org.onap.ccsdk.cds.cdssdclistener.dto; import org.onap.sdc.api.IDistributionClient; +import org.springframework.stereotype.Component; +@Component public class CdsSdcListenerDto { private IDistributionClient distributionClient; diff --git a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/handler/BluePrintProcesssorHandler.java b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/handler/BluePrintProcesssorHandler.java new file mode 100644 index 000000000..6b03b6da2 --- /dev/null +++ b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/handler/BluePrintProcesssorHandler.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2019 Bell Canada. All rights reserved. + * + * NOTICE: All the intellectual and technical concepts contained herein are + * proprietary to Bell Canada and are protected by trade secret or copyright law. + * Unauthorized copying of this file, via any medium is strictly prohibited. + */ + +package org.onap.ccsdk.cds.cdssdclistener.handler; + +import io.grpc.ManagedChannel; +import org.onap.ccsdk.cds.controllerblueprints.common.api.Status; +import org.onap.ccsdk.cds.controllerblueprints.management.api.BluePrintManagementOutput; +import org.onap.ccsdk.cds.controllerblueprints.management.api.BluePrintManagementServiceGrpc; +import org.onap.ccsdk.cds.controllerblueprints.management.api.BluePrintManagementServiceGrpc.BluePrintManagementServiceBlockingStub; +import org.onap.ccsdk.cds.controllerblueprints.management.api.BluePrintUploadInput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@ConfigurationProperties("listenerservice") +@Component +public class BluePrintProcesssorHandler implements AutoCloseable { + + private static final Logger LOGGER = LoggerFactory.getLogger(BluePrintProcesssorHandler.class); + + private ManagedChannel channel; + + /** + * Sending CBA archive to CDS backend to store into its Database. + * + * @param request BluePrintManagementInput object holds CBA archive, its version and blueprints. + * @param managedChannel - ManagedChannel object helps to access the server or application end point. + * @return A response object + */ + public Status sendRequest(BluePrintUploadInput request, ManagedChannel managedChannel) { + LOGGER.info("Sending request to blueprint processor"); + + this.channel = managedChannel; + + final BluePrintManagementServiceBlockingStub syncStub = BluePrintManagementServiceGrpc.newBlockingStub(channel); + + // Send the request to CDS backend. + final BluePrintManagementOutput response = syncStub.uploadBlueprint(request); + + return response.getStatus(); + } + + @Override + public void close() { + if (channel != null) { + channel.shutdown(); + } + LOGGER.info("Stopping GRPC connection to CDS backend"); + } +} diff --git a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerService.java b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerService.java index 5dc0c2194..1efbe8f33 100644 --- a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerService.java +++ b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerService.java @@ -8,7 +8,8 @@ package org.onap.ccsdk.cds.cdssdclistener.service; -import java.util.zip.ZipFile; +import java.nio.file.Path; +import org.onap.sdc.api.results.IDistributionClientDownloadResult; public interface ListenerService { @@ -23,7 +24,15 @@ public interface ListenerService { /** * Store the Zip file into CDS database. * - * @param file The file to be stored. + * @param path path where zip file exists. */ - void saveBluePrintToCdsDatabase(ZipFile file); + void saveBluePrintToCdsDatabase(Path path); + + /** + * Extract and store CSAR to file. + * + * @param result - IDistributionClientDownloadResult contains payload. + * @param csarArchivePath The destination path where CSAR will be stored. + */ + void extractCsarAndStore(IDistributionClientDownloadResult result, String csarArchivePath); } diff --git a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerServiceImpl.java b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerServiceImpl.java index 4ff2a6ea8..37052082a 100644 --- a/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerServiceImpl.java +++ b/ms/cds-sdc-listener/application/src/main/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerServiceImpl.java @@ -8,6 +8,10 @@ package org.onap.ccsdk.cds.cdssdclistener.service; +import static java.nio.file.Files.walk; +import com.google.protobuf.ByteString; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -17,12 +21,24 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Enumeration; +import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import org.apache.commons.io.FileUtils; import org.apache.tomcat.util.http.fileupload.IOUtils; +import org.onap.ccsdk.cds.cdssdclistener.client.CdsSdcListenerAuthClientInterceptor; +import org.onap.ccsdk.cds.cdssdclistener.handler.BluePrintProcesssorHandler; +import org.onap.ccsdk.cds.controllerblueprints.common.api.Status; +import org.onap.ccsdk.cds.controllerblueprints.management.api.BluePrintUploadInput; +import org.onap.ccsdk.cds.controllerblueprints.management.api.FileChunk; +import org.onap.sdc.api.results.IDistributionClientDownloadResult; 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.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -31,13 +47,21 @@ import org.springframework.stereotype.Component; @ConfigurationProperties("listenerservice") public class ListenerServiceImpl implements ListenerService { - @Value("${listenerservice.config.csarArchive}") - private String csarArchivePath; + @Autowired + private BluePrintProcesssorHandler bluePrintProcesssorHandler; - @Value("${listenerservice.config.cbaArchive}") - private String cbaArchivePath; + @Autowired + private CdsSdcListenerAuthClientInterceptor cdsSdcListenerAuthClientInterceptor; + + @Value("${listenerservice.config.grpcAddress}") + private String grpcAddress; + + @Value("${listenerservice.config.grpcPort}") + private int grpcPort; private static final String CBA_ZIP_PATH = "Artifacts/Resources/[a-zA-Z0-9-_]+/Deployment/CONTROLLER_BLUEPRINT_ARCHIVE/[a-zA-Z0-9-_]+[.]zip"; + private static final int SUCCESS_CODE = 200; + private static final String CSAR_FILE_EXTENSION = ".csar"; private static final Logger LOGGER = LoggerFactory.getLogger(ListenerServiceImpl.class); @Override @@ -50,6 +74,7 @@ public class ListenerServiceImpl implements ListenerService { String fileName = entry.getName(); if (Pattern.matches(CBA_ZIP_PATH, fileName)) { final String cbaArchiveName = Paths.get(fileName).getFileName().toString(); + LOGGER.info("Storing the CBA archive {}", cbaArchiveName); storeBluePrint(zipFile, cbaArchiveName, cbaStorageDir, entry); } } @@ -59,13 +84,12 @@ public class ListenerServiceImpl implements ListenerService { } private void storeBluePrint(ZipFile zipFile, String fileName, Path cbaArchivePath, ZipEntry entry) { - final String changedFileName = fileName + ".zip"; - Path targetLocation = cbaArchivePath.resolve(changedFileName); + Path targetLocation = cbaArchivePath.resolve(fileName); + LOGGER.info("The target location for zip file is {}", targetLocation); File targetZipFile = new File(targetLocation.toString()); try { targetZipFile.createNewFile(); - } catch (IOException e) { LOGGER.error("Could not able to create file {}", targetZipFile, e); } @@ -73,15 +97,43 @@ public class ListenerServiceImpl implements ListenerService { try (InputStream inputStream = zipFile.getInputStream(entry); OutputStream out = new FileOutputStream( targetZipFile)) { IOUtils.copy(inputStream, out); + LOGGER.info("Succesfully store the CBA archive {} at this location", targetZipFile); + } catch (Exception e) { + LOGGER.error("Failed to put zip file into target location {}, {}", targetLocation, e); + } + } + + @Override + public void saveBluePrintToCdsDatabase(Path cbaArchivePath) { + Optional> zipFiles = getFilesFromDisk(cbaArchivePath); + zipFiles.ifPresent(this::prepareRequestForCdsBackend); + } + + @Override + public void extractCsarAndStore(IDistributionClientDownloadResult result, String csarArchivePath) { + + // Create CSAR storage directory + Path csarStorageDir = getStorageDirectory(csarArchivePath); + + byte[] payload = result.getArtifactPayload(); + String csarFileName = result.getArtifactFilename() + CSAR_FILE_EXTENSION; + Path targetLocation = csarStorageDir.resolve(csarFileName); + + LOGGER.info("The target location for the CSAR file is {}", targetLocation); + + File targetCsarFile = new File(targetLocation.toString()); + + try (FileOutputStream outFile = new FileOutputStream(targetCsarFile)) { + outFile.write(payload, 0, payload.length); } catch (Exception e) { - LOGGER.error("Failed to put zip file into target location {}", targetLocation, e); + LOGGER.error("Failed to put CSAR file into target location {}, {}", targetLocation, e); } } private Path getStorageDirectory(String path) { Path fileStorageLocation = Paths.get(path).toAbsolutePath().normalize(); - if (!Files.exists(fileStorageLocation)) { + if (!fileStorageLocation.toFile().exists()) { try { return Files.createDirectories(fileStorageLocation); } catch (IOException e) { @@ -91,8 +143,68 @@ public class ListenerServiceImpl implements ListenerService { return fileStorageLocation; } - @Override - public void saveBluePrintToCdsDatabase(ZipFile file) { - //TODO + private void prepareRequestForCdsBackend(List files) { + final ManagedChannel channel = getManagedChannel(); + + files.forEach(zipFile -> { + try { + final BluePrintUploadInput request = generateBluePrintUploadInputBuilder(zipFile); + + // Send request to CDS Backend. + final Status responseStatus = bluePrintProcesssorHandler.sendRequest(request, channel); + + if (responseStatus.getCode() != SUCCESS_CODE) { + LOGGER.error("Failed to store the CBA archive into CDS DB due to {}", + responseStatus.getErrorMessage()); + } else { + LOGGER.info(responseStatus.getMessage()); + } + + } catch (Exception e) { + LOGGER.error("Failure due to", e); + } finally { + //Delete the file from the local disk. + boolean fileDeleted = zipFile.delete(); + + if (!fileDeleted) { + LOGGER.error("Could not able to delete the zip file {}", zipFile.toString()); + } + } + }); + } + + private ManagedChannel getManagedChannel() { + return ManagedChannelBuilder.forAddress(grpcAddress, grpcPort) + .usePlaintext() + .intercept(cdsSdcListenerAuthClientInterceptor) + .build(); + } + + private BluePrintUploadInput generateBluePrintUploadInputBuilder(File file) throws IOException { + byte[] bytes = FileUtils.readFileToByteArray(file); + FileChunk fileChunk = FileChunk.newBuilder().setChunk(ByteString.copyFrom(bytes)).build(); + + return BluePrintUploadInput.newBuilder() + .setFileChunk(fileChunk) + .build(); + } + + /** + * Extract files from the given path + * + * @param path where files reside. + * @return list of files. + */ + public Optional> getFilesFromDisk(Path path) { + try (Stream fileTree = walk(path)) { + // Get the list of files from the path + return Optional.of(fileTree.filter(Files::isRegularFile) + .map(Path::toFile) + .collect(Collectors.toList())); + } catch (IOException e) { + LOGGER.error("Failed to find the file", e); + } + + return Optional.empty(); } } diff --git a/ms/cds-sdc-listener/application/src/main/resources/application.yml b/ms/cds-sdc-listener/application/src/main/resources/application.yml index 88de3b182..657ea9e80 100644 --- a/ms/cds-sdc-listener/application/src/main/resources/application.yml +++ b/ms/cds-sdc-listener/application/src/main/resources/application.yml @@ -14,6 +14,8 @@ listenerservice: keyStorePath: activateServerTLSAuth : false isUseHttpsWithDmaap: false - csarArchive: /opt/app/onap/cds-sdc-listener/csar-archive - cbaArchive: /opt/app/onap/cds/sdc-listener/cba-archive + archivePath: opt/app/onap/cds-sdc-listener/ + grpcAddress: localhost + grpcPort: 9111 + authHeader: Basic Y2NzZGthcHBzOmNjc2RrYXBwcw== diff --git a/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerClientTest.java b/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerClientTest.java index 4d0631f96..948631462 100644 --- a/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerClientTest.java +++ b/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/CdsSdcListenerClientTest.java @@ -17,6 +17,8 @@ import mockit.VerificationsInOrder; import mockit.integration.junit4.JMockit; import org.junit.Test; import org.junit.runner.RunWith; +import org.onap.ccsdk.cds.cdssdclistener.client.CdsSdcListenerClient; +import org.onap.ccsdk.cds.cdssdclistener.dto.CdsSdcListenerDto; import org.onap.ccsdk.cds.cdssdclistener.exceptions.CdsSdcListenerException; import org.onap.sdc.api.IDistributionClient; import org.onap.sdc.api.results.IDistributionClientResult; diff --git a/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/handler/BluePrintProcessorHandlerTest.java b/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/handler/BluePrintProcessorHandlerTest.java new file mode 100644 index 000000000..0d38decdf --- /dev/null +++ b/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/handler/BluePrintProcessorHandlerTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2019 Bell Canada. All rights reserved. + * + * NOTICE: All the intellectual and technical concepts contained herein are + * proprietary to Bell Canada and are protected by trade secret or copyright law. + * Unauthorized copying of this file, via any medium is strictly prohibited. + */ + +package org.onap.ccsdk.cds.cdssdclistener.handler; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import com.google.protobuf.ByteString; +import io.grpc.ManagedChannel; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.onap.ccsdk.cds.cdssdclistener.client.CdsSdcListenerAuthClientInterceptor; +import org.onap.ccsdk.cds.controllerblueprints.common.api.Status; +import org.onap.ccsdk.cds.controllerblueprints.management.api.BluePrintManagementOutput; +import org.onap.ccsdk.cds.controllerblueprints.management.api.BluePrintManagementServiceGrpc.BluePrintManagementServiceImplBase; +import org.onap.ccsdk.cds.controllerblueprints.management.api.BluePrintUploadInput; +import org.onap.ccsdk.cds.controllerblueprints.management.api.FileChunk; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + + +@RunWith(SpringRunner.class) +@EnableConfigurationProperties({BluePrintProcesssorHandler.class, CdsSdcListenerAuthClientInterceptor.class}) +@SpringBootTest(classes = {BluePrintProcessorHandlerTest.class}) +public class BluePrintProcessorHandlerTest { + + @Autowired + private BluePrintProcesssorHandler bluePrintProcesssorHandler; + + @Autowired + private CdsSdcListenerAuthClientInterceptor cdsSdcListenerAuthClientInterceptor; + + @Rule + public GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + private static final String CBA_ARCHIVE = "src/test/resources/testcba.zip"; + private static final String SUCCESS_MSG = "Successfully uploaded CBA"; + private static final int SUCCESS_CODE = 200; + private ManagedChannel channel; + + @Before + public void setUp() throws IOException { + final BluePrintManagementServiceImplBase serviceImplBase = new BluePrintManagementServiceImplBase() { + @Override + public void uploadBlueprint(BluePrintUploadInput request, + StreamObserver responseObserver) { + responseObserver.onNext(getBluePrintManagementOutput()); + responseObserver.onCompleted(); + } + }; + + // Generate server name. + String serverName = InProcessServerBuilder.generateName(); + + // Create a server, add service, start, and register. + grpcCleanup.register( + InProcessServerBuilder.forName(serverName).addService(serviceImplBase).directExecutor().build().start()); + + // Create a client channel. + channel = grpcCleanup.register(InProcessChannelBuilder.forName(serverName).directExecutor().build()); + } + + @Test + public void testApplicationEndPointSucess() throws IOException { + // Arrange + BluePrintUploadInput request = generateRequest(); + + // Act + Status output = bluePrintProcesssorHandler.sendRequest(request, channel); + + // Verify + assertEquals(SUCCESS_CODE, output.getCode()); + assertTrue(output.getMessage().contains(SUCCESS_MSG)); + } + + private BluePrintUploadInput generateRequest() throws IOException { + File file = Paths.get(CBA_ARCHIVE).toFile(); + byte[] bytes = FileUtils.readFileToByteArray(file); + FileChunk fileChunk = FileChunk.newBuilder().setChunk(ByteString.copyFrom(bytes)).build(); + + return BluePrintUploadInput.newBuilder().setFileChunk(fileChunk).build(); + } + + private BluePrintManagementOutput getBluePrintManagementOutput() { + return BluePrintManagementOutput.newBuilder() + .setStatus(Status.newBuilder().setMessage(SUCCESS_MSG).setCode(200).build()) + .build(); + } +} diff --git a/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerServiceImplTest.java b/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerServiceImplTest.java index 05e1ffdec..e33fbcdcc 100644 --- a/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerServiceImplTest.java +++ b/ms/cds-sdc-listener/application/src/test/java/org/onap/ccsdk/cds/cdssdclistener/service/ListenerServiceImplTest.java @@ -8,23 +8,34 @@ package org.onap.ccsdk.cds.cdssdclistener.service; +import static junit.framework.TestCase.assertTrue; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.onap.ccsdk.cds.cdssdclistener.client.CdsSdcListenerAuthClientInterceptor; +import org.onap.ccsdk.cds.cdssdclistener.handler.BluePrintProcesssorHandler; +import org.onap.sdc.impl.mock.DistributionClientDownloadResultStubImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) -@EnableConfigurationProperties(ListenerServiceImpl.class) +@EnableConfigurationProperties({ListenerServiceImpl.class, CdsSdcListenerAuthClientInterceptor.class, + BluePrintProcesssorHandler.class}) @SpringBootTest(classes = {ListenerServiceImplTest.class}) public class ListenerServiceImplTest { private static final String CSAR_SAMPLE = "src/test/resources/service-Testsvc140.csar"; + private static final String ZIP_FILE = ".zip"; + private static final String CSAR_FILE = ".csar"; + private String csarArchivePath; private Path tempDirectoryPath; @Rule @@ -33,12 +44,40 @@ public class ListenerServiceImplTest { @Autowired private ListenerServiceImpl listenerService; + @Before + public void setup() { + csarArchivePath = folder.getRoot().toString(); + tempDirectoryPath = Paths.get(csarArchivePath, "cds-sdc-listener-test"); + } @Test - public void extractBluePrintSuccessfully() { + public void extractBluePrintSuccessfully() throws IOException { + // Act + listenerService.extractBluePrint(CSAR_SAMPLE, tempDirectoryPath.toString()); + + // Verify + String result = checkFileExists(tempDirectoryPath); + assertTrue(result.contains(ZIP_FILE)); + } + + @Test + public void storeCsarArtifactToFileSuccessfully() throws IOException { // Arrange - tempDirectoryPath = Paths.get(folder.getRoot().toString(), "cds-sdc-listener-test"); + DistributionClientDownloadResultStubImpl resultStub = new DistributionClientDownloadResultStubImpl(); // Act - listenerService.extractBluePrint(CSAR_SAMPLE, tempDirectoryPath.toString()); + listenerService.extractCsarAndStore(resultStub, tempDirectoryPath.toString()); + + // Verify + String result = checkFileExists(tempDirectoryPath); + assertTrue(result.contains(CSAR_FILE)); + } + + private String checkFileExists(Path path) throws IOException { + return Files.walk(path) + .filter(Files::isRegularFile) + .map(Path::toFile) + .findAny() + .get() + .getName(); } } diff --git a/ms/cds-sdc-listener/application/src/test/resources/testcba.zip b/ms/cds-sdc-listener/application/src/test/resources/testcba.zip new file mode 100644 index 0000000000000000000000000000000000000000..c886fe6bc3bb5bb4416b7bcad22d730d8de3ea2a GIT binary patch literal 15123 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%0DR2jyp+s8T%aGPt%bhfep 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