blob: c1b5de1a37632a548ccdb63d6e60cae44a5f1c20 [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
Ray Milkey6a51cb92018-03-06 09:03:03 -080018import com.fasterxml.jackson.core.JsonGenerator;
19import com.fasterxml.jackson.core.JsonParser;
20import com.fasterxml.jackson.core.JsonProcessingException;
21import com.fasterxml.jackson.databind.DeserializationContext;
22import com.fasterxml.jackson.databind.JsonDeserializer;
23import com.fasterxml.jackson.databind.JsonNode;
24import com.fasterxml.jackson.databind.JsonSerializer;
25import com.fasterxml.jackson.databind.ObjectMapper;
26import com.fasterxml.jackson.databind.SerializerProvider;
27import com.fasterxml.jackson.databind.module.SimpleModule;
28import com.google.common.collect.Sets;
29import com.google.common.io.Files;
Madan Jampaniad3c5262016-01-20 00:50:17 -080030import org.apache.felix.scr.annotations.Activate;
31import org.apache.felix.scr.annotations.Component;
32import org.apache.felix.scr.annotations.Deactivate;
33import org.apache.felix.scr.annotations.Reference;
34import org.apache.felix.scr.annotations.ReferenceCardinality;
35import org.onlab.packet.IpAddress;
36import org.onosproject.cluster.ClusterMetadata;
37import org.onosproject.cluster.ClusterMetadataProvider;
38import org.onosproject.cluster.ClusterMetadataProviderRegistry;
39import org.onosproject.cluster.ClusterMetadataProviderService;
40import org.onosproject.cluster.ControllerNode;
41import org.onosproject.cluster.DefaultControllerNode;
42import org.onosproject.cluster.DefaultPartition;
43import org.onosproject.cluster.NodeId;
44import org.onosproject.cluster.Partition;
45import org.onosproject.cluster.PartitionId;
46import org.onosproject.net.provider.ProviderId;
47import org.onosproject.store.service.Versioned;
48import org.slf4j.Logger;
49
Ray Milkey6a51cb92018-03-06 09:03:03 -080050import java.io.File;
51import java.io.FileInputStream;
52import java.io.IOException;
53import java.net.HttpURLConnection;
54import java.net.URL;
55import java.util.Set;
56import java.util.concurrent.ScheduledExecutorService;
57import java.util.concurrent.TimeUnit;
58import java.util.concurrent.atomic.AtomicReference;
Madan Jampaniad3c5262016-01-20 00:50:17 -080059
60import static com.google.common.base.Preconditions.checkState;
Yuta HIGUCHI1624df12016-07-21 16:54:33 -070061import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
Ray Milkey6a51cb92018-03-06 09:03:03 -080062import static org.onlab.util.Tools.groupedThreads;
63import static org.slf4j.LoggerFactory.getLogger;
Madan Jampaniad3c5262016-01-20 00:50:17 -080064
65/**
66 * Provider of {@link ClusterMetadata cluster metadata} sourced from a local config file.
67 */
68@Component(immediate = true)
69public class ConfigFileBasedClusterMetadataProvider implements ClusterMetadataProvider {
70
71 private final Logger log = getLogger(getClass());
72
73 // constants for filed names (used in serialization)
74 private static final String ID = "id";
75 private static final String PORT = "port";
76 private static final String IP = "ip";
77
Madan Jampani898fcca2016-02-26 12:42:08 -080078 private static final String CONFIG_DIR = "../config";
79 private static final String CONFIG_FILE_NAME = "cluster.json";
80 private static final File CONFIG_FILE = new File(CONFIG_DIR, CONFIG_FILE_NAME);
Madan Jampaniad3c5262016-01-20 00:50:17 -080081
82 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
83 protected ClusterMetadataProviderRegistry providerRegistry;
84
Madan Jampanif172d402016-03-04 00:56:38 -080085 private static final ProviderId PROVIDER_ID = new ProviderId("file", "none");
Madan Jampani898fcca2016-02-26 12:42:08 -080086 private final AtomicReference<Versioned<ClusterMetadata>> cachedMetadata = new AtomicReference<>();
Madan Jampanif172d402016-03-04 00:56:38 -080087 private final ScheduledExecutorService configFileChangeDetector =
Yuta HIGUCHI1624df12016-07-21 16:54:33 -070088 newSingleThreadScheduledExecutor(groupedThreads("onos/cluster/metadata/config-watcher", "", log));
Madan Jampaniad3c5262016-01-20 00:50:17 -080089
Madan Jampanif172d402016-03-04 00:56:38 -080090 private String metadataUrl;
Madan Jampaniad3c5262016-01-20 00:50:17 -080091 private ObjectMapper mapper;
92 private ClusterMetadataProviderService providerService;
93
94 @Activate
95 public void activate() {
96 mapper = new ObjectMapper();
97 SimpleModule module = new SimpleModule();
98 module.addSerializer(NodeId.class, new NodeIdSerializer());
99 module.addDeserializer(NodeId.class, new NodeIdDeserializer());
100 module.addSerializer(ControllerNode.class, new ControllerNodeSerializer());
101 module.addDeserializer(ControllerNode.class, new ControllerNodeDeserializer());
Jordan Halterman21a74da2017-11-13 21:07:08 -0800102 module.addSerializer(Partition.class, new PartitionSerializer());
Madan Jampaniad3c5262016-01-20 00:50:17 -0800103 module.addDeserializer(Partition.class, new PartitionDeserializer());
104 module.addSerializer(PartitionId.class, new PartitionIdSerializer());
105 module.addDeserializer(PartitionId.class, new PartitionIdDeserializer());
106 mapper.registerModule(module);
107 providerService = providerRegistry.register(this);
Madan Jampanif172d402016-03-04 00:56:38 -0800108 metadataUrl = System.getProperty("onos.cluster.metadata.uri", "file://" + CONFIG_DIR + "/" + CONFIG_FILE);
109 configFileChangeDetector.scheduleWithFixedDelay(() -> watchUrl(metadataUrl), 100, 500, TimeUnit.MILLISECONDS);
Madan Jampaniad3c5262016-01-20 00:50:17 -0800110 log.info("Started");
111 }
112
113 @Deactivate
114 public void deactivate() {
Madan Jampani898fcca2016-02-26 12:42:08 -0800115 configFileChangeDetector.shutdown();
Madan Jampaniad3c5262016-01-20 00:50:17 -0800116 providerRegistry.unregister(this);
117 log.info("Stopped");
118 }
119
120 @Override
121 public ProviderId id() {
122 return PROVIDER_ID;
123 }
124
125 @Override
126 public Versioned<ClusterMetadata> getClusterMetadata() {
127 checkState(isAvailable());
128 synchronized (this) {
129 if (cachedMetadata.get() == null) {
Jordan Haltermandbfff062017-06-19 10:31:32 -0700130 cachedMetadata.set(blockForMetadata(metadataUrl));
Madan Jampaniad3c5262016-01-20 00:50:17 -0800131 }
132 return cachedMetadata.get();
133 }
134 }
135
136 @Override
137 public void setClusterMetadata(ClusterMetadata metadata) {
138 try {
Ray Milkey83200882017-11-13 19:18:21 -0800139 File configFile = new File(metadataUrl.replaceFirst("file://", ""));
140 Files.createParentDirs(configFile);
141 mapper.writeValue(configFile, metadata);
142 cachedMetadata.set(fetchMetadata(metadataUrl));
143 providerService.clusterMetadataChanged(new Versioned<>(metadata, configFile.lastModified()));
Madan Jampaniad3c5262016-01-20 00:50:17 -0800144 } catch (IOException e) {
Ray Milkey6a51cb92018-03-06 09:03:03 -0800145 throw new IllegalArgumentException(e);
Madan Jampaniad3c5262016-01-20 00:50:17 -0800146 }
147 }
148
149 @Override
150 public void addActivePartitionMember(PartitionId partitionId, NodeId nodeId) {
151 throw new UnsupportedOperationException();
152 }
153
154 @Override
155 public void removeActivePartitionMember(PartitionId partitionId, NodeId nodeId) {
156 throw new UnsupportedOperationException();
157 }
158
159 @Override
160 public Set<NodeId> getActivePartitionMembers(PartitionId partitionId) {
161 throw new UnsupportedOperationException();
162 }
163
164 @Override
165 public boolean isAvailable() {
Madan Jampanif172d402016-03-04 00:56:38 -0800166 try {
167 URL url = new URL(metadataUrl);
Jon Halla3fcf672017-03-28 16:53:22 -0700168 if ("file".equals(url.getProtocol())) {
Madan Jampanif172d402016-03-04 00:56:38 -0800169 File file = new File(metadataUrl.replaceFirst("file://", ""));
170 return file.exists();
Madan Jampanif172d402016-03-04 00:56:38 -0800171 } else {
Jordan Halterman46c76902017-08-24 14:55:25 -0700172 // Return true for HTTP URLs since we allow blocking until HTTP servers come up
173 return "http".equals(url.getProtocol());
Madan Jampanif172d402016-03-04 00:56:38 -0800174 }
175 } catch (Exception e) {
Jon Halldb2cf752016-06-28 16:24:45 -0700176 log.warn("Exception accessing metadata file at {}:", metadataUrl, e);
Madan Jampanif172d402016-03-04 00:56:38 -0800177 return false;
178 }
Madan Jampaniad3c5262016-01-20 00:50:17 -0800179 }
180
Jordan Haltermandbfff062017-06-19 10:31:32 -0700181 private Versioned<ClusterMetadata> blockForMetadata(String metadataUrl) {
Ray Milkey3717e602018-02-01 13:49:47 -0800182 long iterations = 0;
Jordan Haltermandbfff062017-06-19 10:31:32 -0700183 for (;;) {
Jordan Halterman9ada34a2017-10-23 16:31:50 -0700184 try {
185 Versioned<ClusterMetadata> metadata = fetchMetadata(metadataUrl);
186 if (metadata != null) {
187 return metadata;
188 }
189 } catch (Exception e) {
190 log.warn("Exception attempting to access metadata file at {}: {}", metadataUrl, e);
Jordan Haltermandbfff062017-06-19 10:31:32 -0700191 }
192
193 try {
Ray Milkey961b19f2018-02-05 13:47:55 -0800194 Thread.sleep((int) Math.pow(2, iterations < 7 ? ++iterations : iterations) * 10L);
Jordan Haltermandbfff062017-06-19 10:31:32 -0700195 } catch (InterruptedException e) {
Ray Milkey5c7d4882018-02-05 14:50:39 -0800196 Thread.currentThread().interrupt();
Ray Milkey6a51cb92018-03-06 09:03:03 -0800197 throw new IllegalStateException(e);
Jordan Haltermandbfff062017-06-19 10:31:32 -0700198 }
199 }
200 }
201
Madan Jampanif172d402016-03-04 00:56:38 -0800202 private Versioned<ClusterMetadata> fetchMetadata(String metadataUrl) {
Madan Jampaniad3c5262016-01-20 00:50:17 -0800203 try {
Madan Jampanif172d402016-03-04 00:56:38 -0800204 URL url = new URL(metadataUrl);
205 ClusterMetadata metadata = null;
206 long version = 0;
Jon Halla3fcf672017-03-28 16:53:22 -0700207 if ("file".equals(url.getProtocol())) {
Madan Jampanif172d402016-03-04 00:56:38 -0800208 File file = new File(metadataUrl.replaceFirst("file://", ""));
209 version = file.lastModified();
210 metadata = mapper.readValue(new FileInputStream(file), ClusterMetadata.class);
Jon Halla3fcf672017-03-28 16:53:22 -0700211 } else if ("http".equals(url.getProtocol())) {
Jordan Haltermanda268372017-09-19 11:18:08 -0700212 try {
213 HttpURLConnection conn = (HttpURLConnection) url.openConnection();
214 if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
215 log.warn("Could not reach metadata URL {}. Retrying...", url);
216 return null;
217 }
218 if (conn.getResponseCode() == HttpURLConnection.HTTP_NO_CONTENT) {
219 return null;
220 }
221 version = conn.getLastModified();
222 metadata = mapper.readValue(conn.getInputStream(), ClusterMetadata.class);
223 } catch (IOException e) {
Jordan Halterman46c76902017-08-24 14:55:25 -0700224 log.warn("Could not reach metadata URL {}. Retrying...", url);
225 return null;
226 }
Madan Jampanif172d402016-03-04 00:56:38 -0800227 }
Jordan Haltermandbfff062017-06-19 10:31:32 -0700228
zhiyong ke60e40002017-04-13 13:50:47 +0800229 if (null == metadata) {
230 log.warn("Metadata is null in the function fetchMetadata");
231 throw new NullPointerException();
232 }
Jordan Haltermandbfff062017-06-19 10:31:32 -0700233
234 // If the configured partitions are empty then return a null metadata to indicate that the configuration
235 // needs to be polled until the partitions are populated.
236 if (metadata.getPartitions().isEmpty() || metadata.getPartitions().stream()
237 .map(partition -> partition.getMembers().size())
238 .reduce(Math::min)
239 .orElse(0) == 0) {
240 return null;
241 }
Madan Jampanif172d402016-03-04 00:56:38 -0800242 return new Versioned<>(new ClusterMetadata(PROVIDER_ID,
243 metadata.getName(),
244 Sets.newHashSet(metadata.getNodes()),
245 Sets.newHashSet(metadata.getPartitions())),
246 version);
Madan Jampaniad3c5262016-01-20 00:50:17 -0800247 } catch (IOException e) {
Ray Milkey6a51cb92018-03-06 09:03:03 -0800248 throw new IllegalArgumentException(e);
Madan Jampaniad3c5262016-01-20 00:50:17 -0800249 }
Madan Jampaniad3c5262016-01-20 00:50:17 -0800250 }
251
Jordan Halterman21a74da2017-11-13 21:07:08 -0800252 private static class PartitionSerializer extends JsonSerializer<Partition> {
253 @Override
254 public void serialize(Partition partition, JsonGenerator jgen, SerializerProvider serializerProvider)
255 throws IOException, JsonProcessingException {
256 jgen.writeStartObject();
257 jgen.writeNumberField("id", partition.getId().asInt());
258 jgen.writeArrayFieldStart("members");
259 for (NodeId nodeId : partition.getMembers()) {
260 jgen.writeString(nodeId.id());
261 }
262 jgen.writeEndArray();
263 jgen.writeEndObject();
264 }
265 }
266
Madan Jampaniad3c5262016-01-20 00:50:17 -0800267 private static class PartitionDeserializer extends JsonDeserializer<Partition> {
268 @Override
269 public Partition deserialize(JsonParser jp, DeserializationContext ctxt)
270 throws IOException, JsonProcessingException {
271 return jp.readValueAs(DefaultPartition.class);
272 }
273 }
274
275 private static class PartitionIdSerializer extends JsonSerializer<PartitionId> {
276 @Override
277 public void serialize(PartitionId partitionId, JsonGenerator jgen, SerializerProvider provider)
278 throws IOException, JsonProcessingException {
279 jgen.writeNumber(partitionId.asInt());
280 }
281 }
282
283 private class PartitionIdDeserializer extends JsonDeserializer<PartitionId> {
284 @Override
285 public PartitionId deserialize(JsonParser jp, DeserializationContext ctxt)
286 throws IOException, JsonProcessingException {
287 JsonNode node = jp.getCodec().readTree(jp);
288 return new PartitionId(node.asInt());
289 }
290 }
291
292 private static class ControllerNodeSerializer extends JsonSerializer<ControllerNode> {
293 @Override
294 public void serialize(ControllerNode node, JsonGenerator jgen, SerializerProvider provider)
295 throws IOException, JsonProcessingException {
296 jgen.writeStartObject();
297 jgen.writeStringField(ID, node.id().toString());
298 jgen.writeStringField(IP, node.ip().toString());
299 jgen.writeNumberField(PORT, node.tcpPort());
300 jgen.writeEndObject();
301 }
302 }
303
304 private static class ControllerNodeDeserializer extends JsonDeserializer<ControllerNode> {
305 @Override
306 public ControllerNode deserialize(JsonParser jp, DeserializationContext ctxt)
307 throws IOException, JsonProcessingException {
308 JsonNode node = jp.getCodec().readTree(jp);
309 NodeId nodeId = new NodeId(node.get(ID).textValue());
310 IpAddress ip = IpAddress.valueOf(node.get(IP).textValue());
311 int port = node.get(PORT).asInt();
312 return new DefaultControllerNode(nodeId, ip, port);
313 }
314 }
315
316 private static class NodeIdSerializer extends JsonSerializer<NodeId> {
317 @Override
318 public void serialize(NodeId nodeId, JsonGenerator jgen, SerializerProvider provider)
319 throws IOException, JsonProcessingException {
320 jgen.writeString(nodeId.toString());
321 }
322 }
323
324 private class NodeIdDeserializer extends JsonDeserializer<NodeId> {
325 @Override
326 public NodeId deserialize(JsonParser jp, DeserializationContext ctxt)
327 throws IOException, JsonProcessingException {
328 JsonNode node = jp.getCodec().readTree(jp);
329 return new NodeId(node.asText());
330 }
331 }
Madan Jampani898fcca2016-02-26 12:42:08 -0800332
333 /**
Madan Jampanif172d402016-03-04 00:56:38 -0800334 * Monitors the metadata url for any updates and notifies providerService accordingly.
Madan Jampani898fcca2016-02-26 12:42:08 -0800335 */
Madan Jampanif172d402016-03-04 00:56:38 -0800336 private void watchUrl(String metadataUrl) {
Ray Milkeye3708c72017-11-13 13:10:46 -0800337 if (!isAvailable()) {
338 return;
339 }
Madan Jampanif172d402016-03-04 00:56:38 -0800340 // TODO: We are merely polling the url.
341 // 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 +0530342 try {
343 Versioned<ClusterMetadata> latestMetadata = fetchMetadata(metadataUrl);
344 if (cachedMetadata.get() != null && latestMetadata != null
345 && cachedMetadata.get().version() < latestMetadata.version()) {
346 cachedMetadata.set(latestMetadata);
347 providerService.clusterMetadataChanged(latestMetadata);
348 }
349 } catch (Exception e) {
350 log.error("Unable to parse metadata : ", e);
Madan Jampani898fcca2016-02-26 12:42:08 -0800351 }
352 }
Madan Jampaniad3c5262016-01-20 00:50:17 -0800353}