diff options
author | 2015-09-09 22:15:21 -0700 | |
---|---|---|
committer | 2015-09-09 22:15:21 -0700 | |
commit | 13d05bc8458758ee39cb829098241e89616717ee (patch) | |
tree | 22a4d1ce65f15952f07a3df5af4b462b4697cb3a /framework/src/onos/web/gui/src/main/java/org | |
parent | 6139282e1e93c2322076de4b91b1c85d0bc4a8b3 (diff) |
ONOS checkin based on commit tag e796610b1f721d02f9b0e213cf6f7790c10ecd60
Change-Id: Ife8810491034fe7becdba75dda20de4267bd15cd
Diffstat (limited to 'framework/src/onos/web/gui/src/main/java/org')
33 files changed, 5647 insertions, 0 deletions
diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java new file mode 100644 index 00000000..398bfee0 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationResource.java @@ -0,0 +1,44 @@ +/* + * 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.ui.impl; + +import com.sun.jersey.multipart.FormDataParam; +import org.onlab.rest.BaseResource; +import org.onosproject.app.ApplicationAdminService; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; + +/** + * Application upload resource. + */ +@Path("applications") +public class ApplicationResource extends BaseResource { + + @Path("upload") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response upload(@FormDataParam("file") InputStream stream) throws IOException { + get(ApplicationAdminService.class).install(stream); + return Response.ok().build(); + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationViewMessageHandler.java new file mode 100644 index 00000000..77a58972 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ApplicationViewMessageHandler.java @@ -0,0 +1,125 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; +import org.onosproject.app.ApplicationAdminService; +import org.onosproject.app.ApplicationService; +import org.onosproject.app.ApplicationState; +import org.onosproject.core.Application; +import org.onosproject.core.ApplicationId; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; + +import java.util.Collection; + +import static org.onosproject.app.ApplicationState.ACTIVE; + +/** + * Message handler for application view related messages. + */ +public class ApplicationViewMessageHandler extends UiMessageHandler { + + private static final String APP_DATA_REQ = "appDataRequest"; + private static final String APP_DATA_RESP = "appDataResponse"; + private static final String APPS = "apps"; + + private static final String APP_MGMT_REQ = "appManagementRequest"; + + private static final String STATE = "state"; + private static final String STATE_IID = "_iconid_state"; + private static final String ID = "id"; + private static final String VERSION = "version"; + private static final String ORIGIN = "origin"; + private static final String DESC = "desc"; + + private static final String ICON_ID_ACTIVE = "active"; + private static final String ICON_ID_INACTIVE = "appInactive"; + + private static final String[] COL_IDS = { + STATE, STATE_IID, ID, VERSION, ORIGIN, DESC + }; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of( + new AppDataRequest(), + new AppMgmtRequest() + ); + } + + // handler for application table requests + private final class AppDataRequest extends TableRequestHandler { + private AppDataRequest() { + super(APP_DATA_REQ, APP_DATA_RESP, APPS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + ApplicationService as = get(ApplicationService.class); + for (Application app : as.getApplications()) { + populateRow(tm.addRow(), app, as); + } + } + + private void populateRow(TableModel.Row row, Application app, + ApplicationService as) { + ApplicationId id = app.id(); + ApplicationState state = as.getState(id); + String iconId = state == ACTIVE ? ICON_ID_ACTIVE : ICON_ID_INACTIVE; + + row.cell(STATE, state) + .cell(STATE_IID, iconId) + .cell(ID, id.name()) + .cell(VERSION, app.version()) + .cell(ORIGIN, app.origin()) + .cell(DESC, app.description()); + } + } + + // handler for application management control button actions + private final class AppMgmtRequest extends RequestHandler { + private AppMgmtRequest() { + super(APP_MGMT_REQ); + } + + @Override + public void process(long sid, ObjectNode payload) { + String action = string(payload, "action"); + String name = string(payload, "name"); + if (action != null && name != null) { + ApplicationAdminService service = get(ApplicationAdminService.class); + ApplicationId appId = service.getId(name); + if (action.equals("activate")) { + service.activate(appId); + } else if (action.equals("deactivate")) { + service.deactivate(appId); + } else if (action.equals("uninstall")) { + service.uninstall(appId); + } + chain(APP_DATA_REQ, sid, payload); + } + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java new file mode 100644 index 00000000..da0eae07 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/ClusterViewMessageHandler.java @@ -0,0 +1,101 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; +import org.joda.time.DateTime; +import org.onosproject.cluster.ClusterService; +import org.onosproject.cluster.ControllerNode; +import org.onosproject.cluster.NodeId; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; +import org.onosproject.ui.table.cell.TimeFormatter; + +import java.util.Collection; + + +/** + * Message handler for cluster view related messages. + */ +public class ClusterViewMessageHandler extends UiMessageHandler { + + private static final String CLUSTER_DATA_REQ = "clusterDataRequest"; + private static final String CLUSTER_DATA_RESP = "clusterDataResponse"; + private static final String CLUSTERS = "clusters"; + + private static final String ID = "id"; + private static final String IP = "ip"; + private static final String TCP_PORT = "tcp"; + private static final String STATE_IID = "_iconid_state"; + private static final String UPDATED = "updated"; + + private static final String[] COL_IDS = { + ID, IP, TCP_PORT, STATE_IID, UPDATED + }; + + private static final String ICON_ID_ONLINE = "active"; + private static final String ICON_ID_OFFLINE = "inactive"; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new ClusterDataRequest()); + } + + // handler for cluster table requests + private final class ClusterDataRequest extends TableRequestHandler { + private ClusterDataRequest() { + super(CLUSTER_DATA_REQ, CLUSTER_DATA_RESP, CLUSTERS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected TableModel createTableModel() { + TableModel tm = super.createTableModel(); + tm.setFormatter(UPDATED, new TimeFormatter()); + return tm; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + ClusterService cs = get(ClusterService.class); + for (ControllerNode node : cs.getNodes()) { + populateRow(tm.addRow(), node, cs); + } + } + + private void populateRow(TableModel.Row row, ControllerNode node, + ClusterService cs) { + NodeId id = node.id(); + DateTime lastUpdated = cs.getLastUpdated(id); + String iconId = (cs.getState(id) == ControllerNode.State.ACTIVE) ? + ICON_ID_ONLINE : ICON_ID_OFFLINE; + + row.cell(ID, id) + .cell(IP, node.ip()) + .cell(TCP_PORT, node.tcpPort()) + .cell(STATE_IID, iconId) + .cell(UPDATED, lastUpdated); + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java new file mode 100644 index 00000000..fb83cdd7 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/DeviceViewMessageHandler.java @@ -0,0 +1,209 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; +import org.onosproject.mastership.MastershipService; +import org.onosproject.net.AnnotationKeys; +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.Device; +import org.onosproject.net.DeviceId; +import org.onosproject.net.Link; +import org.onosproject.net.Port; +import org.onosproject.net.device.DeviceService; +import org.onosproject.net.link.LinkService; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.apache.commons.lang.WordUtils.capitalizeFully; + +/** + * Message handler for device view related messages. + */ +public class DeviceViewMessageHandler extends UiMessageHandler { + + private static final String DEV_DATA_REQ = "deviceDataRequest"; + private static final String DEV_DATA_RESP = "deviceDataResponse"; + private static final String DEVICES = "devices"; + + private static final String DEV_DETAILS_REQ = "deviceDetailsRequest"; + private static final String DEV_DETAILS_RESP = "deviceDetailsResponse"; + private static final String DETAILS = "details"; + + private static final String ID = "id"; + private static final String TYPE = "type"; + private static final String AVAILABLE = "available"; + private static final String AVAILABLE_IID = "_iconid_available"; + private static final String TYPE_IID = "_iconid_type"; + private static final String DEV_ICON_PREFIX = "devIcon_"; + private static final String NUM_PORTS = "num_ports"; + private static final String LINK_DEST = "elinks_dest"; + private static final String MFR = "mfr"; + private static final String HW = "hw"; + private static final String SW = "sw"; + private static final String PROTOCOL = "protocol"; + private static final String MASTER_ID = "masterid"; + private static final String CHASSIS_ID = "chassisid"; + private static final String SERIAL = "serial"; + private static final String PORTS = "ports"; + private static final String ENABLED = "enabled"; + private static final String SPEED = "speed"; + private static final String NAME = "name"; + + + private static final String[] COL_IDS = { + AVAILABLE, AVAILABLE_IID, TYPE_IID, ID, + NUM_PORTS, MASTER_ID, MFR, HW, SW, + PROTOCOL, CHASSIS_ID, SERIAL + }; + + private static final String ICON_ID_ONLINE = "active"; + private static final String ICON_ID_OFFLINE = "inactive"; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of( + new DataRequestHandler(), + new DetailRequestHandler() + ); + } + + private static String getTypeIconId(Device d) { + return DEV_ICON_PREFIX + d.type().toString(); + } + + // handler for device table requests + private final class DataRequestHandler extends TableRequestHandler { + private DataRequestHandler() { + super(DEV_DATA_REQ, DEV_DATA_RESP, DEVICES); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + DeviceService ds = get(DeviceService.class); + MastershipService ms = get(MastershipService.class); + for (Device dev : ds.getDevices()) { + populateRow(tm.addRow(), dev, ds, ms); + } + } + + private void populateRow(TableModel.Row row, Device dev, + DeviceService ds, MastershipService ms) { + DeviceId id = dev.id(); + boolean available = ds.isAvailable(id); + String iconId = available ? ICON_ID_ONLINE : ICON_ID_OFFLINE; + + String protocol = dev.annotations().value(PROTOCOL); + + row.cell(ID, id) + .cell(AVAILABLE, available) + .cell(AVAILABLE_IID, iconId) + .cell(TYPE_IID, getTypeIconId(dev)) + .cell(MFR, dev.manufacturer()) + .cell(HW, dev.hwVersion()) + .cell(SW, dev.swVersion()) + .cell(PROTOCOL, protocol != null ? protocol : "") + .cell(NUM_PORTS, ds.getPorts(id).size()) + .cell(MASTER_ID, ms.getMasterFor(id)); + } + } + + // handler for selected device detail requests + private final class DetailRequestHandler extends RequestHandler { + private DetailRequestHandler() { + super(DEV_DETAILS_REQ); + } + + @Override + public void process(long sid, ObjectNode payload) { + String id = string(payload, "id", "of:0000000000000000"); + + DeviceId deviceId = DeviceId.deviceId(id); + DeviceService service = get(DeviceService.class); + MastershipService ms = get(MastershipService.class); + Device device = service.getDevice(deviceId); + ObjectNode data = MAPPER.createObjectNode(); + + data.put(ID, deviceId.toString()); + data.put(TYPE, capitalizeFully(device.type().toString())); + data.put(TYPE_IID, getTypeIconId(device)); + data.put(MFR, device.manufacturer()); + data.put(HW, device.hwVersion()); + data.put(SW, device.swVersion()); + data.put(SERIAL, device.serialNumber()); + data.put(CHASSIS_ID, device.chassisId().toString()); + data.put(MASTER_ID, ms.getMasterFor(deviceId).toString()); + data.put(PROTOCOL, device.annotations().value(PROTOCOL)); + + ArrayNode ports = MAPPER.createArrayNode(); + + List<Port> portList = new ArrayList<>(service.getPorts(deviceId)); + Collections.sort(portList, (p1, p2) -> { + long delta = p1.number().toLong() - p2.number().toLong(); + return delta == 0 ? 0 : (delta < 0 ? -1 : +1); + }); + + for (Port p : portList) { + ports.add(portData(p, deviceId)); + } + data.set(PORTS, ports); + + ObjectNode rootNode = MAPPER.createObjectNode(); + rootNode.set(DETAILS, data); + sendMessage(DEV_DETAILS_RESP, 0, rootNode); + } + + private ObjectNode portData(Port p, DeviceId id) { + ObjectNode port = MAPPER.createObjectNode(); + LinkService ls = get(LinkService.class); + String name = p.annotations().value(AnnotationKeys.PORT_NAME); + + port.put(ID, capitalizeFully(p.number().toString())); + port.put(TYPE, capitalizeFully(p.type().toString())); + port.put(SPEED, p.portSpeed()); + port.put(ENABLED, p.isEnabled()); + port.put(NAME, name != null ? name : ""); + + Set<Link> links = ls.getEgressLinks(new ConnectPoint(id, p.number())); + if (!links.isEmpty()) { + StringBuilder egressLinks = new StringBuilder(); + for (Link l : links) { + ConnectPoint dest = l.dst(); + egressLinks.append(dest.elementId()).append("/") + .append(dest.port()).append(" "); + } + port.put(LINK_DEST, egressLinks.toString()); + } + + return port; + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/FlowViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/FlowViewMessageHandler.java new file mode 100644 index 00000000..02179115 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/FlowViewMessageHandler.java @@ -0,0 +1,164 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import org.onosproject.net.DeviceId; +import org.onosproject.net.flow.FlowEntry; +import org.onosproject.net.flow.FlowRuleService; +import org.onosproject.net.flow.criteria.Criterion; +import org.onosproject.net.flow.instructions.Instruction; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.CellFormatter; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; +import org.onosproject.ui.table.cell.EnumFormatter; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Message handler for flow view related messages. + */ +public class FlowViewMessageHandler extends UiMessageHandler { + + private static final String FLOW_DATA_REQ = "flowDataRequest"; + private static final String FLOW_DATA_RESP = "flowDataResponse"; + private static final String FLOWS = "flows"; + + private static final String ID = "id"; + private static final String APP_ID = "appId"; + private static final String GROUP_ID = "groupId"; + private static final String TABLE_ID = "tableId"; + private static final String PRIORITY = "priority"; + private static final String SELECTOR = "selector"; + private static final String TREATMENT = "treatment"; + private static final String TIMEOUT = "timeout"; + private static final String PERMANENT = "permanent"; + private static final String STATE = "state"; + private static final String PACKETS = "packets"; + private static final String BYTES = "bytes"; + + private static final String COMMA = ", "; + + private static final String[] COL_IDS = { + ID, APP_ID, GROUP_ID, TABLE_ID, PRIORITY, SELECTOR, + TREATMENT, TIMEOUT, PERMANENT, STATE, PACKETS, BYTES + }; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new FlowDataRequest()); + } + + // handler for flow table requests + private final class FlowDataRequest extends TableRequestHandler { + + private FlowDataRequest() { + super(FLOW_DATA_REQ, FLOW_DATA_RESP, FLOWS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected TableModel createTableModel() { + TableModel tm = super.createTableModel(); + tm.setFormatter(SELECTOR, new SelectorFormatter()); + tm.setFormatter(TREATMENT, new TreatmentFormatter()); + tm.setFormatter(STATE, EnumFormatter.INSTANCE); + return tm; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + String uri = string(payload, "devId"); + if (!Strings.isNullOrEmpty(uri)) { + DeviceId deviceId = DeviceId.deviceId(uri); + FlowRuleService frs = get(FlowRuleService.class); + for (FlowEntry flow : frs.getFlowEntries(deviceId)) { + populateRow(tm.addRow(), flow); + } + } + } + + private void populateRow(TableModel.Row row, FlowEntry flow) { + row.cell(ID, flow.id().value()) + .cell(APP_ID, flow.appId()) + .cell(GROUP_ID, flow.groupId().id()) + .cell(TABLE_ID, flow.tableId()) + .cell(PRIORITY, flow.priority()) + .cell(SELECTOR, flow) + .cell(TREATMENT, flow) + .cell(TIMEOUT, flow.timeout()) + .cell(PERMANENT, flow.isPermanent()) + .cell(STATE, flow.state()) + .cell(PACKETS, flow.packets()) + .cell(BYTES, flow.bytes()); + } + + private final class SelectorFormatter implements CellFormatter { + @Override + public String format(Object value) { + FlowEntry flow = (FlowEntry) value; + Set<Criterion> criteria = flow.selector().criteria(); + + if (criteria.isEmpty()) { + return "(No traffic selector criteria for this flow)"; + } + StringBuilder sb = new StringBuilder("Criteria: "); + for (Criterion c : criteria) { + sb.append(c).append(COMMA); + } + removeTrailingComma(sb); + + return sb.toString(); + } + } + + private final class TreatmentFormatter implements CellFormatter { + @Override + public String format(Object value) { + FlowEntry flow = (FlowEntry) value; + List<Instruction> instructions = flow.treatment().allInstructions(); + + if (instructions.isEmpty()) { + return "(No traffic treatment instructions for this flow)"; + } + StringBuilder sb = new StringBuilder("Treatment Instructions: "); + for (Instruction i : instructions) { + sb.append(i).append(COMMA); + } + removeTrailingComma(sb); + + return sb.toString(); + } + } + + private StringBuilder removeTrailingComma(StringBuilder sb) { + int pos = sb.lastIndexOf(COMMA); + sb.delete(pos, sb.length()); + return sb; + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/GroupViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/GroupViewMessageHandler.java new file mode 100644 index 00000000..c7d539b1 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/GroupViewMessageHandler.java @@ -0,0 +1,131 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import org.onosproject.net.DeviceId; +import org.onosproject.net.group.Group; +import org.onosproject.net.group.GroupBucket; +import org.onosproject.net.group.GroupService; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.CellFormatter; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; +import org.onosproject.ui.table.cell.EnumFormatter; + +import java.util.Collection; +import java.util.List; + + +/** + * Message handler for group view related messages. + */ +public class GroupViewMessageHandler extends UiMessageHandler { + + private static final String GROUP_DATA_REQ = "groupDataRequest"; + private static final String GROUP_DATA_RESP = "groupDataResponse"; + private static final String GROUPS = "groups"; + + private static final String ID = "id"; + private static final String APP_ID = "app_id"; + private static final String STATE = "state"; + private static final String TYPE = "type"; + private static final String PACKETS = "packets"; + private static final String BYTES = "bytes"; + private static final String BUCKETS = "buckets"; + + private static final String[] COL_IDS = { + ID, APP_ID, STATE, TYPE, PACKETS, BYTES, BUCKETS + }; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new GroupDataRequest()); + } + + // handler for group table requests + private final class GroupDataRequest extends TableRequestHandler { + + private GroupDataRequest() { + super(GROUP_DATA_REQ, GROUP_DATA_RESP, GROUPS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected TableModel createTableModel() { + TableModel tm = super.createTableModel(); + tm.setFormatter(TYPE, EnumFormatter.INSTANCE); + tm.setFormatter(BUCKETS, new BucketFormatter()); + return tm; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + String uri = string(payload, "devId"); + if (!Strings.isNullOrEmpty(uri)) { + DeviceId deviceId = DeviceId.deviceId(uri); + GroupService gs = get(GroupService.class); + for (Group group : gs.getGroups(deviceId)) { + populateRow(tm.addRow(), group); + } + } + } + + private void populateRow(TableModel.Row row, Group g) { + row.cell(ID, g.id().id()) + .cell(APP_ID, g.appId().name()) + .cell(STATE, g.state()) + .cell(TYPE, g.type()) + .cell(PACKETS, g.packets()) + .cell(BYTES, g.bytes()) + .cell(BUCKETS, g.buckets().buckets()); + } + + private final class BucketFormatter implements CellFormatter { + private static final String BREAK = "<br>"; + + @Override + public String format(Object value) { + StringBuilder sb = new StringBuilder(); + List<GroupBucket> buckets = (List<GroupBucket>) value; + + if (buckets.isEmpty()) { + return "(No buckets for this group)"; + } + + for (GroupBucket b : buckets) { + sb.append("Bytes: ") + .append(b.bytes()) + .append(" Packets: ") + .append(b.packets()) + .append(" Actions: ") + .append(b.treatment().allInstructions()) + .append(BREAK); + } + + return sb.toString(); + } + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/HostViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/HostViewMessageHandler.java new file mode 100644 index 00000000..12a2b664 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/HostViewMessageHandler.java @@ -0,0 +1,130 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; +import org.onlab.packet.IpAddress; +import org.onosproject.net.AnnotationKeys; +import org.onosproject.net.Host; +import org.onosproject.net.host.HostService; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.CellFormatter; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; +import org.onosproject.ui.table.cell.HostLocationFormatter; + +import java.util.Collection; +import java.util.Set; + +import static com.google.common.base.Strings.isNullOrEmpty; + +/** + * Message handler for host view related messages. + */ +public class HostViewMessageHandler extends UiMessageHandler { + + private static final String HOST_DATA_REQ = "hostDataRequest"; + private static final String HOST_DATA_RESP = "hostDataResponse"; + private static final String HOSTS = "hosts"; + + private static final String TYPE_IID = "_iconid_type"; + private static final String ID = "id"; + private static final String MAC = "mac"; + private static final String VLAN = "vlan"; + private static final String IPS = "ips"; + private static final String LOCATION = "location"; + + private static final String HOST_ICON_PREFIX = "hostIcon_"; + + private static final String[] COL_IDS = { + TYPE_IID, ID, MAC, VLAN, IPS, LOCATION + }; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new HostDataRequest()); + } + + // handler for host table requests + private final class HostDataRequest extends TableRequestHandler { + private HostDataRequest() { + super(HOST_DATA_REQ, HOST_DATA_RESP, HOSTS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected TableModel createTableModel() { + TableModel tm = super.createTableModel(); + tm.setFormatter(LOCATION, HostLocationFormatter.INSTANCE); + tm.setFormatter(IPS, new IpSetFormatter()); + return tm; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + HostService hs = get(HostService.class); + for (Host host : hs.getHosts()) { + populateRow(tm.addRow(), host); + } + } + + private void populateRow(TableModel.Row row, Host host) { + row.cell(TYPE_IID, getTypeIconId(host)) + .cell(ID, host.id()) + .cell(MAC, host.mac()) + .cell(VLAN, host.vlan()) + .cell(IPS, host.ipAddresses()) + .cell(LOCATION, host.location()); + } + + private String getTypeIconId(Host host) { + String hostType = host.annotations().value(AnnotationKeys.TYPE); + return HOST_ICON_PREFIX + + (isNullOrEmpty(hostType) ? "endstation" : hostType); + } + + private final class IpSetFormatter implements CellFormatter { + private static final String COMMA = ", "; + + @Override + public String format(Object value) { + Set<IpAddress> ips = (Set<IpAddress>) value; + if (ips.isEmpty()) { + return "(No IP Addresses for this host)"; + } + StringBuilder sb = new StringBuilder(); + for (IpAddress ip : ips) { + sb.append(ip.toString()) + .append(COMMA); + } + removeTrailingComma(sb); + return sb.toString(); + } + + private StringBuilder removeTrailingComma(StringBuilder sb) { + int pos = sb.lastIndexOf(COMMA); + sb.delete(pos, sb.length()); + return sb; + } + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/IntentViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/IntentViewMessageHandler.java new file mode 100644 index 00000000..bacb76d2 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/IntentViewMessageHandler.java @@ -0,0 +1,276 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.NetworkResource; +import org.onosproject.net.flow.criteria.Criterion; +import org.onosproject.net.flow.instructions.Instruction; +import org.onosproject.net.intent.ConnectivityIntent; +import org.onosproject.net.intent.Constraint; +import org.onosproject.net.intent.HostToHostIntent; +import org.onosproject.net.intent.Intent; +import org.onosproject.net.intent.IntentService; +import org.onosproject.net.intent.LinkCollectionIntent; +import org.onosproject.net.intent.MultiPointToSinglePointIntent; +import org.onosproject.net.intent.PathIntent; +import org.onosproject.net.intent.PointToPointIntent; +import org.onosproject.net.intent.SinglePointToMultiPointIntent; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.CellFormatter; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; +import org.onosproject.ui.table.cell.AppIdFormatter; +import org.onosproject.ui.table.cell.EnumFormatter; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Message handler for intent view related messages. + */ +public class IntentViewMessageHandler extends UiMessageHandler { + + private static final String INTENT_DATA_REQ = "intentDataRequest"; + private static final String INTENT_DATA_RESP = "intentDataResponse"; + private static final String INTENTS = "intents"; + + private static final String APP_ID = "appId"; + private static final String KEY = "key"; + private static final String TYPE = "type"; + private static final String PRIORITY = "priority"; + private static final String STATE = "state"; + private static final String RESOURCES = "resources"; + private static final String DETAILS = "details"; + + private static final String[] COL_IDS = { + APP_ID, KEY, TYPE, PRIORITY, STATE, RESOURCES, DETAILS + }; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new IntentDataRequest()); + } + + // handler for intent table requests + private final class IntentDataRequest extends TableRequestHandler { + private IntentDataRequest() { + super(INTENT_DATA_REQ, INTENT_DATA_RESP, INTENTS); + } + + @Override + protected String defaultColumnId() { + return APP_ID; + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected TableModel createTableModel() { + TableModel tm = super.createTableModel(); + tm.setFormatter(APP_ID, AppIdFormatter.INSTANCE); + tm.setFormatter(RESOURCES, new ResourcesFormatter()); + tm.setFormatter(DETAILS, new DetailsFormatter()); + tm.setFormatter(STATE, EnumFormatter.INSTANCE); + return tm; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + IntentService is = get(IntentService.class); + for (Intent intent : is.getIntents()) { + populateRow(tm.addRow(), intent, is); + } + } + + private void populateRow(TableModel.Row row, Intent intent, IntentService is) { + row.cell(APP_ID, intent.appId()) + .cell(KEY, intent.key()) + .cell(TYPE, intent.getClass().getSimpleName()) + .cell(PRIORITY, intent.priority()) + .cell(STATE, is.getIntentState(intent.key())) + .cell(RESOURCES, intent) + .cell(DETAILS, intent); + } + + private final class ResourcesFormatter implements CellFormatter { + private static final String COMMA = ", "; + + @Override + public String format(Object value) { + Intent intent = (Intent) value; + Collection<NetworkResource> resources = intent.resources(); + if (resources.isEmpty()) { + return "(No resources for this intent)"; + } + StringBuilder sb = new StringBuilder("Resources: "); + for (NetworkResource nr : resources) { + sb.append(nr).append(COMMA); + } + removeTrailingComma(sb); + + return sb.toString(); + } + + private StringBuilder removeTrailingComma(StringBuilder sb) { + int pos = sb.lastIndexOf(COMMA); + sb.delete(pos, sb.length()); + return sb; + } + } + + private final class DetailsFormatter implements CellFormatter { + @Override + public String format(Object value) { + return formatDetails((Intent) value, new StringBuilder()).toString(); + } + + private StringBuilder formatDetails(Intent intent, StringBuilder sb) { + if (intent instanceof ConnectivityIntent) { + buildConnectivityDetails((ConnectivityIntent) intent, sb); + } + + if (intent instanceof HostToHostIntent) { + buildHostToHostDetails((HostToHostIntent) intent, sb); + + } else if (intent instanceof PointToPointIntent) { + buildPointToPointDetails((PointToPointIntent) intent, sb); + + } else if (intent instanceof MultiPointToSinglePointIntent) { + buildMPToSPDetails((MultiPointToSinglePointIntent) intent, sb); + + } else if (intent instanceof SinglePointToMultiPointIntent) { + buildSPToMPDetails((SinglePointToMultiPointIntent) intent, sb); + + } else if (intent instanceof PathIntent) { + buildPathDetails((PathIntent) intent, sb); + + } else if (intent instanceof LinkCollectionIntent) { + buildLinkConnectionDetails((LinkCollectionIntent) intent, sb); + } + + if (sb.length() == 0) { + sb.append("(No details for this intent)"); + } else { + sb.insert(0, "Details: "); + } + return sb; + } + + private void appendMultiPointsDetails(Set<ConnectPoint> points, + StringBuilder sb) { + for (ConnectPoint point : points) { + sb.append(point.elementId()) + .append('/') + .append(point.port()) + .append(' '); + } + } + + private void buildConnectivityDetails(ConnectivityIntent intent, + StringBuilder sb) { + Set<Criterion> criteria = intent.selector().criteria(); + List<Instruction> instructions = intent.treatment().allInstructions(); + List<Constraint> constraints = intent.constraints(); + + if (!criteria.isEmpty()) { + sb.append("Selector: ").append(criteria); + } + if (!instructions.isEmpty()) { + sb.append("Treatment: ").append(instructions); + } + if (constraints != null && !constraints.isEmpty()) { + sb.append("Constraints: ").append(constraints); + } + } + + private void buildHostToHostDetails(HostToHostIntent intent, + StringBuilder sb) { + sb.append(" Host 1: ") + .append(intent.one()) + .append(", Host 2: ") + .append(intent.two()); + } + + private void buildPointToPointDetails(PointToPointIntent intent, + StringBuilder sb) { + ConnectPoint ingress = intent.ingressPoint(); + ConnectPoint egress = intent.egressPoint(); + sb.append(" Ingress: ") + .append(ingress.elementId()) + .append('/') + .append(ingress.port()) + + .append(", Egress: ") + .append(egress.elementId()) + .append('/') + .append(egress.port()) + .append(' '); + } + + private void buildMPToSPDetails(MultiPointToSinglePointIntent intent, + StringBuilder sb) { + ConnectPoint egress = intent.egressPoint(); + + sb.append(" Ingress="); + appendMultiPointsDetails(intent.ingressPoints(), sb); + + sb.append(", Egress=") + .append(egress.elementId()) + .append('/') + .append(egress.port()) + .append(' '); + } + + private void buildSPToMPDetails(SinglePointToMultiPointIntent intent, + StringBuilder sb) { + ConnectPoint ingress = intent.ingressPoint(); + + sb.append(" Ingress=") + .append(ingress.elementId()) + .append('/') + .append(ingress.port()) + .append(", Egress="); + + appendMultiPointsDetails(intent.egressPoints(), sb); + } + + private void buildPathDetails(PathIntent intent, StringBuilder sb) { + sb.append(" path=") + .append(intent.path().links()) + .append(", cost=") + .append(intent.path().cost()); + } + + private void buildLinkConnectionDetails(LinkCollectionIntent intent, + StringBuilder sb) { + sb.append(" links=") + .append(intent.links()) + .append(", egress="); + + appendMultiPointsDetails(intent.egressPoints(), sb); + } + + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java new file mode 100644 index 00000000..7de6d624 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/LinkViewMessageHandler.java @@ -0,0 +1,127 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; +import org.onosproject.net.Link; +import org.onosproject.net.link.LinkService; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.topo.BaseLink; +import org.onosproject.ui.topo.BaseLinkMap; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; +import org.onosproject.ui.table.cell.ConnectPointFormatter; +import org.onosproject.ui.table.cell.EnumFormatter; + +import java.util.Collection; + +/** + * Message handler for link view related messages. + */ +public class LinkViewMessageHandler extends UiMessageHandler { + + private static final String A_BOTH_B = "A ↔ B"; + private static final String A_SINGLE_B = "A → B"; + private static final String SLASH = " / "; + + private static final String LINK_DATA_REQ = "linkDataRequest"; + private static final String LINK_DATA_RESP = "linkDataResponse"; + private static final String LINKS = "links"; + + private static final String ONE = "one"; + private static final String TWO = "two"; + private static final String TYPE = "type"; + private static final String STATE = "_iconid_state"; + private static final String DIRECTION = "direction"; + private static final String DURABLE = "durable"; + + private static final String[] COL_IDS = { + ONE, TWO, TYPE, STATE, DIRECTION, DURABLE + }; + + private static final String ICON_ID_ONLINE = "active"; + private static final String ICON_ID_OFFLINE = "inactive"; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new LinkDataRequest()); + } + + // handler for link table requests + private final class LinkDataRequest extends TableRequestHandler { + private LinkDataRequest() { + super(LINK_DATA_REQ, LINK_DATA_RESP, LINKS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected String defaultColumnId() { + return ONE; + } + + @Override + protected TableModel createTableModel() { + TableModel tm = super.createTableModel(); + tm.setFormatter(ONE, ConnectPointFormatter.INSTANCE); + tm.setFormatter(TWO, ConnectPointFormatter.INSTANCE); + tm.setFormatter(TYPE, EnumFormatter.INSTANCE); + return tm; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + LinkService ls = get(LinkService.class); + BaseLinkMap linkMap = new BaseLinkMap(); + ls.getLinks().forEach(linkMap::add); + linkMap.biLinks().forEach(blink -> populateRow(tm.addRow(), blink)); + } + + private void populateRow(TableModel.Row row, BaseLink blink) { + row.cell(ONE, blink.one().src()) + .cell(TWO, blink.one().dst()) + .cell(TYPE, linkType(blink)) + .cell(STATE, linkState(blink)) + .cell(DIRECTION, linkDir(blink)) + .cell(DURABLE, blink.one().isDurable()); + } + + private String linkType(BaseLink link) { + StringBuilder sb = new StringBuilder(); + sb.append(link.one().type()); + if (link.two() != null && link.two().type() != link.one().type()) { + sb.append(SLASH).append(link.two().type()); + } + return sb.toString(); + } + + private String linkState(BaseLink link) { + return (link.one().state() == Link.State.ACTIVE || + link.two().state() == Link.State.ACTIVE) ? + ICON_ID_ONLINE : ICON_ID_OFFLINE; + } + + private String linkDir(BaseLink link) { + return link.two() != null ? A_BOTH_B : A_SINGLE_B; + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/LogoutResource.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/LogoutResource.java new file mode 100644 index 00000000..b21b5cdb --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/LogoutResource.java @@ -0,0 +1,46 @@ +/* + * 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.ui.impl; + +import org.onlab.rest.BaseResource; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Application upload resource. + */ +@Path("logout") +public class LogoutResource extends BaseResource { + + @Context + private HttpServletRequest servletRequest; + + @GET + public Response logout() throws IOException, URISyntaxException { + servletRequest.getSession().invalidate(); + String url = servletRequest.getRequestURL().toString(); + url = url.replaceFirst("/onos/ui/.*", "/onos/ui/login.html"); + return Response.temporaryRedirect(new URI(url)).build(); + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java new file mode 100644 index 00000000..a30c0470 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java @@ -0,0 +1,102 @@ +/* + * 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.ui.impl; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import org.onlab.osgi.ServiceNotFoundException; +import org.onosproject.rest.AbstractInjectionResource; +import org.onosproject.ui.UiExtensionService; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +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.io.SequenceInputStream; + +import static com.google.common.collect.ImmutableList.of; +import static com.google.common.io.ByteStreams.toByteArray; + +/** + * Resource for serving the dynamically composed index.html. + */ +@Path("/") +public class MainIndexResource extends AbstractInjectionResource { + + private static final String INDEX = "index.html"; + private static final String NOT_READY = "not-ready.html"; + + private static final String INJECT_CSS_START = "<!-- {INJECTED-STYLESHEETS-START} -->"; + private static final String INJECT_CSS_END = "<!-- {INJECTED-STYLESHEETS-END} -->"; + + private static final String INJECT_JS_START = "<!-- {INJECTED-JAVASCRIPT-START} -->"; + private static final String INJECT_JS_END = "<!-- {INJECTED-JAVASCRIPT-END} -->"; + + @GET + @Produces(MediaType.TEXT_HTML) + public Response getMainIndex() throws IOException { + ClassLoader classLoader = getClass().getClassLoader(); + UiExtensionService service; + try { + service = get(UiExtensionService.class); + } catch (ServiceNotFoundException e) { + return Response.ok(classLoader.getResourceAsStream(NOT_READY)).build(); + } + + InputStream indexTemplate = classLoader.getResourceAsStream(INDEX); + String index = new String(toByteArray(indexTemplate)); + + int p1s = split(index, 0, INJECT_JS_START); + int p1e = split(index, p1s, INJECT_JS_END); + int p2s = split(index, p1e, INJECT_CSS_START); + int p2e = split(index, p2s, INJECT_CSS_END); + int p3s = split(index, p2e, null); + + StreamEnumeration streams = + new StreamEnumeration(of(stream(index, 0, p1s), + includeJs(service), + stream(index, p1e, p2s), + includeCss(service), + stream(index, p2e, p3s))); + + return Response.ok(new SequenceInputStream(streams)).build(); + } + + // Produces an input stream including CSS injections from all extensions. + private InputStream includeCss(UiExtensionService service) { + Builder<InputStream> builder = ImmutableList.builder(); + service.getExtensions().forEach(ext -> add(builder, ext.css())); + return new SequenceInputStream(new StreamEnumeration(builder.build())); + } + + // Produces an input stream including JS injections from all extensions. + private InputStream includeJs(UiExtensionService service) { + Builder<InputStream> builder = ImmutableList.builder(); + service.getExtensions().forEach(ext -> add(builder, ext.js())); + return new SequenceInputStream(new StreamEnumeration(builder.build())); + } + + // Safely adds the stream to the list builder only if stream is not null. + private void add(Builder<InputStream> builder, InputStream inputStream) { + if (inputStream != null) { + builder.add(inputStream); + } + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainModuleResource.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainModuleResource.java new file mode 100644 index 00000000..42d1dff9 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainModuleResource.java @@ -0,0 +1,77 @@ +/* + * 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.ui.impl; + +import org.onosproject.rest.AbstractInjectionResource; +import org.onosproject.ui.UiExtension; +import org.onosproject.ui.UiExtensionService; +import org.onosproject.ui.UiView; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; + +import static com.google.common.collect.ImmutableList.of; +import static com.google.common.io.ByteStreams.toByteArray; +import static org.onosproject.ui.impl.MainViewResource.SCRIPT; + +/** + * Resource for serving the dynamically composed onos.js. + */ +@Path("/") +public class MainModuleResource extends AbstractInjectionResource { + + private static final String MAIN_JS = "onos.js"; + + private static final String INJECT_VIEW_IDS_START = "// {INJECTED-VIEW-IDS-START}"; + private static final String INJECT_VIEW_IDS_END = "// {INJECTED-VIEW-IDS-END}"; + + @GET + @Produces(SCRIPT) + public Response getMainModule() throws IOException { + UiExtensionService service = get(UiExtensionService.class); + InputStream jsTemplate = getClass().getClassLoader().getResourceAsStream(MAIN_JS); + String js = new String(toByteArray(jsTemplate)); + + int p1s = split(js, 0, INJECT_VIEW_IDS_START); + int p1e = split(js, 0, INJECT_VIEW_IDS_END); + int p2s = split(js, p1e, null); + + StreamEnumeration streams = + new StreamEnumeration(of(stream(js, 0, p1s), + includeViewIds(service), + stream(js, p1e, p2s))); + + return Response.ok(new SequenceInputStream(streams)).build(); + } + + // Produces an input stream including view id injections from all extensions. + private InputStream includeViewIds(UiExtensionService service) { + StringBuilder sb = new StringBuilder("\n"); + for (UiExtension extension : service.getExtensions()) { + for (UiView view : extension.views()) { + sb.append(" '").append(view.id()).append("',"); + } + } + return new ByteArrayInputStream(sb.toString().getBytes()); + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainNavResource.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainNavResource.java new file mode 100644 index 00000000..7f0af3a5 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainNavResource.java @@ -0,0 +1,124 @@ +/* + * 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.ui.impl; + +import org.onosproject.rest.AbstractInjectionResource; +import org.onosproject.ui.UiExtension; +import org.onosproject.ui.UiExtensionService; +import org.onosproject.ui.UiView; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static com.google.common.collect.ImmutableList.of; +import static com.google.common.io.ByteStreams.toByteArray; + +/** + * Resource for serving the dynamically composed nav.html. + */ +@Path("/") +public class MainNavResource extends AbstractInjectionResource { + + private static final String NAV_HTML = "nav.html"; + + private static final String INJECT_VIEW_ITEMS_START = + "<!-- {INJECTED-VIEW-NAV-START} -->"; + private static final String INJECT_VIEW_ITEMS_END = + "<!-- {INJECTED-VIEW-NAV-END} -->"; + + private static final String HDR_FORMAT = + "<div class=\"nav-hdr\">%s</div>\n"; + private static final String NAV_FORMAT = + "<a ng-click=\"navCtrl.hideNav()\" href=\"#/%s\">%s %s</a>\n"; + + private static final String BLANK_GLYPH = "unknown"; + + @GET + @Produces(MediaType.TEXT_HTML) + public Response getNavigation() throws IOException { + UiExtensionService service = get(UiExtensionService.class); + InputStream navTemplate = + getClass().getClassLoader().getResourceAsStream(NAV_HTML); + String html = new String(toByteArray(navTemplate)); + + int p1s = split(html, 0, INJECT_VIEW_ITEMS_START); + int p1e = split(html, 0, INJECT_VIEW_ITEMS_END); + int p2s = split(html, p1e, null); + + StreamEnumeration streams = + new StreamEnumeration(of(stream(html, 0, p1s), + includeNavItems(service), + stream(html, p1e, p2s))); + + return Response.ok(new SequenceInputStream(streams)).build(); + } + + // Produces an input stream of nav item injections from all extensions. + private InputStream includeNavItems(UiExtensionService service) { + List<UiExtension> extensions = service.getExtensions(); + StringBuilder sb = new StringBuilder("\n"); + + for (UiView.Category cat : UiView.Category.values()) { + if (cat == UiView.Category.HIDDEN) { + continue; + } + + List<UiView> catViews = getViewsForCat(extensions, cat); + if (!catViews.isEmpty()) { + addCatHeader(sb, cat); + addCatItems(sb, catViews); + } + } + + return new ByteArrayInputStream(sb.toString().getBytes()); + } + + private List<UiView> getViewsForCat(List<UiExtension> extensions, + UiView.Category cat) { + List<UiView> views = new ArrayList<>(); + for (UiExtension extension : extensions) { + views.addAll(extension.views().stream().filter( + view -> cat.equals(view.category()) + ).collect(Collectors.toList())); + } + return views; + } + + private void addCatHeader(StringBuilder sb, UiView.Category cat) { + sb.append(String.format(HDR_FORMAT, cat.label())); + } + + private void addCatItems(StringBuilder sb, List<UiView> catViews) { + for (UiView view : catViews) { + sb.append(String.format(NAV_FORMAT, view.id(), icon(view), view.label())); + } + } + + private String icon(UiView view) { + String gid = view.iconId() == null ? BLANK_GLYPH : view.iconId(); + return "<div icon icon-id=\"" + gid + "\"></div>"; + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainViewResource.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainViewResource.java new file mode 100644 index 00000000..130a7eab --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/MainViewResource.java @@ -0,0 +1,60 @@ +/* + * 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.ui.impl; + +import org.onosproject.rest.AbstractInjectionResource; +import org.onosproject.ui.UiExtension; +import org.onosproject.ui.UiExtensionService; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; +import java.io.IOException; + +import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM; +import static javax.ws.rs.core.MediaType.TEXT_HTML; + +/** + * Resource for serving the dynamically composed onos.js. + */ +@Path("/") +public class MainViewResource extends AbstractInjectionResource { + + static final String CONTENT_TYPE = "Content-Type"; + static final String STYLESHEET = "text/css"; + static final String SCRIPT = "text/javascript"; + + @Path("{view}/{resource}") + @GET + public Response getViewResource(@PathParam("view") String viewId, + @PathParam("resource") String resource) throws IOException { + UiExtensionService service = get(UiExtensionService.class); + UiExtension extension = service.getViewExtension(viewId); + return extension != null ? + Response.ok(extension.resource(viewId, resource)) + .header(CONTENT_TYPE, contentType(resource)).build() : + Response.status(Response.Status.NOT_FOUND).build(); + } + + static String contentType(String resource) { + return resource.endsWith(".html") ? TEXT_HTML : + resource.endsWith(".css") ? STYLESHEET : + resource.endsWith(".js") ? SCRIPT : + APPLICATION_OCTET_STREAM; + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java new file mode 100644 index 00000000..92ebcf75 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/PortViewMessageHandler.java @@ -0,0 +1,96 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import org.onosproject.net.DeviceId; +import org.onosproject.net.device.DeviceService; +import org.onosproject.net.device.PortStatistics; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; + +import java.util.Collection; + + +/** + * Message handler for port view related messages. + */ +public class PortViewMessageHandler extends UiMessageHandler { + + private static final String PORT_DATA_REQ = "portDataRequest"; + private static final String PORT_DATA_RESP = "portDataResponse"; + private static final String PORTS = "ports"; + + private static final String ID = "id"; + private static final String PKT_RX = "pkt_rx"; + private static final String PKT_TX = "pkt_tx"; + private static final String BYTES_RX = "bytes_rx"; + private static final String BYTES_TX = "bytes_tx"; + private static final String PKT_RX_DRP = "pkt_rx_drp"; + private static final String PKT_TX_DRP = "pkt_tx_drp"; + private static final String DURATION = "duration"; + + private static final String[] COL_IDS = { + ID, PKT_RX, PKT_TX, BYTES_RX, BYTES_TX, + PKT_RX_DRP, PKT_TX_DRP, DURATION + }; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new PortDataRequest()); + } + + // handler for port table requests + private final class PortDataRequest extends TableRequestHandler { + + private PortDataRequest() { + super(PORT_DATA_REQ, PORT_DATA_RESP, PORTS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + String uri = string(payload, "devId"); + if (!Strings.isNullOrEmpty(uri)) { + DeviceId deviceId = DeviceId.deviceId(uri); + DeviceService ds = get(DeviceService.class); + for (PortStatistics stat : ds.getPortStatistics(deviceId)) { + populateRow(tm.addRow(), stat); + } + } + } + + private void populateRow(TableModel.Row row, PortStatistics stat) { + row.cell(ID, stat.port()) + .cell(PKT_RX, stat.packetsReceived()) + .cell(PKT_TX, stat.packetsSent()) + .cell(BYTES_RX, stat.bytesReceived()) + .cell(BYTES_TX, stat.bytesSent()) + .cell(PKT_RX_DRP, stat.packetsRxDropped()) + .cell(PKT_TX_DRP, stat.packetsTxDropped()) + .cell(DURATION, stat.durationSec()); + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/SettingsViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/SettingsViewMessageHandler.java new file mode 100644 index 00000000..ed413fab --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/SettingsViewMessageHandler.java @@ -0,0 +1,95 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; +import org.onosproject.cfg.ComponentConfigService; +import org.onosproject.cfg.ConfigProperty; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; + +import java.util.Collection; + +/** + * Message handler for component configuration view related messages. + */ +public class SettingsViewMessageHandler extends UiMessageHandler { + + private static final String DATA_REQUEST = "settingDataRequest"; + private static final String DATA_RESPONSE = "settingDataResponse"; + private static final String SETTINGS = "settings"; + + private static final String COMPONENT = "component"; + private static final String ID = "id"; + private static final String TYPE = "type"; + private static final String VALUE = "value"; + private static final String DEFAULT = "defValue"; + private static final String DESC = "desc"; + + private static final String[] COL_IDS = { + COMPONENT, ID, TYPE, VALUE, DEFAULT, DESC + }; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new SettingsRequest()); + } + + // handler for host table requests + private final class SettingsRequest extends TableRequestHandler { + private SettingsRequest() { + super(DATA_REQUEST, DATA_RESPONSE, SETTINGS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected String defaultColumnId() { + return COMPONENT; + } + + @Override + protected TableModel createTableModel() { + TableModel tm = super.createTableModel(); + return tm; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + ComponentConfigService ccs = get(ComponentConfigService.class); + for (String component : ccs.getComponentNames()) { + for (ConfigProperty prop : ccs.getProperties(component)) { + populateRow(tm.addRow(), component, prop); + } + } + } + + private void populateRow(TableModel.Row row, String component, ConfigProperty prop) { + row.cell(COMPONENT, component) + .cell(ID, prop.name()) + .cell(TYPE, prop.type().toString().toLowerCase()) + .cell(VALUE, prop.value()) + .cell(DEFAULT, prop.defaultValue()) + .cell(DESC, prop.description()); + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/SpriteService.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/SpriteService.java new file mode 100644 index 00000000..7312329c --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/SpriteService.java @@ -0,0 +1,50 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Set; + +/** + * Provisional service to keep track of the topology view sprite definitions. + */ +public interface SpriteService { + + /** + * Returns set of registered sprite definition names. + * + * @return set of sprite definition names + */ + Set<String> getNames(); + + /** + * Registers sprite data under the specified name. + * + * @param name sprite definition name + * @param spriteData sprite data + */ + void put(String name, JsonNode spriteData); + + /** + * Returns the sprite definition registered under the given name. + * + * @param name sprite definition name + * @return sprite data + */ + JsonNode get(String name); + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopoOverlayCache.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopoOverlayCache.java new file mode 100644 index 00000000..99557c2d --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopoOverlayCache.java @@ -0,0 +1,116 @@ +/* + * 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.ui.impl; + +import org.onosproject.ui.UiTopoOverlay; + +import java.util.HashMap; +import java.util.Map; + +import static com.google.common.base.Strings.isNullOrEmpty; + +/** + * A cache of {@link org.onosproject.ui.UiTopoOverlay}'s that were registered + * at the time the UI connection was established. + */ +public class TopoOverlayCache { + + private static final String EMPTY = ""; + private static final UiTopoOverlay NONE = new NullOverlay(); + + private final Map<String, UiTopoOverlay> overlays = new HashMap<>(); + private UiTopoOverlay current = NONE; + + public TopoOverlayCache() { + overlays.put(null, NONE); + } + + /** + * Adds a topology overlay to the cache. + * + * @param overlay a topology overlay + */ + public void add(UiTopoOverlay overlay) { + overlays.put(overlay.id(), overlay); + } + + /** + * Invoked when the cache is no longer needed. + */ + public void destroy() { + overlays.clear(); + } + + /** + * Switching currently selected overlay. + * + * @param deact identity of overlay to deactivate + * @param act identity of overlay to activate + */ + public void switchOverlay(String deact, String act) { + UiTopoOverlay toDeactivate = getOverlay(deact); + UiTopoOverlay toActivate = getOverlay(act); + + toDeactivate.deactivate(); + current = toActivate; + current.activate(); + } + + private UiTopoOverlay getOverlay(String id) { + return isNullOrEmpty(id) ? NONE : overlays.get(id); + } + + /** + * Returns the current overlay instance. + * Note that this method always returns a reference; when there is no + * overlay selected the "NULL" overlay instance is returned. + * + * @return the current overlay + */ + public UiTopoOverlay currentOverlay() { + return current; + } + + /** + * Returns the number of overlays in the cache. Remember that this + * includes the "NULL" overlay, representing "no overlay selected". + * + * @return number of overlays + */ + public int size() { + return overlays.size(); + } + + /** + * Returns true if the identifier of the currently active overlay + * matches the given parameter. + * + * @param overlayId overlay identifier + * @return true if this matches the ID of currently active overlay + */ + public boolean isActive(String overlayId) { + return currentOverlay().id().equals(overlayId); + } + + // overlay instance representing "no overlay selected" + private static class NullOverlay extends UiTopoOverlay { + public NullOverlay() { + super(EMPTY); + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java new file mode 100644 index 00000000..30960911 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java @@ -0,0 +1,94 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.onlab.rest.BaseResource; +import org.slf4j.Logger; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * Topology viewer resource. + */ +@Path("topology") +public class TopologyResource extends BaseResource { + + private static final Logger log = getLogger(TopologyResource.class); + + private final ObjectMapper mapper = new ObjectMapper(); + + @Path("geoloc") + @GET + @Produces("application/json") + public Response getGeoLocations() { + ObjectNode rootNode = mapper.createObjectNode(); + ArrayNode devices = mapper.createArrayNode(); + ArrayNode hosts = mapper.createArrayNode(); + + Map<String, ObjectNode> metaUi = TopologyViewMessageHandler.getMetaUi(); + for (String id : metaUi.keySet()) { + ObjectNode memento = metaUi.get(id); + if (id.length() > 17 && id.charAt(17) == '/') { + addGeoData(hosts, "id", id, memento); + } else { + addGeoData(devices, "uri", id, memento); + } + } + + rootNode.set("devices", devices); + rootNode.set("hosts", hosts); + return Response.ok(rootNode.toString()).build(); + } + + private void addGeoData(ArrayNode array, String idField, String id, + ObjectNode memento) { + ObjectNode node = mapper.createObjectNode().put(idField, id); + ObjectNode annot = mapper.createObjectNode(); + node.set("annotations", annot); + try { + annot.put("latitude", memento.get("lat").asDouble()) + .put("longitude", memento.get("lng").asDouble()); + array.add(node); + } catch (Exception e) { + log.debug("Skipping geo entry"); + } + } + + @Path("sprites") + @POST + @Consumes("application/json") + public Response setSprites(InputStream stream) throws IOException { + JsonNode root = mapper.readTree(stream); + String name = root.path("defn_name").asText("sprites"); + get(SpriteService.class).put(name, root); + return Response.ok().build(); + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java new file mode 100644 index 00000000..8acdc2cf --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandler.java @@ -0,0 +1,781 @@ +/* + * 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.ui.impl; + +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.ImmutableSet; +import org.onlab.osgi.ServiceDirectory; +import org.onlab.util.AbstractAccumulator; +import org.onlab.util.Accumulator; +import org.onosproject.cluster.ClusterEvent; +import org.onosproject.cluster.ClusterEventListener; +import org.onosproject.cluster.ControllerNode; +import org.onosproject.core.ApplicationId; +import org.onosproject.core.CoreService; +import org.onosproject.event.Event; +import org.onosproject.mastership.MastershipAdminService; +import org.onosproject.mastership.MastershipEvent; +import org.onosproject.mastership.MastershipListener; +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.Device; +import org.onosproject.net.Host; +import org.onosproject.net.HostId; +import org.onosproject.net.HostLocation; +import org.onosproject.net.Link; +import org.onosproject.net.device.DeviceEvent; +import org.onosproject.net.device.DeviceListener; +import org.onosproject.net.flow.DefaultTrafficSelector; +import org.onosproject.net.flow.DefaultTrafficTreatment; +import org.onosproject.net.flow.FlowRuleEvent; +import org.onosproject.net.flow.FlowRuleListener; +import org.onosproject.net.flow.TrafficSelector; +import org.onosproject.net.flow.TrafficTreatment; +import org.onosproject.net.host.HostEvent; +import org.onosproject.net.host.HostListener; +import org.onosproject.net.intent.HostToHostIntent; +import org.onosproject.net.intent.IntentEvent; +import org.onosproject.net.intent.IntentListener; +import org.onosproject.net.intent.MultiPointToSinglePointIntent; +import org.onosproject.net.link.LinkEvent; +import org.onosproject.net.link.LinkListener; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiConnection; +import org.onosproject.ui.impl.TrafficMonitor.Mode; +import org.onosproject.ui.topo.Highlights; +import org.onosproject.ui.topo.NodeSelection; +import org.onosproject.ui.topo.PropertyPanel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ExecutorService; + +import static java.util.concurrent.Executors.newSingleThreadExecutor; +import static org.onlab.util.Tools.groupedThreads; +import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED; +import static org.onosproject.net.DeviceId.deviceId; +import static org.onosproject.net.HostId.hostId; +import static org.onosproject.net.device.DeviceEvent.Type.*; +import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED; +import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED; +import static org.onosproject.ui.JsonUtils.envelope; +import static org.onosproject.ui.topo.TopoJson.highlightsMessage; +import static org.onosproject.ui.topo.TopoJson.json; + +/** + * Web socket capable of interacting with the GUI topology view. + */ +public class TopologyViewMessageHandler extends TopologyViewMessageHandlerBase { + + // incoming event types + private static final String REQ_DETAILS = "requestDetails"; + private static final String UPDATE_META = "updateMeta"; + private static final String ADD_HOST_INTENT = "addHostIntent"; + private static final String ADD_MULTI_SRC_INTENT = "addMultiSourceIntent"; + private static final String REQ_RELATED_INTENTS = "requestRelatedIntents"; + private static final String REQ_NEXT_INTENT = "requestNextRelatedIntent"; + private static final String REQ_PREV_INTENT = "requestPrevRelatedIntent"; + private static final String REQ_SEL_INTENT_TRAFFIC = "requestSelectedIntentTraffic"; + private static final String REQ_ALL_FLOW_TRAFFIC = "requestAllFlowTraffic"; + private static final String REQ_ALL_PORT_TRAFFIC = "requestAllPortTraffic"; + private static final String REQ_DEV_LINK_FLOWS = "requestDeviceLinkFlows"; + private static final String CANCEL_TRAFFIC = "cancelTraffic"; + private static final String REQ_SUMMARY = "requestSummary"; + private static final String CANCEL_SUMMARY = "cancelSummary"; + private static final String EQ_MASTERS = "equalizeMasters"; + private static final String SPRITE_LIST_REQ = "spriteListRequest"; + private static final String SPRITE_DATA_REQ = "spriteDataRequest"; + private static final String TOPO_START = "topoStart"; + private static final String TOPO_HEARTBEAT = "topoHeartbeat"; + private static final String TOPO_SELECT_OVERLAY = "topoSelectOverlay"; + private static final String TOPO_STOP = "topoStop"; + + // outgoing event types + private static final String SHOW_SUMMARY = "showSummary"; + private static final String SHOW_DETAILS = "showDetails"; + private static final String SPRITE_LIST_RESPONSE = "spriteListResponse"; + private static final String SPRITE_DATA_RESPONSE = "spriteDataResponse"; + private static final String UPDATE_INSTANCE = "updateInstance"; + + // fields + private static final String ID = "id"; + private static final String DEVICE = "device"; + private static final String HOST = "host"; + private static final String CLASS = "class"; + private static final String UNKNOWN = "unknown"; + private static final String ONE = "one"; + private static final String TWO = "two"; + private static final String SRC = "src"; + private static final String DST = "dst"; + private static final String DATA = "data"; + private static final String NAME = "name"; + private static final String NAMES = "names"; + private static final String ACTIVATE = "activate"; + private static final String DEACTIVATE = "deactivate"; + + + private static final String APP_ID = "org.onosproject.gui"; + + private static final long TRAFFIC_PERIOD = 5000; + private static final long SUMMARY_PERIOD = 30000; + + private static final Comparator<? super ControllerNode> NODE_COMPARATOR = + (o1, o2) -> o1.id().toString().compareTo(o2.id().toString()); + + + private final Timer timer = new Timer("onos-topology-view"); + + private static final int MAX_EVENTS = 1000; + private static final int MAX_BATCH_MS = 5000; + private static final int MAX_IDLE_MS = 1000; + + private ApplicationId appId; + + private final ClusterEventListener clusterListener = new InternalClusterListener(); + private final MastershipListener mastershipListener = new InternalMastershipListener(); + private final DeviceListener deviceListener = new InternalDeviceListener(); + private final LinkListener linkListener = new InternalLinkListener(); + private final HostListener hostListener = new InternalHostListener(); + private final IntentListener intentListener = new InternalIntentListener(); + private final FlowRuleListener flowListener = new InternalFlowListener(); + + private final Accumulator<Event> eventAccummulator = new InternalEventAccummulator(); + private final ExecutorService msgSender = + newSingleThreadExecutor(groupedThreads("onos/gui", "msg-sender")); + + private TopoOverlayCache overlayCache; + private TrafficMonitor traffic; + + private TimerTask summaryTask = null; + private boolean summaryRunning = false; + + private boolean listenersRemoved = false; + + + @Override + public void init(UiConnection connection, ServiceDirectory directory) { + super.init(connection, directory); + appId = directory.get(CoreService.class).registerApplication(APP_ID); + traffic = new TrafficMonitor(TRAFFIC_PERIOD, servicesBundle, this); + } + + @Override + public void destroy() { + cancelAllRequests(); + removeListeners(); + super.destroy(); + } + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of( + new TopoStart(), + new TopoHeartbeat(), + new TopoSelectOverlay(), + new TopoStop(), + new ReqSummary(), + new CancelSummary(), + new SpriteListReq(), + new SpriteDataReq(), + new RequestDetails(), + new UpdateMeta(), + new EqMasters(), + + // TODO: migrate traffic related to separate app + new AddHostIntent(), + new AddMultiSourceIntent(), + + new ReqAllFlowTraffic(), + new ReqAllPortTraffic(), + new ReqDevLinkFlows(), + new ReqRelatedIntents(), + new ReqNextIntent(), + new ReqPrevIntent(), + new ReqSelectedIntentTraffic(), + + new CancelTraffic() + ); + } + + /** + * Injects the topology overlay cache. + * + * @param overlayCache injected cache + */ + void setOverlayCache(TopoOverlayCache overlayCache) { + this.overlayCache = overlayCache; + } + + // ================================================================== + + private final class TopoStart extends RequestHandler { + private TopoStart() { + super(TOPO_START); + } + + @Override + public void process(long sid, ObjectNode payload) { + addListeners(); + sendAllInstances(null); + sendAllDevices(); + sendAllLinks(); + sendAllHosts(); + } + } + + private final class TopoHeartbeat extends RequestHandler { + private TopoHeartbeat() { + super(TOPO_HEARTBEAT); + } + + @Override + public void process(long sid, ObjectNode payload) { + // place holder for now + } + } + + private final class TopoSelectOverlay extends RequestHandler { + private TopoSelectOverlay() { + super(TOPO_SELECT_OVERLAY); + } + + @Override + public void process(long sid, ObjectNode payload) { + String deact = string(payload, DEACTIVATE); + String act = string(payload, ACTIVATE); + overlayCache.switchOverlay(deact, act); + } + } + + private final class TopoStop extends RequestHandler { + private TopoStop() { + super(TOPO_STOP); + } + + @Override + public void process(long sid, ObjectNode payload) { + stopSummaryMonitoring(); + traffic.stopMonitoring(); + } + } + + private final class ReqSummary extends RequestHandler { + private ReqSummary() { + super(REQ_SUMMARY); + } + + @Override + public void process(long sid, ObjectNode payload) { + requestSummary(sid); + startSummaryMonitoring(); + } + } + + private final class CancelSummary extends RequestHandler { + private CancelSummary() { + super(CANCEL_SUMMARY); + } + + @Override + public void process(long sid, ObjectNode payload) { + stopSummaryMonitoring(); + } + } + + private final class SpriteListReq extends RequestHandler { + private SpriteListReq() { + super(SPRITE_LIST_REQ); + } + + @Override + public void process(long sid, ObjectNode payload) { + ObjectNode root = objectNode(); + ArrayNode names = arrayNode(); + get(SpriteService.class).getNames().forEach(names::add); + root.set(NAMES, names); + sendMessage(SPRITE_LIST_RESPONSE, sid, root); + } + } + + private final class SpriteDataReq extends RequestHandler { + private SpriteDataReq() { + super(SPRITE_DATA_REQ); + } + + @Override + public void process(long sid, ObjectNode payload) { + String name = string(payload, NAME); + ObjectNode root = objectNode(); + root.set(DATA, get(SpriteService.class).get(name)); + sendMessage(SPRITE_DATA_RESPONSE, sid, root); + } + } + + private final class RequestDetails extends RequestHandler { + private RequestDetails() { + super(REQ_DETAILS); + } + + @Override + public void process(long sid, ObjectNode payload) { + String type = string(payload, CLASS, UNKNOWN); + String id = string(payload, ID); + PropertyPanel pp = null; + + if (type.equals(DEVICE)) { + pp = deviceDetails(deviceId(id), sid); + overlayCache.currentOverlay().modifyDeviceDetails(pp); + } else if (type.equals(HOST)) { + pp = hostDetails(hostId(id), sid); + overlayCache.currentOverlay().modifyHostDetails(pp); + } + + sendMessage(envelope(SHOW_DETAILS, sid, json(pp))); + } + } + + private final class UpdateMeta extends RequestHandler { + private UpdateMeta() { + super(UPDATE_META); + } + + @Override + public void process(long sid, ObjectNode payload) { + updateMetaUi(payload); + } + } + + private final class EqMasters extends RequestHandler { + private EqMasters() { + super(EQ_MASTERS); + } + + @Override + public void process(long sid, ObjectNode payload) { + directory.get(MastershipAdminService.class).balanceRoles(); + } + } + + + // ========= ----------------------------------------------------------------- + + // === TODO: move traffic related classes to traffic app + + private final class AddHostIntent extends RequestHandler { + private AddHostIntent() { + super(ADD_HOST_INTENT); + } + + @Override + public void process(long sid, ObjectNode payload) { + // TODO: add protection against device ids and non-existent hosts. + HostId one = hostId(string(payload, ONE)); + HostId two = hostId(string(payload, TWO)); + + HostToHostIntent intent = HostToHostIntent.builder() + .appId(appId) + .one(one) + .two(two) + .build(); + + intentService.submit(intent); + if (overlayCache.isActive(TrafficOverlay.TRAFFIC_ID)) { + traffic.monitor(intent); + } + } + } + + private final class AddMultiSourceIntent extends RequestHandler { + private AddMultiSourceIntent() { + super(ADD_MULTI_SRC_INTENT); + } + + @Override + public void process(long sid, ObjectNode payload) { + // TODO: add protection against device ids and non-existent hosts. + Set<HostId> src = getHostIds((ArrayNode) payload.path(SRC)); + HostId dst = hostId(string(payload, DST)); + Host dstHost = hostService.getHost(dst); + + Set<ConnectPoint> ingressPoints = getHostLocations(src); + + // FIXME: clearly, this is not enough + TrafficSelector selector = DefaultTrafficSelector.builder() + .matchEthDst(dstHost.mac()).build(); + TrafficTreatment treatment = DefaultTrafficTreatment.emptyTreatment(); + + MultiPointToSinglePointIntent intent = + MultiPointToSinglePointIntent.builder() + .appId(appId) + .selector(selector) + .treatment(treatment) + .ingressPoints(ingressPoints) + .egressPoint(dstHost.location()) + .build(); + + intentService.submit(intent); + if (overlayCache.isActive(TrafficOverlay.TRAFFIC_ID)) { + traffic.monitor(intent); + } + } + } + + // ========= ----------------------------------------------------------------- + + private final class ReqAllFlowTraffic extends RequestHandler { + private ReqAllFlowTraffic() { + super(REQ_ALL_FLOW_TRAFFIC); + } + + @Override + public void process(long sid, ObjectNode payload) { + traffic.monitor(Mode.ALL_FLOW_TRAFFIC); + } + } + + private final class ReqAllPortTraffic extends RequestHandler { + private ReqAllPortTraffic() { + super(REQ_ALL_PORT_TRAFFIC); + } + + @Override + public void process(long sid, ObjectNode payload) { + traffic.monitor(Mode.ALL_PORT_TRAFFIC); + } + } + + private final class ReqDevLinkFlows extends RequestHandler { + private ReqDevLinkFlows() { + super(REQ_DEV_LINK_FLOWS); + } + + @Override + public void process(long sid, ObjectNode payload) { + NodeSelection nodeSelection = + new NodeSelection(payload, deviceService, hostService); + traffic.monitor(Mode.DEV_LINK_FLOWS, nodeSelection); + } + } + + private final class ReqRelatedIntents extends RequestHandler { + private ReqRelatedIntents() { + super(REQ_RELATED_INTENTS); + } + + @Override + public void process(long sid, ObjectNode payload) { + NodeSelection nodeSelection = + new NodeSelection(payload, deviceService, hostService); + traffic.monitor(Mode.RELATED_INTENTS, nodeSelection); + } + } + + private final class ReqNextIntent extends RequestHandler { + private ReqNextIntent() { + super(REQ_NEXT_INTENT); + } + + @Override + public void process(long sid, ObjectNode payload) { + traffic.selectNextIntent(); + } + } + + private final class ReqPrevIntent extends RequestHandler { + private ReqPrevIntent() { + super(REQ_PREV_INTENT); + } + + @Override + public void process(long sid, ObjectNode payload) { + traffic.selectPreviousIntent(); + } + } + + private final class ReqSelectedIntentTraffic extends RequestHandler { + private ReqSelectedIntentTraffic() { + super(REQ_SEL_INTENT_TRAFFIC); + } + + @Override + public void process(long sid, ObjectNode payload) { + traffic.monitor(Mode.SELECTED_INTENT); + } + } + + private final class CancelTraffic extends RequestHandler { + private CancelTraffic() { + super(CANCEL_TRAFFIC); + } + + @Override + public void process(long sid, ObjectNode payload) { + traffic.stopMonitoring(); + } + } + + //======================================================================= + + // Converts highlights to JSON format and sends the message to the client + protected void sendHighlights(Highlights highlights) { + sendMessage(highlightsMessage(highlights)); + } + + // Subscribes for summary messages. + private synchronized void requestSummary(long sid) { + PropertyPanel pp = summmaryMessage(sid); + overlayCache.currentOverlay().modifySummary(pp); + sendMessage(envelope(SHOW_SUMMARY, sid, json(pp))); + } + + + private void cancelAllRequests() { + stopSummaryMonitoring(); + traffic.stopMonitoring(); + } + + // Sends all controller nodes to the client as node-added messages. + private void sendAllInstances(String messageType) { + List<ControllerNode> nodes = new ArrayList<>(clusterService.getNodes()); + Collections.sort(nodes, NODE_COMPARATOR); + for (ControllerNode node : nodes) { + sendMessage(instanceMessage(new ClusterEvent(INSTANCE_ADDED, node), + messageType)); + } + } + + // Sends all devices to the client as device-added messages. + private void sendAllDevices() { + // Send optical first, others later for layered rendering + for (Device device : deviceService.getDevices()) { + if (device.type() == Device.Type.ROADM) { + sendMessage(deviceMessage(new DeviceEvent(DEVICE_ADDED, device))); + } + } + for (Device device : deviceService.getDevices()) { + if (device.type() != Device.Type.ROADM) { + sendMessage(deviceMessage(new DeviceEvent(DEVICE_ADDED, device))); + } + } + } + + // Sends all links to the client as link-added messages. + private void sendAllLinks() { + // Send optical first, others later for layered rendering + for (Link link : linkService.getLinks()) { + if (link.type() == Link.Type.OPTICAL) { + sendMessage(linkMessage(new LinkEvent(LINK_ADDED, link))); + } + } + for (Link link : linkService.getLinks()) { + if (link.type() != Link.Type.OPTICAL) { + sendMessage(linkMessage(new LinkEvent(LINK_ADDED, link))); + } + } + } + + // Sends all hosts to the client as host-added messages. + private void sendAllHosts() { + for (Host host : hostService.getHosts()) { + sendMessage(hostMessage(new HostEvent(HOST_ADDED, host))); + } + } + + private Set<ConnectPoint> getHostLocations(Set<HostId> hostIds) { + Set<ConnectPoint> points = new HashSet<>(); + for (HostId hostId : hostIds) { + points.add(getHostLocation(hostId)); + } + return points; + } + + private HostLocation getHostLocation(HostId hostId) { + return hostService.getHost(hostId).location(); + } + + // Produces a list of host ids from the specified JSON array. + private Set<HostId> getHostIds(ArrayNode ids) { + Set<HostId> hostIds = new HashSet<>(); + for (JsonNode id : ids) { + hostIds.add(hostId(id.asText())); + } + return hostIds; + } + + + private synchronized void startSummaryMonitoring() { + stopSummaryMonitoring(); + summaryTask = new SummaryMonitor(); + timer.schedule(summaryTask, SUMMARY_PERIOD, SUMMARY_PERIOD); + summaryRunning = true; + } + + private synchronized void stopSummaryMonitoring() { + if (summaryTask != null) { + summaryTask.cancel(); + summaryTask = null; + } + summaryRunning = false; + } + + + // Adds all internal listeners. + private synchronized void addListeners() { + listenersRemoved = false; + clusterService.addListener(clusterListener); + mastershipService.addListener(mastershipListener); + deviceService.addListener(deviceListener); + linkService.addListener(linkListener); + hostService.addListener(hostListener); + intentService.addListener(intentListener); + flowService.addListener(flowListener); + } + + // Removes all internal listeners. + private synchronized void removeListeners() { + if (!listenersRemoved) { + listenersRemoved = true; + clusterService.removeListener(clusterListener); + mastershipService.removeListener(mastershipListener); + deviceService.removeListener(deviceListener); + linkService.removeListener(linkListener); + hostService.removeListener(hostListener); + intentService.removeListener(intentListener); + flowService.removeListener(flowListener); + } + } + + // Cluster event listener. + private class InternalClusterListener implements ClusterEventListener { + @Override + public void event(ClusterEvent event) { + msgSender.execute(() -> sendMessage(instanceMessage(event, null))); + } + } + + // Mastership change listener + private class InternalMastershipListener implements MastershipListener { + @Override + public void event(MastershipEvent event) { + msgSender.execute(() -> { + sendAllInstances(UPDATE_INSTANCE); + Device device = deviceService.getDevice(event.subject()); + if (device != null) { + sendMessage(deviceMessage(new DeviceEvent(DEVICE_UPDATED, device))); + } + }); + } + } + + // Device event listener. + private class InternalDeviceListener implements DeviceListener { + @Override + public void event(DeviceEvent event) { + if (event.type() != PORT_STATS_UPDATED) { + msgSender.execute(() -> sendMessage(deviceMessage(event))); + eventAccummulator.add(event); + } + } + } + + // Link event listener. + private class InternalLinkListener implements LinkListener { + @Override + public void event(LinkEvent event) { + msgSender.execute(() -> sendMessage(linkMessage(event))); + eventAccummulator.add(event); + } + } + + // Host event listener. + private class InternalHostListener implements HostListener { + @Override + public void event(HostEvent event) { + msgSender.execute(() -> sendMessage(hostMessage(event))); + eventAccummulator.add(event); + } + } + + // Intent event listener. + private class InternalIntentListener implements IntentListener { + @Override + public void event(IntentEvent event) { + msgSender.execute(traffic::pokeIntent); + eventAccummulator.add(event); + } + } + + // Intent event listener. + private class InternalFlowListener implements FlowRuleListener { + @Override + public void event(FlowRuleEvent event) { + eventAccummulator.add(event); + } + } + + + // === SUMMARY MONITORING + + // Periodic update of the summary information + private class SummaryMonitor extends TimerTask { + @Override + public void run() { + try { + if (summaryRunning) { + msgSender.execute(() -> requestSummary(0)); + } + } catch (Exception e) { + log.warn("Unable to handle summary request due to {}", e.getMessage()); + log.warn("Boom!", e); + } + } + } + + // Accumulates events to drive methodic update of the summary pane. + private class InternalEventAccummulator extends AbstractAccumulator<Event> { + protected InternalEventAccummulator() { + super(new Timer("topo-summary"), MAX_EVENTS, MAX_BATCH_MS, MAX_IDLE_MS); + } + + @Override + public void processItems(List<Event> items) { + // Start-of-Debugging -- Keep in until ONOS-2572 is fixed for reals + long now = System.currentTimeMillis(); + String me = this.toString(); + String miniMe = me.replaceAll("^.*@", "me@"); + log.debug("Time: {}; this: {}, processing items ({} events)", + now, miniMe, items.size()); + // End-of-Debugging + + try { + if (summaryRunning) { + msgSender.execute(() -> requestSummary(0)); + } + } catch (Exception e) { + log.warn("Unable to handle summary request due to {}", e.getMessage()); + log.debug("Boom!", e); + } + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java new file mode 100644 index 00000000..840e89f3 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessageHandlerBase.java @@ -0,0 +1,509 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.onlab.osgi.ServiceDirectory; +import org.onlab.packet.IpAddress; +import org.onosproject.cluster.ClusterEvent; +import org.onosproject.cluster.ClusterService; +import org.onosproject.cluster.ControllerNode; +import org.onosproject.cluster.NodeId; +import org.onosproject.core.CoreService; +import org.onosproject.incubator.net.PortStatisticsService; +import org.onosproject.incubator.net.tunnel.OpticalTunnelEndPoint; +import org.onosproject.incubator.net.tunnel.Tunnel; +import org.onosproject.incubator.net.tunnel.TunnelService; +import org.onosproject.mastership.MastershipService; +import org.onosproject.net.Annotated; +import org.onosproject.net.AnnotationKeys; +import org.onosproject.net.Annotations; +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.DefaultEdgeLink; +import org.onosproject.net.Device; +import org.onosproject.net.DeviceId; +import org.onosproject.net.EdgeLink; +import org.onosproject.net.Host; +import org.onosproject.net.HostId; +import org.onosproject.net.HostLocation; +import org.onosproject.net.Link; +import org.onosproject.net.PortNumber; +import org.onosproject.net.device.DeviceEvent; +import org.onosproject.net.device.DeviceService; +import org.onosproject.net.flow.FlowEntry; +import org.onosproject.net.flow.FlowRuleService; +import org.onosproject.net.flow.TrafficTreatment; +import org.onosproject.net.flow.instructions.Instruction; +import org.onosproject.net.flow.instructions.Instructions.OutputInstruction; +import org.onosproject.net.host.HostEvent; +import org.onosproject.net.host.HostService; +import org.onosproject.net.intent.IntentService; +import org.onosproject.net.link.LinkEvent; +import org.onosproject.net.link.LinkService; +import org.onosproject.net.provider.ProviderId; +import org.onosproject.net.statistic.StatisticService; +import org.onosproject.net.topology.Topology; +import org.onosproject.net.topology.TopologyService; +import org.onosproject.ui.JsonUtils; +import org.onosproject.ui.UiConnection; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.impl.topo.ServicesBundle; +import org.onosproject.ui.topo.PropertyPanel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Strings.isNullOrEmpty; +import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED; +import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_REMOVED; +import static org.onosproject.cluster.ControllerNode.State.ACTIVE; +import static org.onosproject.net.DefaultEdgeLink.createEdgeLink; +import static org.onosproject.net.PortNumber.portNumber; +import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED; +import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_REMOVED; +import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED; +import static org.onosproject.net.host.HostEvent.Type.HOST_REMOVED; +import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED; +import static org.onosproject.net.link.LinkEvent.Type.LINK_REMOVED; +import static org.onosproject.ui.topo.TopoConstants.CoreButtons; +import static org.onosproject.ui.topo.TopoConstants.Properties; +import static org.onosproject.ui.topo.TopoUtils.compactLinkString; + +/** + * Facility for creating messages bound for the topology viewer. + */ +public abstract class TopologyViewMessageHandlerBase extends UiMessageHandler { + + protected static final Logger log = + LoggerFactory.getLogger(TopologyViewMessageHandlerBase.class); + + private static final ProviderId PID = + new ProviderId("core", "org.onosproject.core", true); + + protected static final String SHOW_HIGHLIGHTS = "showHighlights"; + + protected ServiceDirectory directory; + protected ClusterService clusterService; + protected DeviceService deviceService; + protected LinkService linkService; + protected HostService hostService; + protected MastershipService mastershipService; + protected IntentService intentService; + protected FlowRuleService flowService; + protected StatisticService flowStatsService; + protected PortStatisticsService portStatsService; + protected TopologyService topologyService; + protected TunnelService tunnelService; + + protected ServicesBundle servicesBundle; + + private String version; + + // TODO: extract into an external & durable state; good enough for now and demo + private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>(); + + /** + * Returns read-only view of the meta-ui information. + * + * @return map of id to meta-ui mementos + */ + static Map<String, ObjectNode> getMetaUi() { + return Collections.unmodifiableMap(metaUi); + } + + @Override + public void init(UiConnection connection, ServiceDirectory directory) { + super.init(connection, directory); + this.directory = checkNotNull(directory, "Directory cannot be null"); + clusterService = directory.get(ClusterService.class); + deviceService = directory.get(DeviceService.class); + linkService = directory.get(LinkService.class); + hostService = directory.get(HostService.class); + mastershipService = directory.get(MastershipService.class); + intentService = directory.get(IntentService.class); + flowService = directory.get(FlowRuleService.class); + flowStatsService = directory.get(StatisticService.class); + portStatsService = directory.get(PortStatisticsService.class); + topologyService = directory.get(TopologyService.class); + tunnelService = directory.get(TunnelService.class); + + servicesBundle = new ServicesBundle(intentService, deviceService, + hostService, linkService, + flowService, + flowStatsService, portStatsService); + + String ver = directory.get(CoreService.class).version().toString(); + version = ver.replace(".SNAPSHOT", "*").replaceFirst("~.*$", ""); + } + + // Returns the specified set of IP addresses as a string. + private String ip(Set<IpAddress> ipAddresses) { + Iterator<IpAddress> it = ipAddresses.iterator(); + return it.hasNext() ? it.next().toString() : "unknown"; + } + + // Produces JSON structure from annotations. + private JsonNode props(Annotations annotations) { + ObjectNode props = objectNode(); + if (annotations != null) { + for (String key : annotations.keys()) { + props.put(key, annotations.value(key)); + } + } + return props; + } + + // Produces an informational log message event bound to the client. + protected ObjectNode info(long id, String message) { + return message("info", id, message); + } + + // Produces a warning log message event bound to the client. + protected ObjectNode warning(long id, String message) { + return message("warning", id, message); + } + + // Produces an error log message event bound to the client. + protected ObjectNode error(long id, String message) { + return message("error", id, message); + } + + // Produces a log message event bound to the client. + private ObjectNode message(String severity, long id, String message) { + ObjectNode payload = objectNode() + .put("severity", severity) + .put("message", message); + + return JsonUtils.envelope("message", id, payload); + } + + // Produces a cluster instance message to the client. + protected ObjectNode instanceMessage(ClusterEvent event, String messageType) { + ControllerNode node = event.subject(); + int switchCount = mastershipService.getDevicesOf(node.id()).size(); + ObjectNode payload = objectNode() + .put("id", node.id().toString()) + .put("ip", node.ip().toString()) + .put("online", clusterService.getState(node.id()) == ACTIVE) + .put("uiAttached", node.equals(clusterService.getLocalNode())) + .put("switches", switchCount); + + ArrayNode labels = arrayNode(); + labels.add(node.id().toString()); + labels.add(node.ip().toString()); + + // Add labels, props and stuff the payload into envelope. + payload.set("labels", labels); + addMetaUi(node.id().toString(), payload); + + String type = messageType != null ? messageType : + ((event.type() == INSTANCE_ADDED) ? "addInstance" : + ((event.type() == INSTANCE_REMOVED ? "removeInstance" : + "addInstance"))); + return JsonUtils.envelope(type, 0, payload); + } + + // Produces a device event message to the client. + protected ObjectNode deviceMessage(DeviceEvent event) { + Device device = event.subject(); + ObjectNode payload = objectNode() + .put("id", device.id().toString()) + .put("type", device.type().toString().toLowerCase()) + .put("online", deviceService.isAvailable(device.id())) + .put("master", master(device.id())); + + // Generate labels: id, chassis id, no-label, optional-name + String name = device.annotations().value(AnnotationKeys.NAME); + ArrayNode labels = arrayNode(); + labels.add(""); + labels.add(isNullOrEmpty(name) ? device.id().toString() : name); + labels.add(device.id().toString()); + + // Add labels, props and stuff the payload into envelope. + payload.set("labels", labels); + payload.set("props", props(device.annotations())); + addGeoLocation(device, payload); + addMetaUi(device.id().toString(), payload); + + String type = (event.type() == DEVICE_ADDED) ? "addDevice" : + ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice"); + return JsonUtils.envelope(type, 0, payload); + } + + // Produces a link event message to the client. + protected ObjectNode linkMessage(LinkEvent event) { + Link link = event.subject(); + ObjectNode payload = objectNode() + .put("id", compactLinkString(link)) + .put("type", link.type().toString().toLowerCase()) + .put("online", link.state() == Link.State.ACTIVE) + .put("linkWidth", 1.2) + .put("src", link.src().deviceId().toString()) + .put("srcPort", link.src().port().toString()) + .put("dst", link.dst().deviceId().toString()) + .put("dstPort", link.dst().port().toString()); + String type = (event.type() == LINK_ADDED) ? "addLink" : + ((event.type() == LINK_REMOVED) ? "removeLink" : "updateLink"); + return JsonUtils.envelope(type, 0, payload); + } + + // Produces a host event message to the client. + protected ObjectNode hostMessage(HostEvent event) { + Host host = event.subject(); + String hostType = host.annotations().value(AnnotationKeys.TYPE); + ObjectNode payload = objectNode() + .put("id", host.id().toString()) + .put("type", isNullOrEmpty(hostType) ? "endstation" : hostType) + .put("ingress", compactLinkString(edgeLink(host, true))) + .put("egress", compactLinkString(edgeLink(host, false))); + payload.set("cp", hostConnect(host.location())); + payload.set("labels", labels(ip(host.ipAddresses()), + host.mac().toString())); + payload.set("props", props(host.annotations())); + addGeoLocation(host, payload); + addMetaUi(host.id().toString(), payload); + + String type = (event.type() == HOST_ADDED) ? "addHost" : + ((event.type() == HOST_REMOVED) ? "removeHost" : "updateHost"); + return JsonUtils.envelope(type, 0, payload); + } + + // Encodes the specified host location into a JSON object. + private ObjectNode hostConnect(HostLocation location) { + return objectNode() + .put("device", location.deviceId().toString()) + .put("port", location.port().toLong()); + } + + // Encodes the specified list of labels a JSON array. + private ArrayNode labels(String... labels) { + ArrayNode json = arrayNode(); + for (String label : labels) { + json.add(label); + } + return json; + } + + // Returns the name of the master node for the specified device id. + private String master(DeviceId deviceId) { + NodeId master = mastershipService.getMasterFor(deviceId); + return master != null ? master.toString() : ""; + } + + // Generates an edge link from the specified host location. + private EdgeLink edgeLink(Host host, boolean ingress) { + return new DefaultEdgeLink(PID, new ConnectPoint(host.id(), portNumber(0)), + host.location(), ingress); + } + + // Adds meta UI information for the specified object. + private void addMetaUi(String id, ObjectNode payload) { + ObjectNode meta = metaUi.get(id); + if (meta != null) { + payload.set("metaUi", meta); + } + } + + // Adds a geo location JSON to the specified payload object. + private void addGeoLocation(Annotated annotated, ObjectNode payload) { + Annotations annotations = annotated.annotations(); + if (annotations == null) { + return; + } + + String slat = annotations.value(AnnotationKeys.LATITUDE); + String slng = annotations.value(AnnotationKeys.LONGITUDE); + try { + if (slat != null && slng != null && !slat.isEmpty() && !slng.isEmpty()) { + double lat = Double.parseDouble(slat); + double lng = Double.parseDouble(slng); + ObjectNode loc = objectNode() + .put("type", "latlng").put("lat", lat).put("lng", lng); + payload.set("location", loc); + } + } catch (NumberFormatException e) { + log.warn("Invalid geo data latitude={}; longiture={}", slat, slng); + } + } + + // Updates meta UI information for the specified object. + protected void updateMetaUi(ObjectNode payload) { + metaUi.put(JsonUtils.string(payload, "id"), + JsonUtils.node(payload, "memento")); + } + + + // ----------------------------------------------------------------------- + // Create models of the data to return, that overlays can adjust / augment + + // Returns property panel model for summary response. + protected PropertyPanel summmaryMessage(long sid) { + Topology topology = topologyService.currentTopology(); + + return new PropertyPanel("ONOS Summary", "node") + .addProp(Properties.DEVICES, topology.deviceCount()) + .addProp(Properties.LINKS, topology.linkCount()) + .addProp(Properties.HOSTS, hostService.getHostCount()) + .addProp(Properties.TOPOLOGY_SSCS, topology.clusterCount()) + .addSeparator() + .addProp(Properties.INTENTS, intentService.getIntentCount()) + .addProp(Properties.TUNNELS, tunnelService.tunnelCount()) + .addProp(Properties.FLOWS, flowService.getFlowRuleCount()) + .addProp(Properties.VERSION, version); + } + + // Returns property panel model for device details response. + protected PropertyPanel deviceDetails(DeviceId deviceId, long sid) { + Device device = deviceService.getDevice(deviceId); + Annotations annot = device.annotations(); + String name = annot.value(AnnotationKeys.NAME); + int portCount = deviceService.getPorts(deviceId).size(); + int flowCount = getFlowCount(deviceId); + int tunnelCount = getTunnelCount(deviceId); + + String title = isNullOrEmpty(name) ? deviceId.toString() : name; + String typeId = device.type().toString().toLowerCase(); + + PropertyPanel pp = new PropertyPanel(title, typeId) + .id(deviceId.toString()) + + .addProp(Properties.URI, deviceId.toString()) + .addProp(Properties.VENDOR, device.manufacturer()) + .addProp(Properties.HW_VERSION, device.hwVersion()) + .addProp(Properties.SW_VERSION, device.swVersion()) + .addProp(Properties.SERIAL_NUMBER, device.serialNumber()) + .addProp(Properties.PROTOCOL, annot.value(AnnotationKeys.PROTOCOL)) + .addSeparator() + + .addProp(Properties.LATITUDE, annot.value(AnnotationKeys.LATITUDE)) + .addProp(Properties.LONGITUDE, annot.value(AnnotationKeys.LONGITUDE)) + .addSeparator() + + .addProp(Properties.PORTS, portCount) + .addProp(Properties.FLOWS, flowCount) + .addProp(Properties.TUNNELS, tunnelCount) + + .addButton(CoreButtons.SHOW_DEVICE_VIEW) + .addButton(CoreButtons.SHOW_FLOW_VIEW) + .addButton(CoreButtons.SHOW_PORT_VIEW) + .addButton(CoreButtons.SHOW_GROUP_VIEW); + + return pp; + } + + protected int getFlowCount(DeviceId deviceId) { + int count = 0; + for (FlowEntry flowEntry : flowService.getFlowEntries(deviceId)) { + count++; + } + return count; + } + + protected int getTunnelCount(DeviceId deviceId) { + int count = 0; + Collection<Tunnel> tunnels = tunnelService.queryAllTunnels(); + for (Tunnel tunnel : tunnels) { + OpticalTunnelEndPoint src = (OpticalTunnelEndPoint) tunnel.src(); + OpticalTunnelEndPoint dst = (OpticalTunnelEndPoint) tunnel.dst(); + DeviceId srcDevice = (DeviceId) src.elementId().get(); + DeviceId dstDevice = (DeviceId) dst.elementId().get(); + if (srcDevice.toString().equals(deviceId.toString()) || + dstDevice.toString().equals(deviceId.toString())) { + count++; + } + } + return count; + } + + // Counts all flow entries that egress on the links of the given device. + private Map<Link, Integer> getLinkFlowCounts(DeviceId deviceId) { + // get the flows for the device + List<FlowEntry> entries = new ArrayList<>(); + for (FlowEntry flowEntry : flowService.getFlowEntries(deviceId)) { + entries.add(flowEntry); + } + + // get egress links from device, and include edge links + Set<Link> links = new HashSet<>(linkService.getDeviceEgressLinks(deviceId)); + Set<Host> hosts = hostService.getConnectedHosts(deviceId); + if (hosts != null) { + for (Host host : hosts) { + links.add(createEdgeLink(host, false)); + } + } + + // compile flow counts per link + Map<Link, Integer> counts = new HashMap<>(); + for (Link link : links) { + counts.put(link, getEgressFlows(link, entries)); + } + return counts; + } + + // Counts all entries that egress on the link source port. + private int getEgressFlows(Link link, List<FlowEntry> entries) { + int count = 0; + PortNumber out = link.src().port(); + for (FlowEntry entry : entries) { + TrafficTreatment treatment = entry.treatment(); + for (Instruction instruction : treatment.allInstructions()) { + if (instruction.type() == Instruction.Type.OUTPUT && + ((OutputInstruction) instruction).port().equals(out)) { + count++; + } + } + } + return count; + } + + // Returns host details response. + protected PropertyPanel hostDetails(HostId hostId, long sid) { + Host host = hostService.getHost(hostId); + Annotations annot = host.annotations(); + String type = annot.value(AnnotationKeys.TYPE); + String name = annot.value(AnnotationKeys.NAME); + String vlan = host.vlan().toString(); + + String title = isNullOrEmpty(name) ? hostId.toString() : name; + String typeId = isNullOrEmpty(type) ? "endstation" : type; + + PropertyPanel pp = new PropertyPanel(title, typeId) + .id(hostId.toString()) + .addProp(Properties.MAC, host.mac()) + .addProp(Properties.IP, host.ipAddresses(), "[\\[\\]]") + .addProp(Properties.VLAN, vlan.equals("-1") ? "none" : vlan) + .addSeparator() + .addProp(Properties.LATITUDE, annot.value(AnnotationKeys.LATITUDE)) + .addProp(Properties.LONGITUDE, annot.value(AnnotationKeys.LONGITUDE)); + + // Potentially add button descriptors here + return pp; + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java new file mode 100644 index 00000000..3d733f9e --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java @@ -0,0 +1,675 @@ +/* + * 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.ui.impl; + +import com.google.common.collect.ImmutableList; +import org.onosproject.net.Device; +import org.onosproject.net.DeviceId; +import org.onosproject.net.ElementId; +import org.onosproject.net.Host; +import org.onosproject.net.HostId; +import org.onosproject.net.Link; +import org.onosproject.net.PortNumber; +import org.onosproject.net.flow.FlowEntry; +import org.onosproject.net.flow.TrafficTreatment; +import org.onosproject.net.flow.instructions.Instruction; +import org.onosproject.net.flow.instructions.Instructions.OutputInstruction; +import org.onosproject.net.intent.FlowRuleIntent; +import org.onosproject.net.intent.Intent; +import org.onosproject.net.intent.LinkCollectionIntent; +import org.onosproject.net.intent.OpticalConnectivityIntent; +import org.onosproject.net.intent.OpticalPathIntent; +import org.onosproject.net.intent.PathIntent; +import org.onosproject.net.statistic.Load; +import org.onosproject.ui.impl.topo.IntentSelection; +import org.onosproject.ui.impl.topo.ServicesBundle; +import org.onosproject.ui.impl.topo.TopoIntentFilter; +import org.onosproject.ui.impl.topo.TrafficLink; +import org.onosproject.ui.impl.topo.TrafficLink.StatsType; +import org.onosproject.ui.impl.topo.TrafficLinkMap; +import org.onosproject.ui.topo.DeviceHighlight; +import org.onosproject.ui.topo.Highlights; +import org.onosproject.ui.topo.Highlights.Amount; +import org.onosproject.ui.topo.HostHighlight; +import org.onosproject.ui.topo.LinkHighlight.Flavor; +import org.onosproject.ui.topo.NodeHighlight; +import org.onosproject.ui.topo.NodeSelection; +import org.onosproject.ui.topo.TopoUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; + +import static org.onosproject.net.DefaultEdgeLink.createEdgeLink; +import static org.onosproject.ui.impl.TrafficMonitor.Mode.*; + +/** + * Encapsulates the behavior of monitoring specific traffic patterns. + */ +public class TrafficMonitor { + + // 4 Kilo Bytes as threshold + private static final double BPS_THRESHOLD = 4 * TopoUtils.KILO; + + private static final Logger log = + LoggerFactory.getLogger(TrafficMonitor.class); + + /** + * Designates the different modes of operation. + */ + public enum Mode { + IDLE, + ALL_FLOW_TRAFFIC, + ALL_PORT_TRAFFIC, + DEV_LINK_FLOWS, + RELATED_INTENTS, + SELECTED_INTENT + } + + private final long trafficPeriod; + private final ServicesBundle servicesBundle; + private final TopologyViewMessageHandler msgHandler; + private final TopoIntentFilter intentFilter; + + private final Timer timer = new Timer("topo-traffic"); + + private TimerTask trafficTask = null; + private Mode mode = IDLE; + private NodeSelection selectedNodes = null; + private IntentSelection selectedIntents = null; + + + /** + * Constructs a traffic monitor. + * + * @param trafficPeriod traffic task period in ms + * @param servicesBundle bundle of services + * @param msgHandler our message handler + */ + public TrafficMonitor(long trafficPeriod, ServicesBundle servicesBundle, + TopologyViewMessageHandler msgHandler) { + this.trafficPeriod = trafficPeriod; + this.servicesBundle = servicesBundle; + this.msgHandler = msgHandler; + + intentFilter = new TopoIntentFilter(servicesBundle); + } + + // ======================================================================= + // === API === + + /** + * Monitor for traffic data to be sent back to the web client, under + * the given mode. This causes a background traffic task to be + * scheduled to repeatedly compute and transmit the appropriate traffic + * data to the client. + * <p> + * The monitoring mode is expected to be one of: + * <ul> + * <li>ALL_FLOW_TRAFFIC</li> + * <li>ALL_PORT_TRAFFIC</li> + * <li>SELECTED_INTENT</li> + * </ul> + * + * @param mode monitoring mode + */ + public synchronized void monitor(Mode mode) { + log.debug("monitor: {}", mode); + this.mode = mode; + + switch (mode) { + case ALL_FLOW_TRAFFIC: + clearSelection(); + scheduleTask(); + sendAllFlowTraffic(); + break; + + case ALL_PORT_TRAFFIC: + clearSelection(); + scheduleTask(); + sendAllPortTraffic(); + break; + + case SELECTED_INTENT: + scheduleTask(); + sendSelectedIntentTraffic(); + break; + + default: + log.debug("Unexpected call to monitor({})", mode); + clearAll(); + break; + } + } + + /** + * Monitor for traffic data to be sent back to the web client, under + * the given mode, using the given selection of devices and hosts. + * In the case of "device link flows", this causes a background traffic + * task to be scheduled to repeatedly compute and transmit the appropriate + * traffic data to the client. In the case of "related intents", no + * repeating task is scheduled. + * <p> + * The monitoring mode is expected to be one of: + * <ul> + * <li>DEV_LINK_FLOWS</li> + * <li>RELATED_INTENTS</li> + * </ul> + * + * @param mode monitoring mode + * @param nodeSelection how to select a node + */ + public synchronized void monitor(Mode mode, NodeSelection nodeSelection) { + log.debug("monitor: {} -- {}", mode, nodeSelection); + this.mode = mode; + this.selectedNodes = nodeSelection; + + switch (mode) { + case DEV_LINK_FLOWS: + // only care about devices (not hosts) + if (selectedNodes.devicesWithHover().isEmpty()) { + sendClearAll(); + } else { + scheduleTask(); + sendDeviceLinkFlows(); + } + break; + + case RELATED_INTENTS: + if (selectedNodes.none()) { + sendClearAll(); + } else { + selectedIntents = new IntentSelection(selectedNodes, intentFilter); + if (selectedIntents.none()) { + sendClearAll(); + } else { + sendSelectedIntents(); + } + } + break; + + default: + log.debug("Unexpected call to monitor({}, {})", mode, nodeSelection); + clearAll(); + break; + } + } + + // TODO: move this out to the "h2h/multi-intent app" + /** + * Monitor for traffic data to be sent back to the web client, for the + * given intent. + * + * @param intent the intent to monitor + */ + public synchronized void monitor(Intent intent) { + log.debug("monitor intent: {}", intent.id()); + selectedNodes = null; + selectedIntents = new IntentSelection(intent); + mode = SELECTED_INTENT; + scheduleTask(); + sendSelectedIntentTraffic(); + } + + /** + * Selects the next intent in the select group (if there is one), + * and sends highlighting data back to the web client to display + * which path is selected. + */ + public synchronized void selectNextIntent() { + if (selectedIntents != null) { + selectedIntents.next(); + sendSelectedIntents(); + if (mode == SELECTED_INTENT) { + mode = RELATED_INTENTS; + } + } + } + + /** + * Selects the previous intent in the select group (if there is one), + * and sends highlighting data back to the web client to display + * which path is selected. + */ + public synchronized void selectPreviousIntent() { + if (selectedIntents != null) { + selectedIntents.prev(); + sendSelectedIntents(); + if (mode == SELECTED_INTENT) { + mode = RELATED_INTENTS; + } + } + } + + /** + * Resends selected intent traffic data. This is called, for example, + * when the system detects an intent update happened. + */ + public synchronized void pokeIntent() { + if (mode == SELECTED_INTENT) { + sendSelectedIntentTraffic(); + } + } + + /** + * Stop all traffic monitoring. + */ + public synchronized void stopMonitoring() { + log.debug("STOP monitoring"); + if (mode != IDLE) { + sendClearAll(); + } + } + + + // ======================================================================= + // === Helper methods === + + private void sendClearAll() { + clearAll(); + sendClearHighlights(); + } + + private void clearAll() { + this.mode = IDLE; + clearSelection(); + cancelTask(); + } + + private void clearSelection() { + selectedNodes = null; + selectedIntents = null; + } + + private synchronized void scheduleTask() { + if (trafficTask == null) { + log.debug("Starting up background traffic task..."); + trafficTask = new TrafficUpdateTask(); + timer.schedule(trafficTask, trafficPeriod, trafficPeriod); + } else { + log.debug("(traffic task already running)"); + } + } + + private synchronized void cancelTask() { + if (trafficTask != null) { + trafficTask.cancel(); + trafficTask = null; + } + } + + private void sendAllFlowTraffic() { + log.debug("sendAllFlowTraffic"); + msgHandler.sendHighlights(trafficSummary(StatsType.FLOW_STATS)); + } + + private void sendAllPortTraffic() { + log.debug("sendAllPortTraffic"); + msgHandler.sendHighlights(trafficSummary(StatsType.PORT_STATS)); + } + + private void sendDeviceLinkFlows() { + log.debug("sendDeviceLinkFlows: {}", selectedNodes); + msgHandler.sendHighlights(deviceLinkFlows()); + } + + private void sendSelectedIntents() { + log.debug("sendSelectedIntents: {}", selectedIntents); + msgHandler.sendHighlights(intentGroup()); + } + + private void sendSelectedIntentTraffic() { + log.debug("sendSelectedIntentTraffic: {}", selectedIntents); + msgHandler.sendHighlights(intentTraffic()); + } + + private void sendClearHighlights() { + log.debug("sendClearHighlights"); + msgHandler.sendHighlights(new Highlights()); + } + + // ======================================================================= + // === Generate messages in JSON object node format + + private Highlights trafficSummary(StatsType type) { + Highlights highlights = new Highlights(); + + TrafficLinkMap linkMap = new TrafficLinkMap(); + compileLinks(linkMap); + addEdgeLinks(linkMap); + + for (TrafficLink tlink : linkMap.biLinks()) { + if (type == StatsType.FLOW_STATS) { + attachFlowLoad(tlink); + } else if (type == StatsType.PORT_STATS) { + attachPortLoad(tlink); + } + + // we only want to report on links deemed to have traffic + if (tlink.hasTraffic()) { + highlights.add(tlink.highlight(type)); + } + } + return highlights; + } + + // create highlights for links, showing flows for selected devices. + private Highlights deviceLinkFlows() { + Highlights highlights = new Highlights(); + + if (selectedNodes != null && !selectedNodes.devicesWithHover().isEmpty()) { + // capture flow counts on bilinks + TrafficLinkMap linkMap = new TrafficLinkMap(); + + for (Device device : selectedNodes.devicesWithHover()) { + Map<Link, Integer> counts = getLinkFlowCounts(device.id()); + for (Link link : counts.keySet()) { + TrafficLink tlink = linkMap.add(link); + tlink.addFlows(counts.get(link)); + } + } + + // now report on our collated links + for (TrafficLink tlink : linkMap.biLinks()) { + highlights.add(tlink.highlight(StatsType.FLOW_COUNT)); + } + + } + return highlights; + } + + private Highlights intentGroup() { + Highlights highlights = new Highlights(); + + if (selectedIntents != null && !selectedIntents.none()) { + // If 'all' intents are selected, they will all have primary + // highlighting; otherwise, the specifically selected intent will + // have primary highlighting, and the remainder will have secondary + // highlighting. + Set<Intent> primary; + Set<Intent> secondary; + int count = selectedIntents.size(); + + Set<Intent> allBut = new HashSet<>(selectedIntents.intents()); + Intent current; + + if (selectedIntents.all()) { + primary = allBut; + secondary = Collections.emptySet(); + log.debug("Highlight all intents ({})", count); + } else { + current = selectedIntents.current(); + primary = new HashSet<>(); + primary.add(current); + allBut.remove(current); + secondary = allBut; + log.debug("Highlight intent: {} ([{}] of {})", + current.id(), selectedIntents.index(), count); + } + + highlightIntentLinks(highlights, primary, secondary); + } + return highlights; + } + + private Highlights intentTraffic() { + Highlights highlights = new Highlights(); + + if (selectedIntents != null && selectedIntents.single()) { + Intent current = selectedIntents.current(); + Set<Intent> primary = new HashSet<>(); + primary.add(current); + log.debug("Highlight traffic for intent: {} ([{}] of {})", + current.id(), selectedIntents.index(), selectedIntents.size()); + + highlightIntentLinksWithTraffic(highlights, primary); + highlights.subdueAllElse(Amount.MINIMALLY); + } + return highlights; + } + + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + private void compileLinks(TrafficLinkMap linkMap) { + servicesBundle.linkService().getLinks().forEach(linkMap::add); + } + + private void addEdgeLinks(TrafficLinkMap linkMap) { + servicesBundle.hostService().getHosts().forEach(host -> { + linkMap.add(createEdgeLink(host, true)); + linkMap.add(createEdgeLink(host, false)); + }); + } + + private Load getLinkFlowLoad(Link link) { + if (link != null && link.src().elementId() instanceof DeviceId) { + return servicesBundle.flowStatsService().load(link); + } + return null; + } + + private void attachFlowLoad(TrafficLink link) { + link.addLoad(getLinkFlowLoad(link.one())); + link.addLoad(getLinkFlowLoad(link.two())); + } + + private void attachPortLoad(TrafficLink link) { + // For bi-directional traffic links, use + // the max link rate of either direction + // (we choose 'one' since we know that is never null) + Link one = link.one(); + Load egressSrc = servicesBundle.portStatsService().load(one.src()); + Load egressDst = servicesBundle.portStatsService().load(one.dst()); + link.addLoad(maxLoad(egressSrc, egressDst), BPS_THRESHOLD); +// link.addLoad(maxLoad(egressSrc, egressDst), 10); // DEBUG ONLY!! + } + + private Load maxLoad(Load a, Load b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.rate() > b.rate() ? a : b; + } + + // Counts all flow entries that egress on the links of the given device. + private Map<Link, Integer> getLinkFlowCounts(DeviceId deviceId) { + // get the flows for the device + List<FlowEntry> entries = new ArrayList<>(); + for (FlowEntry flowEntry : servicesBundle.flowService() + .getFlowEntries(deviceId)) { + entries.add(flowEntry); + } + + // get egress links from device, and include edge links + Set<Link> links = new HashSet<>(servicesBundle.linkService() + .getDeviceEgressLinks(deviceId)); + Set<Host> hosts = servicesBundle.hostService().getConnectedHosts(deviceId); + if (hosts != null) { + for (Host host : hosts) { + links.add(createEdgeLink(host, false)); + } + } + + // compile flow counts per link + Map<Link, Integer> counts = new HashMap<>(); + for (Link link : links) { + counts.put(link, getEgressFlows(link, entries)); + } + return counts; + } + + // Counts all entries that egress on the link source port. + private int getEgressFlows(Link link, List<FlowEntry> entries) { + int count = 0; + PortNumber out = link.src().port(); + for (FlowEntry entry : entries) { + TrafficTreatment treatment = entry.treatment(); + for (Instruction instruction : treatment.allInstructions()) { + if (instruction.type() == Instruction.Type.OUTPUT && + ((OutputInstruction) instruction).port().equals(out)) { + count++; + } + } + } + return count; + } + + private void highlightIntentLinks(Highlights highlights, + Set<Intent> primary, Set<Intent> secondary) { + TrafficLinkMap linkMap = new TrafficLinkMap(); + // NOTE: highlight secondary first, then primary, so that links shared + // by intents are colored correctly ("last man wins") + createTrafficLinks(highlights, linkMap, secondary, Flavor.SECONDARY_HIGHLIGHT, false); + createTrafficLinks(highlights, linkMap, primary, Flavor.PRIMARY_HIGHLIGHT, false); + colorLinks(highlights, linkMap); + } + + private void highlightIntentLinksWithTraffic(Highlights highlights, + Set<Intent> primary) { + TrafficLinkMap linkMap = new TrafficLinkMap(); + createTrafficLinks(highlights, linkMap, primary, Flavor.PRIMARY_HIGHLIGHT, true); + colorLinks(highlights, linkMap); + } + + private void createTrafficLinks(Highlights highlights, + TrafficLinkMap linkMap, Set<Intent> intents, + Flavor flavor, boolean showTraffic) { + for (Intent intent : intents) { + List<Intent> installables = servicesBundle.intentService() + .getInstallableIntents(intent.key()); + Iterable<Link> links = null; + if (installables != null) { + for (Intent installable : installables) { + + if (installable instanceof PathIntent) { + links = ((PathIntent) installable).path().links(); + } else if (installable instanceof FlowRuleIntent) { + links = linkResources(installable); + } else if (installable instanceof LinkCollectionIntent) { + links = ((LinkCollectionIntent) installable).links(); + } else if (installable instanceof OpticalPathIntent) { + links = ((OpticalPathIntent) installable).path().links(); + } + + boolean isOptical = intent instanceof OpticalConnectivityIntent; + processLinks(linkMap, links, flavor, isOptical, showTraffic); + updateHighlights(highlights, links); + } + } + } + } + + private void updateHighlights(Highlights highlights, Iterable<Link> links) { + for (Link link : links) { + ensureNodePresent(highlights, link.src().elementId()); + ensureNodePresent(highlights, link.dst().elementId()); + } + } + + private void ensureNodePresent(Highlights highlights, ElementId eid) { + String id = eid.toString(); + NodeHighlight nh = highlights.getNode(id); + if (nh == null) { + if (eid instanceof DeviceId) { + nh = new DeviceHighlight(id); + highlights.add((DeviceHighlight) nh); + } else if (eid instanceof HostId) { + nh = new HostHighlight(id); + highlights.add((HostHighlight) nh); + } + } + } + + // Extracts links from the specified flow rule intent resources + private Collection<Link> linkResources(Intent installable) { + ImmutableList.Builder<Link> builder = ImmutableList.builder(); + installable.resources().stream().filter(r -> r instanceof Link) + .forEach(r -> builder.add((Link) r)); + return builder.build(); + } + + private void processLinks(TrafficLinkMap linkMap, Iterable<Link> links, + Flavor flavor, boolean isOptical, + boolean showTraffic) { + if (links != null) { + for (Link link : links) { + TrafficLink tlink = linkMap.add(link); + tlink.tagFlavor(flavor); + tlink.optical(isOptical); + if (showTraffic) { + tlink.addLoad(getLinkFlowLoad(link)); + tlink.antMarch(true); + } + } + } + } + + private void colorLinks(Highlights highlights, TrafficLinkMap linkMap) { + for (TrafficLink tlink : linkMap.biLinks()) { + highlights.add(tlink.highlight(StatsType.TAGGED)); + } + } + + // ======================================================================= + // === Background Task + + // Provides periodic update of traffic information to the client + private class TrafficUpdateTask extends TimerTask { + @Override + public void run() { + try { + switch (mode) { + case ALL_FLOW_TRAFFIC: + sendAllFlowTraffic(); + break; + case ALL_PORT_TRAFFIC: + sendAllPortTraffic(); + break; + case DEV_LINK_FLOWS: + sendDeviceLinkFlows(); + break; + case SELECTED_INTENT: + sendSelectedIntentTraffic(); + break; + + default: + // RELATED_INTENTS and IDLE modes should never invoke + // the background task, but if they do, they have + // nothing to do + break; + } + + } catch (Exception e) { + log.warn("Unable to process traffic task due to {}", e.getMessage()); + log.warn("Boom!", e); + } + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TrafficOverlay.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TrafficOverlay.java new file mode 100644 index 00000000..ea8ca3ea --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TrafficOverlay.java @@ -0,0 +1,62 @@ +/* + * 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.ui.impl; + +import org.onosproject.ui.UiTopoOverlay; +import org.onosproject.ui.topo.ButtonId; +import org.onosproject.ui.topo.PropertyPanel; + +/** + * Topology Overlay for network traffic. + */ +public class TrafficOverlay extends UiTopoOverlay { + /** + * Traffic Overlay identifier. + */ + public static final String TRAFFIC_ID = "traffic"; + + private static final String SDF_ID = "showDeviceFlows"; + private static final String SRT_ID = "showRelatedTraffic"; + + private static final ButtonId SHOW_DEVICE_FLOWS = new ButtonId(SDF_ID); + private static final ButtonId SHOW_RELATED_TRAFFIC = new ButtonId(SRT_ID); + + + public TrafficOverlay() { + super(TRAFFIC_ID); + } + + // override activate and deactivate, to write log messages + @Override + public void activate() { + super.activate(); + log.debug("TrafficOverlay Activated"); + } + + @Override + public void deactivate() { + super.deactivate(); + log.debug("TrafficOverlay Deactivated"); + } + + @Override + public void modifyDeviceDetails(PropertyPanel pp) { + pp.addButton(SHOW_DEVICE_FLOWS) + .addButton(SHOW_RELATED_TRAFFIC); + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TunnelViewMessageHandler.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TunnelViewMessageHandler.java new file mode 100644 index 00000000..13ce9398 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/TunnelViewMessageHandler.java @@ -0,0 +1,80 @@ +package org.onosproject.ui.impl; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.collect.ImmutableSet; +import org.onosproject.incubator.net.tunnel.Tunnel; +import org.onosproject.incubator.net.tunnel.TunnelEndPointFormatter; +import org.onosproject.incubator.net.tunnel.TunnelService; +import org.onosproject.ui.RequestHandler; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.table.TableModel; +import org.onosproject.ui.table.TableRequestHandler; +import org.onosproject.ui.table.cell.EnumFormatter; + +import java.util.Collection; + +public class TunnelViewMessageHandler extends UiMessageHandler { + private static final String TUNNEL_DATA_REQ = "tunnelDataRequest"; + private static final String TUNNEL_DATA_RESP = "tunnelDataResponse"; + private static final String TUNNELS = "tunnels"; + private static final String ID = "id"; + private static final String NAME = "name"; + private static final String ONE = "one"; + private static final String TWO = "two"; + private static final String TYPE = "type"; + private static final String GROUP_ID = "group_id"; + + private static final String BANDWIDTH = "bandwidth"; + private static final String PATH = "path"; + + + private static final String[] COL_IDS = { + ID, NAME, ONE, TWO, TYPE, GROUP_ID, + BANDWIDTH, PATH + }; + + @Override + protected Collection<RequestHandler> createRequestHandlers() { + return ImmutableSet.of(new TunnelDataRequestHandler()); + } + + private final class TunnelDataRequestHandler extends TableRequestHandler { + + public TunnelDataRequestHandler() { + super(TUNNEL_DATA_REQ, TUNNEL_DATA_RESP, TUNNELS); + } + + @Override + protected String[] getColumnIds() { + return COL_IDS; + } + + @Override + protected TableModel createTableModel() { + TableModel tm = super.createTableModel(); + //TODO add more formater class so that we can get a more readable table + tm.setFormatter(ONE, TunnelEndPointFormatter.INSTANCE); + tm.setFormatter(TWO, TunnelEndPointFormatter.INSTANCE); + tm.setFormatter(TYPE, EnumFormatter.INSTANCE); + return tm; + } + + @Override + protected void populateTable(TableModel tm, ObjectNode payload) { + TunnelService ts = get(TunnelService.class); + ts.queryAllTunnels().forEach(tunnel -> populateRow(tm.addRow(), tunnel)); + } + + } + + private void populateRow(TableModel.Row row, Tunnel tunnel) { + row.cell(ID, tunnel.tunnelId().id()) + .cell(NAME, tunnel.tunnelName().value()) + .cell(ONE, tunnel.src()) + .cell(TWO, tunnel.dst()) + .cell(TYPE, tunnel.type()) + .cell(GROUP_ID, tunnel.groupId().id()) + .cell(BANDWIDTH, tunnel.annotations().value(BANDWIDTH)) + .cell(PATH, tunnel.path()); + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java new file mode 100644 index 00000000..2bd0bb61 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java @@ -0,0 +1,181 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.felix.scr.annotations.Activate; +import org.apache.felix.scr.annotations.Component; +import org.apache.felix.scr.annotations.Deactivate; +import org.apache.felix.scr.annotations.Reference; +import org.apache.felix.scr.annotations.ReferenceCardinality; +import org.apache.felix.scr.annotations.Service; +import org.onosproject.mastership.MastershipService; +import org.onosproject.ui.UiExtension; +import org.onosproject.ui.UiExtensionService; +import org.onosproject.ui.UiMessageHandlerFactory; +import org.onosproject.ui.UiTopoOverlayFactory; +import org.onosproject.ui.UiView; +import org.onosproject.ui.UiViewHidden; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.google.common.collect.ImmutableList.of; +import static java.util.stream.Collectors.toSet; +import static org.onosproject.ui.UiView.Category.NETWORK; +import static org.onosproject.ui.UiView.Category.PLATFORM; + +/** + * Manages the user interface extensions. + */ +@Component(immediate = true) +@Service +public class UiExtensionManager implements UiExtensionService, SpriteService { + + private static final ClassLoader CL = + UiExtensionManager.class.getClassLoader(); + private static final String CORE = "core"; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + // List of all extensions + private final List<UiExtension> extensions = Lists.newArrayList(); + + // Map of views to extensions + private final Map<String, UiExtension> views = Maps.newHashMap(); + + // Core views & core extension + private final UiExtension core = createCoreExtension(); + + + @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY) + protected MastershipService mastershipService; + + // Creates core UI extension + private UiExtension createCoreExtension() { + List<UiView> coreViews = of( + new UiView(PLATFORM, "app", "Applications", "nav_apps"), + new UiView(PLATFORM, "settings", "Settings", "nav_settings"), + new UiView(PLATFORM, "cluster", "Cluster Nodes", "nav_cluster"), + new UiView(NETWORK, "topo", "Topology", "nav_topo"), + new UiView(NETWORK, "device", "Devices", "nav_devs"), + new UiViewHidden("flow"), + new UiViewHidden("port"), + new UiViewHidden("group"), + new UiView(NETWORK, "link", "Links", "nav_links"), + new UiView(NETWORK, "host", "Hosts", "nav_hosts"), + new UiView(NETWORK, "intent", "Intents", "nav_intents"), + //TODO add a new type of icon for tunnel + new UiView(NETWORK, "tunnel", "Tunnels", "nav_links") + ); + + UiMessageHandlerFactory messageHandlerFactory = + () -> ImmutableList.of( + new TopologyViewMessageHandler(), + new DeviceViewMessageHandler(), + new LinkViewMessageHandler(), + new HostViewMessageHandler(), + new FlowViewMessageHandler(), + new PortViewMessageHandler(), + new GroupViewMessageHandler(), + new IntentViewMessageHandler(), + new ApplicationViewMessageHandler(), + new SettingsViewMessageHandler(), + new ClusterViewMessageHandler(), + new TunnelViewMessageHandler() + ); + + UiTopoOverlayFactory topoOverlayFactory = + () -> ImmutableList.of( + new TrafficOverlay() + ); + + return new UiExtension.Builder(CL, coreViews) + .messageHandlerFactory(messageHandlerFactory) + .topoOverlayFactory(topoOverlayFactory) + .resourcePath(CORE) + .build(); + } + + @Activate + public void activate() { + register(core); + log.info("Started"); + } + + @Deactivate + public void deactivate() { + UiWebSocketServlet.closeAll(); + unregister(core); + log.info("Stopped"); + } + + @Override + public synchronized void register(UiExtension extension) { + if (!extensions.contains(extension)) { + extensions.add(extension); + for (UiView view : extension.views()) { + views.put(view.id(), extension); + } + } + } + + @Override + public synchronized void unregister(UiExtension extension) { + extensions.remove(extension); + extension.views().stream() + .map(UiView::id).collect(toSet()).forEach(views::remove); + } + + @Override + public synchronized List<UiExtension> getExtensions() { + return ImmutableList.copyOf(extensions); + } + + @Override + public synchronized UiExtension getViewExtension(String viewId) { + return views.get(viewId); + } + + // ===================================================================== + // Provisional tracking of sprite definitions + + private final Map<String, JsonNode> sprites = Maps.newHashMap(); + + @Override + public Set<String> getNames() { + return ImmutableSet.copyOf(sprites.keySet()); + } + + @Override + public void put(String name, JsonNode spriteData) { + log.info("Registered sprite definition [{}]", name); + sprites.put(name, spriteData); + } + + @Override + public JsonNode get(String name) { + return sprites.get(name); + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java new file mode 100644 index 00000000..1b6f7f99 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java @@ -0,0 +1,240 @@ +/* + * 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.ui.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.eclipse.jetty.websocket.WebSocket; +import org.onlab.osgi.ServiceDirectory; +import org.onlab.osgi.ServiceNotFoundException; +import org.onosproject.cluster.ClusterService; +import org.onosproject.cluster.ControllerNode; +import org.onosproject.ui.UiConnection; +import org.onosproject.ui.UiExtensionService; +import org.onosproject.ui.UiMessageHandlerFactory; +import org.onosproject.ui.UiMessageHandler; +import org.onosproject.ui.UiTopoOverlayFactory; +import org.onosproject.ui.topo.TopoConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Web socket capable of interacting with the GUI. + */ +public class UiWebSocket + implements UiConnection, WebSocket.OnTextMessage, WebSocket.OnControl { + + private static final Logger log = LoggerFactory.getLogger(UiWebSocket.class); + + private static final long MAX_AGE_MS = 30_000; + + private static final byte PING = 0x9; + private static final byte PONG = 0xA; + private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad}; + + private final ServiceDirectory directory; + + private Connection connection; + private FrameConnection control; + + private final ObjectMapper mapper = new ObjectMapper(); + + private long lastActive = System.currentTimeMillis(); + + private Map<String, UiMessageHandler> handlers; + private TopoOverlayCache overlayCache; + + /** + * Creates a new web-socket for serving data to GUI. + * + * @param directory service directory + */ + public UiWebSocket(ServiceDirectory directory) { + this.directory = directory; + } + + /** + * Issues a close on the connection. + */ + synchronized void close() { + destroyHandlersAndOverlays(); + if (connection.isOpen()) { + connection.close(); + } + } + + /** + * Indicates if this connection is idle. + * + * @return true if idle or closed + */ + synchronized boolean isIdle() { + long quietFor = System.currentTimeMillis() - lastActive; + boolean idle = quietFor > MAX_AGE_MS; + if (idle || (connection != null && !connection.isOpen())) { + log.debug("IDLE (or closed) websocket [{} ms]", quietFor); + return true; + } else if (connection != null) { + try { + control.sendControl(PING, PING_DATA, 0, PING_DATA.length); + } catch (IOException e) { + log.warn("Unable to send ping message due to: ", e); + } + } + return false; + } + + @Override + public void onOpen(Connection connection) { + this.connection = connection; + this.control = (FrameConnection) connection; + try { + createHandlersAndOverlays(); + sendInstanceData(); + log.info("GUI client connected"); + + } catch (ServiceNotFoundException e) { + log.warn("Unable to open GUI connection; services have been shut-down", e); + this.connection.close(); + this.connection = null; + this.control = null; + } + } + + @Override + public synchronized void onClose(int closeCode, String message) { + destroyHandlersAndOverlays(); + log.info("GUI client disconnected [close-code={}, message={}]", + closeCode, message); + } + + @Override + public boolean onControl(byte controlCode, byte[] data, int offset, int length) { + lastActive = System.currentTimeMillis(); + return true; + } + + @Override + public void onMessage(String data) { + log.debug("onMessage: {}", data); + lastActive = System.currentTimeMillis(); + try { + ObjectNode message = (ObjectNode) mapper.reader().readTree(data); + String type = message.path("event").asText("unknown"); + UiMessageHandler handler = handlers.get(type); + if (handler != null) { + handler.process(message); + } else { + log.warn("No GUI message handler for type {}", type); + } + } catch (Exception e) { + log.warn("Unable to parse GUI message {} due to {}", data, e); + log.debug("Boom!!!", e); + } + } + + @Override + public synchronized void sendMessage(ObjectNode message) { + try { + if (connection.isOpen()) { + connection.sendMessage(message.toString()); + } + } catch (IOException e) { + log.warn("Unable to send message {} to GUI due to {}", message, e); + log.debug("Boom!!!", e); + } + } + + @Override + public synchronized void sendMessage(String type, long sid, ObjectNode payload) { + ObjectNode message = mapper.createObjectNode(); + message.put("event", type); + if (sid > 0) { + message.put("sid", sid); + } + message.set("payload", payload); + sendMessage(message); + + } + + // Creates new message handlers. + private synchronized void createHandlersAndOverlays() { + log.debug("creating handlers and overlays..."); + handlers = new HashMap<>(); + overlayCache = new TopoOverlayCache(); + + UiExtensionService service = directory.get(UiExtensionService.class); + service.getExtensions().forEach(ext -> { + UiMessageHandlerFactory factory = ext.messageHandlerFactory(); + if (factory != null) { + factory.newHandlers().forEach(handler -> { + handler.init(this, directory); + handler.messageTypes().forEach(type -> handlers.put(type, handler)); + + // need to inject the overlay cache into topology message handler + if (handler instanceof TopologyViewMessageHandler) { + ((TopologyViewMessageHandler) handler).setOverlayCache(overlayCache); + } + }); + } + + UiTopoOverlayFactory overlayFactory = ext.topoOverlayFactory(); + if (overlayFactory != null) { + overlayFactory.newOverlays().forEach(overlayCache::add); + } + }); + log.debug("#handlers = {}, #overlays = {}", handlers.size(), + overlayCache.size()); + } + + // Destroys message handlers. + private synchronized void destroyHandlersAndOverlays() { + log.debug("destroying handlers and overlays..."); + handlers.forEach((type, handler) -> handler.destroy()); + handlers.clear(); + + if (overlayCache != null) { + overlayCache.destroy(); + overlayCache = null; + } + } + + // Sends cluster node/instance information to allow GUI to fail-over. + private void sendInstanceData() { + ClusterService service = directory.get(ClusterService.class); + ArrayNode instances = mapper.createArrayNode(); + + for (ControllerNode node : service.getNodes()) { + ObjectNode instance = mapper.createObjectNode() + .put("id", node.id().toString()) + .put("ip", node.ip().toString()) + .put(TopoConstants.Glyphs.UI_ATTACHED, + node.equals(service.getLocalNode())); + instances.add(instance); + } + + ObjectNode payload = mapper.createObjectNode(); + payload.set("clusterNodes", instances); + sendMessage("bootstrap", 0, payload); + } + +} + diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java new file mode 100644 index 00000000..ffc558da --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java @@ -0,0 +1,95 @@ +/* + * 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.ui.impl; + +import org.eclipse.jetty.websocket.WebSocket; +import org.eclipse.jetty.websocket.WebSocketServlet; +import org.onlab.osgi.DefaultServiceDirectory; +import org.onlab.osgi.ServiceDirectory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; + +/** + * Web socket servlet capable of creating web sockets for the user interface. + */ +public class UiWebSocketServlet extends WebSocketServlet { + + private static final long PING_DELAY_MS = 5000; + + private static UiWebSocketServlet instance; + + private ServiceDirectory directory = new DefaultServiceDirectory(); + + private final Set<UiWebSocket> sockets = new HashSet<>(); + private final Timer timer = new Timer(); + private final TimerTask pruner = new Pruner(); + private boolean isStopped = false; + + /** + * Closes all currently open UI web-sockets. + */ + public static void closeAll() { + if (instance != null) { + instance.isStopped = true; + instance.sockets.forEach(UiWebSocket::close); + instance.sockets.clear(); + instance.pruner.cancel(); + instance.timer.cancel(); + } + } + + @Override + public void init() throws ServletException { + super.init(); + instance = this; + timer.schedule(pruner, PING_DELAY_MS, PING_DELAY_MS); + } + + @Override + public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { + if (isStopped) { + return null; + } + UiWebSocket socket = new UiWebSocket(directory); + synchronized (sockets) { + sockets.add(socket); + } + return socket; + } + + // Task for pruning web-sockets that are idle. + private class Pruner extends TimerTask { + @Override + public void run() { + synchronized (sockets) { + Iterator<UiWebSocket> it = sockets.iterator(); + while (it.hasNext()) { + UiWebSocket socket = it.next(); + if (socket.isIdle()) { + it.remove(); + socket.close(); + } + } + } + } + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/package-info.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/package-info.java new file mode 100644 index 00000000..0f228039 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Set of resources providing data for the ONOS GUI. + */ +package org.onosproject.ui.impl; diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/IntentSelection.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/IntentSelection.java new file mode 100644 index 00000000..151e6131 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/IntentSelection.java @@ -0,0 +1,175 @@ +/* + * 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.ui.impl.topo; + +import org.onosproject.net.intent.Intent; +import org.onosproject.ui.topo.NodeSelection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Encapsulates a selection of intents (paths) inferred from a selection + * of devices and/or hosts from the topology view. + */ +public class IntentSelection { + + private static final int ALL = -1; + + protected static final Logger log = + LoggerFactory.getLogger(IntentSelection.class); + + private final NodeSelection nodes; + + private final List<Intent> intents; + private int index = ALL; + + /** + * Creates an intent selection group, based on selected nodes. + * + * @param nodes node selection + * @param filter intent filter + */ + public IntentSelection(NodeSelection nodes, TopoIntentFilter filter) { + this.nodes = nodes; + intents = filter.findPathIntents(nodes.hostsWithHover(), nodes.devicesWithHover()); + if (intents.size() == 1) { + index = 0; // pre-select a single intent + } + } + + /** + * Creates an intent selection group, for a single intent. + * + * @param intent the intent + */ + public IntentSelection(Intent intent) { + nodes = null; + intents = new ArrayList<>(1); + intents.add(intent); + index = 0; + } + + /** + * Returns true if no intents are selected. + * + * @return true if nothing selected + */ + public boolean none() { + return intents.isEmpty(); + } + + /** + * Returns true if all intents in this select group are currently selected. + * This is the initial state, so that all intents are shown on the + * topology view with primary highlighting. + * + * @return true if all selected + */ + public boolean all() { + return index == ALL; + } + + /** + * Returns true if there is a single intent in this select group, or if + * a specific intent has been marked (index != ALL). + * + * @return true if single intent marked + */ + public boolean single() { + return !all(); + } + + /** + * Returns the number of intents in this selection group. + * + * @return number of intents + */ + public int size() { + return intents.size(); + } + + /** + * Returns the index of the currently selected intent. + * + * @return the current index + */ + public int index() { + return index; + } + + /** + * The list of intents in this selection group. + * + * @return list of intents + */ + public List<Intent> intents() { + return Collections.unmodifiableList(intents); + } + + /** + * Marks and returns the next intent in this group. Note that the + * selection wraps around to the beginning again, if necessary. + * + * @return the next intent in the group + */ + public Intent next() { + index += 1; + if (index >= intents.size()) { + index = 0; + } + return intents.get(index); + } + + /** + * Marks and returns the previous intent in this group. Note that the + * selection wraps around to the end again, if necessary. + * + * @return the previous intent in the group + */ + public Intent prev() { + index -= 1; + if (index < 0) { + index = intents.size() - 1; + } + return intents.get(index); + } + + /** + * Returns the currently marked intent, or null if "all" intents + * are marked. + * + * @return the currently marked intent + */ + public Intent current() { + return all() ? null : intents.get(index); + } + + @Override + public String toString() { + return "IntentSelection{" + + "nodes=" + nodes + + ", #intents=" + intents.size() + + ", index=" + index + + '}'; + } + +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/ServicesBundle.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/ServicesBundle.java new file mode 100644 index 00000000..bcc4ad8b --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/ServicesBundle.java @@ -0,0 +1,132 @@ +/* + * 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.ui.impl.topo; + +import org.onosproject.incubator.net.PortStatisticsService; +import org.onosproject.net.device.DeviceService; +import org.onosproject.net.flow.FlowRuleService; +import org.onosproject.net.host.HostService; +import org.onosproject.net.intent.IntentService; +import org.onosproject.net.link.LinkService; +import org.onosproject.net.statistic.StatisticService; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A bundle of services that the topology view requires to get its job done. + */ +public class ServicesBundle { + + private final IntentService intentService; + private final DeviceService deviceService; + private final HostService hostService; + private final LinkService linkService; + private final FlowRuleService flowService; + private final StatisticService flowStatsService; + private final PortStatisticsService portStatsService; + + /** + * Creates the services bundle. + * + * @param intentService intent service reference + * @param deviceService device service reference + * @param hostService host service reference + * @param linkService link service reference + * @param flowService flow service reference + * @param flowStatsService flow statistics service reference + * @param portStatsService port statistics service reference + */ + public ServicesBundle(IntentService intentService, + DeviceService deviceService, + HostService hostService, + LinkService linkService, + FlowRuleService flowService, + StatisticService flowStatsService, + PortStatisticsService portStatsService) { + this.intentService = checkNotNull(intentService); + this.deviceService = checkNotNull(deviceService); + this.hostService = checkNotNull(hostService); + this.linkService = checkNotNull(linkService); + this.flowService = checkNotNull(flowService); + this.flowStatsService = checkNotNull(flowStatsService); + this.portStatsService = checkNotNull(portStatsService); + } + + /** + * Returns a reference to the intent service. + * + * @return intent service reference + */ + public IntentService intentService() { + return intentService; + } + + /** + * Returns a reference to the device service. + * + * @return device service reference + */ + public DeviceService deviceService() { + return deviceService; + } + + /** + * Returns a reference to the host service. + * + * @return host service reference + */ + public HostService hostService() { + return hostService; + } + + /** + * Returns a reference to the link service. + * + * @return link service reference + */ + public LinkService linkService() { + return linkService; + } + + /** + * Returns a reference to the flow rule service. + * + * @return flow service reference + */ + public FlowRuleService flowService() { + return flowService; + } + + /** + * Returns a reference to the flow statistics service. + * + * @return flow statistics service reference + */ + public StatisticService flowStatsService() { + return flowStatsService; + } + + /** + * Returns a reference to the port statistics service. + * + * @return port statistics service reference + */ + public PortStatisticsService portStatsService() { + return portStatsService; + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoIntentFilter.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoIntentFilter.java new file mode 100644 index 00000000..8372dede --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TopoIntentFilter.java @@ -0,0 +1,274 @@ +/* + * 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.ui.impl.topo; + +import org.onosproject.net.ConnectPoint; +import org.onosproject.net.Device; +import org.onosproject.net.DeviceId; +import org.onosproject.net.Host; +import org.onosproject.net.HostId; +import org.onosproject.net.Link; +import org.onosproject.net.device.DeviceService; +import org.onosproject.net.flow.FlowRule; +import org.onosproject.net.host.HostService; +import org.onosproject.net.intent.FlowRuleIntent; +import org.onosproject.net.intent.HostToHostIntent; +import org.onosproject.net.intent.Intent; +import org.onosproject.net.intent.IntentService; +import org.onosproject.net.intent.LinkCollectionIntent; +import org.onosproject.net.intent.MultiPointToSinglePointIntent; +import org.onosproject.net.intent.OpticalConnectivityIntent; +import org.onosproject.net.intent.PathIntent; +import org.onosproject.net.intent.PointToPointIntent; +import org.onosproject.net.link.LinkService; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static org.onosproject.net.intent.IntentState.INSTALLED; + +/** + * Auxiliary facility to query the intent service based on the specified + * set of end-station hosts, edge points or infrastructure devices. + */ +public class TopoIntentFilter { + + private final IntentService intentService; + private final DeviceService deviceService; + private final HostService hostService; + private final LinkService linkService; + + /** + * Creates an intent filter. + * + * @param services service references bundle + */ + public TopoIntentFilter(ServicesBundle services) { + this.intentService = services.intentService(); + this.deviceService = services.deviceService(); + this.hostService = services.hostService(); + this.linkService = services.linkService(); + } + + /** + * Finds all path (host-to-host or point-to-point) intents that pertain + * to the given hosts and devices. + * + * @param hosts set of hosts to query by + * @param devices set of devices to query by + * @return set of intents that 'match' all hosts and devices given + */ + public List<Intent> findPathIntents(Set<Host> hosts, Set<Device> devices) { + // start with all intents + Iterable<Intent> sourceIntents = intentService.getIntents(); + + // Derive from this the set of edge connect points. + Set<ConnectPoint> edgePoints = getEdgePoints(hosts); + + // Iterate over all intents and produce a set that contains only those + // intents that target all selected hosts or derived edge connect points. + return getIntents(hosts, devices, edgePoints, sourceIntents); + } + + + // Produces a set of edge points from the specified set of hosts. + private Set<ConnectPoint> getEdgePoints(Set<Host> hosts) { + Set<ConnectPoint> edgePoints = new HashSet<>(); + for (Host host : hosts) { + edgePoints.add(host.location()); + } + return edgePoints; + } + + // Produces a list of intents that target all selected hosts, devices or connect points. + private List<Intent> getIntents(Set<Host> hosts, Set<Device> devices, + Set<ConnectPoint> edgePoints, + Iterable<Intent> sourceIntents) { + List<Intent> intents = new ArrayList<>(); + if (hosts.isEmpty() && devices.isEmpty()) { + return intents; + } + + Set<OpticalConnectivityIntent> opticalIntents = new HashSet<>(); + + // Search through all intents and see if they are relevant to our search. + for (Intent intent : sourceIntents) { + if (intentService.getIntentState(intent.key()) == INSTALLED) { + boolean isRelevant = false; + if (intent instanceof HostToHostIntent) { + isRelevant = isIntentRelevantToHosts((HostToHostIntent) intent, hosts) && + isIntentRelevantToDevices(intent, devices); + } else if (intent instanceof PointToPointIntent) { + isRelevant = isIntentRelevant((PointToPointIntent) intent, edgePoints) && + isIntentRelevantToDevices(intent, devices); + } else if (intent instanceof MultiPointToSinglePointIntent) { + isRelevant = isIntentRelevant((MultiPointToSinglePointIntent) intent, edgePoints) && + isIntentRelevantToDevices(intent, devices); + } else if (intent instanceof OpticalConnectivityIntent) { + opticalIntents.add((OpticalConnectivityIntent) intent); + } + // TODO: add other intents, e.g. SinglePointToMultiPointIntent + + if (isRelevant) { + intents.add(intent); + } + } + } + + // As a second pass, try to link up any optical intents with the + // packet-level ones. + for (OpticalConnectivityIntent intent : opticalIntents) { + if (isIntentRelevant(intent, intents) && + isIntentRelevantToDevices(intent, devices)) { + intents.add(intent); + } + } + return intents; + } + + // Indicates whether the specified intent involves all of the given hosts. + private boolean isIntentRelevantToHosts(HostToHostIntent intent, Iterable<Host> hosts) { + for (Host host : hosts) { + HostId id = host.id(); + // Bail if intent does not involve this host. + if (!id.equals(intent.one()) && !id.equals(intent.two())) { + return false; + } + } + return true; + } + + // Indicates whether the specified intent involves all of the given devices. + private boolean isIntentRelevantToDevices(Intent intent, Iterable<Device> devices) { + List<Intent> installables = intentService.getInstallableIntents(intent.key()); + for (Device device : devices) { + if (!isIntentRelevantToDevice(installables, device)) { + return false; + } + } + return true; + } + + // Indicates whether the specified intent involves the given device. + private boolean isIntentRelevantToDevice(List<Intent> installables, Device device) { + if (installables != null) { + for (Intent installable : installables) { + if (installable instanceof PathIntent) { + PathIntent pathIntent = (PathIntent) installable; + if (pathContainsDevice(pathIntent.path().links(), device.id())) { + return true; + } + } else if (installable instanceof FlowRuleIntent) { + FlowRuleIntent flowRuleIntent = (FlowRuleIntent) installable; + if (rulesContainDevice(flowRuleIntent.flowRules(), device.id())) { + return true; + } + } else if (installable instanceof LinkCollectionIntent) { + LinkCollectionIntent linksIntent = (LinkCollectionIntent) installable; + if (pathContainsDevice(linksIntent.links(), device.id())) { + return true; + } + } + } + } + return false; + } + + // Indicates whether the specified links involve the given device. + private boolean pathContainsDevice(Iterable<Link> links, DeviceId id) { + for (Link link : links) { + if (link.src().elementId().equals(id) || link.dst().elementId().equals(id)) { + return true; + } + } + return false; + } + + // Indicates whether the specified flow rules involvesthe given device. + private boolean rulesContainDevice(Collection<FlowRule> flowRules, DeviceId id) { + for (FlowRule rule : flowRules) { + if (rule.deviceId().equals(id)) { + return true; + } + } + return false; + } + + private boolean isIntentRelevant(PointToPointIntent intent, + Iterable<ConnectPoint> edgePoints) { + for (ConnectPoint point : edgePoints) { + // Bail if intent does not involve this edge point. + if (!point.equals(intent.egressPoint()) && + !point.equals(intent.ingressPoint())) { + return false; + } + } + return true; + } + + // Indicates whether the specified intent involves all of the given edge points. + private boolean isIntentRelevant(MultiPointToSinglePointIntent intent, + Iterable<ConnectPoint> edgePoints) { + for (ConnectPoint point : edgePoints) { + // Bail if intent does not involve this edge point. + if (!point.equals(intent.egressPoint()) && + !intent.ingressPoints().contains(point)) { + return false; + } + } + return true; + } + + // Indicates whether the specified intent involves all of the given edge points. + private boolean isIntentRelevant(OpticalConnectivityIntent opticalIntent, + Iterable<Intent> intents) { + Link ccSrc = getFirstLink(opticalIntent.getSrc(), false); + Link ccDst = getFirstLink(opticalIntent.getDst(), true); + if (ccSrc == null || ccDst == null) { + return false; + } + + for (Intent intent : intents) { + List<Intent> installables = intentService.getInstallableIntents(intent.key()); + for (Intent installable : installables) { + if (installable instanceof PathIntent) { + List<Link> links = ((PathIntent) installable).path().links(); + if (links.size() == 3) { + Link tunnel = links.get(1); + if (Objects.equals(tunnel.src(), ccSrc.src()) && + Objects.equals(tunnel.dst(), ccDst.dst())) { + return true; + } + } + } + } + } + return false; + } + + private Link getFirstLink(ConnectPoint point, boolean ingress) { + for (Link link : linkService.getLinks(point)) { + if (point.equals(ingress ? link.src() : link.dst())) { + return link; + } + } + return null; + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficLink.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficLink.java new file mode 100644 index 00000000..a0e16620 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficLink.java @@ -0,0 +1,223 @@ +/* + * 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.ui.impl.topo; + +import org.onosproject.net.Link; +import org.onosproject.net.LinkKey; +import org.onosproject.net.statistic.Load; +import org.onosproject.ui.topo.BiLink; +import org.onosproject.ui.topo.LinkHighlight; +import org.onosproject.ui.topo.LinkHighlight.Flavor; +import org.onosproject.ui.topo.TopoUtils; + +import static org.onosproject.ui.topo.LinkHighlight.Flavor.NO_HIGHLIGHT; +import static org.onosproject.ui.topo.LinkHighlight.Flavor.PRIMARY_HIGHLIGHT; +import static org.onosproject.ui.topo.LinkHighlight.Flavor.SECONDARY_HIGHLIGHT; + +/** + * Representation of a link and its inverse, and associated traffic data. + * This class understands how to generate the appropriate + * {@link LinkHighlight}s for showing traffic data on the topology view. + */ +public class TrafficLink extends BiLink { + + private static final String EMPTY = ""; + private static final String QUE = "?"; + + private long bytes = 0; + private long rate = 0; + private long flows = 0; + private Flavor taggedFlavor = NO_HIGHLIGHT; + private boolean hasTraffic = false; + private boolean isOptical = false; + private boolean antMarch = false; + + /** + * Constructs a traffic link for the given key and initial link. + * + * @param key canonical key for this traffic link + * @param link first link + */ + public TrafficLink(LinkKey key, Link link) { + super(key, link); + } + + /** + * Sets the optical flag to the given value. + * + * @param b true if an optical link + * @return self, for chaining + */ + public TrafficLink optical(boolean b) { + isOptical = b; + return this; + } + + /** + * Sets the ant march flag to the given value. + * + * @param b true if marching ants required + * @return self, for chaining + */ + public TrafficLink antMarch(boolean b) { + antMarch = b; + return this; + } + + /** + * Tags this traffic link with the flavor to be used in visual rendering. + * + * @param flavor the flavor to tag + * @return self, for chaining + */ + public TrafficLink tagFlavor(Flavor flavor) { + this.taggedFlavor = flavor; + return this; + } + + /** + * Adds load statistics, marks the traffic link as having traffic. + * + * @param load load to add + */ + public void addLoad(Load load) { + addLoad(load, 0); + } + + /** + * Adds load statistics, marks the traffic link as having traffic, if the + * load {@link Load#rate rate} is greater than the given threshold + * (expressed in bytes per second). + * + * @param load load to add + * @param threshold threshold to register traffic + */ + public void addLoad(Load load, double threshold) { + if (load != null) { + this.hasTraffic = hasTraffic || load.rate() > threshold; + this.bytes += load.latest(); + this.rate += load.rate(); + } + } + + /** + * Adds the given count of flows to this traffic link. + * + * @param count count of flows + */ + public void addFlows(int count) { + this.flows += count; + } + + @Override + public LinkHighlight highlight(Enum<?> type) { + StatsType statsType = (StatsType) type; + switch (statsType) { + case FLOW_COUNT: + return highlightForFlowCount(statsType); + + case FLOW_STATS: + case PORT_STATS: + return highlightForStats(statsType); + + case TAGGED: + return highlightForTagging(statsType); + + default: + throw new IllegalStateException("unexpected case: " + statsType); + } + } + + private LinkHighlight highlightForStats(StatsType type) { + return new LinkHighlight(linkId(), SECONDARY_HIGHLIGHT) + .setLabel(generateLabel(type)); + } + + private LinkHighlight highlightForFlowCount(StatsType type) { + Flavor flavor = flows > 0 ? PRIMARY_HIGHLIGHT : SECONDARY_HIGHLIGHT; + return new LinkHighlight(linkId(), flavor) + .setLabel(generateLabel(type)); + } + + private LinkHighlight highlightForTagging(StatsType type) { + LinkHighlight hlite = new LinkHighlight(linkId(), taggedFlavor) + .setLabel(generateLabel(type)); + if (isOptical) { + hlite.addMod(LinkHighlight.MOD_OPTICAL); + } + if (antMarch) { + hlite.addMod(LinkHighlight.MOD_ANIMATED); + } + return hlite; + } + + // Generates a string representation of the load, to be used as a label + private String generateLabel(StatsType type) { + switch (type) { + case FLOW_COUNT: + return TopoUtils.formatFlows(flows); + + case FLOW_STATS: + return TopoUtils.formatBytes(bytes); + + case PORT_STATS: + return TopoUtils.formatBitRate(rate); + + case TAGGED: + return hasTraffic ? TopoUtils.formatBytes(bytes) : EMPTY; + + default: + return QUE; + } + } + + /** + * Returns true if this link has been deemed to have enough traffic + * to register on the topology view in the web UI. + * + * @return true if this link has displayable traffic + */ + public boolean hasTraffic() { + return hasTraffic; + } + + /** + * Designates type of traffic statistics to report on a highlighted link. + */ + public enum StatsType { + /** + * Number of flows. + */ + FLOW_COUNT, + + /** + * Number of bytes. + */ + FLOW_STATS, + + /** + * Number of bits per second. + */ + PORT_STATS, + + /** + * Custom tagged information. + */ + TAGGED + } +} diff --git a/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficLinkMap.java b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficLinkMap.java new file mode 100644 index 00000000..081964f8 --- /dev/null +++ b/framework/src/onos/web/gui/src/main/java/org/onosproject/ui/impl/topo/TrafficLinkMap.java @@ -0,0 +1,33 @@ +/* + * 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.ui.impl.topo; + +import org.onosproject.net.Link; +import org.onosproject.net.LinkKey; +import org.onosproject.ui.topo.BiLinkMap; + +/** + * Collection of {@link TrafficLink}s. + */ +public class TrafficLinkMap extends BiLinkMap<TrafficLink> { + + @Override + public TrafficLink create(LinkKey key, Link link) { + return new TrafficLink(key, link); + } +} |