Thomas Vachuska
Committed by Gerrit Code Review

Added ability to upload apps as both app.xml or app.zip.

Added a number of app.xml files for built-in apps.
Added ability to install & activate in one command.

Change-Id: I3fa5fa487ef76d9fe3da4d6dce8045d538cba423
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.config" origin="ON.Lab" version="1.1.0"
features="onos-app-config">
<description>ONOS network configuration application</description>
</app>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.fwd" origin="ON.Lab" version="1.1.0"
features="onos-app-fwd">
<description>ONOS Reactive forwarding application using flow subsystem</description>
</app>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.ifwd" origin="ON.Lab" version="1.1.0"
features="onos-app-ifwd">
<description>ONOS Reactive forwarding application using intent subsystem (experimental)</description>
</app>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.metrics.intent" origin="ON.Lab" version="1.1.0"
features="onos-app-metrics-intent">
<description>ONOS intent metrics test application</description>
</app>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.metrics.topology" origin="ON.Lab" version="1.1.0"
features="onos-app-metrics-topology">
<description>ONOS topology metrics test application</description>
</app>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.optical" origin="ON.Lab" version="1.1.0"
features="onos-app-sdnip">
<description>ONOS Packet/Optical use-case application</description>
</app>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.proxyarp" origin="ON.Lab" version="1.1.0"
features="onos-app-proxyarp">
<description>ONOS proxy ARP application</description>
</app>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.sdnip" origin="ON.Lab" version="1.1.0"
features="onos-app-sdnip">
<description>ONOS SDN/IP use-case application</description>
</app>
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<app name="org.onosproject.app.tvue" origin="ON.Lab" version="1.1.0"
features="onos-app-tvue">
<description>Early prototype GUI (deprecated)</description>
</app>
......@@ -29,7 +29,9 @@ public interface ApplicationAdminService extends ApplicationService {
/**
* Installs the application contained in the specified application archive
* input stream.
* input stream. This can be either a ZIP stream containing a compressed
* application archive or a plain XML stream containing just the
* {@code app.xml} application descriptor file.
*
* @param appDescStream application descriptor input stream
* @return installed application descriptor
......
......@@ -40,6 +40,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.NoSuchFileException;
import java.util.List;
import java.util.Set;
......@@ -57,6 +58,13 @@ import static com.google.common.io.Files.write;
public class ApplicationArchive
extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
// Magic strings to search for at the beginning of the archive stream
private static final String XML_MAGIC = "<?xml ";
// Magic strings to search for and how deep to search it into the archive stream
private static final String APP_MAGIC = "<app ";
private static final int APP_MAGIC_DEPTH = 1024;
private static final String NAME = "[@name]";
private static final String ORIGIN = "[@origin]";
private static final String VERSION = "[@version]";
......@@ -144,13 +152,21 @@ public class ApplicationArchive
try (InputStream ais = stream) {
byte[] cache = toByteArray(ais);
InputStream bis = new ByteArrayInputStream(cache);
ApplicationDescription desc = parseAppDescription(bis);
bis.reset();
expandApplication(bis, desc);
bis.reset();
boolean plainXml = isPlainXml(cache);
ApplicationDescription desc = plainXml ?
parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
if (plainXml) {
expandPlainApplication(cache, desc);
} else {
bis.reset();
expandZippedApplication(bis, desc);
bis.reset();
saveApplication(bis, desc);
}
saveApplication(bis, desc);
installArtifacts(desc);
return desc;
} catch (IOException e) {
......@@ -158,28 +174,45 @@ public class ApplicationArchive
}
}
// Indicates whether the stream encoded in the given bytes is plain XML.
private boolean isPlainXml(byte[] bytes) {
return substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC);
}
// Returns the substring of maximum possible length from the specified bytes.
private String substring(byte[] bytes, int length) {
return new String(bytes, 0, Math.min(bytes.length, length), Charset.forName("UTF-8"));
}
/**
* Purges the application archive directory.
*
* @param appName application name
*/
public void purgeApplication(String appName) {
File appDir = new File(appsDir, appName);
try {
Tools.removeDirectory(new File(appsDir, appName));
Tools.removeDirectory(appDir);
} catch (IOException e) {
throw new ApplicationException("Unable to purge application " + appName, e);
}
if (appDir.exists()) {
throw new ApplicationException("Unable to purge application " + appName);
}
}
/**
* Returns application archive stream for the specified application.
* Returns application archive stream for the specified application. This
* will be either the application ZIP file or the application XML file.
*
* @param appName application name
* @return application archive stream
*/
public InputStream getApplicationInputStream(String appName) {
try {
return new FileInputStream(appFile(appName, appName + ".zip"));
File appFile = appFile(appName, appName + ".zip");
return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
} catch (FileNotFoundException e) {
throw new ApplicationException("Application " + appName + " not found");
}
......@@ -187,20 +220,14 @@ public class ApplicationArchive
// Scans the specified ZIP stream for app.xml entry and parses it producing
// an application descriptor.
private ApplicationDescription parseAppDescription(InputStream stream)
private ApplicationDescription parseZippedAppDescription(InputStream stream)
throws IOException {
try (ZipInputStream zis = new ZipInputStream(stream)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().equals(APP_XML)) {
byte[] data = ByteStreams.toByteArray(zis);
XMLConfiguration cfg = new XMLConfiguration();
try {
cfg.load(new ByteArrayInputStream(data));
return loadAppDescription(cfg);
} catch (ConfigurationException e) {
throw new IOException("Unable to parse " + APP_XML, e);
}
return parsePlainAppDescription(new ByteArrayInputStream(data));
}
zis.closeEntry();
}
......@@ -208,6 +235,18 @@ public class ApplicationArchive
throw new IOException("Unable to locate " + APP_XML);
}
// Scans the specified XML stream and parses it producing an application descriptor.
private ApplicationDescription parsePlainAppDescription(InputStream stream)
throws IOException {
XMLConfiguration cfg = new XMLConfiguration();
try {
cfg.load(stream);
return loadAppDescription(cfg);
} catch (ConfigurationException e) {
throw new IOException("Unable to parse " + APP_XML, e);
}
}
private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
cfg.setAttributeSplittingDisabled(true);
cfg.setDelimiterParsingDisabled(true);
......@@ -225,7 +264,7 @@ public class ApplicationArchive
}
// Expands the specified ZIP stream into app-specific directory.
private void expandApplication(InputStream stream, ApplicationDescription desc)
private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
throws IOException {
ZipInputStream zis = new ZipInputStream(stream);
ZipEntry entry;
......@@ -234,7 +273,6 @@ public class ApplicationArchive
if (!entry.isDirectory()) {
byte[] data = ByteStreams.toByteArray(zis);
zis.closeEntry();
File file = new File(appDir, entry.getName());
createParentDirs(file);
write(data, file);
......@@ -243,6 +281,15 @@ public class ApplicationArchive
zis.close();
}
// Saves the specified XML stream into app-specific directory.
private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
throws IOException {
File file = appFile(desc.name(), APP_XML);
createParentDirs(file);
write(stream, file);
}
// Saves the specified ZIP stream into a file under app-specific directory.
private void saveApplication(InputStream stream, ApplicationDescription desc)
throws IOException {
......
......@@ -30,8 +30,7 @@ import java.io.InputStream;
import java.util.Random;
import java.util.Set;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
import static org.onosproject.app.DefaultApplicationDescriptionTest.*;
public class ApplicationArchiveTest {
......@@ -64,43 +63,69 @@ public class ApplicationArchiveTest {
}
@Test
public void saveApp() throws IOException {
public void saveZippedApp() throws IOException {
InputStream stream = getClass().getResourceAsStream("app.zip");
ApplicationDescription app = aar.saveApplication(stream);
validate(app);
}
@Test
public void savePlainApp() throws IOException {
InputStream stream = getClass().getResourceAsStream("app.xml");
ApplicationDescription app = aar.saveApplication(stream);
validate(app);
}
@Test
public void loadApp() throws IOException {
saveApp();
saveZippedApp();
ApplicationDescription app = aar.getApplicationDescription(APP_NAME);
validate(app);
}
@Test
public void getAppNames() throws IOException {
saveApp();
saveZippedApp();
Set<String> names = aar.getApplicationNames();
assertEquals("incorrect names", ImmutableSet.of(APP_NAME), names);
}
@Test
public void purgeApp() throws IOException {
saveApp();
saveZippedApp();
aar.purgeApplication(APP_NAME);
assertEquals("incorrect names", ImmutableSet.<String>of(),
aar.getApplicationNames());
}
@Test
public void getAppStream() throws IOException {
saveApp();
public void getAppZipStream() throws IOException {
saveZippedApp();
InputStream stream = aar.getApplicationInputStream(APP_NAME);
byte[] orig = ByteStreams.toByteArray(getClass().getResourceAsStream("app.zip"));
byte[] loaded = ByteStreams.toByteArray(stream);
assertArrayEquals("incorrect stream", orig, loaded);
}
@Test
public void getAppXmlStream() throws IOException {
savePlainApp();
InputStream stream = aar.getApplicationInputStream(APP_NAME);
byte[] orig = ByteStreams.toByteArray(getClass().getResourceAsStream("app.xml"));
byte[] loaded = ByteStreams.toByteArray(stream);
assertArrayEquals("incorrect stream", orig, loaded);
}
@Test
public void active() throws IOException {
savePlainApp();
assertFalse("should not be active", aar.isActive(APP_NAME));
aar.setActive(APP_NAME);
assertTrue("should not be active", aar.isActive(APP_NAME));
aar.clearActive(APP_NAME);
assertFalse("should not be active", aar.isActive(APP_NAME));
}
@Test(expected = ApplicationException.class)
public void getBadAppDesc() throws IOException {
aar.getApplicationDescription("org.foo.BAD");
......@@ -111,4 +136,14 @@ public class ApplicationArchiveTest {
aar.getApplicationInputStream("org.foo.BAD");
}
@Test(expected = ApplicationException.class)
public void setBadActive() throws IOException {
aar.setActive("org.foo.BAD");
}
@Test(expected = ApplicationException.class)
public void purgeBadApp() throws IOException {
aar.purgeApplication("org.foo.BAD");
}
}
\ No newline at end of file
......
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2015 Open Networking Laboratory
~
......
......@@ -16,7 +16,8 @@
-->
<features xmlns="http://karaf.apache.org/xmlns/features/v1.2.0"
name="onos-@FEATURE-VERSION">
<repository>mvn:org.onosproject/onos-features/@ONOS-VERSION/xml/features</repository>
<repository>mvn:org.onosproject/onos-features/@ONOS-VERSION/xml/features
</repository>
<feature name="onos-thirdparty-base" version="@FEATURE-VERSION"
description="ONOS 3rd party dependencies">
......@@ -126,7 +127,7 @@
</feature>
<feature name="onos-null" version="@FEATURE-VERSION"
description="ONOS Null providers">
description="ONOS Null providers">
<feature>onos-api</feature>
<bundle>mvn:org.onosproject/onos-null-provider-device/@ONOS-VERSION</bundle>
......@@ -197,12 +198,12 @@
<feature>onos-api</feature>
<bundle>mvn:org.onosproject/onos-app-config/@ONOS-VERSION</bundle>
</feature>
<feature name="onos-app-optical" version="@FEATURE-VERSION"
<feature name="onos-app-optical" version="@FEATURE-VERSION"
description="ONOS optical network config">
<feature>onos-api</feature>
<bundle>mvn:org.onosproject/onos-app-optical/@ONOS-VERSION</bundle>
</feature>
</feature>
<feature name="onos-app-sdnip" version="@FEATURE-VERSION"
description="SDN-IP peering application">
......@@ -210,8 +211,8 @@
<feature>onos-app-proxyarp</feature>
<feature>onos-app-config</feature>
<bundle>mvn:org.onosproject/onos-app-sdnip/@ONOS-VERSION</bundle>
<bundle>mvn:org.onosproject/onos-app-routing-api/@ONOS-VERSION</bundle>
<bundle>mvn:org.onosproject/onos-app-routing/@ONOS-VERSION</bundle>
<bundle>mvn:org.onosproject/onos-app-routing-api/@ONOS-VERSION</bundle>
<bundle>mvn:org.onosproject/onos-app-routing/@ONOS-VERSION</bundle>
</feature>
<feature name="onos-app-calendar" version="@FEATURE-VERSION"
......
......@@ -14,7 +14,11 @@ export curl="curl -sS"
case $cmd in
list) $curl -X GET $URL;;
install) $curl -X POST $HDR $URL --data-binary @$app;;
install!) $curl -X POST $HDR $URL?activate=true --data-binary @$app;;
uninstall) $curl -X DELETE $URL/$app;;
activate) $curl -X POST $URL/$app/active;;
deactivate) $curl -X DELETE $URL/$app/active;;
*) echo "usage: onos-app {install|install!} <app-file>" >&2
echo " onos-app {activate|deactivate|uninstall} <app-name>" >&2
exit 1;;
esac
......
......@@ -182,7 +182,11 @@ public abstract class Tools {
* @throws java.io.IOException if unable to remove contents
*/
public static void removeDirectory(String path) throws IOException {
walkFileTree(Paths.get(path), new DirectoryDeleter());
DirectoryDeleter visitor = new DirectoryDeleter();
walkFileTree(Paths.get(path), visitor);
if (visitor.exception != null) {
throw visitor.exception;
}
}
/**
......@@ -194,11 +198,18 @@ public abstract class Tools {
* @throws java.io.IOException if unable to remove contents
*/
public static void removeDirectory(File dir) throws IOException {
walkFileTree(Paths.get(dir.getAbsolutePath()), new DirectoryDeleter());
DirectoryDeleter visitor = new DirectoryDeleter();
walkFileTree(Paths.get(dir.getAbsolutePath()), visitor);
if (visitor.exception != null) {
throw visitor.exception;
}
}
// Auxiliary path visitor for recursive directory structure removal.
private static class DirectoryDeleter extends SimpleFileVisitor<Path> {
private IOException exception;
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes)
throws IOException {
......@@ -218,9 +229,8 @@ public abstract class Tools {
@Override
public FileVisitResult visitFileFailed(Path file, IOException ioe)
throws IOException {
log.warn("Unable to delete file {}", file);
log.warn("Boom", ioe);
return FileVisitResult.CONTINUE;
this.exception = ioe;
return FileVisitResult.TERMINATE;
}
}
......@@ -253,8 +263,8 @@ public abstract class Tools {
dst.getAbsolutePath()));
}
public static class DirectoryCopier extends SimpleFileVisitor<Path> {
// Auxiliary path visitor for recursive directory structure copying.
private static class DirectoryCopier extends SimpleFileVisitor<Path> {
private Path src;
private Path dst;
private StandardCopyOption copyOption = StandardCopyOption.REPLACE_EXISTING;
......
......@@ -21,11 +21,13 @@ import org.onosproject.core.ApplicationId;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.InputStream;
......@@ -55,9 +57,14 @@ public class ApplicationsWebResource extends AbstractWebResource {
@POST
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.APPLICATION_JSON)
public Response installApplication(InputStream stream) {
public Response installApplication(@QueryParam("activate")
@DefaultValue("false") boolean activate,
InputStream stream) {
ApplicationAdminService service = get(ApplicationAdminService.class);
Application app = service.install(stream);
if (activate) {
service.activate(app.id());
}
return ok(codec(Application.class).encode(app, this)).build();
}
......