09c8f92f4a9db52049c7bee4c997760dcaeda404
[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 OPERATIONAL_JSON;
79     private static String MODELS_JSON;
80     private static String CONFIG_JSON;
81     private static String PROPERTIES_FILE = System.getenv("SDNC_CONFIG_DIR") + "/daexim-offsite-backup.properties";
82
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             properties.put("file.operational", "odl_backup_operational.json");
126             properties.put("file.models", "odl_backup_models.json");
127             properties.put("file.config", "odl_backup_config.json");
128             return;
129         }
130         FileInputStream fileInputStream;
131         try {
132             fileInputStream = new FileInputStream(propertiesFile);
133             properties.load(fileInputStream);
134             fileInputStream.close();
135             LOG.info(properties.size() + " properties loaded.");
136             LOG.info("daeximDirectory: " + properties.getProperty("daeximDirectory"));
137             LOG.info("nexusUrl: " + properties.getProperty("nexusUrl"));
138             LOG.info("podName: " + properties.getProperty("podName"));
139             LOG.info("file.operational: " + properties.getProperty("file.operational"));
140             LOG.info("file.models: " + properties.getProperty("file.models"));
141             LOG.info("file.config: " + properties.getProperty("file.config"));
142         } catch(IOException e) {
143             LOG.error("Error loading properties.", e);
144         }
145     }
146
147     private void applyProperties() {
148         LOG.info("Applying properties...");
149         if(POD_NAME == null || POD_NAME.isEmpty()) {
150             LOG.warn("MY_POD_NAME environment variable not set. Using value from properties.");
151             POD_NAME = properties.getProperty("podName");
152         }
153         DAEXIM_DIR =  properties.getProperty("daeximDirectory");
154         NEXUS_URL = properties.getProperty("nexusUrl");
155
156         OPERATIONAL_JSON = properties.getProperty("file.operational");
157         MODELS_JSON = properties.getProperty("file.models");
158         CONFIG_JSON = properties.getProperty("file.config");
159
160         if(!properties.getProperty("credentials").contains(":")) { //Entire thing is encoded
161             CREDENTIALS = new String(Base64.getDecoder().decode(properties.getProperty("credentials")));
162         }
163         else {
164             String[] credentials = properties.getProperty("credentials").split(":", 2);
165             if(credentials[1].startsWith("enc:")) { // Password is encoded
166                 credentials[1] = new String(Base64.getDecoder().decode(credentials[1].split(":")[1]));
167             }
168             CREDENTIALS = credentials[0] + ":" + credentials[1];
169         }
170         LOG.info("Properties applied.");
171     }
172
173     private void createContainers() {
174         final WriteTransaction t = dataBroker.newReadWriteTransaction();
175         try {
176             CheckedFuture<Void, TransactionCommitFailedException> checkedFuture = t.submit();
177             checkedFuture.get();
178             LOG.info("Create Containers succeeded!: ");
179         } catch (InterruptedException | ExecutionException e) {
180             LOG.error("Create Containers Failed: " + e);
181             LOG.error("context", e);
182         }
183     }
184
185     protected void initializeChild() {
186
187     }
188
189     @Override
190     public void close() throws Exception {
191         LOG.info("Closing provider for " + appName);
192         executor.shutdown();
193         rpcRegistration.close();
194         LOG.info("Successfully closed provider for " + appName);
195     }
196
197     @Override
198     public void onDataTreeChanged(@Nonnull Collection changes) {
199
200     }
201
202     @Override
203     public ListenableFuture<RpcResult<BackupDataOutput>> backupData(BackupDataInput input) {
204         final String SVC_OPERATION = "backup-data";
205         LOG.info(appName + ":" + SVC_OPERATION + " called.");
206
207         String statusCode;
208         String message = "Data sent to offsite location.";
209
210         loadProperties();
211         applyProperties();
212
213         LOG.info("Pod Name: " + POD_NAME);
214         Instant timestamp = Instant.now();
215         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HH").withZone(ZoneId.of("GMT"));
216         String timestampedArchive = DAEXIM_DIR + POD_NAME + '-' + formatter.format(timestamp) + "-" + BACKUP_ARCHIVE;
217         try {
218             LOG.info("Creating archive...");
219             List<String> daeximFiles = Arrays.asList(DAEXIM_DIR + OPERATIONAL_JSON,DAEXIM_DIR + MODELS_JSON, DAEXIM_DIR + CONFIG_JSON);
220             createArchive(daeximFiles, timestampedArchive);
221             LOG.info("Archive created.");
222         } catch(IOException e) {
223             LOG.error("Error creating archive " + timestampedArchive);
224             LOG.error(e.getMessage());
225             statusCode = "500";
226             message = "Archive creation failed.";
227             return buildBackupDataFuture(statusCode, message);
228         }
229
230         try{
231             LOG.info("Sending archive to Nexus server: " + NEXUS_URL);
232             statusCode = Integer.toString(putArchive(timestampedArchive));
233             LOG.info("Archive sent to Nexus.");
234         } catch(IOException e) {
235             LOG.error("Nexus creation failed.", e);
236             statusCode = "500";
237             message = "Nexus creation failed.";
238         }
239
240         File archive = new File(timestampedArchive);
241         if(archive.exists()) {
242             archive.delete(); // Save some space on the ODL, keep them from piling up
243         }
244
245         LOG.info("Sending Response statusCode=" + statusCode+ " message=" + message + " | " + SVC_OPERATION);
246         return buildBackupDataFuture(statusCode, message);
247     }
248
249     @Override
250     public ListenableFuture<RpcResult<RetrieveDataOutput>> retrieveData(RetrieveDataInput input) {
251         final String SVC_OPERATION = "retrieve-data";
252         LOG.info(appName + ":" + SVC_OPERATION + " called.");
253
254         String statusCode = "200";
255         String message = "Data retrieved from offsite location.";
256
257         loadProperties();
258         applyProperties();
259
260         LOG.info("Pod Name: " + POD_NAME);
261         String archiveIdentifier = POD_NAME  + '-' + input.getTimestamp();
262         String timestampedArchive = DAEXIM_DIR +  archiveIdentifier + "-" + BACKUP_ARCHIVE;
263         LOG.info("Trying to retrieve " + timestampedArchive);
264         try {
265             statusCode = Integer.toString(getArchive(archiveIdentifier));
266         } catch(IOException e) {
267             LOG.error("Could not retrieve archive.", e);
268             statusCode = "500";
269             message = "Could not retrieve archive.";
270             return retrieveDataOutputRpcResult(statusCode, message);
271         }
272         LOG.info("Retrieved archive.");
273
274         LOG.info("Extracting archive...");
275         try {
276             extractArchive(DAEXIM_DIR + "-" + BACKUP_ARCHIVE);
277         } catch(IOException e) {
278             LOG.error("Could not extract archive.", e);
279             statusCode = "500";
280             message = "Could not extract archive.";
281             return retrieveDataOutputRpcResult(statusCode, message);
282         }
283         LOG.info("Archive extracted.");
284
285         return retrieveDataOutputRpcResult(statusCode, message);
286     }
287
288     private boolean exportExists(List<String> daeximFiles) {
289         File file;
290         for(String f : daeximFiles) {
291             file = new File(f);
292             if(!file.exists()) {
293                 return false;
294             }
295         }
296         return true;
297     }
298
299     private void createArchive(List<String> daeximFiles, String timestampedArchive) throws IOException {
300         if(!exportExists(daeximFiles)) {
301             LOG.error("Daexim exports do not exist.");
302             throw new IOException();
303         }
304         LOG.info("Creating " + timestampedArchive);
305         FileOutputStream fileOutputStream = new FileOutputStream(timestampedArchive);
306         ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream);
307         File targetZipFile;
308         FileInputStream fileInputStream;
309         ZipEntry zipEntry;
310         byte[] bytes;
311         int length;
312         for(String source : daeximFiles) {
313             LOG.info("Adding " + source + " to archive...");
314             targetZipFile = new File(source);
315             fileInputStream = new FileInputStream(targetZipFile);
316             zipEntry = new ZipEntry(targetZipFile.getName());
317             zipOutputStream.putNextEntry(zipEntry);
318             bytes = new byte[1024];
319
320             while((length = fileInputStream.read(bytes)) >= 0) {
321                 zipOutputStream.write(bytes, 0, length);
322             }
323             fileInputStream.close();
324         }
325
326         zipOutputStream.close();
327         fileOutputStream.close();
328     }
329
330     private void extractArchive(String timestampedArchive) throws IOException {
331         byte[] bytes = new byte[1024];
332         ZipInputStream zis = new ZipInputStream(new FileInputStream(timestampedArchive));
333         ZipEntry zipEntry = zis.getNextEntry();
334         while(zipEntry != null){
335             String fileName = zipEntry.getName();
336             File newFile = new File(DAEXIM_DIR + fileName);
337             FileOutputStream fos = new FileOutputStream(newFile);
338             int len;
339             while ((len = zis.read(bytes)) > 0) {
340                 fos.write(bytes, 0, len);
341             }
342             fos.close();
343             LOG.info(zipEntry.getName() + " extracted.");
344             zipEntry = zis.getNextEntry();
345         }
346         zis.closeEntry();
347         zis.close();
348         LOG.info(timestampedArchive + " extracted successfully.");
349     }
350
351     private int putArchive(String timestampedArchive) throws IOException {
352         File archive = new File(timestampedArchive);
353         HttpURLConnection connection = getNexusConnection(archive.getName());
354         connection.setRequestProperty("Content-Length", Long.toString(archive.length()));
355         connection.setRequestMethod("PUT");
356         connection.setDoOutput(true);
357
358         FileInputStream fileInputStream = new FileInputStream(archive);
359         OutputStream outputStream = connection.getOutputStream();
360
361         byte[] bytes = new byte[1024];
362         int length;
363         while((length = fileInputStream.read(bytes)) >= 0) {
364             outputStream.write(bytes, 0, length);
365         }
366
367         outputStream.flush();
368         outputStream.close();
369         fileInputStream.close();
370         connection.disconnect();
371
372         LOG.info("Status: " + connection.getResponseCode());
373         LOG.info("Message: " + connection.getResponseMessage());
374         return connection.getResponseCode();
375     }
376
377     private HttpURLConnection getNexusConnection(String archive) throws IOException {
378         URL url = new URL(NEXUS_URL + archive);
379         String auth = "Basic " + javax.xml.bind.DatatypeConverter.printBase64Binary(CREDENTIALS.getBytes());
380         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
381         connection.addRequestProperty("Authorization", auth);
382         connection.setRequestProperty("Connection", "keep-alive");
383         connection.setRequestProperty("Proxy-Connection", "keep-alive");
384         return connection;
385     }
386
387     private int getArchive(String archiveIdentifier) throws IOException {
388         File archive = new File(DAEXIM_DIR + "backup.zip");
389         if(archive.exists()) {
390             LOG.info("Recently retrieved archive found. Removing old archive...");
391             archive.delete();
392             LOG.info("Archive removed.");
393         }
394         HttpURLConnection connection = getNexusConnection( archiveIdentifier + "-" + BACKUP_ARCHIVE);
395         connection.setRequestMethod("GET");
396         connection.setDoInput(true);
397
398         InputStream connectionInputStream = connection.getInputStream();
399         FileOutputStream fileOutputStream = new FileOutputStream(archive);
400
401         byte[] bytes = new byte[1024];
402         int length;
403         while((length = connectionInputStream.read(bytes)) >= 0) { // while connection has bytes
404             fileOutputStream.write(bytes, 0, length); // write to archive
405         }
406         connection.disconnect();
407
408         LOG.info("Status: " + connection.getResponseCode());
409         LOG.info("Message: " + connection.getResponseMessage());
410         LOG.info(archive.getName() + " successfully created.");
411         return connection.getResponseCode();
412     }
413
414     private ListenableFuture<RpcResult<BackupDataOutput>> buildBackupDataFuture(String statusCode, String message) {
415         BackupDataOutputBuilder outputBuilder = new BackupDataOutputBuilder();
416         outputBuilder.setStatus(statusCode);
417         outputBuilder.setMessage(message);
418         RpcResult<BackupDataOutput> rpcResult = RpcResultBuilder.<BackupDataOutput> status(true).withResult(outputBuilder.build()).build();
419         return Futures.immediateFuture(rpcResult);
420     }
421
422     private ListenableFuture<RpcResult<RetrieveDataOutput>> retrieveDataOutputRpcResult(String status, String message) {
423         RetrieveDataOutputBuilder outputBuilder = new RetrieveDataOutputBuilder();
424         outputBuilder.setStatus(status);
425         outputBuilder.setMessage(message);
426         RpcResult<RetrieveDataOutput> rpcResult = RpcResultBuilder.<RetrieveDataOutput> status(true).withResult(outputBuilder.build()).build();
427         return Futures.immediateFuture(rpcResult);
428     }
429 }