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