Ray Milkey

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
...@@ -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);
......
1 { 1 {
2 - "appId":-29467,
3 "priority":1, 2 "priority":1,
4 "isPermanent":"false", 3 "isPermanent":"false",
5 "timeout":1, 4 "timeout":1,
......
1 { 1 {
2 - "appId":-29467,
3 "priority":1, 2 "priority":1,
4 "isPermanent":"false", 3 "isPermanent":"false",
5 "timeout":1, 4 "timeout":1,
......
1 { 1 {
2 - "appId":-29467,
3 "priority":1, 2 "priority":1,
4 "isPermanent":"false", 3 "isPermanent":"false",
5 "timeout":1, 4 "timeout":1,
......
1 { 1 {
2 - "appId":-29467,
3 "priority":1, 2 "priority":1,
4 "isPermanent":"false", 3 "isPermanent":"false",
5 "timeout":1, 4 "timeout":1,
......
...@@ -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 }
......