ONOS-2144 - Complete implementation of REST API for flows
- URL for creation of a single flow is now /flows/{deviceId} - On successful creation of a flow, Location header contains a reference to the new object's URI - POST operation returns status code of CREATED - implement DELETE operation for /flows/{deviceId}/{flowId} - removed deprecations and warnings from REST API unit test for flows. Change-Id: Idb43a651a659e60c07a6f36dfd69004c814b146b
Showing
8 changed files
with
91 additions
and
25 deletions
... | @@ -33,7 +33,6 @@ import static org.onlab.util.Tools.nullIsIllegal; | ... | @@ -33,7 +33,6 @@ import static org.onlab.util.Tools.nullIsIllegal; |
33 | */ | 33 | */ |
34 | public final class FlowRuleCodec extends JsonCodec<FlowRule> { | 34 | public final class FlowRuleCodec extends JsonCodec<FlowRule> { |
35 | 35 | ||
36 | - private static final String APP_ID = "appId"; | ||
37 | private static final String PRIORITY = "priority"; | 36 | private static final String PRIORITY = "priority"; |
38 | private static final String TIMEOUT = "timeout"; | 37 | private static final String TIMEOUT = "timeout"; |
39 | private static final String IS_PERMANENT = "isPermanent"; | 38 | private static final String IS_PERMANENT = "isPermanent"; |
... | @@ -42,6 +41,7 @@ public final class FlowRuleCodec extends JsonCodec<FlowRule> { | ... | @@ -42,6 +41,7 @@ public final class FlowRuleCodec extends JsonCodec<FlowRule> { |
42 | private static final String SELECTOR = "selector"; | 41 | private static final String SELECTOR = "selector"; |
43 | private static final String MISSING_MEMBER_MESSAGE = | 42 | private static final String MISSING_MEMBER_MESSAGE = |
44 | " member is required in FlowRule"; | 43 | " member is required in FlowRule"; |
44 | + public static final String REST_APP_ID = "org.onosproject.rest"; | ||
45 | 45 | ||
46 | 46 | ||
47 | @Override | 47 | @Override |
... | @@ -52,10 +52,9 @@ public final class FlowRuleCodec extends JsonCodec<FlowRule> { | ... | @@ -52,10 +52,9 @@ public final class FlowRuleCodec extends JsonCodec<FlowRule> { |
52 | 52 | ||
53 | FlowRule.Builder resultBuilder = new DefaultFlowRule.Builder(); | 53 | FlowRule.Builder resultBuilder = new DefaultFlowRule.Builder(); |
54 | 54 | ||
55 | - short appId = nullIsIllegal(json.get(APP_ID), | ||
56 | - APP_ID + MISSING_MEMBER_MESSAGE).shortValue(); | ||
57 | CoreService coreService = context.getService(CoreService.class); | 55 | CoreService coreService = context.getService(CoreService.class); |
58 | - resultBuilder.fromApp(coreService.getAppId(appId)); | 56 | + resultBuilder.fromApp(coreService |
57 | + .registerApplication(REST_APP_ID)); | ||
59 | 58 | ||
60 | int priority = nullIsIllegal(json.get(PRIORITY), | 59 | int priority = nullIsIllegal(json.get(PRIORITY), |
61 | PRIORITY + MISSING_MEMBER_MESSAGE).asInt(); | 60 | PRIORITY + MISSING_MEMBER_MESSAGE).asInt(); | ... | ... |
... | @@ -97,7 +97,7 @@ public class FlowRuleCodecTest { | ... | @@ -97,7 +97,7 @@ public class FlowRuleCodecTest { |
97 | flowRuleCodec = context.codec(FlowRule.class); | 97 | flowRuleCodec = context.codec(FlowRule.class); |
98 | assertThat(flowRuleCodec, notNullValue()); | 98 | assertThat(flowRuleCodec, notNullValue()); |
99 | 99 | ||
100 | - expect(mockCoreService.getAppId(APP_ID.id())) | 100 | + expect(mockCoreService.registerApplication(FlowRuleCodec.REST_APP_ID)) |
101 | .andReturn(APP_ID).anyTimes(); | 101 | .andReturn(APP_ID).anyTimes(); |
102 | replay(mockCoreService); | 102 | replay(mockCoreService); |
103 | context.registerService(CoreService.class, mockCoreService); | 103 | context.registerService(CoreService.class, mockCoreService); | ... | ... |
... | @@ -17,8 +17,12 @@ package org.onosproject.rest.resources; | ... | @@ -17,8 +17,12 @@ package org.onosproject.rest.resources; |
17 | 17 | ||
18 | import java.io.IOException; | 18 | import java.io.IOException; |
19 | import java.io.InputStream; | 19 | import java.io.InputStream; |
20 | +import java.net.URI; | ||
21 | +import java.net.URISyntaxException; | ||
22 | +import java.util.stream.StreamSupport; | ||
20 | 23 | ||
21 | import javax.ws.rs.Consumes; | 24 | import javax.ws.rs.Consumes; |
25 | +import javax.ws.rs.DELETE; | ||
22 | import javax.ws.rs.GET; | 26 | import javax.ws.rs.GET; |
23 | import javax.ws.rs.POST; | 27 | import javax.ws.rs.POST; |
24 | import javax.ws.rs.Path; | 28 | import javax.ws.rs.Path; |
... | @@ -36,6 +40,7 @@ import org.onosproject.net.flow.FlowRule; | ... | @@ -36,6 +40,7 @@ import org.onosproject.net.flow.FlowRule; |
36 | import org.onosproject.net.flow.FlowRuleService; | 40 | import org.onosproject.net.flow.FlowRuleService; |
37 | import org.onosproject.rest.AbstractWebResource; | 41 | import org.onosproject.rest.AbstractWebResource; |
38 | 42 | ||
43 | +import com.fasterxml.jackson.databind.JsonNode; | ||
39 | import com.fasterxml.jackson.databind.node.ArrayNode; | 44 | import com.fasterxml.jackson.databind.node.ArrayNode; |
40 | import com.fasterxml.jackson.databind.node.ObjectNode; | 45 | import com.fasterxml.jackson.databind.node.ObjectNode; |
41 | 46 | ||
... | @@ -125,22 +130,58 @@ public class FlowsWebResource extends AbstractWebResource { | ... | @@ -125,22 +130,58 @@ public class FlowsWebResource extends AbstractWebResource { |
125 | * Creates a flow rule from a POST of a JSON string and attempts to apply it. | 130 | * Creates a flow rule from a POST of a JSON string and attempts to apply it. |
126 | * | 131 | * |
127 | * @param stream input JSON | 132 | * @param stream input JSON |
128 | - * @return status of the request - ACCEPTED if the JSON is correct, | 133 | + * @return status of the request - CREATED if the JSON is correct, |
129 | * BAD_REQUEST if the JSON is invalid | 134 | * BAD_REQUEST if the JSON is invalid |
130 | */ | 135 | */ |
131 | @POST | 136 | @POST |
137 | + @Path("{deviceId}") | ||
132 | @Consumes(MediaType.APPLICATION_JSON) | 138 | @Consumes(MediaType.APPLICATION_JSON) |
133 | @Produces(MediaType.APPLICATION_JSON) | 139 | @Produces(MediaType.APPLICATION_JSON) |
134 | - public Response createFlow(InputStream stream) { | 140 | + public Response createFlow(@PathParam("deviceId") String deviceId, |
141 | + InputStream stream) { | ||
142 | + URI location; | ||
135 | try { | 143 | try { |
136 | FlowRuleService service = get(FlowRuleService.class); | 144 | FlowRuleService service = get(FlowRuleService.class); |
137 | ObjectNode root = (ObjectNode) mapper().readTree(stream); | 145 | ObjectNode root = (ObjectNode) mapper().readTree(stream); |
146 | + JsonNode specifiedDeviceId = root.get("deviceId"); | ||
147 | + if (specifiedDeviceId != null && | ||
148 | + !specifiedDeviceId.asText().equals(deviceId)) { | ||
149 | + throw new IllegalArgumentException( | ||
150 | + "Invalid deviceId in flow creation request"); | ||
151 | + } | ||
152 | + root.put("deviceId", deviceId); | ||
138 | FlowRule rule = codec(FlowRule.class).decode(root, this); | 153 | FlowRule rule = codec(FlowRule.class).decode(root, this); |
139 | service.applyFlowRules(rule); | 154 | service.applyFlowRules(rule); |
140 | - } catch (IOException ex) { | 155 | + location = new URI(Long.toString(rule.id().value())); |
156 | + } catch (IOException | URISyntaxException ex) { | ||
141 | return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); | 157 | return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); |
142 | } | 158 | } |
143 | - return Response.status(Response.Status.ACCEPTED).build(); | 159 | + return Response |
160 | + .created(location) | ||
161 | + .build(); | ||
162 | + } | ||
163 | + | ||
164 | + /** | ||
165 | + * Removes the flows for a given device with the given flow id. | ||
166 | + * | ||
167 | + * @param deviceId Id of device to look up | ||
168 | + * @param flowId Id of flow to look up | ||
169 | + */ | ||
170 | + @DELETE | ||
171 | + @Produces(MediaType.APPLICATION_JSON) | ||
172 | + @Path("{deviceId}/{flowId}") | ||
173 | + public void deleteFlowByDeviceIdAndFlowId(@PathParam("deviceId") String deviceId, | ||
174 | + @PathParam("flowId") long flowId) { | ||
175 | + final Iterable<FlowEntry> deviceEntries = | ||
176 | + service.getFlowEntries(DeviceId.deviceId(deviceId)); | ||
177 | + | ||
178 | + if (!deviceEntries.iterator().hasNext()) { | ||
179 | + throw new ItemNotFoundException(DEVICE_NOT_FOUND); | ||
180 | + } | ||
181 | + | ||
182 | + StreamSupport.stream(deviceEntries.spliterator(), false) | ||
183 | + .filter(entry -> entry.id().value() == flowId) | ||
184 | + .forEach(service::removeFlowRules); | ||
144 | } | 185 | } |
145 | 186 | ||
146 | } | 187 | } | ... | ... |
... | @@ -23,6 +23,7 @@ import java.util.Set; | ... | @@ -23,6 +23,7 @@ import java.util.Set; |
23 | import javax.ws.rs.core.MediaType; | 23 | import javax.ws.rs.core.MediaType; |
24 | 24 | ||
25 | import org.hamcrest.Description; | 25 | import org.hamcrest.Description; |
26 | +import org.hamcrest.Matchers; | ||
26 | import org.hamcrest.TypeSafeMatcher; | 27 | import org.hamcrest.TypeSafeMatcher; |
27 | import org.junit.After; | 28 | import org.junit.After; |
28 | import org.junit.Before; | 29 | import org.junit.Before; |
... | @@ -33,12 +34,14 @@ import org.onlab.packet.MacAddress; | ... | @@ -33,12 +34,14 @@ import org.onlab.packet.MacAddress; |
33 | import org.onlab.rest.BaseResource; | 34 | import org.onlab.rest.BaseResource; |
34 | import org.onosproject.codec.CodecService; | 35 | import org.onosproject.codec.CodecService; |
35 | import org.onosproject.codec.impl.CodecManager; | 36 | import org.onosproject.codec.impl.CodecManager; |
37 | +import org.onosproject.codec.impl.FlowRuleCodec; | ||
36 | import org.onosproject.core.CoreService; | 38 | import org.onosproject.core.CoreService; |
37 | import org.onosproject.core.DefaultGroupId; | 39 | import org.onosproject.core.DefaultGroupId; |
38 | import org.onosproject.core.GroupId; | 40 | import org.onosproject.core.GroupId; |
39 | import org.onosproject.net.DefaultDevice; | 41 | import org.onosproject.net.DefaultDevice; |
40 | import org.onosproject.net.Device; | 42 | import org.onosproject.net.Device; |
41 | import org.onosproject.net.DeviceId; | 43 | import org.onosproject.net.DeviceId; |
44 | +import org.onosproject.net.IndexedLambda; | ||
42 | import org.onosproject.net.NetTestTools; | 45 | import org.onosproject.net.NetTestTools; |
43 | import org.onosproject.net.device.DeviceService; | 46 | import org.onosproject.net.device.DeviceService; |
44 | import org.onosproject.net.flow.DefaultTrafficSelector; | 47 | import org.onosproject.net.flow.DefaultTrafficSelector; |
... | @@ -74,6 +77,7 @@ import static org.hamcrest.Matchers.not; | ... | @@ -74,6 +77,7 @@ import static org.hamcrest.Matchers.not; |
74 | import static org.hamcrest.Matchers.notNullValue; | 77 | import static org.hamcrest.Matchers.notNullValue; |
75 | import static org.junit.Assert.assertThat; | 78 | import static org.junit.Assert.assertThat; |
76 | import static org.junit.Assert.fail; | 79 | import static org.junit.Assert.fail; |
80 | +import static org.onosproject.net.NetTestTools.APP_ID; | ||
77 | 81 | ||
78 | /** | 82 | /** |
79 | * Unit tests for Flows REST APIs. | 83 | * Unit tests for Flows REST APIs. |
... | @@ -103,6 +107,10 @@ public class FlowsResourceTest extends ResourceTest { | ... | @@ -103,6 +107,10 @@ public class FlowsResourceTest extends ResourceTest { |
103 | final MockFlowEntry flow5 = new MockFlowEntry(deviceId2, 5); | 107 | final MockFlowEntry flow5 = new MockFlowEntry(deviceId2, 5); |
104 | final MockFlowEntry flow6 = new MockFlowEntry(deviceId2, 6); | 108 | final MockFlowEntry flow6 = new MockFlowEntry(deviceId2, 6); |
105 | 109 | ||
110 | + private static final String FLOW_JSON = "{\"priority\":1,\"isPermanent\":true," | ||
111 | + + "\"treatment\":{\"instructions\":[ {\"type\":\"OUTPUT\",\"port\":2}]}," | ||
112 | + + "\"selector\":{\"criteria\":[ {\"type\":\"ETH_TYPE\",\"ethType\":2054}]}}"; | ||
113 | + | ||
106 | /** | 114 | /** |
107 | * Mock class for a flow entry. | 115 | * Mock class for a flow entry. |
108 | */ | 116 | */ |
... | @@ -214,8 +222,8 @@ public class FlowsResourceTest extends ResourceTest { | ... | @@ -214,8 +222,8 @@ public class FlowsResourceTest extends ResourceTest { |
214 | */ | 222 | */ |
215 | private void setupMockFlows() { | 223 | private void setupMockFlows() { |
216 | flow2.treatment = DefaultTrafficTreatment.builder() | 224 | flow2.treatment = DefaultTrafficTreatment.builder() |
217 | - .add(Instructions.modL0Lambda((short) 4)) | 225 | + .add(Instructions.modL0Lambda(new IndexedLambda((short) 4))) |
218 | - .add(Instructions.modL0Lambda((short) 5)) | 226 | + .add(Instructions.modL0Lambda(new IndexedLambda((short) 5))) |
219 | .setEthDst(MacAddress.BROADCAST) | 227 | .setEthDst(MacAddress.BROADCAST) |
220 | .build(); | 228 | .build(); |
221 | flow2.selector = DefaultTrafficSelector.builder() | 229 | flow2.selector = DefaultTrafficSelector.builder() |
... | @@ -223,15 +231,15 @@ public class FlowsResourceTest extends ResourceTest { | ... | @@ -223,15 +231,15 @@ public class FlowsResourceTest extends ResourceTest { |
223 | .matchIPProtocol((byte) 9) | 231 | .matchIPProtocol((byte) 9) |
224 | .build(); | 232 | .build(); |
225 | flow4.treatment = DefaultTrafficTreatment.builder() | 233 | flow4.treatment = DefaultTrafficTreatment.builder() |
226 | - .add(Instructions.modL0Lambda((short) 6)) | 234 | + .add(Instructions.modL0Lambda(new IndexedLambda((short) 6))) |
227 | .build(); | 235 | .build(); |
228 | final Set<FlowEntry> flows1 = new HashSet<>(); | 236 | final Set<FlowEntry> flows1 = new HashSet<>(); |
229 | flows1.add(flow1); | 237 | flows1.add(flow1); |
230 | flows1.add(flow2); | 238 | flows1.add(flow2); |
231 | 239 | ||
232 | final Set<FlowEntry> flows2 = new HashSet<>(); | 240 | final Set<FlowEntry> flows2 = new HashSet<>(); |
233 | - flows1.add(flow3); | 241 | + flows2.add(flow3); |
234 | - flows1.add(flow4); | 242 | + flows2.add(flow4); |
235 | 243 | ||
236 | rules.put(deviceId1, flows1); | 244 | rules.put(deviceId1, flows1); |
237 | rules.put(deviceId2, flows2); | 245 | rules.put(deviceId2, flows2); |
... | @@ -258,6 +266,8 @@ public class FlowsResourceTest extends ResourceTest { | ... | @@ -258,6 +266,8 @@ public class FlowsResourceTest extends ResourceTest { |
258 | // Mock Core Service | 266 | // Mock Core Service |
259 | expect(mockCoreService.getAppId(anyShort())) | 267 | expect(mockCoreService.getAppId(anyShort())) |
260 | .andReturn(NetTestTools.APP_ID).anyTimes(); | 268 | .andReturn(NetTestTools.APP_ID).anyTimes(); |
269 | + expect(mockCoreService.registerApplication(FlowRuleCodec.REST_APP_ID)) | ||
270 | + .andReturn(APP_ID).anyTimes(); | ||
261 | replay(mockCoreService); | 271 | replay(mockCoreService); |
262 | 272 | ||
263 | // Register the services needed for the test | 273 | // Register the services needed for the test |
... | @@ -565,10 +575,7 @@ public class FlowsResourceTest extends ResourceTest { | ... | @@ -565,10 +575,7 @@ public class FlowsResourceTest extends ResourceTest { |
565 | */ | 575 | */ |
566 | @Test | 576 | @Test |
567 | public void testPost() { | 577 | public void testPost() { |
568 | - String json = "{\"appId\":2,\"priority\":1,\"isPermanent\":true," | 578 | + |
569 | - + "\"deviceId\":\"of:0000000000000001\"," | ||
570 | - + "\"treatment\":{\"instructions\":[ {\"type\":\"OUTPUT\",\"port\":2}]}," | ||
571 | - + "\"selector\":{\"criteria\":[ {\"type\":\"ETH_TYPE\",\"ethType\":2054}]}}"; | ||
572 | 579 | ||
573 | mockFlowService.applyFlowRules(anyObject()); | 580 | mockFlowService.applyFlowRules(anyObject()); |
574 | expectLastCall(); | 581 | expectLastCall(); |
... | @@ -577,9 +584,32 @@ public class FlowsResourceTest extends ResourceTest { | ... | @@ -577,9 +584,32 @@ public class FlowsResourceTest extends ResourceTest { |
577 | WebResource rs = resource(); | 584 | WebResource rs = resource(); |
578 | 585 | ||
579 | 586 | ||
580 | - ClientResponse response = rs.path("flows/") | 587 | + ClientResponse response = rs.path("flows/of:0000000000000001") |
588 | + .type(MediaType.APPLICATION_JSON_TYPE) | ||
589 | + .post(ClientResponse.class, FLOW_JSON); | ||
590 | + assertThat(response.getStatus(), is(HttpURLConnection.HTTP_CREATED)); | ||
591 | + String location = response.getLocation().getPath(); | ||
592 | + assertThat(location, Matchers.startsWith("/flows/of:0000000000000001/")); | ||
593 | + } | ||
594 | + | ||
595 | + /** | ||
596 | + * Tests deleting a flow. | ||
597 | + */ | ||
598 | + @Test | ||
599 | + public void testDelete() { | ||
600 | + setupMockFlows(); | ||
601 | + mockFlowService.removeFlowRules(anyObject()); | ||
602 | + expectLastCall(); | ||
603 | + replay(mockFlowService); | ||
604 | + | ||
605 | + WebResource rs = resource(); | ||
606 | + | ||
607 | + String location = "/flows/1/155"; | ||
608 | + | ||
609 | + ClientResponse deleteResponse = rs.path(location) | ||
581 | .type(MediaType.APPLICATION_JSON_TYPE) | 610 | .type(MediaType.APPLICATION_JSON_TYPE) |
582 | - .post(ClientResponse.class, json); | 611 | + .delete(ClientResponse.class); |
583 | - assertThat(response.getStatus(), is(HttpURLConnection.HTTP_ACCEPTED)); | 612 | + assertThat(deleteResponse.getStatus(), |
613 | + is(HttpURLConnection.HTTP_NO_CONTENT)); | ||
584 | } | 614 | } |
585 | } | 615 | } | ... | ... |
-
Please register or login to post a comment