Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 1 | /* |
Brian O'Connor | a09fe5b | 2017-08-03 21:12:30 -0700 | [diff] [blame] | 2 | * Copyright 2015-present Open Networking Foundation |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | package org.onosproject.cip; |
| 17 | |
| 18 | import com.google.common.io.ByteStreams; |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 19 | import org.onosproject.cfg.ComponentConfigService; |
| 20 | import org.onosproject.cluster.ClusterService; |
| 21 | import org.onosproject.cluster.LeadershipEvent; |
| 22 | import org.onosproject.cluster.LeadershipEventListener; |
| 23 | import org.onosproject.cluster.LeadershipService; |
| 24 | import org.onosproject.cluster.NodeId; |
| 25 | import org.osgi.service.component.ComponentContext; |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 26 | import org.osgi.service.component.annotations.Activate; |
| 27 | import org.osgi.service.component.annotations.Component; |
| 28 | import org.osgi.service.component.annotations.Deactivate; |
| 29 | import org.osgi.service.component.annotations.Modified; |
| 30 | import org.osgi.service.component.annotations.Reference; |
| 31 | import org.osgi.service.component.annotations.ReferenceCardinality; |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 32 | import org.slf4j.Logger; |
| 33 | import org.slf4j.LoggerFactory; |
| 34 | |
| 35 | import java.io.IOException; |
| 36 | import java.util.Dictionary; |
| 37 | import java.util.Objects; |
| 38 | import java.util.Properties; |
| 39 | |
| 40 | import static com.google.common.base.Strings.isNullOrEmpty; |
| 41 | import static org.onlab.util.Tools.get; |
Ray Milkey | 2069565 | 2018-10-31 10:32:34 -0700 | [diff] [blame] | 42 | import static org.onosproject.cip.OsgiPropertyConstants.ALIAS_ADAPTER; |
| 43 | import static org.onosproject.cip.OsgiPropertyConstants.ALIAS_ADAPTER_DEFAULT; |
| 44 | import static org.onosproject.cip.OsgiPropertyConstants.ALIAS_IP; |
| 45 | import static org.onosproject.cip.OsgiPropertyConstants.ALIAS_IP_DEFAULT; |
| 46 | import static org.onosproject.cip.OsgiPropertyConstants.ALIAS_MASK; |
| 47 | import static org.onosproject.cip.OsgiPropertyConstants.ALIAS_MASK_DEFAULT; |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 48 | |
| 49 | /** |
| 50 | * Manages cluster IP address alias. |
| 51 | * |
| 52 | * To use the application, simply install it on ONOS and then configure it |
| 53 | * with the desired alias IP/mask/adapter configuration. |
| 54 | * |
| 55 | * If you are running it using upstart, you can also add the following |
| 56 | * command to the /opt/onos/options file: |
| 57 | * |
| 58 | * sudo ifconfig eth0:0 down # use the desired alias adapter |
| 59 | * |
| 60 | * This will make sure that if the process is killed abruptly, the IP alias |
| 61 | * will be dropped upon respawn. |
| 62 | */ |
Ray Milkey | 2069565 | 2018-10-31 10:32:34 -0700 | [diff] [blame] | 63 | @Component( |
| 64 | immediate = true, |
| 65 | property = { |
| 66 | ALIAS_IP + "=" + ALIAS_IP_DEFAULT, |
| 67 | ALIAS_MASK + "=" + ALIAS_MASK_DEFAULT, |
| 68 | ALIAS_ADAPTER + "=" + ALIAS_ADAPTER_DEFAULT |
| 69 | } |
| 70 | ) |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 71 | public class ClusterIpManager { |
| 72 | |
| 73 | private final Logger log = LoggerFactory.getLogger(getClass()); |
| 74 | |
| 75 | private static final String CLUSTER_IP = "cluster/ip"; |
| 76 | |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 77 | @Reference(cardinality = ReferenceCardinality.MANDATORY) |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 78 | protected ClusterService clusterService; |
| 79 | |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 80 | @Reference(cardinality = ReferenceCardinality.MANDATORY) |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 81 | protected LeadershipService leadershipService; |
| 82 | |
Ray Milkey | d84f89b | 2018-08-17 14:54:17 -0700 | [diff] [blame] | 83 | @Reference(cardinality = ReferenceCardinality.MANDATORY) |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 84 | protected ComponentConfigService cfgService; |
| 85 | |
| 86 | private final LeadershipEventListener listener = new InternalLeadershipListener(); |
| 87 | |
| 88 | private NodeId localId; |
| 89 | private boolean wasLeader = false; |
| 90 | |
Ray Milkey | 2069565 | 2018-10-31 10:32:34 -0700 | [diff] [blame] | 91 | /** Alias IP address. */ |
| 92 | private String aliasIp = ALIAS_IP_DEFAULT; |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 93 | |
Ray Milkey | 2069565 | 2018-10-31 10:32:34 -0700 | [diff] [blame] | 94 | /** Alias IP mask. */ |
| 95 | private String aliasMask = ALIAS_MASK_DEFAULT; |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 96 | |
Ray Milkey | 2069565 | 2018-10-31 10:32:34 -0700 | [diff] [blame] | 97 | /** Alias IP adapter. */ |
| 98 | private String aliasAdapter = ALIAS_ADAPTER_DEFAULT; |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 99 | |
| 100 | @Activate |
| 101 | protected void activate(ComponentContext context) { |
| 102 | cfgService.registerProperties(getClass()); |
| 103 | |
| 104 | localId = clusterService.getLocalNode().id(); |
Madan Jampani | 620f70d | 2016-01-30 22:22:47 -0800 | [diff] [blame] | 105 | processLeaderChange(leadershipService.getLeader(CLUSTER_IP)); |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 106 | |
| 107 | leadershipService.addListener(listener); |
| 108 | leadershipService.runForLeadership(CLUSTER_IP); |
| 109 | log.info("Started"); |
| 110 | } |
| 111 | |
| 112 | @Deactivate |
| 113 | protected void deactivate(ComponentContext context) { |
| 114 | cfgService.unregisterProperties(getClass(), false); |
| 115 | |
| 116 | removeIpAlias(aliasIp, aliasMask, aliasAdapter); |
| 117 | |
| 118 | leadershipService.removeListener(listener); |
| 119 | leadershipService.withdraw(CLUSTER_IP); |
| 120 | log.info("Stopped"); |
| 121 | } |
| 122 | |
| 123 | @Modified |
| 124 | protected void modified(ComponentContext context) { |
| 125 | log.info("Received configuration change..."); |
| 126 | Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties(); |
Ray Milkey | 2069565 | 2018-10-31 10:32:34 -0700 | [diff] [blame] | 127 | String newIp = get(properties, ALIAS_IP); |
| 128 | String newMask = get(properties, ALIAS_MASK); |
| 129 | String newAdapter = get(properties, ALIAS_ADAPTER); |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 130 | |
| 131 | // Process any changes in the parameters... |
| 132 | if (!Objects.equals(newIp, aliasIp) || |
| 133 | !Objects.equals(newMask, aliasMask) || |
| 134 | !Objects.equals(newAdapter, aliasAdapter)) { |
| 135 | synchronized (this) { |
| 136 | log.info("Reconfiguring with aliasIp={}, aliasMask={}, aliasAdapter={}, wasLeader={}", |
| 137 | newIp, newMask, newAdapter, wasLeader); |
| 138 | if (wasLeader) { |
| 139 | removeIpAlias(aliasIp, aliasMask, aliasAdapter); |
| 140 | addIpAlias(newIp, newMask, newAdapter); |
| 141 | } |
| 142 | aliasIp = newIp; |
| 143 | aliasMask = newMask; |
| 144 | aliasAdapter = newAdapter; |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
Madan Jampani | 620f70d | 2016-01-30 22:22:47 -0800 | [diff] [blame] | 149 | private synchronized void processLeaderChange(NodeId newLeader) { |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 150 | boolean isLeader = Objects.equals(newLeader, localId); |
| 151 | log.info("Processing leadership change; wasLeader={}, isLeader={}", wasLeader, isLeader); |
| 152 | if (!wasLeader && isLeader) { |
| 153 | // Gaining leadership, so setup the IP alias |
| 154 | addIpAlias(aliasIp, aliasMask, aliasAdapter); |
| 155 | wasLeader = true; |
| 156 | } else if (wasLeader && !isLeader) { |
Ray Milkey | c108a6b | 2017-08-23 15:23:50 -0700 | [diff] [blame] | 157 | // Losing leadership, so drop the IP alias |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 158 | removeIpAlias(aliasIp, aliasMask, aliasAdapter); |
| 159 | wasLeader = false; |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | private synchronized void addIpAlias(String ip, String mask, String adapter) { |
| 164 | if (!isNullOrEmpty(ip) && !isNullOrEmpty(mask) && !isNullOrEmpty(adapter)) { |
| 165 | log.info("Adding IP alias {}/{} to {}", ip, mask, adapter); |
| 166 | execute("sudo ifconfig " + adapter + " " + ip + " netmask " + mask + " up", false); |
| 167 | execute("sudo /usr/sbin/arping -c 1 -I " + adapter + " " + ip, true); |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | private synchronized void removeIpAlias(String ip, String mask, String adapter) { |
| 172 | if (!isNullOrEmpty(ip) && !isNullOrEmpty(mask) && !isNullOrEmpty(adapter)) { |
| 173 | log.info("Removing IP alias from {}", adapter, false); |
| 174 | execute("sudo ifconfig " + adapter + " down", true); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | private void execute(String command, boolean ignoreCode) { |
| 179 | try { |
| 180 | log.info("Executing [{}]", command); |
| 181 | Process process = Runtime.getRuntime().exec(command); |
| 182 | byte[] output = ByteStreams.toByteArray(process.getInputStream()); |
| 183 | byte[] error = ByteStreams.toByteArray(process.getErrorStream()); |
| 184 | int code = process.waitFor(); |
| 185 | if (code != 0 && !ignoreCode) { |
| 186 | log.info("Command failed: status={}, output={}, error={}", |
| 187 | code, new String(output), new String(error)); |
| 188 | } |
| 189 | } catch (IOException e) { |
| 190 | log.error("Unable to execute command {}", command, e); |
| 191 | } catch (InterruptedException e) { |
| 192 | log.error("Interrupted executing command {}", command, e); |
Ray Milkey | 5c7d488 | 2018-02-05 14:50:39 -0800 | [diff] [blame] | 193 | Thread.currentThread().interrupt(); |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 194 | } |
| 195 | } |
| 196 | |
| 197 | // Listens for leadership changes. |
| 198 | private class InternalLeadershipListener implements LeadershipEventListener { |
Madan Jampani | 620f70d | 2016-01-30 22:22:47 -0800 | [diff] [blame] | 199 | |
| 200 | @Override |
| 201 | public boolean isRelevant(LeadershipEvent event) { |
| 202 | return CLUSTER_IP.equals(event.subject().topic()); |
| 203 | } |
| 204 | |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 205 | @Override |
| 206 | public void event(LeadershipEvent event) { |
Madan Jampani | 620f70d | 2016-01-30 22:22:47 -0800 | [diff] [blame] | 207 | processLeaderChange(event.subject().leaderNodeId()); |
Thomas Vachuska | c03df25 | 2015-08-26 23:37:13 -0700 | [diff] [blame] | 208 | } |
| 209 | } |
| 210 | |
| 211 | } |