/* * Copyright 2014-2015 Open Networking Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onosproject.rest.resources; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.onlab.packet.ChassisId; import org.onlab.packet.IpAddress; import org.onlab.packet.MacAddress; import org.onlab.packet.VlanId; import org.onlab.util.Frequency; import org.onosproject.net.AnnotationKeys; import org.onosproject.net.ChannelSpacing; import org.onosproject.net.ConnectPoint; import org.onosproject.net.DefaultAnnotations; import org.onosproject.net.Device; import org.onosproject.net.DeviceId; import org.onosproject.net.GridType; import org.onosproject.net.Host; import org.onosproject.net.HostId; import org.onosproject.net.HostLocation; import org.onosproject.net.Link; import org.onosproject.net.MastershipRole; import org.onosproject.net.OchPort; import org.onosproject.net.OchSignal; import org.onosproject.net.OduCltPort; import org.onosproject.net.OduSignalType; import org.onosproject.net.OmsPort; import org.onosproject.net.Port; import org.onosproject.net.PortNumber; import org.onosproject.net.SparseAnnotations; import org.onosproject.net.device.DefaultDeviceDescription; import org.onosproject.net.device.DefaultPortDescription; import org.onosproject.net.device.DeviceDescription; import org.onosproject.net.device.DeviceEvent; import org.onosproject.net.device.DeviceListener; import org.onosproject.net.device.DeviceProvider; import org.onosproject.net.device.DeviceProviderRegistry; import org.onosproject.net.device.DeviceProviderService; import org.onosproject.net.device.DeviceService; import org.onosproject.net.device.OchPortDescription; import org.onosproject.net.device.OduCltPortDescription; import org.onosproject.net.device.OmsPortDescription; import org.onosproject.net.device.PortDescription; import org.onosproject.net.host.DefaultHostDescription; import org.onosproject.net.host.HostProvider; import org.onosproject.net.host.HostProviderRegistry; import org.onosproject.net.host.HostProviderService; import org.onosproject.net.link.DefaultLinkDescription; import org.onosproject.net.link.LinkProvider; import org.onosproject.net.link.LinkProviderRegistry; import org.onosproject.net.link.LinkProviderService; import org.onosproject.net.provider.ProviderId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkNotNull; import static org.onosproject.net.DeviceId.deviceId; 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_AVAILABILITY_CHANGED; /** * Provider of devices and links parsed from a JSON configuration structure. */ class ConfigProvider implements DeviceProvider, LinkProvider, HostProvider { private final Logger log = LoggerFactory.getLogger(getClass()); private static final ProviderId PID = new ProviderId("cfg", "org.onosproject.rest", true); private static final String UNKNOWN = "unknown"; private static final Frequency CENTER = Frequency.ofTHz(193.1); // C-band has 4.4 THz (4,400 GHz) total bandwidth private static final Frequency TOTAL = Frequency.ofTHz(4.4); private CountDownLatch deviceLatch; private final JsonNode cfg; private final DeviceService deviceService; private final DeviceProviderRegistry deviceProviderRegistry; private final LinkProviderRegistry linkProviderRegistry; private final HostProviderRegistry hostProviderRegistry; private DeviceProviderService deviceProviderService; private LinkProviderService linkProviderService; private HostProviderService hostProviderService; private DeviceListener deviceEventCounter = new DeviceEventCounter(); private List connectPoints = Lists.newArrayList(); private Map descriptions = Maps.newHashMap(); /** * Creates a new configuration provider. * * @param cfg JSON configuration * @param deviceService device service * @param deviceProviderRegistry device provider registry * @param linkProviderRegistry link provider registry * @param hostProviderRegistry host provider registry */ ConfigProvider(JsonNode cfg, DeviceService deviceService, DeviceProviderRegistry deviceProviderRegistry, LinkProviderRegistry linkProviderRegistry, HostProviderRegistry hostProviderRegistry) { this.cfg = checkNotNull(cfg, "Configuration cannot be null"); this.deviceService = checkNotNull(deviceService, "Device service cannot be null"); this.deviceProviderRegistry = checkNotNull(deviceProviderRegistry, "Device provider registry cannot be null"); this.linkProviderRegistry = checkNotNull(linkProviderRegistry, "Link provider registry cannot be null"); this.hostProviderRegistry = checkNotNull(hostProviderRegistry, "Host provider registry cannot be null"); } /** * Parses the given JSON and provides links as configured. */ void parse() { try { register(); parseDevices(); parseLinks(); parseHosts(); addMissingPorts(); } finally { unregister(); } } private void register() { deviceProviderService = deviceProviderRegistry.register(this); linkProviderService = linkProviderRegistry.register(this); hostProviderService = hostProviderRegistry.register(this); } private void unregister() { deviceProviderRegistry.unregister(this); linkProviderRegistry.unregister(this); hostProviderRegistry.unregister(this); } // Parses the given JSON and provides devices. private void parseDevices() { try { JsonNode nodes = cfg.get("devices"); if (nodes != null) { prepareForDeviceEvents(nodes.size()); for (JsonNode node : nodes) { parseDevice(node); // FIXME: hack to make sure device attributes take // This will be fixed when GossipDeviceStore uses ECM parseDevice(node); } } } finally { waitForDeviceEvents(); } } // Parses the given node with device data and supplies the device. private void parseDevice(JsonNode node) { URI uri = URI.create(get(node, "uri")); Device.Type type = Device.Type.valueOf(get(node, "type", "SWITCH")); String mfr = get(node, "mfr", UNKNOWN); String hw = get(node, "hw", UNKNOWN); String sw = get(node, "sw", UNKNOWN); String serial = get(node, "serial", UNKNOWN); ChassisId cid = new ChassisId(get(node, "mac", "000000000000")); SparseAnnotations annotations = annotations(node.get("annotations")); DeviceDescription desc = new DefaultDeviceDescription(uri, type, mfr, hw, sw, serial, cid, annotations); DeviceId deviceId = deviceId(uri); deviceProviderService.deviceConnected(deviceId, desc); JsonNode ports = node.get("ports"); if (ports != null) { parsePorts(deviceId, ports); } } // Parses the given node with list of device ports. private void parsePorts(DeviceId deviceId, JsonNode nodes) { List ports = new ArrayList<>(); for (JsonNode node : nodes) { ports.add(parsePort(deviceId, node)); } deviceProviderService.updatePorts(deviceId, ports); } // Parses the given node with port information. private PortDescription parsePort(DeviceId deviceId, JsonNode node) { Port.Type type = Port.Type.valueOf(node.path("type").asText("COPPER")); // TL1-based ports have a name PortNumber port = null; if (node.has("name")) { for (Port p : deviceService.getPorts(deviceId)) { if (p.number().name().equals(node.get("name").asText())) { port = p.number(); break; } } } else { port = portNumber(node.path("port").asLong(0)); } if (port == null) { log.error("Cannot find port given in node {}", node); return null; } String portName = Strings.emptyToNull(port.name()); SparseAnnotations annotations = null; if (portName != null) { annotations = DefaultAnnotations.builder() .set(AnnotationKeys.PORT_NAME, portName).build(); } switch (type) { case COPPER: return new DefaultPortDescription(port, node.path("enabled").asBoolean(true), type, node.path("speed").asLong(1_000), annotations); case FIBER: // Currently, assume OMS when FIBER. Provide sane defaults. annotations = annotations(node.get("annotations")); return new OmsPortDescription(port, node.path("enabled").asBoolean(true), CENTER, CENTER.add(TOTAL), Frequency.ofGHz(100), annotations); case ODUCLT: annotations = annotations(node.get("annotations")); OduCltPort oduCltPort = (OduCltPort) deviceService.getPort(deviceId, port); return new OduCltPortDescription(port, node.path("enabled").asBoolean(true), oduCltPort.signalType(), annotations); case OCH: annotations = annotations(node.get("annotations")); OchPort ochPort = (OchPort) deviceService.getPort(deviceId, port); return new OchPortDescription(port, node.path("enabled").asBoolean(true), ochPort.signalType(), ochPort.isTunable(), ochPort.lambda(), annotations); case OMS: annotations = annotations(node.get("annotations")); OmsPort omsPort = (OmsPort) deviceService.getPort(deviceId, port); return new OmsPortDescription(port, node.path("enabled").asBoolean(true), omsPort.minFrequency(), omsPort.maxFrequency(), omsPort.grid(), annotations); default: log.warn("{}: Unsupported Port Type"); } return new DefaultPortDescription(port, node.path("enabled").asBoolean(true), type, node.path("speed").asLong(1_000), annotations); } // Parses the given JSON and provides links as configured. private void parseLinks() { JsonNode nodes = cfg.get("links"); if (nodes != null) { for (JsonNode node : nodes) { parseLink(node, false); if (!node.has("halfplex")) { parseLink(node, true); } } } } // Parses the given node with link data and supplies the link. private void parseLink(JsonNode node, boolean reverse) { ConnectPoint src = connectPoint(get(node, "src")); ConnectPoint dst = connectPoint(get(node, "dst")); Link.Type type = Link.Type.valueOf(get(node, "type", "DIRECT")); SparseAnnotations annotations = annotations(node.get("annotations")); // take annotations to update optical ports with correct attributes. updatePorts(src, dst, annotations); DefaultLinkDescription desc = reverse ? new DefaultLinkDescription(dst, src, type, annotations) : new DefaultLinkDescription(src, dst, type, annotations); linkProviderService.linkDetected(desc); connectPoints.add(src); connectPoints.add(dst); } private void updatePorts(ConnectPoint src, ConnectPoint dst, SparseAnnotations annotations) { final String linkType = annotations.value("optical.type"); if ("cross-connect".equals(linkType)) { String value = annotations.value("bandwidth").trim(); try { double bw = Double.parseDouble(value); updateOchPort(bw, src, dst); } catch (NumberFormatException e) { log.warn("Invalid bandwidth ({}), can't configure port(s)", value); return; } } else if ("WDM".equals(linkType)) { String value = annotations.value("optical.waves").trim(); try { int numChls = Integer.parseInt(value); updateOmsPorts(numChls, src, dst); } catch (NumberFormatException e) { log.warn("Invalid channel ({}), can't configure port(s)", value); return; } } } // uses 'bandwidth' annotation to determine the channel spacing. private void updateOchPort(double bw, ConnectPoint srcCp, ConnectPoint dstCp) { Device src = deviceService.getDevice(srcCp.deviceId()); Device dst = deviceService.getDevice(dstCp.deviceId()); // bandwidth in MHz (assuming Hz - linc is not clear if that or Mb). Frequency spacing = Frequency.ofMHz(bw); // channel bandwidth is smaller than smallest standard channel spacing. ChannelSpacing chsp = null; if (spacing.compareTo(ChannelSpacing.CHL_6P25GHZ.frequency()) <= 0) { chsp = ChannelSpacing.CHL_6P25GHZ; } for (int i = 1; i < ChannelSpacing.values().length; i++) { Frequency val = ChannelSpacing.values()[i].frequency(); // pick the next highest or equal channel interval. if (val.isLessThan(spacing)) { chsp = ChannelSpacing.values()[i - 1]; break; } } if (chsp == null) { log.warn("Invalid channel spacing ({}), can't configure port(s)", spacing); return; } OchSignal signal = new OchSignal(GridType.DWDM, chsp, 1, 1); if (src.type() == Device.Type.ROADM) { PortDescription portDesc = new OchPortDescription(srcCp.port(), true, OduSignalType.ODU4, true, signal); descriptions.put(srcCp, portDesc); deviceProviderService.portStatusChanged(srcCp.deviceId(), portDesc); } if (dst.type() == Device.Type.ROADM) { PortDescription portDesc = new OchPortDescription(dstCp.port(), true, OduSignalType.ODU4, true, signal); descriptions.put(dstCp, portDesc); deviceProviderService.portStatusChanged(dstCp.deviceId(), portDesc); } } private void updateOmsPorts(int numChls, ConnectPoint srcCp, ConnectPoint dstCp) { // round down to largest slot that allows numChl channels to fit into C band range ChannelSpacing chl = null; Frequency perChl = TOTAL.floorDivision(numChls); for (int i = 0; i < ChannelSpacing.values().length; i++) { Frequency val = ChannelSpacing.values()[i].frequency(); if (val.isLessThan(perChl)) { chl = ChannelSpacing.values()[i]; break; } } if (chl == null) { chl = ChannelSpacing.CHL_6P25GHZ; } // if true, there was less channels than can be tightly packed. Frequency grid = chl.frequency(); // say Linc's 1st slot starts at CENTER and goes up from there. Frequency min = CENTER.add(grid); Frequency max = CENTER.add(grid.multiply(numChls)); PortDescription srcPortDesc = new OmsPortDescription(srcCp.port(), true, min, max, grid); PortDescription dstPortDesc = new OmsPortDescription(dstCp.port(), true, min, max, grid); descriptions.put(srcCp, srcPortDesc); descriptions.put(dstCp, dstPortDesc); deviceProviderService.portStatusChanged(srcCp.deviceId(), srcPortDesc); deviceProviderService.portStatusChanged(dstCp.deviceId(), dstPortDesc); } // Parses the given JSON and provides hosts as configured. private void parseHosts() { try { JsonNode nodes = cfg.get("hosts"); if (nodes != null) { for (JsonNode node : nodes) { parseHost(node); // FIXME: hack to make sure host attributes take // This will be fixed when GossipHostStore uses ECM parseHost(node); } } } finally { hostProviderRegistry.unregister(this); } } // Parses the given node with host data and supplies the host. private void parseHost(JsonNode node) { MacAddress mac = MacAddress.valueOf(get(node, "mac")); VlanId vlanId = VlanId.vlanId((short) node.get("vlan").asInt(VlanId.UNTAGGED)); HostId hostId = HostId.hostId(mac, vlanId); SparseAnnotations annotations = annotations(node.get("annotations")); HostLocation location = new HostLocation(connectPoint(get(node, "location")), 0); String[] ipStrings = get(node, "ip", "").split(","); Set ips = new HashSet<>(); for (String ip : ipStrings) { ips.add(IpAddress.valueOf(ip.trim())); } DefaultHostDescription desc = new DefaultHostDescription(mac, vlanId, location, ips, annotations); hostProviderService.hostDetected(hostId, desc); connectPoints.add(location); } // Adds any missing device ports for configured links and host locations. private void addMissingPorts() { deviceService.getDevices().forEach(this::addMissingPorts); } // Adds any missing device ports. private void addMissingPorts(Device device) { try { List ports = deviceService.getPorts(device.id()); Set existing = ports.stream() .map(p -> new ConnectPoint(device.id(), p.number())) .collect(Collectors.toSet()); Set missing = connectPoints.stream() .filter(cp -> cp.deviceId().equals(device.id())) .filter(cp -> !existing.contains(cp)) .collect(Collectors.toSet()); if (!missing.isEmpty()) { List newPorts = Stream.concat( ports.stream().map(this::description), missing.stream().map(this::description) ).collect(Collectors.toList()); deviceProviderService.updatePorts(device.id(), newPorts); } } catch (IllegalArgumentException e) { log.warn("Error pushing ports: {}", e.getMessage()); } } // Creates a port description from the specified port. private PortDescription description(Port p) { switch (p.type()) { case OMS: OmsPort op = (OmsPort) p; return new OmsPortDescription( op.number(), op.isEnabled(), op.minFrequency(), op.maxFrequency(), op.grid()); case OCH: OchPort ochp = (OchPort) p; return new OchPortDescription( ochp.number(), ochp.isEnabled(), ochp.signalType(), ochp.isTunable(), ochp.lambda()); case ODUCLT: OduCltPort odup = (OduCltPort) p; return new OduCltPortDescription( odup.number(), odup.isEnabled(), odup.signalType()); default: return new DefaultPortDescription(p.number(), p.isEnabled(), p.type(), p.portSpeed()); } } // Creates a port description from the specified connection point if none created earlier. private PortDescription description(ConnectPoint cp) { PortDescription saved = descriptions.get(cp); if (saved != null) { return saved; } Port p = deviceService.getPort(cp.deviceId(), cp.port()); if (p == null) { return new DefaultPortDescription(cp.port(), true); } return description(p); } // Produces set of annotations from the given JSON node. private SparseAnnotations annotations(JsonNode node) { if (node == null) { return DefaultAnnotations.EMPTY; } DefaultAnnotations.Builder builder = DefaultAnnotations.builder(); Iterator it = node.fieldNames(); while (it.hasNext()) { String k = it.next(); builder.set(k, node.get(k).asText()); } return builder.build(); } // Produces a connection point from the specified uri/port text. private ConnectPoint connectPoint(String text) { int i = text.lastIndexOf("/"); String portName = text.substring(i + 1); DeviceId deviceId = deviceId(text.substring(0, i)); for (Port port : deviceService.getPorts(deviceId)) { PortNumber pn = port.number(); if (pn.name().equals(portName)) { return new ConnectPoint(deviceId, pn); } } long portNum; try { portNum = Long.parseLong(portName); } catch (NumberFormatException e) { portNum = 0; } return new ConnectPoint(deviceId, portNumber(portNum, portName)); } // Returns string form of the named property in the given JSON object. private String get(JsonNode node, String name) { return node.path(name).asText(); } // Returns string form of the named property in the given JSON object. private String get(JsonNode node, String name, String defaultValue) { return node.path(name).asText(defaultValue); } @Override public void roleChanged(DeviceId device, MastershipRole newRole) { deviceProviderService.receivedRoleReply(device, newRole, newRole); } @Override public void triggerProbe(DeviceId deviceId) { } @Override public void triggerProbe(Host host) { } @Override public ProviderId id() { return PID; } @Override public boolean isReachable(DeviceId device) { return true; } /** * Prepares to count device added/available/removed events. * * @param count number of events to count */ protected void prepareForDeviceEvents(int count) { deviceLatch = new CountDownLatch(count); deviceService.addListener(deviceEventCounter); } /** * Waits for all expected device added/available/removed events. */ protected void waitForDeviceEvents() { try { deviceLatch.await(2, TimeUnit.SECONDS); } catch (InterruptedException e) { log.warn("Device events did not arrive in time"); } deviceService.removeListener(deviceEventCounter); } // Counts down number of device added/available/removed events. private class DeviceEventCounter implements DeviceListener { @Override public void event(DeviceEvent event) { DeviceEvent.Type type = event.type(); if (type == DEVICE_ADDED || type == DEVICE_AVAILABILITY_CHANGED) { deviceLatch.countDown(); } } } }