blob: 1b1c23bf2bbb89c15c54141c390ff45bae57cd18 [file] [log] [blame]
Thomas Vachuska02aeb032015-01-06 22:36:30 -08001/*
2 * Copyright 2015 Open Networking Laboratory
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 */
16package org.onosproject.common.app;
17
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080018import com.google.common.collect.ImmutableList;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080019import com.google.common.collect.ImmutableSet;
20import com.google.common.io.ByteStreams;
21import com.google.common.io.Files;
22import org.apache.commons.configuration.ConfigurationException;
23import org.apache.commons.configuration.XMLConfiguration;
24import org.onlab.util.Tools;
25import org.onosproject.app.ApplicationDescription;
26import org.onosproject.app.ApplicationEvent;
27import org.onosproject.app.ApplicationException;
28import org.onosproject.app.ApplicationStoreDelegate;
29import org.onosproject.app.DefaultApplicationDescription;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090030import org.onosproject.core.ApplicationRole;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080031import org.onosproject.core.Permission;
32import org.onosproject.core.Version;
33import org.onosproject.store.AbstractStore;
34import org.slf4j.Logger;
35import org.slf4j.LoggerFactory;
36
37import java.io.ByteArrayInputStream;
38import java.io.File;
39import java.io.FileInputStream;
40import java.io.FileNotFoundException;
41import java.io.IOException;
42import java.io.InputStream;
43import java.net.URI;
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080044import java.nio.charset.Charset;
Thomas Vachuska90b453f2015-01-30 18:57:14 -080045import java.nio.file.NoSuchFileException;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090046import java.util.ArrayList;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080047import java.util.List;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090048import java.util.Locale;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080049import java.util.Set;
50import java.util.zip.ZipEntry;
51import java.util.zip.ZipInputStream;
52
Thomas Vachuskae18a3302015-06-23 12:48:28 -070053import static com.google.common.base.Preconditions.checkState;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080054import static com.google.common.io.ByteStreams.toByteArray;
55import static com.google.common.io.Files.createParentDirs;
56import static com.google.common.io.Files.write;
57
58/**
59 * Facility for reading application archive stream and managing application
60 * directory structure.
61 */
62public class ApplicationArchive
63 extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
64
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070065 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
66
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080067 // Magic strings to search for at the beginning of the archive stream
68 private static final String XML_MAGIC = "<?xml ";
69
70 // Magic strings to search for and how deep to search it into the archive stream
71 private static final String APP_MAGIC = "<app ";
72 private static final int APP_MAGIC_DEPTH = 1024;
73
Thomas Vachuska02aeb032015-01-06 22:36:30 -080074 private static final String NAME = "[@name]";
75 private static final String ORIGIN = "[@origin]";
76 private static final String VERSION = "[@version]";
77 private static final String FEATURES_REPO = "[@featuresRepo]";
78 private static final String FEATURES = "[@features]";
79 private static final String DESCRIPTION = "description";
80
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090081 private static final String ROLE = "security.role";
82 private static final String PERMISSIONS = "security.permissions.permission";
83
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070084 private static final String OAR = ".oar";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080085 private static final String APP_XML = "app.xml";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080086 private static final String M2_PREFIX = "m2";
87
Thomas Vachuska40a398b2015-04-03 22:26:30 -070088 private static final String ROOT = "../";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080089 private static final String M2_ROOT = "system/";
Thomas Vachuskad5d9bcb2015-03-18 17:46:20 -070090 private static final String APPS_ROOT = "apps/";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080091
Thomas Vachuska40a398b2015-04-03 22:26:30 -070092 private File root = new File(ROOT);
93 private File appsDir = new File(root, APPS_ROOT);
94 private File m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -080095
96 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -070097 * Sets the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -080098 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -070099 * @param root top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800100 */
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700101 protected void setRootPath(String root) {
102 this.root = new File(root);
103 this.appsDir = new File(this.root, APPS_ROOT);
104 this.m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800105 }
106
107 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700108 * Returns the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800109 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700110 * @return top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800111 */
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700112 public String getRootPath() {
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700113 return root.getPath();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800114 }
115
116 /**
117 * Returns the set of installed application names.
118 *
119 * @return installed application names
120 */
121 public Set<String> getApplicationNames() {
122 ImmutableSet.Builder<String> names = ImmutableSet.builder();
123 File[] files = appsDir.listFiles(File::isDirectory);
124 if (files != null) {
125 for (File file : files) {
126 names.add(file.getName());
127 }
128 }
129 return names.build();
130 }
131
132 /**
Thomas Vachuskacf960112015-03-06 22:36:51 -0800133 * Returns the timestamp in millis since start of epoch, of when the
134 * specified application was last modified or changed state.
135 *
136 * @param appName application name
137 * @return number of millis since start of epoch
138 */
139 public long getUpdateTime(String appName) {
140 return appFile(appName, APP_XML).lastModified();
141 }
142
143 /**
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800144 * Loads the application descriptor from the specified application archive
145 * stream and saves the stream in the appropriate application archive
146 * directory.
147 *
148 * @param appName application name
149 * @return application descriptor
150 * @throws org.onosproject.app.ApplicationException if unable to read application description
151 */
152 public ApplicationDescription getApplicationDescription(String appName) {
153 try {
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700154 XMLConfiguration cfg = new XMLConfiguration();
155 cfg.setAttributeSplittingDisabled(true);
156 cfg.setDelimiterParsingDisabled(true);
157 cfg.load(appFile(appName, APP_XML));
158 return loadAppDescription(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800159 } catch (Exception e) {
160 throw new ApplicationException("Unable to get app description", e);
161 }
162 }
163
164 /**
165 * Loads the application descriptor from the specified application archive
166 * stream and saves the stream in the appropriate application archive
167 * directory.
168 *
169 * @param stream application archive stream
170 * @return application descriptor
171 * @throws org.onosproject.app.ApplicationException if unable to read the
172 * archive stream or store
173 * the application archive
174 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800175 public synchronized ApplicationDescription saveApplication(InputStream stream) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800176 try (InputStream ais = stream) {
177 byte[] cache = toByteArray(ais);
178 InputStream bis = new ByteArrayInputStream(cache);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800179
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800180 boolean plainXml = isPlainXml(cache);
181 ApplicationDescription desc = plainXml ?
182 parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700183 checkState(!appFile(desc.name(), APP_XML).exists(),
184 "Application %s already installed", desc.name());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800185
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800186 if (plainXml) {
187 expandPlainApplication(cache, desc);
188 } else {
189 bis.reset();
190 expandZippedApplication(bis, desc);
191
192 bis.reset();
193 saveApplication(bis, desc);
194 }
195
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800196 installArtifacts(desc);
197 return desc;
198 } catch (IOException e) {
199 throw new ApplicationException("Unable to save application", e);
200 }
201 }
202
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800203 // Indicates whether the stream encoded in the given bytes is plain XML.
204 private boolean isPlainXml(byte[] bytes) {
205 return substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
206 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC);
207 }
208
209 // Returns the substring of maximum possible length from the specified bytes.
210 private String substring(byte[] bytes, int length) {
211 return new String(bytes, 0, Math.min(bytes.length, length), Charset.forName("UTF-8"));
212 }
213
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800214 /**
215 * Purges the application archive directory.
216 *
217 * @param appName application name
218 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800219 public synchronized void purgeApplication(String appName) {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800220 File appDir = new File(appsDir, appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800221 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800222 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800223 } catch (IOException e) {
224 throw new ApplicationException("Unable to purge application " + appName, e);
225 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800226 if (appDir.exists()) {
227 throw new ApplicationException("Unable to purge application " + appName);
228 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800229 }
230
231 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800232 * Returns application archive stream for the specified application. This
233 * will be either the application ZIP file or the application XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800234 *
235 * @param appName application name
236 * @return application archive stream
237 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800238 public synchronized InputStream getApplicationInputStream(String appName) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800239 try {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700240 File appFile = appFile(appName, appName + OAR);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800241 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800242 } catch (FileNotFoundException e) {
243 throw new ApplicationException("Application " + appName + " not found");
244 }
245 }
246
247 // Scans the specified ZIP stream for app.xml entry and parses it producing
248 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800249 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800250 throws IOException {
251 try (ZipInputStream zis = new ZipInputStream(stream)) {
252 ZipEntry entry;
253 while ((entry = zis.getNextEntry()) != null) {
254 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800255 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800256 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800257 }
258 zis.closeEntry();
259 }
260 }
261 throw new IOException("Unable to locate " + APP_XML);
262 }
263
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800264 // Scans the specified XML stream and parses it producing an application descriptor.
265 private ApplicationDescription parsePlainAppDescription(InputStream stream)
266 throws IOException {
267 XMLConfiguration cfg = new XMLConfiguration();
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700268 cfg.setAttributeSplittingDisabled(true);
269 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800270 try {
271 cfg.load(stream);
272 return loadAppDescription(cfg);
273 } catch (ConfigurationException e) {
274 throw new IOException("Unable to parse " + APP_XML, e);
275 }
276 }
277
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800278 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800279 String name = cfg.getString(NAME);
280 Version version = Version.version(cfg.getString(VERSION));
281 String desc = cfg.getString(DESCRIPTION);
282 String origin = cfg.getString(ORIGIN);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900283 ApplicationRole role = getRole(cfg.getString(ROLE));
284 Set<Permission> perms = getPermissions(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800285 String featRepo = cfg.getString(FEATURES_REPO);
286 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700287 List<String> features = ImmutableList.copyOf(cfg.getString(FEATURES).split(","));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800288
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900289 return new DefaultApplicationDescription(name, version, desc, origin, role,
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800290 perms, featuresRepo, features);
291 }
292
293 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800294 private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800295 throws IOException {
296 ZipInputStream zis = new ZipInputStream(stream);
297 ZipEntry entry;
298 File appDir = new File(appsDir, desc.name());
299 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800300 if (!entry.isDirectory()) {
301 byte[] data = ByteStreams.toByteArray(zis);
302 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800303 File file = new File(appDir, entry.getName());
304 createParentDirs(file);
305 write(data, file);
306 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800307 }
308 zis.close();
309 }
310
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800311 // Saves the specified XML stream into app-specific directory.
312 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
313 throws IOException {
314 File file = appFile(desc.name(), APP_XML);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700315 checkState(!file.getParentFile().exists(), "Application already installed");
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800316 createParentDirs(file);
317 write(stream, file);
318 }
319
320
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800321 // Saves the specified ZIP stream into a file under app-specific directory.
322 private void saveApplication(InputStream stream, ApplicationDescription desc)
323 throws IOException {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700324 Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + OAR));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800325 }
326
327 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800328 private void installArtifacts(ApplicationDescription desc) throws IOException {
329 try {
330 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
331 } catch (NoSuchFileException e) {
332 log.debug("Application {} has no M2 artifacts", desc.name());
333 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800334 }
335
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800336 /**
337 * Marks the app as active by creating token file in the app directory.
338 *
339 * @param appName application name
340 * @return true if file was created
341 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800342 protected boolean setActive(String appName) {
343 try {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800344 return appFile(appName, "active").createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800345 } catch (IOException e) {
346 throw new ApplicationException("Unable to mark app as active", e);
347 }
348 }
349
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800350 /**
351 * Clears the app as active by deleting token file in the app directory.
352 *
353 * @param appName application name
354 * @return true if file was deleted
355 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800356 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800357 return appFile(appName, "active").delete() && updateTime(appName);
358 }
359
360 /**
361 * Updates the time-stamp of the app descriptor file.
362 *
363 * @param appName application name
364 * @return true if the app descriptor was updated
365 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700366 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800367 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800368 }
369
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800370 /**
371 * Indicates whether the app was marked as active by checking for token file.
372 *
373 * @param appName application name
374 * @return true if the app is marked as active
375 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800376 protected boolean isActive(String appName) {
377 return appFile(appName, "active").exists();
378 }
379
380
381 // Returns the name of the file located under the specified app directory.
382 private File appFile(String appName, String fileName) {
383 return new File(new File(appsDir, appName), fileName);
384 }
385
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900386 // Returns the set of Permissions specified in the app.xml file
387 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900388 List<Permission> permissionList = new ArrayList();
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900389 for (Object o : cfg.getList(PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900390 String name = (String) o;
391 try {
392 Permission perm = Permission.valueOf(name);
393 permissionList.add(perm);
394 } catch (IllegalArgumentException e) {
395 log.debug("Unknown permission specified: %s", name);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900396 }
397 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900398 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900399 }
400
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900401 //
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900402 // Returns application role type
403 public ApplicationRole getRole(String value) {
404 if (value == null) {
405 return ApplicationRole.UNSPECIFIED;
406 } else {
407 try {
408 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
409 } catch (IllegalArgumentException e) {
410 log.debug("Unknown role value: %s", value);
411 return ApplicationRole.UNSPECIFIED;
412 }
413 }
414 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800415}