Thomas Vachuska
Committed by Ray Milkey

ONOS-785 Adding distributed store for apps & app admin CLIs

Change-Id: Ia7639f3258fca2a18ba513f0c95de0ab8ea7ceee
1 +/*
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 + */
16 +package org.onosproject.cli.app;
17 +
18 +import org.apache.karaf.shell.commands.Argument;
19 +import org.apache.karaf.shell.commands.Command;
20 +import org.onosproject.app.ApplicationAdminService;
21 +import org.onosproject.cli.AbstractShellCommand;
22 +import org.onosproject.core.ApplicationId;
23 +
24 +/**
25 + * Activates an installed application.
26 + */
27 +@Command(scope = "onos", name = "app-activate",
28 + description = "Activates an installed application")
29 +public class ApplicationActivateCommand extends AbstractShellCommand {
30 +
31 + @Argument(index = 0, name = "name", description = "Application name",
32 + required = true, multiValued = false)
33 + String name = null;
34 +
35 + @Override
36 + protected void execute() {
37 + ApplicationAdminService service = get(ApplicationAdminService.class);
38 + ApplicationId appId = service.getId(name);
39 + if (appId != null) {
40 + service.activate(appId);
41 + } else {
42 + print("No such application: %s", name);
43 + }
44 + }
45 +
46 +}
1 +/*
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 + */
16 +package org.onosproject.cli.app;
17 +
18 +import org.apache.karaf.shell.commands.Argument;
19 +import org.apache.karaf.shell.commands.Command;
20 +import org.onosproject.app.ApplicationAdminService;
21 +import org.onosproject.cli.AbstractShellCommand;
22 +import org.onosproject.core.ApplicationId;
23 +
24 +/**
25 + * Deactivates an installed application.
26 + */
27 +@Command(scope = "onos", name = "app-deactivate",
28 + description = "Deactivates an installed application")
29 +public class ApplicationDeactivateCommand extends AbstractShellCommand {
30 +
31 + @Argument(index = 0, name = "name", description = "Application name",
32 + required = true, multiValued = false)
33 + String name = null;
34 +
35 + @Override
36 + protected void execute() {
37 + ApplicationAdminService service = get(ApplicationAdminService.class);
38 + ApplicationId appId = service.getId(name);
39 + if (appId != null) {
40 + service.deactivate(appId);
41 + } else {
42 + print("No such application: %s", name);
43 + }
44 + }
45 +
46 +}
...@@ -13,12 +13,14 @@ ...@@ -13,12 +13,14 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 -package org.onosproject.cli; 16 +package org.onosproject.cli.app;
17 17
18 import com.fasterxml.jackson.databind.JsonNode; 18 import com.fasterxml.jackson.databind.JsonNode;
19 import com.fasterxml.jackson.databind.ObjectMapper; 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 import com.fasterxml.jackson.databind.node.ArrayNode; 20 import com.fasterxml.jackson.databind.node.ArrayNode;
21 import org.apache.karaf.shell.commands.Command; 21 import org.apache.karaf.shell.commands.Command;
22 +import org.onosproject.cli.AbstractShellCommand;
23 +import org.onosproject.cli.Comparators;
22 import org.onosproject.core.ApplicationId; 24 import org.onosproject.core.ApplicationId;
23 import org.onosproject.core.CoreService; 25 import org.onosproject.core.CoreService;
24 26
...@@ -30,7 +32,7 @@ import static com.google.common.collect.Lists.newArrayList; ...@@ -30,7 +32,7 @@ import static com.google.common.collect.Lists.newArrayList;
30 /** 32 /**
31 * Lists application ID information. 33 * Lists application ID information.
32 */ 34 */
33 -@Command(scope = "onos", name = "apps", 35 +@Command(scope = "onos", name = "app-ids",
34 description = "Lists application ID information") 36 description = "Lists application ID information")
35 public class ApplicationIdListCommand extends AbstractShellCommand { 37 public class ApplicationIdListCommand extends AbstractShellCommand {
36 38
......
1 +/*
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 + */
16 +package org.onosproject.cli.app;
17 +
18 +import org.apache.karaf.shell.console.Completer;
19 +import org.apache.karaf.shell.console.completer.StringsCompleter;
20 +import org.onosproject.app.ApplicationService;
21 +import org.onosproject.cli.AbstractShellCommand;
22 +import org.onosproject.core.Application;
23 +
24 +import java.util.Iterator;
25 +import java.util.List;
26 +import java.util.SortedSet;
27 +
28 +/**
29 + * Application name completer.
30 + */
31 +public class ApplicationNameCompleter implements Completer {
32 + @Override
33 + public int complete(String buffer, int cursor, List<String> candidates) {
34 + // Delegate string completer
35 + StringsCompleter delegate = new StringsCompleter();
36 +
37 + // Fetch our service and feed it's offerings to the string completer
38 + ApplicationService service = AbstractShellCommand.get(ApplicationService.class);
39 + Iterator<Application> it = service.getApplications().iterator();
40 + SortedSet<String> strings = delegate.getStrings();
41 + while (it.hasNext()) {
42 + strings.add(it.next().id().name());
43 + }
44 +
45 + // Now let the completer do the work for figuring out what to offer.
46 + return delegate.complete(buffer, cursor, candidates);
47 + }
48 +
49 +}
1 +/*
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 + */
16 +package org.onosproject.cli.app;
17 +
18 +import org.apache.karaf.shell.commands.Argument;
19 +import org.apache.karaf.shell.commands.Command;
20 +import org.onosproject.app.ApplicationAdminService;
21 +import org.onosproject.cli.AbstractShellCommand;
22 +import org.onosproject.core.ApplicationId;
23 +
24 +/**
25 + * Uninstalls an application.
26 + */
27 +@Command(scope = "onos", name = "app-uninstall",
28 + description = "Uninstalls an application")
29 +public class ApplicationUninstallCommand extends AbstractShellCommand {
30 +
31 + @Argument(index = 0, name = "name", description = "Application name",
32 + required = true, multiValued = false)
33 + String name = null;
34 +
35 + @Override
36 + protected void execute() {
37 + ApplicationAdminService service = get(ApplicationAdminService.class);
38 + ApplicationId appId = service.getId(name);
39 + if (appId != null) {
40 + service.uninstall(appId);
41 + } else {
42 + print("No such application: %s", name);
43 + }
44 + }
45 +
46 +}
...@@ -13,32 +13,33 @@ ...@@ -13,32 +13,33 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 -package org.onosproject.cli; 16 +package org.onosproject.cli.app;
17 17
18 import org.apache.karaf.shell.commands.Command; 18 import org.apache.karaf.shell.commands.Command;
19 +import org.onosproject.app.ApplicationService;
20 +import org.onosproject.cli.AbstractShellCommand;
21 +import org.onosproject.core.Application;
19 22
20 -import java.io.BufferedReader; 23 +import static org.onosproject.app.ApplicationState.ACTIVE;
21 -import java.io.IOException;
22 -import java.io.InputStreamReader;
23 24
24 /** 25 /**
25 - * Lists application ID information. 26 + * Lists application information.
26 */ 27 */
27 -@Command(scope = "onos", name = "app-install", 28 +@Command(scope = "onos", name = "apps",
28 - description = "Lists application ID information") 29 + description = "Lists application information")
29 -public class ApplicationInstallCommand extends AbstractShellCommand { 30 +public class ApplicationsListCommand extends AbstractShellCommand {
31 +
32 + private static final String FMT =
33 + "%s id=%d, name=%s, version=%s, origin=%s, description=%s, " +
34 + "features=%s, featuresRepo=%s, permissions=%s";
30 35
31 @Override 36 @Override
32 protected void execute() { 37 protected void execute() {
33 - // FIXME: merely an experiment for now 38 + ApplicationService service = get(ApplicationService.class);
34 - try (InputStreamReader isr = new InputStreamReader(System.in); 39 + for (Application app : service.getApplications()) {
35 - BufferedReader br = new BufferedReader(isr)) { 40 + print(FMT, service.getState(app.id()) == ACTIVE ? "*" : " ",
36 - String line; 41 + app.id().id(), app.id().name(), app.version(), app.origin(),
37 - while ((line = br.readLine()) != null) { 42 + app.description(), app.features(), app.featuresRepo(), app.permissions());
38 - print("%s", line.toUpperCase());
39 - }
40 - } catch (IOException e) {
41 - e.printStackTrace();
42 } 43 }
43 } 44 }
44 45
......
...@@ -21,7 +21,31 @@ ...@@ -21,7 +21,31 @@
21 </command> 21 </command>
22 22
23 <command> 23 <command>
24 - <action class="org.onosproject.cli.ApplicationInstallCommand"/> 24 + <action class="org.onosproject.cli.app.ApplicationsListCommand"/>
25 + </command>
26 +
27 + <command>
28 + <action class="org.onosproject.cli.app.ApplicationActivateCommand"/>
29 + <completers>
30 + <ref component-id="appNameCompleter"/>
31 + <null/>
32 + </completers>
33 + </command>
34 +
35 + <command>
36 + <action class="org.onosproject.cli.app.ApplicationDeactivateCommand"/>
37 + <completers>
38 + <ref component-id="appNameCompleter"/>
39 + <null/>
40 + </completers>
41 + </command>
42 +
43 + <command>
44 + <action class="org.onosproject.cli.app.ApplicationUninstallCommand"/>
45 + <completers>
46 + <ref component-id="appNameCompleter"/>
47 + <null/>
48 + </completers>
25 </command> 49 </command>
26 50
27 <command> 51 <command>
...@@ -77,7 +101,7 @@ ...@@ -77,7 +101,7 @@
77 <action class="org.onosproject.cli.BalanceMastersCommand"/> 101 <action class="org.onosproject.cli.BalanceMastersCommand"/>
78 </command> 102 </command>
79 <command> 103 <command>
80 - <action class="org.onosproject.cli.ApplicationIdListCommand"/> 104 + <action class="org.onosproject.cli.app.ApplicationIdListCommand"/>
81 </command> 105 </command>
82 106
83 <command> 107 <command>
...@@ -275,6 +299,7 @@ ...@@ -275,6 +299,7 @@
275 </command> 299 </command>
276 </command-bundle> 300 </command-bundle>
277 301
302 + <bean id="appNameCompleter" class="org.onosproject.cli.app.ApplicationNameCompleter"/>
278 <bean id="nodeIdCompleter" class="org.onosproject.cli.NodeIdCompleter"/> 303 <bean id="nodeIdCompleter" class="org.onosproject.cli.NodeIdCompleter"/>
279 <bean id="deviceIdCompleter" class="org.onosproject.cli.net.DeviceIdCompleter"/> 304 <bean id="deviceIdCompleter" class="org.onosproject.cli.net.DeviceIdCompleter"/>
280 <bean id="clusterIdCompleter" class="org.onosproject.cli.net.ClusterIdCompleter"/> 305 <bean id="clusterIdCompleter" class="org.onosproject.cli.net.ClusterIdCompleter"/>
......
...@@ -39,6 +39,7 @@ import java.io.FileNotFoundException; ...@@ -39,6 +39,7 @@ import java.io.FileNotFoundException;
39 import java.io.IOException; 39 import java.io.IOException;
40 import java.io.InputStream; 40 import java.io.InputStream;
41 import java.net.URI; 41 import java.net.URI;
42 +import java.nio.file.NoSuchFileException;
42 import java.util.Set; 43 import java.util.Set;
43 import java.util.zip.ZipEntry; 44 import java.util.zip.ZipEntry;
44 import java.util.zip.ZipInputStream; 45 import java.util.zip.ZipInputStream;
...@@ -63,17 +64,25 @@ public class ApplicationArchive ...@@ -63,17 +64,25 @@ public class ApplicationArchive
63 64
64 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class); 65 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
65 private static final String APP_XML = "app.xml"; 66 private static final String APP_XML = "app.xml";
67 + private static final String M2_PREFIX = "m2";
68 +
69 + private static final String KARAF_ROOT = ".";
70 + private static final String M2_ROOT = "system/";
66 private static final String APPS_ROOT = "data/apps/"; 71 private static final String APPS_ROOT = "data/apps/";
67 72
68 - private File appsDir = new File(APPS_ROOT); 73 + private File karafRoot = new File(KARAF_ROOT);
74 + private File m2Dir = new File(karafRoot, M2_ROOT);
75 + private File appsDir = new File(karafRoot, APPS_ROOT);
69 76
70 /** 77 /**
71 * Sets the root directory where application artifacts are kept. 78 * Sets the root directory where application artifacts are kept.
72 * 79 *
73 * @param appsRoot top-level applications directory path 80 * @param appsRoot top-level applications directory path
74 */ 81 */
75 - protected void setAppsRoot(String appsRoot) { 82 + protected void setRootPath(String appsRoot) {
76 - this.appsDir = new File(appsRoot); 83 + this.karafRoot = new File(appsRoot);
84 + this.appsDir = new File(karafRoot, APPS_ROOT);
85 + this.m2Dir = new File(karafRoot, M2_ROOT);
77 } 86 }
78 87
79 /** 88 /**
...@@ -81,8 +90,8 @@ public class ApplicationArchive ...@@ -81,8 +90,8 @@ public class ApplicationArchive
81 * 90 *
82 * @return top-level applications directory path 91 * @return top-level applications directory path
83 */ 92 */
84 - protected String getAppsRoot() { 93 + protected String getRootPath() {
85 - return appsDir.getPath(); 94 + return karafRoot.getPath();
86 } 95 }
87 96
88 /** 97 /**
...@@ -238,10 +247,20 @@ public class ApplicationArchive ...@@ -238,10 +247,20 @@ public class ApplicationArchive
238 } 247 }
239 248
240 // Installs application artifacts into M2 repository. 249 // Installs application artifacts into M2 repository.
241 - private void installArtifacts(ApplicationDescription desc) { 250 + private void installArtifacts(ApplicationDescription desc) throws IOException {
242 - // FIXME: implement M2 repository copy 251 + try {
252 + Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
253 + } catch (NoSuchFileException e) {
254 + log.debug("Application {} has no M2 artifacts", desc.name());
255 + }
243 } 256 }
244 257
258 + /**
259 + * Marks the app as active by creating token file in the app directory.
260 + *
261 + * @param appName application name
262 + * @return true if file was created
263 + */
245 protected boolean setActive(String appName) { 264 protected boolean setActive(String appName) {
246 try { 265 try {
247 return appFile(appName, "active").createNewFile(); 266 return appFile(appName, "active").createNewFile();
...@@ -250,10 +269,22 @@ public class ApplicationArchive ...@@ -250,10 +269,22 @@ public class ApplicationArchive
250 } 269 }
251 } 270 }
252 271
272 + /**
273 + * Clears the app as active by deleting token file in the app directory.
274 + *
275 + * @param appName application name
276 + * @return true if file was deleted
277 + */
253 protected boolean clearActive(String appName) { 278 protected boolean clearActive(String appName) {
254 return appFile(appName, "active").delete(); 279 return appFile(appName, "active").delete();
255 } 280 }
256 281
282 + /**
283 + * Indicates whether the app was marked as active by checking for token file.
284 + *
285 + * @param appName application name
286 + * @return true if the app is marked as active
287 + */
257 protected boolean isActive(String appName) { 288 protected boolean isActive(String appName) {
258 return appFile(appName, "active").exists(); 289 return appFile(appName, "active").exists();
259 } 290 }
......
...@@ -17,13 +17,17 @@ package org.onosproject.common.app; ...@@ -17,13 +17,17 @@ package org.onosproject.common.app;
17 17
18 import com.google.common.collect.ImmutableSet; 18 import com.google.common.collect.ImmutableSet;
19 import com.google.common.io.ByteStreams; 19 import com.google.common.io.ByteStreams;
20 +import org.junit.After;
20 import org.junit.Before; 21 import org.junit.Before;
21 import org.junit.Test; 22 import org.junit.Test;
23 +import org.onlab.util.Tools;
22 import org.onosproject.app.ApplicationDescription; 24 import org.onosproject.app.ApplicationDescription;
23 import org.onosproject.app.ApplicationException; 25 import org.onosproject.app.ApplicationException;
24 26
27 +import java.io.File;
25 import java.io.IOException; 28 import java.io.IOException;
26 import java.io.InputStream; 29 import java.io.InputStream;
30 +import java.util.Random;
27 import java.util.Set; 31 import java.util.Set;
28 32
29 import static org.junit.Assert.assertArrayEquals; 33 import static org.junit.Assert.assertArrayEquals;
...@@ -32,13 +36,20 @@ import static org.onosproject.app.DefaultApplicationDescriptionTest.*; ...@@ -32,13 +36,20 @@ import static org.onosproject.app.DefaultApplicationDescriptionTest.*;
32 36
33 public class ApplicationArchiveTest { 37 public class ApplicationArchiveTest {
34 38
35 - static final String ROOT = "/tmp/app-junit"; 39 + static final String ROOT = "/tmp/app-junit/" + new Random().nextInt();
36 40
37 private ApplicationArchive aar = new ApplicationArchive(); 41 private ApplicationArchive aar = new ApplicationArchive();
38 42
39 @Before 43 @Before
40 public void setUp() { 44 public void setUp() {
41 - aar.setAppsRoot(ROOT); 45 + aar.setRootPath(ROOT);
46 + }
47 +
48 + @After
49 + public void tearDown() throws IOException {
50 + if (new File(aar.getRootPath()).exists()) {
51 + Tools.removeDirectory(aar.getRootPath());
52 + }
42 } 53 }
43 54
44 private void validate(ApplicationDescription app) { 55 private void validate(ApplicationDescription app) {
...@@ -77,7 +88,8 @@ public class ApplicationArchiveTest { ...@@ -77,7 +88,8 @@ public class ApplicationArchiveTest {
77 public void purgeApp() throws IOException { 88 public void purgeApp() throws IOException {
78 saveApp(); 89 saveApp();
79 aar.purgeApplication(APP_NAME); 90 aar.purgeApplication(APP_NAME);
80 - assertEquals("incorrect names", ImmutableSet.of(), aar.getApplicationNames()); 91 + assertEquals("incorrect names", ImmutableSet.<String>of(),
92 + aar.getApplicationNames());
81 } 93 }
82 94
83 @Test 95 @Test
......
...@@ -21,6 +21,7 @@ import org.apache.felix.scr.annotations.Deactivate; ...@@ -21,6 +21,7 @@ import org.apache.felix.scr.annotations.Deactivate;
21 import org.apache.felix.scr.annotations.Reference; 21 import org.apache.felix.scr.annotations.Reference;
22 import org.apache.felix.scr.annotations.ReferenceCardinality; 22 import org.apache.felix.scr.annotations.ReferenceCardinality;
23 import org.apache.felix.scr.annotations.Service; 23 import org.apache.felix.scr.annotations.Service;
24 +import org.apache.karaf.features.Feature;
24 import org.apache.karaf.features.FeaturesService; 25 import org.apache.karaf.features.FeaturesService;
25 import org.onosproject.app.ApplicationAdminService; 26 import org.onosproject.app.ApplicationAdminService;
26 import org.onosproject.app.ApplicationEvent; 27 import org.onosproject.app.ApplicationEvent;
...@@ -202,14 +203,20 @@ public class ApplicationManager implements ApplicationService, ApplicationAdminS ...@@ -202,14 +203,20 @@ public class ApplicationManager implements ApplicationService, ApplicationAdminS
202 203
203 private void installAppFeatures(Application app) throws Exception { 204 private void installAppFeatures(Application app) throws Exception {
204 for (String name : app.features()) { 205 for (String name : app.features()) {
206 + Feature feature = featuresService.getFeature(name);
207 + if (!featuresService.isInstalled(feature)) {
205 featuresService.installFeature(name); 208 featuresService.installFeature(name);
206 } 209 }
207 } 210 }
211 + }
208 212
209 private void uninstallAppFeatures(Application app) throws Exception { 213 private void uninstallAppFeatures(Application app) throws Exception {
210 for (String name : app.features()) { 214 for (String name : app.features()) {
215 + Feature feature = featuresService.getFeature(name);
216 + if (featuresService.isInstalled(feature)) {
211 featuresService.uninstallFeature(name); 217 featuresService.uninstallFeature(name);
212 } 218 }
213 } 219 }
220 + }
214 221
215 } 222 }
......
...@@ -43,6 +43,11 @@ ...@@ -43,6 +43,11 @@
43 <artifactId>onlab-netty</artifactId> 43 <artifactId>onlab-netty</artifactId>
44 <version>${project.version}</version> 44 <version>${project.version}</version>
45 </dependency> 45 </dependency>
46 +
47 + <dependency>
48 + <groupId>org.onosproject</groupId>
49 + <artifactId>onos-core-common</artifactId>
50 + </dependency>
46 <!-- 51 <!--
47 <dependency> 52 <dependency>
48 <groupId>net.kuujo.copycat</groupId> 53 <groupId>net.kuujo.copycat</groupId>
......
1 +/*
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 + */
16 +package org.onosproject.store.app;
17 +
18 +import com.google.common.collect.ImmutableSet;
19 +import com.google.common.util.concurrent.ListenableFuture;
20 +import org.apache.felix.scr.annotations.Activate;
21 +import org.apache.felix.scr.annotations.Component;
22 +import org.apache.felix.scr.annotations.Deactivate;
23 +import org.apache.felix.scr.annotations.Reference;
24 +import org.apache.felix.scr.annotations.ReferenceCardinality;
25 +import org.apache.felix.scr.annotations.Service;
26 +import org.onlab.util.KryoNamespace;
27 +import org.onosproject.app.ApplicationDescription;
28 +import org.onosproject.app.ApplicationEvent;
29 +import org.onosproject.app.ApplicationException;
30 +import org.onosproject.app.ApplicationState;
31 +import org.onosproject.app.ApplicationStore;
32 +import org.onosproject.cluster.ClusterService;
33 +import org.onosproject.cluster.ControllerNode;
34 +import org.onosproject.common.app.ApplicationArchive;
35 +import org.onosproject.core.Application;
36 +import org.onosproject.core.ApplicationId;
37 +import org.onosproject.core.ApplicationIdStore;
38 +import org.onosproject.core.DefaultApplication;
39 +import org.onosproject.core.Permission;
40 +import org.onosproject.store.cluster.messaging.ClusterCommunicationService;
41 +import org.onosproject.store.cluster.messaging.ClusterMessage;
42 +import org.onosproject.store.cluster.messaging.ClusterMessageHandler;
43 +import org.onosproject.store.cluster.messaging.MessageSubject;
44 +import org.onosproject.store.impl.EventuallyConsistentMap;
45 +import org.onosproject.store.impl.EventuallyConsistentMapEvent;
46 +import org.onosproject.store.impl.EventuallyConsistentMapImpl;
47 +import org.onosproject.store.impl.EventuallyConsistentMapListener;
48 +import org.onosproject.store.impl.WallclockClockManager;
49 +import org.onosproject.store.serializers.KryoNamespaces;
50 +import org.slf4j.Logger;
51 +
52 +import java.io.ByteArrayInputStream;
53 +import java.io.IOException;
54 +import java.io.InputStream;
55 +import java.util.HashMap;
56 +import java.util.Map;
57 +import java.util.Set;
58 +import java.util.concurrent.CountDownLatch;
59 +import java.util.concurrent.Executors;
60 +import java.util.concurrent.ScheduledExecutorService;
61 +
62 +import static com.google.common.io.ByteStreams.toByteArray;
63 +import static java.util.concurrent.TimeUnit.MILLISECONDS;
64 +import static org.onlab.util.Tools.namedThreads;
65 +import static org.onosproject.app.ApplicationEvent.Type.*;
66 +import static org.onosproject.store.app.GossipApplicationStore.InternalState.*;
67 +import static org.onosproject.store.impl.EventuallyConsistentMapEvent.Type.PUT;
68 +import static org.onosproject.store.impl.EventuallyConsistentMapEvent.Type.REMOVE;
69 +import static org.slf4j.LoggerFactory.getLogger;
70 +
71 +/**
72 + * Manages inventory of applications in a distributed data store that uses
73 + * optimistic replication and gossip based anti-entropy techniques.
74 + */
75 +@Component(immediate = true)
76 +@Service
77 +public class GossipApplicationStore extends ApplicationArchive
78 + implements ApplicationStore {
79 +
80 + private final Logger log = getLogger(getClass());
81 +
82 + private static final MessageSubject APP_BITS_REQUEST = new MessageSubject("app-bits-request");
83 +
84 + private static final int FETCH_TIMEOUT_MS = 10_000;
85 + private static final int LOAD_TIMEOUT_MS = 5_000;
86 +
87 + public enum InternalState {
88 + INSTALLED, ACTIVATED, DEACTIVATED
89 + }
90 +
91 + private final ScheduledExecutorService executor =
92 + Executors.newSingleThreadScheduledExecutor(namedThreads("onos-app-store"));
93 +
94 + private EventuallyConsistentMap<ApplicationId, Application> apps;
95 + private EventuallyConsistentMap<Application, InternalState> states;
96 + private EventuallyConsistentMap<Application, Set<Permission>> permissions;
97 +
98 + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
99 + protected ClusterCommunicationService clusterCommunicator;
100 +
101 + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
102 + protected ClusterService clusterService;
103 +
104 + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
105 + protected ApplicationIdStore idStore;
106 +
107 + @Activate
108 + public void activate() {
109 + KryoNamespace.Builder intentSerializer = KryoNamespace.newBuilder()
110 + .register(KryoNamespaces.API)
111 + .register(InternalState.class);
112 +
113 + clusterCommunicator.addSubscriber(APP_BITS_REQUEST, new InternalBitServer());
114 +
115 + apps = new EventuallyConsistentMapImpl<>("apps", clusterService,
116 + clusterCommunicator,
117 + intentSerializer,
118 + new WallclockClockManager<>());
119 +
120 + states = new EventuallyConsistentMapImpl<>("app-states",
121 + clusterService,
122 + clusterCommunicator,
123 + intentSerializer,
124 + new WallclockClockManager<>());
125 + states.addListener(new InternalAppStatesListener());
126 +
127 + permissions = new EventuallyConsistentMapImpl<>("app-permissions",
128 + clusterService,
129 + clusterCommunicator,
130 + intentSerializer,
131 + new WallclockClockManager<>());
132 +
133 + // FIXME: figure out load from disk; this will require resolving the dual authority problem
134 +
135 + executor.schedule(this::pruneUninstalledApps, LOAD_TIMEOUT_MS, MILLISECONDS);
136 +
137 + log.info("Started");
138 + }
139 +
140 + private void loadFromDisk() {
141 + for (String name : getApplicationNames()) {
142 + create(getApplicationDescription(name));
143 + // load app permissions
144 + }
145 + }
146 +
147 + @Deactivate
148 + public void deactivate() {
149 + apps.destroy();
150 + states.destroy();
151 + permissions.destroy();
152 + log.info("Stopped");
153 + }
154 +
155 + @Override
156 + public Set<Application> getApplications() {
157 + return ImmutableSet.copyOf(apps.values());
158 + }
159 +
160 + @Override
161 + public ApplicationId getId(String name) {
162 + return idStore.getAppId(name);
163 + }
164 +
165 + @Override
166 + public Application getApplication(ApplicationId appId) {
167 + return apps.get(appId);
168 + }
169 +
170 + @Override
171 + public ApplicationState getState(ApplicationId appId) {
172 + Application app = apps.get(appId);
173 + InternalState s = app == null ? null : states.get(app);
174 + return s == null ? null : s == ACTIVATED ?
175 + ApplicationState.ACTIVE : ApplicationState.INSTALLED;
176 + }
177 +
178 + @Override
179 + public Application create(InputStream appDescStream) {
180 + ApplicationDescription appDesc = saveApplication(appDescStream);
181 + return create(appDesc);
182 + }
183 +
184 + private Application create(ApplicationDescription appDesc) {
185 + Application app = registerApp(appDesc);
186 + apps.put(app.id(), app);
187 + states.put(app, INSTALLED);
188 + return app;
189 + }
190 +
191 + @Override
192 + public void remove(ApplicationId appId) {
193 + Application app = apps.get(appId);
194 + if (app != null) {
195 + apps.remove(appId);
196 + states.remove(app);
197 + permissions.remove(app);
198 + }
199 + }
200 +
201 + @Override
202 + public void activate(ApplicationId appId) {
203 + Application app = apps.get(appId);
204 + if (app != null) {
205 + states.put(app, ACTIVATED);
206 + }
207 + }
208 +
209 + @Override
210 + public void deactivate(ApplicationId appId) {
211 + Application app = apps.get(appId);
212 + if (app != null) {
213 + states.put(app, DEACTIVATED);
214 + }
215 + }
216 +
217 + @Override
218 + public Set<Permission> getPermissions(ApplicationId appId) {
219 + Application app = apps.get(appId);
220 + return app != null ? permissions.get(app) : null;
221 + }
222 +
223 + @Override
224 + public void setPermissions(ApplicationId appId, Set<Permission> permissions) {
225 + Application app = getApplication(appId);
226 + if (app != null) {
227 + this.permissions.put(app, permissions);
228 + delegate.notify(new ApplicationEvent(APP_PERMISSIONS_CHANGED, app));
229 + }
230 + }
231 +
232 + /**
233 + * Listener to application state distributed map changes.
234 + */
235 + private final class InternalAppStatesListener
236 + implements EventuallyConsistentMapListener<Application, InternalState> {
237 + @Override
238 + public void event(EventuallyConsistentMapEvent<Application, InternalState> event) {
239 + Application app = event.key();
240 + InternalState state = event.value();
241 +
242 + if (event.type() == PUT) {
243 + if (state == INSTALLED) {
244 + fetchBitsIfNeeded(app);
245 + delegate.notify(new ApplicationEvent(APP_INSTALLED, app));
246 +
247 + } else if (state == ACTIVATED) {
248 + installAppIfNeeded(app);
249 + setActive(app.id().name());
250 + delegate.notify(new ApplicationEvent(APP_ACTIVATED, app));
251 +
252 + } else if (state == DEACTIVATED) {
253 + clearActive(app.id().name());
254 + delegate.notify(new ApplicationEvent(APP_DEACTIVATED, app));
255 + }
256 + } else if (event.type() == REMOVE) {
257 + delegate.notify(new ApplicationEvent(APP_UNINSTALLED, app));
258 + purgeApplication(app.id().name());
259 + }
260 + }
261 + }
262 +
263 + /**
264 + * Determines if the application bits are available locally.
265 + */
266 + private boolean appBitsAvailable(Application app) {
267 + try {
268 + ApplicationDescription appDesc = getApplicationDescription(app.id().name());
269 + return appDesc.version().equals(app.version());
270 + } catch (ApplicationException e) {
271 + return false;
272 + }
273 + }
274 +
275 + /**
276 + * Fetches the bits from the cluster peers if necessary.
277 + */
278 + private void fetchBitsIfNeeded(Application app) {
279 + if (!appBitsAvailable(app)) {
280 + fetchBits(app);
281 + }
282 + }
283 +
284 + /**
285 + * Installs the application if necessary from the application peers.
286 + */
287 + private void installAppIfNeeded(Application app) {
288 + if (!appBitsAvailable(app)) {
289 + fetchBits(app);
290 + delegate.notify(new ApplicationEvent(APP_INSTALLED, app));
291 + }
292 + }
293 +
294 + /**
295 + * Fetches the bits from the cluster peers.
296 + */
297 + private void fetchBits(Application app) {
298 + ControllerNode localNode = clusterService.getLocalNode();
299 + ClusterMessage message = new ClusterMessage(localNode.id(), APP_BITS_REQUEST,
300 + app.id().name().getBytes());
301 + Map<ControllerNode, ListenableFuture<byte[]>> futures = new HashMap<>();
302 + CountDownLatch latch = new CountDownLatch(1);
303 +
304 + // FIXME: send message with name & version to make sure we don't get served old bits
305 +
306 + log.info("Downloading bits for application {}", app.id().name());
307 + for (ControllerNode node : clusterService.getNodes()) {
308 + try {
309 + ListenableFuture<byte[]> future = clusterCommunicator.sendAndReceive(message, node.id());
310 + future.addListener(new InternalBitListener(app, node, future, latch), executor);
311 + } catch (IOException e) {
312 + log.debug("Unable to request bits for application {} from node {}",
313 + app.id().name(), node.id());
314 + }
315 + }
316 +
317 + try {
318 + if (!latch.await(FETCH_TIMEOUT_MS, MILLISECONDS)) {
319 + log.warn("Unable to fetch bits for application {}", app.id().name());
320 + }
321 + } catch (InterruptedException e) {
322 + log.warn("Interrupted while fetching bits for application {}", app.id().name());
323 + }
324 + }
325 +
326 + /**
327 + * Responder to requests for application bits.
328 + */
329 + private class InternalBitServer implements ClusterMessageHandler {
330 + @Override
331 + public void handle(ClusterMessage message) {
332 + String name = new String(message.payload());
333 + try {
334 + message.respond(toByteArray(getApplicationInputStream(name)));
335 + } catch (Exception e) {
336 + log.debug("Unable to read bits for application {}", name);
337 + }
338 + }
339 + }
340 +
341 + /**
342 + * Processes completed fetch requests.
343 + */
344 + private class InternalBitListener implements Runnable {
345 + private final Application app;
346 + private final ControllerNode node;
347 + private final ListenableFuture<byte[]> future;
348 + private final CountDownLatch latch;
349 +
350 + public InternalBitListener(Application app, ControllerNode node,
351 + ListenableFuture<byte[]> future, CountDownLatch latch) {
352 + this.app = app;
353 + this.node = node;
354 + this.future = future;
355 + this.latch = latch;
356 + }
357 +
358 + @Override
359 + public void run() {
360 + if (latch.getCount() > 0 && !future.isCancelled()) {
361 + try {
362 + byte[] bits = future.get(1, MILLISECONDS);
363 + saveApplication(new ByteArrayInputStream(bits));
364 + log.info("Downloaded bits for application {} from node {}",
365 + app.id().name(), node.id());
366 + latch.countDown();
367 + } catch (Exception e) {
368 + log.warn("Unable to fetch bits for application {} from node {}",
369 + app.id().name(), node.id());
370 + }
371 + }
372 + }
373 + }
374 +
375 + /**
376 + * Prunes applications which are not in the map, but are on disk.
377 + */
378 + private void pruneUninstalledApps() {
379 + for (String name : getApplicationNames()) {
380 + if (getApplication(getId(name)) == null) {
381 + Application app = registerApp(getApplicationDescription(name));
382 + delegate.notify(new ApplicationEvent(APP_UNINSTALLED, app));
383 + purgeApplication(app.id().name());
384 + }
385 + }
386 + }
387 +
388 + /**
389 + * Produces a registered application from the supplied description.
390 + */
391 + private Application registerApp(ApplicationDescription appDesc) {
392 + ApplicationId appId = idStore.registerApplication(appDesc.name());
393 + return new DefaultApplication(appId, appDesc.version(), appDesc.description(),
394 + appDesc.origin(), appDesc.permissions(),
395 + appDesc.featuresRepo(), appDesc.features());
396 + }
397 +}
398 +
1 +/*
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 + */
16 +
17 +/**
18 + * Implementation of distributed applications store.
19 + */
20 +package org.onosproject.store.app;
...@@ -28,14 +28,17 @@ import org.onlab.packet.IpPrefix; ...@@ -28,14 +28,17 @@ import org.onlab.packet.IpPrefix;
28 import org.onlab.packet.MacAddress; 28 import org.onlab.packet.MacAddress;
29 import org.onlab.packet.VlanId; 29 import org.onlab.packet.VlanId;
30 import org.onlab.util.KryoNamespace; 30 import org.onlab.util.KryoNamespace;
31 +import org.onosproject.app.ApplicationState;
31 import org.onosproject.cluster.ControllerNode; 32 import org.onosproject.cluster.ControllerNode;
32 import org.onosproject.cluster.DefaultControllerNode; 33 import org.onosproject.cluster.DefaultControllerNode;
33 import org.onosproject.cluster.Leadership; 34 import org.onosproject.cluster.Leadership;
34 import org.onosproject.cluster.LeadershipEvent; 35 import org.onosproject.cluster.LeadershipEvent;
35 import org.onosproject.cluster.NodeId; 36 import org.onosproject.cluster.NodeId;
36 import org.onosproject.cluster.RoleInfo; 37 import org.onosproject.cluster.RoleInfo;
38 +import org.onosproject.core.DefaultApplication;
37 import org.onosproject.core.DefaultApplicationId; 39 import org.onosproject.core.DefaultApplicationId;
38 import org.onosproject.core.DefaultGroupId; 40 import org.onosproject.core.DefaultGroupId;
41 +import org.onosproject.core.Version;
39 import org.onosproject.mastership.MastershipTerm; 42 import org.onosproject.mastership.MastershipTerm;
40 import org.onosproject.net.ConnectPoint; 43 import org.onosproject.net.ConnectPoint;
41 import org.onosproject.net.DefaultAnnotations; 44 import org.onosproject.net.DefaultAnnotations;
...@@ -192,7 +195,10 @@ public final class KryoNamespaces { ...@@ -192,7 +195,10 @@ public final class KryoNamespaces {
192 .register(MISC) 195 .register(MISC)
193 .nextId(KryoNamespace.INITIAL_ID + 30 + 10) 196 .nextId(KryoNamespace.INITIAL_ID + 30 + 10)
194 .register( 197 .register(
198 + Version.class,
195 ControllerNode.State.class, 199 ControllerNode.State.class,
200 + ApplicationState.class,
201 + DefaultApplication.class,
196 Device.Type.class, 202 Device.Type.class,
197 Port.Type.class, 203 Port.Type.class,
198 ChassisId.class, 204 ChassisId.class,
......
...@@ -8,4 +8,4 @@ export OCN="10.128.11.4" ...@@ -8,4 +8,4 @@ export OCN="10.128.11.4"
8 8
9 export OCI="${OC1}" 9 export OCI="${OC1}"
10 10
11 -export ONOS_FEATURES="webconsole,onos-api,onos-core,onos-cli,onos-openflow,onos-gui,onos-rest,onos-app-fwd,onos-app-proxyarp,onos-app-foo" 11 +export ONOS_FEATURES="webconsole,onos-api,onos-core,onos-cli,onos-openflow,onos-gui,onos-rest,onos-app-fwd,onos-app-proxyarp"
......
...@@ -27,9 +27,11 @@ import java.io.InputStreamReader; ...@@ -27,9 +27,11 @@ import java.io.InputStreamReader;
27 import java.lang.Thread.UncaughtExceptionHandler; 27 import java.lang.Thread.UncaughtExceptionHandler;
28 import java.nio.charset.StandardCharsets; 28 import java.nio.charset.StandardCharsets;
29 import java.nio.file.FileVisitResult; 29 import java.nio.file.FileVisitResult;
30 +import java.nio.file.Files;
30 import java.nio.file.Path; 31 import java.nio.file.Path;
31 import java.nio.file.Paths; 32 import java.nio.file.Paths;
32 import java.nio.file.SimpleFileVisitor; 33 import java.nio.file.SimpleFileVisitor;
34 +import java.nio.file.StandardCopyOption;
33 import java.nio.file.attribute.BasicFileAttributes; 35 import java.nio.file.attribute.BasicFileAttributes;
34 import java.util.ArrayList; 36 import java.util.ArrayList;
35 import java.util.List; 37 import java.util.List;
...@@ -200,4 +202,60 @@ public abstract class Tools { ...@@ -200,4 +202,60 @@ public abstract class Tools {
200 } 202 }
201 } 203 }
202 204
205 +
206 + /**
207 + * Copies the specified directory path.&nbsp;Use with great caution since
208 + * no attempt is made to check for symbolic links, which could result in
209 + * copy of unintended files.
210 + *
211 + * @param src directory to be copied
212 + * @param dst destination directory to be removed
213 + * @throws java.io.IOException if unable to remove contents
214 + */
215 + public static void copyDirectory(String src, String dst) throws IOException {
216 + walkFileTree(Paths.get(src), new DirectoryCopier(src, dst));
217 + }
218 +
219 + /**
220 + * Copies the specified directory path.&nbsp;Use with great caution since
221 + * no attempt is made to check for symbolic links, which could result in
222 + * copy of unintended files.
223 + *
224 + * @param src directory to be copied
225 + * @param dst destination directory to be removed
226 + * @throws java.io.IOException if unable to remove contents
227 + */
228 + public static void copyDirectory(File src, File dst) throws IOException {
229 + walkFileTree(Paths.get(src.getAbsolutePath()),
230 + new DirectoryCopier(src.getAbsolutePath(),
231 + dst.getAbsolutePath()));
232 + }
233 +
234 +
235 + public static class DirectoryCopier extends SimpleFileVisitor<Path> {
236 + private Path src;
237 + private Path dst;
238 + private StandardCopyOption copyOption = StandardCopyOption.REPLACE_EXISTING;
239 +
240 + DirectoryCopier(String src, String dst) {
241 + this.src = Paths.get(src);
242 + this.dst = Paths.get(dst);
243 + }
244 +
245 + @Override
246 + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
247 + Path targetPath = dst.resolve(src.relativize(dir));
248 + if (!Files.exists(targetPath)) {
249 + Files.createDirectory(targetPath);
250 + }
251 + return FileVisitResult.CONTINUE;
252 + }
253 +
254 + @Override
255 + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
256 + Files.copy(file, dst.resolve(src.relativize(file)), copyOption);
257 + return FileVisitResult.CONTINUE;
258 + }
259 + }
260 +
203 } 261 }
......