blob: bdf1399bfd6181b68a68497dfc61d76c82dd6350 [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
53import static com.google.common.io.ByteStreams.toByteArray;
54import static com.google.common.io.Files.createParentDirs;
55import static com.google.common.io.Files.write;
56
57/**
58 * Facility for reading application archive stream and managing application
59 * directory structure.
60 */
61public class ApplicationArchive
62 extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
63
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070064 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
65
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080066 // Magic strings to search for at the beginning of the archive stream
67 private static final String XML_MAGIC = "<?xml ";
68
69 // Magic strings to search for and how deep to search it into the archive stream
70 private static final String APP_MAGIC = "<app ";
71 private static final int APP_MAGIC_DEPTH = 1024;
72
Thomas Vachuska02aeb032015-01-06 22:36:30 -080073 private static final String NAME = "[@name]";
74 private static final String ORIGIN = "[@origin]";
75 private static final String VERSION = "[@version]";
76 private static final String FEATURES_REPO = "[@featuresRepo]";
77 private static final String FEATURES = "[@features]";
78 private static final String DESCRIPTION = "description";
79
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090080 private static final String ROLE = "security.role";
81 private static final String PERMISSIONS = "security.permissions.permission";
82
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070083 private static final String OAR = ".oar";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080084 private static final String APP_XML = "app.xml";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080085 private static final String M2_PREFIX = "m2";
86
Thomas Vachuska40a398b2015-04-03 22:26:30 -070087 private static final String ROOT = "../";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080088 private static final String M2_ROOT = "system/";
Thomas Vachuskad5d9bcb2015-03-18 17:46:20 -070089 private static final String APPS_ROOT = "apps/";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080090
Thomas Vachuska40a398b2015-04-03 22:26:30 -070091 private File root = new File(ROOT);
92 private File appsDir = new File(root, APPS_ROOT);
93 private File m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -080094
95 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -070096 * Sets the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -080097 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -070098 * @param root top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -080099 */
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700100 protected void setRootPath(String root) {
101 this.root = new File(root);
102 this.appsDir = new File(this.root, APPS_ROOT);
103 this.m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800104 }
105
106 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700107 * Returns the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800108 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700109 * @return top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800110 */
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800111 protected String getRootPath() {
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700112 return root.getPath();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800113 }
114
115 /**
116 * Returns the set of installed application names.
117 *
118 * @return installed application names
119 */
120 public Set<String> getApplicationNames() {
121 ImmutableSet.Builder<String> names = ImmutableSet.builder();
122 File[] files = appsDir.listFiles(File::isDirectory);
123 if (files != null) {
124 for (File file : files) {
125 names.add(file.getName());
126 }
127 }
128 return names.build();
129 }
130
131 /**
Thomas Vachuskacf960112015-03-06 22:36:51 -0800132 * Returns the timestamp in millis since start of epoch, of when the
133 * specified application was last modified or changed state.
134 *
135 * @param appName application name
136 * @return number of millis since start of epoch
137 */
138 public long getUpdateTime(String appName) {
139 return appFile(appName, APP_XML).lastModified();
140 }
141
142 /**
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800143 * Loads the application descriptor from the specified application archive
144 * stream and saves the stream in the appropriate application archive
145 * directory.
146 *
147 * @param appName application name
148 * @return application descriptor
149 * @throws org.onosproject.app.ApplicationException if unable to read application description
150 */
151 public ApplicationDescription getApplicationDescription(String appName) {
152 try {
153 return loadAppDescription(new XMLConfiguration(appFile(appName, APP_XML)));
154 } catch (Exception e) {
155 throw new ApplicationException("Unable to get app description", e);
156 }
157 }
158
159 /**
160 * Loads the application descriptor from the specified application archive
161 * stream and saves the stream in the appropriate application archive
162 * directory.
163 *
164 * @param stream application archive stream
165 * @return application descriptor
166 * @throws org.onosproject.app.ApplicationException if unable to read the
167 * archive stream or store
168 * the application archive
169 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800170 public synchronized ApplicationDescription saveApplication(InputStream stream) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800171 try (InputStream ais = stream) {
172 byte[] cache = toByteArray(ais);
173 InputStream bis = new ByteArrayInputStream(cache);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800174
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800175 boolean plainXml = isPlainXml(cache);
176 ApplicationDescription desc = plainXml ?
177 parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800178
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800179 if (plainXml) {
180 expandPlainApplication(cache, desc);
181 } else {
182 bis.reset();
183 expandZippedApplication(bis, desc);
184
185 bis.reset();
186 saveApplication(bis, desc);
187 }
188
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800189 installArtifacts(desc);
190 return desc;
191 } catch (IOException e) {
192 throw new ApplicationException("Unable to save application", e);
193 }
194 }
195
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800196 // Indicates whether the stream encoded in the given bytes is plain XML.
197 private boolean isPlainXml(byte[] bytes) {
198 return substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
199 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC);
200 }
201
202 // Returns the substring of maximum possible length from the specified bytes.
203 private String substring(byte[] bytes, int length) {
204 return new String(bytes, 0, Math.min(bytes.length, length), Charset.forName("UTF-8"));
205 }
206
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800207 /**
208 * Purges the application archive directory.
209 *
210 * @param appName application name
211 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800212 public synchronized void purgeApplication(String appName) {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800213 File appDir = new File(appsDir, appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800214 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800215 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800216 } catch (IOException e) {
217 throw new ApplicationException("Unable to purge application " + appName, e);
218 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800219 if (appDir.exists()) {
220 throw new ApplicationException("Unable to purge application " + appName);
221 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800222 }
223
224 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800225 * Returns application archive stream for the specified application. This
226 * will be either the application ZIP file or the application XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800227 *
228 * @param appName application name
229 * @return application archive stream
230 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800231 public synchronized InputStream getApplicationInputStream(String appName) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800232 try {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700233 File appFile = appFile(appName, appName + OAR);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800234 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800235 } catch (FileNotFoundException e) {
236 throw new ApplicationException("Application " + appName + " not found");
237 }
238 }
239
240 // Scans the specified ZIP stream for app.xml entry and parses it producing
241 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800242 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800243 throws IOException {
244 try (ZipInputStream zis = new ZipInputStream(stream)) {
245 ZipEntry entry;
246 while ((entry = zis.getNextEntry()) != null) {
247 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800248 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800249 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800250 }
251 zis.closeEntry();
252 }
253 }
254 throw new IOException("Unable to locate " + APP_XML);
255 }
256
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800257 // Scans the specified XML stream and parses it producing an application descriptor.
258 private ApplicationDescription parsePlainAppDescription(InputStream stream)
259 throws IOException {
260 XMLConfiguration cfg = new XMLConfiguration();
261 try {
262 cfg.load(stream);
263 return loadAppDescription(cfg);
264 } catch (ConfigurationException e) {
265 throw new IOException("Unable to parse " + APP_XML, e);
266 }
267 }
268
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800269 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
270 cfg.setAttributeSplittingDisabled(true);
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800271 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800272 String name = cfg.getString(NAME);
273 Version version = Version.version(cfg.getString(VERSION));
274 String desc = cfg.getString(DESCRIPTION);
275 String origin = cfg.getString(ORIGIN);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900276 ApplicationRole role = getRole(cfg.getString(ROLE));
277 Set<Permission> perms = getPermissions(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800278 String featRepo = cfg.getString(FEATURES_REPO);
279 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800280 List<String> features = ImmutableList.copyOf(cfg.getStringArray(FEATURES));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800281
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900282 return new DefaultApplicationDescription(name, version, desc, origin, role,
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800283 perms, featuresRepo, features);
284 }
285
286 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800287 private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800288 throws IOException {
289 ZipInputStream zis = new ZipInputStream(stream);
290 ZipEntry entry;
291 File appDir = new File(appsDir, desc.name());
292 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800293 if (!entry.isDirectory()) {
294 byte[] data = ByteStreams.toByteArray(zis);
295 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800296 File file = new File(appDir, entry.getName());
297 createParentDirs(file);
298 write(data, file);
299 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800300 }
301 zis.close();
302 }
303
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800304 // Saves the specified XML stream into app-specific directory.
305 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
306 throws IOException {
307 File file = appFile(desc.name(), APP_XML);
308 createParentDirs(file);
309 write(stream, file);
310 }
311
312
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800313 // Saves the specified ZIP stream into a file under app-specific directory.
314 private void saveApplication(InputStream stream, ApplicationDescription desc)
315 throws IOException {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700316 Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + OAR));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800317 }
318
319 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800320 private void installArtifacts(ApplicationDescription desc) throws IOException {
321 try {
322 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
323 } catch (NoSuchFileException e) {
324 log.debug("Application {} has no M2 artifacts", desc.name());
325 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800326 }
327
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800328 /**
329 * Marks the app as active by creating token file in the app directory.
330 *
331 * @param appName application name
332 * @return true if file was created
333 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800334 protected boolean setActive(String appName) {
335 try {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800336 return appFile(appName, "active").createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800337 } catch (IOException e) {
338 throw new ApplicationException("Unable to mark app as active", e);
339 }
340 }
341
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800342 /**
343 * Clears the app as active by deleting token file in the app directory.
344 *
345 * @param appName application name
346 * @return true if file was deleted
347 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800348 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800349 return appFile(appName, "active").delete() && updateTime(appName);
350 }
351
352 /**
353 * Updates the time-stamp of the app descriptor file.
354 *
355 * @param appName application name
356 * @return true if the app descriptor was updated
357 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700358 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800359 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800360 }
361
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800362 /**
363 * Indicates whether the app was marked as active by checking for token file.
364 *
365 * @param appName application name
366 * @return true if the app is marked as active
367 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800368 protected boolean isActive(String appName) {
369 return appFile(appName, "active").exists();
370 }
371
372
373 // Returns the name of the file located under the specified app directory.
374 private File appFile(String appName, String fileName) {
375 return new File(new File(appsDir, appName), fileName);
376 }
377
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900378 // Returns the set of Permissions specified in the app.xml file
379 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900380 List<Permission> permissionList = new ArrayList();
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900381 for (Object o : cfg.getList(PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900382 String name = (String) o;
383 try {
384 Permission perm = Permission.valueOf(name);
385 permissionList.add(perm);
386 } catch (IllegalArgumentException e) {
387 log.debug("Unknown permission specified: %s", name);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900388 }
389 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900390 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900391 }
392
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900393 //
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900394 // Returns application role type
395 public ApplicationRole getRole(String value) {
396 if (value == null) {
397 return ApplicationRole.UNSPECIFIED;
398 } else {
399 try {
400 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
401 } catch (IllegalArgumentException e) {
402 log.debug("Unknown role value: %s", value);
403 return ApplicationRole.UNSPECIFIED;
404 }
405 }
406 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800407}