From f96d41fd1896a5f779f9ebe85f7432ff2bdd598d Mon Sep 17 00:00:00 2001 From: Jian Li Date: Tue, 3 May 2016 09:49:12 -0700 Subject: [PATCH] [ONOS-4438] Add REST API for mastership service - Correct some typos in mastership codec Change-Id: If8a0127d8d897d4b87cae71a194dfece6aa14f49 --- .../onosproject/codec/impl/RoleInfoCodec.java | 2 +- .../rest/resources/CoreWebApplication.java | 3 +- .../rest/resources/MastershipWebResource.java | 250 ++++++++++++++ .../resources/definitions/MastershipPut.json | 23 ++ .../resources/definitions/MastershipRole.json | 13 + .../main/resources/definitions/RoleInfo.json | 25 ++ .../resources/MastershipResourceTest.java | 316 ++++++++++++++++++ .../rest/resources/put-set-roles.json | 5 + 8 files changed, 635 insertions(+), 2 deletions(-) create mode 100644 web/api/src/main/java/org/onosproject/rest/resources/MastershipWebResource.java create mode 100644 web/api/src/main/resources/definitions/MastershipPut.json create mode 100644 web/api/src/main/resources/definitions/MastershipRole.json create mode 100644 web/api/src/main/resources/definitions/RoleInfo.json create mode 100644 web/api/src/test/java/org/onosproject/rest/resources/MastershipResourceTest.java create mode 100644 web/api/src/test/resources/org/onosproject/rest/resources/put-set-roles.json diff --git a/core/common/src/main/java/org/onosproject/codec/impl/RoleInfoCodec.java b/core/common/src/main/java/org/onosproject/codec/impl/RoleInfoCodec.java index c63c078efd..2bed2826cb 100644 --- a/core/common/src/main/java/org/onosproject/codec/impl/RoleInfoCodec.java +++ b/core/common/src/main/java/org/onosproject/codec/impl/RoleInfoCodec.java @@ -39,7 +39,7 @@ public final class RoleInfoCodec extends JsonCodec { private static final String MASTER = "master"; private static final String BACKUPS = "backups"; - private static final String MISSING_MEMBER_MESSAGE = " member is required in MastershipTerm"; + private static final String MISSING_MEMBER_MESSAGE = " member is required in RoleInfo"; @Override public ObjectNode encode(RoleInfo roleInfo, CodecContext context) { diff --git a/web/api/src/main/java/org/onosproject/rest/resources/CoreWebApplication.java b/web/api/src/main/java/org/onosproject/rest/resources/CoreWebApplication.java index 237ffb1f7d..2a0367104d 100644 --- a/web/api/src/main/java/org/onosproject/rest/resources/CoreWebApplication.java +++ b/web/api/src/main/java/org/onosproject/rest/resources/CoreWebApplication.java @@ -49,7 +49,8 @@ public class CoreWebApplication extends AbstractWebApplication { DeviceKeyWebResource.class, RegionsWebResource.class, TenantWebResource.class, - VirtualNetworkWebResource.class + VirtualNetworkWebResource.class, + MastershipWebResource.class ); } } diff --git a/web/api/src/main/java/org/onosproject/rest/resources/MastershipWebResource.java b/web/api/src/main/java/org/onosproject/rest/resources/MastershipWebResource.java new file mode 100644 index 0000000000..4dfc28f02a --- /dev/null +++ b/web/api/src/main/java/org/onosproject/rest/resources/MastershipWebResource.java @@ -0,0 +1,250 @@ +/* + * Copyright 2016-present 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.cluster.NodeId; +import org.onosproject.cluster.RoleInfo; +import org.onosproject.mastership.MastershipAdminService; +import org.onosproject.mastership.MastershipService; +import org.onosproject.net.DeviceId; +import org.onosproject.net.MastershipRole; +import org.onosproject.rest.AbstractWebResource; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +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.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static org.onlab.util.Tools.nullIsNotFound; + +/** + * Manage the mastership of ONOS instances. + */ +@Path("mastership") +public final class MastershipWebResource extends AbstractWebResource { + + private static final String NODE = "node"; + private static final String DEVICES = "devices"; + private static final String DEVICE_ID = "deviceId"; + private static final String NODE_ID = "nodeId"; + + private static final String DEVICE_ID_INVALID = "Invalid deviceId for setting role"; + private static final String NODE_ID_INVALID = "Invalid nodeId for setting role"; + + private static final String NODE_ID_NOT_FOUND = "Node id is not found"; + private static final String ROLE_INFO_NOT_FOUND = "Role info is not found"; + private static final String MASTERSHIP_ROLE_NOT_FOUND = "Mastership role is not found"; + private static final String RESULT_NOT_FOUND = "Result is not found"; + + private final MastershipService mastershipService = get(MastershipService.class); + private final MastershipAdminService mastershipAdminService = + get(MastershipAdminService.class); + + /** + * Returns the role of the local node for the specified device. + * + * @param deviceId device identifier + * @return role of the current node + * @onos.rsModel MastershipRole + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{deviceId}/local") + public Response getLocalRole(@PathParam("deviceId") String deviceId) { + MastershipRole role = mastershipService.getLocalRole(DeviceId.deviceId(deviceId)); + ObjectNode root = codec(MastershipRole.class).encode(role, this); + return ok(root).build(); + } + + /** + * Returns the current master for a given device. + * + * @param deviceId device identifier + * @return the identifier of the master controller for the device + * // TODO: add swagger doc + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{deviceId}/master") + public Response getMasterFor(@PathParam("deviceId") String deviceId) { + NodeId id = nullIsNotFound(mastershipService.getMasterFor( + DeviceId.deviceId(deviceId)), NODE_ID_NOT_FOUND); + + ObjectNode root = mapper().createObjectNode(); + root.put(NODE, id.id()); + return ok(root).build(); + } + + /** + * Returns controllers connected to a given device, in order of + * preference. The first entry in the list is the current master. + * + * @param deviceId device identifier + * @return a list of controller identifiers + * @onos.rsModel RoleInfo + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{deviceId}/role") + public Response getNodesFor(@PathParam("deviceId") String deviceId) { + RoleInfo info = nullIsNotFound(mastershipService.getNodesFor( + DeviceId.deviceId(deviceId)), ROLE_INFO_NOT_FOUND); + ObjectNode root = codec(RoleInfo.class).encode(info, this); + return ok(root).build(); + } + + /** + * Returns the devices for which a controller is master. + * + * @param nodeId controller identifier + * @return a set of device identifiers + * // TODO: add swagger doc + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{nodeId}/device") + public Response getDeviceOf(@PathParam("nodeId") String nodeId) { + ObjectNode root = mapper().createObjectNode(); + ArrayNode devicesNode = root.putArray(DEVICES); + + Set devices = mastershipService.getDevicesOf(NodeId.nodeId(nodeId)); + if (devices != null) { + devices.forEach(id -> devicesNode.add(id.toString())); + } + + return ok(root).build(); + } + + /** + * Returns the mastership status of the local controller for a given + * device forcing master selection if necessary. + * + * @param deviceId device identifier + * @return the role of this controller instance + * @onos.rsModel MastershipRole + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{deviceId}/request") + public Response requestRoleFor(@PathParam("deviceId") String deviceId) { + + // TODO: will not use CompletableFuture when MastershipService + // provides a non CompletableFuture object as an output + CompletableFuture result = + nullIsNotFound(mastershipService.requestRoleFor( + DeviceId.deviceId(deviceId)), MASTERSHIP_ROLE_NOT_FOUND); + + try { + MastershipRole role = result.get(); + ObjectNode root = codec(MastershipRole.class).encode(role, this); + return ok(root).build(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Abandons mastership of the specified device on the local node thus + * forcing selection of a new master. If the local node is not a master + * for this device, no master selection will occur. + * + * @param deviceId device identifier + * @return status of the request + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("{deviceId}/relinquish") + public Response relinquishMastership(@PathParam("deviceId") String deviceId) { + DeviceId id = DeviceId.deviceId(deviceId); + + // TODO: will not use CompletableFuture when MastershipService + // provides a non CompletableFuture object as an output + CompletableFuture result = + nullIsNotFound(mastershipService.relinquishMastership(id), RESULT_NOT_FOUND); + + try { + result.get(); + return Response.created(id.uri()).build(); + } catch (InterruptedException | ExecutionException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Applies the current mastership role for the specified device. + * + * @param stream JSON representation of device, node, mastership info + * @return status of the request - CREATED if the JSON is correct, + * BAD_REQUEST if the JSON is invalid + * @onos.rsModel MastershipPut + */ + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response setRole(InputStream stream) { + + try { + ObjectNode jsonTree = (ObjectNode) mapper().readTree(stream); + JsonNode deviceIdJson = jsonTree.get(DEVICE_ID); + JsonNode nodeIdJson = jsonTree.get(NODE_ID); + MastershipRole role = codec(MastershipRole.class).decode(jsonTree, this); + + if (deviceIdJson == null) { + throw new IllegalArgumentException(DEVICE_ID_INVALID); + } + + if (nodeIdJson == null) { + throw new IllegalArgumentException(NODE_ID_INVALID); + } + + // TODO: will not use CompletableFuture when MastershipAdminService + // provides a non CompletableFuture object as an output + CompletableFuture result = + nullIsNotFound(mastershipAdminService.setRole(NodeId.nodeId(nodeIdJson.asText()), + DeviceId.deviceId(deviceIdJson.asText()), role), RESULT_NOT_FOUND); + result.get(); + + return Response.ok().build(); + } catch (InterruptedException | ExecutionException | IOException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Balances the mastership to be shared as evenly as possibly by all + * online instances. + * + * @return status of the request - OK if the request is successfully processed + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response balanceRoles() { + mastershipAdminService.balanceRoles(); + return Response.ok().build(); + } +} diff --git a/web/api/src/main/resources/definitions/MastershipPut.json b/web/api/src/main/resources/definitions/MastershipPut.json new file mode 100644 index 0000000000..67f0437419 --- /dev/null +++ b/web/api/src/main/resources/definitions/MastershipPut.json @@ -0,0 +1,23 @@ +{ + "type": "object", + "title": "mastership", + "required": [ + "deviceId", + "nodeId", + "mastershipRole" + ], + "properties": { + "deviceId": { + "type": "string", + "example": "of:0000000000000001" + }, + "nodeId": { + "type": "string", + "example": "1" + }, + "mastershipRole": { + "type": "string", + "example": "MASTER" + } + } +} \ No newline at end of file diff --git a/web/api/src/main/resources/definitions/MastershipRole.json b/web/api/src/main/resources/definitions/MastershipRole.json new file mode 100644 index 0000000000..add66aa0be --- /dev/null +++ b/web/api/src/main/resources/definitions/MastershipRole.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "title": "mastershipRole", + "required": [ + "role" + ], + "properties": { + "role": { + "type": "string", + "example": "MASTER" + } + } +} \ No newline at end of file diff --git a/web/api/src/main/resources/definitions/RoleInfo.json b/web/api/src/main/resources/definitions/RoleInfo.json new file mode 100644 index 0000000000..55e1e9d28f --- /dev/null +++ b/web/api/src/main/resources/definitions/RoleInfo.json @@ -0,0 +1,25 @@ +{ + "type": "object", + "title": "roleInfo", + "required": [ + "master", + "backups" + ], + "properties": { + "master": { + "type": "string", + "example": "1" + }, + "backups": { + "type": "array", + "xml": { + "name": "backups", + "wrapped": true + }, + "items": { + "type": "string", + "example": "1" + } + } + } +} \ No newline at end of file diff --git a/web/api/src/test/java/org/onosproject/rest/resources/MastershipResourceTest.java b/web/api/src/test/java/org/onosproject/rest/resources/MastershipResourceTest.java new file mode 100644 index 0000000000..de92cd5f3a --- /dev/null +++ b/web/api/src/test/java/org/onosproject/rest/resources/MastershipResourceTest.java @@ -0,0 +1,316 @@ +/* + * Copyright 2016-present 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.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.apache.commons.lang3.StringUtils; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +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.cluster.NodeId; +import org.onosproject.cluster.RoleInfo; +import org.onosproject.codec.CodecService; +import org.onosproject.codec.impl.CodecManager; +import org.onosproject.mastership.MastershipAdminService; +import org.onosproject.mastership.MastershipService; +import org.onosproject.net.DeviceId; +import org.onosproject.net.MastershipRole; + +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import java.net.HttpURLConnection; +import java.util.List; +import java.util.Set; + +import static org.easymock.EasyMock.anyObject; +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.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.onosproject.net.MastershipRole.MASTER; + +/** + * Unit tests for Mastership REST APIs. + */ +public final class MastershipResourceTest extends ResourceTest { + + private final MastershipService mockService = createMock(MastershipService.class); + private final MastershipAdminService mockAdminService = + createMock(MastershipAdminService.class); + + private final DeviceId deviceId1 = DeviceId.deviceId("dev:1"); + private final DeviceId deviceId2 = DeviceId.deviceId("dev:2"); + private final DeviceId deviceId3 = DeviceId.deviceId("dev:3"); + + private final NodeId nodeId1 = NodeId.nodeId("node:1"); + private final NodeId nodeId2 = NodeId.nodeId("node:2"); + private final NodeId nodeId3 = NodeId.nodeId("node:3"); + private final MastershipRole role1 = MASTER; + + /** + * Creates a mock role info which is comprised of one master and three backups. + * + * @return a mock role info instance + */ + private RoleInfo createMockRoleInfo() { + NodeId master = NodeId.nodeId("master"); + List backups = ImmutableList.of(nodeId1, nodeId2, nodeId3); + + return new RoleInfo(master, backups); + } + + private static final class RoleInfoJsonMatcher extends TypeSafeMatcher { + private final RoleInfo roleInfo; + private String reason = ""; + + private RoleInfoJsonMatcher(RoleInfo roleInfo) { + this.roleInfo = roleInfo; + } + + @Override + protected boolean matchesSafely(JsonObject jsonNode) { + + // check master node identifier + String jsonNodeId = jsonNode.get("master") != null ? + jsonNode.get("master").asString() : null; + String nodeId = roleInfo.master().id(); + if (!StringUtils.equals(jsonNodeId, nodeId)) { + reason = "master's node id was " + jsonNodeId; + return false; + } + + // check backup nodes size + final JsonArray jsonBackupNodeIds = jsonNode.get("backups").asArray(); + if (jsonBackupNodeIds.size() != roleInfo.backups().size()) { + reason = "backup nodes size was " + jsonBackupNodeIds.size(); + return false; + } + + // check backup nodes' identifier + for (NodeId backupNodeId : roleInfo.backups()) { + boolean backupFound = false; + for (int idx = 0; idx < jsonBackupNodeIds.size(); idx++) { + if (backupNodeId.id().equals(jsonBackupNodeIds.get(idx).asString())) { + backupFound = true; + break; + } + } + if (!backupFound) { + reason = "backup not found " + backupNodeId.id(); + return false; + } + } + + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText(reason); + } + } + + /** + * Factory to allocate a role info json matcher. + * + * @param roleInfo role info object we are looking for + * @return matcher + */ + private static RoleInfoJsonMatcher matchesRoleInfo(RoleInfo roleInfo) { + return new RoleInfoJsonMatcher(roleInfo); + } + + /** + * Sets up the global values for all the tests. + */ + @Before + public void setUpTest() { + + final CodecManager codecService = new CodecManager(); + codecService.activate(); + ServiceDirectory testDirectory = + new TestServiceDirectory() + .add(MastershipService.class, mockService) + .add(MastershipAdminService.class, mockAdminService) + .add(CodecService.class, codecService); + + BaseResource.setServiceDirectory(testDirectory); + } + + /** + * Tests the result of the REST API GET when there are active master roles. + */ + @Test + public void testGetLocalRole() { + expect(mockService.getLocalRole(anyObject())).andReturn(role1).anyTimes(); + replay(mockService); + + final WebTarget wt = target(); + final String response = wt.path("mastership/" + deviceId1.toString() + + "/local").request().get(String.class); + final JsonObject result = Json.parse(response).asObject(); + assertThat(result, notNullValue()); + + assertThat(result.names(), hasSize(1)); + assertThat(result.names().get(0), is("role")); + + final String role = result.get("role").asString(); + assertThat(role, notNullValue()); + assertThat(role, is("MASTER")); + } + + /** + * Tests the result of the REST API GET when there is no active master. + */ + @Test + public void testGetMasterForNull() { + expect(mockService.getMasterFor(anyObject())).andReturn(null).anyTimes(); + replay(mockService); + + final WebTarget wt = target(); + final Response response = wt.path("mastership/" + deviceId1.toString() + + "/master").request().get(); + assertEquals(404, response.getStatus()); + } + + /** + * Tests the result of the REST API GET when there is active master. + */ + @Test + public void testGetMasterFor() { + expect(mockService.getMasterFor(anyObject())).andReturn(nodeId1).anyTimes(); + replay(mockService); + + final WebTarget wt = target(); + final String response = wt.path("mastership/" + deviceId1.toString() + + "/master").request().get(String.class); + final JsonObject result = Json.parse(response).asObject(); + assertThat(result, notNullValue()); + + assertThat(result.names(), hasSize(1)); + assertThat(result.names().get(0), is("node")); + + final String node = result.get("node").asString(); + assertThat(node, notNullValue()); + assertThat(node, is("node:1")); + } + + /** + * Tests the result of the REST API GET when there are no active nodes. + */ + @Test + public void testGetNodesForNull() { + expect(mockService.getNodesFor(anyObject())).andReturn(null).anyTimes(); + replay(mockService); + + final WebTarget wt = target(); + final Response response = wt.path("mastership/" + deviceId1.toString() + + "/role").request().get(); + assertEquals(404, response.getStatus()); + } + + /** + * Tests the result of the REST API GET when there are active nodes. + */ + @Test + public void testGetNodesFor() { + RoleInfo mockRoleInfo = createMockRoleInfo(); + expect(mockService.getNodesFor(anyObject())).andReturn(mockRoleInfo).anyTimes(); + replay(mockService); + + final WebTarget wt = target(); + final String response = wt.path("mastership/" + deviceId1.toString() + + "/role").request().get(String.class); + final JsonObject result = Json.parse(response).asObject(); + assertThat(result, notNullValue()); + + assertThat(result, matchesRoleInfo(mockRoleInfo)); + } + + /** + * Tests the result of the REST API GET when there are active devices. + */ + @Test + public void testGetDevicesOf() { + Set deviceIds = ImmutableSet.of(deviceId1, deviceId2, deviceId3); + expect(mockService.getDevicesOf(anyObject())).andReturn(deviceIds).anyTimes(); + replay(mockService); + + final WebTarget wt = target(); + final String response = wt.path("mastership/" + deviceId1.toString() + + "/device").request().get(String.class); + final JsonObject result = Json.parse(response).asObject(); + assertThat(result, notNullValue()); + + assertThat(result.names(), hasSize(1)); + assertThat(result.names().get(0), is("devices")); + + final JsonArray jsonDevices = result.get("devices").asArray(); + assertThat(jsonDevices, notNullValue()); + assertThat(jsonDevices.size(), is(3)); + } + + /** + * Tests the result of the REST API GET for requesting mastership role. + */ + @Test + public void testRequestRoleFor() { + // TODO: will be added when CompletableFuture is removed + } + + /** + * Tests the result of the REST API GET for relinquishing mastership role. + */ + @Test + public void testRelinquishMastership() { + // TODO: will be added when CompletableFuture is removed + } + + /** + * Tests the result of the REST API PUT for setting role. + */ + @Test + public void testSetRole() { + // TODO: will be added when CompletableFuture is removed + } + + /** + * Tests the result of the REST API GET for balancing roles. + */ + @Test + public void testBalanceRoles() { + mockAdminService.balanceRoles(); + expectLastCall(); + replay(mockAdminService); + + final WebTarget wt = target(); + final Response response = wt.path("mastership").request().get(); + assertThat(response.getStatus(), is(HttpURLConnection.HTTP_OK)); + } +} diff --git a/web/api/src/test/resources/org/onosproject/rest/resources/put-set-roles.json b/web/api/src/test/resources/org/onosproject/rest/resources/put-set-roles.json new file mode 100644 index 0000000000..01cf1e1ed6 --- /dev/null +++ b/web/api/src/test/resources/org/onosproject/rest/resources/put-set-roles.json @@ -0,0 +1,5 @@ +{ + "deviceId": "of:0000000000000001", + "nodeId": "1", + "role": "MASTER" +} \ No newline at end of file