Jian Li
Committed by Gerrit Code Review

[ONOS-3618] Implement REST API for Meter query, insert, delete

* Implement encode & decode method for MeterBandCodec & MeterCodec
* Implement MetersWebResource
* Add unit test for MeterBandCodec & MeterCodec
* Add unit test for MetersWebResource
* Add meter insertion json example
* Add Swagger doc

Change-Id: I07284c6678c08b3cb9e109e86ffb2cf28bf36447
......@@ -51,7 +51,7 @@ public interface Band {
*
* @return the long value of the size
*/
long burst();
Long burst();
/**
* Only meaningful in the case of a REMARK band type.
......@@ -60,7 +60,7 @@ public interface Band {
*
* @return a short value
*/
short dropPrecedence();
Short dropPrecedence();
/**
* Signals the type of band to create.
......
......@@ -45,12 +45,12 @@ public final class DefaultBand implements Band, BandEntry {
}
@Override
public long burst() {
public Long burst() {
return burstSize;
}
@Override
public short dropPrecedence() {
public Short dropPrecedence() {
return prec;
}
......
......@@ -50,6 +50,8 @@ import org.onosproject.net.intent.Constraint;
import org.onosproject.net.intent.HostToHostIntent;
import org.onosproject.net.intent.Intent;
import org.onosproject.net.intent.PointToPointIntent;
import org.onosproject.net.meter.Band;
import org.onosproject.net.meter.Meter;
import org.onosproject.net.statistic.Load;
import org.onosproject.net.topology.Topology;
import org.onosproject.net.topology.TopologyCluster;
......@@ -102,6 +104,8 @@ public class CodecManager implements CodecService {
registerCodec(Driver.class, new DriverCodec());
registerCodec(GroupBucket.class, new GroupBucketCodec());
registerCodec(Load.class, new LoadCodec());
registerCodec(Meter.class, new MeterCodec());
registerCodec(Band.class, new MeterBandCodec());
registerCodec(TableStatisticsEntry.class, new TableStatisticsEntryCodec());
registerCodec(PortStatistics.class, new PortStatisticsCodec());
registerCodec(Metric.class, new MetricCodec());
......
/*
* Copyright 2014-2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.net.meter.Band;
import org.onosproject.net.meter.DefaultBand;
import org.slf4j.Logger;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.onlab.util.Tools.nullIsIllegal;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Meter band JSON codec.
*/
public final class MeterBandCodec extends JsonCodec<Band> {
private final Logger log = getLogger(getClass());
// JSON field names
private static final String TYPE = "type";
private static final String RATE = "rate";
private static final String BURST_SIZE = "burstSize";
private static final String PREC = "prec";
private static final String PACKETS = "packets";
private static final String BYTES = "bytes";
private static final String MISSING_MEMBER_MESSAGE = " member is required in Band";
@Override
public ObjectNode encode(Band band, CodecContext context) {
checkNotNull(band, "Band cannot be null");
ObjectNode result = context.mapper().createObjectNode()
.put(TYPE, band.type().toString())
.put(RATE, band.rate())
.put(PACKETS, band.packets())
.put(BYTES, band.bytes())
.put(BURST_SIZE, band.burst());
if (band.dropPrecedence() != null) {
result.put(PREC, band.dropPrecedence());
}
return result;
}
@Override
public Band decode(ObjectNode json, CodecContext context) {
if (json == null || !json.isObject()) {
return null;
}
// parse rate
long rate = nullIsIllegal(json.get(RATE), RATE + MISSING_MEMBER_MESSAGE).asLong();
// parse burst size
long burstSize = nullIsIllegal(json.get(BURST_SIZE), BURST_SIZE + MISSING_MEMBER_MESSAGE).asLong();
// parse precedence
Short precedence = null;
// parse band type
String typeStr = nullIsIllegal(json.get(TYPE), TYPE + MISSING_MEMBER_MESSAGE).asText();
Band.Type type;
switch (typeStr) {
case "DROP":
type = Band.Type.DROP;
break;
case "REMARK":
type = Band.Type.REMARK;
precedence = (short) nullIsIllegal(json.get(PREC), PREC + MISSING_MEMBER_MESSAGE).asInt();
break;
default:
log.warn("The requested type {} is not defined for band.", typeStr);
return null;
}
Band band = DefaultBand.builder()
.ofType(type)
.burstSize(burstSize)
.withRate(rate)
.dropPrecedence(precedence)
.build();
return band;
}
}
/*
* Copyright 2014-2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.codec.impl;
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.net.DeviceId;
import org.onosproject.net.meter.Band;
import org.onosproject.net.meter.DefaultMeter;
import org.onosproject.net.meter.Meter;
import org.onosproject.net.meter.MeterId;
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;
/**
* Meter JSON codec.
*/
public final class MeterCodec extends JsonCodec<Meter> {
private final Logger log = getLogger(getClass());
// JSON field names
private static final String ID = "id";
private static final String STATE = "state";
private static final String LIFE = "life";
private static final String PACKETS = "packets";
private static final String BYTES = "bytes";
private static final String REFERENCE_COUNT = "referenceCount";
private static final String APP_ID = "appId";
private static final String BURST = "burst";
private static final String DEVICE_ID = "deviceId";
private static final String UNIT = "unit";
private static final String BANDS = "bands";
public static final String REST_APP_ID = "org.onosproject.rest";
private static final String MISSING_MEMBER_MESSAGE = " member is required in Meter";
@Override
public ObjectNode encode(Meter meter, CodecContext context) {
checkNotNull(meter, "Meter cannot be null");
ObjectNode result = context.mapper().createObjectNode()
.put(ID, meter.id().toString())
.put(LIFE, meter.life())
.put(PACKETS, meter.packetsSeen())
.put(BYTES, meter.bytesSeen())
.put(REFERENCE_COUNT, meter.referenceCount())
.put(UNIT, meter.unit().toString())
.put(BURST, meter.isBurst())
.put(DEVICE_ID, meter.deviceId().toString());
if (meter.appId() != null) {
result.put(APP_ID, meter.appId().toString());
}
if (meter.state() != null) {
result.put(STATE, meter.state().toString());
}
ArrayNode bands = context.mapper().createArrayNode();
meter.bands().forEach(band -> {
ObjectNode bandJson = context.codec(Band.class).encode(band, context);
bands.add(bandJson);
});
result.set(BANDS, bands);
return result;
}
@Override
public Meter decode(ObjectNode json, CodecContext context) {
if (json == null || !json.isObject()) {
return null;
}
final JsonCodec<Band> meterBandCodec = context.codec(Band.class);
CoreService coreService = context.getService(CoreService.class);
// parse meter id
int meterIdInt = nullIsIllegal(json.get(ID), ID + MISSING_MEMBER_MESSAGE).asInt();
MeterId meterId = MeterId.meterId(meterIdInt);
// 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 burst
boolean burst = false;
JsonNode burstJson = json.get("burst");
if (burstJson != null) {
burst = burstJson.asBoolean();
}
// parse unit type
String unit = nullIsIllegal(json.get(UNIT), UNIT + MISSING_MEMBER_MESSAGE).asText();
Meter.Unit meterUnit;
switch (unit) {
case "KB_PER_SEC":
meterUnit = Meter.Unit.KB_PER_SEC;
break;
case "PKTS_PER_SEC":
meterUnit = Meter.Unit.PKTS_PER_SEC;
break;
default:
log.warn("The requested unit {} is not defined for meter.", unit);
return null;
}
// parse meter bands
List<Band> bandList = new ArrayList<>();
JsonNode bandsJson = json.get(BANDS);
checkNotNull(bandsJson);
if (bandsJson != null) {
IntStream.range(0, bandsJson.size()).forEach(i -> {
ObjectNode bandJson = get(bandsJson, i);
bandList.add(meterBandCodec.decode(bandJson, context));
});
}
Meter meter;
if (burst) {
meter = DefaultMeter.builder()
.withId(meterId)
.fromApp(appId)
.forDevice(deviceId)
.withUnit(meterUnit)
.withBands(bandList)
.burst().build();
} else {
meter = DefaultMeter.builder()
.withId(meterId)
.fromApp(appId)
.forDevice(deviceId)
.withUnit(meterUnit)
.withBands(bandList).build();
}
return meter;
}
}
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.codec.impl;
import com.fasterxml.jackson.databind.JsonNode;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.onosproject.net.meter.Band;
/**
* Hamcrest matcher for bands.
*/
public final class MeterBandJsonMatcher extends TypeSafeDiagnosingMatcher<JsonNode> {
private final Band band;
private MeterBandJsonMatcher(Band band) {
this.band = band;
}
/**
* Matches the contents of a meter band.
*
* @param bandJson JSON representation of band to match
* @param description Description object used for recording errors
* @return true if contents match, false otherwise
*/
@Override
protected boolean matchesSafely(JsonNode bandJson, Description description) {
// check type
final String jsonType = bandJson.get("type").textValue();
if (!band.type().name().equals(jsonType)) {
description.appendText("type was " + jsonType);
return false;
}
// check rate
final long jsonRate = bandJson.get("rate").longValue();
if (band.rate() != jsonRate) {
description.appendText("rate was " + jsonRate);
return false;
}
// check burst size
final long jsonBurstSize = bandJson.get("burstSize").longValue();
if (band.burst() != jsonBurstSize) {
description.appendText("burst size was " + jsonBurstSize);
return false;
}
// check precedence
final JsonNode jsonNodePrec = bandJson.get("prec");
if (jsonNodePrec != null) {
if (band.dropPrecedence() != jsonNodePrec.shortValue()) {
description.appendText("drop precedence was " + jsonNodePrec.shortValue());
return false;
}
}
// check packets
final JsonNode jsonNodePackets = bandJson.get("packets");
if (jsonNodePackets != null) {
if (band.packets() != jsonNodePackets.asLong()) {
description.appendText("packets was " + jsonNodePackets.asLong());
return false;
}
}
final JsonNode jsonNodeBytes = bandJson.get("bytes");
if (jsonNodeBytes != null) {
if (band.bytes() != jsonNodeBytes.asLong()) {
description.appendText("bytes was " + jsonNodeBytes.asLong());
return false;
}
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText(band.toString());
}
/**
* Factory to allocate a band matcher.
*
* @param band band object we are looking for
* @return matcher
*/
public static MeterBandJsonMatcher matchesMeterBand(Band band) {
return new MeterBandJsonMatcher(band);
}
}
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.net.NetTestTools;
import org.onosproject.net.meter.Band;
import org.onosproject.net.meter.DefaultBand;
import org.onosproject.net.meter.DefaultMeter;
import org.onosproject.net.meter.Meter;
import org.onosproject.net.meter.MeterId;
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.MeterJsonMatcher.matchesMeter;
import static org.onosproject.net.NetTestTools.APP_ID;
/**
* Unit tests for Meter codec.
*/
public class MeterCodecTest {
MockCodecContext context;
JsonCodec<Meter> meterCodec;
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();
meterCodec = context.codec(Meter.class);
assertThat(meterCodec, notNullValue());
expect(mockCoreService.registerApplication(MeterCodec.REST_APP_ID))
.andReturn(APP_ID).anyTimes();
replay(mockCoreService);
context.registerService(CoreService.class, mockCoreService);
}
/**
* Tests encoding of a Meter object.
*/
@Test
public void testMeterEncode() {
Band band1 = DefaultBand.builder()
.ofType(Band.Type.DROP)
.burstSize(10)
.withRate(10).build();
Band band2 = DefaultBand.builder()
.ofType(Band.Type.REMARK)
.burstSize(10)
.withRate(10)
.dropPrecedence((short) 10).build();
Meter meter = DefaultMeter.builder()
.fromApp(APP_ID)
.withId(MeterId.meterId(1L))
.forDevice(NetTestTools.did("d1"))
.withBands(ImmutableList.of(band1, band2))
.withUnit(Meter.Unit.KB_PER_SEC).build();
ObjectNode meterJson = meterCodec.encode(meter, context);
assertThat(meterJson, matchesMeter(meter));
}
/**
* Test decoding of a Meter object.
*/
@Test
public void testMeterDecode() throws IOException {
Meter meter = getMeter("simple-meter.json");
checkCommonData(meter);
assertThat(meter.bands().size(), is(1));
Band band = meter.bands().iterator().next();
assertThat(band.type().toString(), is("REMARK"));
assertThat(band.rate(), is(10L));
assertThat(band.dropPrecedence(), is((short) 20));
assertThat(band.burst(), is(30L));
}
/**
* Checks that the data shared by all the resource is correct for a given meter.
*
* @param meter meter to check
*/
private void checkCommonData(Meter meter) {
assertThat(meter.id().id(), is(1L));
assertThat(meter.deviceId().toString(), is("of:0000000000000001"));
assertThat(meter.appId(), is(APP_ID));
assertThat(meter.unit().toString(), is("KB_PER_SEC"));
}
/**
* Reads in a meter from the given resource and decodes it.
*
* @param resourceName resource to use to read the JSON for the rule
* @return decoded meter
* @throws IOException if processing the resource fails
*/
private Meter getMeter(String resourceName) throws IOException {
InputStream jsonStream = MeterCodecTest.class.getResourceAsStream(resourceName);
JsonNode json = context.mapper().readTree(jsonStream);
assertThat(json, notNullValue());
Meter meter = meterCodec.decode((ObjectNode) json, context);
assertThat(meter, notNullValue());
return meter;
}
}
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.codec.impl;
import com.fasterxml.jackson.databind.JsonNode;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.onosproject.net.meter.Band;
import org.onosproject.net.meter.Meter;
/**
* Hamcrest matcher for meters.
*/
public final class MeterJsonMatcher extends TypeSafeDiagnosingMatcher<JsonNode> {
private final Meter meter;
private MeterJsonMatcher(Meter meter) {
this.meter = meter;
}
@Override
protected boolean matchesSafely(JsonNode jsonMeter, Description description) {
// check id
String jsonMeterId = jsonMeter.get("id").asText();
String meterId = meter.id().toString();
if (!jsonMeterId.equals(meterId)) {
description.appendText("meter id was " + jsonMeterId);
return false;
}
// check unit
String jsonUnit = jsonMeter.get("unit").asText();
String unit = meter.unit().toString();
if (!jsonUnit.equals(unit)) {
description.appendText("unit was " + jsonUnit);
return false;
}
// check burst
boolean jsonBurst = jsonMeter.get("burst").asBoolean();
boolean burst = meter.isBurst();
if (jsonBurst != burst) {
description.appendText("isBurst was " + jsonBurst);
return false;
}
// check state
JsonNode jsonNodeState = jsonMeter.get("state");
if (jsonNodeState != null) {
String state = meter.state().toString();
if (!jsonNodeState.asText().equals(state)) {
description.appendText("state was " + jsonNodeState.asText());
return false;
}
}
// check life
JsonNode jsonNodeLife = jsonMeter.get("life");
if (jsonNodeLife != null) {
long life = meter.life();
if (jsonNodeLife.asLong() != life) {
description.appendText("life was " + jsonNodeLife.asLong());
return false;
}
}
// check bytes
JsonNode jsonNodeBytes = jsonMeter.get("bytes");
if (jsonNodeBytes != null) {
long bytes = meter.bytesSeen();
if (jsonNodeBytes.asLong() != bytes) {
description.appendText("bytes was " + jsonNodeBytes.asLong());
return false;
}
}
// check packets
JsonNode jsonNodePackets = jsonMeter.get("packets");
if (jsonNodePackets != null) {
long packets = meter.packetsSeen();
if (jsonNodePackets.asLong() != packets) {
description.appendText("packets was " + jsonNodePackets.asLong());
return false;
}
}
// check size of band array
JsonNode jsonBands = jsonMeter.get("bands");
if (jsonBands.size() != meter.bands().size()) {
description.appendText("bands size was " + jsonBands.size());
return false;
}
// check bands
for (Band band : meter.bands()) {
boolean bandFound = false;
for (int bandIndex = 0; bandIndex < jsonBands.size(); bandIndex++) {
MeterBandJsonMatcher bandMatcher = MeterBandJsonMatcher.matchesMeterBand(band);
if (bandMatcher.matches(jsonBands.get(bandIndex))) {
bandFound = true;
break;
}
}
if (!bandFound) {
description.appendText("band not found " + band.toString());
return false;
}
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText(meter.toString());
}
/**
* Factory to allocate a meter matcher.
*
* @param meter meter object we are looking for
* @return matcher
*/
public static MeterJsonMatcher matchesMeter(Meter meter) {
return new MeterJsonMatcher(meter);
}
}
{
"id": 1,
"deviceId": "of:0000000000000001",
"unit": "KB_PER_SEC",
"burst": true,
"bands": [
{
"type": "REMARK",
"rate": 10,
"prec": 20,
"burstSize": 30
}
]
}
\ No newline at end of file
......@@ -28,21 +28,22 @@ public class CoreWebApplication extends AbstractWebApplication {
@Override
public Set<Class<?>> getClasses() {
return getClasses(ApiDocResource.class,
ApplicationsWebResource.class,
ComponentConfigWebResource.class,
NetworkConfigWebResource.class,
ClusterWebResource.class,
DevicesWebResource.class,
LinksWebResource.class,
HostsWebResource.class,
IntentsWebResource.class,
FlowsWebResource.class,
GroupsWebResource.class,
TopologyWebResource.class,
ConfigWebResource.class,
PathsWebResource.class,
StatisticsWebResource.class,
MetricsWebResource.class
ApplicationsWebResource.class,
ComponentConfigWebResource.class,
NetworkConfigWebResource.class,
ClusterWebResource.class,
DevicesWebResource.class,
LinksWebResource.class,
HostsWebResource.class,
IntentsWebResource.class,
FlowsWebResource.class,
GroupsWebResource.class,
MetersWebResource.class,
TopologyWebResource.class,
ConfigWebResource.class,
PathsWebResource.class,
StatisticsWebResource.class,
MetricsWebResource.class
);
}
}
}
\ No newline at end of file
......
/*
* Copyright 2014-2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.rest.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onosproject.net.DeviceId;
import org.onosproject.net.meter.DefaultMeterRequest;
import org.onosproject.net.meter.Meter;
import org.onosproject.net.meter.MeterId;
import org.onosproject.net.meter.MeterRequest;
import org.onosproject.net.meter.MeterService;
import org.onosproject.rest.AbstractWebResource;
import org.slf4j.Logger;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import static org.onlab.util.Tools.nullIsNotFound;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Query and program meter rules.
*/
@Path("meters")
public class MetersWebResource extends AbstractWebResource {
private final Logger log = getLogger(getClass());
public static final String DEVICE_INVALID = "Invalid deviceId in meter creation request";
final MeterService meterService = get(MeterService.class);
final ObjectNode root = mapper().createObjectNode();
final ArrayNode metersNode = root.putArray("meters");
/**
* Returns all meters of all devices.
*
* @return array of all the meters in the system
* @onos.rsModel Meters
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getMeters() {
final Iterable<Meter> meters = meterService.getAllMeters();
if (meters != null) {
meters.forEach(meter -> metersNode.add(codec(Meter.class).encode(meter, this)));
}
return ok(root).build();
}
/**
* Returns a meter by the meter id.
*
* @param deviceId device identifier
* @return array of all the groups in the system
* @onos.rsModel Meter
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("{deviceId}/{meterId}")
public Response getMeterByDeviceIdAndMeterId(@PathParam("deviceId") String deviceId,
@PathParam("meterId") String meterId) {
DeviceId did = DeviceId.deviceId(deviceId);
MeterId mid = MeterId.meterId(Long.valueOf(meterId));
final Meter meter = nullIsNotFound(meterService.getMeter(did, mid),
"Meter is not found for " + mid.id());
metersNode.add(codec(Meter.class).encode(meter, this));
return ok(root).build();
}
/**
* Create new meter rule. Creates and installs a new meter rule for the
* specified device.
*
* @param deviceId device identifier
* @param stream meter rule JSON
* @return status of the request - CREATED if the JSON is correct,
* BAD_REQUEST if the JSON is invalid
* @onos.rsModel MeterPost
*/
@POST
@Path("{deviceId}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response createMeter(@PathParam("deviceId") String deviceId,
InputStream stream) {
URI location;
try {
ObjectNode jsonTree = (ObjectNode) mapper().readTree(stream);
JsonNode specifiedDeviceId = jsonTree.get("deviceId");
if (specifiedDeviceId != null &&
!specifiedDeviceId.asText().equals(deviceId)) {
throw new IllegalArgumentException(DEVICE_INVALID);
}
jsonTree.put("deviceId", deviceId);
final Meter tmpMeter = codec(Meter.class).decode(jsonTree, this);
final MeterRequest meterRequest = meterToMeterRequest(tmpMeter, "ADD");
final Meter meter = meterService.submit(meterRequest);
location = new URI(Long.toString(meter.id().id()));
} catch (IOException | URISyntaxException ex) {
throw new IllegalArgumentException(ex);
}
return Response
.created(location)
.build();
}
/**
* Removes the specified meter.
*
* @param deviceId device identifier
* @param meterId meter identifier
*/
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@Path("{deviceId}/{meterId}")
public void deleteMeterByDeviceIdAndMeterId(@PathParam("deviceId") String deviceId,
@PathParam("meterId") String meterId) {
DeviceId did = DeviceId.deviceId(deviceId);
MeterId mid = MeterId.meterId(Long.valueOf(meterId));
final Meter tmpMeter = meterService.getMeter(did, mid);
if (tmpMeter != null) {
final MeterRequest meterRequest = meterToMeterRequest(tmpMeter, "REMOVE");
meterService.withdraw(meterRequest, tmpMeter.id());
}
}
/**
* Convert a meter instance to meterRequest instance with a certain operation.
*
* @param meter meter instance
* @param operation operation
* @return converted meterRequest instance
*/
private MeterRequest meterToMeterRequest(Meter meter, String operation) {
MeterRequest.Builder builder;
MeterRequest meterRequest;
if (meter == null) {
return null;
}
if (meter.isBurst()) {
builder = DefaultMeterRequest.builder()
.fromApp(meter.appId())
.forDevice(meter.deviceId())
.withUnit(meter.unit())
.withBands(meter.bands())
.burst();
} else {
builder = DefaultMeterRequest.builder()
.fromApp(meter.appId())
.forDevice(meter.deviceId())
.withUnit(meter.unit())
.withBands(meter.bands());
}
switch (operation) {
case "ADD":
meterRequest = builder.add();
break;
case "REMOVE":
meterRequest = builder.remove();
break;
default:
log.warn("Invalid operation {}.", operation);
return null;
}
return meterRequest;
}
}
\ No newline at end of file
{
"type": "object",
"title": "meter",
"required": [
"id",
"appId",
"deviceId",
"unit",
"burst",
"state",
"life",
"refCount",
"packets",
"bytes",
"bands"
],
"properties": {
"id": {
"type": "string",
"example": "1"
},
"appId": {
"type": "string",
"example": "1"
},
"deviceId": {
"type": "string",
"example": "of:0000000000000001"
},
"unit": {
"type": "string",
"example": "KB_PER_SEC"
},
"burst": {
"type": "boolean",
"example": true
},
"state": {
"type": "string",
"example": "ADDED"
},
"life": {
"type": "integer",
"format": "int64",
"example": 0
},
"refCount": {
"type": "integer",
"format": "int64",
"example": 0
},
"packets": {
"type": "integer",
"format": "int64",
"example": 0
},
"bytes": {
"type": "integer",
"format": "int64",
"example": 0
},
"bands": {
"type": "array",
"xml": {
"name": "bands",
"wrapped": true
},
"items": {
"type": "object",
"title": "bands",
"required": [
"type",
"rate",
"burstSize",
"prec",
"packets",
"bytes"
],
"properties": {
"type": {
"type": "string",
"example": "REMARK"
},
"rate": {
"type": "integer",
"format": "int64",
"example": 0
},
"burstSize": {
"type": "integer",
"format": "int64",
"example": 0
},
"prec": {
"type": "integer",
"format": "int16",
"example": 0
},
"packets": {
"type": "integer",
"format": "int64",
"example": 0
},
"bytes": {
"type": "integer",
"format": "int64",
"example": 0
}
}
}
}
}
}
\ No newline at end of file
{
"type": "object",
"title": "meter",
"required": [
"id",
"deviceId",
"unit",
"burst",
"bands"
],
"properties": {
"id": {
"type": "string",
"example": "1"
},
"deviceId": {
"type": "string",
"example": "of:0000000000000001"
},
"unit": {
"type": "string",
"example": "KB_PER_SEC"
},
"burst": {
"type": "boolean",
"example": true
},
"bands": {
"type": "array",
"xml": {
"name": "bands",
"wrapped": true
},
"items": {
"type": "object",
"title": "bands",
"required": [
"type",
"rate",
"burstSize",
"prec"
],
"properties": {
"type": {
"type": "string",
"example": "REMARK"
},
"rate": {
"type": "integer",
"format": "int64",
"example": "0"
},
"burstSize": {
"type": "integer",
"format": "int64",
"example": "0"
},
"prec": {
"type": "integer",
"format": "int16",
"example": "0"
}
}
}
}
}
}
\ No newline at end of file
{
"type": "object",
"title": "meters",
"required": [
"meters"
],
"properties": {
"groups": {
"type": "array",
"xml": {
"name": "meters",
"wrapped": true
},
"items": {
"type": "object",
"title": "meter",
"required": [
"id",
"appId",
"deviceId",
"unit",
"burst",
"state",
"life",
"refCount",
"packets",
"bytes",
"bands"
],
"properties": {
"id": {
"type": "string",
"example": "1"
},
"appId": {
"type": "string",
"example": "1"
},
"deviceId": {
"type": "string",
"example": "of:0000000000000001"
},
"unit": {
"type": "string",
"example": "KB_PER_SEC"
},
"burst": {
"type": "boolean",
"example": true
},
"state": {
"type": "string",
"example": "ADDED"
},
"life": {
"type": "integer",
"format": "int64",
"example": 0
},
"refCount": {
"type": "integer",
"format": "int64",
"example": 0
},
"packets": {
"type": "integer",
"format": "int64",
"example": 0
},
"bytes": {
"type": "integer",
"format": "int64",
"example": 0
},
"bands": {
"type": "array",
"xml": {
"name": "bands",
"wrapped": true
},
"items": {
"type": "object",
"title": "bands",
"required": [
"type",
"rate",
"burstSize",
"prec",
"packets",
"bytes"
],
"properties": {
"type": {
"type": "string",
"example": "REMARK"
},
"rate": {
"type": "integer",
"format": "int64",
"example": 0
},
"burstSize": {
"type": "integer",
"format": "int64",
"example": 0
},
"prec": {
"type": "integer",
"format": "int16",
"example": 0
},
"packets": {
"type": "integer",
"format": "int64",
"example": 0
},
"bytes": {
"type": "integer",
"format": "int64",
"example": 0
}
}
}
}
}
}
}
}
}
\ No newline at end of file
/*
* Copyright 2014-2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.rest;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.google.common.collect.ImmutableSet;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.onlab.osgi.ServiceDirectory;
import org.onlab.osgi.TestServiceDirectory;
import org.onlab.rest.BaseResource;
import org.onosproject.codec.CodecService;
import org.onosproject.codec.impl.CodecManager;
import org.onosproject.codec.impl.MeterCodec;
import org.onosproject.core.ApplicationId;
import org.onosproject.core.CoreService;
import org.onosproject.core.DefaultApplicationId;
import org.onosproject.net.DefaultDevice;
import org.onosproject.net.Device;
import org.onosproject.net.DeviceId;
import org.onosproject.net.NetTestTools;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.meter.Band;
import org.onosproject.net.meter.DefaultBand;
import org.onosproject.net.meter.Meter;
import org.onosproject.net.meter.MeterId;
import org.onosproject.net.meter.MeterService;
import org.onosproject.net.meter.MeterState;
import javax.ws.rs.core.MediaType;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.easymock.EasyMock.anyObject;
import static org.easymock.EasyMock.anyShort;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.verify;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.hamcrest.Matchers.notNullValue;
import static org.onosproject.net.NetTestTools.APP_ID;
/**
* Unit tests for meters REST APIs.
*/
public class MetersResourceTest extends ResourceTest {
final MeterService mockMeterService = createMock(MeterService.class);
CoreService mockCoreService = createMock(CoreService.class);
final DeviceService mockDeviceService = createMock(DeviceService.class);
final HashMap<DeviceId, Set<Meter>> meters = new HashMap<>();
final DeviceId deviceId1 = DeviceId.deviceId("1");
final DeviceId deviceId2 = DeviceId.deviceId("2");
final DeviceId deviceId3 = DeviceId.deviceId("3");
final Device device1 = new DefaultDevice(null, deviceId1, Device.Type.OTHER,
"", "", "", "", null);
final Device device2 = new DefaultDevice(null, deviceId2, Device.Type.OTHER,
"", "", "", "", null);
final MockMeter meter1 = new MockMeter(deviceId1, 1, 111, 1);
final MockMeter meter2 = new MockMeter(deviceId1, 2, 222, 2);
final MockMeter meter3 = new MockMeter(deviceId2, 3, 333, 3);
final MockMeter meter4 = new MockMeter(deviceId2, 4, 444, 4);
final MockMeter meter5 = new MockMeter(deviceId3, 5, 555, 5);
/**
* Mock class for a meter.
*/
private static class MockMeter implements Meter {
final DeviceId deviceId;
final ApplicationId appId;
final MeterId meterId;
final long baseValue;
final List<Band> bandList;
public MockMeter(DeviceId deviceId, int appId, long meterId, int id) {
this.deviceId = deviceId;
this.appId = new DefaultApplicationId(appId, String.valueOf(appId));
this.baseValue = id * 200;
this.meterId = MeterId.meterId(meterId);
Band band = DefaultBand.builder()
.ofType(Band.Type.REMARK)
.withRate(10)
.dropPrecedence((short) 20)
.burstSize(30).build();
this.bandList = new ArrayList<>();
this.bandList.add(band);
}
@Override
public DeviceId deviceId() {
return this.deviceId;
}
@Override
public MeterId id() {
return this.meterId;
}
@Override
public ApplicationId appId() {
return this.appId;
}
@Override
public Unit unit() {
return Unit.KB_PER_SEC;
}
@Override
public boolean isBurst() {
return false;
}
@Override
public Collection<Band> bands() {
return this.bandList;
}
@Override
public MeterState state() {
return MeterState.ADDED;
}
@Override
public long life() {
return baseValue + 11;
}
@Override
public long referenceCount() {
return baseValue + 22;
}
@Override
public long packetsSeen() {
return baseValue + 33;
}
@Override
public long bytesSeen() {
return baseValue + 44;
}
}
/**
* Populates some meters used as testing data.
*/
private void setupMockMeters() {
final Set<Meter> meters1 = new HashSet<>();
meters1.add(meter1);
meters1.add(meter2);
final Set<Meter> meters2 = new HashSet<>();
meters2.add(meter3);
meters2.add(meter4);
meters.put(deviceId1, meters1);
meters.put(deviceId2, meters2);
Set<Meter> allMeters = new HashSet<>();
for (DeviceId deviceId : meters.keySet()) {
allMeters.addAll(meters.get(deviceId));
}
expect(mockMeterService.getAllMeters()).andReturn(allMeters).anyTimes();
}
/**
* Sets up the global values for all the tests.
*/
@Before
public void setUpTest() {
// Mock device service
expect(mockDeviceService.getDevice(deviceId1))
.andReturn(device1);
expect(mockDeviceService.getDevice(deviceId2))
.andReturn(device2);
expect(mockDeviceService.getDevices())
.andReturn(ImmutableSet.of(device1, device2));
// Mock Core Service
expect(mockCoreService.getAppId(anyShort()))
.andReturn(NetTestTools.APP_ID).anyTimes();
expect(mockCoreService.registerApplication(MeterCodec.REST_APP_ID))
.andReturn(APP_ID).anyTimes();
replay(mockCoreService);
// Register the services needed for the test
final CodecManager codecService = new CodecManager();
codecService.activate();
ServiceDirectory testDirectory =
new TestServiceDirectory()
.add(MeterService.class, mockMeterService)
.add(DeviceService.class, mockDeviceService)
.add(CodecService.class, codecService)
.add(CoreService.class, mockCoreService);
BaseResource.setServiceDirectory(testDirectory);
}
/**
* Cleans up and verifies the mocks.
*/
@After
public void tearDownTest() {
verify(mockMeterService);
verify(mockCoreService);
}
/**
* Hamcrest matcher to check that a meter representation in JSON matches
* the actual meter.
*/
public static class MeterJsonMatcher extends TypeSafeMatcher<JsonObject> {
private final Meter meter;
private String reason = "";
public MeterJsonMatcher(Meter meterValue) {
this.meter = meterValue;
}
@Override
protected boolean matchesSafely(JsonObject jsonMeter) {
// check application id
final String jsonAppId = jsonMeter.get("appId").asString();
final String appId = meter.appId().toString();
if (!jsonAppId.equals(appId)) {
reason = "appId " + meter.appId().toString();
return false;
}
// check device id
final String jsonDeviceId = jsonMeter.get("deviceId").asString();
if (!jsonDeviceId.equals(meter.deviceId().toString())) {
reason = "deviceId " + meter.deviceId();
return false;
}
// check band array
if (meter.bands() != null) {
final JsonArray jsonBands = jsonMeter.get("bands").asArray();
if (meter.bands().size() != jsonBands.size()) {
reason = "bands array size of " +
Integer.toString(meter.bands().size());
return false;
}
for (final Band band : meter.bands()) {
boolean bandFound = false;
for (int bandIndex = 0; bandIndex < jsonBands.size(); bandIndex++) {
final String jsonType = jsonBands.get(bandIndex).asObject().get("type").asString();
final String bandType = band.type().name();
if (jsonType.equals(bandType)) {
bandFound = true;
}
}
if (!bandFound) {
reason = "meter band " + band.toString();
return false;
}
}
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText(reason);
}
}
private static MeterJsonMatcher matchesMeter(Meter meter) {
return new MeterJsonMatcher(meter);
}
/**
* Hamcrest matcher to check that a meter is represented properly in a JSON
* array of meters.
*/
public static class MeterJsonArrayMatcher extends TypeSafeMatcher<JsonArray> {
private final Meter meter;
private String reason = "";
public MeterJsonArrayMatcher(Meter meterValue) {
meter = meterValue;
}
@Override
protected boolean matchesSafely(JsonArray json) {
boolean meterFound = false;
for (int jsonMeterIndex = 0; jsonMeterIndex < json.size(); jsonMeterIndex++) {
final JsonObject jsonMeter = json.get(jsonMeterIndex).asObject();
final String meterId = meter.id().toString();
final String jsonMeterId = jsonMeter.get("id").asString();
if (jsonMeterId.equals(meterId)) {
meterFound = true;
assertThat(jsonMeter, matchesMeter(meter));
}
}
if (!meterFound) {
reason = "Meter with id " + meter.id().toString() + " not found";
return false;
} else {
return true;
}
}
@Override
public void describeTo(Description description) {
description.appendText(reason);
}
}
/**
* Factory to allocate a meter array matcher.
*
* @param meter meter object we are looking for
* @return matcher
*/
private static MeterJsonArrayMatcher hasMeter(Meter meter) {
return new MeterJsonArrayMatcher(meter);
}
@Test
public void testMeterEmptyArray() {
expect(mockMeterService.getAllMeters()).andReturn(null).anyTimes();
replay(mockMeterService);
replay(mockDeviceService);
final WebResource rs = resource();
final String response = rs.path("meters").get(String.class);
assertThat(response, is("{\"meters\":[]}"));
}
/**
* Tests the result of the rest api GET when there are active meters.
*/
@Test
public void testMetersPopulatedArray() {
setupMockMeters();
replay(mockMeterService);
replay(mockDeviceService);
final WebResource rs = resource();
final String response = rs.path("meters").get(String.class);
final JsonObject result = JsonObject.readFrom(response);
assertThat(result, notNullValue());
assertThat(result.names(), hasSize(1));
assertThat(result.names().get(0), is("meters"));
final JsonArray jsonMeters = result.get("meters").asArray();
assertThat(jsonMeters, notNullValue());
assertThat(jsonMeters, hasMeter(meter1));
assertThat(jsonMeters, hasMeter(meter2));
assertThat(jsonMeters, hasMeter(meter3));
assertThat(jsonMeters, hasMeter(meter4));
}
/**
* Tests the result of a rest api GET for a device.
*/
@Test
public void testMeterSingleDeviceWithId() {
setupMockMeters();
expect(mockMeterService.getMeter(anyObject(), anyObject()))
.andReturn(meter5).anyTimes();
replay(mockMeterService);
replay(mockDeviceService);
final WebResource rs = resource();
final String response = rs.path("meters/" + deviceId3.toString()
+ "/" + meter5.id().id()).get(String.class);
final JsonObject result = JsonObject.readFrom(response);
assertThat(result, notNullValue());
assertThat(result.names(), hasSize(1));
assertThat(result.names().get(0), is("meters"));
final JsonArray jsonFlows = result.get("meters").asArray();
assertThat(jsonFlows, notNullValue());
assertThat(jsonFlows, hasMeter(meter5));
}
/**
* Tests creating a meter with POST.
*/
@Test
public void testPost() {
mockMeterService.submit(anyObject());
expectLastCall().andReturn(meter5).anyTimes();
replay(mockMeterService);
WebResource rs = resource();
InputStream jsonStream = MetersResourceTest.class
.getResourceAsStream("post-meter.json");
ClientResponse response = rs.path("meters/of:0000000000000001")
.type(MediaType.APPLICATION_JSON_TYPE)
.post(ClientResponse.class, jsonStream);
assertThat(response.getStatus(), is(HttpURLConnection.HTTP_CREATED));
}
/**
* Tests deleting a meter.
*/
@Test
public void testDelete() {
setupMockMeters();
expect(mockMeterService.getMeter(anyObject(), anyObject()))
.andReturn(meter5).anyTimes();
mockMeterService.withdraw(anyObject(), anyObject());
expectLastCall();
replay(mockMeterService);
WebResource rs = resource();
String location = "/meters/3/555";
ClientResponse deleteResponse = rs.path(location)
.type(MediaType.APPLICATION_JSON_TYPE)
.delete(ClientResponse.class);
assertThat(deleteResponse.getStatus(),
is(HttpURLConnection.HTTP_NO_CONTENT));
}
}
{
"id": 1,
"deviceId": "of:0000000000000001",
"unit": "KB_PER_SEC",
"bands": [
{
"type": "REMARK",
"rate": 10,
"prec": 20,
"burstSize": 30
}
]
}
\ No newline at end of file