ONOS-7577 - REST API to add and remove buckets from a group
Change-Id: I95bad8db7baf6231ffb0c077d9e5d8243da64fd4
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/GroupBucketCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/GroupBucketCodec.java
index a9bf5d8..9170820 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/GroupBucketCodec.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/GroupBucketCodec.java
@@ -39,6 +39,7 @@
private static final String WATCH_GROUP = "watchGroup";
private static final String PACKETS = "packets";
private static final String BYTES = "bytes";
+ private static final String BUCKET_ID = "bucketId";
private static final String MISSING_MEMBER_MESSAGE =
" member is required in Group";
@@ -50,7 +51,8 @@
.put(TYPE, bucket.type().toString())
.put(WEIGHT, bucket.weight())
.put(PACKETS, bucket.packets())
- .put(BYTES, bucket.bytes());
+ .put(BYTES, bucket.bytes())
+ .put(BUCKET_ID, bucket.hashCode());
if (bucket.watchPort() != null) {
result.put(WATCH_PORT, bucket.watchPort().toString());
diff --git a/web/api/src/main/java/org/onosproject/rest/resources/GroupsWebResource.java b/web/api/src/main/java/org/onosproject/rest/resources/GroupsWebResource.java
index a0875d5..d2a361e 100644
--- a/web/api/src/main/java/org/onosproject/rest/resources/GroupsWebResource.java
+++ b/web/api/src/main/java/org/onosproject/rest/resources/GroupsWebResource.java
@@ -18,12 +18,17 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableList;
+import org.onlab.util.HexString;
+import org.onosproject.codec.JsonCodec;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.group.DefaultGroupDescription;
import org.onosproject.net.group.DefaultGroupKey;
import org.onosproject.net.group.Group;
+import org.onosproject.net.group.GroupBucket;
+import org.onosproject.net.group.GroupBuckets;
import org.onosproject.net.group.GroupDescription;
import org.onosproject.net.group.GroupKey;
import org.onosproject.net.group.GroupService;
@@ -43,8 +48,10 @@
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
-import org.onlab.util.HexString;
import static org.onlab.util.Tools.nullIsNotFound;
import static org.onlab.util.Tools.readTreeFromStream;
@@ -65,6 +72,14 @@
private final ObjectNode root = mapper().createObjectNode();
private final ArrayNode groupsNode = root.putArray("groups");
+ private GroupKey createKey(String appCookieString) {
+ if (!appCookieString.startsWith("0x")) {
+ throw new IllegalArgumentException("APP_COOKIE must be a hex string starts with 0x");
+ }
+ return new DefaultGroupKey(HexString.fromHexString(
+ appCookieString.split("0x")[1], ""));
+ }
+
/**
* Returns all groups of all devices.
*
@@ -118,11 +133,7 @@
@PathParam("appCookie") String appCookie) {
final DeviceId deviceIdInstance = DeviceId.deviceId(deviceId);
- if (!appCookie.startsWith("0x")) {
- throw new IllegalArgumentException("APP_COOKIE must be a hex string starts with 0x");
- }
- final GroupKey appCookieInstance = new DefaultGroupKey(HexString.fromHexString(
- appCookie.split("0x")[1], ""));
+ final GroupKey appCookieInstance = createKey(appCookie);
Group group = nullIsNotFound(groupService.getGroup(deviceIdInstance, appCookieInstance),
GROUP_NOT_FOUND);
@@ -187,13 +198,120 @@
@PathParam("appCookie") String appCookie) {
DeviceId deviceIdInstance = DeviceId.deviceId(deviceId);
- if (!appCookie.startsWith("0x")) {
- throw new IllegalArgumentException("APP_COOKIE must be a hex string starts with 0x");
- }
- GroupKey appCookieInstance = new DefaultGroupKey(HexString.fromHexString(
- appCookie.split("0x")[1], ""));
+ final GroupKey appCookieInstance = createKey(appCookie);
groupService.removeGroup(deviceIdInstance, appCookieInstance, null);
return Response.noContent().build();
}
+
+ /**
+ * Adds buckets to a group using the group service.
+ *
+ * @param deviceIdString device Id
+ * @param appCookieString application cookie
+ * @param stream JSON stream
+ */
+ private void updateGroupBuckets(String deviceIdString, String appCookieString, InputStream stream)
+ throws IOException {
+ DeviceId deviceId = DeviceId.deviceId(deviceIdString);
+ final GroupKey groupKey = createKey(appCookieString);
+
+ Group group = nullIsNotFound(groupService.getGroup(deviceId, groupKey), GROUP_NOT_FOUND);
+
+ ObjectNode jsonTree = readTreeFromStream(mapper(), stream);
+
+ GroupBuckets buckets = null;
+ List<GroupBucket> groupBucketList = new ArrayList<>();
+ JsonNode bucketsJson = jsonTree.get("buckets");
+ final JsonCodec<GroupBucket> groupBucketCodec = codec(GroupBucket.class);
+ if (bucketsJson != null) {
+ IntStream.range(0, bucketsJson.size())
+ .forEach(i -> {
+ ObjectNode bucketJson = (ObjectNode) bucketsJson.get(i);
+ groupBucketList.add(groupBucketCodec.decode(bucketJson, this));
+ });
+ buckets = new GroupBuckets(groupBucketList);
+ }
+ groupService.addBucketsToGroup(deviceId, groupKey, buckets, groupKey, group.appId());
+ }
+
+ /**
+ * Adds buckets to an existing group.
+ *
+ * @param deviceIdString device identifier
+ * @param appCookieString application cookie
+ * @param stream buckets JSON
+ * @return status of the request - NO_CONTENT if the JSON is correct,
+ * BAD_REQUEST if the JSON is invalid
+ * @onos.rsModel GroupsBucketsPost
+ */
+ @POST
+ @Path("{deviceId}/{appCookie}/buckets")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response addBucket(@PathParam("deviceId") String deviceIdString,
+ @PathParam("appCookie") String appCookieString,
+ InputStream stream) {
+ try {
+ updateGroupBuckets(deviceIdString, appCookieString, stream);
+
+ return Response
+ .noContent()
+ .build();
+ } catch (IOException ex) {
+ throw new IllegalArgumentException(ex);
+ }
+ }
+
+ /**
+ * Removes buckets from a group using the group service.
+ *
+ * @param deviceIdString device Id
+ * @param appCookieString application cookie
+ * @param bucketIds comma separated list of bucket Ids to remove
+ */
+ private void removeGroupBuckets(String deviceIdString, String appCookieString, String bucketIds) {
+ DeviceId deviceId = DeviceId.deviceId(deviceIdString);
+ final GroupKey groupKey = createKey(appCookieString);
+
+ Group group = nullIsNotFound(groupService.getGroup(deviceId, groupKey), GROUP_NOT_FOUND);
+
+ List<GroupBucket> groupBucketList = new ArrayList<>();
+
+ List<String> bucketsToRemove = ImmutableList.copyOf(bucketIds.split(","));
+
+ bucketsToRemove.forEach(
+ bucketIdToRemove -> {
+ group.buckets().buckets().stream()
+ .filter(bucket -> Integer.toString(bucket.hashCode()).equals(bucketIdToRemove))
+ .forEach(groupBucketList::add);
+ }
+ );
+ groupService.removeBucketsFromGroup(deviceId, groupKey,
+ new GroupBuckets(groupBucketList), groupKey,
+ group.appId());
+ }
+
+ /**
+ * Removes buckets from an existing group.
+ *
+ * @param deviceIdString device identifier
+ * @param appCookieString application cookie
+ * @param bucketIds comma separated list of identifiers of buckets to remove from this group
+ * @return status of the request - NO_CONTENT if the JSON is correct,
+ * BAD_REQUEST if the JSON is invalid
+ */
+ @DELETE
+ @Path("{deviceId}/{appCookie}/buckets/{bucketIds}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response deleteBuckets(@PathParam("deviceId") String deviceIdString,
+ @PathParam("appCookie") String appCookieString,
+ @PathParam("bucketIds") String bucketIds) {
+ removeGroupBuckets(deviceIdString, appCookieString, bucketIds);
+
+ return Response
+ .noContent()
+ .build();
+ }
}
diff --git a/web/api/src/main/resources/definitions/GroupsBucketsPost.json b/web/api/src/main/resources/definitions/GroupsBucketsPost.json
new file mode 100644
index 0000000..2229548
--- /dev/null
+++ b/web/api/src/main/resources/definitions/GroupsBucketsPost.json
@@ -0,0 +1,65 @@
+{
+ "type": "object",
+ "title": "buckets",
+ "required": [
+ "buckets"
+ ],
+ "properties": {
+ "buckets": {
+ "type": "array",
+ "xml": {
+ "name": "buckets",
+ "wrapped": true
+ },
+ "items": {
+ "type": "object",
+ "title": "buckets",
+ "required": [
+ "treatment",
+ "weight",
+ "watchPort",
+ "watchGroup"
+ ],
+ "properties": {
+ "treatment": {
+ "type": "object",
+ "title": "treatment",
+ "required": [
+ "instructions",
+ "deferred"
+ ],
+ "properties": {
+ "instructions": {
+ "type": "array",
+ "title": "treatment",
+ "required": [
+ "properties",
+ "port"
+ ],
+ "items": {
+ "type": "object",
+ "title": "instructions",
+ "required": [
+ "type",
+ "port"
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "example": "OUTPUT"
+ },
+ "port": {
+ "type": "string",
+ "example": "2"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/web/api/src/test/java/org/onosproject/rest/resources/GroupsResourceTest.java b/web/api/src/test/java/org/onosproject/rest/resources/GroupsResourceTest.java
index ab4cf9d..2cdb6ff 100644
--- a/web/api/src/test/java/org/onosproject/rest/resources/GroupsResourceTest.java
+++ b/web/api/src/test/java/org/onosproject/rest/resources/GroupsResourceTest.java
@@ -19,6 +19,7 @@
import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.hamcrest.Description;
import org.hamcrest.Matchers;
@@ -47,6 +48,7 @@
import org.onosproject.net.group.GroupDescription;
import org.onosproject.net.group.GroupKey;
import org.onosproject.net.group.GroupService;
+import org.onosproject.net.group.GroupServiceAdapter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
@@ -528,4 +530,122 @@
assertThat(deleteResponse.getStatus(),
is(HttpURLConnection.HTTP_NO_CONTENT));
}
+
+ private JsonArray fetchAndCheckBuckets(DeviceId deviceId, int expectedBucketCount) {
+ WebTarget wt = target();
+ final String fetchGroupPath = wt.path("groups/" + deviceId).request().get(String.class);
+ final JsonObject result = Json.parse(fetchGroupPath).asObject();
+ assertThat(result, notNullValue());
+ assertThat(result.names(), hasSize(1));
+ assertThat(result.names().get(0), is("groups"));
+ JsonArray jsonGroups = result.get("groups").asArray();
+ assertThat(jsonGroups, notNullValue());
+ JsonObject group = jsonGroups.get(0).asObject();
+ JsonArray buckets = group.get("buckets").asArray();
+ assertThat(buckets.size(), is(expectedBucketCount));
+ return buckets;
+ }
+
+ private void removeBucketsViaRest(String endpointPath) {
+ WebTarget wt = target();
+ Response removeResponse = wt.path(endpointPath)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .delete();
+ assertThat(removeResponse.getStatus(), is(HttpURLConnection.HTTP_NO_CONTENT));
+ }
+
+
+ class TestGroupService extends GroupServiceAdapter {
+ @Override
+ public void addBucketsToGroup(DeviceId deviceId, GroupKey oldCookie, GroupBuckets buckets,
+ GroupKey newCookie, ApplicationId appId) {
+ group1.buckets = buckets;
+ }
+
+ @Override
+ public void removeBucketsFromGroup(DeviceId deviceId, GroupKey oldCookie, GroupBuckets buckets,
+ GroupKey newCookie, ApplicationId appId) {
+ if (!group1.buckets.buckets().isEmpty()) {
+ ArrayList<GroupBucket> newList = new ArrayList<>(group1.buckets.buckets());
+ for (GroupBucket bucketToRemove : buckets.buckets()) {
+ for (GroupBucket bucketToCheck : group1.buckets.buckets()) {
+ if (bucketToCheck.equals(bucketToRemove)) {
+ newList.remove(bucketToCheck);
+ }
+ }
+ }
+ group1.buckets = new GroupBuckets(newList);
+ }
+ }
+
+ @Override
+ public Group getGroup(DeviceId deviceId, GroupKey appCookie) {
+ return group1;
+ }
+
+ @Override
+ public Iterable<Group> getGroups(DeviceId deviceId) {
+ return ImmutableList.of(group1);
+ }
+ }
+
+ /**
+ * Tests adding and removing buckets.
+ */
+ @Test
+ public void testAddRemoveBucket() {
+ String deviceIdBasePath = "groups/%s/";
+ String endpointBasePath = deviceIdBasePath + "%s/buckets/";
+ TestGroupService groupService = new TestGroupService();
+ expect(mockGroupService.getGroup(anyObject(), anyObject()))
+ .andDelegateTo(groupService).anyTimes();
+ expect(mockGroupService.getGroups(anyObject()))
+ .andDelegateTo(groupService).anyTimes();
+ mockGroupService.addBucketsToGroup(anyObject(), anyObject(), anyObject(), anyObject(), anyObject());
+ expectLastCall().andDelegateTo(groupService).anyTimes();
+ mockGroupService.removeBucketsFromGroup(anyObject(), anyObject(), anyObject(), anyObject(), anyObject());
+ expectLastCall().andDelegateTo(groupService).anyTimes();
+ replay(mockGroupService);
+
+ WebTarget wt = target();
+
+ // Add buckets
+ String addEndpointPath = String.format(endpointBasePath, group1.deviceId(), group1.appCookie());
+ InputStream addJsonStream = GroupsResourceTest.class
+ .getResourceAsStream("post-group-add-buckets.json");
+
+ Response addResponse = wt.path(addEndpointPath)
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .post(Entity.json(addJsonStream));
+ assertThat(addResponse.getStatus(), is(HttpURLConnection.HTTP_NO_CONTENT));
+
+ // Check that buckets are there
+ JsonArray bucketsAfterAdd = fetchAndCheckBuckets(deviceId1, 4);
+
+ String bucketId1 = Long.toString(bucketsAfterAdd.get(0).asObject().get("bucketId").asLong());
+ String bucketId2 = Long.toString(bucketsAfterAdd.get(1).asObject().get("bucketId").asLong());
+ String bucketId3 = Long.toString(bucketsAfterAdd.get(2).asObject().get("bucketId").asLong());
+ String bucketId4 = Long.toString(bucketsAfterAdd.get(3).asObject().get("bucketId").asLong());
+
+ // Remove one bucket
+ String removeEndpointPath = addEndpointPath + bucketId1;
+ removeBucketsViaRest(removeEndpointPath);
+ fetchAndCheckBuckets(deviceId1, 3);
+
+ // Remove two buckets
+ String removeTwoEndpointPath = addEndpointPath + bucketId2 + ',' + bucketId3;
+ removeBucketsViaRest(removeTwoEndpointPath);
+ fetchAndCheckBuckets(deviceId1, 1);
+
+ // Remove nothing - non-existent bucket id
+ String removeNothingEndpointPath = addEndpointPath + "no-such-bucket";
+ removeBucketsViaRest(removeNothingEndpointPath);
+ fetchAndCheckBuckets(deviceId1, 1);
+
+ // Remove last bucket - bucket list should be empty
+ String lastOneEndpointPath = addEndpointPath + bucketId4;
+ removeBucketsViaRest(lastOneEndpointPath);
+ fetchAndCheckBuckets(deviceId1, 0);
+ }
+
}
diff --git a/web/api/src/test/resources/org/onosproject/rest/resources/post-group-add-buckets.json b/web/api/src/test/resources/org/onosproject/rest/resources/post-group-add-buckets.json
new file mode 100644
index 0000000..82f3c9d
--- /dev/null
+++ b/web/api/src/test/resources/org/onosproject/rest/resources/post-group-add-buckets.json
@@ -0,0 +1,49 @@
+{
+ "appCookie": "0x1",
+ "buckets": [
+ {
+ "type": "ALL",
+ "treatment": {
+ "instructions": [
+ {
+ "type": "OUTPUT",
+ "port": 21
+ }
+ ]
+ }
+ },
+ {
+ "type": "ALL",
+ "treatment": {
+ "instructions": [
+ {
+ "type": "OUTPUT",
+ "port": 22
+ }
+ ]
+ }
+ },
+ {
+ "type": "ALL",
+ "treatment": {
+ "instructions": [
+ {
+ "type": "OUTPUT",
+ "port": 23
+ }
+ ]
+ }
+ },
+ {
+ "type": "ALL",
+ "treatment": {
+ "instructions": [
+ {
+ "type": "OUTPUT",
+ "port": 24
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file