blob: 54f0fb89029bb87512a9ca01e1575a91ba9b2444 [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;
Changhoon Yoonb856b812015-08-10 03:47:19 +090023import org.apache.commons.configuration.HierarchicalConfiguration;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080024import org.apache.commons.configuration.XMLConfiguration;
25import org.onlab.util.Tools;
26import org.onosproject.app.ApplicationDescription;
27import org.onosproject.app.ApplicationEvent;
28import org.onosproject.app.ApplicationException;
29import org.onosproject.app.ApplicationStoreDelegate;
30import org.onosproject.app.DefaultApplicationDescription;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090031import org.onosproject.core.ApplicationRole;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080032import org.onosproject.core.Version;
Changhoon Yoonb856b812015-08-10 03:47:19 +090033import org.onosproject.security.AppPermission;
34import org.onosproject.security.Permission;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080035import org.onosproject.store.AbstractStore;
Changhoon Yoonb856b812015-08-10 03:47:19 +090036
Thomas Vachuska02aeb032015-01-06 22:36:30 -080037import 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;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090049import java.util.ArrayList;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080050import java.util.List;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090051import java.util.Locale;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080052import java.util.Set;
53import java.util.zip.ZipEntry;
54import java.util.zip.ZipInputStream;
55
Thomas Vachuskae18a3302015-06-23 12:48:28 -070056import static com.google.common.base.Preconditions.checkState;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080057import static com.google.common.io.ByteStreams.toByteArray;
58import static com.google.common.io.Files.createParentDirs;
59import static com.google.common.io.Files.write;
60
61/**
62 * Facility for reading application archive stream and managing application
63 * directory structure.
64 */
65public class ApplicationArchive
66 extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
67
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070068 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
69
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080070 // Magic strings to search for at the beginning of the archive stream
71 private static final String XML_MAGIC = "<?xml ";
72
73 // Magic strings to search for and how deep to search it into the archive stream
74 private static final String APP_MAGIC = "<app ";
75 private static final int APP_MAGIC_DEPTH = 1024;
76
Thomas Vachuska02aeb032015-01-06 22:36:30 -080077 private static final String NAME = "[@name]";
78 private static final String ORIGIN = "[@origin]";
79 private static final String VERSION = "[@version]";
80 private static final String FEATURES_REPO = "[@featuresRepo]";
81 private static final String FEATURES = "[@features]";
82 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
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900294 return new DefaultApplicationDescription(name, version, desc, origin, role,
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800295 perms, featuresRepo, features);
296 }
297
298 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800299 private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800300 throws IOException {
301 ZipInputStream zis = new ZipInputStream(stream);
302 ZipEntry entry;
303 File appDir = new File(appsDir, desc.name());
304 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800305 if (!entry.isDirectory()) {
306 byte[] data = ByteStreams.toByteArray(zis);
307 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800308 File file = new File(appDir, entry.getName());
309 createParentDirs(file);
310 write(data, file);
311 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800312 }
313 zis.close();
314 }
315
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800316 // Saves the specified XML stream into app-specific directory.
317 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
318 throws IOException {
319 File file = appFile(desc.name(), APP_XML);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700320 checkState(!file.getParentFile().exists(), "Application already installed");
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800321 createParentDirs(file);
322 write(stream, file);
323 }
324
325
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800326 // Saves the specified ZIP stream into a file under app-specific directory.
327 private void saveApplication(InputStream stream, ApplicationDescription desc)
328 throws IOException {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700329 Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + OAR));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800330 }
331
332 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800333 private void installArtifacts(ApplicationDescription desc) throws IOException {
334 try {
335 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
336 } catch (NoSuchFileException e) {
337 log.debug("Application {} has no M2 artifacts", desc.name());
338 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800339 }
340
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800341 /**
342 * Marks the app as active by creating token file in the app directory.
343 *
344 * @param appName application name
345 * @return true if file was created
346 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800347 protected boolean setActive(String appName) {
348 try {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800349 return appFile(appName, "active").createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800350 } catch (IOException e) {
351 throw new ApplicationException("Unable to mark app as active", e);
352 }
353 }
354
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800355 /**
356 * Clears the app as active by deleting token file in the app directory.
357 *
358 * @param appName application name
359 * @return true if file was deleted
360 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800361 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800362 return appFile(appName, "active").delete() && updateTime(appName);
363 }
364
365 /**
366 * Updates the time-stamp of the app descriptor file.
367 *
368 * @param appName application name
369 * @return true if the app descriptor was updated
370 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700371 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800372 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800373 }
374
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800375 /**
376 * Indicates whether the app was marked as active by checking for token file.
377 *
378 * @param appName application name
379 * @return true if the app is marked as active
380 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800381 protected boolean isActive(String appName) {
382 return appFile(appName, "active").exists();
383 }
384
385
386 // Returns the name of the file located under the specified app directory.
387 private File appFile(String appName, String fileName) {
388 return new File(new File(appsDir, appName), fileName);
389 }
390
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900391 // Returns the set of Permissions specified in the app.xml file
392 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900393 List<Permission> permissionList = new ArrayList();
Changhoon Yoonb856b812015-08-10 03:47:19 +0900394
395 for (Object o : cfg.getList(APP_PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900396 String name = (String) o;
Changhoon Yoonb856b812015-08-10 03:47:19 +0900397 permissionList.add(new Permission(AppPermission.class.getName(), name));
398 }
399 for (Object o : cfg.getList(NET_PERMISSIONS)) {
400 //TODO: TO BE FLESHED OUT WHEN NETWORK PERMISSIONS ARE SUPPORTED
401 break;
402 }
403
404 List<HierarchicalConfiguration> fields =
405 cfg.configurationsAt(JAVA_PERMISSIONS);
406 for (HierarchicalConfiguration sub : fields) {
407 String classname = sub.getString("classname");
408 String name = sub.getString("name");
409 String actions = sub.getString("actions");
410
411 if (classname != null && name != null) {
412 permissionList.add(new Permission(classname, name, actions));
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900413 }
414 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900415 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900416 }
417
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900418 //
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900419 // Returns application role type
420 public ApplicationRole getRole(String value) {
421 if (value == null) {
422 return ApplicationRole.UNSPECIFIED;
423 } else {
424 try {
425 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
426 } catch (IllegalArgumentException e) {
427 log.debug("Unknown role value: %s", value);
428 return ApplicationRole.UNSPECIFIED;
429 }
430 }
431 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800432}