[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
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java b/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
index e9fc7ac..d68b287 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/CodecManager.java
@@ -17,7 +17,6 @@
import com.codahale.metrics.Metric;
import com.google.common.collect.ImmutableSet;
-
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
@@ -35,11 +34,11 @@
import org.onosproject.net.Link;
import org.onosproject.net.Path;
import org.onosproject.net.Port;
+import org.onosproject.net.device.PortStatistics;
import org.onosproject.net.driver.Driver;
import org.onosproject.net.flow.FlowEntry;
import org.onosproject.net.flow.FlowRule;
import org.onosproject.net.flow.TableStatisticsEntry;
-import org.onosproject.net.device.PortStatistics;
import org.onosproject.net.flow.TrafficSelector;
import org.onosproject.net.flow.TrafficTreatment;
import org.onosproject.net.flow.criteria.Criterion;
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 c710514..c3819b3 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
@@ -15,14 +15,18 @@
*/
package org.onosproject.codec.impl;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onosproject.codec.CodecContext;
import org.onosproject.codec.JsonCodec;
+import org.onosproject.core.DefaultGroupId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.PortNumber;
import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.group.DefaultGroupBucket;
import org.onosproject.net.group.GroupBucket;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
/**
* Group bucket JSON codec.
@@ -36,6 +40,8 @@
private static final String WATCH_GROUP = "watchGroup";
private static final String PACKETS = "packets";
private static final String BYTES = "bytes";
+ private static final String MISSING_MEMBER_MESSAGE =
+ " member is required in Group";
@Override
public ObjectNode encode(GroupBucket bucket, CodecContext context) {
@@ -61,4 +67,59 @@
return result;
}
+
+ @Override
+ public GroupBucket decode(ObjectNode json, CodecContext context) {
+ if (json == null || !json.isObject()) {
+ return null;
+ }
+
+ // build traffic treatment
+ ObjectNode treatmentJson = get(json, TREATMENT);
+ TrafficTreatment trafficTreatment = null;
+ if (treatmentJson != null) {
+ JsonCodec<TrafficTreatment> treatmentCodec =
+ context.codec(TrafficTreatment.class);
+ trafficTreatment = treatmentCodec.decode(treatmentJson, context);
+ }
+
+ // parse group type
+ String type = nullIsIllegal(json.get(TYPE), TYPE + MISSING_MEMBER_MESSAGE).asText();
+ GroupBucket groupBucket = null;
+
+ switch (type) {
+ case "SELECT":
+ // parse weight
+ int weightInt = nullIsIllegal(json.get(WEIGHT), WEIGHT + MISSING_MEMBER_MESSAGE).asInt();
+
+ groupBucket =
+ DefaultGroupBucket.createSelectGroupBucket(trafficTreatment, (short) weightInt);
+ break;
+ case "INDIRECT":
+ groupBucket =
+ DefaultGroupBucket.createIndirectGroupBucket(trafficTreatment);
+ break;
+ case "ALL":
+ groupBucket =
+ DefaultGroupBucket.createAllGroupBucket(trafficTreatment);
+ break;
+ case "FAILOVER":
+ // parse watchPort
+ PortNumber watchPort = PortNumber.portNumber(nullIsIllegal(json.get(WATCH_PORT),
+ WATCH_PORT + MISSING_MEMBER_MESSAGE).asText());
+
+ // parse watchGroup
+ int groupIdInt = nullIsIllegal(json.get(WATCH_GROUP),
+ WATCH_GROUP + MISSING_MEMBER_MESSAGE).asInt();
+ GroupId watchGroup = new DefaultGroupId((short) groupIdInt);
+
+ groupBucket =
+ DefaultGroupBucket.createFailoverGroupBucket(trafficTreatment, watchPort, watchGroup);
+ break;
+ default:
+ DefaultGroupBucket.createAllGroupBucket(trafficTreatment);
+ }
+
+ return groupBucket;
+ }
}
diff --git a/core/common/src/main/java/org/onosproject/codec/impl/GroupCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/GroupCodec.java
index a2f33ce..6a7e404 100644
--- a/core/common/src/main/java/org/onosproject/codec/impl/GroupCodec.java
+++ b/core/common/src/main/java/org/onosproject/codec/impl/GroupCodec.java
@@ -15,20 +15,40 @@
*/
package org.onosproject.codec.impl;
-import org.onosproject.codec.CodecContext;
-import org.onosproject.codec.JsonCodec;
-import org.onosproject.net.group.Group;
-import org.onosproject.net.group.GroupBucket;
-
+import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.core.DefaultGroupId;
+import org.onosproject.core.GroupId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.group.DefaultGroup;
+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.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onlab.util.Tools.nullIsIllegal;
+import static org.slf4j.LoggerFactory.getLogger;
/**
* Group JSON codec.
*/
public final class GroupCodec extends JsonCodec<Group> {
+ private final Logger log = getLogger(getClass());
+
// JSON field names
private static final String ID = "id";
private static final String STATE = "state";
@@ -37,11 +57,15 @@
private static final String BYTES = "bytes";
private static final String REFERENCE_COUNT = "referenceCount";
private static final String TYPE = "type";
+ private static final String GROUP_ID = "groupId";
private static final String DEVICE_ID = "deviceId";
private static final String APP_ID = "appId";
- private static final String APP_COOKIE = "appCookie";
- private static final String GIVEN_GROUP_ID = "givenGroupId";
+ private static final String APP_COOKIE = "appCookie";
+ private static final String GIVEN_GROUP_ID = "givenGroupId";
private static final String BUCKETS = "buckets";
+ private static final String MISSING_MEMBER_MESSAGE =
+ " member is required in Group";
+ public static final String REST_APP_ID = "org.onosproject.rest";
@Override
public ObjectNode encode(Group group, CodecContext context) {
@@ -70,10 +94,81 @@
ArrayNode buckets = context.mapper().createArrayNode();
group.buckets().buckets().forEach(bucket -> {
- ObjectNode bucketJson = context.codec(GroupBucket.class).encode(bucket, context);
- buckets.add(bucketJson);
- });
+ ObjectNode bucketJson = context.codec(GroupBucket.class).encode(bucket, context);
+ buckets.add(bucketJson);
+ });
result.set(BUCKETS, buckets);
return result;
}
+
+ @Override
+ public Group decode(ObjectNode json, CodecContext context) {
+ if (json == null || !json.isObject()) {
+ return null;
+ }
+
+ final JsonCodec<GroupBucket> groupBucketCodec = context.codec(GroupBucket.class);
+ CoreService coreService = context.getService(CoreService.class);
+
+ // parse group id
+ int groupIdInt = nullIsIllegal(json.get(GROUP_ID),
+ GROUP_ID + MISSING_MEMBER_MESSAGE).asInt();
+ GroupId groupId = new DefaultGroupId((short) groupIdInt);
+
+ // parse group key (appCookie)
+ String groupKeyStr = nullIsIllegal(json.get(APP_COOKIE),
+ APP_COOKIE + MISSING_MEMBER_MESSAGE).asText();
+ GroupKey groupKey = new DefaultGroupKey(groupKeyStr.getBytes());
+
+ // parse device id
+ DeviceId deviceId = DeviceId.deviceId(nullIsIllegal(json.get(DEVICE_ID),
+ DEVICE_ID + MISSING_MEMBER_MESSAGE).asText());
+
+ // application id
+ ApplicationId appId = coreService.registerApplication(REST_APP_ID);
+
+ // parse group type
+ String type = nullIsIllegal(json.get(TYPE),
+ TYPE + MISSING_MEMBER_MESSAGE).asText();
+ GroupDescription.Type groupType = null;
+
+ switch (type) {
+ case "SELECT":
+ groupType = Group.Type.SELECT;
+ break;
+ case "INDIRECT":
+ groupType = Group.Type.INDIRECT;
+ break;
+ case "ALL":
+ groupType = Group.Type.ALL;
+ break;
+ case "FAILOVER":
+ groupType = Group.Type.FAILOVER;
+ break;
+ default:
+ log.warn("The requested type {} is not defined for group.", type);
+ return null;
+ }
+
+ // parse group buckets
+ // TODO: make sure that INDIRECT group only has one bucket
+ GroupBuckets buckets = null;
+ List<GroupBucket> groupBucketList = new ArrayList<>();
+ JsonNode bucketsJson = json.get(BUCKETS);
+ checkNotNull(bucketsJson);
+ if (bucketsJson != null) {
+ IntStream.range(0, bucketsJson.size())
+ .forEach(i -> {
+ ObjectNode bucketJson = get(bucketsJson, i);
+ bucketJson.put("type", type);
+ groupBucketList.add(groupBucketCodec.decode(bucketJson, context));
+ });
+ buckets = new GroupBuckets(groupBucketList);
+ }
+
+ GroupDescription groupDescription = new DefaultGroupDescription(deviceId,
+ groupType, buckets, groupKey, groupIdInt, appId);
+
+ return new DefaultGroup(groupId, groupDescription);
+ }
}
diff --git a/core/common/src/test/java/org/onosproject/codec/impl/GroupCodecTest.java b/core/common/src/test/java/org/onosproject/codec/impl/GroupCodecTest.java
index 409f8eb..ffaefd6 100644
--- a/core/common/src/test/java/org/onosproject/codec/impl/GroupCodecTest.java
+++ b/core/common/src/test/java/org/onosproject/codec/impl/GroupCodecTest.java
@@ -15,21 +15,37 @@
*/
package org.onosproject.codec.impl;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableList;
+import org.junit.Before;
import org.junit.Test;
+import org.onosproject.codec.JsonCodec;
+import org.onosproject.core.CoreService;
import org.onosproject.core.DefaultGroupId;
import org.onosproject.net.NetTestTools;
+import org.onosproject.net.PortNumber;
import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions;
import org.onosproject.net.group.DefaultGroup;
import org.onosproject.net.group.DefaultGroupBucket;
+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 com.fasterxml.jackson.databind.node.ObjectNode;
-import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.io.InputStream;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
import static org.onosproject.codec.impl.GroupJsonMatcher.matchesGroup;
+import static org.onosproject.net.NetTestTools.APP_ID;
/**
* Group codec unit tests.
@@ -37,8 +53,28 @@
public class GroupCodecTest {
+ MockCodecContext context;
+ JsonCodec<Group> groupCodec;
+ final CoreService mockCoreService = createMock(CoreService.class);
+
+ /**
+ * Sets up for each test. Creates a context and fetches the flow rule
+ * codec.
+ */
+ @Before
+ public void setUp() {
+ context = new MockCodecContext();
+ groupCodec = context.codec(Group.class);
+ assertThat(groupCodec, notNullValue());
+
+ expect(mockCoreService.registerApplication(GroupCodec.REST_APP_ID))
+ .andReturn(APP_ID).anyTimes();
+ replay(mockCoreService);
+ context.registerService(CoreService.class, mockCoreService);
+ }
+
@Test
- public void codecTest() {
+ public void codecEncodeTest() {
GroupBucket bucket1 = DefaultGroupBucket
.createSelectGroupBucket(DefaultTrafficTreatment.emptyTreatment());
GroupBucket bucket2 = DefaultGroupBucket
@@ -58,4 +94,48 @@
assertThat(groupJson, matchesGroup(group));
}
+
+ @Test
+ public void codecDecodeTest() throws IOException {
+ Group group = getGroup("simple-group.json");
+ checkCommonData(group);
+
+ assertThat(group.buckets().buckets().size(), is(1));
+ GroupBucket groupBucket = group.buckets().buckets().get(0);
+ assertThat(groupBucket.type().toString(), is("ALL"));
+ assertThat(groupBucket.treatment().allInstructions().size(), is(1));
+ Instruction instruction1 = groupBucket.treatment().allInstructions().get(0);
+ assertThat(instruction1.type(), is(Instruction.Type.OUTPUT));
+ assertThat(((Instructions.OutputInstruction) instruction1).port(), is(PortNumber.portNumber(2)));
+ }
+
+ /**
+ * Checks that the data shared by all the resource is correct for a given group.
+ *
+ * @param group group to check
+ */
+ private void checkCommonData(Group group) {
+ assertThat(group.appId(), is(APP_ID));
+ assertThat(group.deviceId().toString(), is("of:0000000000000001"));
+ assertThat(group.type().toString(), is("ALL"));
+ assertThat(group.appCookie().key(), is("1".getBytes()));
+ assertThat(group.id().id(), is(1));
+ }
+
+ /**
+ * Reads in a group from the given resource and decodes it.
+ *
+ * @param resourceName resource to use to read the JSON for the rule
+ * @return decoded group
+ * @throws IOException if processing the resource fails
+ */
+ private Group getGroup(String resourceName) throws IOException {
+ InputStream jsonStream = GroupCodecTest.class
+ .getResourceAsStream(resourceName);
+ JsonNode json = context.mapper().readTree(jsonStream);
+ assertThat(json, notNullValue());
+ Group group = groupCodec.decode((ObjectNode) json, context);
+ assertThat(group, notNullValue());
+ return group;
+ }
}
diff --git a/core/common/src/test/resources/org/onosproject/codec/impl/simple-group.json b/core/common/src/test/resources/org/onosproject/codec/impl/simple-group.json
new file mode 100644
index 0000000..675f244
--- /dev/null
+++ b/core/common/src/test/resources/org/onosproject/codec/impl/simple-group.json
@@ -0,0 +1,18 @@
+{
+ "type": "ALL",
+ "deviceId": "of:0000000000000001",
+ "appCookie": "1",
+ "groupId": "1",
+ "buckets": [
+ {
+ "treatment": {
+ "instructions": [
+ {
+ "type": "OUTPUT",
+ "port": 2
+ }
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file