blob: 37cdbdfc60955c4eaba7b0585421aa8110c41838 [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;
Thomas Vachuska761f0042015-11-11 19:10:17 -080020import com.google.common.collect.Lists;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080021import com.google.common.io.ByteStreams;
22import com.google.common.io.Files;
23import org.apache.commons.configuration.ConfigurationException;
Changhoon Yoonb856b812015-08-10 03:47:19 +090024import org.apache.commons.configuration.HierarchicalConfiguration;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080025import org.apache.commons.configuration.XMLConfiguration;
26import org.onlab.util.Tools;
27import org.onosproject.app.ApplicationDescription;
28import org.onosproject.app.ApplicationEvent;
29import org.onosproject.app.ApplicationException;
30import org.onosproject.app.ApplicationStoreDelegate;
31import org.onosproject.app.DefaultApplicationDescription;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090032import org.onosproject.core.ApplicationRole;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080033import org.onosproject.core.Version;
Changhoon Yoonb856b812015-08-10 03:47:19 +090034import org.onosproject.security.AppPermission;
35import org.onosproject.security.Permission;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080036import org.onosproject.store.AbstractStore;
37import org.slf4j.Logger;
38import org.slf4j.LoggerFactory;
39
40import java.io.ByteArrayInputStream;
41import java.io.File;
42import java.io.FileInputStream;
43import java.io.FileNotFoundException;
44import java.io.IOException;
45import java.io.InputStream;
46import java.net.URI;
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080047import java.nio.charset.Charset;
Thomas Vachuska90b453f2015-01-30 18:57:14 -080048import java.nio.file.NoSuchFileException;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080049import java.util.List;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090050import java.util.Locale;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080051import java.util.Set;
52import java.util.zip.ZipEntry;
53import java.util.zip.ZipInputStream;
54
Thomas Vachuskae18a3302015-06-23 12:48:28 -070055import static com.google.common.base.Preconditions.checkState;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080056import static com.google.common.io.ByteStreams.toByteArray;
57import static com.google.common.io.Files.createParentDirs;
58import static com.google.common.io.Files.write;
59
60/**
61 * Facility for reading application archive stream and managing application
62 * directory structure.
63 */
64public class ApplicationArchive
65 extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
66
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070067 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
68
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080069 // Magic strings to search for at the beginning of the archive stream
70 private static final String XML_MAGIC = "<?xml ";
71
72 // Magic strings to search for and how deep to search it into the archive stream
73 private static final String APP_MAGIC = "<app ";
74 private static final int APP_MAGIC_DEPTH = 1024;
75
Thomas Vachuska02aeb032015-01-06 22:36:30 -080076 private static final String NAME = "[@name]";
77 private static final String ORIGIN = "[@origin]";
78 private static final String VERSION = "[@version]";
79 private static final String FEATURES_REPO = "[@featuresRepo]";
80 private static final String FEATURES = "[@features]";
Thomas Vachuska761f0042015-11-11 19:10:17 -080081 private static final String APPS = "[@apps]";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080082 private static final String DESCRIPTION = "description";
83
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090084 private static final String ROLE = "security.role";
Changhoon Yoonb856b812015-08-10 03:47:19 +090085 private static final String APP_PERMISSIONS = "security.permissions.app-perm";
86 private static final String NET_PERMISSIONS = "security.permissions.net-perm";
87 private static final String JAVA_PERMISSIONS = "security.permissions.java-perm";
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090088
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070089 private static final String OAR = ".oar";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080090 private static final String APP_XML = "app.xml";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080091 private static final String M2_PREFIX = "m2";
92
Thomas Vachuska40a398b2015-04-03 22:26:30 -070093 private static final String ROOT = "../";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080094 private static final String M2_ROOT = "system/";
Thomas Vachuskad5d9bcb2015-03-18 17:46:20 -070095 private static final String APPS_ROOT = "apps/";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080096
Thomas Vachuska40a398b2015-04-03 22:26:30 -070097 private File root = new File(ROOT);
98 private File appsDir = new File(root, APPS_ROOT);
99 private File m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800100
101 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700102 * Sets the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800103 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700104 * @param root top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800105 */
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700106 protected void setRootPath(String root) {
107 this.root = new File(root);
108 this.appsDir = new File(this.root, APPS_ROOT);
109 this.m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800110 }
111
112 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700113 * Returns the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800114 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700115 * @return top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800116 */
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700117 public String getRootPath() {
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700118 return root.getPath();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800119 }
120
121 /**
122 * Returns the set of installed application names.
123 *
124 * @return installed application names
125 */
126 public Set<String> getApplicationNames() {
127 ImmutableSet.Builder<String> names = ImmutableSet.builder();
128 File[] files = appsDir.listFiles(File::isDirectory);
129 if (files != null) {
130 for (File file : files) {
131 names.add(file.getName());
132 }
133 }
134 return names.build();
135 }
136
137 /**
Thomas Vachuskacf960112015-03-06 22:36:51 -0800138 * Returns the timestamp in millis since start of epoch, of when the
139 * specified application was last modified or changed state.
140 *
141 * @param appName application name
142 * @return number of millis since start of epoch
143 */
144 public long getUpdateTime(String appName) {
145 return appFile(appName, APP_XML).lastModified();
146 }
147
148 /**
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800149 * Loads the application descriptor from the specified application archive
150 * stream and saves the stream in the appropriate application archive
151 * directory.
152 *
153 * @param appName application name
154 * @return application descriptor
155 * @throws org.onosproject.app.ApplicationException if unable to read application description
156 */
157 public ApplicationDescription getApplicationDescription(String appName) {
158 try {
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700159 XMLConfiguration cfg = new XMLConfiguration();
160 cfg.setAttributeSplittingDisabled(true);
161 cfg.setDelimiterParsingDisabled(true);
162 cfg.load(appFile(appName, APP_XML));
163 return loadAppDescription(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800164 } catch (Exception e) {
165 throw new ApplicationException("Unable to get app description", e);
166 }
167 }
168
169 /**
170 * Loads the application descriptor from the specified application archive
171 * stream and saves the stream in the appropriate application archive
172 * directory.
173 *
174 * @param stream application archive stream
175 * @return application descriptor
176 * @throws org.onosproject.app.ApplicationException if unable to read the
177 * archive stream or store
178 * the application archive
179 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800180 public synchronized ApplicationDescription saveApplication(InputStream stream) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800181 try (InputStream ais = stream) {
182 byte[] cache = toByteArray(ais);
183 InputStream bis = new ByteArrayInputStream(cache);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800184
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800185 boolean plainXml = isPlainXml(cache);
186 ApplicationDescription desc = plainXml ?
187 parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700188 checkState(!appFile(desc.name(), APP_XML).exists(),
189 "Application %s already installed", desc.name());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800190
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800191 if (plainXml) {
192 expandPlainApplication(cache, desc);
193 } else {
194 bis.reset();
195 expandZippedApplication(bis, desc);
196
197 bis.reset();
198 saveApplication(bis, desc);
199 }
200
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800201 installArtifacts(desc);
202 return desc;
203 } catch (IOException e) {
204 throw new ApplicationException("Unable to save application", e);
205 }
206 }
207
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800208 // Indicates whether the stream encoded in the given bytes is plain XML.
209 private boolean isPlainXml(byte[] bytes) {
210 return substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
211 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC);
212 }
213
214 // Returns the substring of maximum possible length from the specified bytes.
215 private String substring(byte[] bytes, int length) {
216 return new String(bytes, 0, Math.min(bytes.length, length), Charset.forName("UTF-8"));
217 }
218
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800219 /**
220 * Purges the application archive directory.
221 *
222 * @param appName application name
223 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800224 public synchronized void purgeApplication(String appName) {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800225 File appDir = new File(appsDir, appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800226 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800227 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800228 } catch (IOException e) {
229 throw new ApplicationException("Unable to purge application " + appName, e);
230 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800231 if (appDir.exists()) {
232 throw new ApplicationException("Unable to purge application " + appName);
233 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800234 }
235
236 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800237 * Returns application archive stream for the specified application. This
238 * will be either the application ZIP file or the application XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800239 *
240 * @param appName application name
241 * @return application archive stream
242 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800243 public synchronized InputStream getApplicationInputStream(String appName) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800244 try {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700245 File appFile = appFile(appName, appName + OAR);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800246 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800247 } catch (FileNotFoundException e) {
248 throw new ApplicationException("Application " + appName + " not found");
249 }
250 }
251
252 // Scans the specified ZIP stream for app.xml entry and parses it producing
253 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800254 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800255 throws IOException {
256 try (ZipInputStream zis = new ZipInputStream(stream)) {
257 ZipEntry entry;
258 while ((entry = zis.getNextEntry()) != null) {
259 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800260 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800261 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800262 }
263 zis.closeEntry();
264 }
265 }
266 throw new IOException("Unable to locate " + APP_XML);
267 }
268
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800269 // Scans the specified XML stream and parses it producing an application descriptor.
270 private ApplicationDescription parsePlainAppDescription(InputStream stream)
271 throws IOException {
272 XMLConfiguration cfg = new XMLConfiguration();
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700273 cfg.setAttributeSplittingDisabled(true);
274 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800275 try {
276 cfg.load(stream);
277 return loadAppDescription(cfg);
278 } catch (ConfigurationException e) {
279 throw new IOException("Unable to parse " + APP_XML, e);
280 }
281 }
282
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800283 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800284 String name = cfg.getString(NAME);
285 Version version = Version.version(cfg.getString(VERSION));
286 String desc = cfg.getString(DESCRIPTION);
287 String origin = cfg.getString(ORIGIN);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900288 ApplicationRole role = getRole(cfg.getString(ROLE));
289 Set<Permission> perms = getPermissions(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800290 String featRepo = cfg.getString(FEATURES_REPO);
291 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700292 List<String> features = ImmutableList.copyOf(cfg.getString(FEATURES).split(","));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800293
Thomas Vachuska761f0042015-11-11 19:10:17 -0800294 String apps = cfg.getString(APPS, "");
295 List<String> requiredApps = apps.isEmpty() ?
296 ImmutableList.of() : ImmutableList.copyOf(apps.split(","));
297
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900298 return new DefaultApplicationDescription(name, version, desc, origin, role,
Thomas Vachuska761f0042015-11-11 19:10:17 -0800299 perms, featuresRepo, features,
300 requiredApps);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800301 }
302
303 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800304 private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800305 throws IOException {
306 ZipInputStream zis = new ZipInputStream(stream);
307 ZipEntry entry;
308 File appDir = new File(appsDir, desc.name());
309 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800310 if (!entry.isDirectory()) {
311 byte[] data = ByteStreams.toByteArray(zis);
312 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800313 File file = new File(appDir, entry.getName());
314 createParentDirs(file);
315 write(data, file);
316 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800317 }
318 zis.close();
319 }
320
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800321 // Saves the specified XML stream into app-specific directory.
322 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
323 throws IOException {
324 File file = appFile(desc.name(), APP_XML);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700325 checkState(!file.getParentFile().exists(), "Application already installed");
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800326 createParentDirs(file);
327 write(stream, file);
328 }
329
330
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800331 // Saves the specified ZIP stream into a file under app-specific directory.
332 private void saveApplication(InputStream stream, ApplicationDescription desc)
333 throws IOException {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700334 Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + OAR));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800335 }
336
337 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800338 private void installArtifacts(ApplicationDescription desc) throws IOException {
339 try {
340 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
341 } catch (NoSuchFileException e) {
342 log.debug("Application {} has no M2 artifacts", desc.name());
343 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800344 }
345
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800346 /**
347 * Marks the app as active by creating token file in the app directory.
348 *
349 * @param appName application name
350 * @return true if file was created
351 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800352 protected boolean setActive(String appName) {
353 try {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800354 return appFile(appName, "active").createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800355 } catch (IOException e) {
356 throw new ApplicationException("Unable to mark app as active", e);
357 }
358 }
359
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800360 /**
361 * Clears the app as active by deleting token file in the app directory.
362 *
363 * @param appName application name
364 * @return true if file was deleted
365 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800366 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800367 return appFile(appName, "active").delete() && updateTime(appName);
368 }
369
370 /**
371 * Updates the time-stamp of the app descriptor file.
372 *
373 * @param appName application name
374 * @return true if the app descriptor was updated
375 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700376 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800377 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800378 }
379
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800380 /**
381 * Indicates whether the app was marked as active by checking for token file.
382 *
383 * @param appName application name
384 * @return true if the app is marked as active
385 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800386 protected boolean isActive(String appName) {
387 return appFile(appName, "active").exists();
388 }
389
390
391 // Returns the name of the file located under the specified app directory.
392 private File appFile(String appName, String fileName) {
393 return new File(new File(appsDir, appName), fileName);
394 }
395
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900396 // Returns the set of Permissions specified in the app.xml file
397 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Thomas Vachuska761f0042015-11-11 19:10:17 -0800398 List<Permission> permissionList = Lists.newArrayList();
Changhoon Yoonb856b812015-08-10 03:47:19 +0900399
400 for (Object o : cfg.getList(APP_PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900401 String name = (String) o;
Changhoon Yoonb856b812015-08-10 03:47:19 +0900402 permissionList.add(new Permission(AppPermission.class.getName(), name));
403 }
404 for (Object o : cfg.getList(NET_PERMISSIONS)) {
405 //TODO: TO BE FLESHED OUT WHEN NETWORK PERMISSIONS ARE SUPPORTED
406 break;
407 }
408
409 List<HierarchicalConfiguration> fields =
410 cfg.configurationsAt(JAVA_PERMISSIONS);
411 for (HierarchicalConfiguration sub : fields) {
412 String classname = sub.getString("classname");
413 String name = sub.getString("name");
414 String actions = sub.getString("actions");
415
416 if (classname != null && name != null) {
417 permissionList.add(new Permission(classname, name, actions));
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900418 }
419 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900420 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900421 }
422
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900423 //
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900424 // Returns application role type
425 public ApplicationRole getRole(String value) {
426 if (value == null) {
427 return ApplicationRole.UNSPECIFIED;
428 } else {
429 try {
430 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
431 } catch (IllegalArgumentException e) {
432 log.debug("Unknown role value: %s", value);
433 return ApplicationRole.UNSPECIFIED;
434 }
435 }
436 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800437}