blob: 1ad50d2d30f3b05d2e496fdf36b478c9fedfaf63 [file] [log] [blame]
Serhii Boiko992bf522019-03-28 18:38:04 +02001/*
2 * Copyright 2019-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package org.onosproject.drivers.gnoi;
18
19import com.google.protobuf.ByteString;
20import gnoi.system.SystemOuterClass;
21import gnoi.system.SystemOuterClass.SetPackageRequest;
22import gnoi.system.SystemOuterClass.SetPackageResponse;
23import gnoi.system.SystemOuterClass.SwitchControlProcessorRequest;
24import gnoi.types.Types;
25import org.onosproject.gnoi.api.GnoiClient;
26import org.onosproject.gnoi.api.GnoiController;
27import org.onosproject.grpc.utils.AbstractGrpcHandlerBehaviour;
28import org.onosproject.net.behaviour.SoftwareUpgrade;
29import org.apache.commons.io.FilenameUtils;
30import java.util.concurrent.CompletableFuture;
31import java.util.concurrent.SynchronousQueue;
32import java.util.concurrent.TimeUnit;
33import java.util.UUID;
34import java.io.File;
35import java.io.FileInputStream;
36import java.io.IOException;
37import java.nio.file.Path;
38import java.nio.file.Paths;
39import java.security.MessageDigest;
40import java.security.NoSuchAlgorithmException;
41import javax.xml.bind.DatatypeConverter;
42
43
44import static com.google.common.base.Preconditions.checkNotNull;
45
46
47/**
48 * Implementation that upgrades the software on a device.
49 */
50public class GnoiSoftwareUpgradeImpl
51 extends AbstractGrpcHandlerBehaviour<GnoiClient, GnoiController>
52 implements SoftwareUpgrade {
53
54 private static final String HASHING_METHOD = "MD5";
55 private static final int MAX_CHUNK_SIZE = 64_000;
56 private static final Path DEFAULT_PACKAGE_PATH = Paths.get("/tmp");
57 private static final int STREAM_TIMEOUT_SECONDS = 10;
58
59 public GnoiSoftwareUpgradeImpl() {
60 super(GnoiController.class);
61 }
62
63 @Override
64 public CompletableFuture<String> uploadPackage(String sourcePath, String dst) {
65 if (!setupBehaviour("uploadPackage()")) {
66 return CompletableFuture.completedFuture(null);
67 }
68 checkNotNull(sourcePath, "Source file not specified.");
69
70 final CompletableFuture<String> future = new CompletableFuture<>();
71
72 final File deb = Paths.get(sourcePath).toFile();
73 final String destinationPath;
74
75 final SetPackageRequest.Builder requestBuilder = SetPackageRequest.newBuilder();
76 final SystemOuterClass.Package.Builder pkgBuilder = SystemOuterClass.Package.newBuilder();
77 final Types.HashType.Builder hashBuilder = Types.HashType.newBuilder();
78
79 final SynchronousQueue<SetPackageRequest> stream = new SynchronousQueue<>();
80 final CompletableFuture<SetPackageResponse> futureResponse = client.setPackage(stream);
81
82 if (dst == null) {
83 destinationPath = getTempPath(sourcePath);
84 } else {
85 destinationPath = dst;
86 }
87
88 futureResponse.whenComplete((response, exception) -> {
89 if (exception == null) {
90 future.complete(destinationPath);
91 } else {
92 future.complete(null);
93 }
94 });
95
96 // Handle reading file, creating requests, etc...
97 CompletableFuture.runAsync(() -> {
98 try {
99
100 if (!deb.isFile()) {
101 log.error("File {} does not exist", sourcePath);
102 future.complete(null);
103 return;
104 }
105 // Set general package info (filename, version, etc...).
106 pkgBuilder.setFilename(destinationPath);
107 requestBuilder.setPackage(pkgBuilder.build());
108 boolean requestSent = stream.offer(requestBuilder.build(), STREAM_TIMEOUT_SECONDS, TimeUnit.SECONDS);
109 if (!requestSent) {
110 future.complete(null);
111 return;
112 }
113
114 final MessageDigest md = MessageDigest.getInstance(HASHING_METHOD);
115 final FileInputStream buffer = new FileInputStream(deb);
116 byte[] contents = new byte[MAX_CHUNK_SIZE];
117 int read = 0;
118
119 // Read file in 64k chunks.
120 while ((read = buffer.read(contents, 0, MAX_CHUNK_SIZE)) != -1) {
121 // Calculate File hash.
122 md.update(contents, 0, read);
123
124 // Form next request.
125 requestBuilder.setContents(ByteString.copyFrom(contents, 0, read));
126
127 // Add file chunk to stream.
128 requestSent = stream.offer(requestBuilder.build(), STREAM_TIMEOUT_SECONDS, TimeUnit.SECONDS);
129 if (!requestSent) {
130 future.complete(null);
131 return;
132 }
133 }
134
135 // Convert hash to lowercase string.
136 String hash = DatatypeConverter
137 .printHexBinary(md.digest())
138 .toLowerCase();
139
140 hashBuilder
141 .setMethodValue(Types.HashType.HashMethod.MD5.getNumber())
142 .setHash(ByteString.copyFrom(hash.getBytes()));
143
144 // Form last request with file hash.
145 requestBuilder.setHash(hashBuilder.build());
146
147 // Add file chunk to stream.
148 requestSent = stream.offer(requestBuilder.build(), STREAM_TIMEOUT_SECONDS, TimeUnit.SECONDS);
149 if (!requestSent) {
150 future.complete(null);
151 return;
152 }
153
154 } catch (IOException e) {
155 log.error("Error while reading file {}", sourcePath, e);
156 future.complete(null);
157 return;
158 } catch (InterruptedException e) {
159 log.error("Interrupted while sending package", e);
160 future.complete(null);
161 return;
162 } catch (SecurityException e) {
163 log.error("File {} cannot be accessed", sourcePath, e);
164 future.complete(null);
165 return;
166 } catch (NoSuchAlgorithmException e) {
167 log.error("Invalid hashing algorithm {}", HASHING_METHOD, e);
168 future.complete(null);
169 return;
170 }
171 });
172
173 return future;
174 }
175
176 @Override
177 public CompletableFuture<Response> swapAgent(String packagePath) {
178 if (!setupBehaviour("swapAgent()")) {
179 return CompletableFuture.completedFuture(new Response());
180 }
181 checkNotNull(packagePath, "File path not specified.");
182
183 final CompletableFuture<Response> future = new CompletableFuture<>();
184 final Types.Path.Builder routeProcessor = Types.Path.newBuilder();
185 final SwitchControlProcessorRequest.Builder requestMsg = SwitchControlProcessorRequest.newBuilder();
186
187 Paths.get(packagePath)
188 .iterator()
189 .forEachRemaining(part -> {
190 routeProcessor.addElem(
191 Types.PathElem.newBuilder()
192 .setName(part.toString())
193 .build());
194 });
195
196 requestMsg.setControlProcessor(routeProcessor.build());
197
198 client.switchControlProcessor(requestMsg.build())
199 .whenComplete((response, exception) -> {
200 if (exception != null) {
201 future.complete(new Response());
202 } else {
203 future.complete(new Response(response.getUptime(), response.getVersion()));
204 }
205 });
206
207 return future;
208 }
209
210 private String getTempPath(String source) {
211 String baseName = FilenameUtils.getBaseName(source);
212 String extension = FilenameUtils.getExtension(source);
213
214 if (extension.length() != 0) {
215 extension = "." + extension;
216 }
217
218 String filename = baseName + "_" + UUID.randomUUID().toString() + extension;
219 return DEFAULT_PACKAGE_PATH.resolve(filename).toString();
220 }
221}