blob: 1773154c5e78ceb1da6dd506a2963bf110eea509 [file] [log] [blame]
Madan Jampaniad3c5262016-01-20 00:50:17 -08001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2016-present Open Networking Foundation
Madan Jampaniad3c5262016-01-20 00:50:17 -08003 *
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 */
16package org.onosproject.cluster.impl;
17
Madan Jampani898fcca2016-02-26 12:42:08 -080018import static org.onlab.util.Tools.groupedThreads;
Madan Jampaniad3c5262016-01-20 00:50:17 -080019import static org.slf4j.LoggerFactory.getLogger;
20
21import java.io.File;
Madan Jampanif172d402016-03-04 00:56:38 -080022import java.io.FileInputStream;
Madan Jampaniad3c5262016-01-20 00:50:17 -080023import java.io.IOException;
Jordan Haltermandbfff062017-06-19 10:31:32 -070024import java.net.HttpURLConnection;
Madan Jampanif172d402016-03-04 00:56:38 -080025import java.net.URL;
Madan Jampaniad3c5262016-01-20 00:50:17 -080026import java.util.Set;
Madan Jampanif172d402016-03-04 00:56:38 -080027import java.util.concurrent.ScheduledExecutorService;
28import java.util.concurrent.TimeUnit;
Madan Jampaniad3c5262016-01-20 00:50:17 -080029import java.util.concurrent.atomic.AtomicReference;
30
31import org.apache.felix.scr.annotations.Activate;
32import org.apache.felix.scr.annotations.Component;
33import org.apache.felix.scr.annotations.Deactivate;
34import org.apache.felix.scr.annotations.Reference;
35import org.apache.felix.scr.annotations.ReferenceCardinality;
36import org.onlab.packet.IpAddress;
37import org.onosproject.cluster.ClusterMetadata;
38import org.onosproject.cluster.ClusterMetadataProvider;
39import org.onosproject.cluster.ClusterMetadataProviderRegistry;
40import org.onosproject.cluster.ClusterMetadataProviderService;
41import org.onosproject.cluster.ControllerNode;
42import org.onosproject.cluster.DefaultControllerNode;
43import org.onosproject.cluster.DefaultPartition;
44import org.onosproject.cluster.NodeId;
45import org.onosproject.cluster.Partition;
46import org.onosproject.cluster.PartitionId;
47import org.onosproject.net.provider.ProviderId;
48import org.onosproject.store.service.Versioned;
49import org.slf4j.Logger;
50
51import com.fasterxml.jackson.core.JsonGenerator;
52import com.fasterxml.jackson.core.JsonParser;
53import com.fasterxml.jackson.core.JsonProcessingException;
54import com.fasterxml.jackson.databind.DeserializationContext;
55import com.fasterxml.jackson.databind.JsonDeserializer;
56import com.fasterxml.jackson.databind.JsonNode;
57import com.fasterxml.jackson.databind.JsonSerializer;
58import com.fasterxml.jackson.databind.ObjectMapper;
59import com.fasterxml.jackson.databind.SerializerProvider;
60import com.fasterxml.jackson.databind.module.SimpleModule;
61import com.google.common.base.Throwables;
62import com.google.common.collect.Sets;
63import com.google.common.io.Files;
64
65import static com.google.common.base.Preconditions.checkState;
Yuta HIGUCHI1624df12016-07-21 16:54:33 -070066import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
Madan Jampaniad3c5262016-01-20 00:50:17 -080067
68/**
69 * Provider of {@link ClusterMetadata cluster metadata} sourced from a local config file.
70 */
71@Component(immediate = true)
72public class ConfigFileBasedClusterMetadataProvider implements ClusterMetadataProvider {
73
74 private final Logger log = getLogger(getClass());
75
76 // constants for filed names (used in serialization)
77 private static final String ID = "id";
78 private static final String PORT = "port";
79 private static final String IP = "ip";
80
Madan Jampani898fcca2016-02-26 12:42:08 -080081 private static final String CONFIG_DIR = "../config";
82 private static final String CONFIG_FILE_NAME = "cluster.json";
83 private static final File CONFIG_FILE = new File(CONFIG_DIR, CONFIG_FILE_NAME);
Madan Jampaniad3c5262016-01-20 00:50:17 -080084
85 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
86 protected ClusterMetadataProviderRegistry providerRegistry;
87
Madan Jampanif172d402016-03-04 00:56:38 -080088 private static final ProviderId PROVIDER_ID = new ProviderId("file", "none");
Madan Jampani898fcca2016-02-26 12:42:08 -080089 private final AtomicReference<Versioned<ClusterMetadata>> cachedMetadata = new AtomicReference<>();
Madan Jampanif172d402016-03-04 00:56:38 -080090 private final ScheduledExecutorService configFileChangeDetector =
Yuta HIGUCHI1624df12016-07-21 16:54:33 -070091 newSingleThreadScheduledExecutor(groupedThreads("onos/cluster/metadata/config-watcher", "", log));
Madan Jampaniad3c5262016-01-20 00:50:17 -080092
Madan Jampanif172d402016-03-04 00:56:38 -080093 private String metadataUrl;
Madan Jampaniad3c5262016-01-20 00:50:17 -080094 private ObjectMapper mapper;
95 private ClusterMetadataProviderService providerService;
96
97 @Activate
98 public void activate() {
99 mapper = new ObjectMapper();
100 SimpleModule module = new SimpleModule();
101 module.addSerializer(NodeId.class, new NodeIdSerializer());
102 module.addDeserializer(NodeId.class, new NodeIdDeserializer());
103 module.addSerializer(ControllerNode.class, new ControllerNodeSerializer());
104 module.addDeserializer(ControllerNode.class, new ControllerNodeDeserializer());
Jordan Halterman21a74da2017-11-13 21:07:08 -0800105 module.addSerializer(Partition.class, new PartitionSerializer());
Madan Jampaniad3c5262016-01-20 00:50:17 -0800106 module.addDeserializer(Partition.class, new PartitionDeserializer());
107 module.addSerializer(PartitionId.class, new PartitionIdSerializer());
108 module.addDeserializer(PartitionId.class, new PartitionIdDeserializer());
109 mapper.registerModule(module);
110 providerService = providerRegistry.register(this);
Madan Jampanif172d402016-03-04 00:56:38 -0800111 metadataUrl = System.getProperty("onos.cluster.metadata.uri", "file://" + CONFIG_DIR + "/" + CONFIG_FILE);
112 configFileChangeDetector.scheduleWithFixedDelay(() -> watchUrl(metadataUrl), 100, 500, TimeUnit.MILLISECONDS);
Madan Jampaniad3c5262016-01-20 00:50:17 -0800113 log.info("Started");
114 }
115
116 @Deactivate
117 public void deactivate() {
Madan Jampani898fcca2016-02-26 12:42:08 -0800118 configFileChangeDetector.shutdown();
Madan Jampaniad3c5262016-01-20 00:50:17 -0800119 providerRegistry.unregister(this);
120 log.info("Stopped");
121 }
122
123 @Override
124 public ProviderId id() {
125 return PROVIDER_ID;
126 }
127
128 @Override
129 public Versioned<ClusterMetadata> getClusterMetadata() {
130 checkState(isAvailable());
131 synchronized (this) {
132 if (cachedMetadata.get() == null) {
Jordan Haltermandbfff062017-06-19 10:31:32 -0700133 cachedMetadata.set(blockForMetadata(metadataUrl));
Madan Jampaniad3c5262016-01-20 00:50:17 -0800134 }
135 return cachedMetadata.get();
136 }
137 }
138
139 @Override
140 public void setClusterMetadata(ClusterMetadata metadata) {
141 try {
Ray Milkey83200882017-11-13 19:18:21 -0800142 File configFile = new File(metadataUrl.replaceFirst("file://", ""));
143 Files.createParentDirs(configFile);
144 mapper.writeValue(configFile, metadata);
145 cachedMetadata.set(fetchMetadata(metadataUrl));
146 providerService.clusterMetadataChanged(new Versioned<>(metadata, configFile.lastModified()));
Madan Jampaniad3c5262016-01-20 00:50:17 -0800147 } catch (IOException e) {
148 Throwables.propagate(e);
149 }
150 }
151
152 @Override
153 public void addActivePartitionMember(PartitionId partitionId, NodeId nodeId) {
154 throw new UnsupportedOperationException();
155 }
156
157 @Override
158 public void removeActivePartitionMember(PartitionId partitionId, NodeId nodeId) {
159 throw new UnsupportedOperationException();
160 }
161
162 @Override
163 public Set<NodeId> getActivePartitionMembers(PartitionId partitionId) {
164 throw new UnsupportedOperationException();
165 }
166
167 @Override
168 public boolean isAvailable() {
Madan Jampanif172d402016-03-04 00:56:38 -0800169 try {
170 URL url = new URL(metadataUrl);
Jon Halla3fcf672017-03-28 16:53:22 -0700171 if ("file".equals(url.getProtocol())) {
Madan Jampanif172d402016-03-04 00:56:38 -0800172 File file = new File(metadataUrl.replaceFirst("file://", ""));
173 return file.exists();
Madan Jampanif172d402016-03-04 00:56:38 -0800174 } else {
Jordan Halterman46c76902017-08-24 14:55:25 -0700175 // Return true for HTTP URLs since we allow blocking until HTTP servers come up
176 return "http".equals(url.getProtocol());
Madan Jampanif172d402016-03-04 00:56:38 -0800177 }
178 } catch (Exception e) {
Jon Halldb2cf752016-06-28 16:24:45 -0700179 log.warn("Exception accessing metadata file at {}:", metadataUrl, e);
Madan Jampanif172d402016-03-04 00:56:38 -0800180 return false;
181 }
Madan Jampaniad3c5262016-01-20 00:50:17 -0800182 }
183
Jordan Haltermandbfff062017-06-19 10:31:32 -0700184 private Versioned<ClusterMetadata> blockForMetadata(String metadataUrl) {
Ray Milkey3717e602018-02-01 13:49:47 -0800185 long iterations = 0;
Jordan Haltermandbfff062017-06-19 10:31:32 -0700186 for (;;) {
Jordan Halterman9ada34a2017-10-23 16:31:50 -0700187 try {
188 Versioned<ClusterMetadata> metadata = fetchMetadata(metadataUrl);
189 if (metadata != null) {
190 return metadata;
191 }
192 } catch (Exception e) {
193 log.warn("Exception attempting to access metadata file at {}: {}", metadataUrl, e);
Jordan Haltermandbfff062017-06-19 10:31:32 -0700194 }
195
196 try {
Ray Milkey961b19f2018-02-05 13:47:55 -0800197 Thread.sleep((int) Math.pow(2, iterations < 7 ? ++iterations : iterations) * 10L);
Jordan Haltermandbfff062017-06-19 10:31:32 -0700198 } catch (InterruptedException e) {
Ray Milkey5c7d4882018-02-05 14:50:39 -0800199 Thread.currentThread().interrupt();
Jordan Haltermandbfff062017-06-19 10:31:32 -0700200 throw Throwables.propagate(e);
201 }
202 }
203 }
204
Madan Jampanif172d402016-03-04 00:56:38 -0800205 private Versioned<ClusterMetadata> fetchMetadata(String metadataUrl) {
Madan Jampaniad3c5262016-01-20 00:50:17 -0800206 try {
Madan Jampanif172d402016-03-04 00:56:38 -0800207 URL url = new URL(metadataUrl);
208 ClusterMetadata metadata = null;
209 long version = 0;
Jon Halla3fcf672017-03-28 16:53:22 -0700210 if ("file".equals(url.getProtocol())) {
Madan Jampanif172d402016-03-04 00:56:38 -0800211 File file = new File(metadataUrl.replaceFirst("file://", ""));
212 version = file.lastModified();
213 metadata = mapper.readValue(new FileInputStream(file), ClusterMetadata.class);
Jon Halla3fcf672017-03-28 16:53:22 -0700214 } else if ("http".equals(url.getProtocol())) {
Jordan Haltermanda268372017-09-19 11:18:08 -0700215 try {
216 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
217 if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
218 log.warn("Could not reach metadata URL {}. Retrying...", url);
219 return null;
220 }
221 if (conn.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT) {
222 return null;
223 }
224 version = conn.getLastModified();
225 metadata = mapper.readValue(conn.getInputStream(), ClusterMetadata.class);
226 } catch (IOException e) {
Jordan Halterman46c76902017-08-24 14:55:25 -0700227 log.warn("Could not reach metadata URL {}. Retrying...", url);
228 return null;
229 }
Madan Jampanif172d402016-03-04 00:56:38 -0800230 }
Jordan Haltermandbfff062017-06-19 10:31:32 -0700231
zhiyong ke60e40002017-04-13 13:50:47 +0800232 if (null == metadata) {
233 log.warn("Metadata is null in the function fetchMetadata");
234 throw new NullPointerException();
235 }
Jordan Haltermandbfff062017-06-19 10:31:32 -0700236
237 // If the configured partitions are empty then return a null metadata to indicate that the configuration
238 // needs to be polled until the partitions are populated.
239 if (metadata.getPartitions().isEmpty() || metadata.getPartitions().stream()
240 .map(partition -> partition.getMembers().size())
241 .reduce(Math::min)
242 .orElse(0) == 0) {
243 return null;
244 }
Madan Jampanif172d402016-03-04 00:56:38 -0800245 return new Versioned<>(new ClusterMetadata(PROVIDER_ID,
246 metadata.getName(),
247 Sets.newHashSet(metadata.getNodes()),
248 Sets.newHashSet(metadata.getPartitions())),
249 version);
Madan Jampaniad3c5262016-01-20 00:50:17 -0800250 } catch (IOException e) {
Madan Jampanif172d402016-03-04 00:56:38 -0800251 throw Throwables.propagate(e);
Madan Jampaniad3c5262016-01-20 00:50:17 -0800252 }
Madan Jampaniad3c5262016-01-20 00:50:17 -0800253 }
254
Jordan Halterman21a74da2017-11-13 21:07:08 -0800255 private static class PartitionSerializer extends JsonSerializer<Partition> {
256 @Override
257 public void serialize(Partition partition, JsonGenerator jgen, SerializerProvider serializerProvider)
258 throws IOException, JsonProcessingException {
259 jgen.writeStartObject();
260 jgen.writeNumberField("id", partition.getId().asInt());
261 jgen.writeArrayFieldStart("members");
262 for (NodeId nodeId : partition.getMembers()) {
263 jgen.writeString(nodeId.id());
264 }
265 jgen.writeEndArray();
266 jgen.writeEndObject();
267 }
268 }
269
Madan Jampaniad3c5262016-01-20 00:50:17 -0800270 private static class PartitionDeserializer extends JsonDeserializer<Partition> {
271 @Override
272 public Partition deserialize(JsonParser jp, DeserializationContext ctxt)
273 throws IOException, JsonProcessingException {
274 return jp.readValueAs(DefaultPartition.class);
275 }
276 }
277
278 private static class PartitionIdSerializer extends JsonSerializer<PartitionId> {
279 @Override
280 public void serialize(PartitionId partitionId, JsonGenerator jgen, SerializerProvider provider)
281 throws IOException, JsonProcessingException {
282 jgen.writeNumber(partitionId.asInt());
283 }
284 }
285
286 private class PartitionIdDeserializer extends JsonDeserializer<PartitionId> {
287 @Override
288 public PartitionId deserialize(JsonParser jp, DeserializationContext ctxt)
289 throws IOException, JsonProcessingException {
290 JsonNode node = jp.getCodec().readTree(jp);
291 return new PartitionId(node.asInt());
292 }
293 }
294
295 private static class ControllerNodeSerializer extends JsonSerializer<ControllerNode> {
296 @Override
297 public void serialize(ControllerNode node, JsonGenerator jgen, SerializerProvider provider)
298 throws IOException, JsonProcessingException {
299 jgen.writeStartObject();
300 jgen.writeStringField(ID, node.id().toString());
301 jgen.writeStringField(IP, node.ip().toString());
302 jgen.writeNumberField(PORT, node.tcpPort());
303 jgen.writeEndObject();
304 }
305 }
306
307 private static class ControllerNodeDeserializer extends JsonDeserializer<ControllerNode> {
308 @Override
309 public ControllerNode deserialize(JsonParser jp, DeserializationContext ctxt)
310 throws IOException, JsonProcessingException {
311 JsonNode node = jp.getCodec().readTree(jp);
312 NodeId nodeId = new NodeId(node.get(ID).textValue());
313 IpAddress ip = IpAddress.valueOf(node.get(IP).textValue());
314 int port = node.get(PORT).asInt();
315 return new DefaultControllerNode(nodeId, ip, port);
316 }
317 }
318
319 private static class NodeIdSerializer extends JsonSerializer<NodeId> {
320 @Override
321 public void serialize(NodeId nodeId, JsonGenerator jgen, SerializerProvider provider)
322 throws IOException, JsonProcessingException {
323 jgen.writeString(nodeId.toString());
324 }
325 }
326
327 private class NodeIdDeserializer extends JsonDeserializer<NodeId> {
328 @Override
329 public NodeId deserialize(JsonParser jp, DeserializationContext ctxt)
330 throws IOException, JsonProcessingException {
331 JsonNode node = jp.getCodec().readTree(jp);
332 return new NodeId(node.asText());
333 }
334 }
Madan Jampani898fcca2016-02-26 12:42:08 -0800335
336 /**
Madan Jampanif172d402016-03-04 00:56:38 -0800337 * Monitors the metadata url for any updates and notifies providerService accordingly.
Madan Jampani898fcca2016-02-26 12:42:08 -0800338 */
Madan Jampanif172d402016-03-04 00:56:38 -0800339 private void watchUrl(String metadataUrl) {
Ray Milkeye3708c72017-11-13 13:10:46 -0800340 if (!isAvailable()) {
341 return;
342 }
Madan Jampanif172d402016-03-04 00:56:38 -0800343 // TODO: We are merely polling the url.
344 // This can be easily addressed for files. For http urls we need to move to a push style protocol.
dvaddire49add802017-08-23 09:45:49 +0530345 try {
346 Versioned<ClusterMetadata> latestMetadata = fetchMetadata(metadataUrl);
347 if (cachedMetadata.get() != null && latestMetadata != null
348 && cachedMetadata.get().version() < latestMetadata.version()) {
349 cachedMetadata.set(latestMetadata);
350 providerService.clusterMetadataChanged(latestMetadata);
351 }
352 } catch (Exception e) {
353 log.error("Unable to parse metadata : ", e);
Madan Jampani898fcca2016-02-26 12:42:08 -0800354 }
355 }
Madan Jampaniad3c5262016-01-20 00:50:17 -0800356}