Jian Li
Committed by Ray Milkey

[ONOS-3603] Implement REST API for Group query, insert, delete

* Implement decoding feature for GroupBucketCodec and GroupCodec
* Implement GroupsWebResource
* Add unit test for GroupBucketCodec and GroupCodec
* Add unit test for GroupsWebResource
* Add group insertion json example
* Add Swagger doc

Change-Id: Ie58cba2e1af996c7b8652a55d9ef0c27207beafc
...@@ -17,7 +17,6 @@ package org.onosproject.codec.impl; ...@@ -17,7 +17,6 @@ package org.onosproject.codec.impl;
17 17
18 import com.codahale.metrics.Metric; 18 import com.codahale.metrics.Metric;
19 import com.google.common.collect.ImmutableSet; 19 import com.google.common.collect.ImmutableSet;
20 -
21 import org.apache.felix.scr.annotations.Activate; 20 import org.apache.felix.scr.annotations.Activate;
22 import org.apache.felix.scr.annotations.Component; 21 import org.apache.felix.scr.annotations.Component;
23 import org.apache.felix.scr.annotations.Deactivate; 22 import org.apache.felix.scr.annotations.Deactivate;
...@@ -35,11 +34,11 @@ import org.onosproject.net.HostLocation; ...@@ -35,11 +34,11 @@ import org.onosproject.net.HostLocation;
35 import org.onosproject.net.Link; 34 import org.onosproject.net.Link;
36 import org.onosproject.net.Path; 35 import org.onosproject.net.Path;
37 import org.onosproject.net.Port; 36 import org.onosproject.net.Port;
37 +import org.onosproject.net.device.PortStatistics;
38 import org.onosproject.net.driver.Driver; 38 import org.onosproject.net.driver.Driver;
39 import org.onosproject.net.flow.FlowEntry; 39 import org.onosproject.net.flow.FlowEntry;
40 import org.onosproject.net.flow.FlowRule; 40 import org.onosproject.net.flow.FlowRule;
41 import org.onosproject.net.flow.TableStatisticsEntry; 41 import org.onosproject.net.flow.TableStatisticsEntry;
42 -import org.onosproject.net.device.PortStatistics;
43 import org.onosproject.net.flow.TrafficSelector; 42 import org.onosproject.net.flow.TrafficSelector;
44 import org.onosproject.net.flow.TrafficTreatment; 43 import org.onosproject.net.flow.TrafficTreatment;
45 import org.onosproject.net.flow.criteria.Criterion; 44 import org.onosproject.net.flow.criteria.Criterion;
......
...@@ -15,14 +15,18 @@ ...@@ -15,14 +15,18 @@
15 */ 15 */
16 package org.onosproject.codec.impl; 16 package org.onosproject.codec.impl;
17 17
18 +import com.fasterxml.jackson.databind.node.ObjectNode;
18 import org.onosproject.codec.CodecContext; 19 import org.onosproject.codec.CodecContext;
19 import org.onosproject.codec.JsonCodec; 20 import org.onosproject.codec.JsonCodec;
21 +import org.onosproject.core.DefaultGroupId;
22 +import org.onosproject.core.GroupId;
23 +import org.onosproject.net.PortNumber;
20 import org.onosproject.net.flow.TrafficTreatment; 24 import org.onosproject.net.flow.TrafficTreatment;
25 +import org.onosproject.net.group.DefaultGroupBucket;
21 import org.onosproject.net.group.GroupBucket; 26 import org.onosproject.net.group.GroupBucket;
22 27
23 -import com.fasterxml.jackson.databind.node.ObjectNode;
24 -
25 import static com.google.common.base.Preconditions.checkNotNull; 28 import static com.google.common.base.Preconditions.checkNotNull;
29 +import static org.onlab.util.Tools.nullIsIllegal;
26 30
27 /** 31 /**
28 * Group bucket JSON codec. 32 * Group bucket JSON codec.
...@@ -36,6 +40,8 @@ public class GroupBucketCodec extends JsonCodec<GroupBucket> { ...@@ -36,6 +40,8 @@ public class GroupBucketCodec extends JsonCodec<GroupBucket> {
36 private static final String WATCH_GROUP = "watchGroup"; 40 private static final String WATCH_GROUP = "watchGroup";
37 private static final String PACKETS = "packets"; 41 private static final String PACKETS = "packets";
38 private static final String BYTES = "bytes"; 42 private static final String BYTES = "bytes";
43 + private static final String MISSING_MEMBER_MESSAGE =
44 + " member is required in Group";
39 45
40 @Override 46 @Override
41 public ObjectNode encode(GroupBucket bucket, CodecContext context) { 47 public ObjectNode encode(GroupBucket bucket, CodecContext context) {
...@@ -61,4 +67,59 @@ public class GroupBucketCodec extends JsonCodec<GroupBucket> { ...@@ -61,4 +67,59 @@ public class GroupBucketCodec extends JsonCodec<GroupBucket> {
61 67
62 return result; 68 return result;
63 } 69 }
70 +
71 + @Override
72 + public GroupBucket decode(ObjectNode json, CodecContext context) {
73 + if (json == null || !json.isObject()) {
74 + return null;
75 + }
76 +
77 + // build traffic treatment
78 + ObjectNode treatmentJson = get(json, TREATMENT);
79 + TrafficTreatment trafficTreatment = null;
80 + if (treatmentJson != null) {
81 + JsonCodec<TrafficTreatment> treatmentCodec =
82 + context.codec(TrafficTreatment.class);
83 + trafficTreatment = treatmentCodec.decode(treatmentJson, context);
84 + }
85 +
86 + // parse group type
87 + String type = nullIsIllegal(json.get(TYPE), TYPE + MISSING_MEMBER_MESSAGE).asText();
88 + GroupBucket groupBucket = null;
89 +
90 + switch (type) {
91 + case "SELECT":
92 + // parse weight
93 + int weightInt = nullIsIllegal(json.get(WEIGHT), WEIGHT + MISSING_MEMBER_MESSAGE).asInt();
94 +
95 + groupBucket =
96 + DefaultGroupBucket.createSelectGroupBucket(trafficTreatment, (short) weightInt);
97 + break;
98 + case "INDIRECT":
99 + groupBucket =
100 + DefaultGroupBucket.createIndirectGroupBucket(trafficTreatment);
101 + break;
102 + case "ALL":
103 + groupBucket =
104 + DefaultGroupBucket.createAllGroupBucket(trafficTreatment);
105 + break;
106 + case "FAILOVER":
107 + // parse watchPort
108 + PortNumber watchPort = PortNumber.portNumber(nullIsIllegal(json.get(WATCH_PORT),
109 + WATCH_PORT + MISSING_MEMBER_MESSAGE).asText());
110 +
111 + // parse watchGroup
112 + int groupIdInt = nullIsIllegal(json.get(WATCH_GROUP),
113 + WATCH_GROUP + MISSING_MEMBER_MESSAGE).asInt();
114 + GroupId watchGroup = new DefaultGroupId((short) groupIdInt);
115 +
116 + groupBucket =
117 + DefaultGroupBucket.createFailoverGroupBucket(trafficTreatment, watchPort, watchGroup);
118 + break;
119 + default:
120 + DefaultGroupBucket.createAllGroupBucket(trafficTreatment);
121 + }
122 +
123 + return groupBucket;
124 + }
64 } 125 }
......
...@@ -15,20 +15,40 @@ ...@@ -15,20 +15,40 @@
15 */ 15 */
16 package org.onosproject.codec.impl; 16 package org.onosproject.codec.impl;
17 17
18 +import com.fasterxml.jackson.databind.JsonNode;
19 +import com.fasterxml.jackson.databind.node.ArrayNode;
20 +import com.fasterxml.jackson.databind.node.ObjectNode;
18 import org.onosproject.codec.CodecContext; 21 import org.onosproject.codec.CodecContext;
19 import org.onosproject.codec.JsonCodec; 22 import org.onosproject.codec.JsonCodec;
23 +import org.onosproject.core.ApplicationId;
24 +import org.onosproject.core.CoreService;
25 +import org.onosproject.core.DefaultGroupId;
26 +import org.onosproject.core.GroupId;
27 +import org.onosproject.net.DeviceId;
28 +import org.onosproject.net.group.DefaultGroup;
29 +import org.onosproject.net.group.DefaultGroupDescription;
30 +import org.onosproject.net.group.DefaultGroupKey;
20 import org.onosproject.net.group.Group; 31 import org.onosproject.net.group.Group;
21 import org.onosproject.net.group.GroupBucket; 32 import org.onosproject.net.group.GroupBucket;
33 +import org.onosproject.net.group.GroupBuckets;
34 +import org.onosproject.net.group.GroupDescription;
35 +import org.onosproject.net.group.GroupKey;
36 +import org.slf4j.Logger;
22 37
23 -import com.fasterxml.jackson.databind.node.ArrayNode; 38 +import java.util.ArrayList;
24 -import com.fasterxml.jackson.databind.node.ObjectNode; 39 +import java.util.List;
40 +import java.util.stream.IntStream;
25 41
26 import static com.google.common.base.Preconditions.checkNotNull; 42 import static com.google.common.base.Preconditions.checkNotNull;
43 +import static org.onlab.util.Tools.nullIsIllegal;
44 +import static org.slf4j.LoggerFactory.getLogger;
27 45
28 /** 46 /**
29 * Group JSON codec. 47 * Group JSON codec.
30 */ 48 */
31 public final class GroupCodec extends JsonCodec<Group> { 49 public final class GroupCodec extends JsonCodec<Group> {
50 + private final Logger log = getLogger(getClass());
51 +
32 // JSON field names 52 // JSON field names
33 private static final String ID = "id"; 53 private static final String ID = "id";
34 private static final String STATE = "state"; 54 private static final String STATE = "state";
...@@ -37,11 +57,15 @@ public final class GroupCodec extends JsonCodec<Group> { ...@@ -37,11 +57,15 @@ public final class GroupCodec extends JsonCodec<Group> {
37 private static final String BYTES = "bytes"; 57 private static final String BYTES = "bytes";
38 private static final String REFERENCE_COUNT = "referenceCount"; 58 private static final String REFERENCE_COUNT = "referenceCount";
39 private static final String TYPE = "type"; 59 private static final String TYPE = "type";
60 + private static final String GROUP_ID = "groupId";
40 private static final String DEVICE_ID = "deviceId"; 61 private static final String DEVICE_ID = "deviceId";
41 private static final String APP_ID = "appId"; 62 private static final String APP_ID = "appId";
42 private static final String APP_COOKIE = "appCookie"; 63 private static final String APP_COOKIE = "appCookie";
43 private static final String GIVEN_GROUP_ID = "givenGroupId"; 64 private static final String GIVEN_GROUP_ID = "givenGroupId";
44 private static final String BUCKETS = "buckets"; 65 private static final String BUCKETS = "buckets";
66 + private static final String MISSING_MEMBER_MESSAGE =
67 + " member is required in Group";
68 + public static final String REST_APP_ID = "org.onosproject.rest";
45 69
46 @Override 70 @Override
47 public ObjectNode encode(Group group, CodecContext context) { 71 public ObjectNode encode(Group group, CodecContext context) {
...@@ -76,4 +100,75 @@ public final class GroupCodec extends JsonCodec<Group> { ...@@ -76,4 +100,75 @@ public final class GroupCodec extends JsonCodec<Group> {
76 result.set(BUCKETS, buckets); 100 result.set(BUCKETS, buckets);
77 return result; 101 return result;
78 } 102 }
103 +
104 + @Override
105 + public Group decode(ObjectNode json, CodecContext context) {
106 + if (json == null || !json.isObject()) {
107 + return null;
108 + }
109 +
110 + final JsonCodec<GroupBucket> groupBucketCodec = context.codec(GroupBucket.class);
111 + CoreService coreService = context.getService(CoreService.class);
112 +
113 + // parse group id
114 + int groupIdInt = nullIsIllegal(json.get(GROUP_ID),
115 + GROUP_ID + MISSING_MEMBER_MESSAGE).asInt();
116 + GroupId groupId = new DefaultGroupId((short) groupIdInt);
117 +
118 + // parse group key (appCookie)
119 + String groupKeyStr = nullIsIllegal(json.get(APP_COOKIE),
120 + APP_COOKIE + MISSING_MEMBER_MESSAGE).asText();
121 + GroupKey groupKey = new DefaultGroupKey(groupKeyStr.getBytes());
122 +
123 + // parse device id
124 + DeviceId deviceId = DeviceId.deviceId(nullIsIllegal(json.get(DEVICE_ID),
125 + DEVICE_ID + MISSING_MEMBER_MESSAGE).asText());
126 +
127 + // application id
128 + ApplicationId appId = coreService.registerApplication(REST_APP_ID);
129 +
130 + // parse group type
131 + String type = nullIsIllegal(json.get(TYPE),
132 + TYPE + MISSING_MEMBER_MESSAGE).asText();
133 + GroupDescription.Type groupType = null;
134 +
135 + switch (type) {
136 + case "SELECT":
137 + groupType = Group.Type.SELECT;
138 + break;
139 + case "INDIRECT":
140 + groupType = Group.Type.INDIRECT;
141 + break;
142 + case "ALL":
143 + groupType = Group.Type.ALL;
144 + break;
145 + case "FAILOVER":
146 + groupType = Group.Type.FAILOVER;
147 + break;
148 + default:
149 + log.warn("The requested type {} is not defined for group.", type);
150 + return null;
151 + }
152 +
153 + // parse group buckets
154 + // TODO: make sure that INDIRECT group only has one bucket
155 + GroupBuckets buckets = null;
156 + List<GroupBucket> groupBucketList = new ArrayList<>();
157 + JsonNode bucketsJson = json.get(BUCKETS);
158 + checkNotNull(bucketsJson);
159 + if (bucketsJson != null) {
160 + IntStream.range(0, bucketsJson.size())
161 + .forEach(i -> {
162 + ObjectNode bucketJson = get(bucketsJson, i);
163 + bucketJson.put("type", type);
164 + groupBucketList.add(groupBucketCodec.decode(bucketJson, context));
165 + });
166 + buckets = new GroupBuckets(groupBucketList);
167 + }
168 +
169 + GroupDescription groupDescription = new DefaultGroupDescription(deviceId,
170 + groupType, buckets, groupKey, groupIdInt, appId);
171 +
172 + return new DefaultGroup(groupId, groupDescription);
173 + }
79 } 174 }
......
...@@ -15,21 +15,37 @@ ...@@ -15,21 +15,37 @@
15 */ 15 */
16 package org.onosproject.codec.impl; 16 package org.onosproject.codec.impl;
17 17
18 +import com.fasterxml.jackson.databind.JsonNode;
19 +import com.fasterxml.jackson.databind.node.ObjectNode;
20 +import com.google.common.collect.ImmutableList;
21 +import org.junit.Before;
18 import org.junit.Test; 22 import org.junit.Test;
23 +import org.onosproject.codec.JsonCodec;
24 +import org.onosproject.core.CoreService;
19 import org.onosproject.core.DefaultGroupId; 25 import org.onosproject.core.DefaultGroupId;
20 import org.onosproject.net.NetTestTools; 26 import org.onosproject.net.NetTestTools;
27 +import org.onosproject.net.PortNumber;
21 import org.onosproject.net.flow.DefaultTrafficTreatment; 28 import org.onosproject.net.flow.DefaultTrafficTreatment;
29 +import org.onosproject.net.flow.instructions.Instruction;
30 +import org.onosproject.net.flow.instructions.Instructions;
22 import org.onosproject.net.group.DefaultGroup; 31 import org.onosproject.net.group.DefaultGroup;
23 import org.onosproject.net.group.DefaultGroupBucket; 32 import org.onosproject.net.group.DefaultGroupBucket;
33 +import org.onosproject.net.group.Group;
24 import org.onosproject.net.group.GroupBucket; 34 import org.onosproject.net.group.GroupBucket;
25 import org.onosproject.net.group.GroupBuckets; 35 import org.onosproject.net.group.GroupBuckets;
26 import org.onosproject.net.group.GroupDescription; 36 import org.onosproject.net.group.GroupDescription;
27 37
28 -import com.fasterxml.jackson.databind.node.ObjectNode; 38 +import java.io.IOException;
29 -import com.google.common.collect.ImmutableList; 39 +import java.io.InputStream;
30 40
41 +import static org.easymock.EasyMock.createMock;
42 +import static org.easymock.EasyMock.expect;
43 +import static org.easymock.EasyMock.replay;
31 import static org.hamcrest.MatcherAssert.assertThat; 44 import static org.hamcrest.MatcherAssert.assertThat;
45 +import static org.hamcrest.Matchers.is;
46 +import static org.hamcrest.Matchers.notNullValue;
32 import static org.onosproject.codec.impl.GroupJsonMatcher.matchesGroup; 47 import static org.onosproject.codec.impl.GroupJsonMatcher.matchesGroup;
48 +import static org.onosproject.net.NetTestTools.APP_ID;
33 49
34 /** 50 /**
35 * Group codec unit tests. 51 * Group codec unit tests.
...@@ -37,8 +53,28 @@ import static org.onosproject.codec.impl.GroupJsonMatcher.matchesGroup; ...@@ -37,8 +53,28 @@ import static org.onosproject.codec.impl.GroupJsonMatcher.matchesGroup;
37 53
38 public class GroupCodecTest { 54 public class GroupCodecTest {
39 55
56 + MockCodecContext context;
57 + JsonCodec<Group> groupCodec;
58 + final CoreService mockCoreService = createMock(CoreService.class);
59 +
60 + /**
61 + * Sets up for each test. Creates a context and fetches the flow rule
62 + * codec.
63 + */
64 + @Before
65 + public void setUp() {
66 + context = new MockCodecContext();
67 + groupCodec = context.codec(Group.class);
68 + assertThat(groupCodec, notNullValue());
69 +
70 + expect(mockCoreService.registerApplication(GroupCodec.REST_APP_ID))
71 + .andReturn(APP_ID).anyTimes();
72 + replay(mockCoreService);
73 + context.registerService(CoreService.class, mockCoreService);
74 + }
75 +
40 @Test 76 @Test
41 - public void codecTest() { 77 + public void codecEncodeTest() {
42 GroupBucket bucket1 = DefaultGroupBucket 78 GroupBucket bucket1 = DefaultGroupBucket
43 .createSelectGroupBucket(DefaultTrafficTreatment.emptyTreatment()); 79 .createSelectGroupBucket(DefaultTrafficTreatment.emptyTreatment());
44 GroupBucket bucket2 = DefaultGroupBucket 80 GroupBucket bucket2 = DefaultGroupBucket
...@@ -58,4 +94,48 @@ public class GroupCodecTest { ...@@ -58,4 +94,48 @@ public class GroupCodecTest {
58 94
59 assertThat(groupJson, matchesGroup(group)); 95 assertThat(groupJson, matchesGroup(group));
60 } 96 }
97 +
98 + @Test
99 + public void codecDecodeTest() throws IOException {
100 + Group group = getGroup("simple-group.json");
101 + checkCommonData(group);
102 +
103 + assertThat(group.buckets().buckets().size(), is(1));
104 + GroupBucket groupBucket = group.buckets().buckets().get(0);
105 + assertThat(groupBucket.type().toString(), is("ALL"));
106 + assertThat(groupBucket.treatment().allInstructions().size(), is(1));
107 + Instruction instruction1 = groupBucket.treatment().allInstructions().get(0);
108 + assertThat(instruction1.type(), is(Instruction.Type.OUTPUT));
109 + assertThat(((Instructions.OutputInstruction) instruction1).port(), is(PortNumber.portNumber(2)));
110 + }
111 +
112 + /**
113 + * Checks that the data shared by all the resource is correct for a given group.
114 + *
115 + * @param group group to check
116 + */
117 + private void checkCommonData(Group group) {
118 + assertThat(group.appId(), is(APP_ID));
119 + assertThat(group.deviceId().toString(), is("of:0000000000000001"));
120 + assertThat(group.type().toString(), is("ALL"));
121 + assertThat(group.appCookie().key(), is("1".getBytes()));
122 + assertThat(group.id().id(), is(1));
123 + }
124 +
125 + /**
126 + * Reads in a group from the given resource and decodes it.
127 + *
128 + * @param resourceName resource to use to read the JSON for the rule
129 + * @return decoded group
130 + * @throws IOException if processing the resource fails
131 + */
132 + private Group getGroup(String resourceName) throws IOException {
133 + InputStream jsonStream = GroupCodecTest.class
134 + .getResourceAsStream(resourceName);
135 + JsonNode json = context.mapper().readTree(jsonStream);
136 + assertThat(json, notNullValue());
137 + Group group = groupCodec.decode((ObjectNode) json, context);
138 + assertThat(group, notNullValue());
139 + return group;
140 + }
61 } 141 }
......
1 +{
2 + "type": "ALL",
3 + "deviceId": "of:0000000000000001",
4 + "appCookie": "1",
5 + "groupId": "1",
6 + "buckets": [
7 + {
8 + "treatment": {
9 + "instructions": [
10 + {
11 + "type": "OUTPUT",
12 + "port": 2
13 + }
14 + ]
15 + }
16 + }
17 + ]
18 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -37,6 +37,7 @@ public class CoreWebApplication extends AbstractWebApplication { ...@@ -37,6 +37,7 @@ public class CoreWebApplication extends AbstractWebApplication {
37 HostsWebResource.class, 37 HostsWebResource.class,
38 IntentsWebResource.class, 38 IntentsWebResource.class,
39 FlowsWebResource.class, 39 FlowsWebResource.class,
40 + GroupsWebResource.class,
40 TopologyWebResource.class, 41 TopologyWebResource.class,
41 ConfigWebResource.class, 42 ConfigWebResource.class,
42 PathsWebResource.class, 43 PathsWebResource.class,
......
1 +/*
2 + * Copyright 2014-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.resources;
17 +
18 +import com.fasterxml.jackson.databind.JsonNode;
19 +import com.fasterxml.jackson.databind.node.ArrayNode;
20 +import com.fasterxml.jackson.databind.node.ObjectNode;
21 +import org.onosproject.net.Device;
22 +import org.onosproject.net.DeviceId;
23 +import org.onosproject.net.device.DeviceService;
24 +import org.onosproject.net.group.DefaultGroupDescription;
25 +import org.onosproject.net.group.DefaultGroupKey;
26 +import org.onosproject.net.group.Group;
27 +import org.onosproject.net.group.GroupDescription;
28 +import org.onosproject.net.group.GroupKey;
29 +import org.onosproject.net.group.GroupService;
30 +import org.onosproject.rest.AbstractWebResource;
31 +
32 +import javax.ws.rs.Consumes;
33 +import javax.ws.rs.DELETE;
34 +import javax.ws.rs.GET;
35 +import javax.ws.rs.POST;
36 +import javax.ws.rs.Path;
37 +import javax.ws.rs.PathParam;
38 +import javax.ws.rs.Produces;
39 +import javax.ws.rs.core.MediaType;
40 +import javax.ws.rs.core.Response;
41 +import java.io.IOException;
42 +import java.io.InputStream;
43 +import java.net.URI;
44 +import java.net.URISyntaxException;
45 +
46 +/**
47 + * Query and program group rules.
48 + */
49 +
50 +@Path("groups")
51 +public class GroupsWebResource extends AbstractWebResource {
52 + public static final String DEVICE_INVALID = "Invalid deviceId in group creation request";
53 +
54 + final GroupService groupService = get(GroupService.class);
55 + final ObjectNode root = mapper().createObjectNode();
56 + final ArrayNode groupsNode = root.putArray("groups");
57 +
58 + /**
59 + * Returns all groups of all devices.
60 + * @onos.rsModel Groups
61 + * @return array of all the groups in the system
62 + */
63 + @GET
64 + @Produces(MediaType.APPLICATION_JSON)
65 + public Response getGroups() {
66 + final Iterable<Device> devices = get(DeviceService.class).getDevices();
67 + devices.forEach(device -> {
68 + final Iterable<Group> groups = groupService.getGroups(device.id());
69 + if (groups != null) {
70 + groups.forEach(group -> groupsNode.add(codec(Group.class).encode(group, this)));
71 + }
72 + });
73 +
74 + return ok(root).build();
75 + }
76 +
77 + /**
78 + * Returns all groups associated with the given device.
79 + *
80 + * @param deviceId device identifier
81 + * @onos.rsModel Groups
82 + * @return array of all the groups in the system
83 + */
84 + @GET
85 + @Produces(MediaType.APPLICATION_JSON)
86 + @Path("{deviceId}")
87 + public Response getGroupsByDeviceId(@PathParam("deviceId") String deviceId) {
88 + final Iterable<Group> groups = groupService.getGroups(DeviceId.deviceId(deviceId));
89 +
90 + groups.forEach(group -> groupsNode.add(codec(Group.class).encode(group, this)));
91 +
92 + return ok(root).build();
93 + }
94 +
95 + /**
96 + * Create new group rule. Creates and installs a new group rule for the
97 + * specified device.
98 + *
99 + * @param deviceId device identifier
100 + * @param stream group rule JSON
101 + * @onos.rsModel GroupsPost
102 + * @return status of the request - CREATED if the JSON is correct,
103 + * BAD_REQUEST if the JSON is invalid
104 + */
105 + @POST
106 + @Path("{deviceId}")
107 + @Consumes(MediaType.APPLICATION_JSON)
108 + @Produces(MediaType.APPLICATION_JSON)
109 + public Response createGroup(@PathParam("deviceId") String deviceId,
110 + InputStream stream) {
111 + URI location;
112 + try {
113 + ObjectNode jsonTree = (ObjectNode) mapper().readTree(stream);
114 + JsonNode specifiedDeviceId = jsonTree.get("deviceId");
115 +
116 + if (specifiedDeviceId != null &&
117 + !specifiedDeviceId.asText().equals(deviceId)) {
118 + throw new IllegalArgumentException(DEVICE_INVALID);
119 + }
120 + jsonTree.put("deviceId", deviceId);
121 + Group group = codec(Group.class).decode(jsonTree, this);
122 + GroupDescription description = new DefaultGroupDescription(
123 + group.deviceId(), group.type(), group.buckets(),
124 + group.appCookie(), group.id().id(), group.appId());
125 + groupService.addGroup(description);
126 + location = new URI(Long.toString(group.id().id()));
127 + } catch (IOException | URISyntaxException ex) {
128 + throw new IllegalArgumentException(ex);
129 + }
130 +
131 + return Response
132 + .created(location)
133 + .build();
134 + }
135 +
136 + /**
137 + * Removes the specified group.
138 + *
139 + * @param deviceId device identifier
140 + * @param appCookie application cookie to be used for lookup
141 + */
142 + @DELETE
143 + @Produces(MediaType.APPLICATION_JSON)
144 + @Path("{deviceId}/{appCookie}")
145 + public void deleteGroupByDeviceIdAndAppCookie(@PathParam("deviceId") String deviceId,
146 + @PathParam("appCookie") String appCookie) {
147 + DeviceId deviceIdInstance = DeviceId.deviceId(deviceId);
148 + GroupKey appCookieInstance = new DefaultGroupKey(appCookie.getBytes());
149 +
150 + groupService.removeGroup(deviceIdInstance, appCookieInstance, null);
151 + }
152 +}
1 +{
2 + "type": "object",
3 + "title": "groups",
4 + "required": [
5 + "groups"
6 + ],
7 + "properties": {
8 + "groups": {
9 + "type": "array",
10 + "xml": {
11 + "name": "groups",
12 + "wrapped": true
13 + },
14 + "items": {
15 + "type": "object",
16 + "title": "group",
17 + "required": [
18 + "id",
19 + "state",
20 + "life",
21 + "packets",
22 + "bytes",
23 + "referenceCount",
24 + "type",
25 + "deviceId",
26 + "buckets"
27 + ],
28 + "properties": {
29 + "id": {
30 + "type": "string",
31 + "example": "1"
32 + },
33 + "state": {
34 + "type": "string",
35 + "example": "PENDING_ADD"
36 + },
37 + "life": {
38 + "type": "integer",
39 + "format": "int64",
40 + "example": 69889
41 + },
42 + "packets": {
43 + "type": "integer",
44 + "format": "int64",
45 + "example": 22546
46 + },
47 + "bytes": {
48 + "type": "integer",
49 + "format": "int64",
50 + "example": 1826226
51 + },
52 + "referenceCount": {
53 + "type": "integer",
54 + "format": "int64",
55 + "example": 1826226
56 + },
57 + "type": {
58 + "type": "string",
59 + "example": "ALL"
60 + },
61 + "deviceId": {
62 + "type": "string",
63 + "example": "of:0000000000000003"
64 + },
65 + "buckets": {
66 + "type": "array",
67 + "xml": {
68 + "name": "buckets",
69 + "wrapped": true
70 + },
71 + "items": {
72 + "type": "object",
73 + "title": "buckets",
74 + "required": [
75 + "treatment",
76 + "weight",
77 + "watchPort",
78 + "watchGroup"
79 + ],
80 + "properties": {
81 + "treatment": {
82 + "type": "object",
83 + "title": "treatment",
84 + "required": [
85 + "instructions",
86 + "deferred"
87 + ],
88 + "properties": {
89 + "instructions": {
90 + "type": "array",
91 + "title": "treatment",
92 + "required": [
93 + "properties",
94 + "port"
95 + ],
96 + "items": {
97 + "type": "object",
98 + "title": "instructions",
99 + "required": [
100 + "type",
101 + "port"
102 + ],
103 + "properties": {
104 + "type": {
105 + "type": "string",
106 + "example": "OUTPUT"
107 + },
108 + "port": {
109 + "type": "string",
110 + "example": "2"
111 + }
112 + }
113 + }
114 + }
115 + }
116 + }
117 + }
118 + }
119 + }
120 + }
121 + }
122 + }
123 + }
124 +}
...\ No newline at end of file ...\ No newline at end of file
1 +{
2 + "type": "object",
3 + "title": "group",
4 + "required": [
5 + "type",
6 + "deviceId",
7 + "appCookie",
8 + "groupId",
9 + "buckets"
10 + ],
11 + "properties": {
12 + "type": {
13 + "type": "string",
14 + "example": "ALL"
15 + },
16 + "deviceId": {
17 + "type": "string",
18 + "example": "of:0000000000000001"
19 + },
20 + "appCookie": {
21 + "type": "string",
22 + "example": "1"
23 + },
24 + "groupId": {
25 + "type": "string",
26 + "example": "1"
27 + },
28 + "buckets": {
29 + "type": "array",
30 + "xml": {
31 + "name": "buckets",
32 + "wrapped": true
33 + },
34 + "items": {
35 + "type": "object",
36 + "title": "buckets",
37 + "required": [
38 + "treatment",
39 + "weight",
40 + "watchPort",
41 + "watchGroup"
42 + ],
43 + "properties": {
44 + "treatment": {
45 + "type": "object",
46 + "title": "treatment",
47 + "required": [
48 + "instructions",
49 + "deferred"
50 + ],
51 + "properties": {
52 + "instructions": {
53 + "type": "array",
54 + "title": "treatment",
55 + "required": [
56 + "properties",
57 + "port"
58 + ],
59 + "items": {
60 + "type": "object",
61 + "title": "instructions",
62 + "required": [
63 + "type",
64 + "port"
65 + ],
66 + "properties": {
67 + "type": {
68 + "type": "string",
69 + "example": "OUTPUT"
70 + },
71 + "port": {
72 + "type": "string",
73 + "example": "2"
74 + }
75 + }
76 + }
77 + }
78 + }
79 + }
80 + }
81 + }
82 + }
83 + }
84 +}
...\ No newline at end of file ...\ No newline at end of file
1 +/*
2 + * Copyright 2014-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 +package org.onosproject.rest;
18 +
19 +import com.eclipsesource.json.JsonArray;
20 +import com.eclipsesource.json.JsonObject;
21 +import com.google.common.collect.ImmutableSet;
22 +import com.sun.jersey.api.client.ClientResponse;
23 +import com.sun.jersey.api.client.WebResource;
24 +import org.hamcrest.Description;
25 +import org.hamcrest.TypeSafeMatcher;
26 +import org.junit.After;
27 +import org.junit.Before;
28 +import org.junit.Test;
29 +import org.onlab.osgi.ServiceDirectory;
30 +import org.onlab.osgi.TestServiceDirectory;
31 +import org.onlab.rest.BaseResource;
32 +import org.onosproject.codec.CodecService;
33 +import org.onosproject.codec.impl.CodecManager;
34 +import org.onosproject.codec.impl.GroupCodec;
35 +import org.onosproject.core.ApplicationId;
36 +import org.onosproject.core.CoreService;
37 +import org.onosproject.core.DefaultApplicationId;
38 +import org.onosproject.core.DefaultGroupId;
39 +import org.onosproject.core.GroupId;
40 +import org.onosproject.net.DefaultDevice;
41 +import org.onosproject.net.Device;
42 +import org.onosproject.net.DeviceId;
43 +import org.onosproject.net.NetTestTools;
44 +import org.onosproject.net.device.DeviceService;
45 +import org.onosproject.net.group.DefaultGroupKey;
46 +import org.onosproject.net.group.Group;
47 +import org.onosproject.net.group.GroupBucket;
48 +import org.onosproject.net.group.GroupBuckets;
49 +import org.onosproject.net.group.GroupDescription;
50 +import org.onosproject.net.group.GroupKey;
51 +import org.onosproject.net.group.GroupService;
52 +
53 +import javax.ws.rs.core.MediaType;
54 +import java.io.InputStream;
55 +import java.net.HttpURLConnection;
56 +import java.util.ArrayList;
57 +import java.util.HashMap;
58 +import java.util.HashSet;
59 +import java.util.List;
60 +import java.util.Set;
61 +
62 +import static org.easymock.EasyMock.anyObject;
63 +import static org.easymock.EasyMock.anyShort;
64 +import static org.easymock.EasyMock.createMock;
65 +import static org.easymock.EasyMock.expect;
66 +import static org.easymock.EasyMock.expectLastCall;
67 +import static org.easymock.EasyMock.replay;
68 +import static org.easymock.EasyMock.verify;
69 +import static org.hamcrest.Matchers.hasSize;
70 +import static org.hamcrest.Matchers.is;
71 +import static org.hamcrest.Matchers.notNullValue;
72 +import static org.junit.Assert.assertThat;
73 +import static org.onosproject.net.NetTestTools.APP_ID;
74 +
75 +/**
76 + * Unit tests for Groups REST APIs.
77 + */
78 +public class GroupsResourceTest extends ResourceTest {
79 + final GroupService mockGroupService = createMock(GroupService.class);
80 + CoreService mockCoreService = createMock(CoreService.class);
81 + final DeviceService mockDeviceService = createMock(DeviceService.class);
82 +
83 + final HashMap<DeviceId, Set<Group>> groups = new HashMap<>();
84 +
85 +
86 + final DeviceId deviceId1 = DeviceId.deviceId("1");
87 + final DeviceId deviceId2 = DeviceId.deviceId("2");
88 + final DeviceId deviceId3 = DeviceId.deviceId("3");
89 + final Device device1 = new DefaultDevice(null, deviceId1, Device.Type.OTHER,
90 + "", "", "", "", null);
91 + final Device device2 = new DefaultDevice(null, deviceId2, Device.Type.OTHER,
92 + "", "", "", "", null);
93 +
94 + final MockGroup group1 = new MockGroup(deviceId1, 1, "111", 1);
95 + final MockGroup group2 = new MockGroup(deviceId1, 2, "222", 2);
96 +
97 + final MockGroup group3 = new MockGroup(deviceId2, 3, "333", 3);
98 + final MockGroup group4 = new MockGroup(deviceId2, 4, "444", 4);
99 +
100 + final MockGroup group5 = new MockGroup(deviceId3, 5, "555", 5);
101 + final MockGroup group6 = new MockGroup(deviceId3, 6, "666", 6);
102 +
103 + /**
104 + * Mock class for a group.
105 + */
106 + private static class MockGroup implements Group {
107 +
108 + final DeviceId deviceId;
109 + final ApplicationId appId;
110 + final GroupKey appCookie;
111 + final long baseValue;
112 + final List<GroupBucket> bucketList;
113 + GroupBuckets buckets;
114 +
115 + public MockGroup(DeviceId deviceId, int appId, String appCookie, int id) {
116 + this.deviceId = deviceId;
117 + this.appId = new DefaultApplicationId(appId, String.valueOf(appId));
118 + this.appCookie = new DefaultGroupKey(appCookie.getBytes());
119 + this.baseValue = id * 100;
120 + this.bucketList = new ArrayList<>();
121 + this.buckets = new GroupBuckets(bucketList);
122 + }
123 +
124 + @Override
125 + public GroupId id() {
126 + return new DefaultGroupId((int) baseValue + 55);
127 + }
128 +
129 + @Override
130 + public GroupState state() {
131 + return GroupState.ADDED;
132 + }
133 +
134 + @Override
135 + public long life() {
136 + return baseValue + 11;
137 + }
138 +
139 + @Override
140 + public long packets() {
141 + return baseValue + 22;
142 + }
143 +
144 + @Override
145 + public long bytes() {
146 + return baseValue + 33;
147 + }
148 +
149 + @Override
150 + public long referenceCount() {
151 + return baseValue + 44;
152 + }
153 +
154 + @Override
155 + public Type type() {
156 + return GroupDescription.Type.ALL;
157 + }
158 +
159 + @Override
160 + public DeviceId deviceId() {
161 + return this.deviceId;
162 + }
163 +
164 + @Override
165 + public ApplicationId appId() {
166 + return this.appId;
167 + }
168 +
169 + @Override
170 + public GroupKey appCookie() {
171 + return this.appCookie;
172 + }
173 +
174 + @Override
175 + public Integer givenGroupId() {
176 + return (int) baseValue + 55;
177 + }
178 +
179 + @Override
180 + public GroupBuckets buckets() {
181 + return this.buckets;
182 + }
183 + }
184 +
185 + /**
186 + * Populates some groups used as testing data.
187 + */
188 + private void setupMockGroups() {
189 + final Set<Group> groups1 = new HashSet<>();
190 + groups1.add(group1);
191 + groups1.add(group2);
192 +
193 + final Set<Group> groups2 = new HashSet<>();
194 + groups2.add(group3);
195 + groups2.add(group4);
196 +
197 + groups.put(deviceId1, groups1);
198 + groups.put(deviceId2, groups2);
199 +
200 + expect(mockGroupService.getGroups(deviceId1))
201 + .andReturn(groups.get(deviceId1)).anyTimes();
202 + expect(mockGroupService.getGroups(deviceId2))
203 + .andReturn(groups.get(deviceId2)).anyTimes();
204 + }
205 +
206 + /**
207 + * Sets up the global values for all the tests.
208 + */
209 + @Before
210 + public void setUpTest() {
211 + // Mock device service
212 + expect(mockDeviceService.getDevice(deviceId1))
213 + .andReturn(device1);
214 + expect(mockDeviceService.getDevice(deviceId2))
215 + .andReturn(device2);
216 + expect(mockDeviceService.getDevices())
217 + .andReturn(ImmutableSet.of(device1, device2));
218 +
219 + // Mock Core Service
220 + expect(mockCoreService.getAppId(anyShort()))
221 + .andReturn(NetTestTools.APP_ID).anyTimes();
222 + expect(mockCoreService.registerApplication(GroupCodec.REST_APP_ID))
223 + .andReturn(APP_ID).anyTimes();
224 + replay(mockCoreService);
225 +
226 + // Register the services needed for the test
227 + final CodecManager codecService = new CodecManager();
228 + codecService.activate();
229 + ServiceDirectory testDirectory =
230 + new TestServiceDirectory()
231 + .add(GroupService.class, mockGroupService)
232 + .add(DeviceService.class, mockDeviceService)
233 + .add(CodecService.class, codecService)
234 + .add(CoreService.class, mockCoreService);
235 +
236 + BaseResource.setServiceDirectory(testDirectory);
237 + }
238 +
239 + /**
240 + * Cleans up and verifies the mocks.
241 + */
242 + @After
243 + public void tearDownTest() {
244 + verify(mockGroupService);
245 + verify(mockCoreService);
246 + }
247 +
248 + /**
249 + * Hamcrest matcher to check that a group representation in JSON matches
250 + * the actual group.
251 + */
252 + public static class GroupJsonMatcher extends TypeSafeMatcher<JsonObject> {
253 + private final Group group;
254 + private final String expectedAppId;
255 + private String reason = "";
256 +
257 + public GroupJsonMatcher(Group groupValue, String expectedAppIdValue) {
258 + group = groupValue;
259 + expectedAppId = expectedAppIdValue;
260 + }
261 +
262 + @Override
263 + public boolean matchesSafely(JsonObject jsonGroup) {
264 + // check id
265 + final String jsonId = jsonGroup.get("id").asString();
266 + final String groupId = group.id().toString();
267 + if (!jsonId.equals(groupId)) {
268 + reason = "id " + group.id().toString();
269 + return false;
270 + }
271 +
272 + // check application id
273 + final String jsonAppId = jsonGroup.get("appId").asString();
274 + final String appId = group.appId().toString();
275 + if (!jsonAppId.equals(appId)) {
276 + reason = "appId " + group.appId().toString();
277 + return false;
278 + }
279 +
280 + // check device id
281 + final String jsonDeviceId = jsonGroup.get("deviceId").asString();
282 + if (!jsonDeviceId.equals(group.deviceId().toString())) {
283 + reason = "deviceId " + group.deviceId();
284 + return false;
285 + }
286 +
287 + // check bucket array
288 + if (group.buckets().buckets() != null) {
289 + final JsonArray jsonBuckets = jsonGroup.get("buckets").asArray();
290 + if (group.buckets().buckets().size() != jsonBuckets.size()) {
291 + reason = "buckets array size of " +
292 + Integer.toString(group.buckets().buckets().size());
293 + return false;
294 + }
295 + for (final GroupBucket groupBucket : group.buckets().buckets()) {
296 + boolean groupBucketFound = false;
297 + for (int groupBucketIndex = 0; groupBucketIndex < jsonBuckets.size(); groupBucketIndex++) {
298 + final String jsonType = jsonBuckets.get(groupBucketIndex).asObject().get("type").asString();
299 + final String bucketType = groupBucket.type().name();
300 + if (jsonType.equals(bucketType)) {
301 + groupBucketFound = true;
302 + }
303 + }
304 + if (!groupBucketFound) {
305 + reason = "group bucket " + groupBucket.toString();
306 + return false;
307 + }
308 + }
309 + }
310 +
311 + return true;
312 + }
313 +
314 + @Override
315 + public void describeTo(Description description) {
316 + description.appendText(reason);
317 + }
318 + }
319 +
320 + /**
321 + * Factory to allocate a group matcher.
322 + *
323 + * @param group group object we are looking for
324 + * @return matcher
325 + */
326 + private static GroupJsonMatcher matchesGroup(Group group, String expectedAppName) {
327 + return new GroupJsonMatcher(group, expectedAppName);
328 + }
329 +
330 + /**
331 + * Hamcrest matcher to check that a group is represented properly in a JSON
332 + * array of flows.
333 + */
334 + public static class GroupJsonArrayMatcher extends TypeSafeMatcher<JsonArray> {
335 + private final Group group;
336 + private String reason = "";
337 +
338 + public GroupJsonArrayMatcher(Group groupValue) {
339 + group = groupValue;
340 + }
341 +
342 + @Override
343 + public boolean matchesSafely(JsonArray json) {
344 + boolean groupFound = false;
345 + for (int jsonGroupIndex = 0; jsonGroupIndex < json.size();
346 + jsonGroupIndex++) {
347 +
348 + final JsonObject jsonGroup = json.get(jsonGroupIndex).asObject();
349 +
350 + final String groupId = group.id().toString();
351 + final String jsonGroupId = jsonGroup.get("id").asString();
352 + if (jsonGroupId.equals(groupId)) {
353 + groupFound = true;
354 +
355 + // We found the correct group, check attribute values
356 + assertThat(jsonGroup, matchesGroup(group, APP_ID.name()));
357 + }
358 + }
359 + if (!groupFound) {
360 + reason = "Group with id " + group.id().toString() + " not found";
361 + return false;
362 + } else {
363 + return true;
364 + }
365 + }
366 +
367 + @Override
368 + public void describeTo(Description description) {
369 + description.appendText(reason);
370 + }
371 + }
372 +
373 + /**
374 + * Factory to allocate a group array matcher.
375 + *
376 + * @param group group object we are looking for
377 + * @return matcher
378 + */
379 + private static GroupJsonArrayMatcher hasGroup(Group group) {
380 + return new GroupJsonArrayMatcher(group);
381 + }
382 +
383 + /**
384 + * Tests the result of the rest api GET when there are no groups.
385 + */
386 + @Test
387 + public void testGroupsEmptyArray() {
388 + expect(mockGroupService.getGroups(deviceId1)).andReturn(null).anyTimes();
389 + expect(mockGroupService.getGroups(deviceId2)).andReturn(null).anyTimes();
390 + replay(mockGroupService);
391 + replay(mockDeviceService);
392 + final WebResource rs = resource();
393 + final String response = rs.path("groups").get(String.class);
394 + assertThat(response, is("{\"groups\":[]}"));
395 + }
396 +
397 + /**
398 + * Tests the result of the rest api GET when there are active groups.
399 + */
400 + @Test
401 + public void testGroupsPopulatedArray() {
402 + setupMockGroups();
403 + replay(mockGroupService);
404 + replay(mockDeviceService);
405 + final WebResource rs = resource();
406 + final String response = rs.path("groups").get(String.class);
407 + final JsonObject result = JsonObject.readFrom(response);
408 + assertThat(result, notNullValue());
409 +
410 + assertThat(result.names(), hasSize(1));
411 + assertThat(result.names().get(0), is("groups"));
412 + final JsonArray jsonGroups = result.get("groups").asArray();
413 + assertThat(jsonGroups, notNullValue());
414 + assertThat(jsonGroups, hasGroup(group1));
415 + assertThat(jsonGroups, hasGroup(group2));
416 + assertThat(jsonGroups, hasGroup(group3));
417 + assertThat(jsonGroups, hasGroup(group4));
418 + }
419 +
420 + /**
421 + * Tests the result of a rest api GET for a device.
422 + */
423 + @Test
424 + public void testGroupsSingleDevice() {
425 + setupMockGroups();
426 + final Set<Group> groups = new HashSet<>();
427 + groups.add(group5);
428 + groups.add(group6);
429 + expect(mockGroupService.getGroups(anyObject()))
430 + .andReturn(groups).anyTimes();
431 + replay(mockGroupService);
432 + replay(mockDeviceService);
433 + final WebResource rs = resource();
434 + final String response = rs.path("groups/" + deviceId3).get(String.class);
435 + final JsonObject result = JsonObject.readFrom(response);
436 + assertThat(result, notNullValue());
437 +
438 + assertThat(result.names(), hasSize(1));
439 + assertThat(result.names().get(0), is("groups"));
440 + final JsonArray jsonFlows = result.get("groups").asArray();
441 + assertThat(jsonFlows, notNullValue());
442 + assertThat(jsonFlows, hasGroup(group5));
443 + assertThat(jsonFlows, hasGroup(group6));
444 + }
445 +
446 + /**
447 + * Tests creating a group with POST.
448 + */
449 + @Test
450 + public void testPost() {
451 + mockGroupService.addGroup(anyObject());
452 + expectLastCall();
453 + replay(mockGroupService);
454 +
455 + WebResource rs = resource();
456 + InputStream jsonStream = GroupsResourceTest.class
457 + .getResourceAsStream("post-group.json");
458 +
459 + ClientResponse response = rs.path("groups/of:0000000000000001")
460 + .type(MediaType.APPLICATION_JSON_TYPE)
461 + .post(ClientResponse.class, jsonStream);
462 + assertThat(response.getStatus(), is(HttpURLConnection.HTTP_CREATED));
463 + }
464 +
465 + /**
466 + * Tests deleting a group.
467 + */
468 + @Test
469 + public void testDelete() {
470 + setupMockGroups();
471 + mockGroupService.removeGroup(anyObject(), anyObject(), anyObject());
472 + expectLastCall();
473 + replay(mockGroupService);
474 +
475 + WebResource rs = resource();
476 +
477 + String location = "/groups/1/111";
478 +
479 + ClientResponse deleteResponse = rs.path(location)
480 + .type(MediaType.APPLICATION_JSON_TYPE)
481 + .delete(ClientResponse.class);
482 + assertThat(deleteResponse.getStatus(),
483 + is(HttpURLConnection.HTTP_NO_CONTENT));
484 + }
485 +}
1 +{
2 + "type": "ALL",
3 + "deviceId": "of:0000000000000001",
4 + "appCookie": "1",
5 + "groupId": "1",
6 + "buckets": [
7 + {
8 + "treatment": {
9 + "instructions": [
10 + {
11 + "type": "OUTPUT",
12 + "port": 2
13 + }
14 + ]
15 + }
16 + }
17 + ]
18 +}
...\ No newline at end of file ...\ No newline at end of file