cc16bf70dd8578d2dbac02fb73cf73f870878b0d
[ccsdk/sli.git] /
1 /*
2  * ============LICENSE_START=======================================================
3  * openECOMP : SDN-C
4  * ================================================================================
5  * Copyright (C) 2018 AT&T Intellectual Property. All rights
6  *                      reserved.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  * ============LICENSE_END=========================================================
20 */
21
22 package org.onap.ccsdk.sli.northbound.daeximoffsitebackup;
23
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.OutputStream;
30 import java.net.HttpURLConnection;
31 import java.net.URL;
32 import java.time.Instant;
33 import java.time.ZoneId;
34 import java.time.format.DateTimeFormatter;
35 import java.util.Arrays;
36 import java.util.Base64;
37 import java.util.Collection;
38 import java.util.List;
39 import java.util.Properties;
40 import java.util.concurrent.ExecutionException;
41 import java.util.concurrent.ExecutorService;
42 import java.util.concurrent.Executors;
43 import java.util.zip.ZipEntry;
44 import java.util.zip.ZipInputStream;
45 import java.util.zip.ZipOutputStream;
46 import javax.annotation.Nonnull;
47
48 import com.google.common.util.concurrent.CheckedFuture;
49 import com.google.common.util.concurrent.Futures;
50 import com.google.common.util.concurrent.ListenableFuture;
51
52 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
53 import org.opendaylight.controller.md.sal.binding.api.DataTreeChangeListener;
54 import org.opendaylight.controller.md.sal.binding.api.WriteTransaction;
55 import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
56 import org.opendaylight.controller.sal.binding.api.BindingAwareBroker;
57 import org.opendaylight.controller.sal.binding.api.RpcProviderRegistry;
58 import org.opendaylight.yang.gen.v1.org.onap.ccsdk.sli.northbound.daeximoffsitebackup.rev180926.BackupDataInput;
59 import org.opendaylight.yang.gen.v1.org.onap.ccsdk.sli.northbound.daeximoffsitebackup.rev180926.BackupDataOutput;
60 import org.opendaylight.yang.gen.v1.org.onap.ccsdk.sli.northbound.daeximoffsitebackup.rev180926.BackupDataOutputBuilder;
61 import org.opendaylight.yang.gen.v1.org.onap.ccsdk.sli.northbound.daeximoffsitebackup.rev180926.DaeximOffsiteBackupService;
62 import org.opendaylight.yang.gen.v1.org.onap.ccsdk.sli.northbound.daeximoffsitebackup.rev180926.RetrieveDataOutput;
63 import org.opendaylight.yang.gen.v1.org.onap.ccsdk.sli.northbound.daeximoffsitebackup.rev180926.RetrieveDataOutputBuilder;
64 import org.opendaylight.yang.gen.v1.org.onap.ccsdk.sli.northbound.daeximoffsitebackup.rev180926.RetrieveDataInput;
65 import org.opendaylight.yangtools.yang.common.RpcResult;
66 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
67
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70
71 public class DaeximOffsiteBackupProvider implements AutoCloseable, DaeximOffsiteBackupService, DataTreeChangeListener {
72     private static final Logger LOG = LoggerFactory.getLogger(DaeximOffsiteBackupProvider.class);
73
74     private static String DAEXIM_DIR;
75     private static String CREDENTIALS;
76     private static String NEXUS_URL;
77     private static String POD_NAME;
78     private static String PROPERTIES_FILE = System.getenv("SDNC_CONFIG_DIR") + "/daexim-offsite-backup.properties";
79
80     private static final String OPERATIONAL_JSON = "odl_backup_operational.json";
81     private static final String MODELS_JSON = "odl_backup_models.json";
82     private static final String CONFIG_JSON = "odl_backup_config.json";
83     private static final String BACKUP_ARCHIVE = "odl_backup.zip";
84     private static final String appName = "daexim-offsite-backup";
85
86     private final ExecutorService executor;
87     private Properties properties;
88     private DataBroker dataBroker;
89     private RpcProviderRegistry rpcRegistry;
90     private BindingAwareBroker.RpcRegistration<DaeximOffsiteBackupService> rpcRegistration;
91
92     public DaeximOffsiteBackupProvider(DataBroker dataBroker,
93                                        RpcProviderRegistry rpcProviderRegistry) {
94         LOG.info("Creating provider for " + appName);
95         this.executor = Executors.newFixedThreadPool(1);
96         this.dataBroker = dataBroker;
97         this.rpcRegistry = rpcProviderRegistry;
98         initialize();
99     }
100
101     public void initialize() {
102         LOG.info("Initializing provider for " + appName);
103         // Create the top level containers
104         createContainers();
105         try {
106             DaeximOffsiteBackupUtil.loadProperties();
107         } catch (Exception e) {
108             LOG.error("Caught Exception while trying to load properties file", e);
109         }
110         rpcRegistration = rpcRegistry.addRpcImplementation(DaeximOffsiteBackupService.class, this);
111         LOG.info("Initialization complete for " + appName);
112     }
113
114     private void loadProperties() {
115         LOG.info("Loading properties from " + PROPERTIES_FILE);
116         if(properties == null)
117             properties = new Properties();
118         File propertiesFile = new File(PROPERTIES_FILE);
119         if(!propertiesFile.exists()) {
120             LOG.warn("Properties file (" + PROPERTIES_FILE + ") not found. Using default properties.");
121             properties.put("daeximDirectory", "/opt/opendaylight/current/daexim/");
122             properties.put("credentials", "admin:enc:YWRtaW4xMjM=");
123             properties.put("nexusUrl", "http://localhost:8081/nexus/content/repositories/");
124             properties.put("podName", "UNKNOWN_ODL");
125             return;
126         }
127         FileInputStream fileInputStream;
128         try {
129             fileInputStream = new FileInputStream(propertiesFile);
130             properties.load(fileInputStream);
131             fileInputStream.close();
132             LOG.info(properties.size() + " properties loaded.");
133             LOG.info("daeximDirectory: " + properties.getProperty("daeximDirectory"));
134             LOG.info("nexusUrl: " + properties.getProperty("nexusUrl"));
135             LOG.info("podName: " + properties.getProperty("podName"));
136         } catch(IOException e) {
137             LOG.error("Error loading properties.", e);
138         }
139     }
140
141     private void applyProperties() {
142         LOG.info("Applying properties...");
143         if(POD_NAME == null || POD_NAME.isEmpty()) {
144             LOG.warn("MY_POD_NAME environment variable not set. Using value from properties.");
145             POD_NAME = properties.getProperty("podName");
146         }
147         DAEXIM_DIR =  properties.getProperty("daeximDirectory");
148         NEXUS_URL = properties.getProperty("nexusUrl");
149
150         if(!properties.getProperty("credentials").contains(":")) { //Entire thing is encoded
151             CREDENTIALS = new String(Base64.getDecoder().decode(properties.getProperty("credentials")));
152         }
153         else {
154             String[] credentials = properties.getProperty("credentials").split(":", 2);
155             if(credentials[1].startsWith("enc:")) { // Password is encoded
156                 credentials[1] = new String(Base64.getDecoder().decode(credentials[1].split(":")[1]));
157             }
158             CREDENTIALS = credentials[0] + ":" + credentials[1];
159         }
160         LOG.info("Properties applied.");
161     }
162
163     private void createContainers() {
164         final WriteTransaction t = dataBroker.newReadWriteTransaction();
165         try {
166             CheckedFuture<Void, TransactionCommitFailedException> checkedFuture = t.submit();
167             checkedFuture.get();
168             LOG.info("Create Containers succeeded!: ");
169         } catch (InterruptedException | ExecutionException e) {
170             LOG.error("Create Containers Failed: " + e);
171             LOG.error("context", e);
172         }
173     }
174
175     protected void initializeChild() {
176
177     }
178
179     @Override
180     public void close() throws Exception {
181         LOG.info("Closing provider for " + appName);
182         executor.shutdown();
183         rpcRegistration.close();
184         LOG.info("Successfully closed provider for " + appName);
185     }
186
187     @Override
188     public void onDataTreeChanged(@Nonnull Collection changes) {
189
190     }
191
192     @Override
193     public ListenableFuture<RpcResult<BackupDataOutput>> backupData(BackupDataInput input) {
194         final String SVC_OPERATION = "backup-data";
195         LOG.info(appName + ":" + SVC_OPERATION + " called.");
196
197         String statusCode;
198         String message = "Data sent to offsite location.";
199
200         loadProperties();
201         applyProperties();
202
203         LOG.info("Pod Name: " + POD_NAME);
204         Instant timestamp = Instant.now();
205         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HH").withZone(ZoneId.of("GMT"));
206         String timestampedArchive = DAEXIM_DIR + POD_NAME + '-' + formatter.format(timestamp) + "-" + BACKUP_ARCHIVE;
207         try {
208             LOG.info("Creating archive...");
209             List<String> daeximFiles = Arrays.asList(DAEXIM_DIR + OPERATIONAL_JSON,DAEXIM_DIR + MODELS_JSON, DAEXIM_DIR + CONFIG_JSON);
210             createArchive(daeximFiles, timestampedArchive);
211             LOG.info("Archive created.");
212         } catch(IOException e) {
213             LOG.error("Error creating archive " + timestampedArchive);
214             LOG.error(e.getMessage());
215             statusCode = "500";
216             message = "Archive creation failed.";
217             return buildBackupDataFuture(statusCode, message);
218         }
219
220         try{
221             LOG.info("Sending archive to Nexus server: " + NEXUS_URL);
222             statusCode = Integer.toString(putArchive(timestampedArchive));
223             LOG.info("Archive sent to Nexus.");
224         } catch(IOException e) {
225             LOG.error("Nexus creation failed.", e);
226             statusCode = "500";
227             message = "Nexus creation failed.";
228         }
229
230         File archive = new File(timestampedArchive);
231         if(archive.exists()) {
232             archive.delete(); // Save some space on the ODL, keep them from piling up
233         }
234
235         LOG.info("Sending Response statusCode=" + statusCode+ " message=" + message + " | " + SVC_OPERATION);
236         return buildBackupDataFuture(statusCode, message);
237     }
238
239     @Override
240     public ListenableFuture<RpcResult<RetrieveDataOutput>> retrieveData(RetrieveDataInput input) {
241         final String SVC_OPERATION = "retrieve-data";
242         LOG.info(appName + ":" + SVC_OPERATION + " called.");
243
244         String statusCode = "200";
245         String message = "Data retrieved from offsite location.";
246
247         loadProperties();
248         applyProperties();
249
250         LOG.info("Pod Name: " + POD_NAME);
251         String archiveIdentifier = POD_NAME  + '-' + input.getTimestamp();
252         String timestampedArchive = DAEXIM_DIR +  archiveIdentifier + "-" + BACKUP_ARCHIVE;
253         LOG.info("Trying to retrieve " + timestampedArchive);
254         try {
255             statusCode = Integer.toString(getArchive(archiveIdentifier));
256         } catch(IOException e) {
257             LOG.error("Could not retrieve archive.", e);
258             statusCode = "500";
259             message = "Could not retrieve archive.";
260             return retrieveDataOutputRpcResult(statusCode, message);
261         }
262         LOG.info("Retrieved archive.");
263
264         LOG.info("Extracting archive...");
265         try {
266             extractArchive(DAEXIM_DIR + "-" + BACKUP_ARCHIVE);
267         } catch(IOException e) {
268             LOG.error("Could not extract archive.", e);
269             statusCode = "500";
270             message = "Could not extract archive.";
271             return retrieveDataOutputRpcResult(statusCode, message);
272         }
273         LOG.info("Archive extracted.");
274
275         return retrieveDataOutputRpcResult(statusCode, message);
276     }
277
278     private boolean exportExists(List<String> daeximFiles) {
279         File file;
280         for(String f : daeximFiles) {
281             file = new File(f);
282             if(!file.exists()) {
283                 return false;
284             }
285         }
286         return true;
287     }
288
289     private void createArchive(List<String> daeximFiles, String timestampedArchive) throws IOException {
290         if(!exportExists(daeximFiles)) {
291             LOG.error("Daexim exports do not exist.");
292             throw new IOException();
293         }
294         LOG.info("Creating " + timestampedArchive);
295         FileOutputStream fileOutputStream = new FileOutputStream(timestampedArchive);
296         ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
297         File targetZipFile;
298         FileInputStream fileInputStream;
299         ZipEntry zipEntry;
300         byte[] bytes;
301         int length;
302         for(String source : daeximFiles) {
303             LOG.info("Adding " + source + " to archive...");
304             targetZipFile = new File(source);
305             fileInputStream = new FileInputStream(targetZipFile);
306             zipEntry = new ZipEntry(targetZipFile.getName());
307             zipOutputStream.putNextEntry(zipEntry);
308             bytes = new byte[1024];
309
310             while((length = fileInputStream.read(bytes)) >= 0) {
311                 zipOutputStream.write(bytes, 0, length);
312             }
313             fileInputStream.close();
314         }
315
316         zipOutputStream.close();
317         fileOutputStream.close();
318     }
319
320     private void extractArchive(String timestampedArchive) throws IOException {
321         byte[] bytes = new byte[1024];
322         ZipInputStream zis = new ZipInputStream(new FileInputStream(timestampedArchive));
323         ZipEntry zipEntry = zis.getNextEntry();
324         while(zipEntry != null){
325             String fileName = zipEntry.getName();
326             File newFile = new File(DAEXIM_DIR + fileName);
327             FileOutputStream fos = new FileOutputStream(newFile);
328             int len;
329             while ((len = zis.read(bytes)) > 0) {
330                 fos.write(bytes, 0, len);
331             }
332             fos.close();
333             LOG.info(zipEntry.getName() + " extracted.");
334             zipEntry = zis.getNextEntry();
335         }
336         zis.closeEntry();
337         zis.close();
338         LOG.info(timestampedArchive + " extracted successfully.");
339     }
340
341     private int putArchive(String timestampedArchive) throws IOException {
342         File archive = new File(timestampedArchive);
343         HttpURLConnection connection = getNexusConnection(archive.getName());
344         connection.setRequestProperty("Content-Length", Long.toString(archive.length()));
345         connection.setRequestMethod("PUT");
346         connection.setDoOutput(true);
347
348         FileInputStream fileInputStream = new FileInputStream(archive);
349         OutputStream outputStream = connection.getOutputStream();
350
351         byte[] bytes = new byte[1024];
352         int length;
353         while((length = fileInputStream.read(bytes)) >= 0) {
354             outputStream.write(bytes, 0, length);
355         }
356
357         outputStream.flush();
358         outputStream.close();
359         fileInputStream.close();
360         connection.disconnect();
361
362         LOG.info("Status: " + connection.getResponseCode());
363         LOG.info("Message: " + connection.getResponseMessage());
364         return connection.getResponseCode();
365     }
366
367     private HttpURLConnection getNexusConnection(String archive) throws IOException {
368         URL url = new URL(NEXUS_URL + archive);
369         String auth = "Basic " + javax.xml.bind.DatatypeConverter.printBase64Binary(CREDENTIALS.getBytes());
370         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
371         connection.addRequestProperty("Authorization", auth);
372         connection.setRequestProperty("Connection", "keep-alive");
373         connection.setRequestProperty("Proxy-Connection", "keep-alive");
374         return connection;
375     }
376
377     private int getArchive(String archiveIdentifier) throws IOException {
378         File archive = new File(DAEXIM_DIR + "backup.zip");
379         if(archive.exists()) {
380             LOG.info("Recently retrieved archive found. Removing old archive...");
381             archive.delete();
382             LOG.info("Archive removed.");
383         }
384         HttpURLConnection connection = getNexusConnection( archiveIdentifier + "-" + BACKUP_ARCHIVE);
385         connection.setRequestMethod("GET");
386         connection.setDoInput(true);
387
388         InputStream connectionInputStream = connection.getInputStream();
389         FileOutputStream fileOutputStream = new FileOutputStream(archive);
390
391         byte[] bytes = new byte[1024];
392         int length;
393         while((length = connectionInputStream.read(bytes)) >= 0) { // while connection has bytes
394             fileOutputStream.write(bytes, 0, length); // write to archive
395         }
396         connection.disconnect();
397
398         LOG.info("Status: " + connection.getResponseCode());
399         LOG.info("Message: " + connection.getResponseMessage());
400         LOG.info(archive.getName() + " successfully created.");
401         return connection.getResponseCode();
402     }
403
404     private ListenableFuture<RpcResult<BackupDataOutput>> buildBackupDataFuture(String statusCode, String message) {
405         BackupDataOutputBuilder outputBuilder = new BackupDataOutputBuilder();
406         outputBuilder.setStatus(statusCode);
407         outputBuilder.setMessage(message);
408         RpcResult<BackupDataOutput> rpcResult = RpcResultBuilder.<BackupDataOutput> status(true).withResult(outputBuilder.build()).build();
409         return Futures.immediateFuture(rpcResult);
410     }
411
412     private ListenableFuture<RpcResult<RetrieveDataOutput>> retrieveDataOutputRpcResult(String status, String message) {
413         RetrieveDataOutputBuilder outputBuilder = new RetrieveDataOutputBuilder();
414         outputBuilder.setStatus(status);
415         outputBuilder.setMessage(message);
416         RpcResult<RetrieveDataOutput> rpcResult = RpcResultBuilder.<RetrieveDataOutput> status(true).withResult(outputBuilder.build()).build();
417         return Futures.immediateFuture(rpcResult);
418     }
419 }