ONOS-790 - REST APIs for paths
Change-Id: I5e08778e75c98c1156df7972af9897b52677d371
Showing
6 changed files
with
394 additions
and
3 deletions
... | @@ -33,6 +33,7 @@ import org.onosproject.net.Device; | ... | @@ -33,6 +33,7 @@ import org.onosproject.net.Device; |
33 | import org.onosproject.net.Host; | 33 | import org.onosproject.net.Host; |
34 | import org.onosproject.net.HostLocation; | 34 | import org.onosproject.net.HostLocation; |
35 | import org.onosproject.net.Link; | 35 | import org.onosproject.net.Link; |
36 | +import org.onosproject.net.Path; | ||
36 | import org.onosproject.net.Port; | 37 | import org.onosproject.net.Port; |
37 | import org.onosproject.net.flow.FlowEntry; | 38 | import org.onosproject.net.flow.FlowEntry; |
38 | import org.onosproject.net.flow.TrafficSelector; | 39 | import org.onosproject.net.flow.TrafficSelector; |
... | @@ -86,6 +87,7 @@ public class CodecManager implements CodecService { | ... | @@ -86,6 +87,7 @@ public class CodecManager implements CodecService { |
86 | registerCodec(Constraint.class, new ConstraintCodec()); | 87 | registerCodec(Constraint.class, new ConstraintCodec()); |
87 | registerCodec(Topology.class, new TopologyCodec()); | 88 | registerCodec(Topology.class, new TopologyCodec()); |
88 | registerCodec(TopologyCluster.class, new TopologyClusterCodec()); | 89 | registerCodec(TopologyCluster.class, new TopologyClusterCodec()); |
90 | + registerCodec(Path.class, new PathCodec()); | ||
89 | log.info("Started"); | 91 | log.info("Started"); |
90 | } | 92 | } |
91 | 93 | ... | ... |
... | @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; | ... | @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; |
19 | import org.onosproject.codec.CodecContext; | 19 | import org.onosproject.codec.CodecContext; |
20 | import org.onosproject.codec.JsonCodec; | 20 | import org.onosproject.codec.JsonCodec; |
21 | import org.onosproject.net.ConnectPoint; | 21 | import org.onosproject.net.ConnectPoint; |
22 | +import org.onosproject.net.DeviceId; | ||
23 | +import org.onosproject.net.HostId; | ||
22 | 24 | ||
23 | import static com.google.common.base.Preconditions.checkNotNull; | 25 | import static com.google.common.base.Preconditions.checkNotNull; |
24 | 26 | ||
... | @@ -30,9 +32,16 @@ public class ConnectPointCodec extends JsonCodec<ConnectPoint> { | ... | @@ -30,9 +32,16 @@ public class ConnectPointCodec extends JsonCodec<ConnectPoint> { |
30 | @Override | 32 | @Override |
31 | public ObjectNode encode(ConnectPoint point, CodecContext context) { | 33 | public ObjectNode encode(ConnectPoint point, CodecContext context) { |
32 | checkNotNull(point, "Connect point cannot be null"); | 34 | checkNotNull(point, "Connect point cannot be null"); |
33 | - return context.mapper().createObjectNode() | 35 | + ObjectNode root = context.mapper().createObjectNode() |
34 | - .put("device", point.deviceId().toString()) | ||
35 | .put("port", point.port().toString()); | 36 | .put("port", point.port().toString()); |
37 | + | ||
38 | + if (point.elementId() instanceof DeviceId) { | ||
39 | + root.put("device", point.deviceId().toString()); | ||
40 | + } else if (point.elementId() instanceof HostId) { | ||
41 | + root.put("host", point.hostId().toString()); | ||
42 | + } | ||
43 | + | ||
44 | + return root; | ||
36 | } | 45 | } |
37 | 46 | ||
38 | } | 47 | } | ... | ... |
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.codec.impl; | ||
17 | + | ||
18 | +import org.onosproject.codec.CodecContext; | ||
19 | +import org.onosproject.codec.JsonCodec; | ||
20 | +import org.onosproject.net.Link; | ||
21 | +import org.onosproject.net.Path; | ||
22 | + | ||
23 | +import com.fasterxml.jackson.databind.node.ArrayNode; | ||
24 | +import com.fasterxml.jackson.databind.node.ObjectNode; | ||
25 | + | ||
26 | +import static com.google.common.base.Preconditions.checkNotNull; | ||
27 | + | ||
28 | +/** | ||
29 | + * Path JSON codec. | ||
30 | + */ | ||
31 | +public class PathCodec extends AnnotatedCodec<Path> { | ||
32 | + @Override | ||
33 | + public ObjectNode encode(Path path, CodecContext context) { | ||
34 | + checkNotNull(path, "Path cannot be null"); | ||
35 | + JsonCodec<Link> codec = context.codec(Link.class); | ||
36 | + ObjectNode result = context.mapper() | ||
37 | + .createObjectNode() | ||
38 | + .put("cost", path.cost()); | ||
39 | + ArrayNode jsonLinks = result.putArray("links"); | ||
40 | + | ||
41 | + for (Link link : path.links()) { | ||
42 | + jsonLinks.add(codec.encode(link, context)); | ||
43 | + } | ||
44 | + | ||
45 | + return annotate(result, path, context); | ||
46 | + } | ||
47 | +} |
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.rest; | ||
17 | + | ||
18 | +import java.util.Set; | ||
19 | + | ||
20 | +import javax.ws.rs.GET; | ||
21 | +import javax.ws.rs.Path; | ||
22 | +import javax.ws.rs.PathParam; | ||
23 | +import javax.ws.rs.Produces; | ||
24 | +import javax.ws.rs.core.MediaType; | ||
25 | +import javax.ws.rs.core.Response; | ||
26 | + | ||
27 | +import org.onosproject.net.DeviceId; | ||
28 | +import org.onosproject.net.ElementId; | ||
29 | +import org.onosproject.net.HostId; | ||
30 | +import org.onosproject.net.topology.PathService; | ||
31 | + | ||
32 | +import com.fasterxml.jackson.databind.node.ObjectNode; | ||
33 | + | ||
34 | +/** | ||
35 | + * REST resource for interacting with path calculations. | ||
36 | + */ | ||
37 | +@Path("paths") | ||
38 | +public class PathsWebResource extends AbstractWebResource { | ||
39 | + | ||
40 | + // Host id format is 00:00:00:00:00:01/-1 | ||
41 | + private static final int VLAN_SEPARATOR_OFFSET = 17; | ||
42 | + private static final int FIRST_MAC_ADDRESS_SEPARATOR_OFFSET = 2; | ||
43 | + private static final int SECOND_MAC_ADDRESS_SEPARATOR_OFFSET = 5; | ||
44 | + private static final int THIRD_MAC_ADDRESS_SEPARATOR_OFFSET = 8; | ||
45 | + | ||
46 | + /** | ||
47 | + * Determines if the id appears to be the id of a host. | ||
48 | + * | ||
49 | + * @param id id string | ||
50 | + * @return HostId if the id is valid, null otherwise | ||
51 | + */ | ||
52 | + private HostId isHostId(String id) { | ||
53 | + | ||
54 | + if (id.length() < VLAN_SEPARATOR_OFFSET + 1 || | ||
55 | + id.charAt(FIRST_MAC_ADDRESS_SEPARATOR_OFFSET) != ':' || | ||
56 | + id.charAt(SECOND_MAC_ADDRESS_SEPARATOR_OFFSET) != ':' || | ||
57 | + id.charAt(THIRD_MAC_ADDRESS_SEPARATOR_OFFSET) != ':' || | ||
58 | + id.charAt(VLAN_SEPARATOR_OFFSET) != '/') { | ||
59 | + return null; | ||
60 | + } | ||
61 | + | ||
62 | + return HostId.hostId(id); | ||
63 | + } | ||
64 | + | ||
65 | + /** | ||
66 | + * Gets the paths between two elements. | ||
67 | + * | ||
68 | + * @param src source | ||
69 | + * @param dst destination | ||
70 | + * @return path data | ||
71 | + */ | ||
72 | + @GET | ||
73 | + @Produces(MediaType.APPLICATION_JSON) | ||
74 | + @Path("{src}/{dst}") | ||
75 | + public Response getPath(@PathParam("src") String src, | ||
76 | + @PathParam("dst") String dst) { | ||
77 | + PathService pathService = get(PathService.class); | ||
78 | + | ||
79 | + ElementId srcElement = isHostId(src); | ||
80 | + ElementId dstElement = isHostId(dst); | ||
81 | + | ||
82 | + if (srcElement == null) { | ||
83 | + // Doesn't look like a host, assume it is a device | ||
84 | + srcElement = DeviceId.deviceId(src); | ||
85 | + } | ||
86 | + | ||
87 | + if (dstElement == null) { | ||
88 | + // Doesn't look like a host, assume it is a device | ||
89 | + dstElement = DeviceId.deviceId(dst); | ||
90 | + } | ||
91 | + | ||
92 | + Set<org.onosproject.net.Path> paths = | ||
93 | + pathService.getPaths(srcElement, dstElement); | ||
94 | + ObjectNode root = | ||
95 | + encodeArray(org.onosproject.net.Path.class, "paths", paths); | ||
96 | + return ok(root).build(); | ||
97 | + } | ||
98 | + | ||
99 | +} |
... | @@ -43,7 +43,8 @@ | ... | @@ -43,7 +43,8 @@ |
43 | org.onosproject.rest.IntentsWebResource, | 43 | org.onosproject.rest.IntentsWebResource, |
44 | org.onosproject.rest.FlowsWebResource, | 44 | org.onosproject.rest.FlowsWebResource, |
45 | org.onosproject.rest.TopologyWebResource, | 45 | org.onosproject.rest.TopologyWebResource, |
46 | - org.onosproject.rest.ConfigResource | 46 | + org.onosproject.rest.ConfigResource, |
47 | + org.onosproject.rest.PathsWebResource | ||
47 | </param-value> | 48 | </param-value> |
48 | </init-param> | 49 | </init-param> |
49 | <load-on-startup>1</load-on-startup> | 50 | <load-on-startup>1</load-on-startup> | ... | ... |
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.rest; | ||
17 | + | ||
18 | +import java.io.UnsupportedEncodingException; | ||
19 | +import java.net.URLEncoder; | ||
20 | +import java.nio.charset.StandardCharsets; | ||
21 | +import java.util.Set; | ||
22 | + | ||
23 | +import org.hamcrest.Description; | ||
24 | +import org.hamcrest.TypeSafeDiagnosingMatcher; | ||
25 | +import org.junit.After; | ||
26 | +import org.junit.Before; | ||
27 | +import org.junit.Test; | ||
28 | +import org.onlab.osgi.ServiceDirectory; | ||
29 | +import org.onlab.osgi.TestServiceDirectory; | ||
30 | +import org.onlab.rest.BaseResource; | ||
31 | +import org.onosproject.codec.CodecService; | ||
32 | +import org.onosproject.codec.impl.CodecManager; | ||
33 | +import org.onosproject.net.ElementId; | ||
34 | +import org.onosproject.net.Link; | ||
35 | +import org.onosproject.net.Path; | ||
36 | +import org.onosproject.net.topology.PathService; | ||
37 | + | ||
38 | +import com.eclipsesource.json.JsonArray; | ||
39 | +import com.eclipsesource.json.JsonObject; | ||
40 | +import com.google.common.collect.ImmutableSet; | ||
41 | +import com.sun.jersey.api.client.WebResource; | ||
42 | + | ||
43 | +import static org.easymock.EasyMock.createMock; | ||
44 | +import static org.easymock.EasyMock.expect; | ||
45 | +import static org.easymock.EasyMock.replay; | ||
46 | +import static org.easymock.EasyMock.verify; | ||
47 | +import static org.hamcrest.Matchers.containsString; | ||
48 | +import static org.hamcrest.Matchers.hasSize; | ||
49 | +import static org.hamcrest.Matchers.is; | ||
50 | +import static org.hamcrest.Matchers.notNullValue; | ||
51 | +import static org.junit.Assert.assertThat; | ||
52 | +import static org.onosproject.net.NetTestTools.createPath; | ||
53 | +import static org.onosproject.net.NetTestTools.did; | ||
54 | +import static org.onosproject.net.NetTestTools.hid; | ||
55 | + | ||
56 | +/** | ||
57 | + * Unit tests for paths REST APIs. | ||
58 | + */ | ||
59 | +public class PathsResourceTest extends ResourceTest { | ||
60 | + Path path1 = createPath("dev1", "dev2"); | ||
61 | + Path path2 = createPath("dev2", "dev3"); | ||
62 | + Set<Path> paths = ImmutableSet.of(path1, path2); | ||
63 | + | ||
64 | + final PathService mockPathService = createMock(PathService.class); | ||
65 | + | ||
66 | + /** | ||
67 | + * Hamcrest matcher for a path and its JSON representation. | ||
68 | + */ | ||
69 | + private final class PathJsonMatcher extends TypeSafeDiagnosingMatcher<JsonObject> { | ||
70 | + | ||
71 | + private final Path path; | ||
72 | + | ||
73 | + /** | ||
74 | + * Creates the matcher. | ||
75 | + * | ||
76 | + * @param pathValue the path object to match | ||
77 | + */ | ||
78 | + private PathJsonMatcher(Path pathValue) { | ||
79 | + path = pathValue; | ||
80 | + } | ||
81 | + | ||
82 | + @Override | ||
83 | + public boolean matchesSafely(JsonObject pathJson, Description description) { | ||
84 | + | ||
85 | + double jsonCost = pathJson.get("cost").asDouble(); | ||
86 | + if (jsonCost != path.cost()) { | ||
87 | + description.appendText("src device was " + jsonCost); | ||
88 | + return false; | ||
89 | + } | ||
90 | + | ||
91 | + JsonArray jsonLinks = pathJson.get("links").asArray(); | ||
92 | + assertThat(jsonLinks.size(), is(path.links().size())); | ||
93 | + | ||
94 | + for (int linkIndex = 0; linkIndex < jsonLinks.size(); linkIndex++) { | ||
95 | + Link link = path.links().get(linkIndex); | ||
96 | + JsonObject jsonLink = jsonLinks.get(0).asObject(); | ||
97 | + | ||
98 | + JsonObject jsonLinkSrc = jsonLink.get("src").asObject(); | ||
99 | + String srcDevice = jsonLinkSrc.get("device").asString(); | ||
100 | + if (!srcDevice.equals(link.src().deviceId().toString())) { | ||
101 | + description.appendText("src device was " + jsonLinkSrc); | ||
102 | + return false; | ||
103 | + } | ||
104 | + | ||
105 | + JsonObject jsonLinkDst = jsonLink.get("dst").asObject(); | ||
106 | + String dstDevice = jsonLinkDst.get("device").asString(); | ||
107 | + if (!dstDevice.equals(link.dst().deviceId().toString())) { | ||
108 | + description.appendText("dst device was " + jsonLinkDst); | ||
109 | + return false; | ||
110 | + } | ||
111 | + } | ||
112 | + | ||
113 | + return true; | ||
114 | + } | ||
115 | + | ||
116 | + @Override | ||
117 | + public void describeTo(Description description) { | ||
118 | + description.appendText(path.toString()); | ||
119 | + } | ||
120 | + } | ||
121 | + | ||
122 | + /** | ||
123 | + * Factory to allocate an connect point matcher. | ||
124 | + * | ||
125 | + * @param path path object we are looking for | ||
126 | + * @return matcher | ||
127 | + */ | ||
128 | + private PathJsonMatcher matchesPath(Path path) { | ||
129 | + return new PathJsonMatcher(path); | ||
130 | + } | ||
131 | + | ||
132 | + @Before | ||
133 | + public void setUp() { | ||
134 | + | ||
135 | + // Register the services needed for the test | ||
136 | + CodecManager codecService = new CodecManager(); | ||
137 | + codecService.activate(); | ||
138 | + ServiceDirectory testDirectory = | ||
139 | + new TestServiceDirectory() | ||
140 | + .add(PathService.class, mockPathService) | ||
141 | + .add(CodecService.class, codecService); | ||
142 | + | ||
143 | + BaseResource.setServiceDirectory(testDirectory); | ||
144 | + } | ||
145 | + | ||
146 | + @After | ||
147 | + public void tearDown() throws Exception { | ||
148 | + super.tearDown(); | ||
149 | + verify(mockPathService); | ||
150 | + } | ||
151 | + | ||
152 | + /** | ||
153 | + * Tests a REST path GET for the given endpoints. | ||
154 | + * | ||
155 | + * @param srcElement source element of the path | ||
156 | + * @param dstElement destination element of the path | ||
157 | + * | ||
158 | + * @throws UnsupportedEncodingException | ||
159 | + */ | ||
160 | + private void runTest(ElementId srcElement, ElementId dstElement) | ||
161 | + throws UnsupportedEncodingException { | ||
162 | + expect(mockPathService.getPaths(srcElement, dstElement)) | ||
163 | + .andReturn(paths) | ||
164 | + .once(); | ||
165 | + replay(mockPathService); | ||
166 | + | ||
167 | + String srcId = URLEncoder.encode(srcElement.toString(), | ||
168 | + StandardCharsets.UTF_8.name()); | ||
169 | + String dstId = URLEncoder.encode(dstElement.toString(), | ||
170 | + StandardCharsets.UTF_8.name()); | ||
171 | + | ||
172 | + String url = "paths/" + srcId + "/" + dstId; | ||
173 | + WebResource rs = resource(); | ||
174 | + String response = rs.path(url).get(String.class); | ||
175 | + assertThat(response, containsString("{\"paths\":[")); | ||
176 | + | ||
177 | + JsonObject result = JsonObject.readFrom(response); | ||
178 | + assertThat(result, notNullValue()); | ||
179 | + | ||
180 | + assertThat(result.names(), hasSize(1)); | ||
181 | + assertThat(result.names().get(0), is("paths")); | ||
182 | + | ||
183 | + JsonArray jsonPaths = result.get("paths").asArray(); | ||
184 | + assertThat(jsonPaths, notNullValue()); | ||
185 | + assertThat(jsonPaths.size(), is(2)); | ||
186 | + | ||
187 | + JsonObject path1Json = jsonPaths.get(0).asObject(); | ||
188 | + assertThat(path1Json, matchesPath(path1)); | ||
189 | + | ||
190 | + JsonObject path2Json = jsonPaths.get(1).asObject(); | ||
191 | + assertThat(path2Json, matchesPath(path2)); | ||
192 | + } | ||
193 | + | ||
194 | + /** | ||
195 | + * Tests a path between two hosts. | ||
196 | + * | ||
197 | + * @throws UnsupportedEncodingException if UTF-8 not found | ||
198 | + */ | ||
199 | + @Test | ||
200 | + public void hostToHost() throws UnsupportedEncodingException { | ||
201 | + runTest(hid("01:23:45:67:89:AB/2"), hid("AB:89:67:45:23:01/4")); | ||
202 | + } | ||
203 | + | ||
204 | + /** | ||
205 | + * Tests a path with a host as the source and a switch as the destination. | ||
206 | + * | ||
207 | + * @throws UnsupportedEncodingException if UTF-8 not found | ||
208 | + */ | ||
209 | + @Test | ||
210 | + public void hostToDevice() throws UnsupportedEncodingException { | ||
211 | + runTest(hid("01:23:45:67:89:AB/2"), did("switch1")); | ||
212 | + } | ||
213 | + | ||
214 | + /** | ||
215 | + * Tests a path with a switch as the source and a host as the destination. | ||
216 | + * | ||
217 | + * @throws UnsupportedEncodingException if UTF-8 not found | ||
218 | + */ | ||
219 | + @Test | ||
220 | + public void deviceToHost() throws UnsupportedEncodingException { | ||
221 | + runTest(did("switch1"), hid("01:23:45:67:89:AB/2")); | ||
222 | + } | ||
223 | + | ||
224 | + /** | ||
225 | + * Tests a path between two switches. | ||
226 | + * | ||
227 | + * @throws UnsupportedEncodingException if UTF-8 not found | ||
228 | + */ | ||
229 | + @Test | ||
230 | + public void deviceToDevice() throws UnsupportedEncodingException { | ||
231 | + runTest(did("switch1"), did("switch2")); | ||
232 | + } | ||
233 | +} |
-
Please register or login to post a comment