blob: 1f4d09a965ee73f72da4ab5654ddf9c753ed6a3c [file] [log] [blame]
/*
* Copyright 2019-present Open Networking Foundation
*
* 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.k8snetworking.util;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ContainerPort;
import io.fabric8.kubernetes.api.model.Namespace;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.util.SubnetUtils;
import org.onlab.osgi.DefaultServiceDirectory;
import org.onlab.packet.IpAddress;
import org.onlab.packet.MacAddress;
import org.onlab.packet.TpPort;
import org.onosproject.cfg.ConfigProperty;
import org.onosproject.k8snetworking.api.DefaultK8sPort;
import org.onosproject.k8snetworking.api.K8sNamespaceService;
import org.onosproject.k8snetworking.api.K8sNetwork;
import org.onosproject.k8snetworking.api.K8sNetworkAdminService;
import org.onosproject.k8snetworking.api.K8sNetworkService;
import org.onosproject.k8snetworking.api.K8sPodService;
import org.onosproject.k8snetworking.api.K8sPort;
import org.onosproject.k8snetworking.api.K8sServiceService;
import org.onosproject.k8snode.api.K8sApiConfig;
import org.onosproject.k8snode.api.K8sApiConfigService;
import org.onosproject.k8snode.api.K8sHost;
import org.onosproject.k8snode.api.K8sHostService;
import org.onosproject.k8snode.api.K8sNode;
import org.onosproject.k8snode.api.K8sNodeService;
import org.onosproject.k8snode.api.K8sRouterBridge;
import org.onosproject.k8snode.api.K8sTunnelBridge;
import org.onosproject.net.DeviceId;
import org.onosproject.net.Port;
import org.onosproject.net.PortNumber;
import org.onosproject.net.device.DeviceService;
import org.onosproject.net.group.DefaultGroupKey;
import org.onosproject.net.group.GroupKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.onosproject.k8snetworking.api.Constants.DEFAULT_NAMESPACE_HASH;
import static org.onosproject.k8snetworking.api.Constants.NORMAL_PORT_NAME_PREFIX_CONTAINER;
import static org.onosproject.k8snetworking.api.Constants.NORMAL_PORT_PREFIX_LENGTH;
import static org.onosproject.k8snetworking.api.Constants.PT_PORT_NAME_PREFIX_CONTAINER;
import static org.onosproject.k8snetworking.api.Constants.PT_PORT_PREFIX_LENGTH;
import static org.onosproject.k8snetworking.api.K8sPort.State.INACTIVE;
import static org.onosproject.k8snode.api.K8sApiConfig.Mode.PASSTHROUGH;
import static org.onosproject.net.AnnotationKeys.PORT_NAME;
/**
* An utility that used in kubernetes networking app.
*/
public final class K8sNetworkingUtil {
private static final Logger log = LoggerFactory.getLogger(K8sNetworkingUtil.class);
private static final String COLON_SLASH = "://";
private static final String COLON = ":";
private static final String STR_ZERO = "0";
private static final String STR_ONE = "1";
private static final String STR_PADDING = "0000000000000000";
private static final int MASK_BEGIN_IDX = 0;
private static final int MASK_MAX_IDX = 16;
private static final int MASK_RADIX = 2;
private static final int PORT_RADIX = 16;
private static final String PORT_ID = "portId";
private static final String DEVICE_ID = "deviceId";
private static final String PORT_NUMBER = "portNumber";
private static final String IP_ADDRESS = "ipAddress";
private static final String MAC_ADDRESS = "macAddress";
private static final String NETWORK_ID = "networkId";
private K8sNetworkingUtil() {
}
/**
* Checks that whether the port is associated with container interface.
*
* @param portName port name
* @return true if the port is associated with container; false otherwise
*/
public static boolean isContainer(String portName) {
return portName != null && (portName.contains(NORMAL_PORT_NAME_PREFIX_CONTAINER) ||
portName.contains(PT_PORT_NAME_PREFIX_CONTAINER));
}
/**
* Checks that whether the compared ports exist in the source name.
*
* @param sourceName source port name
* @param comparedName port name to be compared
* @return true if the compared port name exists, false otherwise
*/
public static boolean existingContainerPortByName(String sourceName, String comparedName) {
if (comparedName == null) {
return false;
}
if (comparedName.contains(NORMAL_PORT_NAME_PREFIX_CONTAINER)) {
return sourceName.contains(comparedName.substring(NORMAL_PORT_PREFIX_LENGTH));
}
if (comparedName.contains(PT_PORT_NAME_PREFIX_CONTAINER)) {
return sourceName.contains(comparedName.substring(PT_PORT_PREFIX_LENGTH));
}
return false;
}
/**
* Checks that whether the compared ports exist in the source MAC address.
*
* @param sourceMac source port MAC address
* @param comparedMac MAC address of port to be compared
* @return true if the compared port MAC address exists, false otherwise
*/
public static boolean existingContainerPortByMac(String sourceMac, String comparedMac) {
if (comparedMac == null || sourceMac == null) {
return false;
}
String shortSourceMac = sourceMac.substring(3).toUpperCase();
String shortComparedMac = comparedMac.substring(3).toUpperCase();
return shortSourceMac.equals(shortComparedMac);
}
/**
* Returns the tunnel port number with specified net ID and kubernetes node.
*
* @param netId network ID
* @param netService network service
* @param node kubernetes node
* @return tunnel port number
*/
public static PortNumber tunnelPortNumByNetId(String netId,
K8sNetworkService netService,
K8sNode node) {
K8sNetwork.Type netType = netService.network(netId).type();
if (netType == null) {
return null;
}
return tunnelPortNumByNetType(netType, node);
}
/**
* Returns the tunnel port number with specified net type and kubernetes node.
*
* @param netType network type
* @param node kubernetes node
* @return tunnel port number
*/
public static PortNumber tunnelPortNumByNetType(K8sNetwork.Type netType,
K8sNode node) {
if (node.mode() == PASSTHROUGH) {
K8sHostService hostService =
DefaultServiceDirectory.getService(K8sHostService.class);
Port port = null;
for (K8sHost host : hostService.hosts()) {
if (host.nodeNames().contains(node.hostname())) {
for (K8sTunnelBridge bridge : host.tunBridges()) {
if (bridge.tunnelId() == node.segmentId()) {
String portName = netType.name().toLowerCase() +
"-" + node.segmentId();
port = port(bridge.deviceId(), portName);
}
}
}
}
if (port == null) {
return null;
} else {
return port.number();
}
} else {
switch (netType) {
case VXLAN:
return node.vxlanPortNum();
case GRE:
return node.grePortNum();
case GENEVE:
return node.genevePortNum();
default:
return null;
}
}
}
/**
* Obtains the port from the device with the given port name.
*
* @param deviceId device identifier
* @param portName port name
* @return port object
*/
public static Port port(DeviceId deviceId, String portName) {
DeviceService deviceService = DefaultServiceDirectory.getService(DeviceService.class);
return deviceService.getPorts(deviceId).stream()
.filter(p -> p.isEnabled() &&
Objects.equals(p.annotations().value(PORT_NAME), portName))
.findAny().orElse(null);
}
/**
* Obtains the property value with specified property key name.
*
* @param properties a collection of properties
* @param name key name
* @return mapping value
*/
public static String getPropertyValue(Set<ConfigProperty> properties,
String name) {
Optional<ConfigProperty> property =
properties.stream().filter(p -> p.name().equals(name)).findFirst();
return property.map(ConfigProperty::value).orElse(null);
}
/**
* Prints out the JSON string in pretty format.
*
* @param mapper Object mapper
* @param jsonString JSON string
* @return pretty formatted JSON string
*/
public static String prettyJson(ObjectMapper mapper, String jsonString) {
try {
Object jsonObject = mapper.readValue(jsonString, Object.class);
return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject);
} catch (JsonParseException e) {
log.debug("JsonParseException", e);
} catch (JsonMappingException e) {
log.debug("JsonMappingException", e);
} catch (JsonProcessingException e) {
log.debug("JsonProcessingException", e);
} catch (IOException e) {
log.debug("IOException", e);
}
return null;
}
/**
* Obtains valid IP addresses of the given subnet.
*
* @param cidr CIDR
* @return set of IP addresses
*/
public static Set<IpAddress> getSubnetIps(String cidr) {
SubnetUtils utils = new SubnetUtils(cidr);
utils.setInclusiveHostCount(false);
SubnetUtils.SubnetInfo info = utils.getInfo();
Set<String> allAddresses =
new HashSet<>(Arrays.asList(info.getAllAddresses()));
if (allAddresses.size() > 2) {
allAddresses.remove(info.getLowAddress());
allAddresses.remove(info.getHighAddress());
}
return allAddresses.stream()
.map(IpAddress::valueOf).collect(Collectors.toSet());
}
/**
* Obtains gateway IP address of the given subnet.
*
* @param cidr CIDR
* @return gateway IP address
*/
public static IpAddress getGatewayIp(String cidr) {
SubnetUtils utils = new SubnetUtils(cidr);
utils.setInclusiveHostCount(false);
SubnetUtils.SubnetInfo info = utils.getInfo();
return IpAddress.valueOf(info.getLowAddress());
}
/**
* Obtains flow group key from the given id.
*
* @param groupId flow group identifier
* @return flow group key
*/
public static GroupKey getGroupKey(int groupId) {
return new DefaultGroupKey((Integer.toString(groupId)).getBytes());
}
/**
* Generates endpoint URL by referring to scheme, ipAddress and port.
*
* @param scheme scheme
* @param ipAddress IP address
* @param port port number
* @return generated endpoint URL
*/
public static String endpoint(K8sApiConfig.Scheme scheme, IpAddress ipAddress, int port) {
StringBuilder endpoint = new StringBuilder();
String protocol = StringUtils.lowerCase(scheme.name());
endpoint.append(protocol);
endpoint.append(COLON_SLASH);
endpoint.append(ipAddress.toString());
endpoint.append(COLON);
endpoint.append(port);
return endpoint.toString();
}
/**
* Generates endpoint URL by referring to scheme, ipAddress and port.
*
* @param apiConfig kubernetes API config
* @return generated endpoint URL
*/
public static String endpoint(K8sApiConfig apiConfig) {
return endpoint(apiConfig.scheme(), apiConfig.ipAddress(), apiConfig.port());
}
/**
* Obtains workable kubernetes client.
*
* @param config kubernetes API config
* @return kubernetes client
*/
public static KubernetesClient k8sClient(K8sApiConfig config) {
if (config == null) {
log.warn("Kubernetes API server config is empty.");
return null;
}
String endpoint = endpoint(config);
ConfigBuilder configBuilder = new ConfigBuilder().withMasterUrl(endpoint);
if (config.scheme() == K8sApiConfig.Scheme.HTTPS) {
configBuilder.withTrustCerts(true)
.withOauthToken(config.token())
.withCaCertData(config.caCertData())
.withClientCertData(config.clientCertData())
.withClientKeyData(config.clientKeyData());
}
return new DefaultKubernetesClient(configBuilder.build());
}
/**
* Obtains workable kubernetes client.
*
* @param service kubernetes API service
* @return kubernetes client
*/
public static KubernetesClient k8sClient(K8sApiConfigService service) {
K8sApiConfig config =
service.apiConfigs().stream().findAny().orElse(null);
if (config == null) {
log.error("Failed to find valid kubernetes API configuration.");
return null;
}
KubernetesClient client = k8sClient(config);
if (client == null) {
log.error("Failed to connect to kubernetes API server.");
return null;
}
return client;
}
/**
* Obtains the kubernetes node IP and kubernetes network gateway IP map.
*
* @param nodeService kubernetes node service
* @param networkService kubernetes network service
* @return kubernetes node IP and kubernetes network gateway IP map
*/
public static Map<String, String> nodeIpGatewayIpMap(K8sNodeService nodeService,
K8sNetworkService networkService) {
Map<String, String> ipMap = Maps.newConcurrentMap();
nodeService.completeNodes().forEach(n -> {
K8sNetwork network = networkService.network(n.hostname());
if (network != null) {
ipMap.put(n.nodeIp().toString(), network.gatewayIp().toString());
}
});
return ipMap;
}
/**
* Returns a shifted IP address.
*
* @param ipAddress IP address to be shifted
* @param shiftPrefix A IP prefix used in shifted IP address
* @return shifted Ip address
*/
public static String shiftIpDomain(String ipAddress, String shiftPrefix) {
String origIpPrefix = ipAddress.split("\\.")[0] + "." + ipAddress.split("\\.")[1];
return StringUtils.replace(ipAddress, origIpPrefix, shiftPrefix);
}
/**
* Returns an unshifted IP address.
*
* @param ipAddress IP address to be unshifted
* @param ipPrefix IP prefix which to be used for unshifting
* @param cidr a POD network CIDR
* @return unshifted IP address
*/
public static String unshiftIpDomain(String ipAddress,
String ipPrefix,
String cidr) {
String origIpPrefix = cidr.split("\\.")[0] + "." + cidr.split("\\.")[1];
return StringUtils.replace(ipAddress, ipPrefix, origIpPrefix);
}
/**
* Returns the B class IP prefix of the given CIDR.
*
* @param cidr CIDR
* @return IP prefix
*/
public static String getBclassIpPrefixFromCidr(String cidr) {
if (cidr == null) {
return null;
}
return cidr.split("\\.")[0] + "." + cidr.split("\\.")[1];
}
/**
* Returns the A class IP prefix of the given CIDR.
*
* @param cidr CIDR
* @return IP prefix
*/
public static String getAclassIpPrefixFromCidr(String cidr) {
if (cidr == null) {
return null;
}
return cidr.split("\\.")[0];
}
/**
* Returns the map of port range.
*
* @param portMin minimum port number
* @param portMax maximum port number
* @return map of port range
*/
public static Map<TpPort, TpPort> buildPortRangeMatches(int portMin, int portMax) {
boolean processing = true;
int start = portMin;
Map<TpPort, TpPort> portMaskMap = Maps.newHashMap();
while (processing) {
String minStr = Integer.toBinaryString(start);
String binStrMinPadded = STR_PADDING.substring(minStr.length()) + minStr;
int mask = testMasks(binStrMinPadded, start, portMax);
int maskStart = binLower(binStrMinPadded, mask);
int maskEnd = binHigher(binStrMinPadded, mask);
log.debug("start : {} port/mask = {} / {} ", start, getMask(mask), maskStart);
portMaskMap.put(TpPort.tpPort(maskStart), TpPort.tpPort(
Integer.parseInt(Objects.requireNonNull(getMask(mask)), PORT_RADIX)));
start = maskEnd + 1;
if (start > portMax) {
processing = false;
}
}
return portMaskMap;
}
/**
* Returns the namespace hash value by given POD IP.
*
* @param k8sPodService kubernetes POD service
* @param k8sNamespaceService kubernetes namespace service
* @param podIp POD IP address
* @return namespace hash value
*/
public static Integer namespaceHashByPodIp(K8sPodService k8sPodService,
K8sNamespaceService k8sNamespaceService,
String podIp) {
String ns = k8sPodService.pods().stream()
.filter(pod -> pod.getStatus().getPodIP() != null)
.filter(pod -> pod.getStatus().getPodIP().equals(podIp))
.map(pod -> pod.getMetadata().getNamespace())
.findAny().orElse(null);
if (ns != null) {
return k8sNamespaceService.namespaces().stream()
.filter(n -> n.getMetadata().getName().equals(ns))
.map(Namespace::hashCode).findAny().orElse(null);
} else {
return null;
}
}
/**
* Returns the namespace hash value by given service IP.
*
* @param k8sServiceService kubernetes service service
* @param k8sNamespaceService kubernetes namespace service
* @param serviceIp service IP address
* @return namespace hash value
*/
public static int namespaceHashByServiceIp(K8sServiceService k8sServiceService,
K8sNamespaceService k8sNamespaceService,
String serviceIp) {
String ns = k8sServiceService.services().stream()
.filter(service -> service.getSpec().getClusterIP() != null)
.filter(service -> service.getSpec().getClusterIP().equalsIgnoreCase(serviceIp))
.map(service -> service.getMetadata().getNamespace())
.findAny().orElse(null);
if (ns != null) {
return namespaceHashByNamespace(k8sNamespaceService, ns);
} else {
return DEFAULT_NAMESPACE_HASH;
}
}
/**
* Returns the namespace hash value by given namespace name.
*
* @param k8sNamespaceService kubernetes namespace service
* @param ns namespace name
* @return namespace hash value
*/
public static int namespaceHashByNamespace(K8sNamespaceService k8sNamespaceService,
String ns) {
return k8sNamespaceService.namespaces().stream()
.filter(n -> n.getMetadata().getName() != null)
.filter(n -> n.getMetadata().getName().equalsIgnoreCase(ns))
.map(Namespace::hashCode).findAny().orElse(DEFAULT_NAMESPACE_HASH);
}
/**
* Returns POD instance by POD IP address.
*
* @param podService kubernetes POD service
* @param podIp POD IP address
* @return POD instance
*/
public static Pod podByIp(K8sPodService podService, String podIp) {
return podService.pods().stream()
.filter(pod -> pod.getStatus().getPodIP() != null)
.filter(pod -> pod.getStatus().getPodIP().equals(podIp))
.findAny().orElse(null);
}
/**
* Returns the container port number by given container port name.
*
* @param pod kubernetes POD
* @param portName port name
* @return container port number,
* return 0 if there is no port number mapped with the given port name
*/
public static int portNumberByName(Pod pod, String portName) {
if (pod == null || pod.getSpec() == null) {
return 0;
}
for (Container container : pod.getSpec().getContainers()) {
for (ContainerPort cp : container.getPorts()) {
if (cp.getName() != null && cp.getName().equals(portName)) {
return cp.getContainerPort();
}
}
}
return 0;
}
/**
* Synchronizes port from kubernetes POD.
*
* @param pod kubernetes POD
* @param adminService admin service
*/
public static void syncPortFromPod(Pod pod, K8sNetworkAdminService adminService) {
Map<String, String> annotations = pod.getMetadata().getAnnotations();
if (annotations != null && !annotations.isEmpty() &&
annotations.get(PORT_ID) != null) {
String portId = annotations.get(PORT_ID);
K8sPort oldPort = adminService.port(portId);
String networkId = annotations.get(NETWORK_ID);
DeviceId deviceId = DeviceId.deviceId(annotations.get(DEVICE_ID));
PortNumber portNumber = PortNumber.portNumber(annotations.get(PORT_NUMBER));
IpAddress ipAddress = IpAddress.valueOf(annotations.get(IP_ADDRESS));
MacAddress macAddress = MacAddress.valueOf(annotations.get(MAC_ADDRESS));
K8sPort newPort = DefaultK8sPort.builder()
.portId(portId)
.networkId(networkId)
.deviceId(deviceId)
.ipAddress(ipAddress)
.macAddress(macAddress)
.portNumber(portNumber)
.state(INACTIVE)
.build();
if (oldPort == null) {
adminService.createPort(newPort);
} else {
adminService.updatePort(newPort);
}
}
}
/**
* Generates string format based on the given string length list.
*
* @param stringLengths a list of string lengths
* @return string format (e.g., %-28s%-15s%-24s%-20s%-15s)
*/
public static String genFormatString(List<Integer> stringLengths) {
StringBuilder fsb = new StringBuilder();
stringLengths.forEach(length -> {
fsb.append("%-");
fsb.append(length);
fsb.append("s");
});
return fsb.toString();
}
/**
* Returns all device identifiers belong to kubernetes nodes and hosts.
*
* @param nodeService node service
* @param hostService host service
* @return all device identifiers belong to kubernetes nodes and hosts
*/
public static Set<DeviceId> allK8sDevices(K8sNodeService nodeService,
K8sHostService hostService) {
Set<DeviceId> allDevIds = new HashSet<>();
Set<DeviceId> intgDevIds = nodeService.completeNodes().stream()
.map(K8sNode::intgBridge).collect(Collectors.toSet());
Set<DeviceId> extDevIds = nodeService.completeNodes().stream()
.map(K8sNode::extBridge).collect(Collectors.toSet());
Set<DeviceId> tunDevIds = nodeService.completeNodes().stream()
.map(K8sNode::tunBridge).collect(Collectors.toSet());
Set<DeviceId> localDevIds = nodeService.completeNodes().stream()
.map(K8sNode::localBridge).collect(Collectors.toSet());
Set<DeviceId> hostTunDevIds = new HashSet<>();
Set<DeviceId> hostRouterDevIds = new HashSet<>();
for (K8sHost host : hostService.completeHosts()) {
Set<K8sTunnelBridge> hostTunBrs = host.tunBridges();
Set<K8sRouterBridge> hostRouterBrs = host.routerBridges();
hostTunDevIds.addAll(hostTunBrs.stream().map(K8sTunnelBridge::deviceId)
.collect(Collectors.toSet()));
hostRouterDevIds.addAll(hostRouterBrs.stream().map(K8sRouterBridge::deviceId)
.collect(Collectors.toSet()));
}
allDevIds.addAll(intgDevIds);
allDevIds.addAll(extDevIds);
allDevIds.addAll(tunDevIds);
allDevIds.addAll(localDevIds);
allDevIds.addAll(hostTunDevIds);
allDevIds.addAll(hostRouterDevIds);
return allDevIds;
}
private static int binLower(String binStr, int bits) {
StringBuilder outBin = new StringBuilder(
binStr.substring(MASK_BEGIN_IDX, MASK_MAX_IDX - bits));
for (int i = 0; i < bits; i++) {
outBin.append(STR_ZERO);
}
return Integer.parseInt(outBin.toString(), MASK_RADIX);
}
private static int binHigher(String binStr, int bits) {
StringBuilder outBin = new StringBuilder(
binStr.substring(MASK_BEGIN_IDX, MASK_MAX_IDX - bits));
for (int i = 0; i < bits; i++) {
outBin.append(STR_ONE);
}
return Integer.parseInt(outBin.toString(), MASK_RADIX);
}
private static int testMasks(String binStr, int start, int end) {
int mask = MASK_BEGIN_IDX;
for (; mask <= MASK_MAX_IDX; mask++) {
int maskStart = binLower(binStr, mask);
int maskEnd = binHigher(binStr, mask);
if (maskStart < start || maskEnd > end) {
return mask - 1;
}
}
return mask;
}
private static String getMask(int bits) {
switch (bits) {
case 0: return "ffff";
case 1: return "fffe";
case 2: return "fffc";
case 3: return "fff8";
case 4: return "fff0";
case 5: return "ffe0";
case 6: return "ffc0";
case 7: return "ff80";
case 8: return "ff00";
case 9: return "fe00";
case 10: return "fc00";
case 11: return "f800";
case 12: return "f000";
case 13: return "e000";
case 14: return "c000";
case 15: return "8000";
case 16: return "0000";
default: return null;
}
}
}